初始化 Panda RPKI v0.1.0 开源候选版本
This commit is contained in:
412
src/model/aspa.rs
Normal file
412
src/model/aspa.rs
Normal 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
289
src/model/common.rs
Normal 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
537
src/model/crl.rs
Normal 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
976
src/model/manifest.rs
Normal 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
13
src/model/mod.rs
Normal 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
78
src/model/oid.rs
Normal 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
663
src/model/projection.rs
Normal 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
30
src/model/rc.rs
Normal 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");
|
||||
248
src/model/rc/certificate_validation.rs
Normal file
248
src/model/rc/certificate_validation.rs
Normal 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(())
|
||||
}
|
||||
346
src/model/rc/parsed_validation.rs
Normal file
346
src/model/rc/parsed_validation.rs
Normal 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
584
src/model/rc/parsing.rs
Normal 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)
|
||||
}
|
||||
102
src/model/rc/strict_name_tests.rs
Normal file
102
src/model/rc/strict_name_tests.rs
Normal 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
564
src/model/rc/types.rs
Normal 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
631
src/model/roa.rs
Normal 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
365
src/model/router_cert.rs
Normal 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(())
|
||||
}
|
||||
27
src/model/signed_object.rs
Normal file
27
src/model/signed_object.rs
Normal 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;
|
||||
172
src/model/signed_object/cms_reader.rs
Normal file
172
src/model/signed_object/cms_reader.rs
Normal 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)
|
||||
}
|
||||
542
src/model/signed_object/parsed_profile.rs
Normal file
542
src/model/signed_object/parsed_profile.rs
Normal 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,
|
||||
}],
|
||||
})
|
||||
}
|
||||
344
src/model/signed_object/signed_attrs.rs
Normal file
344
src/model/signed_object/signed_attrs.rs
Normal 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..]
|
||||
}
|
||||
}
|
||||
118
src/model/signed_object/signed_object_impl.rs
Normal file
118
src/model/signed_object/signed_object_impl.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
117
src/model/signed_object/tests.rs
Normal file
117
src/model/signed_object/tests.rs
Normal 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);
|
||||
}
|
||||
300
src/model/signed_object/types_errors.rs
Normal file
300
src/model/signed_object/types_errors.rs
Normal 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
312
src/model/ta.rs
Normal 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
183
src/model/tal.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user