增加rc,完成所有模型的解析,优化error code 的RFC引用

This commit is contained in:
2026-02-03 16:50:52 +08:00
parent 56ae2ca4fc
commit cc9f3f21de
47 changed files with 3837 additions and 1516 deletions

View File

@@ -1,4 +1,5 @@
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};
@@ -19,73 +20,64 @@ pub struct AspaEContent {
#[derive(Debug, thiserror::Error)]
pub enum AspaDecodeError {
#[error("SignedObject decode error: {0}")]
#[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}")]
#[error("ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)")]
InvalidEContentType(String),
#[error("ASPA parse error: {0}")]
#[error("ASPA parse error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")]
Parse(String),
#[error("ASPA trailing bytes: {0} bytes")]
#[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")]
#[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")]
#[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}")]
#[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")]
#[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}")]
#[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")]
#[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")]
#[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 AspaValidateError {
#[error("ASPA EE certificate must contain AS resources extension (RFC 3779)")]
#[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")]
#[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")]
#[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 customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id})")]
#[error("ASPA EE certificate AS resources must not include RDI (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.5; RFC 6487 §4.8.11)")]
EeAsResourcesRdiPresent,
#[error("ASPA EE certificate AS resources must contain exactly one ASID (id element) (draft-ietf-sidrops-aspa-profile-21 §4)")]
EeAsResourcesNotSingleId,
#[error("ASPA customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id}) (draft-ietf-sidrops-aspa-profile-21 §4)")]
CustomerAsIdMismatch { customer_as_id: u32, ee_as_id: u32 },
#[error("ASPA EE certificate must not contain IP resources extension (RFC 3779)")]
#[error("ASPA EE certificate must not contain IP resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §2.2)")]
EeIpResourcesPresent,
}
/// Minimal EE resource information required to validate ASPA payload semantics.
///
/// The caller is responsible for extracting this from the EE certificate (RFC 3779).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EeAspaResources {
/// The single ASID carried in EE's AS resources "id" element (profile requires exactly one).
pub as_id: Option<u32>,
/// Whether EE's AS resources uses inherit (must be false).
pub as_resources_inherit: bool,
/// Whether EE's AS resources includes any range elements (must be false).
pub as_resources_range_present: bool,
/// Whether EE certificate contains IP resources extension (must be false).
pub ip_resources_present: bool,
}
impl AspaObject {
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
let signed_object = RpkiSignedObject::decode_der(der)?;
@@ -109,6 +101,12 @@ impl AspaObject {
econtent_type: OID_CT_ASPA.to_string(),
})
}
/// Validate this ASPA's embedded EE certificate resources.
pub fn validate_embedded_ee_cert(&self) -> Result<(), AspaValidateError> {
let ee = &self.signed_object.signed_data.certificates[0].resource_cert;
self.aspa.validate_against_ee_cert(ee)
}
}
impl AspaEContent {
@@ -165,27 +163,45 @@ impl AspaEContent {
})
}
/// Validate ASPA payload against EE resources extracted by the caller.
/// Validate ASPA payload against the embedded EE resource certificate.
///
/// This implements the EE/payload semantic checks described in
/// `draft-ietf-sidrops-aspa-profile-21` §4 (as summarized in `rpki/specs/08_aspa.md`).
pub fn validate_against_ee_resources(&self, ee: &EeAspaResources) -> Result<(), AspaValidateError> {
if ee.ip_resources_present {
pub fn validate_against_ee_cert(
&self,
ee: &ResourceCertificate,
) -> Result<(), AspaValidateError> {
if ee.tbs.extensions.ip_resources.is_some() {
return Err(AspaValidateError::EeIpResourcesPresent);
}
if ee.as_resources_inherit {
let asn = ee
.tbs
.extensions
.as_resources
.as_ref()
.ok_or(AspaValidateError::EeAsResourcesMissing)?;
if asn.rdi.is_some() {
return Err(AspaValidateError::EeAsResourcesRdiPresent);
}
if asn.is_asnum_inherit() {
return Err(AspaValidateError::EeAsResourcesInherit);
}
if ee.as_resources_range_present {
if asn.has_any_range() {
return Err(AspaValidateError::EeAsResourcesRangePresent);
}
let ee_as_id = ee.as_id.ok_or(AspaValidateError::EeAsResourcesMissing)?;
let ee_as_id = asn
.asnum_single_id()
.ok_or(AspaValidateError::EeAsResourcesNotSingleId)?;
if ee_as_id != self.customer_as_id {
return Err(AspaValidateError::CustomerAsIdMismatch {
customer_as_id: self.customer_as_id,
ee_as_id,
});
}
Ok(())
}
}

View File

@@ -69,7 +69,7 @@ impl BigUnsigned {
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{field} time encoding invalid for year {year}: got {encoding:?}")]
#[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,

View File

@@ -37,52 +37,52 @@ pub struct RpkixCrl {
#[derive(Debug, thiserror::Error)]
pub enum CrlDecodeError {
#[error("X.509 CRL parse error: {0}")]
#[error("X.509 CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
Parse(String),
#[error("trailing bytes after CRL DER: {0} bytes")]
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
TrailingBytes(usize),
#[error("CRL version must be v2, got {0:?}")]
#[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}")]
#[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")]
#[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")]
#[error("CRL signatureAlgorithm must match TBSCertList.signature (RFC 5280 §5.1)")]
SignatureAlgorithmMismatch,
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0}")]
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0} (RFC 9829 §3.1)")]
InvalidExtensionsCount(usize),
#[error("unsupported CRL extension OID {0}")]
#[error("unsupported CRL extension OID {0} (RFC 9829 §3.1)")]
UnsupportedExtension(String),
#[error("duplicate CRL extension OID {0}")]
#[error("duplicate CRL extension OID {0} (RFC 5280 §4.2; RFC 9829 §3.1)")]
DuplicateExtension(String),
#[error("AuthorityKeyIdentifier must contain keyIdentifier")]
#[error("AuthorityKeyIdentifier must contain keyIdentifier (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
AkiMissingKeyIdentifier,
#[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber")]
#[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
AkiHasOtherFields,
#[error("CRLNumber must be non-critical")]
#[error("CRLNumber must be non-critical (RFC 9829 §3.1; RFC 5280 §5.2.3)")]
CrlNumberCritical,
#[error("CRLNumber out of range (must fit in 0..2^159-1)")]
#[error("CRLNumber out of range (must fit in 0..2^159-1) (RFC 9829 §3.1)")]
CrlNumberOutOfRange,
#[error("CRL entry extensions must not be present")]
#[error("CRL entry extensions must not be present (RFC 6487 §5; RFC 5280 §5.1)")]
EntryExtensionsNotAllowed,
#[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5)")]
#[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:?}")]
#[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,
@@ -231,37 +231,37 @@ impl RpkixCrl {
#[derive(Debug, thiserror::Error)]
pub enum CrlVerifyError {
#[error("issuer certificate parse error: {0}")]
#[error("issuer certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
IssuerCertificateParse(String),
#[error("trailing bytes after issuer certificate DER: {0} bytes")]
#[error("trailing bytes after issuer certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
IssuerCertificateTrailingBytes(usize),
#[error("issuer SubjectPublicKeyInfo parse error: {0}")]
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
IssuerSpkiParse(String),
#[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes")]
#[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")]
IssuerSpkiTrailingBytes(usize),
#[error("CRL parse error: {0}")]
#[error("CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
CrlParse(String),
#[error("trailing bytes after CRL DER: {0} bytes")]
#[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")]
#[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")]
#[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")]
#[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}")]
#[error("CRL signature verification failed: {0} (RFC 5280 §6.3.3(g); RFC 7935 §2)")]
InvalidSignature(String),
}

View File

@@ -1,5 +1,6 @@
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 der_parser::ber::BerObjectContent;
use der_parser::der::{parse_der, DerObject, Tag};
@@ -30,61 +31,76 @@ pub struct FileAndHash {
#[derive(Debug, thiserror::Error)]
pub enum ManifestDecodeError {
#[error("signed object decode error: {0}")]
#[error("signed object decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4; RFC 9286 §4)")]
SignedObject(#[from] SignedObjectDecodeError),
#[error("DER parse error: {0}")]
#[error("DER parse error: {0} (RFC 9286 §4.2; DER)")]
Parse(String),
#[error("trailing bytes after DER object: {0} bytes")]
#[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}")]
#[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 must be a SEQUENCE of 5 or 6 elements, got {0}")]
#[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}")]
#[error("Manifest.version must be 0, got {0} (RFC 9286 §4.2.1)")]
InvalidManifestVersion(u64),
#[error("Manifest.manifestNumber must be non-negative INTEGER")]
#[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")]
#[error("Manifest.manifestNumber longer than 20 octets (RFC 9286 §4.2.1)")]
ManifestNumberTooLong,
#[error("Manifest.thisUpdate must be GeneralizedTime")]
#[error("Manifest.thisUpdate must be GeneralizedTime (RFC 9286 §4.2)")]
InvalidThisUpdate,
#[error("Manifest.nextUpdate must be GeneralizedTime")]
#[error("Manifest.nextUpdate must be GeneralizedTime (RFC 9286 §4.2)")]
InvalidNextUpdate,
#[error("Manifest.nextUpdate must be later than thisUpdate")]
#[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}")]
#[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")]
#[error("Manifest.fileList must be a SEQUENCE (RFC 9286 §4.2)")]
InvalidFileList,
#[error("FileAndHash must be SEQUENCE of 2")]
#[error("FileAndHash must be SEQUENCE of 2 (RFC 9286 §4.2)")]
InvalidFileAndHash,
#[error("fileList file name invalid: {0}")]
#[error("fileList file name invalid: {0} (RFC 9286 §4.2.2)")]
InvalidFileName(String),
#[error("fileList hash must be BIT STRING")]
#[error("fileList hash must be BIT STRING (RFC 9286 §4.2)")]
InvalidHashType,
#[error("fileList hash BIT STRING must be octet-aligned (unused bits=0)")]
#[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")]
#[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 ManifestValidateError {
#[error("Manifest EE certificate MUST include at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 3779; RFC 9286 §5.1)")]
EeResourcesMissing,
#[error("Manifest EE certificate IP resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §2.2.3.5)")]
EeIpResourcesNotInherit,
#[error("Manifest EE certificate AS resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §3.2.3.3)")]
EeAsResourcesNotInherit,
#[error("Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)")]
EeAsResourcesRdiPresent,
}
impl ManifestObject {
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
let signed_object = RpkiSignedObject::decode_der(der)?;
@@ -107,6 +123,44 @@ impl ManifestObject {
manifest,
})
}
/// Validate the embedded EE certificate resources against RFC 9286 §5.1.
///
/// This does **not** perform certificate path validation. It assumes `ee` is a parsed and
/// profile-validated RPKI EE resource certificate.
pub fn validate_against_ee_cert(
&self,
ee: &ResourceCertificate,
) -> Result<(), ManifestValidateError> {
let ip = ee.tbs.extensions.ip_resources.as_ref();
let asn = ee.tbs.extensions.as_resources.as_ref();
if ip.is_none() && asn.is_none() {
return Err(ManifestValidateError::EeResourcesMissing);
}
if let Some(ip) = ip {
if !ip.is_all_inherit() {
return Err(ManifestValidateError::EeIpResourcesNotInherit);
}
}
if let Some(asn) = asn {
if asn.rdi.is_some() {
return Err(ManifestValidateError::EeAsResourcesRdiPresent);
}
if !asn.is_asnum_inherit() {
return Err(ManifestValidateError::EeAsResourcesNotInherit);
}
}
Ok(())
}
/// Validate this manifest's embedded EE certificate resources.
pub fn validate_embedded_ee_cert(&self) -> Result<(), ManifestValidateError> {
let ee = &self.signed_object.signed_data.certificates[0].resource_cert;
self.validate_against_ee_cert(ee)
}
}
impl ManifestEContent {

View File

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

View File

@@ -9,6 +9,14 @@ pub const OID_CMS_ATTR_SIGNING_TIME: &str = "1.2.840.113549.1.9.5";
pub const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
// X.509 extensions (RFC 5280 / RFC 6487)
pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19";
pub const OID_KEY_USAGE: &str = "2.5.29.15";
pub const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37";
pub const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31";
pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1";
pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32";
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
pub const OID_CRL_NUMBER: &str = "2.5.29.20";
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
@@ -20,3 +28,15 @@ pub const OID_CT_ASPA: &str = "1.2.840.113549.1.9.16.1.49";
// X.509 extensions / access methods (RFC 5280 / RFC 6487)
pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11";
pub const OID_AD_SIGNED_OBJECT: &str = "1.3.6.1.5.5.7.48.11";
pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2";
pub const OID_AD_CA_REPOSITORY: &str = "1.3.6.1.5.5.7.48.5";
pub const OID_AD_RPKI_MANIFEST: &str = "1.3.6.1.5.5.7.48.10";
pub const OID_AD_RPKI_NOTIFY: &str = "1.3.6.1.5.5.7.48.13";
// RFC 3779 resource extensions (RFC 6487 profile)
pub const OID_IP_ADDR_BLOCKS: &str = "1.3.6.1.5.5.7.1.7";
pub const OID_AUTONOMOUS_SYS_IDS: &str = "1.3.6.1.5.5.7.1.8";
// RPKI CP (RFC 6484 / RFC 6487)
pub const OID_CP_IPADDR_ASNUMBER: &str = "1.3.6.1.5.5.7.14.2";

View File

@@ -1,13 +0,0 @@
pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19";
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
pub const OID_KEY_USAGE: &str = "2.5.29.15";
pub const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37";
pub const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31";
pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1";
pub const OID_ACCESS_DESCRIPTION: &str = "1.3.6.1.5.5.7.48";
pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2";
pub const OID_AD_OCSP: &str = "1.3.6.1.5.5.7.48.1";
pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11";
pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32";
pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";

File diff suppressed because it is too large Load Diff

View File

@@ -1,90 +0,0 @@
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ASIdentifiers {
pub asn: Vec<ASIdentifierChoice>
}
// ASN
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ASIdentifierChoice {
Inherit,
ASIDsOrRanges(Vec<ASIDOrRange>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ASIDOrRange {
Id(Asn),
AsRange(ASRange),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ASRange {
pub min: Asn,
pub max: Asn,
}
impl ASRange {
/// Creates a new AS number range from the smallest and largest number.
pub fn new(min: Asn, max: Asn) -> Self {
ASRange { min, max }
}
/// Returns an AS block covering all ASNs.
pub fn all() -> ASRange {
ASRange::new(Asn::MIN, Asn::MAX)
}
/// Returns the smallest AS number that is part of this range.
pub fn min(self) -> Asn {
self.min
}
/// Returns the largest AS number that is still part of this range.
pub fn max(self) -> Asn {
self.max
}
/// Returns the number of ASNs covered by this value.
pub fn asn_count(self) -> u32 {
u32::from(self.max) - u32::from(self.min) + 1
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Asn(u32);
impl Asn {
pub const MIN: Asn = Asn(u32::MIN);
pub const MAX: Asn = Asn(u32::MAX);
/// Creates an AS number from a `u32`.
pub fn from_u32(value: u32) -> Self {
Asn(value)
}
/// Converts an AS number into a `u32`.
pub fn into_u32(self) -> u32 {
self.0
}
/// Converts an AS number into a network-order byte array.
pub fn to_raw(self) -> [u8; 4] {
self.0.to_be_bytes()
}
}
impl From<u32> for Asn {
fn from(id: u32) -> Self {
Asn(id)
}
}
impl From<Asn> for u32 {
fn from(id: Asn) -> Self {
id.0
}
}

View File

@@ -1,46 +0,0 @@
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IPAddrBlocks {
ips: Vec<IPAddressFamily>
}
// IP Address Family
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IPAddressFamily {
pub address_family: Afi,
pub ip_address_choice: IPAddressChoice,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Afi {
Ipv4,
Ipv6,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IPAddressChoice {
Inherit,
AddressOrRange(Vec<IPAddressOrRange>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IPAddressOrRange {
AddressPrefix(IPAddressPrefix),
AddressRange(IPAddressRange),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IPAddressPrefix {
pub address: IPAddress,
pub prefix_length: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IPAddressRange {
pub min: IPAddress,
pub max: IPAddress,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct IPAddress(u128);

View File

@@ -1,3 +0,0 @@
pub(crate) mod ip_resources;
pub(crate) mod as_resources;
pub mod resource;

View File

@@ -1,10 +0,0 @@
use crate::data_model::resources::as_resources::ASIdentifiers;
use crate::data_model::resources::ip_resources::IPAddrBlocks;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResourceSet {
ip_addr_blocks: IPAddrBlocks,
as_identifiers: ASIdentifiers,
}

View File

@@ -1,4 +1,5 @@
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 der_parser::ber::{BerObjectContent, Class};
use der_parser::der::{parse_der, DerObject, Tag};
@@ -19,58 +20,58 @@ pub struct RoaEContent {
#[derive(Debug, thiserror::Error)]
pub enum RoaDecodeError {
#[error("SignedObject decode error: {0}")]
#[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}")]
#[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")]
InvalidEContentType(String),
#[error("ROA parse error: {0}")]
#[error("ROA parse error: {0} (RFC 9582 §4; DER)")]
Parse(String),
#[error("ROA trailing bytes: {0} bytes")]
#[error("ROA trailing bytes: {0} bytes (RFC 9582 §4; DER)")]
TrailingBytes(usize),
#[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0}")]
#[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0} (RFC 9582 §4)")]
InvalidAttestationSequenceLen(usize),
#[error("ROA version must be 0, got {0}")]
#[error("ROA version must be 0, got {0} (RFC 9582 §4.1)")]
InvalidVersion(u64),
#[error("ROA asID out of range (0..=4294967295), got {0}")]
#[error("ROA asID out of range (0..=4294967295), got {0} (RFC 9582 §4.2)")]
AsIdOutOfRange(u64),
#[error("ROA ipAddrBlocks must have length 1..2, got {0}")]
#[error("ROA ipAddrBlocks must have length 1..2, got {0} (RFC 9582 §4; RFC 9582 §4.3.1)")]
InvalidIpAddrBlocksLen(usize),
#[error("ROAIPAddressFamily must be a SEQUENCE of 2 elements")]
#[error("ROAIPAddressFamily must be a SEQUENCE of 2 elements (RFC 9582 §4.3.1)")]
InvalidIpAddressFamily,
#[error("ROA addressFamily must be an OCTET STRING of 2 bytes")]
#[error("ROA addressFamily must be an OCTET STRING of 2 bytes (RFC 9582 §4.3.1)")]
InvalidAddressFamily,
#[error("ROA addressFamily AFI not supported: {0:02X?}")]
#[error("ROA addressFamily AFI not supported: {0:02X?} (RFC 9582 §4.3.1)")]
UnsupportedAfi(Vec<u8>),
#[error("ROA contains duplicate AFI {0:?}")]
#[error("ROA contains duplicate AFI {0:?} (RFC 9582 §4.3.1)")]
DuplicateAfi(RoaAfi),
#[error("ROAAddresses must have at least one entry")]
#[error("ROAAddresses must have at least one entry (RFC 9582 §4.3.2)")]
EmptyAddressList,
#[error("ROAIPAddress must be a SEQUENCE of 1..2 elements")]
#[error("ROAIPAddress must be a SEQUENCE of 1..2 elements (RFC 9582 §4.3.2)")]
InvalidRoaIpAddress,
#[error("ROAIPAddress.address must be a BIT STRING")]
#[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")]
#[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:?}")]
#[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}")]
#[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,
@@ -83,10 +84,13 @@ pub enum RoaValidateError {
#[error("ROA EE certificate must not contain AS resources extension (RFC 9582 §5)")]
EeAsResourcesPresent,
#[error("ROA EE certificate must contain IP resources extension (RFC 9582 §5)")]
EeIpResourcesMissing,
#[error("ROA EE certificate IP resources must not use inherit (RFC 9582 §5)")]
EeIpResourcesInherit,
#[error("ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len}")]
#[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>,
@@ -94,26 +98,6 @@ pub enum RoaValidateError {
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IpResourceSet {
pub prefixes: Vec<IpPrefix>,
}
impl IpResourceSet {
pub fn contains_prefix(&self, p: &IpPrefix) -> bool {
self.prefixes
.iter()
.any(|r| r.afi == p.afi && prefix_covers(r, p))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EeResources {
pub ip_resources: IpResourceSet,
pub ip_resources_inherit: bool,
pub as_resources_present: bool,
}
impl RoaObject {
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
let signed_object = RpkiSignedObject::decode_der(der)?;
@@ -137,6 +121,12 @@ impl RoaObject {
econtent_type: OID_CT_ROUTE_ORIGIN_AUTHZ.to_string(),
})
}
/// Validate this ROA's embedded EE certificate resources.
pub fn validate_embedded_ee_cert(&self) -> Result<(), RoaValidateError> {
let ee = &self.signed_object.signed_data.certificates[0].resource_cert;
self.roa.validate_against_ee_cert(ee)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -247,21 +237,30 @@ impl RoaEContent {
}
}
/// Validate ROA payload against EE resources extracted by the caller.
/// Validate ROA payload against the embedded EE resource certificate (RFC 9582 §5).
///
/// This implements the ROA/EE semantic checks from RFC 9582 §5 that do not
/// require certificate path validation.
pub fn validate_against_ee_resources(&self, ee: &EeResources) -> Result<(), RoaValidateError> {
if ee.as_resources_present {
/// This performs the EE/payload semantic checks that do not require certificate path
/// validation.
pub fn validate_against_ee_cert(&self, ee: &ResourceCertificate) -> Result<(), RoaValidateError> {
if ee.tbs.extensions.as_resources.is_some() {
return Err(RoaValidateError::EeAsResourcesPresent);
}
if ee.ip_resources_inherit {
let ip = ee
.tbs
.extensions
.ip_resources
.as_ref()
.ok_or(RoaValidateError::EeIpResourcesMissing)?;
if ip.has_any_inherit() {
return Err(RoaValidateError::EeIpResourcesInherit);
}
for fam in &self.ip_addr_blocks {
for entry in &fam.addresses {
if !ee.ip_resources.contains_prefix(&entry.prefix) {
let rc_prefix = roa_prefix_to_rc(&entry.prefix);
if !ip.contains_prefix(&rc_prefix) {
return Err(RoaValidateError::PrefixNotInEeResources {
afi: entry.prefix.afi,
addr: entry.prefix.addr.clone(),
@@ -274,6 +273,18 @@ impl RoaEContent {
}
}
fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix {
let afi = match p.afi {
RoaAfi::Ipv4 => RcAfi::Ipv4,
RoaAfi::Ipv6 => RcAfi::Ipv6,
};
RcIpPrefix {
afi,
prefix_len: p.prefix_len,
addr: p.addr.clone(),
}
}
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaDecodeError> {
let seq = obj
.as_sequence()
@@ -434,33 +445,3 @@ fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> Vec<u
}
addr
}
fn prefix_covers(resource: &IpPrefix, subject: &IpPrefix) -> bool {
if resource.afi != subject.afi {
return false;
}
if resource.prefix_len > subject.prefix_len {
return false;
}
let full_len = match resource.afi {
RoaAfi::Ipv4 => 4,
RoaAfi::Ipv6 => 16,
};
if resource.addr.len() != full_len || subject.addr.len() != full_len {
return false;
}
let n = resource.prefix_len as usize;
let whole = n / 8;
let rem = (n % 8) as u8;
if resource.addr[..whole] != subject.addr[..whole] {
return false;
}
if rem == 0 {
return true;
}
let mask = 0xFFu8 << (8 - rem);
(resource.addr[whole] & mask) == (subject.addr[whole] & mask)
}

View File

@@ -2,17 +2,15 @@ use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc};
use crate::data_model::oid::{
OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, OID_CMS_ATTR_SIGNING_TIME,
OID_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SIGNED_DATA,
OID_AD_SIGNED_OBJECT, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER,
OID_AD_SIGNED_OBJECT, OID_SUBJECT_INFO_ACCESS,
};
use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess};
use der_parser::ber::Class;
use der_parser::der::{parse_der, DerObject, Tag};
use sha2::{Digest, Sha256};
use x509_parser::extensions::GeneralName;
use x509_parser::extensions::ParsedExtension;
use x509_parser::public_key::PublicKey;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
use x509_parser::certificate::X509Certificate;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResourceEeCertificate {
@@ -20,6 +18,7 @@ pub struct ResourceEeCertificate {
pub subject_key_identifier: Vec<u8>,
pub spki_der: Vec<u8>,
pub sia_signed_object_uris: Vec<String>,
pub resource_cert: ResourceCertificate,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -67,121 +66,121 @@ pub struct SignedAttrsProfiled {
#[derive(Debug, thiserror::Error)]
pub enum SignedObjectDecodeError {
#[error("DER parse error: {0}")]
#[error("DER parse error: {0} (RFC 6488 §2; RFC 6488 §3(1l); RFC 5652 §3/§5)")]
Parse(String),
#[error("trailing bytes after DER object: {0} bytes")]
#[error("trailing bytes after DER object: {0} bytes (DER; RFC 6488 §3(1l))")]
TrailingBytes(usize),
#[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0}")]
#[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0} (RFC 6488 §3(1a); RFC 5652 §3)")]
InvalidContentInfoContentType(String),
#[error("SignedData.version must be 3, got {0}")]
#[error("SignedData.version must be 3, got {0} (RFC 6488 §2.1.1; RFC 6488 §3(1b); RFC 5652 §5.1)")]
InvalidSignedDataVersion(u64),
#[error("SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0}")]
#[error("SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 5652 §5.1)")]
InvalidDigestAlgorithmsCount(usize),
#[error("digest algorithm must be id-sha256 ({OID_SHA256}), got {0}")]
#[error("digest algorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 7935 §2)")]
InvalidDigestAlgorithm(String),
#[error("SignedData.certificates MUST be present")]
#[error("SignedData.certificates MUST be present (RFC 6488 §3(1c); RFC 5652 §5.1)")]
CertificatesMissing,
#[error("SignedData.certificates must contain exactly one EE certificate, got {0}")]
#[error("SignedData.certificates must contain exactly one EE certificate, got {0} (RFC 6488 §3(1c))")]
InvalidCertificatesCount(usize),
#[error("SignedData.crls MUST be omitted")]
#[error("SignedData.crls MUST be omitted (RFC 6488 §3(1d))")]
CrlsPresent,
#[error("SignedData.signerInfos must contain exactly one SignerInfo, got {0}")]
#[error("SignedData.signerInfos must contain exactly one SignerInfo, got {0} (RFC 6488 §2.1; RFC 6488 §3(1e); RFC 5652 §5.1)")]
InvalidSignerInfosCount(usize),
#[error("SignerInfo.version must be 3, got {0}")]
#[error("SignerInfo.version must be 3, got {0} (RFC 6488 §3(1e); RFC 5652 §5.3)")]
InvalidSignerInfoVersion(u64),
#[error("SignerInfo.sid must be subjectKeyIdentifier [0]")]
#[error("SignerInfo.sid must be subjectKeyIdentifier [0] (RFC 6488 §3(1c); RFC 5652 §5.3)")]
InvalidSignerIdentifier,
#[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0}")]
#[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §3(1j); RFC 7935 §2)")]
InvalidSignerInfoDigestAlgorithm(String),
#[error("SignerInfo.signedAttrs MUST be present")]
#[error("SignerInfo.signedAttrs MUST be present (RFC 9589 §4; RFC 6488 §3(1f))")]
SignedAttrsMissing,
#[error("SignerInfo.unsignedAttrs MUST be omitted")]
#[error("SignerInfo.unsignedAttrs MUST be omitted (RFC 6488 §3(1i))")]
UnsignedAttrsPresent,
#[error(
"SignerInfo.signatureAlgorithm must be rsaEncryption ({OID_RSA_ENCRYPTION}) or \
sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}"
sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6488 §3(1k); RFC 7935 §2)"
)]
InvalidSignatureAlgorithm(String),
#[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL")]
#[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")]
InvalidSignatureAlgorithmParameters,
#[error("signedAttrs contains unsupported attribute OID {0}")]
#[error("signedAttrs contains unsupported attribute OID {0} (RFC 9589 §4; RFC 6488 §2.1.6.4)")]
UnsupportedSignedAttribute(String),
#[error("signedAttrs contains duplicate attribute OID {0}")]
#[error("signedAttrs contains duplicate attribute OID {0} (RFC 6488 §2.1.6.4; RFC 9589 §4)")]
DuplicateSignedAttribute(String),
#[error("signedAttrs attribute {oid} attrValues must contain exactly one value, got {count}")]
#[error("signedAttrs attribute {oid} attrValues must contain exactly one value, got {count} (RFC 6488 §2.1.6.4; RFC 5652 §5.3)")]
InvalidSignedAttributeValuesCount { oid: String, count: usize },
#[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type}")]
#[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type} (RFC 6488 §3(1h); RFC 9589 §4)")]
ContentTypeAttrMismatch {
econtent_type: String,
attr_content_type: String,
},
#[error("EncapsulatedContentInfo.eContent MUST be present")]
#[error("EncapsulatedContentInfo.eContent MUST be present (RFC 6488 §2.1.3; RFC 5652 §5.2)")]
EContentMissing,
#[error("signedAttrs.message-digest does not match SHA-256(eContent)")]
#[error("signedAttrs.message-digest does not match SHA-256(eContent) (RFC 6488 §3(1f); RFC 5652 §11.2)")]
MessageDigestMismatch,
#[error("EE certificate parse error: {0}")]
#[error("EE certificate parse error: {0} (RFC 6488 §3(1c); RFC 6487 §4)")]
EeCertificateParse(String),
#[error("EE certificate missing SubjectKeyIdentifier extension")]
#[error("EE certificate missing SubjectKeyIdentifier extension (RFC 6488 §3(1c); RFC 6487 §4.8.2)")]
EeCertificateMissingSki,
#[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS})")]
#[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS}) (RFC 6487 §4.8.8.2)")]
EeCertificateMissingSia,
#[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT})")]
#[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT}) (RFC 6487 §4.8.8.2)")]
EeCertificateMissingSignedObjectSia,
#[error("EE certificate SIA id-ad-signedObject accessLocation must be a URI")]
#[error("EE certificate SIA id-ad-signedObject accessLocation must be a URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)")]
EeCertificateSignedObjectSiaNotUri,
#[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI")]
#[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")]
EeCertificateSignedObjectSiaNoRsync,
#[error("SignerInfo.sid SKI does not match EE certificate SKI")]
#[error("SignerInfo.sid SKI does not match EE certificate SKI (RFC 6488 §3(1c); RFC 5652 §5.3)")]
SidSkiMismatch,
#[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime)")]
#[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime) (RFC 5652 §11.3; RFC 9589 §4)")]
InvalidSigningTimeValue,
}
#[derive(Debug, thiserror::Error)]
pub enum SignedObjectVerifyError {
#[error("EE SubjectPublicKeyInfo parse error: {0}")]
#[error("EE SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
EeSpkiParse(String),
#[error("trailing bytes after EE SubjectPublicKeyInfo DER: {0} bytes")]
#[error("trailing bytes after EE SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")]
EeSpkiTrailingBytes(usize),
#[error("unsupported EE public key algorithm (only RSA supported in M3)")]
#[error("unsupported EE public key algorithm (only RSA supported in M3) (RFC 7935 §2)")]
UnsupportedEePublicKeyAlgorithm,
#[error("EE RSA public exponent invalid")]
#[error("EE RSA public exponent invalid (RFC 8017 §A.1.1; RFC 7935 §2)")]
InvalidEeRsaExponent,
#[error("signature verification failed")]
#[error("signature verification failed (RFC 6488 §3(2)-(3); RFC 5652 §5.3; RFC 7935 §2)")]
InvalidSignature,
}
@@ -463,80 +462,60 @@ fn parse_certificate_set_implicit(obj: &DerObject<'_>) -> Result<Vec<ResourceEeC
}
fn parse_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedObjectDecodeError> {
let (rem, cert) = X509Certificate::from_der(der)
.map_err(|e| SignedObjectDecodeError::EeCertificateParse(e.to_string()))?;
let _ = rem;
let rc = match ResourceCertificate::from_der(der) {
Ok(v) => v,
Err(e) => {
return match e {
crate::data_model::rc::ResourceCertificateError::SignedObjectSiaNotUri => {
Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri)
}
crate::data_model::rc::ResourceCertificateError::SignedObjectSiaNoRsync => {
Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync)
}
_ => Err(SignedObjectDecodeError::EeCertificateParse(e.to_string())),
};
}
};
let ski = cert
.extensions()
.iter()
.find(|ext| ext.oid.to_id_string() == OID_SUBJECT_KEY_IDENTIFIER)
.and_then(|ext| match ext.parsed_extension() {
ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()),
_ => None,
})
let ski = rc
.tbs
.extensions
.subject_key_identifier
.clone()
.ok_or(SignedObjectDecodeError::EeCertificateMissingSki)?;
let spki_der = cert.public_key().raw.to_vec();
let spki_der = rc.tbs.subject_public_key_info.clone();
let sia_signed_object_uris = parse_ee_sia_signed_object_uris(&cert)?;
let sia = rc
.tbs
.extensions
.subject_info_access
.as_ref()
.ok_or(SignedObjectDecodeError::EeCertificateMissingSia)?;
let signed_object_uris: Vec<String> = match sia {
SubjectInfoAccess::Ee(ee) => ee
.signed_object_uris
.iter()
.map(|u| u.as_str().to_string())
.collect(),
SubjectInfoAccess::Ca(_ca) => Vec::new(),
};
if signed_object_uris.is_empty() {
return Err(SignedObjectDecodeError::EeCertificateMissingSignedObjectSia);
}
if !signed_object_uris.iter().any(|u| u.starts_with("rsync://")) {
return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync);
}
Ok(ResourceEeCertificate {
raw_der: der.to_vec(),
subject_key_identifier: ski,
spki_der,
sia_signed_object_uris,
sia_signed_object_uris: signed_object_uris,
resource_cert: rc,
})
}
fn parse_ee_sia_signed_object_uris(
cert: &X509Certificate<'_>,
) -> Result<Vec<String>, SignedObjectDecodeError> {
let mut sia: Option<x509_parser::extensions::SubjectInfoAccess<'_>> = None;
for ext in cert.extensions() {
if ext.oid.to_id_string() != OID_SUBJECT_INFO_ACCESS {
continue;
}
match ext.parsed_extension() {
ParsedExtension::SubjectInfoAccess(s) => {
if sia.is_some() {
return Err(SignedObjectDecodeError::Parse(
"duplicate SubjectInfoAccess extensions".into(),
));
}
sia = Some(s.clone());
}
ParsedExtension::ParseError { error } => {
return Err(SignedObjectDecodeError::Parse(error.to_string()));
}
_ => {
return Err(SignedObjectDecodeError::Parse(
"SubjectInfoAccess extension parse failed".into(),
));
}
}
}
let sia = sia.ok_or(SignedObjectDecodeError::EeCertificateMissingSia)?;
let mut uris: Vec<String> = Vec::new();
for ad in sia.iter() {
if ad.access_method.to_id_string() != OID_AD_SIGNED_OBJECT {
continue;
}
match &ad.access_location {
GeneralName::URI(u) => uris.push((*u).to_string()),
_ => return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri),
}
}
if uris.is_empty() {
return Err(SignedObjectDecodeError::EeCertificateMissingSignedObjectSia);
}
if !uris.iter().any(|u| u.starts_with("rsync://")) {
return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync);
}
Ok(uris)
}
fn parse_signer_info(obj: &DerObject<'_>) -> Result<SignerInfoProfiled, SignedObjectDecodeError> {
let seq = obj
.as_sequence()

View File

@@ -1,27 +1,188 @@
use url::Url;
use x509_parser::prelude::*;
use x509_parser::prelude::{FromDer, X509Certificate};
use crate::data_model::resources::resource::ResourceSet;
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
use crate::data_model::rc::{AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate};
use crate::data_model::tal::Tal;
//
// #[derive(Debug, Clone)]
// pub struct TrustAnchorCert {
// /// 信任锚证书名称
// pub name: String,
//
// /// 证书原始DER内容
// pub cert_der: Vec<u8>,
//
// /// 证书
// pub cert: X509Certificate<'static>,
//
// /// 资源集合
// pub resources: ResourceSet,
//
// ///发布点
// pub publication_point: Url,
// }
//
// impl TrustAnchorCert {
//
// }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaCertificate {
pub raw_der: Vec<u8>,
pub rc_ca: ResourceCertificate,
}
#[derive(Debug, thiserror::Error)]
pub enum TaCertificateError {
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")]
Parse(String),
#[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1; RFC 6487 §4)")]
TrailingBytes(usize),
#[error("TA certificate must be a CA certificate (RFC 8630 §2.3; RFC 6487 §4.8.1)")]
NotCa,
#[error("TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)")]
NotSelfSignedIssuerSubject,
#[error("TA certificate 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)")]
MissingOrInvalidCertificatePolicies,
#[error("TA certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8630 §2.3)")]
MissingSubjectKeyIdentifier,
#[error("TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)")]
ResourcesMissing,
#[error("TA certificate resources must be non-empty (RFC 8630 §2.3)")]
ResourcesEmpty,
#[error("TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)")]
IpResourcesInherit,
#[error("TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)")]
AsResourcesInherit,
}
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,
})
}
pub fn spki_der(&self) -> &[u8] {
&self.rc_ca.tbs.subject_public_key_info
}
/// 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> {
if rc_ca.kind != ResourceCertKind::Ca {
return Err(TaCertificateError::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.subject_key_identifier.is_none() {
return Err(TaCertificateError::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);
}
let mut has_any_resource = false;
if let Some(ip) = ip {
if ip.has_any_inherit() {
return Err(TaCertificateError::IpResourcesInherit);
}
for fam in &ip.families {
match &fam.choice {
IpAddressChoice::Inherit => return Err(TaCertificateError::IpResourcesInherit),
IpAddressChoice::AddressesOrRanges(items) => {
if !items.is_empty() {
has_any_resource = true;
}
}
}
}
}
if let Some(asn) = asn {
if matches!(asn.asnum, Some(AsIdentifierChoice::Inherit))
|| matches!(asn.rdi, Some(AsIdentifierChoice::Inherit))
{
return Err(TaCertificateError::AsResourcesInherit);
}
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.asnum.as_ref() {
if !items.is_empty() {
has_any_resource = true;
}
}
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.rdi.as_ref() {
if !items.is_empty() {
has_any_resource = true;
}
}
}
if !has_any_resource {
return Err(TaCertificateError::ResourcesEmpty);
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrustAnchor {
pub tal: Tal,
pub ta_certificate: TaCertificate,
pub resolved_ta_uri: Option<Url>,
}
#[derive(Debug, thiserror::Error)]
pub enum TrustAnchorError {
#[error("TA certificate error: {0} (RFC 8630 §2.3)")]
TaCertificate(#[from] TaCertificateError),
#[error("resolved TA URI not listed in TAL: {0} (RFC 8630 §2.2-§2.3)")]
ResolvedUriNotInTal(String),
#[error("TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)")]
TalSpkiMismatch,
}
impl TrustAnchor {
/// Bind a TAL and a downloaded TA certificate.
///
/// This does not download anything; it only validates the binding rules from RFC 8630 §2.3.
pub fn bind(tal: Tal, ta_der: &[u8], resolved_uri: Option<&Url>) -> Result<Self, TrustAnchorError> {
if let Some(u) = resolved_uri {
if !tal.ta_uris.iter().any(|x| x == u) {
return Err(TrustAnchorError::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);
}
Ok(Self {
tal,
ta_certificate,
resolved_ta_uri: resolved_uri.cloned(),
})
}
}

View File

@@ -1,18 +1,134 @@
/// TAL Model
#[derive(Clone, Debug)]
use base64::Engine;
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tal {
/// Optional human-readable comments
pub raw: Vec<u8>,
pub comments: Vec<String>,
/// Ordered list of URIs pointing to the TA certificate
pub uris: Vec<TalUri>,
/// SubjectPublicKeyInfo DER
pub spki_der: Vec<u8>,
pub ta_uris: Vec<Url>,
pub subject_public_key_info_der: Vec<u8>,
}
#[derive(Debug, Clone)]
pub enum TalUri {
Rsync(String),
Https(String),
#[derive(Debug, thiserror::Error)]
pub enum TalDecodeError {
#[error("TAL must be valid UTF-8 (RFC 8630 §2.2)")]
InvalidUtf8,
#[error("TAL comments must appear only at the beginning (RFC 8630 §2.2)")]
CommentAfterHeader,
#[error("TAL must contain at least one TA URI line (RFC 8630 §2.2)")]
MissingTaUris,
#[error("TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)")]
MissingSeparatorEmptyLine,
#[error("TAL TA URI invalid: {0} (RFC 8630 §2.2)")]
InvalidUri(String),
#[error("TAL TA URI scheme must be rsync or https, got {0} (RFC 8630 §2.2)")]
UnsupportedUriScheme(String),
#[error("TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)")]
UriIsDirectory(String),
#[error("TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)")]
MissingSpki,
#[error("TAL SPKI base64 decode failed (RFC 8630 §2.2)")]
SpkiBase64Decode,
#[error("TAL SPKI DER is empty (RFC 8630 §2.2)")]
SpkiDerEmpty,
}
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)?;
let lines: Vec<&str> = text
.split('\n')
.map(|l| l.strip_suffix('\r').unwrap_or(l))
.collect();
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());
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();
if line.is_empty() {
break;
}
if line.starts_with('#') {
return Err(TalDecodeError::CommentAfterHeader);
}
let url = match Url::parse(line) {
Ok(u) => u,
Err(_) => {
if !ta_uris.is_empty() {
return Err(TalDecodeError::MissingSeparatorEmptyLine);
}
return Err(TalDecodeError::InvalidUri(line.to_string()));
}
};
match url.scheme() {
"rsync" | "https" => {}
s => return Err(TalDecodeError::UnsupportedUriScheme(s.to_string())),
}
if url.path().ends_with('/') {
return Err(TalDecodeError::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()));
}
ta_uris.push(url);
idx += 1;
}
if ta_uris.is_empty() {
return Err(TalDecodeError::MissingTaUris);
}
// 3) Empty line separator (must exist).
if idx >= lines.len() || !lines[idx].trim().is_empty() {
return Err(TalDecodeError::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();
if !line.is_empty() {
b64.push_str(line);
}
idx += 1;
}
if b64.is_empty() {
return Err(TalDecodeError::MissingSpki);
}
let spki_der = base64::engine::general_purpose::STANDARD
.decode(b64.as_bytes())
.map_err(|_| TalDecodeError::SpkiBase64Decode)?;
if spki_der.is_empty() {
return Err(TalDecodeError::SpkiDerEmpty);
}
Ok(Self {
raw,
comments,
ta_uris,
subject_public_key_info_der: spki_der,
})
}
}