增加rc,完成所有模型的解析,优化error code 的RFC引用
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
1291
src/data_model/rc.rs
1291
src/data_model/rc.rs
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -1,3 +0,0 @@
|
||||
pub(crate) mod ip_resources;
|
||||
pub(crate) mod as_resources;
|
||||
pub mod resource;
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user