重构error code

This commit is contained in:
2026-02-04 17:02:17 +08:00
parent cc9f3f21de
commit a58e507f92
44 changed files with 6095 additions and 1165 deletions

View File

@@ -1,8 +1,10 @@
use crate::data_model::oid::OID_CT_ASPA;
use crate::data_model::rc::ResourceCertificate;
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
use der_parser::ber::{Class};
use der_parser::der::{parse_der, DerObject, Tag};
use crate::data_model::signed_object::{
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
};
use der_parser::ber::Class;
use der_parser::der::{DerObject, Tag, parse_der};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AspaObject {
@@ -11,6 +13,13 @@ pub struct AspaObject {
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,
@@ -18,70 +27,151 @@ pub struct AspaEContent {
pub provider_as_ids: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AspaEContentParsed {
der: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum AspaDecodeError {
#[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
SignedObjectDecode(#[from] SignedObjectDecodeError),
#[error("ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)")]
InvalidEContentType(String),
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),
}
#[error("ASProviderAttestation must be a SEQUENCE of 3 elements (draft-ietf-sidrops-aspa-profile-21 §3)")]
#[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)")]
#[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)")]
#[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)")]
#[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)")]
#[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)")]
#[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)")]
#[error(
"ASPA providers contains the customerASID ({0}) which is not allowed (draft-ietf-sidrops-aspa-profile-21 §3.3)"
)]
ProvidersContainCustomer(u32),
}
#[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)")]
#[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)")]
#[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)")]
#[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)")]
#[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)")]
#[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)")]
#[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)")]
#[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> {
let signed_object = RpkiSignedObject::decode_der(der)?;
Self::from_signed_object(signed_object)
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, AspaDecodeError> {
@@ -91,10 +181,11 @@ impl AspaObject {
.econtent_type
.clone();
if econtent_type != OID_CT_ASPA {
return Err(AspaDecodeError::InvalidEContentType(econtent_type));
return Err(AspaProfileError::InvalidEContentType(econtent_type).into());
}
let aspa = AspaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
let aspa =
AspaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
Ok(Self {
aspa,
signed_object,
@@ -110,57 +201,27 @@ impl AspaObject {
}
impl AspaEContent {
/// Decode the DER-encoded ASProviderAttestation defined in draft-ietf-sidrops-aspa-profile-21 §3.
/// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(der: &[u8]) -> Result<AspaEContentParsed, AspaParseError> {
let (rem, _obj) = parse_der(der).map_err(|e| AspaParseError::Parse(e.to_string()))?;
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> {
let (rem, obj) = parse_der(der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(AspaDecodeError::TrailingBytes(rem.len()));
}
let seq = obj
.as_sequence()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if seq.len() != 3 {
return Err(AspaDecodeError::InvalidAttestationSequence);
}
// version [0] EXPLICIT INTEGER MUST be present and MUST be 1.
let v_obj = &seq[0];
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
return Err(AspaDecodeError::VersionMustBeExplicitOne);
}
let inner_der = v_obj
.as_slice()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
let (rem, inner) =
parse_der(inner_der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(AspaDecodeError::Parse(
"trailing bytes inside ASProviderAttestation.version".into(),
));
}
let v = inner
.as_u64()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if v != 1 {
return Err(AspaDecodeError::VersionMustBeExplicitOne);
}
let customer_u64 = seq[1]
.as_u64()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if customer_u64 > u32::MAX as u64 {
return Err(AspaDecodeError::CustomerAsIdOutOfRange(customer_u64));
}
let customer_as_id = customer_u64 as u32;
let providers = parse_providers(&seq[2], customer_as_id)?;
Ok(Self {
version: 1,
customer_as_id,
provider_as_ids: providers,
})
Ok(Self::parse_der(der)?.validate_profile()?)
}
/// Validate ASPA payload against the embedded EE resource certificate.
@@ -206,12 +267,87 @@ impl AspaEContent {
}
}
fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>, AspaDecodeError> {
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> {
let (_rem, obj) =
parse_der(&self.der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
let seq = obj
.as_sequence()
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
if seq.len() != 3 {
return Err(AspaProfileError::InvalidAttestationSequence);
}
// version [0] EXPLICIT INTEGER MUST be present and MUST be 1.
let v_obj = &seq[0];
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
return Err(AspaProfileError::VersionMustBeExplicitOne);
}
let inner_der = v_obj
.as_slice()
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
let (rem, inner) =
parse_der(inner_der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
if !rem.is_empty() {
return Err(AspaProfileError::ProfileDecode(
"trailing bytes inside ASProviderAttestation.version".into(),
));
}
let v = inner
.as_u64()
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
if v != 1 {
return Err(AspaProfileError::VersionMustBeExplicitOne);
}
let customer_u64 = seq[1]
.as_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 = parse_providers(&seq[2], customer_as_id)?;
Ok(AspaEContent {
version: 1,
customer_as_id,
provider_as_ids: providers,
})
}
}
fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>, AspaProfileError> {
let seq = obj
.as_sequence()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
if seq.is_empty() {
return Err(AspaDecodeError::EmptyProviders);
return Err(AspaProfileError::EmptyProviders);
}
let mut out: Vec<u32> = Vec::with_capacity(seq.len());
@@ -219,17 +355,17 @@ fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>,
for item in seq {
let v = item
.as_u64()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
if v > u32::MAX as u64 {
return Err(AspaDecodeError::ProviderAsIdOutOfRange(v));
return Err(AspaProfileError::ProviderAsIdOutOfRange(v));
}
let asn = v as u32;
if asn == customer_as_id {
return Err(AspaDecodeError::ProvidersContainCustomer(customer_as_id));
return Err(AspaProfileError::ProvidersContainCustomer(customer_as_id));
}
if let Some(p) = prev {
if asn <= p {
return Err(AspaDecodeError::ProvidersNotStrictlyIncreasing);
return Err(AspaProfileError::ProvidersNotStrictlyIncreasing);
}
}
prev = Some(asn);

View File

@@ -69,7 +69,9 @@ impl BigUnsigned {
}
#[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)")]
#[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,
@@ -103,6 +105,5 @@ pub fn algorithm_params_absent_or_null(sig: &AlgorithmIdentifier<'_>) -> bool {
///
/// 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",
];
pub const IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS: &[&str] =
&["asa", "cer", "crl", "gbr", "mft", "roa", "sig", "tak"];

View File

@@ -3,13 +3,12 @@ use crate::data_model::oid::{
OID_AUTHORITY_KEY_IDENTIFIER, OID_CRL_NUMBER, OID_SHA256_WITH_RSA_ENCRYPTION,
OID_SUBJECT_KEY_IDENTIFIER,
};
use x509_parser::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension};
use x509_parser::prelude::FromDer;
use x509_parser::prelude::X509Version;
use x509_parser::revocation_list::CertificateRevocationList;
use x509_parser::certificate::X509Certificate;
use x509_parser::x509::SubjectPublicKeyInfo;
use x509_parser::x509::AlgorithmIdentifier;
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 {
@@ -35,26 +34,93 @@ pub struct RpkixCrl {
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 CrlDecodeError {
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 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 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),
@@ -64,12 +130,20 @@ pub enum CrlDecodeError {
#[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)")]
#[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,
@@ -82,7 +156,9 @@ pub enum CrlDecodeError {
#[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)")]
#[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,
@@ -90,64 +166,47 @@ pub enum CrlDecodeError {
},
}
#[derive(Debug, thiserror::Error)]
pub enum CrlDecodeError {
#[error("{0}")]
Parse(#[from] CrlParseError),
#[error("{0}")]
Validate(#[from] CrlProfileError),
}
impl RpkixCrl {
/// 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> {
/// 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| CrlDecodeError::Parse(e.to_string()))?;
.map_err(|e| CrlParseError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(CrlDecodeError::TrailingBytes(rem.len()));
return Err(CrlParseError::TrailingBytes(rem.len()));
}
let version = match crl.version() {
Some(X509Version::V2) => 2,
Some(v) => return Err(CrlDecodeError::InvalidVersion(Some(v.0))),
None => return Err(CrlDecodeError::InvalidVersion(None)),
};
let sig_oid = crl.signature_algorithm.algorithm.to_id_string();
let tbs_sig_oid = crl.tbs_cert_list.signature.algorithm.to_id_string();
if sig_oid != tbs_sig_oid {
return Err(CrlDecodeError::SignatureAlgorithmMismatch);
}
if sig_oid != OID_SHA256_WITH_RSA_ENCRYPTION {
return Err(CrlDecodeError::InvalidSignatureAlgorithm(sig_oid));
}
validate_sig_params(&crl.signature_algorithm)?;
validate_sig_params(&crl.tbs_cert_list.signature)?;
let extensions = parse_and_validate_extensions(crl.extensions())?;
let revoked_certs = crl
.iter_revoked_certificates()
.map(|rc| {
if !rc.extensions().is_empty() {
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
}
let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date);
validate_time_encoding_rfc5280("revocationDate", &revocation_date)?;
Ok(RevokedCert {
serial_number: BigUnsigned::from_biguint(rc.serial()),
revocation_date,
})
.map(|rc| RevokedCertParsed {
serial_number: BigUnsigned::from_biguint(rc.serial()),
revocation_date: crate::data_model::common::asn1_time_to_model(rc.revocation_date),
has_extensions: !rc.extensions().is_empty(),
})
.collect::<Result<Vec<_>, _>>()?;
.collect::<Vec<_>>();
let this_update = crate::data_model::common::asn1_time_to_model(crl.last_update());
validate_time_encoding_rfc5280("thisUpdate", &this_update)?;
let next_update = crl
.next_update()
.map(crate::data_model::common::asn1_time_to_model)
.ok_or(CrlDecodeError::NextUpdateMissing)?;
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
.map(crate::data_model::common::asn1_time_to_model);
Ok(RpkixCrl {
let extensions =
parse_extensions_parse(crl.extensions()).map_err(|e| CrlParseError::Parse(e))?;
Ok(RpkixCrlParsed {
raw_der: der.to_vec(),
version,
version: crl.version(),
issuer_dn: crl.issuer().to_string(),
signature_algorithm_oid: OID_SHA256_WITH_RSA_ENCRYPTION.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,
@@ -155,6 +214,20 @@ impl RpkixCrl {
})
}
/// 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)).
@@ -229,6 +302,59 @@ impl RpkixCrl {
}
}
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)")]
@@ -240,7 +366,9 @@ pub enum CrlVerifyError {
#[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)")]
#[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)")]
@@ -249,16 +377,22 @@ pub enum CrlVerifyError {
#[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))")]
#[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))")]
#[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))")]
#[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)")]
@@ -268,7 +402,7 @@ pub enum CrlVerifyError {
fn validate_time_encoding_rfc5280(
field: &'static str,
t: &Asn1TimeUtc,
) -> Result<(), CrlDecodeError> {
) -> Result<(), CrlProfileError> {
let year = t.utc.year();
let expected = if year <= 2049 {
Asn1TimeEncoding::UtcTime
@@ -276,7 +410,7 @@ fn validate_time_encoding_rfc5280(
Asn1TimeEncoding::GeneralizedTime
};
if t.encoding != expected {
return Err(CrlDecodeError::InvalidTimeEncoding {
return Err(CrlProfileError::InvalidTimeEncoding {
field,
year,
encoding: t.encoding,
@@ -285,86 +419,109 @@ fn validate_time_encoding_rfc5280(
Ok(())
}
fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
if crate::data_model::common::algorithm_params_absent_or_null(sig) {
Ok(())
} else {
Err(CrlDecodeError::InvalidSignatureAlgorithmParameters)
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(),
});
AlgorithmIdentifierValue {
oid: ai.algorithm.to_id_string(),
parameters,
}
}
fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result<CrlExtensions, CrlDecodeError> {
if exts.len() != 2 {
return Err(CrlDecodeError::InvalidExtensionsCount(exts.len()));
}
let mut authority_key_identifier: Option<Vec<u8>> = None;
let mut crl_number: Option<BigUnsigned> = None;
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.to_id_string();
match oid.as_str() {
OID_AUTHORITY_KEY_IDENTIFIER => {
if authority_key_identifier.is_some() {
return Err(CrlDecodeError::DuplicateExtension(oid));
}
let aki = parse_aki(ext)?;
authority_key_identifier = Some(aki);
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,
});
}
OID_CRL_NUMBER => {
if crl_number.is_some() {
return Err(CrlDecodeError::DuplicateExtension(oid));
OID_CRL_NUMBER => 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()),
},
_ => out.push(CrlExtensionParsed::Other {
oid,
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));
}
if ext.critical {
return Err(CrlDecodeError::CrlNumberCritical);
seen.push(oid.clone());
if *has_other_fields {
return Err(CrlProfileError::AkiHasOtherFields);
}
let n = parse_crl_number(ext)?;
if n.bits() > 159 {
return Err(CrlDecodeError::CrlNumberOutOfRange);
}
crl_number = Some(BigUnsigned::from_biguint(&n));
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()));
}
_ => return Err(CrlDecodeError::UnsupportedExtension(oid)),
}
}
Ok(CrlExtensions {
authority_key_identifier: authority_key_identifier.unwrap(),
crl_number: crl_number.unwrap(),
authority_key_identifier: authority_key_identifier.ok_or(CrlProfileError::AkiMissing)?,
crl_number: crl_number.ok_or(CrlProfileError::CrlNumberMissing)?,
})
}
fn parse_aki(ext: &X509Extension<'_>) -> Result<Vec<u8>, CrlDecodeError> {
let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else {
return Err(CrlDecodeError::Parse("AKI extension parse failed".into()));
};
validate_aki_profile(aki)?;
Ok(aki
.key_identifier
.as_ref()
.ok_or(CrlDecodeError::AkiMissingKeyIdentifier)?
.0
.to_vec())
}
fn validate_aki_profile(aki: &AuthorityKeyIdentifier<'_>) -> Result<(), CrlDecodeError> {
if aki.key_identifier.is_none() {
return Err(CrlDecodeError::AkiMissingKeyIdentifier);
}
if aki.authority_cert_issuer.is_some() || aki.authority_cert_serial.is_some() {
return Err(CrlDecodeError::AkiHasOtherFields);
}
Ok(())
}
fn parse_crl_number(ext: &X509Extension<'_>) -> Result<der_parser::num_bigint::BigUint, CrlDecodeError> {
match ext.parsed_extension() {
ParsedExtension::CRLNumber(n) => Ok(n.clone()),
ParsedExtension::ParseError { error } => Err(CrlDecodeError::Parse(error.to_string())),
_ => Err(CrlDecodeError::Parse("CRLNumber extension parse failed".into())),
}
}
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
cert.extensions()
.iter()

View File

@@ -1,9 +1,11 @@
use crate::data_model::common::BigUnsigned;
use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256};
use crate::data_model::rc::ResourceCertificate;
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
use crate::data_model::signed_object::{
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
};
use der_parser::ber::BerObjectContent;
use der_parser::der::{parse_der, DerObject, Tag};
use der_parser::der::{DerObject, Tag, parse_der};
use time::OffsetDateTime;
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -13,6 +15,13 @@ pub struct ManifestObject {
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,
@@ -23,6 +32,11 @@ pub struct ManifestEContent {
pub files: Vec<FileAndHash>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ManifestEContentParsed {
der: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileAndHash {
pub file_name: String,
@@ -30,26 +44,38 @@ pub struct FileAndHash {
}
#[derive(Debug, thiserror::Error)]
pub enum ManifestDecodeError {
#[error("signed object decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4; RFC 9286 §4)")]
SignedObject(#[from] SignedObjectDecodeError),
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),
}
#[error("eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0} (RFC 9286 §4.1; RFC 9286 §4.4(1))")]
#[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)")]
#[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)")]
@@ -64,7 +90,9 @@ pub enum ManifestDecodeError {
#[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)")]
#[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)")]
@@ -79,44 +107,97 @@ pub enum ManifestDecodeError {
#[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)")]
#[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)")]
#[error(
"fileList hash length invalid for sha256: got {0} bytes (RFC 9286 §4.2.1; RFC 7935 §2)"
)]
InvalidHashLength(usize),
}
#[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)")]
#[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)")]
#[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)")]
#[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)")]
#[error(
"Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)"
)]
EeAsResourcesRdiPresent,
}
impl ManifestObject {
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
let signed_object = RpkiSignedObject::decode_der(der)?;
Self::from_signed_object(signed_object)
/// 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,
})
}
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, ManifestDecodeError> {
/// 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 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(ManifestDecodeError::InvalidEContentType(econtent_type));
return Err(ManifestProfileError::InvalidEContentType(econtent_type).into());
}
let manifest = ManifestEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
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(),
@@ -164,18 +245,62 @@ impl ManifestObject {
}
impl ManifestEContent {
/// Decode the DER-encoded Manifest eContent defined in RFC 9286 §4.2.
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
let (rem, obj) = parse_der(der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
/// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(der: &[u8]) -> Result<ManifestEContentParsed, ManifestParseError> {
let (rem, _obj) = parse_der(der).map_err(|e| ManifestParseError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(ManifestDecodeError::TrailingBytes(rem.len()));
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()?)
}
}
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> {
let (_rem, obj) =
parse_der(&self.der).map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
let seq = obj
.as_sequence()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
if seq.len() != 5 && seq.len() != 6 {
return Err(ManifestDecodeError::InvalidManifestSequenceLen(seq.len()));
return Err(ManifestProfileError::InvalidManifestSequenceLen(seq.len()));
}
let mut idx = 0;
@@ -183,25 +308,25 @@ impl ManifestEContent {
if seq.len() == 6 {
let v_obj = &seq[0];
if v_obj.class() != der_parser::ber::Class::ContextSpecific || v_obj.tag() != Tag(0) {
return Err(ManifestDecodeError::Parse(
return Err(ManifestProfileError::ProfileDecode(
"Manifest.version must be [0] EXPLICIT INTEGER".into(),
));
}
let inner_der = v_obj
.as_slice()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
let (rem, inner) =
parse_der(inner_der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
let (rem, inner) = parse_der(inner_der)
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
if !rem.is_empty() {
return Err(ManifestDecodeError::Parse(
return Err(ManifestProfileError::ProfileDecode(
"trailing bytes inside Manifest.version".into(),
));
}
let v = inner
.as_u64()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
if v != 0 {
return Err(ManifestDecodeError::InvalidManifestVersion(v));
return Err(ManifestProfileError::InvalidManifestVersion(v));
}
version = 0;
idx = 1;
@@ -211,24 +336,24 @@ impl ManifestEContent {
idx += 1;
let this_update =
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidThisUpdate)?;
parse_generalized_time(&seq[idx], ManifestProfileError::InvalidThisUpdate)?;
idx += 1;
let next_update =
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidNextUpdate)?;
parse_generalized_time(&seq[idx], ManifestProfileError::InvalidNextUpdate)?;
idx += 1;
if next_update <= this_update {
return Err(ManifestDecodeError::NextUpdateNotLater);
return Err(ManifestProfileError::NextUpdateNotLater);
}
let file_hash_alg = oid_to_string(&seq[idx])?;
idx += 1;
if file_hash_alg != OID_SHA256 {
return Err(ManifestDecodeError::InvalidFileHashAlg(file_hash_alg));
return Err(ManifestProfileError::InvalidFileHashAlg(file_hash_alg));
}
let files = parse_file_list_sha256(&seq[idx])?;
Ok(Self {
Ok(ManifestEContent {
version,
manifest_number,
this_update,
@@ -239,39 +364,39 @@ impl ManifestEContent {
}
}
fn parse_manifest_number(obj: &DerObject<'_>) -> Result<BigUnsigned, ManifestDecodeError> {
fn parse_manifest_number(obj: &DerObject<'_>) -> Result<BigUnsigned, ManifestProfileError> {
let n = obj
.as_biguint()
.map_err(|_e| ManifestDecodeError::InvalidManifestNumber)?;
.map_err(|_e| ManifestProfileError::InvalidManifestNumber)?;
let out = BigUnsigned::from_biguint(&n);
if out.bytes_be.len() > 20 {
return Err(ManifestDecodeError::ManifestNumberTooLong);
return Err(ManifestProfileError::ManifestNumberTooLong);
}
Ok(out)
}
fn parse_generalized_time(
obj: &DerObject<'_>,
err: ManifestDecodeError,
) -> Result<OffsetDateTime, ManifestDecodeError> {
err: ManifestProfileError,
) -> Result<OffsetDateTime, ManifestProfileError> {
match &obj.content {
BerObjectContent::GeneralizedTime(dt) => dt
.to_datetime()
.map_err(|e| ManifestDecodeError::Parse(e.to_string())),
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string())),
_ => Err(err),
}
}
fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, ManifestDecodeError> {
fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, ManifestProfileError> {
let seq = obj
.as_sequence()
.map_err(|_e| ManifestDecodeError::InvalidFileList)?;
.map_err(|_e| ManifestProfileError::InvalidFileList)?;
let mut out = Vec::with_capacity(seq.len());
for entry in seq {
let (file_name, hash_bytes) = parse_file_and_hash(entry)?;
validate_file_name(&file_name)?;
if hash_bytes.len() != 32 {
return Err(ManifestDecodeError::InvalidHashLength(hash_bytes.len()));
return Err(ManifestProfileError::InvalidHashLength(hash_bytes.len()));
}
out.push(FileAndHash {
file_name,
@@ -281,58 +406,58 @@ fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, Manif
Ok(out)
}
fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec<u8>), ManifestDecodeError> {
fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec<u8>), ManifestProfileError> {
let seq = obj
.as_sequence()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
if seq.len() != 2 {
return Err(ManifestDecodeError::InvalidFileAndHash);
return Err(ManifestProfileError::InvalidFileAndHash);
}
let file_name = seq[0]
.as_str()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?
.to_string();
let (unused_bits, bits) = match &seq[1].content {
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
_ => return Err(ManifestDecodeError::InvalidHashType),
_ => return Err(ManifestProfileError::InvalidHashType),
};
if unused_bits != 0 {
return Err(ManifestDecodeError::HashNotOctetAligned);
return Err(ManifestProfileError::HashNotOctetAligned);
}
Ok((file_name, bits))
}
fn validate_file_name(name: &str) -> Result<(), ManifestDecodeError> {
fn validate_file_name(name: &str) -> Result<(), ManifestProfileError> {
// RFC 9286 §4.2.2:
// 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension.
let Some((base, ext)) = name.rsplit_once('.') else {
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
};
if base.is_empty() || ext.len() != 3 {
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
}
if !base
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
}
if !ext.bytes().all(|b| b.is_ascii_alphabetic()) {
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
}
let ext_lower = ext.to_ascii_lowercase();
if !crate::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS
.iter()
.any(|&e| e == ext_lower)
{
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
}
Ok(())
}
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, ManifestDecodeError> {
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, ManifestProfileError> {
let oid = obj
.as_oid()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
Ok(oid.to_id_string())
}

View File

@@ -1,10 +1,10 @@
pub mod aspa;
pub mod common;
pub mod crl;
pub mod rc;
pub mod oid;
pub mod signed_object;
pub mod manifest;
pub mod oid;
pub mod rc;
pub mod roa;
pub mod aspa;
pub mod tal;
pub mod signed_object;
pub mod ta;
pub mod tal;

View File

@@ -1,12 +1,12 @@
use der_parser::ber::{BerObjectContent, Class};
use der_parser::der::{parse_der, DerObject, Tag};
use der_parser::der::{DerObject, Tag, parse_der};
use der_parser::num_bigint::BigUint;
use time::OffsetDateTime;
use url::Url;
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 crate::data_model::common::algorithm_params_absent_or_null;
use crate::data_model::oid::{
OID_AD_SIGNED_OBJECT, OID_AUTONOMOUS_SYS_IDS, OID_CP_IPADDR_ASNUMBER, OID_IP_ADDR_BLOCKS,
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER,
@@ -59,6 +59,62 @@ pub struct RcExtensions {
pub as_resources: Option<AsResourceSet>,
}
#[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_dn: String,
pub subject_dn: String,
pub validity_not_before: OffsetDateTime,
pub validity_not_after: OffsetDateTime,
/// 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_ca: Vec<bool>,
pub subject_key_identifier: Vec<(Vec<u8>, bool)>,
pub subject_info_access: Vec<(SubjectInfoAccessParsed, bool)>,
pub certificate_policies: Vec<(Vec<String>, bool)>,
pub ip_resources: Vec<(IpResourceSet, bool)>,
pub as_resources: Vec<(AsResourceSet, bool)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubjectInfoAccessParsed {
pub access_descriptions: Vec<AccessDescription>,
pub signed_object_uris: Vec<Url>,
pub signed_object_access_location_not_uri: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SubjectInfoAccess {
Ca(SubjectInfoAccessCa),
@@ -204,10 +260,7 @@ impl AsResourceSet {
}
pub fn has_any_range(&self) -> bool {
self.asnum
.as_ref()
.map(|c| c.has_range())
.unwrap_or(false)
self.asnum.as_ref().map(|c| c.has_range()).unwrap_or(false)
|| self.rdi.as_ref().map(|c| c.has_range()).unwrap_or(false)
}
@@ -243,7 +296,9 @@ impl AsIdentifierChoice {
pub fn has_range(&self) -> bool {
match self {
AsIdentifierChoice::Inherit => false,
AsIdentifierChoice::AsIdsOrRanges(items) => items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. })),
AsIdentifierChoice::AsIdsOrRanges(items) => {
items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. }))
}
}
}
}
@@ -255,20 +310,31 @@ pub enum AsIdOrRange {
}
#[derive(Debug, thiserror::Error)]
pub enum ResourceCertificateError {
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("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)")]
#[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)")]
@@ -286,45 +352,46 @@ pub enum ResourceCertificateError {
#[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")]
CertificatePoliciesCriticality,
#[error("certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)")]
#[error(
"certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)"
)]
InvalidCertificatePolicy(String),
#[error("SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)")]
#[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("invalid RFC 3779 IP resources extension (RFC 6487 §4.8.10; RFC 3779 §2.2)")]
InvalidIpResources,
#[error("ipAddrBlocks criticality must be critical when present (RFC 6487 §4.8.10)")]
IpResourcesCriticality,
#[error("invalid RFC 3779 AS resources extension (RFC 6487 §4.8.11; RFC 3779 §3.2)")]
InvalidAsResources,
#[error("autonomousSysIds criticality must be critical when present (RFC 6487 §4.8.11)")]
AsResourcesCriticality,
}
#[derive(Debug, thiserror::Error)]
pub enum ResourceCertificateDecodeError {
#[error("{0}")]
Parse(#[from] ResourceCertificateParseError),
#[error("{0}")]
Validate(#[from] ResourceCertificateProfileError),
}
pub type ResourceCertificateError = ResourceCertificateDecodeError;
impl ResourceCertificate {
pub fn from_der(der: &[u8]) -> Result<Self, ResourceCertificateError> {
let (rem, cert) =
X509Certificate::from_der(der).map_err(|e| ResourceCertificateError::Parse(e.to_string()))?;
/// 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(ResourceCertificateError::TrailingBytes(rem.len()));
}
let version = match cert.version() {
X509Version::V3 => 2u32,
_ => return Err(ResourceCertificateError::InvalidVersion),
};
let outer = &cert.signature_algorithm;
let inner = &cert.tbs_certificate.signature;
if outer.algorithm != inner.algorithm || outer.parameters != inner.parameters {
return Err(ResourceCertificateError::SignatureAlgorithmMismatch);
}
if outer.algorithm.to_id_string() != OID_SHA256_WITH_RSA_ENCRYPTION {
return Err(ResourceCertificateError::UnsupportedSignatureAlgorithm);
}
if !algorithm_params_absent_or_null(outer) {
return Err(ResourceCertificateError::InvalidSignatureAlgorithmParameters);
return Err(ResourceCertificateParseError::TrailingBytes(rem.len()));
}
let validity_not_before = cert.validity().not_before.to_datetime();
@@ -332,7 +399,62 @@ impl ResourceCertificate {
let subject_public_key_info = cert.tbs_certificate.subject_pki.raw.to_vec();
let extensions = parse_extensions(cert.extensions())?;
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_dn: cert.issuer().to_string(),
subject_dn: cert.subject().to_string(),
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(())
}
/// Decode a resource certificate (`parse + validate`).
pub fn decode_der(der: &[u8]) -> Result<Self, ResourceCertificateDecodeError> {
Ok(Self::parse_der(der)?.validate_profile()?)
}
/// Backwards-compatible helper (historical name).
pub fn from_der(der: &[u8]) -> Result<Self, ResourceCertificateError> {
Self::decode_der(der)
}
}
impl ResourceCertificateParsed {
pub fn validate_profile(self) -> Result<ResourceCertificate, ResourceCertificateProfileError> {
let version = match self.version {
X509Version::V3 => 2u32,
_ => return Err(ResourceCertificateProfileError::InvalidVersion),
};
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 extensions = self.extensions.validate_profile()?;
let kind = if extensions.basic_constraints_ca {
ResourceCertKind::Ca
} else {
@@ -340,16 +462,16 @@ impl ResourceCertificate {
};
Ok(ResourceCertificate {
raw_der: der.to_vec(),
raw_der: self.raw_der,
tbs: RpkixTbsCertificate {
version,
serial_number: cert.tbs_certificate.serial.clone(),
signature_algorithm: outer.algorithm.to_id_string(),
issuer_dn: cert.issuer().to_string(),
subject_dn: cert.subject().to_string(),
validity_not_before,
validity_not_after,
subject_public_key_info,
serial_number: self.serial_number,
signature_algorithm: self.signature_algorithm.oid,
issuer_dn: self.issuer_dn,
subject_dn: self.subject_dn,
validity_not_before: self.validity_not_before,
validity_not_after: self.validity_not_after,
subject_public_key_info: self.subject_public_key_info,
extensions,
},
kind,
@@ -357,114 +479,220 @@ impl ResourceCertificate {
}
}
fn parse_extensions(exts: &[X509Extension<'_>]) -> Result<RcExtensions, ResourceCertificateError> {
let mut basic_constraints_ca: Option<bool> = None;
let mut ski: Option<Vec<u8>> = None;
let mut sia: Option<SubjectInfoAccess> = None;
let mut cert_policies_oid: Option<String> = None;
impl RcExtensionsParsed {
pub fn validate_profile(self) -> Result<RcExtensions, ResourceCertificateProfileError> {
if self.basic_constraints_ca.len() > 1 {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"basicConstraints",
));
}
let basic_constraints_ca = self.basic_constraints_ca.first().copied().unwrap_or(false);
let mut ip_resources: Option<IpResourceSet> = None;
let mut as_resources: Option<AsResourceSet> = None;
let subject_key_identifier = match self.subject_key_identifier.as_slice() {
[] => None,
[(ski, critical)] => {
if *critical {
return Err(ResourceCertificateProfileError::SkiCriticality);
}
Some(ski.clone())
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"subjectKeyIdentifier",
));
}
};
let subject_info_access = match self.subject_info_access.as_slice() {
[] => None,
[(sia, critical)] => {
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.scheme() == "rsync")
{
return Err(ResourceCertificateProfileError::SignedObjectSiaNoRsync);
}
if sia.signed_object_uris.is_empty() {
Some(SubjectInfoAccess::Ca(SubjectInfoAccessCa {
access_descriptions: sia.access_descriptions.clone(),
}))
} else {
Some(SubjectInfoAccess::Ee(SubjectInfoAccessEe {
signed_object_uris: sia.signed_object_uris.clone(),
access_descriptions: sia.access_descriptions.clone(),
}))
}
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"subjectInfoAccess",
));
}
};
let certificate_policies_oid = match self.certificate_policies.as_slice() {
[] => None,
[(oids, critical)] => {
if !*critical {
return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality);
}
if oids.len() != 1 {
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
"expected exactly one policy".into(),
));
}
let policy_oid = oids[0].clone();
if policy_oid != OID_CP_IPADDR_ASNUMBER {
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
policy_oid,
));
}
Some(OID_CP_IPADDR_ASNUMBER.to_string())
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"certificatePolicies",
));
}
};
let ip_resources = match self.ip_resources.as_slice() {
[] => None,
[(ip, critical)] => {
if !*critical {
return Err(ResourceCertificateProfileError::IpResourcesCriticality);
}
Some(ip.clone())
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"ipAddrBlocks",
));
}
};
let as_resources = match self.as_resources.as_slice() {
[] => None,
[(asn, critical)] => {
if !*critical {
return Err(ResourceCertificateProfileError::AsResourcesCriticality);
}
Some(asn.clone())
}
_ => {
return Err(ResourceCertificateProfileError::DuplicateExtension(
"autonomousSysIds",
));
}
};
Ok(RcExtensions {
basic_constraints_ca,
subject_key_identifier,
subject_info_access,
certificate_policies_oid,
ip_resources,
as_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(),
});
AlgorithmIdentifierValue {
oid: ai.algorithm.to_id_string(),
parameters,
}
}
fn parse_extensions_parse(
exts: &[X509Extension<'_>],
) -> Result<RcExtensionsParsed, ResourceCertificateParseError> {
let mut basic_constraints_ca: Vec<bool> = Vec::new();
let mut ski: Vec<(Vec<u8>, bool)> = Vec::new();
let mut sia: Vec<(SubjectInfoAccessParsed, bool)> = Vec::new();
let mut cert_policies: Vec<(Vec<String>, bool)> = Vec::new();
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.to_id_string();
match oid.as_str() {
crate::data_model::oid::OID_BASIC_CONSTRAINTS => {
if basic_constraints_ca.is_some() {
return Err(ResourceCertificateError::DuplicateExtension("basicConstraints"));
}
let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else {
return Err(ResourceCertificateError::Parse("basicConstraints parse failed".into()));
return Err(ResourceCertificateParseError::Parse(
"basicConstraints parse failed".into(),
));
};
basic_constraints_ca = Some(bc.ca);
basic_constraints_ca.push(bc.ca);
}
OID_SUBJECT_KEY_IDENTIFIER => {
if ski.is_some() {
return Err(ResourceCertificateError::DuplicateExtension("subjectKeyIdentifier"));
}
if ext.critical {
return Err(ResourceCertificateError::SkiCriticality);
}
let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else {
return Err(ResourceCertificateError::Parse("subjectKeyIdentifier parse failed".into()));
return Err(ResourceCertificateParseError::Parse(
"subjectKeyIdentifier parse failed".into(),
));
};
ski = Some(s.0.to_vec());
ski.push((s.0.to_vec(), ext.critical));
}
OID_SUBJECT_INFO_ACCESS => {
if sia.is_some() {
return Err(ResourceCertificateError::DuplicateExtension("subjectInfoAccess"));
}
if ext.critical {
return Err(ResourceCertificateError::SiaCriticality);
}
let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else {
return Err(ResourceCertificateError::Parse("subjectInfoAccess parse failed".into()));
return Err(ResourceCertificateParseError::Parse(
"subjectInfoAccess parse failed".into(),
));
};
sia = Some(parse_sia(s.accessdescs.as_slice())?);
sia.push((parse_sia_parse(s.accessdescs.as_slice())?, ext.critical));
}
crate::data_model::oid::OID_CERTIFICATE_POLICIES => {
if cert_policies_oid.is_some() {
return Err(ResourceCertificateError::DuplicateExtension("certificatePolicies"));
}
if !ext.critical {
return Err(ResourceCertificateError::CertificatePoliciesCriticality);
}
let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else {
return Err(ResourceCertificateError::Parse("certificatePolicies parse failed".into()));
};
if cp.len() != 1 {
return Err(ResourceCertificateError::InvalidCertificatePolicy(
"expected exactly one policy".into(),
return Err(ResourceCertificateParseError::Parse(
"certificatePolicies parse failed".into(),
));
}
let policy_oid = cp[0].policy_id.to_id_string();
if policy_oid != OID_CP_IPADDR_ASNUMBER {
return Err(ResourceCertificateError::InvalidCertificatePolicy(policy_oid));
}
cert_policies_oid = Some(OID_CP_IPADDR_ASNUMBER.to_string());
};
let oids: Vec<String> = cp.iter().map(|p| p.policy_id.to_id_string()).collect();
cert_policies.push((oids, ext.critical));
}
OID_IP_ADDR_BLOCKS => {
if ip_resources.is_some() {
return Err(ResourceCertificateError::DuplicateExtension("ipAddrBlocks"));
}
// Must be critical per RPKI profile; we only enforce when present.
if !ext.critical {
return Err(ResourceCertificateError::InvalidIpResources);
}
let parsed = IpResourceSet::decode_extn_value(ext.value)
.map_err(|_e| ResourceCertificateError::InvalidIpResources)?;
ip_resources = Some(parsed);
.map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?;
ip_resources.push((parsed, ext.critical));
}
OID_AUTONOMOUS_SYS_IDS => {
if as_resources.is_some() {
return Err(ResourceCertificateError::DuplicateExtension("autonomousSysIds"));
}
if !ext.critical {
return Err(ResourceCertificateError::InvalidAsResources);
}
let parsed = AsResourceSet::decode_extn_value(ext.value)
.map_err(|_e| ResourceCertificateError::InvalidAsResources)?;
as_resources = Some(parsed);
.map_err(|_e| ResourceCertificateParseError::InvalidAsResourcesEncoding)?;
as_resources.push((parsed, ext.critical));
}
_ => {}
}
}
let basic_constraints_ca = basic_constraints_ca.unwrap_or(false);
Ok(RcExtensions {
Ok(RcExtensionsParsed {
basic_constraints_ca,
subject_key_identifier: ski,
subject_info_access: sia,
certificate_policies_oid: cert_policies_oid,
certificate_policies: cert_policies,
ip_resources,
as_resources,
})
}
fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Result<SubjectInfoAccess, ResourceCertificateError> {
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<Url> = Vec::new();
let mut signed_object_access_location_not_uri = false;
for ad in access {
let access_method_oid = ad.access_method.to_id_string();
@@ -472,13 +700,13 @@ fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Resul
x509_parser::extensions::GeneralName::URI(u) => u,
_ => {
if access_method_oid == OID_AD_SIGNED_OBJECT {
return Err(ResourceCertificateError::SignedObjectSiaNotUri);
signed_object_access_location_not_uri = true;
}
continue;
}
};
let url =
Url::parse(uri).map_err(|_| ResourceCertificateError::Parse(format!("invalid URI: {uri}")))?;
let url = Url::parse(uri)
.map_err(|_| ResourceCertificateParseError::Parse(format!("invalid URI: {uri}")))?;
if access_method_oid == OID_AD_SIGNED_OBJECT {
signed_object_uris.push(url.clone());
}
@@ -488,20 +716,11 @@ fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Resul
});
}
if signed_object_uris.is_empty() {
return Ok(SubjectInfoAccess::Ca(SubjectInfoAccessCa {
access_descriptions: all,
}));
}
if !signed_object_uris.iter().any(|u| u.scheme() == "rsync") {
return Err(ResourceCertificateError::SignedObjectSiaNoRsync);
}
Ok(SubjectInfoAccess::Ee(SubjectInfoAccessEe {
signed_object_uris,
Ok(SubjectInfoAccessParsed {
access_descriptions: all,
}))
signed_object_uris,
signed_object_access_location_not_uri,
})
}
fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result<IpResourceSet, ()> {
@@ -545,7 +764,9 @@ fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result<IpResourceSet, ()> {
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::BitString(_, _) => {
Ok(IpAddressOrRange::Prefix(parse_ip_prefix(afi, obj)?))
}
BerObjectContent::Sequence(_) => {
let seq = obj.as_sequence().map_err(|_| ())?;
if seq.len() != 2 {
@@ -595,7 +816,11 @@ fn parse_ip_prefix(afi: Afi, obj: &DerObject<'_>) -> Result<IpPrefix, ()> {
/// 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>, ()> {
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(()),

View File

@@ -1,8 +1,10 @@
use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ;
use crate::data_model::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate};
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
use crate::data_model::signed_object::{
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
};
use der_parser::ber::{BerObjectContent, Class};
use der_parser::der::{parse_der, DerObject, Tag};
use der_parser::der::{DerObject, Tag, parse_der};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaObject {
@@ -11,6 +13,13 @@ pub struct RoaObject {
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,
@@ -18,19 +27,32 @@ pub struct RoaEContent {
pub ip_addr_blocks: Vec<RoaIpAddressFamily>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaEContentParsed {
der: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum RoaDecodeError {
#[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
SignedObjectDecode(#[from] SignedObjectDecodeError),
#[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")]
InvalidEContentType(String),
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),
@@ -65,13 +87,19 @@ pub enum RoaDecodeError {
#[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)")]
#[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)")]
#[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)")]
#[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,
@@ -79,6 +107,15 @@ pub enum RoaDecodeError {
},
}
#[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)")]
@@ -90,7 +127,9 @@ pub enum RoaValidateError {
#[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)")]
#[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>,
@@ -99,9 +138,38 @@ pub enum RoaValidateError {
}
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> {
let signed_object = RpkiSignedObject::decode_der(der)?;
Self::from_signed_object(signed_object)
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, RoaDecodeError> {
@@ -111,7 +179,7 @@ impl RoaObject {
.econtent_type
.clone();
if econtent_type != OID_CT_ROUTE_ORIGIN_AUTHZ {
return Err(RoaDecodeError::InvalidEContentType(econtent_type));
return Err(RoaProfileError::InvalidEContentType(econtent_type).into());
}
let roa = RoaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
@@ -166,67 +234,26 @@ pub struct IpPrefix {
}
impl RoaEContent {
/// Decode the DER-encoded RouteOriginAttestation defined in RFC 9582 §4.
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
let (rem, obj) = parse_der(der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
/// Parse step of scheme A (`parse → validate → verify`).
pub fn parse_der(der: &[u8]) -> Result<RoaEContentParsed, RoaParseError> {
let (rem, _obj) = parse_der(der).map_err(|e| RoaParseError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(RoaDecodeError::TrailingBytes(rem.len()));
return Err(RoaParseError::TrailingBytes(rem.len()));
}
Ok(RoaEContentParsed { der: der.to_vec() })
}
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if seq.len() != 2 && seq.len() != 3 {
return Err(RoaDecodeError::InvalidAttestationSequenceLen(seq.len()));
}
/// 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(())
}
let mut idx = 0;
let mut version: u32 = 0;
if seq.len() == 3 {
let v_obj = &seq[0];
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
return Err(RoaDecodeError::Parse(
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
));
}
let inner_der = v_obj
.as_slice()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
let (rem, inner) =
parse_der(inner_der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(RoaDecodeError::Parse(
"trailing bytes inside RouteOriginAttestation.version".into(),
));
}
let v = inner
.as_u64()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if v != 0 {
return Err(RoaDecodeError::InvalidVersion(v));
}
version = 0;
idx = 1;
}
let as_id_u64 = seq[idx]
.as_u64()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if as_id_u64 > u32::MAX as u64 {
return Err(RoaDecodeError::AsIdOutOfRange(as_id_u64));
}
let as_id = as_id_u64 as u32;
idx += 1;
let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?;
let mut out = Self {
version,
as_id,
ip_addr_blocks,
};
out.canonicalize();
Ok(out)
/// 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) {
@@ -241,7 +268,10 @@ impl RoaEContent {
///
/// 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> {
pub fn validate_against_ee_cert(
&self,
ee: &ResourceCertificate,
) -> Result<(), RoaValidateError> {
if ee.tbs.extensions.as_resources.is_some() {
return Err(RoaValidateError::EeAsResourcesPresent);
}
@@ -273,6 +303,91 @@ impl RoaEContent {
}
}
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> {
let (_rem, obj) =
parse_der(&self.der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
let seq = obj
.as_sequence()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if seq.len() != 2 && seq.len() != 3 {
return Err(RoaProfileError::InvalidAttestationSequenceLen(seq.len()));
}
let mut idx = 0;
let mut version: u32 = 0;
if seq.len() == 3 {
let v_obj = &seq[0];
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
return Err(RoaProfileError::ProfileDecode(
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
));
}
let inner_der = v_obj
.as_slice()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
let (rem, inner) =
parse_der(inner_der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if !rem.is_empty() {
return Err(RoaProfileError::ProfileDecode(
"trailing bytes inside RouteOriginAttestation.version".into(),
));
}
let v = inner
.as_u64()
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if v != 0 {
return Err(RoaProfileError::InvalidVersion(v));
}
version = 0;
idx = 1;
}
let as_id_u64 = seq[idx]
.as_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;
idx += 1;
let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?;
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,
@@ -285,60 +400,63 @@ fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix {
}
}
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaDecodeError> {
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaProfileError> {
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if seq.is_empty() || seq.len() > 2 {
return Err(RoaDecodeError::InvalidIpAddrBlocksLen(seq.len()));
return Err(RoaProfileError::InvalidIpAddrBlocksLen(seq.len()));
}
let mut out: Vec<RoaIpAddressFamily> = Vec::new();
for fam in seq {
let family = parse_ip_address_family(fam)?;
if out.iter().any(|f| f.afi == family.afi) {
return Err(RoaDecodeError::DuplicateAfi(family.afi));
return Err(RoaProfileError::DuplicateAfi(family.afi));
}
out.push(family);
}
Ok(out)
}
fn parse_ip_address_family(obj: &DerObject<'_>) -> Result<RoaIpAddressFamily, RoaDecodeError> {
fn parse_ip_address_family(obj: &DerObject<'_>) -> Result<RoaIpAddressFamily, RoaProfileError> {
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if seq.len() != 2 {
return Err(RoaDecodeError::InvalidIpAddressFamily);
return Err(RoaProfileError::InvalidIpAddressFamily);
}
let afi = parse_afi(&seq[0])?;
let addresses = parse_roa_addresses(afi, &seq[1])?;
if addresses.is_empty() {
return Err(RoaDecodeError::EmptyAddressList);
return Err(RoaProfileError::EmptyAddressList);
}
Ok(RoaIpAddressFamily { afi, addresses })
}
fn parse_afi(obj: &DerObject<'_>) -> Result<RoaAfi, RoaDecodeError> {
fn parse_afi(obj: &DerObject<'_>) -> Result<RoaAfi, RoaProfileError> {
let bytes = obj
.as_slice()
.map_err(|_e| RoaDecodeError::InvalidAddressFamily)?;
.map_err(|_e| RoaProfileError::InvalidAddressFamily)?;
if bytes.len() != 2 {
return Err(RoaDecodeError::InvalidAddressFamily);
return Err(RoaProfileError::InvalidAddressFamily);
}
match bytes {
[0x00, 0x01] => Ok(RoaAfi::Ipv4),
[0x00, 0x02] => Ok(RoaAfi::Ipv6),
_ => Err(RoaDecodeError::UnsupportedAfi(bytes.to_vec())),
_ => Err(RoaProfileError::UnsupportedAfi(bytes.to_vec())),
}
}
fn parse_roa_addresses(afi: RoaAfi, obj: &DerObject<'_>) -> Result<Vec<RoaIpAddress>, RoaDecodeError> {
fn parse_roa_addresses(
afi: RoaAfi,
obj: &DerObject<'_>,
) -> Result<Vec<RoaIpAddress>, RoaProfileError> {
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
let mut out = Vec::with_capacity(seq.len());
for entry in seq {
out.push(parse_roa_ip_address(afi, entry)?);
@@ -346,12 +464,12 @@ fn parse_roa_addresses(afi: RoaAfi, obj: &DerObject<'_>) -> Result<Vec<RoaIpAddr
Ok(out)
}
fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress, RoaDecodeError> {
fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress, RoaProfileError> {
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
if seq.is_empty() || seq.len() > 2 {
return Err(RoaDecodeError::InvalidRoaIpAddress);
return Err(RoaProfileError::InvalidRoaIpAddress);
}
let prefix = parse_prefix_bits(afi, &seq[0])?;
@@ -360,10 +478,10 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress
Some(m) => {
let v = m
.as_u64()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
let max_len: u16 = v
.try_into()
.map_err(|_e| RoaDecodeError::InvalidMaxLength {
.map_err(|_e| RoaProfileError::InvalidMaxLength {
afi,
prefix_len: prefix.prefix_len,
max_len: u16::MAX,
@@ -375,7 +493,7 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress
if let Some(max_len) = max_length {
let ub = afi.ub();
if max_len > ub || max_len < prefix.prefix_len {
return Err(RoaDecodeError::InvalidMaxLength {
return Err(RoaProfileError::InvalidMaxLength {
afi,
prefix_len: prefix.prefix_len,
max_len,
@@ -386,31 +504,31 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress
Ok(RoaIpAddress { prefix, max_length })
}
fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaDecodeError> {
fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaProfileError> {
let (unused_bits, bytes) = match &obj.content {
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
_ => return Err(RoaDecodeError::InvalidPrefixBitString),
_ => return Err(RoaProfileError::InvalidPrefixBitString),
};
if unused_bits > 7 {
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
return Err(RoaProfileError::InvalidPrefixUnusedBits);
}
if bytes.is_empty() {
if unused_bits != 0 {
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
return Err(RoaProfileError::InvalidPrefixUnusedBits);
}
} else if unused_bits != 0 {
let mask = (1u8 << unused_bits) - 1;
if (bytes[bytes.len() - 1] & mask) != 0 {
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
return Err(RoaProfileError::InvalidPrefixUnusedBits);
}
}
let prefix_len = (bytes.len() * 8)
.checked_sub(unused_bits as usize)
.ok_or(RoaDecodeError::InvalidPrefixUnusedBits)? as u16;
.ok_or(RoaProfileError::InvalidPrefixUnusedBits)? as u16;
if prefix_len > afi.ub() {
return Err(RoaDecodeError::PrefixLenOutOfRange { afi, prefix_len });
return Err(RoaProfileError::PrefixLenOutOfRange { afi, prefix_len });
}
let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes);

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,10 @@ use url::Url;
use x509_parser::prelude::{FromDer, X509Certificate};
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
use crate::data_model::rc::{AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate};
use crate::data_model::rc::{
AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate,
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
};
use crate::data_model::tal::Tal;
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -12,103 +15,160 @@ pub struct TaCertificate {
}
#[derive(Debug, thiserror::Error)]
pub enum TaCertificateError {
pub enum TaCertificateParseError {
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")]
Parse(String),
ResourceCertificate(#[from] ResourceCertificateParseError),
}
#[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1; RFC 6487 §4)")]
TrailingBytes(usize),
#[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)")]
#[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 self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)")]
InvalidSelfSignature(String),
#[error("TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)")]
#[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)")]
#[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)")]
#[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)")]
#[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 {
pub fn from_der(der: &[u8]) -> Result<Self, TaCertificateError> {
let rc_ca = ResourceCertificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
if rc_ca.kind != ResourceCertKind::Ca {
return Err(TaCertificateError::NotCa);
}
// Strong self-signed check: issuer==subject AND signature verifies with its own SPKI.
let (rem, cert) =
X509Certificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(TaCertificateError::TrailingBytes(rem.len()));
}
if cert.issuer().to_string() != cert.subject().to_string() {
return Err(TaCertificateError::NotSelfSignedIssuerSubject);
}
cert.verify_signature(None)
.map_err(|e| TaCertificateError::InvalidSelfSignature(e.to_string()))?;
Self::validate_rc_constraints(&rc_ca)?;
Ok(Self {
raw_der: der.to_vec(),
rc_ca,
/// 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()?)
}
/// 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<(), TaCertificateError> {
pub fn validate_rc_constraints(
rc_ca: &ResourceCertificate,
) -> Result<(), TaCertificateProfileError> {
if rc_ca.kind != ResourceCertKind::Ca {
return Err(TaCertificateError::NotCa);
return Err(TaCertificateProfileError::NotCa);
}
if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER) {
return Err(TaCertificateError::MissingOrInvalidCertificatePolicies);
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(TaCertificateError::MissingSubjectKeyIdentifier);
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(TaCertificateError::ResourcesMissing);
return Err(TaCertificateProfileError::ResourcesMissing);
}
let mut has_any_resource = false;
if let Some(ip) = ip {
if ip.has_any_inherit() {
return Err(TaCertificateError::IpResourcesInherit);
return Err(TaCertificateProfileError::IpResourcesInherit);
}
for fam in &ip.families {
match &fam.choice {
IpAddressChoice::Inherit => return Err(TaCertificateError::IpResourcesInherit),
IpAddressChoice::Inherit => {
return Err(TaCertificateProfileError::IpResourcesInherit);
}
IpAddressChoice::AddressesOrRanges(items) => {
if !items.is_empty() {
has_any_resource = true;
@@ -122,7 +182,7 @@ impl TaCertificate {
if matches!(asn.asnum, Some(AsIdentifierChoice::Inherit))
|| matches!(asn.rdi, Some(AsIdentifierChoice::Inherit))
{
return Err(TaCertificateError::AsResourcesInherit);
return Err(TaCertificateProfileError::AsResourcesInherit);
}
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.asnum.as_ref() {
if !items.is_empty() {
@@ -137,13 +197,33 @@ impl TaCertificate {
}
if !has_any_resource {
return Err(TaCertificateError::ResourcesEmpty);
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);
}
if rc_ca.tbs.issuer_dn != rc_ca.tbs.subject_dn {
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,
@@ -154,12 +234,20 @@ pub struct TrustAnchor {
#[derive(Debug, thiserror::Error)]
pub enum TrustAnchorError {
#[error("TA certificate error: {0} (RFC 8630 §2.3)")]
TaCertificate(#[from] TaCertificateError),
TaCertificate(#[from] TaCertificateDecodeError),
#[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)")]
#[error(
"TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)"
)]
TalSpkiMismatch,
}
@@ -167,16 +255,28 @@ 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(tal: Tal, ta_der: &[u8], resolved_uri: Option<&Url>) -> Result<Self, TrustAnchorError> {
pub fn bind_der(
tal: Tal,
ta_der: &[u8],
resolved_uri: Option<&Url>,
) -> Result<Self, TrustAnchorError> {
let ta_certificate = TaCertificate::decode_der(ta_der)?;
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 {
if !tal.ta_uris.iter().any(|x| x == u) {
return Err(TrustAnchorError::ResolvedUriNotInTal(u.to_string()));
return Err(TrustAnchorBindError::ResolvedUriNotInTal(u.to_string()));
}
}
let ta_certificate = TaCertificate::from_der(ta_der)?;
if tal.subject_public_key_info_der != ta_certificate.spki_der() {
return Err(TrustAnchorError::TalSpkiMismatch);
return Err(TrustAnchorBindError::TalSpkiMismatch);
}
Ok(Self {

View File

@@ -1,6 +1,13 @@
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>,
@@ -10,17 +17,22 @@ pub struct Tal {
}
#[derive(Debug, thiserror::Error)]
pub enum TalDecodeError {
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)")]
#[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)")]
@@ -29,10 +41,14 @@ pub enum TalDecodeError {
#[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)")]
#[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)")]
#[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)")]
@@ -42,90 +58,123 @@ pub enum TalDecodeError {
SpkiDerEmpty,
}
impl Tal {
pub fn decode_bytes(input: &[u8]) -> Result<Self, TalDecodeError> {
let raw = input.to_vec();
let text = std::str::from_utf8(input).map_err(|_| TalDecodeError::InvalidUtf8)?;
#[derive(Debug, thiserror::Error)]
pub enum TalDecodeError {
#[error("{0}")]
Parse(#[from] TalParseError),
let lines: Vec<&str> = text
#[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))
.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 < lines.len() && lines[idx].starts_with('#') {
comments.push(lines[idx][1..].to_string());
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 < lines.len() {
let line = lines[idx].trim();
while idx < self.lines.len() {
let line = self.lines[idx].trim();
if line.is_empty() {
break;
}
if line.starts_with('#') {
return Err(TalDecodeError::CommentAfterHeader);
return Err(TalProfileError::CommentAfterHeader);
}
let url = match Url::parse(line) {
Ok(u) => u,
Err(_) => {
if !ta_uris.is_empty() {
return Err(TalDecodeError::MissingSeparatorEmptyLine);
return Err(TalProfileError::MissingSeparatorEmptyLine);
}
return Err(TalDecodeError::InvalidUri(line.to_string()));
return Err(TalProfileError::InvalidUri(line.to_string()));
}
};
match url.scheme() {
"rsync" | "https" => {}
s => return Err(TalDecodeError::UnsupportedUriScheme(s.to_string())),
s => return Err(TalProfileError::UnsupportedUriScheme(s.to_string())),
}
if url.path().ends_with('/') {
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
return Err(TalProfileError::UriIsDirectory(line.to_string()));
}
if url.path_segments().and_then(|mut s| s.next_back()).unwrap_or("").is_empty() {
return Err(TalDecodeError::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(TalDecodeError::MissingTaUris);
return Err(TalProfileError::MissingTaUris);
}
// 3) Empty line separator (must exist).
if idx >= lines.len() || !lines[idx].trim().is_empty() {
return Err(TalDecodeError::MissingSeparatorEmptyLine);
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 < lines.len() {
let line = lines[idx].trim();
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(TalDecodeError::MissingSpki);
return Err(TalProfileError::MissingSpki);
}
let spki_der = base64::engine::general_purpose::STANDARD
.decode(b64.as_bytes())
.map_err(|_| TalDecodeError::SpkiBase64Decode)?;
.map_err(|_| TalProfileError::SpkiBase64Decode)?;
if spki_der.is_empty() {
return Err(TalDecodeError::SpkiDerEmpty);
return Err(TalProfileError::SpkiDerEmpty);
}
Ok(Self {
raw,
Ok(Tal {
raw: self.raw,
comments,
ta_uris,
subject_public_key_info_der: spki_der,