初始化 Panda RPKI v0.1.0 开源候选版本
Some checks failed
ci / rust (push) Has been cancelled
ci / docker (push) Has been cancelled

This commit is contained in:
Panda RPKI OSS Local
2026-09-09 18:01:15 +08:00
commit 277cbca878
203 changed files with 92495 additions and 0 deletions

412
src/model/aspa.rs Normal file
View File

@@ -0,0 +1,412 @@
use crate::model::common::{DerReader, der_take_tlv};
use crate::model::oid::OID_CT_ASPA;
use crate::model::rc::ResourceCertificate;
use crate::model::signed_object::{
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, 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),
}
impl From<SignedObjectDecodeError> for AspaProfileError {
fn from(value: SignedObjectDecodeError) -> Self {
match value {
SignedObjectDecodeError::Parse(e) => AspaProfileError::ProfileDecode(e.to_string()),
SignedObjectDecodeError::Validate(e) => AspaProfileError::SignedObject(e),
}
}
}
#[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 decode_der_with_strict_options(
der: &[u8],
strict_cms_der: bool,
strict_name: bool,
) -> Result<Self, AspaDecodeError> {
let signed_object =
RpkiSignedObject::decode_der_with_strict_options(der, strict_cms_der, strict_name)
.map_err(AspaProfileError::from)?;
Self::from_signed_object(signed_object)
}
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(AspaProfileError::ProfileDecode)?;
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::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
&& asn <= p
{
return Err(AspaProfileError::ProvidersNotStrictlyIncreasing);
}
prev = Some(asn);
out.push(asn);
}
Ok(out)
}

289
src/model/common.rs Normal file
View File

@@ -0,0 +1,289 @@
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"];

537
src/model/crl.rs Normal file
View File

@@ -0,0 +1,537 @@
pub use crate::model::common::{Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned};
use crate::model::oid::{
OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTHORITY_KEY_IDENTIFIER_RAW, OID_CRL_NUMBER,
OID_CRL_NUMBER_RAW, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW,
OID_SUBJECT_KEY_IDENTIFIER_RAW,
};
use x509_parser::certificate::X509Certificate;
use x509_parser::extensions::{ParsedExtension, X509Extension};
use x509_parser::prelude::{FromDer, X509Version};
use x509_parser::revocation_list::CertificateRevocationList;
use x509_parser::x509::{AlgorithmIdentifier, SubjectPublicKeyInfo};
use x509_parser::{asn1_rs::Class as Asn1Class, asn1_rs::Tag as Asn1Tag};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RevokedCert {
pub serial_number: BigUnsigned,
pub revocation_date: Asn1TimeUtc,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CrlExtensions {
pub authority_key_identifier: Vec<u8>,
pub crl_number: BigUnsigned,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RpkixCrl {
pub raw_der: Vec<u8>,
pub version: u32,
pub issuer_dn: String,
pub signature_algorithm_oid: String,
pub this_update: Asn1TimeUtc,
pub next_update: Asn1TimeUtc,
pub revoked_certs: Vec<RevokedCert>,
pub extensions: CrlExtensions,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RpkixCrlParsed {
pub raw_der: Vec<u8>,
pub version: Option<X509Version>,
pub issuer_dn: String,
pub signature_algorithm: AlgorithmIdentifierValue,
pub tbs_signature_algorithm: AlgorithmIdentifierValue,
pub this_update: Asn1TimeUtc,
pub next_update: Option<Asn1TimeUtc>,
pub revoked_certs: Vec<RevokedCertParsed>,
pub extensions: Vec<CrlExtensionParsed>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RevokedCertParsed {
pub serial_number: BigUnsigned,
pub revocation_date: Asn1TimeUtc,
pub has_extensions: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CrlExtensionParsed {
AuthorityKeyIdentifier {
key_identifier: Option<Vec<u8>>,
has_other_fields: bool,
critical: bool,
},
CrlNumber {
number: der_parser::num_bigint::BigUint,
critical: bool,
},
Other {
oid: String,
critical: bool,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlgorithmIdentifierValue {
pub oid: String,
pub parameters: Option<AlgorithmParametersValue>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlgorithmParametersValue {
pub class: Asn1Class,
pub tag: Asn1Tag,
pub data: Vec<u8>,
}
impl AlgorithmIdentifierValue {
pub fn params_absent_or_null(&self) -> bool {
match &self.parameters {
None => true,
Some(p) if p.class == Asn1Class::Universal && p.tag == Asn1Tag::Null => true,
Some(_p) => false,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CrlParseError {
#[error("X.509 CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
Parse(String),
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
TrailingBytes(usize),
}
#[derive(Debug, thiserror::Error)]
pub enum CrlProfileError {
#[error("CRL version must be v2, got {0:?} (RFC 5280 §5.1; RFC 6487 §5)")]
InvalidVersion(Option<u32>),
#[error("CRL signatureAlgorithm must match TBSCertList.signature (RFC 5280 §5.1)")]
SignatureAlgorithmMismatch,
#[error(
"CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)"
)]
InvalidSignatureAlgorithm(String),
#[error(
"CRL signature algorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)"
)]
InvalidSignatureAlgorithmParameters,
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0} (RFC 9829 §3.1)")]
InvalidExtensionsCount(usize),
#[error("unsupported CRL extension OID {0} (RFC 9829 §3.1)")]
UnsupportedExtension(String),
#[error("duplicate CRL extension OID {0} (RFC 5280 §4.2; RFC 9829 §3.1)")]
DuplicateExtension(String),
#[error("AuthorityKeyIdentifier CRL extension missing (RFC 9829 §3.1; RFC 5280 §5.2.1)")]
AkiMissing,
#[error("AuthorityKeyIdentifier must contain keyIdentifier (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
AkiMissingKeyIdentifier,
#[error(
"AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)"
)]
AkiHasOtherFields,
#[error("CRLNumber CRL extension missing (RFC 9829 §3.1; RFC 5280 §5.2.3)")]
CrlNumberMissing,
#[error("CRLNumber must be non-critical (RFC 9829 §3.1; RFC 5280 §5.2.3)")]
CrlNumberCritical,
#[error("CRLNumber out of range (must fit in 0..2^159-1) (RFC 9829 §3.1)")]
CrlNumberOutOfRange,
#[error("CRL entry extensions must not be present (RFC 6487 §5; RFC 5280 §5.1)")]
EntryExtensionsNotAllowed,
#[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5; RFC 6487 §5)")]
NextUpdateMissing,
#[error(
"{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §5.1.2.4-§5.1.2.6)"
)]
InvalidTimeEncoding {
field: &'static str,
year: i32,
encoding: Asn1TimeEncoding,
},
}
#[derive(Debug, thiserror::Error)]
pub enum CrlDecodeError {
#[error("{0}")]
Parse(#[from] CrlParseError),
#[error("{0}")]
Validate(#[from] CrlProfileError),
}
impl RpkixCrl {
/// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(der: &[u8]) -> Result<RpkixCrlParsed, CrlParseError> {
let (rem, crl) = CertificateRevocationList::from_der(der)
.map_err(|e| CrlParseError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(CrlParseError::TrailingBytes(rem.len()));
}
let revoked_certs = crl
.iter_revoked_certificates()
.map(|rc| RevokedCertParsed {
serial_number: BigUnsigned::from_biguint(rc.serial()),
revocation_date: crate::model::common::asn1_time_to_model(rc.revocation_date),
has_extensions: !rc.extensions().is_empty(),
})
.collect::<Vec<_>>();
let this_update = crate::model::common::asn1_time_to_model(crl.last_update());
let next_update = crl
.next_update()
.map(crate::model::common::asn1_time_to_model);
let extensions = parse_extensions_parse(crl.extensions()).map_err(CrlParseError::Parse)?;
Ok(RpkixCrlParsed {
raw_der: der.to_vec(),
version: crl.version(),
issuer_dn: crl.issuer().to_string(),
signature_algorithm: algorithm_identifier_value(&crl.signature_algorithm),
tbs_signature_algorithm: algorithm_identifier_value(&crl.tbs_cert_list.signature),
this_update,
next_update,
revoked_certs,
extensions,
})
}
/// Decode a DER-encoded X.509 v2 CRL and enforce the RPKI profile constraints from
/// `specs/prepare/data_models/04_crl.md` (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1).
pub fn decode_der(der: &[u8]) -> Result<Self, CrlDecodeError> {
Ok(Self::parse_der(der)?.validate_profile()?)
}
/// Profile validate step of scheme A (`parse → validate → verify`).
///
/// `RpkixCrl` is already profile-validated when constructed via `decode_der()` /
/// `RpkixCrlParsed::validate_profile()`.
pub fn validate_profile(&self) -> Result<(), CrlProfileError> {
Ok(())
}
/// Verify the cryptographic signature on this CRL using the issuer certificate.
///
/// Signature verification needs the issuer public key (RFC 5280 §6.3.3 (f)-(g)).
/// In RPKI practice, this public key is obtained from the CRL issuer CA certificate
/// (and that certificate must already be validated up to the same trust anchor).
///
/// This helper also performs common binding checks:
/// - CRL `issuer_dn` must equal issuer certificate `subject`
/// - if issuer KeyUsage is present, require `cRLSign`
/// - if issuer SKI is present, require it matches CRL AKI.keyIdentifier
pub fn verify_signature_with_issuer_certificate_der(
&self,
issuer_cert_der: &[u8],
) -> Result<(), CrlVerifyError> {
let (rem, issuer_cert) = X509Certificate::from_der(issuer_cert_der)
.map_err(|e| CrlVerifyError::IssuerCertificateParse(e.to_string()))?;
if !rem.is_empty() {
return Err(CrlVerifyError::IssuerCertificateTrailingBytes(rem.len()));
}
let subject_dn = issuer_cert.subject().to_string();
if subject_dn != self.issuer_dn {
return Err(CrlVerifyError::IssuerSubjectMismatch {
crl_issuer_dn: self.issuer_dn.clone(),
issuer_subject_dn: subject_dn,
});
}
if let Some(ku) = issuer_cert
.key_usage()
.map_err(|e| CrlVerifyError::IssuerCertificateParse(e.to_string()))?
&& !ku.value.crl_sign()
{
return Err(CrlVerifyError::IssuerKeyUsageMissingCrlSign);
}
if let Some(issuer_ski) = get_subject_key_identifier(&issuer_cert)
&& issuer_ski != self.extensions.authority_key_identifier
{
return Err(CrlVerifyError::AkiSkiMismatch);
}
self.verify_signature_with_issuer_spki(issuer_cert.public_key())
}
/// Verify the cryptographic signature on this CRL using the issuer SubjectPublicKeyInfo.
pub fn verify_signature_with_issuer_spki(
&self,
issuer_spki: &SubjectPublicKeyInfo<'_>,
) -> Result<(), CrlVerifyError> {
let (rem, crl) = CertificateRevocationList::from_der(&self.raw_der)
.map_err(|e| CrlVerifyError::CrlParse(e.to_string()))?;
if !rem.is_empty() {
return Err(CrlVerifyError::CrlTrailingBytes(rem.len()));
}
crl.verify_signature(issuer_spki)
.map_err(|e| CrlVerifyError::InvalidSignature(e.to_string()))
}
/// Verify the cryptographic signature on this CRL using a DER-encoded SubjectPublicKeyInfo.
pub fn verify_signature_with_issuer_spki_der(
&self,
issuer_spki_der: &[u8],
) -> Result<(), CrlVerifyError> {
let (rem, spki) = SubjectPublicKeyInfo::from_der(issuer_spki_der)
.map_err(|e| CrlVerifyError::IssuerSpkiParse(e.to_string()))?;
if !rem.is_empty() {
return Err(CrlVerifyError::IssuerSpkiTrailingBytes(rem.len()));
}
self.verify_signature_with_issuer_spki(&spki)
}
}
impl RpkixCrlParsed {
/// Profile validate step of scheme A (`parse → validate → verify`).
pub fn validate_profile(self) -> Result<RpkixCrl, CrlProfileError> {
let version = match self.version {
Some(X509Version::V2) => 2,
Some(v) => return Err(CrlProfileError::InvalidVersion(Some(v.0))),
None => return Err(CrlProfileError::InvalidVersion(None)),
};
// signatureAlgorithm must match tbsCertList.signature
if self.signature_algorithm != self.tbs_signature_algorithm {
return Err(CrlProfileError::SignatureAlgorithmMismatch);
}
let sig_oid = self.signature_algorithm.oid.clone();
if sig_oid != OID_SHA256_WITH_RSA_ENCRYPTION {
return Err(CrlProfileError::InvalidSignatureAlgorithm(sig_oid));
}
if !self.signature_algorithm.params_absent_or_null() {
return Err(CrlProfileError::InvalidSignatureAlgorithmParameters);
}
let extensions = validate_extensions_profile(&self.extensions)?;
let mut revoked_out = Vec::with_capacity(self.revoked_certs.len());
for rc in self.revoked_certs {
if rc.has_extensions {
return Err(CrlProfileError::EntryExtensionsNotAllowed);
}
validate_time_encoding_rfc5280("revocationDate", &rc.revocation_date)?;
revoked_out.push(RevokedCert {
serial_number: rc.serial_number,
revocation_date: rc.revocation_date,
});
}
validate_time_encoding_rfc5280("thisUpdate", &self.this_update)?;
let next_update = self.next_update.ok_or(CrlProfileError::NextUpdateMissing)?;
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
Ok(RpkixCrl {
raw_der: self.raw_der,
version,
issuer_dn: self.issuer_dn,
signature_algorithm_oid: OID_SHA256_WITH_RSA_ENCRYPTION.to_string(),
this_update: self.this_update,
next_update,
revoked_certs: revoked_out,
extensions,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum CrlVerifyError {
#[error("issuer certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
IssuerCertificateParse(String),
#[error("trailing bytes after issuer certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
IssuerCertificateTrailingBytes(usize),
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
IssuerSpkiParse(String),
#[error(
"trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)"
)]
IssuerSpkiTrailingBytes(usize),
#[error("CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
CrlParse(String),
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
CrlTrailingBytes(usize),
#[error(
"CRL issuer DN does not match issuer certificate subject (RFC 5280 §5.1; RFC 5280 §6.3.3(b))"
)]
IssuerSubjectMismatch {
crl_issuer_dn: String,
issuer_subject_dn: String,
},
#[error(
"issuer certificate keyUsage present but missing cRLSign (RFC 5280 §4.2.1.3; RFC 5280 §6.3.3(f))"
)]
IssuerKeyUsageMissingCrlSign,
#[error(
"CRL AKI.keyIdentifier does not match issuer certificate SKI (RFC 5280 §4.2.1.1; RFC 5280 §4.2.1.2; RFC 5280 §6.3.3(c)/(f))"
)]
AkiSkiMismatch,
#[error("CRL signature verification failed: {0} (RFC 5280 §6.3.3(g); RFC 7935 §2)")]
InvalidSignature(String),
}
fn validate_time_encoding_rfc5280(
field: &'static str,
t: &Asn1TimeUtc,
) -> Result<(), CrlProfileError> {
let year = t.utc.year();
let expected = if year <= 2049 {
Asn1TimeEncoding::UtcTime
} else {
Asn1TimeEncoding::GeneralizedTime
};
if t.encoding != expected {
return Err(CrlProfileError::InvalidTimeEncoding {
field,
year,
encoding: t.encoding,
});
}
Ok(())
}
fn algorithm_identifier_value(ai: &AlgorithmIdentifier<'_>) -> AlgorithmIdentifierValue {
let parameters = ai.parameters.as_ref().map(|p| AlgorithmParametersValue {
class: p.class(),
tag: p.tag(),
data: p.as_bytes().to_vec(),
});
// NOTE(perf): Avoid `to_id_string()` allocations for the signature algorithms we expect
// in RPKI CRLs. Fall back to `to_id_string()` for unexpected algorithms (mostly error paths).
let oid = if ai.algorithm.as_bytes() == OID_SHA256_WITH_RSA_ENCRYPTION_RAW {
OID_SHA256_WITH_RSA_ENCRYPTION.to_string()
} else {
ai.algorithm.to_id_string()
};
AlgorithmIdentifierValue { oid, parameters }
}
fn parse_extensions_parse(exts: &[X509Extension<'_>]) -> Result<Vec<CrlExtensionParsed>, String> {
let mut out = Vec::with_capacity(exts.len());
for ext in exts {
let oid = ext.oid.as_bytes();
if oid == OID_AUTHORITY_KEY_IDENTIFIER_RAW {
let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else {
return Err("AKI extension parse failed".to_string());
};
out.push(CrlExtensionParsed::AuthorityKeyIdentifier {
key_identifier: aki.key_identifier.as_ref().map(|k| k.0.to_vec()),
has_other_fields: aki.authority_cert_issuer.is_some()
|| aki.authority_cert_serial.is_some(),
critical: ext.critical,
});
} else if oid == OID_CRL_NUMBER_RAW {
match ext.parsed_extension() {
ParsedExtension::CRLNumber(n) => out.push(CrlExtensionParsed::CrlNumber {
number: n.clone(),
critical: ext.critical,
}),
_ => return Err("CRLNumber extension parse failed".to_string()),
}
} else {
out.push(CrlExtensionParsed::Other {
oid: ext.oid.to_id_string(),
critical: ext.critical,
})
}
}
Ok(out)
}
fn validate_extensions_profile(
exts: &[CrlExtensionParsed],
) -> Result<CrlExtensions, CrlProfileError> {
if exts.len() != 2 {
return Err(CrlProfileError::InvalidExtensionsCount(exts.len()));
}
let mut seen: Vec<String> = Vec::new();
let mut authority_key_identifier: Option<Vec<u8>> = None;
let mut crl_number: Option<BigUnsigned> = None;
for ext in exts {
match ext {
CrlExtensionParsed::AuthorityKeyIdentifier {
key_identifier,
has_other_fields,
critical: _,
} => {
let oid = OID_AUTHORITY_KEY_IDENTIFIER.to_string();
if seen.iter().any(|s| s == &oid) {
return Err(CrlProfileError::DuplicateExtension(oid));
}
seen.push(oid.clone());
if *has_other_fields {
return Err(CrlProfileError::AkiHasOtherFields);
}
let ki = key_identifier
.as_ref()
.ok_or(CrlProfileError::AkiMissingKeyIdentifier)?;
authority_key_identifier = Some(ki.clone());
}
CrlExtensionParsed::CrlNumber { number, critical } => {
let oid = OID_CRL_NUMBER.to_string();
if seen.iter().any(|s| s == &oid) {
return Err(CrlProfileError::DuplicateExtension(oid));
}
seen.push(oid.clone());
if *critical {
return Err(CrlProfileError::CrlNumberCritical);
}
if number.bits() > 159 {
return Err(CrlProfileError::CrlNumberOutOfRange);
}
crl_number = Some(BigUnsigned::from_biguint(number));
}
CrlExtensionParsed::Other { oid, .. } => {
return Err(CrlProfileError::UnsupportedExtension(oid.clone()));
}
}
}
Ok(CrlExtensions {
authority_key_identifier: authority_key_identifier.ok_or(CrlProfileError::AkiMissing)?,
crl_number: crl_number.ok_or(CrlProfileError::CrlNumberMissing)?,
})
}
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
cert.extensions()
.iter()
.find(|ext| ext.oid.as_bytes() == OID_SUBJECT_KEY_IDENTIFIER_RAW)
.and_then(|ext| match ext.parsed_extension() {
ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()),
_ => None,
})
}

976
src/model/manifest.rs Normal file
View File

@@ -0,0 +1,976 @@
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<ManifestEContentParsed>,
}
#[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<u8>,
/// Count of FileAndHash entries in `fileList`.
pub file_count: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ManifestEContentParsed {
der: Vec<u8>,
}
#[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<SignedObjectDecodeError> 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<ManifestObjectParsed, ManifestParseError> {
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<Self, ManifestDecodeError> {
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn decode_der_with_strict_options(
der: &[u8],
strict_cms_der: bool,
strict_name: bool,
) -> Result<Self, ManifestDecodeError> {
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<Self, ManifestDecodeError> {
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<ManifestEContentParsed, ManifestParseError> {
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<Self, ManifestDecodeError> {
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<Vec<FileAndHash>, 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<ManifestObject, ManifestProfileError> {
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<ManifestEContent, ManifestProfileError> {
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<u8> {
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<ManifestEContent, ManifestProfileError> {
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<usize, ManifestProfileError> {
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<Vec<FileAndHash>, 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<FileAndHash> = 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<String, ManifestProfileError> {
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<usize, String> {
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<u64, String> {
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<BigUnsigned, ManifestProfileError> {
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<UtcTime, String> {
// 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<u64> = oid_content_iter(bytes).collect();
if arcs.is_empty() {
return "<invalid oid>".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<Item = u64> + '_ {
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<u64> {
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<u8> {
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<u8> {
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(&[]), "<invalid oid>".to_string());
}
#[test]
fn validate_file_list_sha256_fast_counts_and_rejects_bad_hash() {
fn file_and_hash(file: &str, digest: u8) -> Vec<u8> {
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(_))
));
}
}

13
src/model/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
pub mod aspa;
pub mod common;
pub mod crl;
pub mod manifest;
pub mod oid;
pub mod rc;
pub mod roa;
pub mod signed_object;
pub mod ta;
pub mod tal;
pub mod projection;
pub mod router_cert;

78
src/model/oid.rs Normal file
View File

@@ -0,0 +1,78 @@
pub const OID_SHA256: &str = "2.16.840.1.101.3.4.2.1";
pub const OID_SHA256_RAW: &[u8] = &asn1_rs::oid!(raw 2.16.840.1.101.3.4.2.1);
pub const OID_SIGNED_DATA: &str = "1.2.840.113549.1.7.2";
pub const OID_SIGNED_DATA_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.7.2);
pub const OID_CMS_ATTR_CONTENT_TYPE: &str = "1.2.840.113549.1.9.3";
pub const OID_CMS_ATTR_CONTENT_TYPE_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.3);
pub const OID_CMS_ATTR_MESSAGE_DIGEST: &str = "1.2.840.113549.1.9.4";
pub const OID_CMS_ATTR_MESSAGE_DIGEST_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.4);
pub const OID_CMS_ATTR_SIGNING_TIME: &str = "1.2.840.113549.1.9.5";
pub const OID_CMS_ATTR_SIGNING_TIME_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.5);
pub const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
pub const OID_RSA_ENCRYPTION_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.1.1);
pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
pub const OID_SHA256_WITH_RSA_ENCRYPTION_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.1.11);
// X.509 extensions (RFC 5280 / RFC 6487)
pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19";
pub const OID_BASIC_CONSTRAINTS_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.19);
pub const OID_KEY_USAGE: &str = "2.5.29.15";
pub const OID_KEY_USAGE_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.15);
pub const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37";
pub const OID_EXTENDED_KEY_USAGE_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.37);
pub const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31";
pub const OID_CRL_DISTRIBUTION_POINTS_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.31);
pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1";
pub const OID_AUTHORITY_INFO_ACCESS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.1);
pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32";
pub const OID_CERTIFICATE_POLICIES_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.32);
pub const OID_QT_CPS: &str = "1.3.6.1.5.5.7.2.1";
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
pub const OID_AUTHORITY_KEY_IDENTIFIER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.35);
pub const OID_CRL_NUMBER: &str = "2.5.29.20";
pub const OID_CRL_NUMBER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.20);
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
pub const OID_SUBJECT_KEY_IDENTIFIER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.14);
pub const OID_CT_RPKI_MANIFEST: &str = "1.2.840.113549.1.9.16.1.26";
pub const OID_CT_RPKI_MANIFEST_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.16.1.26);
pub const OID_CT_ROUTE_ORIGIN_AUTHZ: &str = "1.2.840.113549.1.9.16.1.24";
pub const OID_CT_ROUTE_ORIGIN_AUTHZ_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.16.1.24);
pub const OID_CT_ASPA: &str = "1.2.840.113549.1.9.16.1.49";
pub const OID_CT_ASPA_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.16.1.49);
// X.509 extensions / access methods (RFC 5280 / RFC 6487)
pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11";
pub const OID_SUBJECT_INFO_ACCESS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.11);
pub const OID_AD_SIGNED_OBJECT: &str = "1.3.6.1.5.5.7.48.11";
pub const OID_AD_SIGNED_OBJECT_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.11);
pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2";
pub const OID_AD_CA_ISSUERS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.2);
pub const OID_AD_CA_REPOSITORY: &str = "1.3.6.1.5.5.7.48.5";
pub const OID_AD_CA_REPOSITORY_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.5);
pub const OID_AD_RPKI_MANIFEST: &str = "1.3.6.1.5.5.7.48.10";
pub const OID_AD_RPKI_MANIFEST_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.10);
pub const OID_AD_RPKI_NOTIFY: &str = "1.3.6.1.5.5.7.48.13";
pub const OID_AD_RPKI_NOTIFY_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.48.13);
// RFC 3779 resource extensions (RFC 6487 profile)
pub const OID_IP_ADDR_BLOCKS: &str = "1.3.6.1.5.5.7.1.7";
pub const OID_IP_ADDR_BLOCKS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.7);
pub const OID_AUTONOMOUS_SYS_IDS: &str = "1.3.6.1.5.5.7.1.8";
pub const OID_AUTONOMOUS_SYS_IDS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.8);
// RPKI CP (RFC 6484 / RFC 6487)
pub const OID_CP_IPADDR_ASNUMBER: &str = "1.3.6.1.5.5.7.14.2";
pub const OID_CP_IPADDR_ASNUMBER_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.14.2);
pub const OID_CT_RPKI_CCR: &str = "1.2.840.113549.1.9.16.1.54";
pub const OID_CT_RPKI_CCR_RAW: &[u8] = &asn1_rs::oid!(raw 1.2.840.113549.1.9.16.1.54);
pub const OID_KP_BGPSEC_ROUTER: &str = "1.3.6.1.5.5.7.3.30";
pub const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
pub const OID_SECP256R1: &str = "1.2.840.10045.3.1.7";

663
src/model/projection.rs Normal file
View File

@@ -0,0 +1,663 @@
use std::net::{Ipv4Addr, Ipv6Addr};
use std::path::Path;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use crate::model::aspa::AspaObject;
use crate::model::crl::RpkixCrl;
use crate::model::manifest::ManifestObject;
use crate::model::rc::{AccessDescription, RcExtensions, ResourceCertificate, SubjectInfoAccess};
use crate::model::roa::{IpPrefix as RoaIpPrefix, RoaAfi, RoaObject};
use crate::model::signed_object::{
ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, SignerInfoProfiled,
};
use crate::model::ta::TaCertificate;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ObjectType {
Auto,
Cer,
Mft,
Crl,
Roa,
Aspa,
}
impl ObjectType {
pub fn parse(value: &str) -> Result<Self, String> {
match value.to_ascii_lowercase().as_str() {
"auto" => Ok(Self::Auto),
"cer" | ".cer" | "cert" | "certificate" => Ok(Self::Cer),
"mft" | ".mft" | "manifest" => Ok(Self::Mft),
"crl" | ".crl" => Ok(Self::Crl),
"roa" | ".roa" => Ok(Self::Roa),
"asa" | ".asa" | "aspa" => Ok(Self::Aspa),
_ => Err(format!("unsupported object type: {value}")),
}
}
pub fn label(self) -> &'static str {
object_type_label(self)
}
}
pub fn resolve_object_type(object_type: ObjectType, path: &Path) -> Result<ObjectType, String> {
if object_type != ObjectType::Auto {
return Ok(object_type);
}
match path
.extension()
.and_then(|v| v.to_str())
.map(|v| v.to_ascii_lowercase())
.as_deref()
{
Some("cer") => Ok(ObjectType::Cer),
Some("mft") => Ok(ObjectType::Mft),
Some("crl") => Ok(ObjectType::Crl),
Some("roa") => Ok(ObjectType::Roa),
Some("asa") | Some("aspa") => Ok(ObjectType::Aspa),
_ => Err(format!(
"cannot infer object type from path: {}",
path.display()
)),
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ObjectProjectionRecord {
pub schema_version: u32,
pub sha256: String,
pub object_type: String,
pub parse_status: String,
pub error_summary: Option<String>,
pub projection: Value,
}
pub fn build_object_projection(
object_type: ObjectType,
input_path: &Path,
bytes: &[u8],
entry_limit: usize,
) -> ObjectProjectionRecord {
let resolved = match resolve_object_type(object_type, input_path) {
Ok(value) => value,
Err(err) => {
return ObjectProjectionRecord {
schema_version: 1,
sha256: sha256_hex(bytes),
object_type: "unknown".to_string(),
parse_status: "error".to_string(),
error_summary: Some(err),
projection: json!({"decode": {"profileValid": false}}),
};
}
};
let projection = parse_object_json(resolved, input_path, bytes, entry_limit);
let parse_status = if projection
.get("object")
.and_then(|v| v.get("decode"))
.and_then(|v| v.get("profileValid"))
.and_then(Value::as_bool)
.unwrap_or(false)
{
"ok"
} else {
"error"
};
let error_summary = projection
.get("object")
.and_then(|v| v.get("decode"))
.and_then(|v| v.get("error"))
.and_then(Value::as_str)
.map(str::to_string);
ObjectProjectionRecord {
schema_version: 1,
sha256: sha256_hex(bytes),
object_type: resolved.label().to_string(),
parse_status: parse_status.to_string(),
error_summary,
projection,
}
}
pub fn parse_object_json(
object_type: ObjectType,
input_path: &Path,
bytes: &[u8],
entry_limit: usize,
) -> Value {
let object = match object_type {
ObjectType::Auto => unreachable!("auto must be resolved"),
ObjectType::Cer => parse_cer_json(bytes),
ObjectType::Mft => parse_mft_json(bytes, entry_limit),
ObjectType::Crl => parse_crl_json(bytes, entry_limit),
ObjectType::Roa => parse_roa_json(bytes, entry_limit),
ObjectType::Aspa => parse_aspa_json(bytes, entry_limit),
};
json!({
"tool": "rpki_object_parse",
"schemaVersion": 1,
"input": {
"path": input_path.display().to_string(),
"type": object_type_label(object_type),
"bytes": bytes_summary(bytes),
},
"object": object,
})
}
pub fn parse_cer_json(bytes: &[u8]) -> Value {
match ResourceCertificate::decode_der(bytes) {
Ok(cert) => {
let ta_profile = match TaCertificate::decode_der(bytes) {
Ok(ta) => json!({
"valid": true,
"selfSignature": result_json(ta.verify_self_signature().map_err(|e| e.to_string())),
}),
Err(err) => json!({
"valid": false,
"error": err.to_string(),
}),
};
json!({
"type": "cer",
"decode": {"profileValid": true},
"resourceCertificate": resource_certificate_json(&cert),
"trustAnchorProfile": ta_profile,
})
}
Err(err) => json!({
"type": "cer",
"decode": {"profileValid": false, "error": err.to_string()},
}),
}
}
pub fn parse_mft_json(bytes: &[u8], entry_limit: usize) -> Value {
match ManifestObject::decode_der(bytes) {
Ok(mft) => {
let files = mft.manifest.parse_files();
let (file_sample, file_list_error) = match files {
Ok(entries) => (
json!({
"count": entries.len(),
"truncated": entries.len() > entry_limit,
"entries": entries.iter().take(entry_limit).map(|item| {
json!({"fileName": item.file_name, "hashHex": hex::encode(item.hash_bytes)})
}).collect::<Vec<_>>(),
}),
Value::Null,
),
Err(err) => (Value::Null, json!(err.to_string())),
};
json!({
"type": "mft",
"decode": {"profileValid": true},
"eContentType": mft.econtent_type,
"signedObject": signed_object_json(&mft.signed_object),
"manifest": {
"version": mft.manifest.version,
"manifestNumberHex": mft.manifest.manifest_number.to_hex_upper(),
"thisUpdate": format_time(mft.manifest.this_update),
"nextUpdate": format_time(mft.manifest.next_update),
"fileHashAlg": mft.manifest.file_hash_alg,
"fileCount": mft.manifest.file_count(),
"fileList": file_sample,
"fileListError": file_list_error,
},
"embeddedEeProfile": result_json(mft.validate_embedded_ee_cert().map_err(|e| e.to_string())),
"cmsSignature": result_json(mft.signed_object.verify_signature().map_err(|e| e.to_string())),
})
}
Err(err) => json!({
"type": "mft",
"decode": {"profileValid": false, "error": err.to_string()},
}),
}
}
pub fn parse_crl_json(bytes: &[u8], entry_limit: usize) -> Value {
match RpkixCrl::decode_der(bytes) {
Ok(crl) => json!({
"type": "crl",
"decode": {"profileValid": true},
"rawDer": bytes_summary(&crl.raw_der),
"version": crl.version,
"issuer": crl.issuer_dn,
"signatureAlgorithm": crl.signature_algorithm_oid,
"thisUpdate": format_time(crl.this_update.utc),
"nextUpdate": format_time(crl.next_update.utc),
"extensions": {
"authorityKeyIdentifier": hex::encode(&crl.extensions.authority_key_identifier),
"crlNumberHex": crl.extensions.crl_number.to_hex_upper(),
"crlNumber": crl.extensions.crl_number.to_u64(),
},
"revokedCertificates": {
"count": crl.revoked_certs.len(),
"truncated": crl.revoked_certs.len() > entry_limit,
"entries": crl.revoked_certs.iter().take(entry_limit).map(|item| {
json!({
"serialNumberHex": item.serial_number.to_hex_upper(),
"serialNumber": item.serial_number.to_u64(),
"revocationDate": format_time(item.revocation_date.utc),
})
}).collect::<Vec<_>>(),
},
}),
Err(err) => json!({
"type": "crl",
"decode": {"profileValid": false, "error": err.to_string()},
}),
}
}
pub fn manifest_file_entries_page(
bytes: &[u8],
offset: usize,
limit: usize,
) -> Result<(usize, Vec<Value>), String> {
let mft = ManifestObject::decode_der(bytes).map_err(|err| err.to_string())?;
let entries = mft.manifest.parse_files().map_err(|err| err.to_string())?;
let total = entries.len();
let end = (offset + limit).min(total);
let page = entries[offset.min(total)..end]
.iter()
.map(|item| json!({"fileName": item.file_name, "hashHex": hex::encode(item.hash_bytes)}))
.collect::<Vec<_>>();
Ok((total, page))
}
pub fn crl_revoked_entries_page(
bytes: &[u8],
offset: usize,
limit: usize,
) -> Result<(usize, Vec<Value>), String> {
let crl = RpkixCrl::decode_der(bytes).map_err(|err| err.to_string())?;
let total = crl.revoked_certs.len();
let end = (offset + limit).min(total);
let page = crl.revoked_certs[offset.min(total)..end]
.iter()
.map(|item| {
json!({
"serialNumberHex": item.serial_number.to_hex_upper(),
"serialNumber": item.serial_number.to_u64(),
"revocationDate": format_time(item.revocation_date.utc),
})
})
.collect::<Vec<_>>();
Ok((total, page))
}
pub fn parse_roa_json(bytes: &[u8], entry_limit: usize) -> Value {
match RoaObject::decode_der(bytes) {
Ok(roa) => json!({
"type": "roa",
"decode": {"profileValid": true},
"eContentType": roa.econtent_type,
"signedObject": signed_object_json(&roa.signed_object),
"roa": {
"version": roa.roa.version,
"asId": roa.roa.as_id,
"ipAddressFamilies": roa.roa.ip_addr_blocks.iter().map(|family| {
json!({
"afi": format!("{:?}", family.afi),
"addressCount": family.addresses.len(),
"truncated": family.addresses.len() > entry_limit,
"addresses": family.addresses.iter().take(entry_limit).map(|entry| {
json!({
"prefix": roa_prefix_string(&entry.prefix),
"maxLength": entry.max_length,
})
}).collect::<Vec<_>>(),
})
}).collect::<Vec<_>>(),
},
"embeddedEeProfile": result_json(roa.validate_embedded_ee_cert().map_err(|e| e.to_string())),
"cmsSignature": result_json(roa.signed_object.verify_signature().map_err(|e| e.to_string())),
}),
Err(err) => json!({
"type": "roa",
"decode": {"profileValid": false, "error": err.to_string()},
}),
}
}
pub fn parse_aspa_json(bytes: &[u8], entry_limit: usize) -> Value {
match AspaObject::decode_der(bytes) {
Ok(aspa) => json!({
"type": "aspa",
"decode": {"profileValid": true},
"eContentType": aspa.econtent_type,
"signedObject": signed_object_json(&aspa.signed_object),
"aspa": {
"version": aspa.aspa.version,
"customerAsId": aspa.aspa.customer_as_id,
"providerCount": aspa.aspa.provider_as_ids.len(),
"providersTruncated": aspa.aspa.provider_as_ids.len() > entry_limit,
"providerAsIds": aspa.aspa.provider_as_ids.iter().take(entry_limit).copied().collect::<Vec<_>>(),
},
"embeddedEeProfile": result_json(aspa.validate_embedded_ee_cert().map_err(|e| e.to_string())),
"cmsSignature": result_json(aspa.signed_object.verify_signature().map_err(|e| e.to_string())),
}),
Err(err) => json!({
"type": "aspa",
"decode": {"profileValid": false, "error": err.to_string()},
}),
}
}
fn resource_certificate_json(cert: &ResourceCertificate) -> Value {
let tbs = &cert.tbs;
json!({
"rawDer": bytes_summary(&cert.raw_der),
"kind": format!("{:?}", cert.kind),
"version": tbs.version,
"serialNumberHex": hex::encode(tbs.serial_number.to_bytes_be()),
"signatureAlgorithm": tbs.signature_algorithm,
"issuer": tbs.issuer_name.to_string(),
"subject": tbs.subject_name.to_string(),
"validity": {
"notBefore": format_time(tbs.validity_not_before),
"notAfter": format_time(tbs.validity_not_after),
},
"subjectPublicKeyInfo": bytes_summary(&tbs.subject_public_key_info),
"extensions": rc_extensions_json(&tbs.extensions),
})
}
fn rc_extensions_json(ext: &RcExtensions) -> Value {
json!({
"basicConstraintsCa": ext.basic_constraints_ca,
"subjectKeyIdentifier": ext.subject_key_identifier.as_ref().map(hex::encode),
"authorityKeyIdentifier": ext.authority_key_identifier.as_ref().map(hex::encode),
"crlDistributionPointsUris": ext.crl_distribution_points_uris,
"caIssuersUris": ext.ca_issuers_uris,
"subjectInfoAccess": subject_info_access_json(ext.subject_info_access.as_ref()),
"certificatePoliciesOid": ext.certificate_policies_oid,
"ipResources": serde_json::to_value(&ext.ip_resources).unwrap_or(Value::Null),
"asResources": serde_json::to_value(&ext.as_resources).unwrap_or(Value::Null),
})
}
fn subject_info_access_json(value: Option<&SubjectInfoAccess>) -> Value {
match value {
None => Value::Null,
Some(SubjectInfoAccess::Ca(ca)) => json!({
"kind": "ca",
"accessDescriptions": ca.access_descriptions.iter().map(access_description_json).collect::<Vec<_>>(),
}),
Some(SubjectInfoAccess::Ee(ee)) => json!({
"kind": "ee",
"signedObjectUris": ee.signed_object_uris,
"accessDescriptions": ee.access_descriptions.iter().map(access_description_json).collect::<Vec<_>>(),
}),
}
}
fn access_description_json(value: &AccessDescription) -> Value {
json!({
"accessMethodOid": value.access_method_oid,
"accessLocation": value.access_location,
})
}
fn signed_object_json(signed_object: &RpkiSignedObject) -> Value {
let signed_data = &signed_object.signed_data;
json!({
"rawDer": bytes_summary(&signed_object.raw_der),
"contentInfoContentType": signed_object.content_info_content_type,
"signedData": {
"version": signed_data.version,
"digestAlgorithms": signed_data.digest_algorithms,
"encapContentInfo": {
"eContentType": signed_data.encap_content_info.econtent_type,
"eContent": bytes_summary(&signed_data.encap_content_info.econtent),
},
"certificates": signed_data.certificates.iter().map(ee_certificate_json).collect::<Vec<_>>(),
"crlsPresent": signed_data.crls_present,
"signerInfos": signed_data.signer_infos.iter().map(signer_info_json).collect::<Vec<_>>(),
},
})
}
fn ee_certificate_json(cert: &ResourceEeCertificate) -> Value {
json!({
"rawDer": bytes_summary(&cert.raw_der),
"subjectKeyIdentifier": hex::encode(&cert.subject_key_identifier),
"spkiDer": bytes_summary(&cert.spki_der),
"rsaPublicKey": {
"modulus": bytes_summary(&cert.rsa_public_modulus),
"exponent": bytes_summary(&cert.rsa_public_exponent),
},
"tbsCertificate": bytes_summary(&cert.tbs_certificate_der),
"certificateSignature": bytes_summary(&cert.signature_bytes),
"keyUsageSummary": format!("{:?}", cert.key_usage_summary),
"siaSignedObjectUris": cert.sia_signed_object_uris,
"resourceCertificate": resource_certificate_json(&cert.resource_cert),
})
}
fn signer_info_json(info: &SignerInfoProfiled) -> Value {
json!({
"version": info.version,
"sidSki": hex::encode(&info.sid_ski),
"digestAlgorithm": info.digest_algorithm,
"signatureAlgorithm": info.signature_algorithm,
"signedAttrs": signed_attrs_json(&info.signed_attrs),
"unsignedAttrsPresent": info.unsigned_attrs_present,
"signature": bytes_summary(&info.signature),
"signedAttrsDerForSignature": bytes_summary(&info.signed_attrs_der_for_signature),
})
}
fn signed_attrs_json(attrs: &SignedAttrsProfiled) -> Value {
json!({
"contentType": attrs.content_type,
"messageDigest": hex::encode(&attrs.message_digest),
"signingTime": {
"utc": format_time(attrs.signing_time.utc),
"encoding": format!("{:?}", attrs.signing_time.encoding),
},
"otherAttrsPresent": attrs.other_attrs_present,
})
}
fn result_json(result: Result<(), String>) -> Value {
match result {
Ok(()) => json!({"valid": true}),
Err(err) => json!({"valid": false, "error": err}),
}
}
fn object_type_label(object_type: ObjectType) -> &'static str {
match object_type {
ObjectType::Auto => "auto",
ObjectType::Cer => "cer",
ObjectType::Mft => "mft",
ObjectType::Crl => "crl",
ObjectType::Roa => "roa",
ObjectType::Aspa => "aspa",
}
}
fn bytes_summary(bytes: &[u8]) -> Value {
let head_len = bytes.len().min(16);
let tail_len = bytes.len().min(16);
json!({
"len": bytes.len(),
"sha256": sha256_hex(bytes),
"headHex": hex::encode(&bytes[..head_len]),
"tailHex": hex::encode(&bytes[bytes.len().saturating_sub(tail_len)..]),
})
}
fn sha256_hex(bytes: &[u8]) -> String {
hex::encode(Sha256::digest(bytes))
}
fn format_time(value: time::OffsetDateTime) -> String {
value
.to_offset(time::UtcOffset::UTC)
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_else(|_| value.unix_timestamp().to_string())
}
fn roa_prefix_string(prefix: &RoaIpPrefix) -> String {
let bytes = prefix.addr_bytes();
match prefix.afi {
RoaAfi::Ipv4 => {
let octets = [bytes[0], bytes[1], bytes[2], bytes[3]];
format!("{}/{}", Ipv4Addr::from(octets), prefix.prefix_len)
}
RoaAfi::Ipv6 => {
let mut octets = [0u8; 16];
octets.copy_from_slice(bytes);
format!("{}/{}", Ipv6Addr::from(octets), prefix.prefix_len)
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn object_type_parser_and_resolver_cover_aliases() {
assert_eq!(ObjectType::parse("auto").unwrap(), ObjectType::Auto);
assert_eq!(ObjectType::parse(".cer").unwrap(), ObjectType::Cer);
assert_eq!(ObjectType::parse("certificate").unwrap(), ObjectType::Cer);
assert_eq!(ObjectType::parse("manifest").unwrap(), ObjectType::Mft);
assert_eq!(ObjectType::parse(".crl").unwrap(), ObjectType::Crl);
assert_eq!(ObjectType::parse("roa").unwrap(), ObjectType::Roa);
assert_eq!(ObjectType::parse("aspa").unwrap(), ObjectType::Aspa);
assert_eq!(ObjectType::parse(".asa").unwrap(), ObjectType::Aspa);
assert!(ObjectType::parse("unknown").is_err());
assert_eq!(ObjectType::Aspa.label(), "aspa");
assert_eq!(
resolve_object_type(ObjectType::Auto, Path::new("repo/a.cer")).unwrap(),
ObjectType::Cer
);
assert_eq!(
resolve_object_type(ObjectType::Auto, Path::new("repo/a.mft")).unwrap(),
ObjectType::Mft
);
assert_eq!(
resolve_object_type(ObjectType::Auto, Path::new("repo/a.crl")).unwrap(),
ObjectType::Crl
);
assert_eq!(
resolve_object_type(ObjectType::Auto, Path::new("repo/a.roa")).unwrap(),
ObjectType::Roa
);
assert_eq!(
resolve_object_type(ObjectType::Auto, Path::new("repo/a.asa")).unwrap(),
ObjectType::Aspa
);
assert_eq!(
resolve_object_type(ObjectType::Roa, Path::new("repo/a.bin")).unwrap(),
ObjectType::Roa
);
assert!(resolve_object_type(ObjectType::Auto, Path::new("repo/a.bin")).is_err());
}
#[test]
fn invalid_der_returns_error_projection_for_all_object_types() {
let bytes = b"not der";
for object_type in [
ObjectType::Cer,
ObjectType::Mft,
ObjectType::Crl,
ObjectType::Roa,
ObjectType::Aspa,
] {
let value = parse_object_json(object_type, Path::new("bad.der"), bytes, 1);
assert_eq!(
value["object"]["decode"]["profileValid"].as_bool(),
Some(false)
);
assert!(value["object"]["decode"]["error"].as_str().is_some());
}
let record = build_object_projection(ObjectType::Auto, Path::new("bad.bin"), bytes, 1);
assert_eq!(record.object_type, "unknown");
assert_eq!(record.parse_status, "error");
assert!(record.error_summary.is_some());
}
#[test]
fn parses_synthetic_objects_into_human_readable_projection() {
let fixture = crate::test_support::synthetic_repository();
let repository = fixture.case_repository("baseline-v1");
let cases = [
(
ObjectType::Cer,
fixture.trust_anchor(),
"cer",
"resourceCertificate",
),
(
ObjectType::Mft,
repository.join("child/child.mft"),
"mft",
"manifest",
),
(
ObjectType::Crl,
repository.join("child/child.crl"),
"crl",
"revokedCertificates",
),
(
ObjectType::Roa,
repository.join("child/valid.roa"),
"roa",
"roa",
),
(
ObjectType::Aspa,
repository.join("child/valid.asa"),
"aspa",
"aspa",
),
];
for (object_type, path, expected_type, expected_section) in cases {
let bytes = std::fs::read(&path).expect("synthetic object");
let record = build_object_projection(object_type, &path, &bytes, 1);
assert_eq!(record.object_type, expected_type);
assert_eq!(record.parse_status, "ok");
assert_eq!(
record.projection["object"]["decode"]["profileValid"].as_bool(),
Some(true)
);
assert!(record.projection["object"][expected_section].is_object());
}
}
#[test]
fn large_projection_lists_are_paged_from_raw_bytes() {
let repository = crate::test_support::synthetic_repository().case_repository("baseline-v1");
let mft_bytes = std::fs::read(repository.join("child/child.mft")).expect("synthetic mft");
let (total, page) = manifest_file_entries_page(&mft_bytes, 1, 3).expect("mft page");
assert!(total >= 3);
assert_eq!(page.len(), (total - 1).min(3));
assert!(page[0]["fileName"].as_str().is_some());
let (_, empty_page) =
manifest_file_entries_page(&mft_bytes, total + 10, 3).expect("empty page");
assert!(empty_page.is_empty());
let crl_bytes = std::fs::read(repository.join("child/child.crl")).expect("synthetic crl");
let (total, page) = crl_revoked_entries_page(&crl_bytes, 0, 5).expect("crl page");
assert!(page.len() <= total);
let (_, empty_page) =
crl_revoked_entries_page(&crl_bytes, total + 10, 5).expect("empty crl page");
assert!(empty_page.is_empty());
}
}

30
src/model/rc.rs Normal file
View File

@@ -0,0 +1,30 @@
use der_parser::ber::{BerObjectContent, Class};
use der_parser::der::{DerObject, Tag, parse_der};
use der_parser::num_bigint::BigUint;
use x509_parser::asn1_rs::{Class as Asn1Class, Tag as Asn1Tag};
use x509_parser::extensions::ParsedExtension;
use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version};
use serde::{Deserialize, Serialize};
use crate::model::common::{
Asn1TimeUtc, DerReader, InvalidTimeEncodingError, UtcTime, X509NameDer, asn1_time_to_model,
};
use crate::model::oid::{
OID_AD_CA_ISSUERS_RAW, OID_AD_CA_REPOSITORY, OID_AD_CA_REPOSITORY_RAW, OID_AD_RPKI_MANIFEST,
OID_AD_RPKI_MANIFEST_RAW, OID_AD_RPKI_NOTIFY, OID_AD_RPKI_NOTIFY_RAW, OID_AD_SIGNED_OBJECT,
OID_AD_SIGNED_OBJECT_RAW, OID_AUTHORITY_INFO_ACCESS, OID_AUTHORITY_INFO_ACCESS_RAW,
OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTHORITY_KEY_IDENTIFIER_RAW, OID_AUTONOMOUS_SYS_IDS,
OID_AUTONOMOUS_SYS_IDS_RAW, OID_BASIC_CONSTRAINTS, OID_BASIC_CONSTRAINTS_RAW,
OID_CERTIFICATE_POLICIES, OID_CERTIFICATE_POLICIES_RAW, OID_CP_IPADDR_ASNUMBER,
OID_CP_IPADDR_ASNUMBER_RAW, OID_CRL_DISTRIBUTION_POINTS, OID_CRL_DISTRIBUTION_POINTS_RAW,
OID_EXTENDED_KEY_USAGE, OID_IP_ADDR_BLOCKS, OID_IP_ADDR_BLOCKS_RAW, OID_KEY_USAGE, OID_QT_CPS,
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SUBJECT_INFO_ACCESS,
OID_SUBJECT_INFO_ACCESS_RAW, OID_SUBJECT_KEY_IDENTIFIER, OID_SUBJECT_KEY_IDENTIFIER_RAW,
};
include!("rc/types.rs");
include!("rc/certificate_validation.rs");
include!("rc/parsed_validation.rs");
include!("rc/parsing.rs");
include!("rc/strict_name_tests.rs");

View File

@@ -0,0 +1,248 @@
// Resource certificate profile validation and strict-name checks.
impl ResourceCertificate {
/// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(
der: &[u8],
) -> Result<ResourceCertificateParsed, ResourceCertificateParseError> {
let (rem, cert) = X509Certificate::from_der(der)
.map_err(|e| ResourceCertificateParseError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(ResourceCertificateParseError::TrailingBytes(rem.len()));
}
let validity_not_before = asn1_time_to_model(cert.validity().not_before);
let validity_not_after = asn1_time_to_model(cert.validity().not_after);
let subject_public_key_info = cert.tbs_certificate.subject_pki.raw.to_vec();
let signature_algorithm = algorithm_identifier_value(&cert.signature_algorithm);
let tbs_signature_algorithm = algorithm_identifier_value(&cert.tbs_certificate.signature);
let extensions = parse_extensions_parse(cert.extensions())?;
Ok(ResourceCertificateParsed {
raw_der: der.to_vec(),
version: cert.version(),
serial_number: cert.tbs_certificate.serial.clone(),
signature_algorithm,
tbs_signature_algorithm,
issuer_name: X509NameDer(cert.issuer().as_raw().to_vec()),
subject_name: X509NameDer(cert.subject().as_raw().to_vec()),
validity_not_before,
validity_not_after,
subject_public_key_info,
extensions,
})
}
/// Profile validate step of scheme A (`parse → validate → verify`).
///
/// `ResourceCertificate` is already profile-validated when constructed via `decode_der()` /
/// `ResourceCertificateParsed::validate_profile()`.
pub fn validate_profile(&self) -> Result<(), ResourceCertificateProfileError> {
Ok(())
}
pub fn validate_rfc6487_profile(
&self,
role: ResourceCertificateRole,
) -> Result<(), ResourceCertificateProfileError> {
let role_name = match role {
ResourceCertificateRole::TrustAnchor => "trust anchor CA",
ResourceCertificateRole::Ca => "CA",
ResourceCertificateRole::SignedObjectEe => "signed-object EE",
ResourceCertificateRole::RouterEe => "router EE",
};
let ca_role = matches!(
role,
ResourceCertificateRole::TrustAnchor | ResourceCertificateRole::Ca
);
if ca_role {
let constraints = self
.tbs
.extensions
.basic_constraints
.as_ref()
.ok_or(ResourceCertificateProfileError::BasicConstraintsMissing)?;
if !constraints.critical {
return Err(ResourceCertificateProfileError::BasicConstraintsCriticality);
}
if !constraints.ca {
return Err(ResourceCertificateProfileError::BasicConstraintsCaFalse);
}
if constraints.path_len_constraint.is_some() {
return Err(ResourceCertificateProfileError::BasicConstraintsPathLenPresent);
}
} else if self.tbs.extensions.basic_constraints.is_some() {
return Err(ResourceCertificateProfileError::BasicConstraintsEeMustOmit);
}
for oid in &self.tbs.extensions.extension_oids {
if !is_permitted_extension(oid, role) {
return Err(ResourceCertificateProfileError::DisallowedExtension {
role: role_name,
oid: oid.clone(),
});
}
}
let policies = self
.tbs
.extensions
.certificate_policies
.as_ref()
.ok_or(ResourceCertificateProfileError::CertificatePoliciesMissing)?;
if policies.policy_oid != OID_CP_IPADDR_ASNUMBER {
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
policies.policy_oid.clone(),
));
}
if policies.qualifier_oids.len() > 1 {
return Err(ResourceCertificateProfileError::CertificatePoliciesTooManyQualifiers);
}
if let Some(qualifier_oid) = policies.qualifier_oids.first()
&& qualifier_oid != OID_QT_CPS {
return Err(
ResourceCertificateProfileError::CertificatePoliciesInvalidQualifier(
qualifier_oid.clone(),
),
);
}
if self
.tbs
.extensions
.as_resources
.as_ref()
.is_some_and(|resources| resources.rdi.is_some())
{
return Err(ResourceCertificateProfileError::AsResourcesRdiPresent);
}
Ok(())
}
pub fn validate_strict_name_profile(&self) -> Result<(), ResourceCertificateProfileError> {
validate_strict_rpki_name(&self.tbs.issuer_name, "issuer")?;
validate_strict_rpki_name(&self.tbs.subject_name, "subject")?;
Ok(())
}
/// Decode a resource certificate (`parse + validate`).
pub fn decode_der(der: &[u8]) -> Result<Self, ResourceCertificateDecodeError> {
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn decode_der_with_strict_name(der: &[u8]) -> Result<Self, ResourceCertificateDecodeError> {
let cert = Self::decode_der(der)?;
cert.validate_strict_name_profile()?;
Ok(cert)
}
/// Backwards-compatible helper (historical name).
pub fn from_der(der: &[u8]) -> Result<Self, ResourceCertificateError> {
Self::decode_der(der)
}
}
fn is_permitted_extension(oid: &str, role: ResourceCertificateRole) -> bool {
matches!(
oid,
OID_BASIC_CONSTRAINTS
| OID_KEY_USAGE
| OID_SUBJECT_KEY_IDENTIFIER
| OID_AUTHORITY_KEY_IDENTIFIER
| OID_CRL_DISTRIBUTION_POINTS
| OID_AUTHORITY_INFO_ACCESS
| OID_SUBJECT_INFO_ACCESS
| OID_CERTIFICATE_POLICIES
| OID_IP_ADDR_BLOCKS
| OID_AUTONOMOUS_SYS_IDS
) || (role == ResourceCertificateRole::RouterEe && oid == OID_EXTENDED_KEY_USAGE)
}
fn validate_strict_rpki_name(
name: &X509NameDer,
role: &'static str,
) -> Result<(), ResourceCertificateProfileError> {
let mut name_seq = DerReader::new(name.as_raw())
.take_sequence()
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
let mut common_name_count = 0usize;
let mut serial_number_count = 0usize;
while !name_seq.is_empty() {
let set_bytes = name_seq
.take_tag(0x31)
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
let mut rdn_set = DerReader::new(set_bytes);
if rdn_set.is_empty() {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: "RelativeDistinguishedName SET is empty".to_string(),
});
}
while !rdn_set.is_empty() {
let mut attr = rdn_set
.take_sequence()
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
let oid = attr
.take_tag(0x06)
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
let (value_tag, _value) = attr
.take_any()
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
if !attr.is_empty() {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: "AttributeTypeAndValue must be SEQUENCE of 2".to_string(),
});
}
match *oid {
// 2.5.4.3 commonName
[0x55, 0x04, 0x03] => {
common_name_count += 1;
if value_tag != 0x13 {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: format!(
"commonName must be PrintableString, got tag 0x{value_tag:02X}"
),
});
}
}
// 2.5.4.5 serialNumber
[0x55, 0x04, 0x05] => {
serial_number_count += 1;
if value_tag != 0x13 {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: format!(
"serialNumber must be PrintableString, got tag 0x{value_tag:02X}"
),
});
}
}
_ => {}
}
}
}
if common_name_count != 1 {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: format!("commonName must appear exactly once, got {common_name_count}"),
});
}
if serial_number_count > 1 {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: format!("serialNumber must appear at most once, got {serial_number_count}"),
});
}
Ok(())
}

View File

@@ -0,0 +1,346 @@
// Parsed certificate and extension profile validation.
impl ResourceCertificateParsed {
pub fn validate_profile(self) -> Result<ResourceCertificate, ResourceCertificateProfileError> {
let version = match self.version {
X509Version::V3 => 2u32,
_ => return Err(ResourceCertificateProfileError::InvalidVersion),
};
self.validity_not_before
.validate_encoding_rfc5280("notBefore")?;
self.validity_not_after
.validate_encoding_rfc5280("notAfter")?;
if self.signature_algorithm != self.tbs_signature_algorithm {
return Err(ResourceCertificateProfileError::SignatureAlgorithmMismatch);
}
if self.signature_algorithm.oid != OID_SHA256_WITH_RSA_ENCRYPTION {
return Err(ResourceCertificateProfileError::UnsupportedSignatureAlgorithm);
}
if !self.signature_algorithm.params_absent_or_null() {
return Err(ResourceCertificateProfileError::InvalidSignatureAlgorithmParameters);
}
let is_self_signed = self.issuer_name == self.subject_name;
let extensions = self.extensions.validate_profile(is_self_signed)?;
let kind = if extensions.basic_constraints_ca {
ResourceCertKind::Ca
} else {
ResourceCertKind::Ee
};
Ok(ResourceCertificate {
raw_der: self.raw_der,
tbs: RpkixTbsCertificate {
version,
serial_number: self.serial_number,
signature_algorithm: self.signature_algorithm.oid,
issuer_name: self.issuer_name,
subject_name: self.subject_name,
validity_not_before: self.validity_not_before.utc,
validity_not_after: self.validity_not_after.utc,
subject_public_key_info: self.subject_public_key_info,
extensions,
},
kind,
})
}
}
impl RcExtensionsParsed {
pub fn validate_profile(
self,
is_self_signed: bool,
) -> Result<RcExtensions, ResourceCertificateProfileError> {
// NOTE(perf): `self` is consumed. Prefer moving decoded fields out rather than cloning,
// especially for large resource sets and URI lists.
let RcExtensionsParsed {
basic_constraints,
subject_key_identifier,
authority_key_identifier,
crl_distribution_points,
authority_info_access,
subject_info_access,
certificate_policies,
extension_oids,
ip_resources,
as_resources,
} = self;
if basic_constraints.len() > 1 {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"basicConstraints",
));
}
let basic_constraints = basic_constraints.into_iter().next();
let basic_constraints_ca = basic_constraints.as_ref().is_some_and(|bc| bc.ca);
let subject_key_identifier = match subject_key_identifier.len() {
0 => None,
1 => {
let (ski, critical) = subject_key_identifier.into_iter().next().expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::SkiCriticality);
}
Some(ski)
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"subjectKeyIdentifier",
));
}
};
let authority_key_identifier = match authority_key_identifier.len() {
0 => {
if is_self_signed {
None
} else {
return Err(ResourceCertificateProfileError::AkiMissing);
}
}
1 => {
let (aki, critical) = authority_key_identifier.into_iter().next().expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::AkiCriticality);
}
if aki.has_authority_cert_issuer {
return Err(ResourceCertificateProfileError::AkiAuthorityCertIssuerPresent);
}
if aki.has_authority_cert_serial {
return Err(ResourceCertificateProfileError::AkiAuthorityCertSerialPresent);
}
let keyid = aki.key_identifier;
if is_self_signed {
if let (Some(keyid), Some(ski)) =
(keyid.as_ref(), subject_key_identifier.as_ref())
&& keyid != ski {
return Err(ResourceCertificateProfileError::AkiSelfSignedNotEqualSki);
}
} else if keyid.is_none() {
return Err(ResourceCertificateProfileError::AkiMissing);
}
keyid
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"authorityKeyIdentifier",
));
}
};
let crl_distribution_points_uris = match crl_distribution_points.len() {
0 => {
if is_self_signed {
None
} else {
return Err(ResourceCertificateProfileError::CrlDistributionPointsMissing);
}
}
1 => {
let (crldp, critical) = crl_distribution_points.into_iter().next().expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::CrlDistributionPointsCriticality);
}
if is_self_signed {
return Err(
ResourceCertificateProfileError::CrlDistributionPointsSelfSignedMustOmit,
);
}
if crldp.distribution_points.len() != 1 {
return Err(ResourceCertificateProfileError::CrlDistributionPointsNotSingle);
}
let dp = crldp
.distribution_points
.into_iter()
.next()
.expect("len==1");
if dp.reasons_present {
return Err(ResourceCertificateProfileError::CrlDistributionPointsHasReasons);
}
if dp.crl_issuer_present {
return Err(ResourceCertificateProfileError::CrlDistributionPointsHasCrlIssuer);
}
if !dp.distribution_point_present {
return Err(
ResourceCertificateProfileError::CrlDistributionPointsNoDistributionPoint,
);
}
if dp.name_relative_to_crl_issuer_present || !dp.full_name_present {
return Err(ResourceCertificateProfileError::CrlDistributionPointsInvalidName);
}
if dp.full_name_not_uri {
return Err(
ResourceCertificateProfileError::CrlDistributionPointsFullNameNotUri,
);
}
if !dp.full_name_uris.iter().any(|u| u.starts_with("rsync://")) {
return Err(ResourceCertificateProfileError::CrlDistributionPointsNoRsync);
}
Some(dp.full_name_uris)
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"cRLDistributionPoints",
));
}
};
let ca_issuers_uris = match authority_info_access.len() {
0 => {
if is_self_signed {
None
} else {
return Err(ResourceCertificateProfileError::AuthorityInfoAccessMissing);
}
}
1 => {
let (aia, critical) = authority_info_access.into_iter().next().expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::AuthorityInfoAccessCriticality);
}
if is_self_signed {
return Err(
ResourceCertificateProfileError::AuthorityInfoAccessSelfSignedMustOmit,
);
}
if aia.ca_issuers_access_location_not_uri {
return Err(
ResourceCertificateProfileError::AuthorityInfoAccessCaIssuersNotUri,
);
}
if aia.ca_issuers_uris.is_empty() {
return Err(
ResourceCertificateProfileError::AuthorityInfoAccessMissingCaIssuers,
);
}
if !aia
.ca_issuers_uris
.iter()
.any(|u| u.starts_with("rsync://"))
{
return Err(ResourceCertificateProfileError::AuthorityInfoAccessNoRsync);
}
Some(aia.ca_issuers_uris)
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"authorityInfoAccess",
));
}
};
let subject_info_access = match subject_info_access.len() {
0 => None,
1 => {
let (sia, critical) = subject_info_access.into_iter().next().expect("len==1");
if critical {
return Err(ResourceCertificateProfileError::SiaCriticality);
}
if sia.signed_object_access_location_not_uri {
return Err(ResourceCertificateProfileError::SignedObjectSiaNotUri);
}
if !sia.signed_object_uris.is_empty()
&& !sia
.signed_object_uris
.iter()
.any(|u| u.starts_with("rsync://"))
{
return Err(ResourceCertificateProfileError::SignedObjectSiaNoRsync);
}
if sia.signed_object_uris.is_empty() {
Some(SubjectInfoAccess::Ca(SubjectInfoAccessCa {
access_descriptions: sia.access_descriptions,
}))
} else {
Some(SubjectInfoAccess::Ee(SubjectInfoAccessEe {
signed_object_uris: sia.signed_object_uris,
access_descriptions: sia.access_descriptions,
}))
}
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"subjectInfoAccess",
));
}
};
let certificate_policies = match certificate_policies.len() {
0 => None,
1 => {
let (policies, critical) = certificate_policies.into_iter().next().expect("len==1");
if !critical {
return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality);
}
if policies.len() != 1 {
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
"expected exactly one policy".into(),
));
}
let policy = policies.into_iter().next().expect("len==1");
if policy.policy_oid != OID_CP_IPADDR_ASNUMBER {
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
policy.policy_oid,
));
}
Some(policy)
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"certificatePolicies",
));
}
};
let ip_resources = match ip_resources.len() {
0 => None,
1 => {
let (ip, critical) = ip_resources.into_iter().next().expect("len==1");
if !critical {
return Err(ResourceCertificateProfileError::IpResourcesCriticality);
}
Some(ip)
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"ipAddrBlocks",
));
}
};
let as_resources = match as_resources.len() {
0 => None,
1 => {
let (asn, critical) = as_resources.into_iter().next().expect("len==1");
if !critical {
return Err(ResourceCertificateProfileError::AsResourcesCriticality);
}
Some(asn)
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"autonomousSysIds",
));
}
};
Ok(RcExtensions {
basic_constraints_ca,
basic_constraints,
subject_key_identifier,
authority_key_identifier,
crl_distribution_points_uris,
ca_issuers_uris,
subject_info_access,
certificate_policies_oid: certificate_policies
.as_ref()
.map(|_| OID_CP_IPADDR_ASNUMBER.to_string()),
certificate_policies,
extension_oids,
ip_resources,
as_resources,
})
}
}

584
src/model/rc/parsing.rs Normal file
View File

@@ -0,0 +1,584 @@
// DER parsing helpers for certificate extensions and resources.
fn algorithm_identifier_value(
ai: &x509_parser::x509::AlgorithmIdentifier<'_>,
) -> AlgorithmIdentifierValue {
let parameters = ai.parameters.as_ref().map(|p| AlgorithmParametersValue {
class: p.class(),
tag: p.tag(),
data: p.as_bytes().to_vec(),
});
// NOTE(perf): Avoid `to_id_string()` allocations for the algorithms we expect
// in RPKI resource certificates. Fall back to `to_id_string()` for unexpected
// algorithms (mostly error paths).
let oid = if ai.algorithm.as_bytes() == OID_SHA256_WITH_RSA_ENCRYPTION_RAW {
OID_SHA256_WITH_RSA_ENCRYPTION.to_string()
} else {
ai.algorithm.to_id_string()
};
AlgorithmIdentifierValue { oid, parameters }
}
fn parse_extensions_parse(
exts: &[X509Extension<'_>],
) -> Result<RcExtensionsParsed, ResourceCertificateParseError> {
let mut basic_constraints: Vec<BasicConstraintsProfile> = Vec::new();
let mut ski: Vec<(Vec<u8>, bool)> = Vec::new();
let mut aki: Vec<(AuthorityKeyIdentifierParsed, bool)> = Vec::new();
let mut crldp: Vec<(CrlDistributionPointsParsed, bool)> = Vec::new();
let mut aia: Vec<(AuthorityInfoAccessParsed, bool)> = Vec::new();
let mut sia: Vec<(SubjectInfoAccessParsed, bool)> = Vec::new();
let mut cert_policies: Vec<(Vec<CertificatePoliciesProfile>, bool)> = Vec::new();
let mut extension_oids: Vec<String> = Vec::with_capacity(exts.len());
let mut ip_resources: Vec<(IpResourceSet, bool)> = Vec::new();
let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new();
for ext in exts {
let oid = ext.oid.as_bytes();
extension_oids.push(ext.oid.to_id_string());
if oid == OID_BASIC_CONSTRAINTS_RAW {
let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse(
"basicConstraints parse failed".into(),
));
};
basic_constraints.push(BasicConstraintsProfile {
ca: bc.ca,
critical: ext.critical,
path_len_constraint: bc.path_len_constraint,
});
} else if oid == OID_SUBJECT_KEY_IDENTIFIER_RAW {
let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse(
"subjectKeyIdentifier parse failed".into(),
));
};
ski.push((s.0.to_vec(), ext.critical));
} else if oid == OID_AUTHORITY_KEY_IDENTIFIER_RAW {
let ParsedExtension::AuthorityKeyIdentifier(a) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse(
"authorityKeyIdentifier parse failed".into(),
));
};
aki.push((
AuthorityKeyIdentifierParsed {
key_identifier: a.key_identifier.as_ref().map(|k| k.0.to_vec()),
has_authority_cert_issuer: a.authority_cert_issuer.is_some(),
has_authority_cert_serial: a.authority_cert_serial.is_some(),
},
ext.critical,
));
} else if oid == OID_CRL_DISTRIBUTION_POINTS_RAW {
let ParsedExtension::CRLDistributionPoints(p) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse(
"cRLDistributionPoints parse failed".into(),
));
};
crldp.push((parse_crldp_parse(p)?, ext.critical));
} else if oid == OID_AUTHORITY_INFO_ACCESS_RAW {
let ParsedExtension::AuthorityInfoAccess(p) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse(
"authorityInfoAccess parse failed".into(),
));
};
aia.push((parse_aia_parse(p.accessdescs.as_slice())?, ext.critical));
} else if oid == OID_SUBJECT_INFO_ACCESS_RAW {
let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse(
"subjectInfoAccess parse failed".into(),
));
};
sia.push((parse_sia_parse(s.accessdescs.as_slice())?, ext.critical));
} else if oid == OID_CERTIFICATE_POLICIES_RAW {
let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse(
"certificatePolicies parse failed".into(),
));
};
let mut policies: Vec<CertificatePoliciesProfile> = Vec::with_capacity(cp.len());
for p in cp.iter() {
let b = p.policy_id.as_bytes();
let policy_oid = if b == OID_CP_IPADDR_ASNUMBER_RAW {
OID_CP_IPADDR_ASNUMBER.to_string()
} else {
p.policy_id.to_id_string()
};
let qualifier_oids = p
.policy_qualifiers
.as_ref()
.map(|qualifiers| {
qualifiers
.iter()
.map(|qualifier| qualifier.policy_qualifier_id.to_id_string())
.collect()
})
.unwrap_or_default();
policies.push(CertificatePoliciesProfile {
policy_oid,
qualifier_oids,
});
}
cert_policies.push((policies, ext.critical));
} else if oid == OID_IP_ADDR_BLOCKS_RAW {
let parsed = IpResourceSet::decode_extn_value(ext.value)
.map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?;
ip_resources.push((parsed, ext.critical));
} else if oid == OID_AUTONOMOUS_SYS_IDS_RAW {
let parsed = AsResourceSet::decode_extn_value(ext.value)
.map_err(|_e| ResourceCertificateParseError::InvalidAsResourcesEncoding)?;
as_resources.push((parsed, ext.critical));
}
}
Ok(RcExtensionsParsed {
basic_constraints,
subject_key_identifier: ski,
authority_key_identifier: aki,
crl_distribution_points: crldp,
authority_info_access: aia,
subject_info_access: sia,
certificate_policies: cert_policies,
extension_oids,
ip_resources,
as_resources,
})
}
fn parse_aia_parse(
access: &[x509_parser::extensions::AccessDescription<'_>],
) -> Result<AuthorityInfoAccessParsed, ResourceCertificateParseError> {
let mut ca_issuers_uris: Vec<String> = Vec::new();
let mut ca_issuers_access_location_not_uri = false;
for ad in access {
if ad.access_method.as_bytes() != OID_AD_CA_ISSUERS_RAW {
continue;
}
let uri = match &ad.access_location {
x509_parser::extensions::GeneralName::URI(u) => u,
_ => {
ca_issuers_access_location_not_uri = true;
continue;
}
};
ca_issuers_uris.push(uri.to_string());
}
Ok(AuthorityInfoAccessParsed {
ca_issuers_uris,
ca_issuers_access_location_not_uri,
})
}
fn parse_crldp_parse(
crldp: &x509_parser::extensions::CRLDistributionPoints<'_>,
) -> Result<CrlDistributionPointsParsed, ResourceCertificateParseError> {
let mut out: Vec<CrlDistributionPointParsed> = Vec::new();
for p in crldp.iter() {
let mut full_name_uris: Vec<String> = Vec::new();
let mut full_name_not_uri = false;
let mut full_name_present = false;
let mut name_relative_to_crl_issuer_present = false;
let mut distribution_point_present = false;
if let Some(dp) = &p.distribution_point {
distribution_point_present = true;
match dp {
x509_parser::extensions::DistributionPointName::FullName(names) => {
full_name_present = true;
for n in names {
match n {
x509_parser::extensions::GeneralName::URI(u) => {
full_name_uris.push(u.to_string());
}
_ => {
full_name_not_uri = true;
}
}
}
}
x509_parser::extensions::DistributionPointName::NameRelativeToCRLIssuer(_) => {
name_relative_to_crl_issuer_present = true;
}
}
}
out.push(CrlDistributionPointParsed {
distribution_point_present,
reasons_present: p.reasons.is_some(),
crl_issuer_present: p.crl_issuer.is_some(),
name_relative_to_crl_issuer_present,
full_name_uris,
full_name_not_uri,
full_name_present,
});
}
Ok(CrlDistributionPointsParsed {
distribution_points: out,
})
}
fn parse_sia_parse(
access: &[x509_parser::extensions::AccessDescription<'_>],
) -> Result<SubjectInfoAccessParsed, ResourceCertificateParseError> {
let mut all = Vec::with_capacity(access.len());
let mut signed_object_uris: Vec<String> = Vec::new();
let mut signed_object_access_location_not_uri = false;
for ad in access {
let access_method_oid = if ad.access_method.as_bytes() == OID_AD_CA_REPOSITORY_RAW {
OID_AD_CA_REPOSITORY.to_string()
} else if ad.access_method.as_bytes() == OID_AD_RPKI_MANIFEST_RAW {
OID_AD_RPKI_MANIFEST.to_string()
} else if ad.access_method.as_bytes() == OID_AD_RPKI_NOTIFY_RAW {
OID_AD_RPKI_NOTIFY.to_string()
} else if ad.access_method.as_bytes() == OID_AD_SIGNED_OBJECT_RAW {
OID_AD_SIGNED_OBJECT.to_string()
} else {
ad.access_method.to_id_string()
};
let is_signed_object = access_method_oid == OID_AD_SIGNED_OBJECT;
let uri = match &ad.access_location {
x509_parser::extensions::GeneralName::URI(u) => u,
_ => {
if is_signed_object {
signed_object_access_location_not_uri = true;
}
continue;
}
};
if is_signed_object {
signed_object_uris.push(uri.to_string());
}
all.push(AccessDescription {
access_method_oid,
access_location: uri.to_string(),
});
}
Ok(SubjectInfoAccessParsed {
access_descriptions: all,
signed_object_uris,
signed_object_access_location_not_uri,
})
}
fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result<IpResourceSet, ()> {
let (rem, obj) = parse_der(ext_value).map_err(|_| ())?;
if !rem.is_empty() {
return Err(());
}
let seq = obj.as_sequence().map_err(|_| ())?;
let mut families = Vec::with_capacity(seq.len());
for fam in seq {
let fam_seq = fam.as_sequence().map_err(|_| ())?;
if fam_seq.len() != 2 {
return Err(());
}
let af_bytes = fam_seq[0].as_slice().map_err(|_| ())?;
if af_bytes.len() != 2 {
return Err(());
}
let afi = match af_bytes {
[0x00, 0x01] => Afi::Ipv4,
[0x00, 0x02] => Afi::Ipv6,
_ => return Err(()),
};
let choice = match &fam_seq[1].content {
BerObjectContent::Null => IpAddressChoice::Inherit,
BerObjectContent::Sequence(_) => {
let items_seq = fam_seq[1].as_sequence().map_err(|_| ())?;
let mut items = Vec::with_capacity(items_seq.len());
for item in items_seq {
items.push(parse_ip_address_or_range(afi, item)?);
}
IpAddressChoice::AddressesOrRanges(items)
}
_ => return Err(()),
};
families.push(IpAddressFamily { afi, choice });
}
Ok(IpResourceSet { families })
}
fn parse_ip_address_or_range(afi: Afi, obj: &DerObject<'_>) -> Result<IpAddressOrRange, ()> {
match &obj.content {
BerObjectContent::BitString(_, _) => {
Ok(IpAddressOrRange::Prefix(parse_ip_prefix(afi, obj)?))
}
BerObjectContent::Sequence(_) => {
let seq = obj.as_sequence().map_err(|_| ())?;
if seq.len() != 2 {
return Err(());
}
let min = parse_ip_address_bound(afi, &seq[0], false)?;
let max = parse_ip_address_bound(afi, &seq[1], true)?;
Ok(IpAddressOrRange::Range(IpAddressRange { min, max }))
}
_ => Err(()),
}
}
fn parse_ip_prefix(afi: Afi, obj: &DerObject<'_>) -> Result<IpPrefix, ()> {
let (unused_bits, bytes) = match &obj.content {
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
_ => return Err(()),
};
if unused_bits > 7 {
return Err(());
}
if !bytes.is_empty() && unused_bits != 0 {
let mask = (1u8 << unused_bits) - 1;
if (bytes[bytes.len() - 1] & mask) != 0 {
return Err(());
}
} else if bytes.is_empty() && unused_bits != 0 {
return Err(());
}
let prefix_len = (bytes.len() * 8)
.checked_sub(unused_bits as usize)
.ok_or(())? as u16;
if prefix_len > afi.ub() {
return Err(());
}
let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes);
Ok(IpPrefix {
afi,
prefix_len,
addr,
})
}
/// Parse an RFC 3779 `IPAddress` BIT STRING into an address-like byte array.
///
/// When used as an `IPAddressRange` endpoint, RFC 3779 allows endpoints to be encoded with
/// fewer than `ub` bits. In that case, the missing bits are interpreted as 0s for the lower
/// bound and 1s for the upper bound. This is essential to correctly interpret ranges that
/// are expressed on non-octet boundaries.
fn parse_ip_address_bound(
afi: Afi,
obj: &DerObject<'_>,
fill_remaining_ones: bool,
) -> Result<Vec<u8>, ()> {
let (unused_bits, bytes) = match &obj.content {
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
_ => return Err(()),
};
if unused_bits > 7 {
return Err(());
}
if !bytes.is_empty() && unused_bits != 0 {
let mask = (1u8 << unused_bits) - 1;
if (bytes[bytes.len() - 1] & mask) != 0 {
return Err(());
}
} else if bytes.is_empty() && unused_bits != 0 {
return Err(());
}
let bit_len: u16 = (bytes.len() * 8)
.checked_sub(unused_bits as usize)
.ok_or(())?
.try_into()
.map_err(|_| ())?;
if bit_len > afi.ub() {
return Err(());
}
let mut out = vec![0u8; afi.octets_len()];
let copy_len = bytes.len().min(out.len());
out[..copy_len].copy_from_slice(&bytes[..copy_len]);
if fill_remaining_ones {
if bit_len == 0 {
out.fill(0xFF);
return Ok(out);
}
let last_bit = (bit_len - 1) as usize;
let last_byte = last_bit / 8;
let rem = (bit_len % 8) as u8;
if rem != 0 && last_byte < out.len() {
// Set the (8-rem) trailing bits in the last byte to 1.
let mask: u8 = (1u8 << (8 - rem)) - 1;
out[last_byte] |= mask;
}
for b in out.iter_mut().skip(last_byte + 1) {
*b = 0xFF;
}
}
Ok(out)
}
fn parse_as_identifiers(ext_value: &[u8]) -> Result<AsResourceSet, ()> {
let (rem, obj) = parse_der(ext_value).map_err(|_| ())?;
if !rem.is_empty() {
return Err(());
}
let seq = obj.as_sequence().map_err(|_| ())?;
let mut asnum: Option<AsIdentifierChoice> = None;
let mut rdi: Option<AsIdentifierChoice> = None;
for item in seq {
if item.class() != Class::ContextSpecific {
return Err(());
}
match item.tag() {
Tag(0) => {
if asnum.is_some() {
return Err(());
}
let inner = parse_explicit_inner(item)?;
asnum = Some(parse_as_identifier_choice(&inner)?);
}
Tag(1) => {
if rdi.is_some() {
return Err(());
}
let inner = parse_explicit_inner(item)?;
rdi = Some(parse_as_identifier_choice(&inner)?);
}
_ => return Err(()),
}
}
Ok(AsResourceSet { asnum, rdi })
}
fn parse_explicit_inner<'a>(obj: &'a DerObject<'a>) -> Result<DerObject<'a>, ()> {
let inner_der = obj.as_slice().map_err(|_| ())?;
let (rem, inner) = parse_der(inner_der).map_err(|_| ())?;
if !rem.is_empty() {
return Err(());
}
Ok(inner)
}
fn parse_as_identifier_choice(obj: &DerObject<'_>) -> Result<AsIdentifierChoice, ()> {
match &obj.content {
BerObjectContent::Null => Ok(AsIdentifierChoice::Inherit),
BerObjectContent::Sequence(_) => {
let seq = obj.as_sequence().map_err(|_| ())?;
let mut items = Vec::with_capacity(seq.len());
for item in seq {
items.push(parse_as_id_or_range(item)?);
}
Ok(AsIdentifierChoice::AsIdsOrRanges(items))
}
_ => Err(()),
}
}
fn parse_as_id_or_range(obj: &DerObject<'_>) -> Result<AsIdOrRange, ()> {
match &obj.content {
BerObjectContent::Integer(_) => {
let v = obj.as_u64().map_err(|_| ())?;
if v > u32::MAX as u64 {
return Err(());
}
Ok(AsIdOrRange::Id(v as u32))
}
BerObjectContent::Sequence(_) => {
let seq = obj.as_sequence().map_err(|_| ())?;
if seq.len() != 2 {
return Err(());
}
let min = seq[0].as_u64().map_err(|_| ())?;
let max = seq[1].as_u64().map_err(|_| ())?;
if min > u32::MAX as u64 || max > u32::MAX as u64 || min > max {
return Err(());
}
Ok(AsIdOrRange::Range {
min: min as u32,
max: max as u32,
})
}
_ => Err(()),
}
}
fn canonicalize_prefix_addr(afi: Afi, prefix_len: u16, bytes: &[u8]) -> Vec<u8> {
let full_len = afi.octets_len();
let mut addr = vec![0u8; full_len];
let copy_len = bytes.len().min(full_len);
addr[..copy_len].copy_from_slice(&bytes[..copy_len]);
if prefix_len == 0 {
return addr;
}
let last_prefix_bit = (prefix_len - 1) as usize;
let last_prefix_byte = last_prefix_bit / 8;
let rem = (prefix_len % 8) as u8;
if rem != 0 && last_prefix_byte < addr.len() {
let mask: u8 = 0xFF << (8 - rem);
addr[last_prefix_byte] &= mask;
}
addr
}
fn prefix_covers(resource: &IpPrefix, subject: &IpPrefix) -> bool {
if resource.afi != subject.afi {
return false;
}
if resource.prefix_len > subject.prefix_len {
return false;
}
let n = resource.prefix_len as usize;
let whole = n / 8;
let rem = (n % 8) as u8;
if resource.addr.len() != subject.addr.len() {
return false;
}
if resource.addr[..whole] != subject.addr[..whole] {
return false;
}
if rem == 0 {
return true;
}
let mask = 0xFFu8 << (8 - rem);
(resource.addr[whole] & mask) == (subject.addr[whole] & mask)
}
fn prefix_range(afi: Afi, p: &IpPrefix) -> (u128, u128) {
let mut base_bytes = [0u8; 16];
match afi {
Afi::Ipv4 => {
base_bytes[12..].copy_from_slice(&p.addr[..4]);
}
Afi::Ipv6 => {
base_bytes.copy_from_slice(&p.addr[..16]);
}
}
let base = u128::from_be_bytes(base_bytes);
let host_bits = (afi.ub() - p.prefix_len) as u32;
if host_bits == 0 {
return (base, base);
}
let mask = (1u128 << host_bits) - 1;
(base, base | mask)
}
fn range_covers_prefix(afi: Afi, r: &IpAddressRange, p: &IpPrefix) -> bool {
let (p_min, p_max) = prefix_range(afi, p);
let r_min = bytes_to_u128(afi, &r.min);
let r_max = bytes_to_u128(afi, &r.max);
r_min <= p_min && p_max <= r_max
}
fn bytes_to_u128(afi: Afi, bytes: &[u8]) -> u128 {
let mut out = [0u8; 16];
match afi {
Afi::Ipv4 => {
let copy_len = bytes.len().min(4);
out[12..12 + copy_len].copy_from_slice(&bytes[..copy_len]);
}
Afi::Ipv6 => {
let copy_len = bytes.len().min(16);
out[..copy_len].copy_from_slice(&bytes[..copy_len]);
}
}
u128::from_be_bytes(out)
}

View File

@@ -0,0 +1,102 @@
// Strict RPKI name profile tests.
#[cfg(test)]
mod strict_name_tests {
use super::*;
fn name_with_attrs(attrs: &[(&[u8], u8, &[u8])]) -> X509NameDer {
let mut rdns = Vec::new();
for (oid, tag, value) in attrs {
let mut attr = Vec::new();
attr.extend(der_tlv(0x06, oid));
attr.extend(der_tlv(*tag, value));
let attr = der_tlv(0x30, &attr);
let rdn = der_tlv(0x31, &attr);
rdns.extend(rdn);
}
X509NameDer(der_tlv(0x30, &rdns))
}
fn der_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
encode_len(value.len(), &mut out);
out.extend_from_slice(value);
out
}
fn encode_len(len: usize, out: &mut Vec<u8>) {
if len < 0x80 {
out.push(len as u8);
return;
}
let mut bytes = Vec::new();
let mut value = len;
while value > 0 {
bytes.push((value & 0xFF) as u8);
value >>= 8;
}
bytes.reverse();
out.push(0x80 | bytes.len() as u8);
out.extend(bytes);
}
#[test]
fn strict_name_accepts_printable_common_name_and_serial_number() {
let name = name_with_attrs(&[
(&[0x55, 0x04, 0x03], 0x13, b"CN1"),
(&[0x55, 0x04, 0x05], 0x13, b"SN1"),
]);
validate_strict_rpki_name(&name, "subject").expect("strict name");
}
#[test]
fn strict_name_rejects_utf8_common_name() {
let name = name_with_attrs(&[(&[0x55, 0x04, 0x03], 0x0C, b"CN1")]);
let err = validate_strict_rpki_name(&name, "subject").expect_err("strict name fails");
assert!(err.to_string().contains("PrintableString"), "{err}");
}
#[test]
fn strict_name_rejects_duplicate_common_name() {
let name = name_with_attrs(&[
(&[0x55, 0x04, 0x03], 0x13, b"CN1"),
(&[0x55, 0x04, 0x03], 0x13, b"CN2"),
]);
let err = validate_strict_rpki_name(&name, "subject").expect_err("strict name fails");
assert!(err.to_string().contains("exactly once"), "{err}");
}
#[test]
fn profile_rejects_rfc8360_v2_policy_oid() {
let extensions = RcExtensionsParsed {
basic_constraints: vec![BasicConstraintsProfile {
ca: true,
critical: true,
path_len_constraint: None,
}],
subject_key_identifier: Vec::new(),
authority_key_identifier: Vec::new(),
crl_distribution_points: Vec::new(),
authority_info_access: Vec::new(),
subject_info_access: Vec::new(),
certificate_policies: vec![(
vec![CertificatePoliciesProfile {
policy_oid: "1.3.6.1.5.5.7.14.3".to_string(),
qualifier_oids: Vec::new(),
}],
true,
)],
extension_oids: Vec::new(),
ip_resources: Vec::new(),
as_resources: Vec::new(),
};
let err = extensions
.validate_profile(true)
.expect_err("v2 policy OID must remain invalid");
assert!(
matches!(&err, ResourceCertificateProfileError::InvalidCertificatePolicy(oid) if oid == "1.3.6.1.5.5.7.14.3"),
"{err}"
);
}
}

564
src/model/rc/types.rs Normal file
View File

@@ -0,0 +1,564 @@
// Resource certificate and resource-set model types.
/// Resource Certificate kind (semantic classification).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ResourceCertKind {
Ca,
Ee,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ResourceCertificateRole {
TrustAnchor,
Ca,
SignedObjectEe,
RouterEe,
}
/// A parsed RPKI Resource Certificate (RFC 6487) data model.
///
/// This module intentionally focuses on the semantics needed by Signed Object validation and
/// object-specific EE certificate checks (MFT/ROA/ASPA), as described in
/// `rpki/specs/03_resource_certificate_rc.md`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResourceCertificate {
pub raw_der: Vec<u8>,
pub tbs: RpkixTbsCertificate,
pub kind: ResourceCertKind,
}
pub type ResourceCaCertificate = ResourceCertificate;
pub type ResourceEeCertificate = ResourceCertificate;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RpkixTbsCertificate {
pub version: u32,
pub serial_number: BigUint,
pub signature_algorithm: String,
pub issuer_name: X509NameDer,
pub subject_name: X509NameDer,
pub validity_not_before: UtcTime,
pub validity_not_after: UtcTime,
/// DER encoding of SubjectPublicKeyInfo.
pub subject_public_key_info: Vec<u8>,
pub extensions: RcExtensions,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RcExtensions {
pub basic_constraints_ca: bool,
pub basic_constraints: Option<BasicConstraintsProfile>,
pub subject_key_identifier: Option<Vec<u8>>,
/// Authority Key Identifier (AKI) keyIdentifier value.
pub authority_key_identifier: Option<Vec<u8>>,
/// CRL Distribution Points URIs (fullName).
pub crl_distribution_points_uris: Option<Vec<String>>,
/// Authority Information Access (AIA) caIssuers URIs.
pub ca_issuers_uris: Option<Vec<String>>,
pub subject_info_access: Option<SubjectInfoAccess>,
pub certificate_policies_oid: Option<String>,
pub certificate_policies: Option<CertificatePoliciesProfile>,
pub extension_oids: Vec<String>,
pub ip_resources: Option<IpResourceSet>,
pub as_resources: Option<AsResourceSet>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BasicConstraintsProfile {
pub ca: bool,
pub critical: bool,
pub path_len_constraint: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CertificatePoliciesProfile {
pub policy_oid: String,
pub qualifier_oids: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResourceCertificateParsed {
pub raw_der: Vec<u8>,
pub version: X509Version,
pub serial_number: BigUint,
pub signature_algorithm: AlgorithmIdentifierValue,
pub tbs_signature_algorithm: AlgorithmIdentifierValue,
pub issuer_name: X509NameDer,
pub subject_name: X509NameDer,
pub validity_not_before: Asn1TimeUtc,
pub validity_not_after: Asn1TimeUtc,
/// DER encoding of SubjectPublicKeyInfo.
pub subject_public_key_info: Vec<u8>,
pub extensions: RcExtensionsParsed,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlgorithmIdentifierValue {
pub oid: String,
pub parameters: Option<AlgorithmParametersValue>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlgorithmParametersValue {
pub class: Asn1Class,
pub tag: Asn1Tag,
pub data: Vec<u8>,
}
impl AlgorithmIdentifierValue {
pub fn params_absent_or_null(&self) -> bool {
match &self.parameters {
None => true,
Some(p) if p.class == Asn1Class::Universal && p.tag == Asn1Tag::Null => true,
Some(_p) => false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RcExtensionsParsed {
pub basic_constraints: Vec<BasicConstraintsProfile>,
pub subject_key_identifier: Vec<(Vec<u8>, bool)>,
pub authority_key_identifier: Vec<(AuthorityKeyIdentifierParsed, bool)>,
pub crl_distribution_points: Vec<(CrlDistributionPointsParsed, bool)>,
pub authority_info_access: Vec<(AuthorityInfoAccessParsed, bool)>,
pub subject_info_access: Vec<(SubjectInfoAccessParsed, bool)>,
pub certificate_policies: Vec<(Vec<CertificatePoliciesProfile>, bool)>,
pub extension_oids: Vec<String>,
pub ip_resources: Vec<(IpResourceSet, bool)>,
pub as_resources: Vec<(AsResourceSet, bool)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthorityKeyIdentifierParsed {
pub key_identifier: Option<Vec<u8>>,
pub has_authority_cert_issuer: bool,
pub has_authority_cert_serial: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthorityInfoAccessParsed {
pub ca_issuers_uris: Vec<String>,
pub ca_issuers_access_location_not_uri: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CrlDistributionPointsParsed {
pub distribution_points: Vec<CrlDistributionPointParsed>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CrlDistributionPointParsed {
pub distribution_point_present: bool,
pub reasons_present: bool,
pub crl_issuer_present: bool,
pub name_relative_to_crl_issuer_present: bool,
pub full_name_uris: Vec<String>,
pub full_name_not_uri: bool,
pub full_name_present: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubjectInfoAccessParsed {
pub access_descriptions: Vec<AccessDescription>,
pub signed_object_uris: Vec<String>,
pub signed_object_access_location_not_uri: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SubjectInfoAccess {
Ca(SubjectInfoAccessCa),
Ee(SubjectInfoAccessEe),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubjectInfoAccessCa {
pub access_descriptions: Vec<AccessDescription>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubjectInfoAccessEe {
pub signed_object_uris: Vec<String>,
/// The full list of access descriptions as carried in the SIA extension.
pub access_descriptions: Vec<AccessDescription>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AccessDescription {
pub access_method_oid: String,
pub access_location: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Afi {
Ipv4,
Ipv6,
}
impl Afi {
pub fn ub(self) -> u16 {
match self {
Afi::Ipv4 => 32,
Afi::Ipv6 => 128,
}
}
pub fn octets_len(self) -> usize {
match self {
Afi::Ipv4 => 4,
Afi::Ipv6 => 16,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IpResourceSet {
pub families: Vec<IpAddressFamily>,
}
impl IpResourceSet {
/// Decode the DER bytes carried inside the X.509 `extnValue` OCTET STRING for
/// `id-pe-ipAddrBlocks` (RFC 3779 / RFC 6487).
pub fn decode_extn_value(extn_value: &[u8]) -> Result<Self, IpResourceSetDecodeError> {
parse_ip_addr_blocks(extn_value).map_err(|_| IpResourceSetDecodeError::InvalidEncoding)
}
pub fn is_all_inherit(&self) -> bool {
self.families
.iter()
.all(|f| matches!(f.choice, IpAddressChoice::Inherit))
}
pub fn has_any_inherit(&self) -> bool {
self.families
.iter()
.any(|f| matches!(f.choice, IpAddressChoice::Inherit))
}
pub fn contains_prefix(&self, prefix: &IpPrefix) -> bool {
self.families.iter().any(|fam| fam.contains_prefix(prefix))
}
}
#[derive(Debug, thiserror::Error)]
pub enum IpResourceSetDecodeError {
#[error("invalid ipAddrBlocks encoding (RFC 3779 §2.2.3; RFC 6487 §4.8.10)")]
InvalidEncoding,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IpAddressFamily {
pub afi: Afi,
pub choice: IpAddressChoice,
}
impl IpAddressFamily {
pub fn contains_prefix(&self, prefix: &IpPrefix) -> bool {
if self.afi != prefix.afi {
return false;
}
match &self.choice {
IpAddressChoice::Inherit => true,
IpAddressChoice::AddressesOrRanges(items) => items.iter().any(|item| match item {
IpAddressOrRange::Prefix(p) => prefix_covers(p, prefix),
IpAddressOrRange::Range(r) => range_covers_prefix(self.afi, r, prefix),
}),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum IpAddressChoice {
Inherit,
AddressesOrRanges(Vec<IpAddressOrRange>),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum IpAddressOrRange {
Prefix(IpPrefix),
Range(IpAddressRange),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IpAddressRange {
pub min: Vec<u8>,
pub max: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct IpPrefix {
pub afi: Afi,
pub prefix_len: u16,
pub addr: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AsResourceSet {
pub asnum: Option<AsIdentifierChoice>,
pub rdi: Option<AsIdentifierChoice>,
}
impl AsResourceSet {
/// Decode the DER bytes carried inside the X.509 `extnValue` OCTET STRING for
/// `id-pe-autonomousSysIds` (RFC 3779 / RFC 6487).
pub fn decode_extn_value(extn_value: &[u8]) -> Result<Self, AsResourceSetDecodeError> {
parse_as_identifiers(extn_value).map_err(|_| AsResourceSetDecodeError::InvalidEncoding)
}
pub fn is_asnum_inherit(&self) -> bool {
matches!(self.asnum, Some(AsIdentifierChoice::Inherit))
}
pub fn has_any_range(&self) -> bool {
self.asnum.as_ref().map(|c| c.has_range()).unwrap_or(false)
|| self.rdi.as_ref().map(|c| c.has_range()).unwrap_or(false)
}
pub fn asnum_single_id(&self) -> Option<u32> {
match self.asnum.as_ref()? {
AsIdentifierChoice::Inherit => None,
AsIdentifierChoice::AsIdsOrRanges(items) => {
if items.len() != 1 {
return None;
}
match &items[0] {
AsIdOrRange::Id(v) => Some(*v),
AsIdOrRange::Range { .. } => None,
}
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum AsResourceSetDecodeError {
#[error("invalid autonomousSysIds encoding (RFC 3779 §3.2.3; RFC 6487 §4.8.11)")]
InvalidEncoding,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AsIdentifierChoice {
Inherit,
AsIdsOrRanges(Vec<AsIdOrRange>),
}
impl AsIdentifierChoice {
pub fn has_range(&self) -> bool {
match self {
AsIdentifierChoice::Inherit => false,
AsIdentifierChoice::AsIdsOrRanges(items) => {
items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. }))
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AsIdOrRange {
Id(u32),
Range { min: u32, max: u32 },
}
#[derive(Debug, thiserror::Error)]
pub enum ResourceCertificateParseError {
#[error("X.509 parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
Parse(String),
#[error("trailing bytes after certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
TrailingBytes(usize),
#[error("invalid RFC 3779 IP resources extension encoding (RFC 6487 §4.8.10; RFC 3779 §2.2)")]
InvalidIpResourcesEncoding,
#[error("invalid RFC 3779 AS resources extension encoding (RFC 6487 §4.8.11; RFC 3779 §3.2)")]
InvalidAsResourcesEncoding,
}
#[derive(Debug, thiserror::Error)]
pub enum ResourceCertificateProfileError {
#[error("{0}")]
InvalidTimeEncoding(#[from] InvalidTimeEncodingError),
#[error("certificate version must be v3 (RFC 5280 §4.1; RFC 6487 §4)")]
InvalidVersion,
#[error("signatureAlgorithm does not match tbsCertificate.signature (RFC 5280 §4.1)")]
SignatureAlgorithmMismatch,
#[error(
"unsupported signature algorithm (expected sha256WithRSAEncryption {OID_SHA256_WITH_RSA_ENCRYPTION}) (RFC 7935 §2; RFC 6487 §4)"
)]
UnsupportedSignatureAlgorithm,
#[error("invalid signature algorithm parameters (RFC 5280 §4.1.1.2)")]
InvalidSignatureAlgorithmParameters,
#[error(
"{role} Name strict validation failed: {detail} (RFC 6487 §4.4; RFC 5280 §4.1.2.4/§4.1.2.6)"
)]
StrictName { role: &'static str, detail: String },
#[error("duplicate extension: {0} (RFC 5280 §4.2; RFC 6487 §4.8)")]
DuplicateExtension(&'static str),
#[error("SubjectKeyIdentifier criticality must be non-critical (RFC 6487 §4.8.2)")]
SkiCriticality,
#[error("SubjectInfoAccess criticality must be non-critical (RFC 6487 §4.8.8)")]
SiaCriticality,
#[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")]
CertificatePoliciesCriticality,
#[error("certificatePolicies must be present (RFC 6487 §4.8.9)")]
CertificatePoliciesMissing,
#[error(
"certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)"
)]
InvalidCertificatePolicy(String),
#[error("certificatePolicies may contain at most one CPS qualifier (RFC 6487 §4.8.9)")]
CertificatePoliciesTooManyQualifiers,
#[error(
"certificatePolicies qualifier must be id-qt-cps ({OID_QT_CPS}), got {0} (RFC 6487 §4.8.9)"
)]
CertificatePoliciesInvalidQualifier(String),
#[error("basicConstraints must be present in CA certificates (RFC 6487 §4.8.1)")]
BasicConstraintsMissing,
#[error("basicConstraints criticality must be critical in CA certificates (RFC 6487 §4.8.1)")]
BasicConstraintsCriticality,
#[error("basicConstraints cA must be TRUE in CA certificates (RFC 6487 §4.8.1)")]
BasicConstraintsCaFalse,
#[error(
"basicConstraints pathLenConstraint must be absent in CA certificates (RFC 6487 §4.8.1)"
)]
BasicConstraintsPathLenPresent,
#[error("basicConstraints must be absent in EE certificates (RFC 6487 §4.8.1)")]
BasicConstraintsEeMustOmit,
#[error("extension {oid} is not permitted for {role} resource certificates (RFC 6487 §4.8)")]
DisallowedExtension { role: &'static str, oid: String },
#[error("autonomousSysIds RDI field must be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3)")]
AsResourcesRdiPresent,
#[error(
"SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)"
)]
SignedObjectSiaNotUri,
#[error("SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")]
SignedObjectSiaNoRsync,
#[error("ipAddrBlocks criticality must be critical when present (RFC 6487 §4.8.10)")]
IpResourcesCriticality,
#[error("autonomousSysIds criticality must be critical when present (RFC 6487 §4.8.11)")]
AsResourcesCriticality,
#[error(
"authorityKeyIdentifier must be present in non-self-signed certificates (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)"
)]
AkiMissing,
#[error(
"authorityKeyIdentifier criticality must be non-critical (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)"
)]
AkiCriticality,
#[error(
"authorityKeyIdentifier authorityCertIssuer MUST NOT be present (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)"
)]
AkiAuthorityCertIssuerPresent,
#[error(
"authorityKeyIdentifier authorityCertSerialNumber MUST NOT be present (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)"
)]
AkiAuthorityCertSerialPresent,
#[error(
"self-signed certificate authorityKeyIdentifier must equal subjectKeyIdentifier when present (RFC 6487 §4.8.3)"
)]
AkiSelfSignedNotEqualSki,
#[error(
"CRLDistributionPoints must be present in non-self-signed certificates (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)"
)]
CrlDistributionPointsMissing,
#[error(
"CRLDistributionPoints criticality must be non-critical (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)"
)]
CrlDistributionPointsCriticality,
#[error("CRLDistributionPoints MUST be omitted in self-signed certificates (RFC 6487 §4.8.6)")]
CrlDistributionPointsSelfSignedMustOmit,
#[error("CRLDistributionPoints must contain exactly one DistributionPoint (RFC 6487 §4.8.6)")]
CrlDistributionPointsNotSingle,
#[error("CRLDistributionPoints distributionPoint field MUST be present (RFC 6487 §4.8.6)")]
CrlDistributionPointsNoDistributionPoint,
#[error("CRLDistributionPoints reasons field MUST be omitted (RFC 6487 §4.8.6)")]
CrlDistributionPointsHasReasons,
#[error("CRLDistributionPoints cRLIssuer field MUST be omitted (RFC 6487 §4.8.6)")]
CrlDistributionPointsHasCrlIssuer,
#[error(
"CRLDistributionPoints distributionPoint MUST contain fullName and MUST NOT contain nameRelativeToCRLIssuer (RFC 6487 §4.8.6)"
)]
CrlDistributionPointsInvalidName,
#[error(
"CRLDistributionPoints fullName must contain only URI GeneralNames (RFC 6487 §4.8.6; RFC 5280 §4.2.1.6)"
)]
CrlDistributionPointsFullNameNotUri,
#[error("CRLDistributionPoints must include at least one rsync:// URI (RFC 6487 §4.8.6)")]
CrlDistributionPointsNoRsync,
#[error(
"authorityInfoAccess must be present in non-self-signed certificates (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)"
)]
AuthorityInfoAccessMissing,
#[error(
"authorityInfoAccess criticality must be non-critical (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)"
)]
AuthorityInfoAccessCriticality,
#[error("authorityInfoAccess MUST be omitted in self-signed certificates (RFC 6487 §4.8.7)")]
AuthorityInfoAccessSelfSignedMustOmit,
#[error(
"authorityInfoAccess id-ad-caIssuers accessLocation must be URI (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)"
)]
AuthorityInfoAccessCaIssuersNotUri,
#[error("authorityInfoAccess must include at least one id-ad-caIssuers URI (RFC 6487 §4.8.7)")]
AuthorityInfoAccessMissingCaIssuers,
#[error("authorityInfoAccess must include at least one rsync:// URI (RFC 6487 §4.8.7)")]
AuthorityInfoAccessNoRsync,
}
#[derive(Debug, thiserror::Error)]
pub enum ResourceCertificateDecodeError {
#[error("{0}")]
Parse(#[from] ResourceCertificateParseError),
#[error("{0}")]
Validate(#[from] ResourceCertificateProfileError),
}
pub type ResourceCertificateError = ResourceCertificateDecodeError;

631
src/model/roa.rs Normal file
View File

@@ -0,0 +1,631 @@
use crate::model::common::{DerReader, der_take_tlv};
use crate::model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ;
use crate::model::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate};
use crate::model::signed_object::{
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, SignedObjectParseError,
SignedObjectValidateError,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaObject {
pub signed_object: RpkiSignedObject,
pub econtent_type: String,
pub roa: RoaEContent,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaObjectParsed {
pub signed_object: RpkiSignedObjectParsed,
pub econtent_type: String,
pub roa: Option<RoaEContentParsed>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaEContent {
pub version: u32,
pub as_id: u32,
pub ip_addr_blocks: Vec<RoaIpAddressFamily>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaEContentParsed {
der: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum RoaParseError {
#[error("signed object parse error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
SignedObject(#[from] SignedObjectParseError),
#[error("ROA parse error: {0} (RFC 9582 §4; DER)")]
Parse(String),
#[error("ROA trailing bytes: {0} bytes (RFC 9582 §4; DER)")]
TrailingBytes(usize),
}
#[derive(Debug, thiserror::Error)]
pub enum RoaProfileError {
#[error("signed object profile error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
SignedObject(#[from] SignedObjectValidateError),
#[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")]
InvalidEContentType(String),
#[error("ROA profile decode error: {0} (RFC 9582 §4; DER)")]
ProfileDecode(String),
#[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0} (RFC 9582 §4)")]
InvalidAttestationSequenceLen(usize),
#[error("ROA version must be 0, got {0} (RFC 9582 §4.1)")]
InvalidVersion(u64),
#[error("ROA asID out of range (0..=4294967295), got {0} (RFC 9582 §4.2)")]
AsIdOutOfRange(u64),
#[error("ROA ipAddrBlocks must have length 1..2, got {0} (RFC 9582 §4; RFC 9582 §4.3.1)")]
InvalidIpAddrBlocksLen(usize),
#[error("ROAIPAddressFamily must be a SEQUENCE of 2 elements (RFC 9582 §4.3.1)")]
InvalidIpAddressFamily,
#[error("ROA addressFamily must be an OCTET STRING of 2 bytes (RFC 9582 §4.3.1)")]
InvalidAddressFamily,
#[error("ROA addressFamily AFI not supported: {0:02X?} (RFC 9582 §4.3.1)")]
UnsupportedAfi(Vec<u8>),
#[error("ROA contains duplicate AFI {0:?} (RFC 9582 §4.3.1)")]
DuplicateAfi(RoaAfi),
#[error("ROAAddresses must have at least one entry (RFC 9582 §4.3.2)")]
EmptyAddressList,
#[error("ROAIPAddress must be a SEQUENCE of 1..2 elements (RFC 9582 §4.3.2)")]
InvalidRoaIpAddress,
#[error("ROAIPAddress.address must be a BIT STRING (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)")]
InvalidPrefixBitString,
#[error(
"ROAIPAddress.address has invalid unused bits encoding (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)"
)]
InvalidPrefixUnusedBits,
#[error(
"ROAIPAddress.address prefix length {prefix_len} out of range for {afi:?} (RFC 9582 §4.3.2.1)"
)]
PrefixLenOutOfRange { afi: RoaAfi, prefix_len: u16 },
#[error(
"ROAIPAddress.maxLength out of range for {afi:?}: prefix_len={prefix_len}, max_len={max_len} (RFC 9582 §4.3.2.2)"
)]
InvalidMaxLength {
afi: RoaAfi,
prefix_len: u16,
max_len: u16,
},
}
impl From<SignedObjectDecodeError> for RoaProfileError {
fn from(value: SignedObjectDecodeError) -> Self {
match value {
SignedObjectDecodeError::Parse(e) => RoaProfileError::ProfileDecode(e.to_string()),
SignedObjectDecodeError::Validate(e) => RoaProfileError::SignedObject(e),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum RoaDecodeError {
#[error("{0}")]
Parse(#[from] RoaParseError),
#[error("{0}")]
Validate(#[from] RoaProfileError),
}
#[derive(Debug, thiserror::Error)]
pub enum RoaValidateError {
#[error("ROA EE certificate must not contain AS resources extension (RFC 9582 §5)")]
EeAsResourcesPresent,
#[error("ROA EE certificate must contain IP resources extension (RFC 9582 §5)")]
EeIpResourcesMissing,
#[error("ROA EE certificate IP resources must not use inherit (RFC 9582 §5)")]
EeIpResourcesInherit,
#[error(
"ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len} (RFC 9582 §5; RFC 3779 §2.3)"
)]
PrefixNotInEeResources {
afi: RoaAfi,
addr: Vec<u8>,
prefix_len: u16,
},
}
impl RoaObject {
/// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(der: &[u8]) -> Result<RoaObjectParsed, RoaParseError> {
let signed_object = RpkiSignedObject::parse_der(der)?;
let econtent_type = signed_object
.signed_data
.encap_content_info
.econtent_type
.clone();
let roa = signed_object
.signed_data
.encap_content_info
.econtent
.as_deref()
.map(RoaEContent::parse_der)
.transpose()?;
Ok(RoaObjectParsed {
signed_object,
econtent_type,
roa,
})
}
/// Profile validate step of scheme A (`parse → validate → verify`).
///
/// `RoaObject` is already profile-validated when constructed via `decode_der()` /
/// `RoaObjectParsed::validate_profile()`.
pub fn validate_profile(&self) -> Result<(), RoaProfileError> {
Ok(())
}
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn decode_der_with_strict_options(
der: &[u8],
strict_cms_der: bool,
strict_name: bool,
) -> Result<Self, RoaDecodeError> {
let signed_object =
RpkiSignedObject::decode_der_with_strict_options(der, strict_cms_der, strict_name)
.map_err(RoaProfileError::from)?;
Self::from_signed_object(signed_object)
}
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, RoaDecodeError> {
let econtent_type = signed_object
.signed_data
.encap_content_info
.econtent_type
.clone();
if econtent_type != OID_CT_ROUTE_ORIGIN_AUTHZ {
return Err(RoaProfileError::InvalidEContentType(econtent_type).into());
}
let roa = RoaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
Ok(Self {
roa,
signed_object,
econtent_type: OID_CT_ROUTE_ORIGIN_AUTHZ.to_string(),
})
}
/// Validate this ROA's embedded EE certificate resources.
pub fn validate_embedded_ee_cert(&self) -> Result<(), RoaValidateError> {
let ee = &self.signed_object.signed_data.certificates[0].resource_cert;
self.roa.validate_against_ee_cert(ee)
}
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub enum RoaAfi {
Ipv4,
Ipv6,
}
impl RoaAfi {
fn ub(self) -> u16 {
match self {
RoaAfi::Ipv4 => 32,
RoaAfi::Ipv6 => 128,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaIpAddressFamily {
pub afi: RoaAfi,
pub addresses: Vec<RoaIpAddress>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RoaIpAddress {
pub prefix: IpPrefix,
pub max_length: Option<u16>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct IpPrefix {
pub afi: RoaAfi,
/// Prefix length in bits.
pub prefix_len: u16,
/// Network order address bytes (always 16 bytes), with host bits cleared.
///
/// For IPv4 prefixes, only the first 4 bytes are used and the remaining 12 bytes are zero.
pub addr: [u8; 16],
}
impl IpPrefix {
pub fn addr_bytes(&self) -> &[u8] {
match self.afi {
RoaAfi::Ipv4 => &self.addr[..4],
RoaAfi::Ipv6 => &self.addr[..16],
}
}
}
impl RoaEContent {
/// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(der: &[u8]) -> Result<RoaEContentParsed, RoaParseError> {
let (_tag, _value, rem) = der_take_tlv(der).map_err(RoaParseError::Parse)?;
if !rem.is_empty() {
return Err(RoaParseError::TrailingBytes(rem.len()));
}
Ok(RoaEContentParsed { der: der.to_vec() })
}
/// Profile validate step of scheme A (`parse → validate → verify`).
///
/// `RoaEContent` is already profile-validated when constructed via `decode_der()` /
/// `RoaEContentParsed::validate_profile()`.
pub fn validate_profile(&self) -> Result<(), RoaProfileError> {
Ok(())
}
/// Decode the DER-encoded RouteOriginAttestation defined in RFC 9582 §4 (`parse + validate`).
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn canonicalize(&mut self) {
self.ip_addr_blocks.sort_by_key(|f| f.afi);
for fam in &mut self.ip_addr_blocks {
fam.addresses.sort();
fam.addresses.dedup();
}
}
/// Validate ROA payload against the embedded EE resource certificate (RFC 9582 §5).
///
/// This performs the EE/payload semantic checks that do not require certificate path
/// validation.
pub fn validate_against_ee_cert(
&self,
ee: &ResourceCertificate,
) -> Result<(), RoaValidateError> {
if ee.tbs.extensions.as_resources.is_some() {
return Err(RoaValidateError::EeAsResourcesPresent);
}
let ip = ee
.tbs
.extensions
.ip_resources
.as_ref()
.ok_or(RoaValidateError::EeIpResourcesMissing)?;
if ip.has_any_inherit() {
return Err(RoaValidateError::EeIpResourcesInherit);
}
for fam in &self.ip_addr_blocks {
for entry in &fam.addresses {
let rc_prefix = roa_prefix_to_rc(&entry.prefix);
if !ip.contains_prefix(&rc_prefix) {
return Err(RoaValidateError::PrefixNotInEeResources {
afi: entry.prefix.afi,
addr: entry.prefix.addr_bytes().to_vec(),
prefix_len: entry.prefix.prefix_len,
});
}
}
}
Ok(())
}
}
impl RoaObjectParsed {
pub fn validate_profile(self) -> Result<RoaObject, RoaProfileError> {
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_ROUTE_ORIGIN_AUTHZ {
return Err(RoaProfileError::InvalidEContentType(econtent_type));
}
let roa = self
.roa
.ok_or_else(|| RoaProfileError::ProfileDecode("ROA.eContent missing".into()))?
.validate_profile()?;
Ok(RoaObject {
signed_object,
econtent_type: OID_CT_ROUTE_ORIGIN_AUTHZ.to_string(),
roa,
})
}
}
impl RoaEContentParsed {
pub fn validate_profile(self) -> Result<RoaEContent, RoaProfileError> {
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(RoaProfileError::ProfileDecode)?;
if !r.is_empty() {
return Err(RoaProfileError::ProfileDecode(
"trailing bytes after RouteOriginAttestation".into(),
));
}
let elem_count =
count_elements(seq).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if elem_count != 2 && elem_count != 3 {
return Err(RoaProfileError::InvalidAttestationSequenceLen(elem_count));
}
let mut version: u32 = 0;
if elem_count == 3 {
if seq
.peek_tag()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?
!= 0xA0
{
return Err(RoaProfileError::ProfileDecode(
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
));
}
let (inner_tag, inner_val) = seq
.take_explicit(0xA0)
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if inner_tag != 0x02 {
return Err(RoaProfileError::ProfileDecode(
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
));
}
let v = crate::model::common::der_uint_from_bytes(inner_val)
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if v != 0 {
return Err(RoaProfileError::InvalidVersion(v));
}
version = 0;
}
let as_id_u64 = seq
.take_uint_u64()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if as_id_u64 > u32::MAX as u64 {
return Err(RoaProfileError::AsIdOutOfRange(as_id_u64));
}
let as_id = as_id_u64 as u32;
let ip_addr_blocks = parse_ip_addr_blocks_cursor(
seq.take_sequence()
.map_err(|e| RoaProfileError::ProfileDecode(format!("ipAddrBlocks: {e}")))?,
)?;
if !seq.is_empty() {
// Extra elements beyond the expected 2..3.
let extra =
count_elements(seq).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
return Err(RoaProfileError::InvalidAttestationSequenceLen(
elem_count + extra,
));
}
let mut out = RoaEContent {
version,
as_id,
ip_addr_blocks,
};
out.canonicalize();
Ok(out)
}
}
fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix {
let afi = match p.afi {
RoaAfi::Ipv4 => RcAfi::Ipv4,
RoaAfi::Ipv6 => RcAfi::Ipv6,
};
RcIpPrefix {
afi,
prefix_len: p.prefix_len,
addr: p.addr_bytes().to_vec(),
}
}
fn parse_ip_addr_blocks_cursor(
mut seq: DerReader<'_>,
) -> Result<Vec<RoaIpAddressFamily>, RoaProfileError> {
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 fam_count =
count_elements(seq).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if fam_count == 0 || fam_count > 2 {
return Err(RoaProfileError::InvalidIpAddrBlocksLen(fam_count));
}
let mut out: Vec<RoaIpAddressFamily> = Vec::with_capacity(fam_count);
while !seq.is_empty() {
let family = parse_ip_address_family_cursor(
seq.take_sequence()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?,
)?;
if out.iter().any(|f| f.afi == family.afi) {
return Err(RoaProfileError::DuplicateAfi(family.afi));
}
out.push(family);
}
Ok(out)
}
fn parse_ip_address_family_cursor(
mut fam: DerReader<'_>,
) -> Result<RoaIpAddressFamily, RoaProfileError> {
let afi = {
let bytes = fam
.take_octet_string()
.map_err(|_e| RoaProfileError::InvalidAddressFamily)?;
if bytes.len() != 2 {
return Err(RoaProfileError::InvalidAddressFamily);
}
match bytes {
[0x00, 0x01] => RoaAfi::Ipv4,
[0x00, 0x02] => RoaAfi::Ipv6,
_ => return Err(RoaProfileError::UnsupportedAfi(bytes.to_vec())),
}
};
let mut addrs = fam
.take_sequence()
.map_err(|_e| RoaProfileError::InvalidIpAddressFamily)?;
if !fam.is_empty() {
return Err(RoaProfileError::InvalidIpAddressFamily);
}
if addrs.is_empty() {
return Err(RoaProfileError::EmptyAddressList);
}
let mut addresses: Vec<RoaIpAddress> = Vec::new();
while !addrs.is_empty() {
let entry = addrs
.take_sequence()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
addresses.push(parse_roa_ip_address_cursor(afi, entry)?);
}
Ok(RoaIpAddressFamily { afi, addresses })
}
fn parse_roa_ip_address_cursor(
afi: RoaAfi,
mut seq: DerReader<'_>,
) -> Result<RoaIpAddress, RoaProfileError> {
if seq.is_empty() {
return Err(RoaProfileError::InvalidRoaIpAddress);
}
let (unused_bits, bytes) = seq
.take_bit_string()
.map_err(|_e| RoaProfileError::InvalidPrefixBitString)?;
let prefix = parse_prefix_bits_bytes(afi, unused_bits, bytes)?;
let max_length = if !seq.is_empty() {
let v = seq
.take_uint_u64()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
let max_len: u16 = v
.try_into()
.map_err(|_e| RoaProfileError::InvalidMaxLength {
afi,
prefix_len: prefix.prefix_len,
max_len: u16::MAX,
})?;
Some(max_len)
} else {
None
};
if !seq.is_empty() {
return Err(RoaProfileError::InvalidRoaIpAddress);
}
if let Some(max_len) = max_length {
let ub = afi.ub();
if max_len > ub || max_len < prefix.prefix_len {
return Err(RoaProfileError::InvalidMaxLength {
afi,
prefix_len: prefix.prefix_len,
max_len,
});
}
}
Ok(RoaIpAddress { prefix, max_length })
}
fn parse_prefix_bits_bytes(
afi: RoaAfi,
unused_bits: u8,
bytes: &[u8],
) -> Result<IpPrefix, RoaProfileError> {
if unused_bits > 7 {
return Err(RoaProfileError::InvalidPrefixUnusedBits);
}
if bytes.is_empty() {
if unused_bits != 0 {
return Err(RoaProfileError::InvalidPrefixUnusedBits);
}
} else if unused_bits != 0 {
let mask = (1u8 << unused_bits) - 1;
if (bytes[bytes.len() - 1] & mask) != 0 {
return Err(RoaProfileError::InvalidPrefixUnusedBits);
}
}
let prefix_len = (bytes.len() * 8)
.checked_sub(unused_bits as usize)
.ok_or(RoaProfileError::InvalidPrefixUnusedBits)? as u16;
if prefix_len > afi.ub() {
return Err(RoaProfileError::PrefixLenOutOfRange { afi, prefix_len });
}
let addr = canonicalize_prefix_addr(afi, prefix_len, bytes);
Ok(IpPrefix {
afi,
prefix_len,
addr,
})
}
fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> [u8; 16] {
let full_len = match afi {
RoaAfi::Ipv4 => 4,
RoaAfi::Ipv6 => 16,
};
let mut addr = [0u8; 16];
let copy_len = bytes.len().min(full_len);
addr[..copy_len].copy_from_slice(&bytes[..copy_len]);
if prefix_len == 0 {
return addr;
}
let last_prefix_bit = (prefix_len - 1) as usize;
let last_prefix_byte = last_prefix_bit / 8;
let rem = (prefix_len % 8) as u8;
if rem != 0 {
let mask: u8 = 0xFF << (8 - rem);
if last_prefix_byte < full_len {
addr[last_prefix_byte] &= mask;
}
}
addr
}

365
src/model/router_cert.rs Normal file
View File

@@ -0,0 +1,365 @@
#![allow(clippy::too_many_arguments)]
use crate::model::oid::{
OID_EC_PUBLIC_KEY, OID_EXTENDED_KEY_USAGE_RAW, OID_KP_BGPSEC_ROUTER, OID_SECP256R1,
};
use crate::model::rc::{
AsIdOrRange, AsIdentifierChoice, ResourceCertKind, ResourceCertificate,
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
ResourceCertificateRole,
};
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_predecoded_ee};
use x509_parser::extensions::ParsedExtension;
use x509_parser::prelude::{FromDer, X509Certificate};
use x509_parser::public_key::PublicKey;
use x509_parser::x509::SubjectPublicKeyInfo;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BgpsecRouterCertificateParsed {
pub rc_parsed: ResourceCertificateParsed,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BgpsecRouterCertificate {
pub raw_der: Vec<u8>,
pub resource_cert: ResourceCertificate,
pub subject_key_identifier: Vec<u8>,
pub spki_der: Vec<u8>,
pub asns: Vec<u32>,
}
#[derive(Debug, thiserror::Error)]
pub enum BgpsecRouterCertificateParseError {
#[error("resource certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8209 §3.1)")]
ResourceCertificate(#[from] ResourceCertificateParseError),
#[error("X.509 parse error: {0} (RFC 5280 §4.1; RFC 8209 §3.1)")]
X509(String),
#[error("trailing bytes after router certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
TrailingBytes(usize),
#[error("router SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7; RFC 8208 §3.1)")]
SpkiParse(String),
#[error("trailing bytes after router SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 8208 §3.1)")]
SpkiTrailingBytes(usize),
}
#[derive(Debug, thiserror::Error)]
pub enum BgpsecRouterCertificateProfileError {
#[error("resource certificate profile error: {0} (RFC 6487 §4; RFC 8209 §3.1)")]
ResourceCertificate(#[from] ResourceCertificateProfileError),
#[error("BGPsec router certificate must be an EE certificate (RFC 8209 §3.1)")]
NotEe,
#[error(
"BGPsec router certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8209 §3.3)"
)]
MissingSki,
#[error(
"BGPsec router certificate must include ExtendedKeyUsage (RFC 8209 §3.1.3.2; RFC 8209 §3.3)"
)]
MissingExtendedKeyUsage,
#[error(
"BGPsec router certificate ExtendedKeyUsage must be non-critical (RFC 6487 §4.8.4; RFC 8209 §3.1.3.2)"
)]
ExtendedKeyUsageCriticality,
#[error(
"BGPsec router certificate ExtendedKeyUsage must contain id-kp-bgpsec-router ({OID_KP_BGPSEC_ROUTER}) (RFC 8209 §3.1.3.2; RFC 8209 §3.3)"
)]
MissingBgpsecRouterEku,
#[error(
"BGPsec router certificate MUST NOT include Subject Information Access (RFC 8209 §3.1.3.3; RFC 8209 §3.3)"
)]
SubjectInfoAccessPresent,
#[error(
"BGPsec router certificate MUST NOT include IP resources extension (RFC 8209 §3.1.3.4; RFC 8209 §3.3)"
)]
IpResourcesPresent,
#[error(
"BGPsec router certificate MUST include AS resources extension (RFC 8209 §3.1.3.5; RFC 8209 §3.3)"
)]
AsResourcesMissing,
#[error(
"BGPsec router certificate AS resources MUST include one or more ASNs (RFC 8209 §3.1.3.5)"
)]
AsResourcesAsnumMissing,
#[error("BGPsec router certificate AS resources MUST NOT use inherit (RFC 8209 §3.1.3.5)")]
AsResourcesInherit,
#[error(
"BGPsec router certificate AS resources MUST contain explicit ASNs, not ranges (RFC 8209 §3.1.3.5)"
)]
AsResourcesRangeNotAllowed,
#[error(
"BGPsec router certificate subjectPublicKeyInfo.algorithm must be id-ecPublicKey ({OID_EC_PUBLIC_KEY}) (RFC 8208 §3.1)"
)]
SpkiAlgorithmNotEcPublicKey,
#[error(
"BGPsec router certificate subjectPublicKeyInfo.parameters must be secp256r1 ({OID_SECP256R1}) (RFC 8208 §3.1)"
)]
SpkiWrongCurve,
#[error(
"BGPsec router certificate subjectPublicKeyInfo.parameters missing or invalid (RFC 8208 §3.1)"
)]
SpkiParametersMissingOrInvalid,
#[error(
"BGPsec router certificate subjectPublicKey MUST be uncompressed P-256 ECPoint (RFC 8208 §3.1)"
)]
SpkiEcPointNotUncompressedP256,
}
#[derive(Debug, thiserror::Error)]
pub enum BgpsecRouterCertificateDecodeError {
#[error("{0}")]
Parse(#[from] BgpsecRouterCertificateParseError),
#[error("{0}")]
Validate(#[from] BgpsecRouterCertificateProfileError),
}
#[derive(Debug, thiserror::Error)]
pub enum BgpsecRouterCertificatePathError {
#[error("{0}")]
Decode(#[from] BgpsecRouterCertificateDecodeError),
#[error("{0}")]
CertPath(#[from] CertPathError),
}
impl BgpsecRouterCertificate {
pub fn parse_der(
der: &[u8],
) -> Result<BgpsecRouterCertificateParsed, BgpsecRouterCertificateParseError> {
let (rem, cert) = X509Certificate::from_der(der)
.map_err(|e| BgpsecRouterCertificateParseError::X509(e.to_string()))?;
if !rem.is_empty() {
return Err(BgpsecRouterCertificateParseError::TrailingBytes(rem.len()));
}
let (spki_rem, _spki) =
SubjectPublicKeyInfo::from_der(cert.tbs_certificate.subject_pki.raw)
.map_err(|e| BgpsecRouterCertificateParseError::SpkiParse(e.to_string()))?;
if !spki_rem.is_empty() {
return Err(BgpsecRouterCertificateParseError::SpkiTrailingBytes(
spki_rem.len(),
));
}
let rc_parsed = ResourceCertificate::parse_der(der)?;
Ok(BgpsecRouterCertificateParsed { rc_parsed })
}
pub fn validate_profile(&self) -> Result<(), BgpsecRouterCertificateProfileError> {
Ok(())
}
pub fn decode_der(der: &[u8]) -> Result<Self, BgpsecRouterCertificateDecodeError> {
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn from_der(der: &[u8]) -> Result<Self, BgpsecRouterCertificateDecodeError> {
Self::decode_der(der)
}
pub fn validate_path_with_prevalidated_issuer(
der: &[u8],
issuer_ca: &ResourceCertificate,
issuer_spki: &SubjectPublicKeyInfo<'_>,
issuer_crl: &crate::model::crl::RpkixCrl,
issuer_crl_revoked_serials: &std::collections::HashSet<Vec<u8>>,
issuer_ca_rsync_uri: Option<&str>,
issuer_crl_rsync_uri: Option<&str>,
validation_time: time::OffsetDateTime,
) -> Result<Self, BgpsecRouterCertificatePathError> {
let cert = Self::decode_der(der)?;
validate_ee_cert_path_with_predecoded_ee(
&cert.resource_cert,
der,
issuer_ca,
issuer_spki,
issuer_crl,
issuer_crl_revoked_serials,
issuer_ca_rsync_uri,
issuer_crl_rsync_uri,
validation_time,
)?;
Ok(cert)
}
}
impl BgpsecRouterCertificateParsed {
pub fn validate_profile(
self,
) -> Result<BgpsecRouterCertificate, BgpsecRouterCertificateProfileError> {
let rc = self.rc_parsed.validate_profile()?;
rc.validate_rfc6487_profile(ResourceCertificateRole::RouterEe)?;
if rc.kind != ResourceCertKind::Ee {
return Err(BgpsecRouterCertificateProfileError::NotEe);
}
let ski = rc
.tbs
.extensions
.subject_key_identifier
.clone()
.ok_or(BgpsecRouterCertificateProfileError::MissingSki)?;
if rc.tbs.extensions.subject_info_access.is_some() {
return Err(BgpsecRouterCertificateProfileError::SubjectInfoAccessPresent);
}
if rc.tbs.extensions.ip_resources.is_some() {
return Err(BgpsecRouterCertificateProfileError::IpResourcesPresent);
}
let as_resources = rc
.tbs
.extensions
.as_resources
.as_ref()
.ok_or(BgpsecRouterCertificateProfileError::AsResourcesMissing)?;
let asns = extract_router_asns(as_resources)?;
let (rem, cert) = X509Certificate::from_der(&rc.raw_der).map_err(|e| {
BgpsecRouterCertificateProfileError::ResourceCertificate(
ResourceCertificateProfileError::InvalidCertificatePolicy(e.to_string()),
)
})?;
if !rem.is_empty() {
return Err(BgpsecRouterCertificateProfileError::ResourceCertificate(
ResourceCertificateProfileError::InvalidCertificatePolicy(format!(
"trailing bytes after router certificate DER: {}",
rem.len()
)),
));
}
validate_router_eku(&cert)?;
validate_router_spki(&rc.tbs.subject_public_key_info)?;
Ok(BgpsecRouterCertificate {
raw_der: rc.raw_der.clone(),
resource_cert: rc.clone(),
subject_key_identifier: ski,
spki_der: rc.tbs.subject_public_key_info.clone(),
asns,
})
}
}
fn extract_router_asns(
as_resources: &crate::model::rc::AsResourceSet,
) -> Result<Vec<u32>, BgpsecRouterCertificateProfileError> {
let asnum = as_resources
.asnum
.as_ref()
.ok_or(BgpsecRouterCertificateProfileError::AsResourcesAsnumMissing)?;
if matches!(asnum, AsIdentifierChoice::Inherit)
|| matches!(as_resources.rdi.as_ref(), Some(AsIdentifierChoice::Inherit))
{
return Err(BgpsecRouterCertificateProfileError::AsResourcesInherit);
}
let AsIdentifierChoice::AsIdsOrRanges(items) = asnum else {
return Err(BgpsecRouterCertificateProfileError::AsResourcesInherit);
};
if items.is_empty() {
return Err(BgpsecRouterCertificateProfileError::AsResourcesAsnumMissing);
}
let mut asns = Vec::with_capacity(items.len());
for item in items {
match item {
AsIdOrRange::Id(v) => asns.push(*v),
AsIdOrRange::Range { .. } => {
return Err(BgpsecRouterCertificateProfileError::AsResourcesRangeNotAllowed);
}
}
}
asns.sort_unstable();
asns.dedup();
Ok(asns)
}
fn validate_router_eku(
cert: &X509Certificate<'_>,
) -> Result<(), BgpsecRouterCertificateProfileError> {
let mut matches = cert
.tbs_certificate
.extensions()
.iter()
.filter(|ext| ext.oid.as_bytes() == OID_EXTENDED_KEY_USAGE_RAW);
let Some(ext) = matches.next() else {
return Err(BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage);
};
if matches.next().is_some() {
return Err(BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage);
}
if ext.critical {
return Err(BgpsecRouterCertificateProfileError::ExtendedKeyUsageCriticality);
}
let ParsedExtension::ExtendedKeyUsage(eku) = ext.parsed_extension() else {
return Err(BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage);
};
let found = eku
.other
.iter()
.any(|oid| oid.to_id_string() == OID_KP_BGPSEC_ROUTER);
if !found {
return Err(BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku);
}
Ok(())
}
fn validate_router_spki(spki_der: &[u8]) -> Result<(), BgpsecRouterCertificateProfileError> {
let (rem, spki) = SubjectPublicKeyInfo::from_der(spki_der)
.map_err(|_| BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid)?;
if !rem.is_empty() {
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
}
if spki.algorithm.algorithm.to_id_string() != OID_EC_PUBLIC_KEY {
return Err(BgpsecRouterCertificateProfileError::SpkiAlgorithmNotEcPublicKey);
}
let Some(params) = spki.algorithm.parameters.as_ref() else {
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
};
if params.header.tag().0 != 0x06 {
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
}
let mut der = Vec::with_capacity(params.data.len() + 2);
der.push(0x06);
if params.data.len() >= 0x80 {
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
}
der.push(params.data.len() as u8);
der.extend_from_slice(params.data);
let (prem, oid) = der_parser::der::parse_der_oid(&der)
.map_err(|_| BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid)?;
if !prem.is_empty() {
return Err(BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid);
}
let curve = oid
.as_oid_val()
.map_err(|_| BgpsecRouterCertificateProfileError::SpkiParametersMissingOrInvalid)?
.to_string();
if curve != OID_SECP256R1 {
return Err(BgpsecRouterCertificateProfileError::SpkiWrongCurve);
}
let parsed = spki
.parsed()
.map_err(|_| BgpsecRouterCertificateProfileError::SpkiEcPointNotUncompressedP256)?;
let PublicKey::EC(ec) = parsed else {
return Err(BgpsecRouterCertificateProfileError::SpkiAlgorithmNotEcPublicKey);
};
if ec.data().len() != 65 || ec.data().first() != Some(&0x04) {
return Err(BgpsecRouterCertificateProfileError::SpkiEcPointNotUncompressedP256);
}
Ok(())
}

View File

@@ -0,0 +1,27 @@
use crate::model::common::{Asn1TimeEncoding, Asn1TimeUtc, DerReader, der_uint_from_bytes};
use crate::model::oid::{
OID_AD_SIGNED_OBJECT, OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_CONTENT_TYPE_RAW,
OID_CMS_ATTR_MESSAGE_DIGEST, OID_CMS_ATTR_MESSAGE_DIGEST_RAW, OID_CMS_ATTR_SIGNING_TIME,
OID_CMS_ATTR_SIGNING_TIME_RAW, OID_CT_ASPA, OID_CT_ASPA_RAW, OID_CT_ROUTE_ORIGIN_AUTHZ,
OID_CT_ROUTE_ORIGIN_AUTHZ_RAW, OID_CT_RPKI_MANIFEST, OID_CT_RPKI_MANIFEST_RAW,
OID_KEY_USAGE_RAW, OID_RSA_ENCRYPTION, OID_RSA_ENCRYPTION_RAW, OID_SHA256, OID_SHA256_RAW,
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SIGNED_DATA,
OID_SIGNED_DATA_RAW, OID_SUBJECT_INFO_ACCESS,
};
use crate::model::rc::{ResourceCertificate, ResourceCertificateRole, SubjectInfoAccess};
use asn1_rs::{Any, Class, FromBer, FromDer as Asn1FromDer, Header, Tag};
use ring::digest;
use x509_parser::extensions::ParsedExtension;
use x509_parser::prelude::X509Certificate;
use x509_parser::public_key::PublicKey;
use x509_parser::x509::SubjectPublicKeyInfo;
include!("signed_object/types_errors.rs");
include!("signed_object/signed_object_impl.rs");
include!("signed_object/cms_reader.rs");
include!("signed_object/parsed_profile.rs");
include!("signed_object/signed_attrs.rs");
#[cfg(test)]
#[path = "signed_object/tests.rs"]
mod tests;

View File

@@ -0,0 +1,172 @@
// CMS reader and BER/DER content parsing primitives.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CmsParseMode {
BerCompatible,
DerStrict,
}
struct CmsReader<'a> {
buf: &'a [u8],
mode: CmsParseMode,
}
impl<'a> CmsReader<'a> {
fn new(buf: &'a [u8], mode: CmsParseMode) -> Self {
Self { buf, mode }
}
fn is_empty(&self) -> bool {
self.buf.is_empty()
}
fn remaining_len(&self) -> usize {
self.buf.len()
}
fn peek_tag(&self) -> Result<u8, String> {
let (_rem, any) = parse_any(self.buf, self.mode)?;
header_to_single_byte_tag(&any.header)
}
fn take_any(&mut self) -> Result<(u8, &'a [u8]), String> {
let (rem, any) = parse_any(self.buf, self.mode)?;
let tag = header_to_single_byte_tag(&any.header)?;
self.buf = rem;
Ok((tag, any.data))
}
fn take_any_full(&mut self) -> Result<(u8, &'a [u8], &'a [u8]), String> {
let (rem, any) = parse_any(self.buf, self.mode)?;
let consumed = self.buf.len() - rem.len();
let full = &self.buf[..consumed];
let tag = header_to_single_byte_tag(&any.header)?;
self.buf = rem;
Ok((tag, full, any.data))
}
fn skip_any(&mut self) -> Result<(), String> {
let _ = self.take_any()?;
Ok(())
}
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)
}
fn take_sequence(&mut self) -> Result<CmsReader<'a>, String> {
let value = self.take_tag(0x30)?;
Ok(CmsReader::new(value, self.mode))
}
fn take_octet_string(&mut self) -> Result<Vec<u8>, String> {
let (rem, any) = parse_any(self.buf, self.mode)?;
let tag = header_to_single_byte_tag(&any.header)?;
if self.mode == CmsParseMode::DerStrict && tag != 0x04 {
return Err(format!(
"unexpected tag in DER strict mode: got 0x{tag:02X}, expected 0x04"
));
}
if tag != 0x04 && tag != 0x24 {
return Err(format!("unexpected tag: got 0x{tag:02X}, expected 0x04"));
}
let octets = flatten_octet_string(any, self.mode)?;
self.buf = rem;
Ok(octets)
}
fn take_uint_u64(&mut self) -> Result<u64, String> {
let value = self.take_tag(0x02)?;
der_uint_from_bytes(value)
}
fn take_explicit_der(&mut self, expected_outer_tag: u8) -> Result<&'a [u8], String> {
let inner_der = self.take_tag(expected_outer_tag)?;
let (_tag, _value, rem) = cms_take_tlv(inner_der, self.mode)?;
if !rem.is_empty() {
return Err("trailing bytes inside EXPLICIT value".into());
}
Ok(inner_der)
}
}
fn parse_signed_object_content_info(
raw_der: &[u8],
parse_der: &[u8],
mode: CmsParseMode,
) -> Result<RpkiSignedObjectParsed, SignedObjectParseError> {
let mut r = CmsReader::new(parse_der, mode);
let mut content_info_seq = r
.take_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if !r.is_empty() {
return Err(SignedObjectParseError::TrailingBytes(r.remaining_len()));
}
let content_type = take_oid_string(&mut content_info_seq)?;
let signed_data = parse_signed_data_from_contentinfo_cursor(&mut content_info_seq)?;
if !content_info_seq.is_empty() {
return Err(SignedObjectParseError::Parse(
"ContentInfo must be a SEQUENCE of 2 elements".into(),
));
}
Ok(RpkiSignedObjectParsed {
raw_der: raw_der.to_vec(),
content_info_content_type: content_type,
signed_data,
})
}
fn parse_any<'a>(input: &'a [u8], mode: CmsParseMode) -> Result<(&'a [u8], Any<'a>), String> {
match mode {
CmsParseMode::BerCompatible => {
Any::from_ber(input).map_err(|e| format!("BER parse error: {e}"))
}
CmsParseMode::DerStrict => {
Any::from_der(input).map_err(|e| format!("DER parse error: {e}"))
}
}
}
fn header_to_single_byte_tag(header: &Header<'_>) -> Result<u8, String> {
let tag_no = header.tag().0;
if tag_no > 30 {
return Err(format!("high-tag-number form not supported: {tag_no}"));
}
Ok(((header.class() as u8) << 6)
| if header.constructed() { 0x20 } else { 0x00 }
| tag_no as u8)
}
fn cms_take_tlv(input: &[u8], mode: CmsParseMode) -> Result<(u8, &[u8], &[u8]), String> {
let (rem, any) = parse_any(input, mode)?;
let tag = header_to_single_byte_tag(&any.header)?;
Ok((tag, any.data, rem))
}
fn flatten_octet_string(any: Any<'_>, mode: CmsParseMode) -> Result<Vec<u8>, String> {
if any.class() != Class::Universal || any.tag() != Tag::OctetString {
return Err("expected OCTET STRING".into());
}
if !any.header.constructed() {
return Ok(any.data.to_vec());
}
if mode == CmsParseMode::DerStrict {
return Err("constructed OCTET STRING is not allowed in DER strict mode".into());
}
let mut out = Vec::new();
let mut input = any.data;
while !input.is_empty() {
let (rem, child) = Any::from_ber(input).map_err(|e| format!("BER parse error: {e}"))?;
out.extend(flatten_octet_string(child, mode)?);
input = rem;
}
Ok(out)
}

View File

@@ -0,0 +1,542 @@
// SignedData profile validation and EE certificate checks.
impl RpkiSignedObjectParsed {
pub fn validate_profile(self) -> Result<RpkiSignedObject, SignedObjectValidateError> {
self.validate_profile_with_strict_name(false)
}
pub fn validate_profile_with_strict_name(
self,
strict_name: bool,
) -> Result<RpkiSignedObject, SignedObjectValidateError> {
if self.content_info_content_type != OID_SIGNED_DATA {
return Err(SignedObjectValidateError::InvalidContentInfoContentType(
self.content_info_content_type,
));
}
let signed_data = validate_signed_data_profile(self.signed_data, strict_name)?;
Ok(RpkiSignedObject {
raw_der: self.raw_der,
content_info_content_type: OID_SIGNED_DATA.to_string(),
signed_data,
})
}
}
fn parse_signed_data_from_contentinfo_cursor(
seq: &mut CmsReader<'_>,
) -> Result<SignedDataParsed, SignedObjectParseError> {
let inner_der = seq.take_explicit_der(0xA0).map_err(|_e| {
SignedObjectParseError::Parse("ContentInfo.content must be [0] EXPLICIT".into())
})?;
let mut r = CmsReader::new(inner_der, seq.mode);
let signed_data_seq = r
.take_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if !r.is_empty() {
return Err(SignedObjectParseError::Parse(
"trailing bytes inside ContentInfo.content".into(),
));
}
parse_signed_data_cursor(signed_data_seq)
}
fn parse_signed_data_cursor(
mut seq: CmsReader<'_>,
) -> Result<SignedDataParsed, SignedObjectParseError> {
let version = seq
.take_uint_u64()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let digest_set_bytes = seq
.take_tag(0x31)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let mut digest_set = CmsReader::new(digest_set_bytes, seq.mode);
let mut digest_algorithms: Vec<AlgorithmIdentifierParsed> = Vec::new();
while !digest_set.is_empty() {
let alg = digest_set
.take_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let (oid, params_ok) = parse_algorithm_identifier_cursor(alg)?;
digest_algorithms.push(AlgorithmIdentifierParsed { oid, params_ok });
}
let encap_content_info = parse_encapsulated_content_info_cursor(
seq.take_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?,
)?;
let mut certificates: Option<Vec<Vec<u8>>> = None;
let mut crls_present = false;
let mut signer_infos: Option<Vec<SignerInfoParsed>> = None;
while !seq.is_empty() {
let tag = seq
.peek_tag()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
match tag {
0xA0 => {
if certificates.is_some() {
return Err(SignedObjectParseError::Parse(
"SignedData.certificates appears more than once".into(),
));
}
let content = seq
.take_tag(0xA0)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
certificates = Some(split_der_objects(content, seq.mode)?);
}
0xA1 => {
crls_present = true;
seq.skip_any()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
}
0x31 => {
if signer_infos.is_some() {
return Err(SignedObjectParseError::Parse(
"SignedData.signerInfos appears more than once".into(),
));
}
let set_bytes = seq
.take_tag(0x31)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
signer_infos = Some(parse_signer_infos_set_cursor(set_bytes, seq.mode)?);
}
_ => {
return Err(SignedObjectParseError::Parse(
"unexpected field in SignedData".into(),
));
}
}
}
let signer_infos = signer_infos
.ok_or_else(|| SignedObjectParseError::Parse("SignedData.signerInfos missing".into()))?;
Ok(SignedDataParsed {
version,
digest_algorithms,
encap_content_info,
certificates,
crls_present,
signer_infos,
})
}
fn parse_encapsulated_content_info_cursor(
mut seq: CmsReader<'_>,
) -> Result<EncapsulatedContentInfoParsed, SignedObjectParseError> {
if seq.is_empty() {
return Err(SignedObjectParseError::Parse(
"EncapsulatedContentInfo must be SEQUENCE of 1..2".into(),
));
}
let econtent_type = take_oid_string(&mut seq)?;
let econtent = if seq.is_empty() {
None
} else {
let inner_der = seq.take_explicit_der(0xA0).map_err(|_e| {
SignedObjectParseError::Parse(
"EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(),
)
})?;
let mut inner = CmsReader::new(inner_der, seq.mode);
let octets = inner
.take_octet_string()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if !inner.is_empty() {
return Err(SignedObjectParseError::Parse(
"trailing bytes inside EncapsulatedContentInfo.eContent".into(),
));
}
Some(octets)
};
if !seq.is_empty() {
return Err(SignedObjectParseError::Parse(
"EncapsulatedContentInfo must be SEQUENCE of 1..2".into(),
));
}
Ok(EncapsulatedContentInfoParsed {
econtent_type,
econtent,
})
}
fn split_der_objects(
mut input: &[u8],
mode: CmsParseMode,
) -> Result<Vec<Vec<u8>>, SignedObjectParseError> {
let mut out: Vec<Vec<u8>> = Vec::new();
while !input.is_empty() {
let (_tag, _value, rem) =
cms_take_tlv(input, mode).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let consumed = input.len() - rem.len();
out.push(input[..consumed].to_vec());
input = rem;
}
Ok(out)
}
fn parse_signer_infos_set_cursor(
set_bytes: &[u8],
mode: CmsParseMode,
) -> Result<Vec<SignerInfoParsed>, SignedObjectParseError> {
let mut set = CmsReader::new(set_bytes, mode);
let mut out: Vec<SignerInfoParsed> = Vec::new();
while !set.is_empty() {
let si = set
.take_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
out.push(parse_signer_info_cursor(si)?);
}
Ok(out)
}
fn validate_ee_certificate(
der: &[u8],
strict_name: bool,
) -> Result<ResourceEeCertificate, SignedObjectValidateError> {
let (rem, cert) = X509Certificate::from_der(der)
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;
if !rem.is_empty() {
return Err(SignedObjectValidateError::EeCertificateParse(format!(
"trailing bytes after EE certificate DER: {}",
rem.len()
)));
}
let rc = match ResourceCertificate::from_der(der) {
Ok(v) => v,
Err(e) => {
return match e {
crate::model::rc::ResourceCertificateDecodeError::Validate(
crate::model::rc::ResourceCertificateProfileError::SignedObjectSiaNotUri,
) => Err(SignedObjectValidateError::EeCertificateSignedObjectSiaNotUri),
crate::model::rc::ResourceCertificateDecodeError::Validate(
crate::model::rc::ResourceCertificateProfileError::SignedObjectSiaNoRsync,
) => Err(SignedObjectValidateError::EeCertificateSignedObjectSiaNoRsync),
_ => Err(SignedObjectValidateError::EeCertificateParse(e.to_string())),
};
}
};
rc.validate_rfc6487_profile(ResourceCertificateRole::SignedObjectEe)
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;
if strict_name {
rc.validate_strict_name_profile()
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;
}
let ski = rc
.tbs
.extensions
.subject_key_identifier
.clone()
.ok_or(SignedObjectValidateError::EeCertificateMissingSki)?;
let spki_der = rc.tbs.subject_public_key_info.clone();
let (rem, spki) = SubjectPublicKeyInfo::from_der(&spki_der)
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;
if !rem.is_empty() {
return Err(SignedObjectValidateError::EeCertificateParse(
"trailing bytes after EE SubjectPublicKeyInfo DER".to_string(),
));
}
let parsed_pk = spki.parsed().map_err(|_e| {
SignedObjectValidateError::EeCertificateParse(
"unsupported EE public key algorithm".to_string(),
)
})?;
let (rsa_public_modulus, rsa_public_exponent) = match parsed_pk {
PublicKey::RSA(rsa) => {
let modulus = strip_leading_zeros(rsa.modulus).to_vec();
let exponent = strip_leading_zeros(rsa.exponent).to_vec();
let _ = rsa.try_exponent().map_err(|_e| {
SignedObjectValidateError::EeCertificateParse("invalid EE RSA exponent".to_string())
})?;
(modulus, exponent)
}
_ => {
return Err(SignedObjectValidateError::EeCertificateParse(
"unsupported EE public key algorithm".to_string(),
));
}
};
let sia = rc
.tbs
.extensions
.subject_info_access
.as_ref()
.ok_or(SignedObjectValidateError::EeCertificateMissingSia)?;
let signed_object_uris: Vec<String> = match sia {
SubjectInfoAccess::Ee(ee) => ee.signed_object_uris.clone(),
SubjectInfoAccess::Ca(_ca) => Vec::new(),
};
if signed_object_uris.is_empty() {
return Err(SignedObjectValidateError::EeCertificateMissingSignedObjectSia);
}
if !signed_object_uris.iter().any(|u| u.starts_with("rsync://")) {
return Err(SignedObjectValidateError::EeCertificateSignedObjectSiaNoRsync);
}
Ok(ResourceEeCertificate {
raw_der: der.to_vec(),
subject_key_identifier: ski,
spki_der,
rsa_public_modulus,
rsa_public_exponent,
tbs_certificate_der: cert.tbs_certificate.as_ref().to_vec(),
signature_bytes: cert.signature_value.data.to_vec(),
key_usage_summary: summarize_ee_key_usage(&cert),
sia_signed_object_uris: signed_object_uris,
resource_cert: rc,
})
}
fn summarize_ee_key_usage(cert: &X509Certificate<'_>) -> EeKeyUsageSummary {
for ext in cert.extensions() {
if ext.oid.as_bytes() == OID_KEY_USAGE_RAW {
match ext.parsed_extension() {
ParsedExtension::KeyUsage(ku) => {
if !ext.critical {
return EeKeyUsageSummary::NotCritical;
}
let ok = ku.digital_signature()
&& !ku.key_cert_sign()
&& !ku.crl_sign()
&& !ku.non_repudiation()
&& !ku.key_encipherment()
&& !ku.data_encipherment()
&& !ku.key_agreement()
&& !ku.encipher_only()
&& !ku.decipher_only();
return if ok {
EeKeyUsageSummary::DigitalSignatureOnly
} else {
EeKeyUsageSummary::InvalidBits
};
}
other => {
return EeKeyUsageSummary::ParseError(format!(
"unexpected parsed keyUsage extension: {other:?}"
));
}
}
}
}
EeKeyUsageSummary::Missing
}
fn parse_signer_info_cursor(
mut seq: CmsReader<'_>,
) -> Result<SignerInfoParsed, SignedObjectParseError> {
let version = seq
.take_uint_u64()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let (sid_tag, sid_bytes) = seq
.take_any()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let sid = if (sid_tag & 0xC0) == 0x80 && (sid_tag & 0x1F) == 0 {
SignerIdentifierParsed::SubjectKeyIdentifier(sid_bytes.to_vec())
} else {
SignerIdentifierParsed::Other
};
let digest_alg_seq = seq
.take_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let (digest_oid, digest_params_ok) = parse_algorithm_identifier_cursor(digest_alg_seq)?;
let digest_algorithm = AlgorithmIdentifierParsed {
oid: digest_oid,
params_ok: digest_params_ok,
};
let mut signed_attrs_content: Option<Vec<u8>> = None;
let mut signed_attrs_der_for_signature: Option<Vec<u8>> = None;
if seq
.peek_tag()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?
== 0xA0
{
let (tag, full_tlv, value) = seq
.take_any_full()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
if tag != 0xA0 {
return Err(SignedObjectParseError::Parse(
"SignerInfo.signedAttrs must be [0] IMPLICIT".into(),
));
}
signed_attrs_content = Some(value.to_vec());
signed_attrs_der_for_signature = Some(make_signed_attrs_der_for_signature(full_tlv)?);
}
let sig_alg_seq = seq
.take_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let (signature_oid, signature_params_ok) = parse_algorithm_identifier_cursor(sig_alg_seq)?;
let signature_algorithm = AlgorithmIdentifierParsed {
oid: signature_oid,
params_ok: signature_params_ok,
};
let signature = seq
.take_octet_string()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?
.to_vec();
let unsigned_attrs_present = !seq.is_empty();
Ok(SignerInfoParsed {
version,
sid,
digest_algorithm,
signature_algorithm,
signed_attrs_content,
signed_attrs_der_for_signature,
unsigned_attrs_present,
signature,
})
}
fn validate_signed_data_profile(
signed_data: SignedDataParsed,
strict_name: bool,
) -> Result<SignedDataProfiled, SignedObjectValidateError> {
if signed_data.version != 3 {
return Err(SignedObjectValidateError::InvalidSignedDataVersion(
signed_data.version,
));
}
if signed_data.digest_algorithms.len() != 1 {
return Err(SignedObjectValidateError::InvalidDigestAlgorithmsCount(
signed_data.digest_algorithms.len(),
));
}
let digest_alg = &signed_data.digest_algorithms[0];
if digest_alg.oid != OID_SHA256 {
return Err(SignedObjectValidateError::InvalidDigestAlgorithm(
digest_alg.oid.clone(),
));
}
if signed_data.crls_present {
return Err(SignedObjectValidateError::CrlsPresent);
}
let econtent = signed_data
.encap_content_info
.econtent
.clone()
.ok_or(SignedObjectValidateError::EContentMissing)?;
if econtent.is_empty() {
return Err(SignedObjectValidateError::EContentMissing);
}
let encap_content_info = EncapsulatedContentInfo {
econtent_type: signed_data.encap_content_info.econtent_type.clone(),
econtent: econtent.clone(),
};
let certs = signed_data
.certificates
.as_ref()
.ok_or(SignedObjectValidateError::CertificatesMissing)?;
if certs.len() != 1 {
return Err(SignedObjectValidateError::InvalidCertificatesCount(
certs.len(),
));
}
let ee = validate_ee_certificate(&certs[0], strict_name)?;
if signed_data.signer_infos.len() != 1 {
return Err(SignedObjectValidateError::InvalidSignerInfosCount(
signed_data.signer_infos.len(),
));
}
let signer = &signed_data.signer_infos[0];
if signer.version != 3 {
return Err(SignedObjectValidateError::InvalidSignerInfoVersion(
signer.version,
));
}
let sid_ski = match &signer.sid {
SignerIdentifierParsed::SubjectKeyIdentifier(ski) => ski.clone(),
SignerIdentifierParsed::Other => {
return Err(SignedObjectValidateError::InvalidSignerIdentifier);
}
};
if signer.digest_algorithm.oid != OID_SHA256 {
return Err(SignedObjectValidateError::InvalidSignerInfoDigestAlgorithm(
signer.digest_algorithm.oid.clone(),
));
}
let signed_attrs_content = signer
.signed_attrs_content
.as_deref()
.ok_or(SignedObjectValidateError::SignedAttrsMissing)?;
let signed_attrs_der_for_signature = signer
.signed_attrs_der_for_signature
.clone()
.ok_or(SignedObjectValidateError::SignedAttrsMissing)?;
let signed_attrs = parse_signed_attrs_implicit(signed_attrs_content)?;
if signer.unsigned_attrs_present {
return Err(SignedObjectValidateError::UnsignedAttrsPresent);
}
if !signer.signature_algorithm.params_ok {
return Err(SignedObjectValidateError::InvalidSignatureAlgorithmParameters);
}
let signature_algorithm = signer.signature_algorithm.oid.clone();
if signature_algorithm != OID_RSA_ENCRYPTION
&& signature_algorithm != OID_SHA256_WITH_RSA_ENCRYPTION
{
return Err(SignedObjectValidateError::InvalidSignatureAlgorithm(
signature_algorithm,
));
}
if sid_ski != ee.subject_key_identifier {
return Err(SignedObjectValidateError::SidSkiMismatch);
}
if signed_attrs.content_type != encap_content_info.econtent_type {
return Err(SignedObjectValidateError::ContentTypeAttrMismatch {
econtent_type: encap_content_info.econtent_type.clone(),
attr_content_type: signed_attrs.content_type.clone(),
});
}
let computed = digest::digest(&digest::SHA256, &encap_content_info.econtent);
if computed.as_ref() != signed_attrs.message_digest.as_slice() {
return Err(SignedObjectValidateError::MessageDigestMismatch);
}
Ok(SignedDataProfiled {
version: 3,
digest_algorithms: vec![OID_SHA256.to_string()],
encap_content_info,
certificates: vec![ee.clone()],
crls_present: false,
signer_infos: vec![SignerInfoProfiled {
version: 3,
sid_ski,
digest_algorithm: OID_SHA256.to_string(),
signature_algorithm: signer.signature_algorithm.oid.clone(),
signed_attrs,
unsigned_attrs_present: false,
signature: signer.signature.clone(),
signed_attrs_der_for_signature,
}],
})
}

View File

@@ -0,0 +1,344 @@
// Signed attributes, signing-time, and algorithm parsing helpers.
fn parse_signed_attrs_implicit(
input: &[u8],
) -> Result<SignedAttrsProfiled, SignedObjectValidateError> {
let mut content_type: Option<String> = None;
let mut message_digest: Option<Vec<u8>> = None;
let mut signing_time: Option<Asn1TimeUtc> = None;
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 remaining = DerReader::new(input);
while !remaining.is_empty() {
let mut attr = remaining
.take_sequence()
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?;
let oid_bytes = attr
.take_tag(0x06)
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?;
let oid = oid_value_bytes_to_string(oid_bytes);
let values_bytes = attr
.take_tag(0x31)
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?;
if !attr.is_empty() {
return Err(SignedObjectValidateError::SignedAttrsParse(
"Attribute must be SEQUENCE of 2".into(),
));
}
let mut values = DerReader::new(values_bytes);
let count = if values.is_empty() {
0
} else {
values
.skip_any()
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?;
if values.is_empty() {
1
} else {
1 + count_elements(values)
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?
}
};
if count != 1 {
return Err(
SignedObjectValidateError::InvalidSignedAttributeValuesCount { oid, count },
);
}
// Re-parse the sole value.
let mut values = DerReader::new(values_bytes);
let (val_tag, val_bytes) = values
.take_any()
.map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?;
match oid.as_str() {
OID_CMS_ATTR_CONTENT_TYPE => {
if content_type.is_some() {
return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid));
}
if val_tag != 0x06 {
return Err(SignedObjectValidateError::SignedAttrsParse(
"content-type attr value must be OBJECT IDENTIFIER".into(),
));
}
content_type = Some(oid_value_bytes_to_string(val_bytes));
}
OID_CMS_ATTR_MESSAGE_DIGEST => {
if message_digest.is_some() {
return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid));
}
if val_tag != 0x04 {
return Err(SignedObjectValidateError::SignedAttrsParse(
"message-digest attr value must be OCTET STRING".into(),
));
}
message_digest = Some(val_bytes.to_vec());
}
OID_CMS_ATTR_SIGNING_TIME => {
if signing_time.is_some() {
return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid));
}
signing_time = Some(parse_signing_time_value_tlv(val_tag, val_bytes)?);
}
_ => {
return Err(SignedObjectValidateError::UnsupportedSignedAttribute(oid));
}
}
}
Ok(SignedAttrsProfiled {
content_type: content_type
.ok_or(SignedObjectValidateError::SignedAttrsContentTypeMissing)?,
message_digest: message_digest
.ok_or(SignedObjectValidateError::SignedAttrsMessageDigestMissing)?,
signing_time: signing_time
.ok_or(SignedObjectValidateError::SignedAttrsSigningTimeMissing)?,
other_attrs_present: false,
})
}
fn parse_signing_time_value_tlv(
tag: u8,
value: &[u8],
) -> Result<Asn1TimeUtc, SignedObjectValidateError> {
match tag {
0x17 => Ok(Asn1TimeUtc {
utc: parse_utctime(value)?,
encoding: Asn1TimeEncoding::UtcTime,
}),
0x18 => Ok(Asn1TimeUtc {
utc: parse_generalized_time(value)?,
encoding: Asn1TimeEncoding::GeneralizedTime,
}),
_ => Err(SignedObjectValidateError::InvalidSigningTimeValue),
}
}
fn make_signed_attrs_der_for_signature(full_tlv: &[u8]) -> Result<Vec<u8>, SignedObjectParseError> {
// We need the DER encoding of SignedAttributes (SET OF Attribute) as signature input.
// The SignedAttributes field in SignerInfo is `[0] IMPLICIT`, so the on-wire bytes start with
// a context-specific constructed tag (0xA0 for tag 0). For signature verification, this tag
// is replaced with the universal SET tag (0x31), leaving length+content unchanged.
//
let mut cs_der = full_tlv.to_vec();
if cs_der.is_empty() {
return Err(SignedObjectParseError::Parse(
"signedAttrs encoding is empty".into(),
));
}
// The first byte should be the context-specific tag (0xA0) for [0] constructed.
// Replace it with universal SET (0x31) for signature input.
cs_der[0] = 0x31;
Ok(cs_der)
}
fn take_oid_string(seq: &mut CmsReader<'_>) -> Result<String, SignedObjectParseError> {
let oid = seq
.take_tag(0x06)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
Ok(oid_value_bytes_to_string(oid))
}
fn oid_value_bytes_to_string(oid_value: &[u8]) -> String {
if oid_value == OID_SHA256_RAW {
return OID_SHA256.to_string();
}
if oid_value == OID_SIGNED_DATA_RAW {
return OID_SIGNED_DATA.to_string();
}
if oid_value == OID_CMS_ATTR_CONTENT_TYPE_RAW {
return OID_CMS_ATTR_CONTENT_TYPE.to_string();
}
if oid_value == OID_CMS_ATTR_MESSAGE_DIGEST_RAW {
return OID_CMS_ATTR_MESSAGE_DIGEST.to_string();
}
if oid_value == OID_CMS_ATTR_SIGNING_TIME_RAW {
return OID_CMS_ATTR_SIGNING_TIME.to_string();
}
if oid_value == OID_RSA_ENCRYPTION_RAW {
return OID_RSA_ENCRYPTION.to_string();
}
if oid_value == OID_SHA256_WITH_RSA_ENCRYPTION_RAW {
return OID_SHA256_WITH_RSA_ENCRYPTION.to_string();
}
if oid_value == OID_CT_RPKI_MANIFEST_RAW {
return OID_CT_RPKI_MANIFEST.to_string();
}
if oid_value == OID_CT_ROUTE_ORIGIN_AUTHZ_RAW {
return OID_CT_ROUTE_ORIGIN_AUTHZ.to_string();
}
if oid_value == OID_CT_ASPA_RAW {
return OID_CT_ASPA.to_string();
}
decode_oid_to_dotted_string(oid_value)
}
fn decode_oid_to_dotted_string(value: &[u8]) -> String {
if value.is_empty() {
return "<empty-oid>".into();
}
let first = value[0];
let a = (first / 40) as u32;
let b = (first % 40) as u32;
let mut out = String::new();
out.push_str(&a.to_string());
out.push('.');
out.push_str(&b.to_string());
let mut idx = 1usize;
while idx < value.len() {
let mut v: u32 = 0;
loop {
if idx >= value.len() {
out.push_str(".<truncated>");
return out;
}
let byte = value[idx];
idx += 1;
v = (v << 7) | (byte as u32 & 0x7F);
if (byte & 0x80) == 0 {
break;
}
}
out.push('.');
out.push_str(&v.to_string());
}
out
}
fn parse_algorithm_identifier_cursor(
mut seq: CmsReader<'_>,
) -> Result<(String, bool), SignedObjectParseError> {
if seq.is_empty() {
return Err(SignedObjectParseError::Parse(
"AlgorithmIdentifier must be SEQUENCE of 1..2".into(),
));
}
let oid = take_oid_string(&mut seq)?;
let params_ok = if seq.is_empty() {
true
} else {
let (tag, value) = seq
.take_any()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
tag == 0x05 && value.is_empty()
};
if !seq.is_empty() {
return Err(SignedObjectParseError::Parse(
"AlgorithmIdentifier must be SEQUENCE of 1..2".into(),
));
}
Ok((oid, params_ok))
}
fn parse_utctime(value: &[u8]) -> Result<time::OffsetDateTime, SignedObjectValidateError> {
let s = std::str::from_utf8(value)
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
if !s.ends_with('Z') {
return Err(SignedObjectValidateError::InvalidSigningTimeValue);
}
let digits = &s[..s.len() - 1];
if digits.len() != 10 && digits.len() != 12 {
return Err(SignedObjectValidateError::InvalidSigningTimeValue);
}
if !digits.as_bytes().iter().all(|b| b.is_ascii_digit()) {
return Err(SignedObjectValidateError::InvalidSigningTimeValue);
}
let yy: i32 = digits[0..2]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let year = if yy <= 49 { 2000 + yy } else { 1900 + yy };
let mon: u8 = digits[2..4]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let day: u8 = digits[4..6]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let hour: u8 = digits[6..8]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let min: u8 = digits[8..10]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let sec: u8 = if digits.len() == 12 {
digits[10..12]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?
} else {
0
};
let month = time::Month::try_from(mon)
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let date = time::Date::from_calendar_date(year, month, day)
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let time = time::Time::from_hms(hour, min, sec)
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
Ok(time::OffsetDateTime::new_utc(date, time))
}
fn parse_generalized_time(value: &[u8]) -> Result<time::OffsetDateTime, SignedObjectValidateError> {
let s = std::str::from_utf8(value)
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
if !s.ends_with('Z') {
return Err(SignedObjectValidateError::InvalidSigningTimeValue);
}
let digits = &s[..s.len() - 1];
if digits.len() != 12 && digits.len() != 14 {
return Err(SignedObjectValidateError::InvalidSigningTimeValue);
}
if !digits.as_bytes().iter().all(|b| b.is_ascii_digit()) {
return Err(SignedObjectValidateError::InvalidSigningTimeValue);
}
let year: i32 = digits[0..4]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let mon: u8 = digits[4..6]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let day: u8 = digits[6..8]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let hour: u8 = digits[8..10]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let min: u8 = digits[10..12]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let sec: u8 = if digits.len() == 14 {
digits[12..14]
.parse()
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?
} else {
0
};
let month = time::Month::try_from(mon)
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let date = time::Date::from_calendar_date(year, month, day)
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
let time = time::Time::from_hms(hour, min, sec)
.map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?;
Ok(time::OffsetDateTime::new_utc(date, time))
}
fn strip_leading_zeros(bytes: &[u8]) -> &[u8] {
let mut idx = 0;
while idx < bytes.len() && bytes[idx] == 0 {
idx += 1;
}
if idx == bytes.len() {
&bytes[bytes.len() - 1..]
} else {
&bytes[idx..]
}
}

View File

@@ -0,0 +1,118 @@
// Signed-object decoding and RSA signature verification API.
impl RpkiSignedObject {
/// Parse a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData).
///
/// This performs encoding/structure parsing only. Profile constraints are enforced by
/// `RpkiSignedObjectParsed::validate_profile`.
pub fn parse_der(der: &[u8]) -> Result<RpkiSignedObjectParsed, SignedObjectParseError> {
parse_signed_object_content_info(der, der, CmsParseMode::BerCompatible)
}
pub fn parse_der_strict_cms(
der: &[u8],
) -> Result<RpkiSignedObjectParsed, SignedObjectParseError> {
parse_signed_object_content_info(der, der, CmsParseMode::DerStrict)
}
/// Return the strict-DER CMS parse error for an object that was otherwise
/// accepted through the normal BER-compatible CMS parser.
///
/// Callers must only surface this as a compatibility warning after normal
/// decoding and validation have succeeded; a strict parse failure alone
/// does not prove that an arbitrary byte string is an RPKI signed object.
pub fn strict_cms_der_error(der: &[u8]) -> Option<SignedObjectParseError> {
Self::parse_der_strict_cms(der).err()
}
/// Decode a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData) and enforce
/// the profile constraints from RFC 6488 §2-§3 and RFC 9589 §4.
pub fn decode_der(der: &[u8]) -> Result<Self, SignedObjectDecodeError> {
let parsed = Self::parse_der(der)?;
Ok(parsed.validate_profile()?)
}
pub fn decode_der_with_strict_options(
der: &[u8],
strict_cms_der: bool,
strict_name: bool,
) -> Result<Self, SignedObjectDecodeError> {
let parsed = if strict_cms_der {
Self::parse_der_strict_cms(der)?
} else {
Self::parse_der(der)?
};
Ok(parsed.validate_profile_with_strict_name(strict_name)?)
}
/// Scheme-A naming for signature verification.
pub fn verify(&self) -> Result<(), SignedObjectVerifyError> {
self.verify_signature()
}
/// Verify the CMS signature using the embedded EE certificate public key.
pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> {
let ee = &self.signed_data.certificates[0];
self.verify_signature_with_rsa_components(
&ee.rsa_public_modulus,
&ee.rsa_public_exponent,
)
}
/// Verify the CMS signature using a DER-encoded SubjectPublicKeyInfo.
pub fn verify_signature_with_ee_spki_der(
&self,
ee_spki_der: &[u8],
) -> Result<(), SignedObjectVerifyError> {
let (rem, spki) = SubjectPublicKeyInfo::from_der(ee_spki_der)
.map_err(|e| SignedObjectVerifyError::EeSpkiParse(e.to_string()))?;
if !rem.is_empty() {
return Err(SignedObjectVerifyError::EeSpkiTrailingBytes(rem.len()));
}
self.verify_signature_with_ee_spki(&spki)
}
/// Verify the CMS signature using a parsed SubjectPublicKeyInfo.
pub fn verify_signature_with_ee_spki(
&self,
ee_spki: &SubjectPublicKeyInfo<'_>,
) -> Result<(), SignedObjectVerifyError> {
let pk = ee_spki
.parsed()
.map_err(|_e| SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm)?;
let (n, e) = match pk {
PublicKey::RSA(rsa) => {
let n = strip_leading_zeros(rsa.modulus).to_vec();
let e = strip_leading_zeros(rsa.exponent).to_vec();
let _exp = rsa
.try_exponent()
.map_err(|_e| SignedObjectVerifyError::InvalidEeRsaExponent)?;
(n, e)
}
_ => return Err(SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm),
};
self.verify_signature_with_rsa_components(n.as_slice(), e.as_slice())
}
fn verify_signature_with_rsa_components(
&self,
modulus: &[u8],
exponent: &[u8],
) -> Result<(), SignedObjectVerifyError> {
let signer = &self.signed_data.signer_infos[0];
let msg = &signer.signed_attrs_der_for_signature;
let pk = ring::signature::RsaPublicKeyComponents {
n: modulus,
e: exponent,
};
pk.verify(
&ring::signature::RSA_PKCS1_2048_8192_SHA256,
msg,
&signer.signature,
)
.map_err(|_e| SignedObjectVerifyError::InvalidSignature)
}
}

View File

@@ -0,0 +1,117 @@
// Signed-object CMS compatibility and profile tests.
use super::*;
#[test]
fn strict_cms_der_rejects_constructed_octet_string_fixture() {
let der = std::fs::read(
crate::test_support::synthetic_repository()
.case_repository("baseline-v1")
.join("child/child.mft"),
)
.expect("read synthetic manifest");
let parsed = RpkiSignedObject::parse_der(&der).expect("parse fixture");
let econtent = parsed
.signed_data
.encap_content_info
.econtent
.clone()
.expect("fixture eContent");
assert_eq!(
parsed.signed_data.encap_content_info.econtent.as_deref(),
Some(econtent.as_slice())
);
let primitive_octets = der_tlv(0x04, &econtent);
let constructed_octets = same_size_constructed_octet_string(&primitive_octets, &econtent);
let mutated = replace_first_subslice(&der, &primitive_octets, &constructed_octets)
.expect("replace eContent OCTET STRING");
let compatible = RpkiSignedObject::parse_der(&mutated).expect("BER-compatible parse");
assert!(
econtent.starts_with(
compatible
.signed_data
.encap_content_info
.econtent
.as_deref()
.expect("compatible eContent")
)
);
let err = RpkiSignedObject::parse_der_strict_cms(&mutated)
.expect_err("DER strict rejects constructed OCTET STRING");
assert!(err.to_string().contains("DER"), "{err}");
let compatibility_error = RpkiSignedObject::strict_cms_der_error(&mutated)
.expect("BER-compatible fixture must report strict-DER incompatibility");
assert_eq!(compatibility_error.to_string(), err.to_string());
assert!(RpkiSignedObject::strict_cms_der_error(&der).is_none());
}
fn replace_first_subslice(input: &[u8], from: &[u8], to: &[u8]) -> Option<Vec<u8>> {
let pos = input
.windows(from.len())
.position(|candidate| candidate == from)?;
let mut out = Vec::with_capacity(input.len() - from.len() + to.len());
out.extend_from_slice(&input[..pos]);
out.extend_from_slice(to);
out.extend_from_slice(&input[pos + from.len()..]);
Some(out)
}
fn same_size_constructed_octet_string(primitive: &[u8], content: &[u8]) -> Vec<u8> {
assert_eq!(primitive[0], 0x04);
let header_len = tlv_header_len(primitive);
let outer_value_len = primitive.len() - header_len;
let child_len = (0..=outer_value_len)
.rev()
.find(|candidate| 1 + len_len(*candidate) + *candidate == outer_value_len)
.expect("find child length");
let mut out = primitive[..header_len].to_vec();
out[0] = 0x24;
out.extend(der_tlv(0x04, &content[..child_len]));
assert_eq!(out.len(), primitive.len());
out
}
fn tlv_header_len(tlv: &[u8]) -> usize {
if tlv[1] & 0x80 == 0 {
2
} else {
2 + (tlv[1] & 0x7F) as usize
}
}
fn len_len(len: usize) -> usize {
if len < 0x80 {
return 1;
}
let mut value = len;
let mut n = 0usize;
while value > 0 {
n += 1;
value >>= 8;
}
1 + n
}
fn der_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
encode_len(value.len(), &mut out);
out.extend_from_slice(value);
out
}
fn encode_len(len: usize, out: &mut Vec<u8>) {
if len < 0x80 {
out.push(len as u8);
return;
}
let mut bytes = Vec::new();
let mut value = len;
while value > 0 {
bytes.push((value & 0xFF) as u8);
value >>= 8;
}
bytes.reverse();
out.push(0x80 | bytes.len() as u8);
out.extend(bytes);
}

View File

@@ -0,0 +1,300 @@
// CMS signed-object model types and parse/validation errors.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EeKeyUsageSummary {
DigitalSignatureOnly,
Missing,
NotCritical,
InvalidBits,
ParseError(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResourceEeCertificate {
pub raw_der: Vec<u8>,
pub subject_key_identifier: Vec<u8>,
pub spki_der: Vec<u8>,
pub rsa_public_modulus: Vec<u8>,
pub rsa_public_exponent: Vec<u8>,
pub tbs_certificate_der: Vec<u8>,
pub signature_bytes: Vec<u8>,
pub key_usage_summary: EeKeyUsageSummary,
pub sia_signed_object_uris: Vec<String>,
pub resource_cert: ResourceCertificate,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RpkiSignedObject {
pub raw_der: Vec<u8>,
pub content_info_content_type: String,
pub signed_data: SignedDataProfiled,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedDataProfiled {
pub version: u32,
pub digest_algorithms: Vec<String>,
pub encap_content_info: EncapsulatedContentInfo,
pub certificates: Vec<ResourceEeCertificate>,
pub crls_present: bool,
pub signer_infos: Vec<SignerInfoProfiled>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EncapsulatedContentInfo {
pub econtent_type: String,
pub econtent: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignerInfoProfiled {
pub version: u32,
pub sid_ski: Vec<u8>,
pub digest_algorithm: String,
pub signature_algorithm: String,
pub signed_attrs: SignedAttrsProfiled,
pub unsigned_attrs_present: bool,
pub signature: Vec<u8>,
pub signed_attrs_der_for_signature: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedAttrsProfiled {
pub content_type: String,
pub message_digest: Vec<u8>,
pub signing_time: Asn1TimeUtc,
pub other_attrs_present: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RpkiSignedObjectParsed {
pub raw_der: Vec<u8>,
pub content_info_content_type: String,
pub signed_data: SignedDataParsed,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedDataParsed {
pub version: u64,
pub digest_algorithms: Vec<AlgorithmIdentifierParsed>,
pub encap_content_info: EncapsulatedContentInfoParsed,
pub certificates: Option<Vec<Vec<u8>>>,
pub crls_present: bool,
pub signer_infos: Vec<SignerInfoParsed>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlgorithmIdentifierParsed {
pub oid: String,
pub params_ok: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EncapsulatedContentInfoParsed {
pub econtent_type: String,
pub econtent: Option<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignerInfoParsed {
pub version: u64,
pub sid: SignerIdentifierParsed,
pub digest_algorithm: AlgorithmIdentifierParsed,
pub signature_algorithm: AlgorithmIdentifierParsed,
pub signed_attrs_content: Option<Vec<u8>>,
pub signed_attrs_der_for_signature: Option<Vec<u8>>,
pub unsigned_attrs_present: bool,
pub signature: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SignerIdentifierParsed {
SubjectKeyIdentifier(Vec<u8>),
Other,
}
#[derive(Debug, thiserror::Error)]
pub enum SignedObjectParseError {
#[error("DER parse error: {0} (RFC 6488 §2; RFC 6488 §3(1l); RFC 5652 §3/§5)")]
Parse(String),
#[error("trailing bytes after DER object: {0} bytes (DER; RFC 6488 §3(1l))")]
TrailingBytes(usize),
}
#[derive(Debug, thiserror::Error)]
pub enum SignedObjectValidateError {
#[error(
"ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0} (RFC 6488 §3(1a); RFC 5652 §3)"
)]
InvalidContentInfoContentType(String),
#[error(
"SignedData.version must be 3, got {0} (RFC 6488 §2.1.1; RFC 6488 §3(1b); RFC 5652 §5.1)"
)]
InvalidSignedDataVersion(u64),
#[error(
"SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 5652 §5.1)"
)]
InvalidDigestAlgorithmsCount(usize),
#[error(
"digest algorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 7935 §2)"
)]
InvalidDigestAlgorithm(String),
#[error("SignedData.certificates MUST be present (RFC 6488 §3(1c); RFC 5652 §5.1)")]
CertificatesMissing,
#[error(
"SignedData.certificates must contain exactly one EE certificate, got {0} (RFC 6488 §3(1c))"
)]
InvalidCertificatesCount(usize),
#[error("SignedData.crls MUST be omitted (RFC 6488 §3(1d))")]
CrlsPresent,
#[error(
"SignedData.signerInfos must contain exactly one SignerInfo, got {0} (RFC 6488 §2.1; RFC 6488 §3(1e); RFC 5652 §5.1)"
)]
InvalidSignerInfosCount(usize),
#[error("SignerInfo.version must be 3, got {0} (RFC 6488 §3(1e); RFC 5652 §5.3)")]
InvalidSignerInfoVersion(u64),
#[error("SignerInfo.sid must be subjectKeyIdentifier [0] (RFC 6488 §3(1c); RFC 5652 §5.3)")]
InvalidSignerIdentifier,
#[error(
"SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §3(1j); RFC 7935 §2)"
)]
InvalidSignerInfoDigestAlgorithm(String),
#[error("SignerInfo.signedAttrs MUST be present (RFC 9589 §4; RFC 6488 §3(1f))")]
SignedAttrsMissing,
#[error("SignerInfo.unsignedAttrs MUST be omitted (RFC 6488 §3(1i))")]
UnsignedAttrsPresent,
#[error(
"SignerInfo.signatureAlgorithm must be rsaEncryption ({OID_RSA_ENCRYPTION}) or \
sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6488 §3(1k); RFC 7935 §2)"
)]
InvalidSignatureAlgorithm(String),
#[error(
"SignerInfo.signatureAlgorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)"
)]
InvalidSignatureAlgorithmParameters,
#[error("signedAttrs contains unsupported attribute OID {0} (RFC 9589 §4; RFC 6488 §2.1.6.4)")]
UnsupportedSignedAttribute(String),
#[error("signedAttrs contains duplicate attribute OID {0} (RFC 6488 §2.1.6.4; RFC 9589 §4)")]
DuplicateSignedAttribute(String),
#[error("signedAttrs parse error: {0} (RFC 5652 §5.3; RFC 6488 §3(1f); RFC 9589 §4)")]
SignedAttrsParse(String),
#[error(
"signedAttrs attribute {oid} attrValues must contain exactly one value, got {count} (RFC 6488 §2.1.6.4; RFC 5652 §5.3)"
)]
InvalidSignedAttributeValuesCount { oid: String, count: usize },
#[error(
"signedAttrs missing content-type attribute (RFC 9589 §4; RFC 5652 §11.1; RFC 6488 §2.1.6.4)"
)]
SignedAttrsContentTypeMissing,
#[error(
"signedAttrs missing message-digest attribute (RFC 9589 §4; RFC 5652 §11.2; RFC 6488 §2.1.6.4)"
)]
SignedAttrsMessageDigestMissing,
#[error(
"signedAttrs missing signing-time attribute (RFC 9589 §4; RFC 5652 §11.3; RFC 6488 §2.1.6.4)"
)]
SignedAttrsSigningTimeMissing,
#[error(
"signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type} (RFC 6488 §3(1h); RFC 9589 §4)"
)]
ContentTypeAttrMismatch {
econtent_type: String,
attr_content_type: String,
},
#[error("EncapsulatedContentInfo.eContent MUST be present (RFC 6488 §2.1.3; RFC 5652 §5.2)")]
EContentMissing,
#[error(
"signedAttrs.message-digest does not match SHA-256(eContent) (RFC 6488 §3(1f); RFC 5652 §11.2)"
)]
MessageDigestMismatch,
#[error("EE certificate parse error: {0} (RFC 6488 §3(1c); RFC 6487 §4)")]
EeCertificateParse(String),
#[error(
"EE certificate missing SubjectKeyIdentifier extension (RFC 6488 §3(1c); RFC 6487 §4.8.2)"
)]
EeCertificateMissingSki,
#[error(
"EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS}) (RFC 6487 §4.8.8.2)"
)]
EeCertificateMissingSia,
#[error(
"EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT}) (RFC 6487 §4.8.8.2)"
)]
EeCertificateMissingSignedObjectSia,
#[error(
"EE certificate SIA id-ad-signedObject accessLocation must be a URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)"
)]
EeCertificateSignedObjectSiaNotUri,
#[error(
"EE certificate SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)"
)]
EeCertificateSignedObjectSiaNoRsync,
#[error(
"SignerInfo.sid SKI does not match EE certificate SKI (RFC 6488 §3(1c); RFC 5652 §5.3)"
)]
SidSkiMismatch,
#[error(
"invalid signing-time attribute value (expected UTCTime or GeneralizedTime) (RFC 5652 §11.3; RFC 9589 §4)"
)]
InvalidSigningTimeValue,
}
#[derive(Debug, thiserror::Error)]
pub enum SignedObjectDecodeError {
#[error("SignedObject parse error: {0}")]
Parse(#[from] SignedObjectParseError),
#[error("SignedObject validate error: {0}")]
Validate(#[from] SignedObjectValidateError),
}
#[derive(Debug, thiserror::Error)]
pub enum SignedObjectVerifyError {
#[error("EE SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
EeSpkiParse(String),
#[error("trailing bytes after EE SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")]
EeSpkiTrailingBytes(usize),
#[error("unsupported EE public key algorithm (only RSA is supported) (RFC 7935 §2)")]
UnsupportedEePublicKeyAlgorithm,
#[error("EE RSA public exponent invalid (RFC 8017 §A.1.1; RFC 7935 §2)")]
InvalidEeRsaExponent,
#[error("signature verification failed (RFC 6488 §3(2)-(3); RFC 5652 §5.3; RFC 7935 §2)")]
InvalidSignature,
}

312
src/model/ta.rs Normal file
View File

@@ -0,0 +1,312 @@
use url::Url;
use x509_parser::prelude::{FromDer, X509Certificate};
use crate::model::oid::OID_CP_IPADDR_ASNUMBER;
use crate::model::rc::{
AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate,
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
ResourceCertificateRole,
};
use crate::model::tal::Tal;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaCertificate {
pub raw_der: Vec<u8>,
pub rc_ca: ResourceCertificate,
}
#[derive(Debug, thiserror::Error)]
pub enum TaCertificateParseError {
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")]
ResourceCertificate(#[from] ResourceCertificateParseError),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaCertificateParsed {
pub rc_parsed: ResourceCertificateParsed,
}
#[derive(Debug, thiserror::Error)]
pub enum TaCertificateProfileError {
#[error("resource certificate profile error: {0} (RFC 5280 §4; RFC 6487 §4)")]
ResourceCertificate(#[from] ResourceCertificateProfileError),
#[error("TA certificate must be a CA certificate (RFC 8630 §2.3; RFC 6487 §4.8.1)")]
NotCa,
#[error(
"TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)"
)]
NotSelfSignedIssuerSubject,
#[error(
"TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)"
)]
MissingOrInvalidCertificatePolicies,
#[error("TA certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8630 §2.3)")]
MissingSubjectKeyIdentifier,
#[error(
"TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)"
)]
ResourcesMissing,
#[error("TA certificate resources must be non-empty (RFC 8630 §2.3)")]
ResourcesEmpty,
#[error(
"TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)"
)]
IpResourcesInherit,
#[error(
"TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)"
)]
AsResourcesInherit,
}
#[derive(Debug, thiserror::Error)]
pub enum TaCertificateDecodeError {
#[error("{0}")]
Parse(#[from] TaCertificateParseError),
#[error("{0}")]
Validate(#[from] TaCertificateProfileError),
}
/// Backwards-compatible name: TA certificate errors from parse+validate.
pub type TaCertificateError = TaCertificateDecodeError;
#[derive(Debug, thiserror::Error)]
pub enum TaCertificateVerifyError {
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 8630 §2.3)")]
Parse(String),
#[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
TrailingBytes(usize),
#[error(
"TA certificate self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)"
)]
InvalidSelfSignature(String),
}
impl TaCertificate {
/// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(der: &[u8]) -> Result<TaCertificateParsed, TaCertificateParseError> {
Ok(TaCertificateParsed {
rc_parsed: ResourceCertificate::parse_der(der)?,
})
}
/// Profile validate step of scheme A (`parse → validate → verify`).
///
/// `TaCertificate` is already profile-validated when constructed via `decode_der()` /
/// `TaCertificateParsed::validate_profile()`.
pub fn validate_profile(&self) -> Result<(), TaCertificateProfileError> {
Ok(())
}
/// Decode a TA certificate (`parse + validate`).
pub fn decode_der(der: &[u8]) -> Result<Self, TaCertificateDecodeError> {
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn decode_der_with_strict_name(der: &[u8]) -> Result<Self, TaCertificateDecodeError> {
let ta = Self::decode_der(der)?;
ta.rc_ca
.validate_strict_name_profile()
.map_err(TaCertificateProfileError::from)?;
Ok(ta)
}
/// Backwards-compatible helper (historical name).
pub fn from_der(der: &[u8]) -> Result<Self, TaCertificateError> {
Self::decode_der(der)
}
pub fn spki_der(&self) -> &[u8] {
&self.rc_ca.tbs.subject_public_key_info
}
/// Verify step of scheme A (`parse → validate → verify`).
pub fn verify_self_signature(&self) -> Result<(), TaCertificateVerifyError> {
let (rem, cert) = X509Certificate::from_der(&self.raw_der)
.map_err(|e| TaCertificateVerifyError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(TaCertificateVerifyError::TrailingBytes(rem.len()));
}
cert.verify_signature(None)
.map_err(|e| TaCertificateVerifyError::InvalidSelfSignature(e.to_string()))?;
Ok(())
}
/// Validate TA-specific semantic constraints on a parsed Resource Certificate.
///
/// Note: this does not verify the X.509 signature; it is intended for higher-level logic and
/// for unit tests that exercise individual constraint branches.
pub fn validate_rc_constraints(
rc_ca: &ResourceCertificate,
) -> Result<(), TaCertificateProfileError> {
if rc_ca.kind != ResourceCertKind::Ca {
return Err(TaCertificateProfileError::NotCa);
}
if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER)
{
return Err(TaCertificateProfileError::MissingOrInvalidCertificatePolicies);
}
if rc_ca.tbs.extensions.subject_key_identifier.is_none() {
return Err(TaCertificateProfileError::MissingSubjectKeyIdentifier);
}
let ip = rc_ca.tbs.extensions.ip_resources.as_ref();
let asn = rc_ca.tbs.extensions.as_resources.as_ref();
if ip.is_none() && asn.is_none() {
return Err(TaCertificateProfileError::ResourcesMissing);
}
let mut has_any_resource = false;
if let Some(ip) = ip {
if ip.has_any_inherit() {
return Err(TaCertificateProfileError::IpResourcesInherit);
}
for fam in &ip.families {
match &fam.choice {
IpAddressChoice::Inherit => {
return Err(TaCertificateProfileError::IpResourcesInherit);
}
IpAddressChoice::AddressesOrRanges(items) => {
if !items.is_empty() {
has_any_resource = true;
}
}
}
}
}
if let Some(asn) = asn {
if matches!(asn.asnum, Some(AsIdentifierChoice::Inherit))
|| matches!(asn.rdi, Some(AsIdentifierChoice::Inherit))
{
return Err(TaCertificateProfileError::AsResourcesInherit);
}
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.asnum.as_ref()
&& !items.is_empty()
{
has_any_resource = true;
}
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.rdi.as_ref()
&& !items.is_empty()
{
has_any_resource = true;
}
}
if !has_any_resource {
return Err(TaCertificateProfileError::ResourcesEmpty);
}
Ok(())
}
}
impl TaCertificateParsed {
pub fn validate_profile(self) -> Result<TaCertificate, TaCertificateProfileError> {
let rc_ca = self.rc_parsed.validate_profile()?;
if rc_ca.kind != ResourceCertKind::Ca {
return Err(TaCertificateProfileError::NotCa);
}
rc_ca.validate_rfc6487_profile(ResourceCertificateRole::TrustAnchor)?;
if rc_ca.tbs.issuer_name != rc_ca.tbs.subject_name {
return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject);
}
TaCertificate::validate_rc_constraints(&rc_ca)?;
Ok(TaCertificate {
raw_der: rc_ca.raw_der.clone(),
rc_ca,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrustAnchor {
pub tal: Tal,
pub ta_certificate: TaCertificate,
pub resolved_ta_uri: Option<Url>,
}
#[derive(Debug, thiserror::Error)]
pub enum TrustAnchorError {
#[error("TA certificate error: {0} (RFC 8630 §2.3)")]
TaCertificate(#[from] TaCertificateDecodeError),
#[error("TA certificate self-signature error: {0} (RFC 8630 §2.3)")]
TaSelfSignature(#[from] TaCertificateVerifyError),
#[error("{0}")]
Bind(#[from] TrustAnchorBindError),
}
#[derive(Debug, thiserror::Error)]
pub enum TrustAnchorBindError {
#[error("resolved TA URI not listed in TAL: {0} (RFC 8630 §2.2-§2.3)")]
ResolvedUriNotInTal(String),
#[error(
"TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)"
)]
TalSpkiMismatch,
}
impl TrustAnchor {
/// Bind a TAL and a downloaded TA certificate.
///
/// This does not download anything; it only validates the binding rules from RFC 8630 §2.3.
pub fn bind_der(
tal: Tal,
ta_der: &[u8],
resolved_uri: Option<&Url>,
) -> Result<Self, TrustAnchorError> {
let ta_certificate = TaCertificate::decode_der(ta_der)?;
ta_certificate.verify_self_signature()?;
Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
}
pub fn bind_der_with_strict_name(
tal: Tal,
ta_der: &[u8],
resolved_uri: Option<&Url>,
) -> Result<Self, TrustAnchorError> {
let ta_certificate = TaCertificate::decode_der_with_strict_name(ta_der)?;
ta_certificate.verify_self_signature()?;
Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
}
pub fn bind(
tal: Tal,
ta_certificate: TaCertificate,
resolved_uri: Option<&Url>,
) -> Result<Self, TrustAnchorBindError> {
if let Some(u) = resolved_uri
&& !tal.ta_uris.iter().any(|x| x == u)
{
return Err(TrustAnchorBindError::ResolvedUriNotInTal(u.to_string()));
}
if tal.subject_public_key_info_der != ta_certificate.spki_der() {
return Err(TrustAnchorBindError::TalSpkiMismatch);
}
Ok(Self {
tal,
ta_certificate,
resolved_ta_uri: resolved_uri.cloned(),
})
}
}

183
src/model/tal.rs Normal file
View File

@@ -0,0 +1,183 @@
use base64::Engine;
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TalParsed {
pub raw: Vec<u8>,
/// Lines split by '\n' and normalized by stripping a trailing '\r' per line.
pub lines: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tal {
pub raw: Vec<u8>,
pub comments: Vec<String>,
pub ta_uris: Vec<Url>,
pub subject_public_key_info_der: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum TalParseError {
#[error("TAL must be valid UTF-8 (RFC 8630 §2.2)")]
InvalidUtf8,
}
#[derive(Debug, thiserror::Error)]
pub enum TalProfileError {
#[error("TAL comments must appear only at the beginning (RFC 8630 §2.2)")]
CommentAfterHeader,
#[error("TAL must contain at least one TA URI line (RFC 8630 §2.2)")]
MissingTaUris,
#[error(
"TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)"
)]
MissingSeparatorEmptyLine,
#[error("TAL TA URI invalid: {0} (RFC 8630 §2.2)")]
InvalidUri(String),
#[error("TAL TA URI scheme must be rsync or https, got {0} (RFC 8630 §2.2)")]
UnsupportedUriScheme(String),
#[error(
"TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)"
)]
UriIsDirectory(String),
#[error(
"TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)"
)]
MissingSpki,
#[error("TAL SPKI base64 decode failed (RFC 8630 §2.2)")]
SpkiBase64Decode,
#[error("TAL SPKI DER is empty (RFC 8630 §2.2)")]
SpkiDerEmpty,
}
#[derive(Debug, thiserror::Error)]
pub enum TalDecodeError {
#[error("{0}")]
Parse(#[from] TalParseError),
#[error("{0}")]
Validate(#[from] TalProfileError),
}
impl Tal {
/// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_bytes(input: &[u8]) -> Result<TalParsed, TalParseError> {
let raw = input.to_vec();
let text = std::str::from_utf8(input).map_err(|_| TalParseError::InvalidUtf8)?;
let lines: Vec<String> = text
.split('\n')
.map(|l| l.strip_suffix('\r').unwrap_or(l).to_string())
.collect();
Ok(TalParsed { raw, lines })
}
/// Validate step of scheme A (`parse → validate → verify`).
///
/// `Tal` is already profile-validated when constructed via `decode_bytes()` /
/// `TalParsed::validate_profile()`.
pub fn validate_profile(&self) -> Result<(), TalProfileError> {
Ok(())
}
pub fn decode_bytes(input: &[u8]) -> Result<Self, TalDecodeError> {
Ok(Self::parse_bytes(input)?.validate_profile()?)
}
}
impl TalParsed {
pub fn validate_profile(self) -> Result<Tal, TalProfileError> {
let mut idx = 0usize;
// 1) Leading comments.
let mut comments: Vec<String> = Vec::new();
while idx < self.lines.len() && self.lines[idx].starts_with('#') {
comments.push(self.lines[idx][1..].to_string());
idx += 1;
}
// 2) URI list (one or more non-empty lines).
let mut ta_uris: Vec<Url> = Vec::new();
while idx < self.lines.len() {
let line = self.lines[idx].trim();
if line.is_empty() {
break;
}
if line.starts_with('#') {
return Err(TalProfileError::CommentAfterHeader);
}
let url = match Url::parse(line) {
Ok(u) => u,
Err(_) => {
if !ta_uris.is_empty() {
return Err(TalProfileError::MissingSeparatorEmptyLine);
}
return Err(TalProfileError::InvalidUri(line.to_string()));
}
};
match url.scheme() {
"rsync" | "https" => {}
s => return Err(TalProfileError::UnsupportedUriScheme(s.to_string())),
}
if url.path().ends_with('/') {
return Err(TalProfileError::UriIsDirectory(line.to_string()));
}
if url
.path_segments()
.and_then(|mut s| s.next_back())
.unwrap_or("")
.is_empty()
{
return Err(TalProfileError::UriIsDirectory(line.to_string()));
}
ta_uris.push(url);
idx += 1;
}
if ta_uris.is_empty() {
return Err(TalProfileError::MissingTaUris);
}
// 3) Empty line separator (must exist).
if idx >= self.lines.len() || !self.lines[idx].trim().is_empty() {
return Err(TalProfileError::MissingSeparatorEmptyLine);
}
idx += 1;
// 4) Base64(SPKI DER) remainder; allow line wrapping.
let mut b64 = String::new();
while idx < self.lines.len() {
let line = self.lines[idx].trim();
if !line.is_empty() {
b64.push_str(line);
}
idx += 1;
}
if b64.is_empty() {
return Err(TalProfileError::MissingSpki);
}
let spki_der = base64::engine::general_purpose::STANDARD
.decode(b64.as_bytes())
.map_err(|_| TalProfileError::SpkiBase64Decode)?;
if spki_der.is_empty() {
return Err(TalProfileError::SpkiDerEmpty);
}
Ok(Tal {
raw: self.raw,
comments,
ta_uris,
subject_public_key_info_der: spki_der,
})
}
}