From 6de0bced1c8b61dd89542a2b0a5fdd85b77a2d35 Mon Sep 17 00:00:00 2001 From: yuyr Date: Sat, 11 Jul 2026 21:40:52 +0800 Subject: [PATCH] add role-aware resource certificate profile --- src/data_model/oid.rs | 1 + src/data_model/rc.rs | 258 ++++++++++++++++-- src/data_model/router_cert.rs | 2 + src/data_model/signed_object.rs | 4 +- src/data_model/ta.rs | 2 + src/validation/ca_path.rs | 42 ++- src/validation/cert_path.rs | 31 ++- src/validation/tree_runner.rs | 14 +- tests/bench_stage2_collect_selected_der_v2.rs | 2 +- tests/test_aspa_validate_ee_resources.rs | 3 + tests/test_roa_validate_ee_resources.rs | 3 + tests/test_ta_certificate.rs | 6 + 12 files changed, 329 insertions(+), 39 deletions(-) diff --git a/src/data_model/oid.rs b/src/data_model/oid.rs index 5e351ce..a5504cb 100644 --- a/src/data_model/oid.rs +++ b/src/data_model/oid.rs @@ -29,6 +29,7 @@ pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1"; pub const OID_AUTHORITY_INFO_ACCESS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.1); pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32"; pub const OID_CERTIFICATE_POLICIES_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.32); +pub const OID_QT_CPS: &str = "1.3.6.1.5.5.7.2.1"; pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35"; pub const OID_AUTHORITY_KEY_IDENTIFIER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.35); diff --git a/src/data_model/rc.rs b/src/data_model/rc.rs index 2a0329a..701ea91 100644 --- a/src/data_model/rc.rs +++ b/src/data_model/rc.rs @@ -13,11 +13,14 @@ use crate::data_model::common::{ use crate::data_model::oid::{ OID_AD_CA_ISSUERS_RAW, OID_AD_CA_REPOSITORY, OID_AD_CA_REPOSITORY_RAW, OID_AD_RPKI_MANIFEST, OID_AD_RPKI_MANIFEST_RAW, OID_AD_RPKI_NOTIFY, OID_AD_RPKI_NOTIFY_RAW, OID_AD_SIGNED_OBJECT, - OID_AD_SIGNED_OBJECT_RAW, OID_AUTHORITY_INFO_ACCESS_RAW, OID_AUTHORITY_KEY_IDENTIFIER_RAW, - OID_AUTONOMOUS_SYS_IDS_RAW, OID_BASIC_CONSTRAINTS_RAW, OID_CERTIFICATE_POLICIES_RAW, - OID_CP_IPADDR_ASNUMBER, OID_CP_IPADDR_ASNUMBER_RAW, OID_CRL_DISTRIBUTION_POINTS_RAW, - OID_IP_ADDR_BLOCKS_RAW, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, - OID_SUBJECT_INFO_ACCESS_RAW, OID_SUBJECT_KEY_IDENTIFIER_RAW, + OID_AD_SIGNED_OBJECT_RAW, OID_AUTHORITY_INFO_ACCESS, OID_AUTHORITY_INFO_ACCESS_RAW, + OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTHORITY_KEY_IDENTIFIER_RAW, OID_AUTONOMOUS_SYS_IDS, + OID_AUTONOMOUS_SYS_IDS_RAW, OID_BASIC_CONSTRAINTS, OID_BASIC_CONSTRAINTS_RAW, + OID_CERTIFICATE_POLICIES, OID_CERTIFICATE_POLICIES_RAW, OID_CP_IPADDR_ASNUMBER, + OID_CP_IPADDR_ASNUMBER_RAW, OID_CRL_DISTRIBUTION_POINTS, OID_CRL_DISTRIBUTION_POINTS_RAW, + OID_EXTENDED_KEY_USAGE, OID_IP_ADDR_BLOCKS, OID_IP_ADDR_BLOCKS_RAW, OID_KEY_USAGE, OID_QT_CPS, + OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SUBJECT_INFO_ACCESS, + OID_SUBJECT_INFO_ACCESS_RAW, OID_SUBJECT_KEY_IDENTIFIER, OID_SUBJECT_KEY_IDENTIFIER_RAW, }; /// Resource Certificate kind (semantic classification). @@ -27,6 +30,14 @@ pub enum ResourceCertKind { Ee, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ResourceCertificateRole { + TrustAnchor, + Ca, + SignedObjectEe, + RouterEe, +} + /// A parsed RPKI Resource Certificate (RFC 6487) data model. /// /// This module intentionally focuses on the semantics needed by Signed Object validation and @@ -59,6 +70,7 @@ pub struct RpkixTbsCertificate { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RcExtensions { pub basic_constraints_ca: bool, + pub basic_constraints: Option, pub subject_key_identifier: Option>, /// Authority Key Identifier (AKI) keyIdentifier value. pub authority_key_identifier: Option>, @@ -68,11 +80,26 @@ pub struct RcExtensions { pub ca_issuers_uris: Option>, pub subject_info_access: Option, pub certificate_policies_oid: Option, + pub certificate_policies: Option, + pub extension_oids: Vec, pub ip_resources: Option, pub as_resources: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BasicConstraintsProfile { + pub ca: bool, + pub critical: bool, + pub path_len_constraint: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CertificatePoliciesProfile { + pub policy_oid: String, + pub qualifier_oids: Vec, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResourceCertificateParsed { pub raw_der: Vec, @@ -114,13 +141,14 @@ impl AlgorithmIdentifierValue { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RcExtensionsParsed { - pub basic_constraints_ca: Vec, + pub basic_constraints: Vec, pub subject_key_identifier: Vec<(Vec, bool)>, pub authority_key_identifier: Vec<(AuthorityKeyIdentifierParsed, bool)>, pub crl_distribution_points: Vec<(CrlDistributionPointsParsed, bool)>, pub authority_info_access: Vec<(AuthorityInfoAccessParsed, bool)>, pub subject_info_access: Vec<(SubjectInfoAccessParsed, bool)>, - pub certificate_policies: Vec<(Vec, bool)>, + pub certificate_policies: Vec<(Vec, bool)>, + pub extension_oids: Vec, pub ip_resources: Vec<(IpResourceSet, bool)>, pub as_resources: Vec<(AsResourceSet, bool)>, } @@ -406,11 +434,45 @@ pub enum ResourceCertificateProfileError { #[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")] CertificatePoliciesCriticality, + #[error("certificatePolicies must be present (RFC 6487 §4.8.9)")] + CertificatePoliciesMissing, + #[error( "certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)" )] InvalidCertificatePolicy(String), + #[error("certificatePolicies may contain at most one CPS qualifier (RFC 6487 §4.8.9)")] + CertificatePoliciesTooManyQualifiers, + + #[error( + "certificatePolicies qualifier must be id-qt-cps ({OID_QT_CPS}), got {0} (RFC 6487 §4.8.9)" + )] + CertificatePoliciesInvalidQualifier(String), + + #[error("basicConstraints must be present in CA certificates (RFC 6487 §4.8.1)")] + BasicConstraintsMissing, + + #[error("basicConstraints criticality must be critical in CA certificates (RFC 6487 §4.8.1)")] + BasicConstraintsCriticality, + + #[error("basicConstraints cA must be TRUE in CA certificates (RFC 6487 §4.8.1)")] + BasicConstraintsCaFalse, + + #[error( + "basicConstraints pathLenConstraint must be absent in CA certificates (RFC 6487 §4.8.1)" + )] + BasicConstraintsPathLenPresent, + + #[error("basicConstraints must be absent in EE certificates (RFC 6487 §4.8.1)")] + BasicConstraintsEeMustOmit, + + #[error("extension {oid} is not permitted for {role} resource certificates (RFC 6487 §4.8)")] + DisallowedExtension { role: &'static str, oid: String }, + + #[error("autonomousSysIds RDI field must be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3)")] + AsResourcesRdiPresent, + #[error( "SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)" )] @@ -567,6 +629,87 @@ impl ResourceCertificate { Ok(()) } + pub fn validate_rfc6487_profile( + &self, + role: ResourceCertificateRole, + ) -> Result<(), ResourceCertificateProfileError> { + let role_name = match role { + ResourceCertificateRole::TrustAnchor => "trust anchor CA", + ResourceCertificateRole::Ca => "CA", + ResourceCertificateRole::SignedObjectEe => "signed-object EE", + ResourceCertificateRole::RouterEe => "router EE", + }; + let ca_role = matches!( + role, + ResourceCertificateRole::TrustAnchor | ResourceCertificateRole::Ca + ); + + if ca_role { + let constraints = self + .tbs + .extensions + .basic_constraints + .as_ref() + .ok_or(ResourceCertificateProfileError::BasicConstraintsMissing)?; + if !constraints.critical { + return Err(ResourceCertificateProfileError::BasicConstraintsCriticality); + } + if !constraints.ca { + return Err(ResourceCertificateProfileError::BasicConstraintsCaFalse); + } + if constraints.path_len_constraint.is_some() { + return Err(ResourceCertificateProfileError::BasicConstraintsPathLenPresent); + } + } else if self.tbs.extensions.basic_constraints.is_some() { + return Err(ResourceCertificateProfileError::BasicConstraintsEeMustOmit); + } + + for oid in &self.tbs.extensions.extension_oids { + if !is_permitted_extension(oid, role) { + return Err(ResourceCertificateProfileError::DisallowedExtension { + role: role_name, + oid: oid.clone(), + }); + } + } + + let policies = self + .tbs + .extensions + .certificate_policies + .as_ref() + .ok_or(ResourceCertificateProfileError::CertificatePoliciesMissing)?; + if policies.policy_oid != OID_CP_IPADDR_ASNUMBER { + return Err(ResourceCertificateProfileError::InvalidCertificatePolicy( + policies.policy_oid.clone(), + )); + } + if policies.qualifier_oids.len() > 1 { + return Err(ResourceCertificateProfileError::CertificatePoliciesTooManyQualifiers); + } + if let Some(qualifier_oid) = policies.qualifier_oids.first() { + if qualifier_oid != OID_QT_CPS { + return Err( + ResourceCertificateProfileError::CertificatePoliciesInvalidQualifier( + qualifier_oid.clone(), + ), + ); + } + } + + if self + .tbs + .extensions + .as_resources + .as_ref() + .is_some_and(|resources| resources.rdi.is_some()) + { + return Err(ResourceCertificateProfileError::AsResourcesRdiPresent); + } + + Ok(()) + } + pub fn validate_strict_name_profile(&self) -> Result<(), ResourceCertificateProfileError> { validate_strict_rpki_name(&self.tbs.issuer_name, "issuer")?; validate_strict_rpki_name(&self.tbs.subject_name, "subject")?; @@ -590,6 +733,22 @@ impl ResourceCertificate { } } +fn is_permitted_extension(oid: &str, role: ResourceCertificateRole) -> bool { + matches!( + oid, + OID_BASIC_CONSTRAINTS + | OID_KEY_USAGE + | OID_SUBJECT_KEY_IDENTIFIER + | OID_AUTHORITY_KEY_IDENTIFIER + | OID_CRL_DISTRIBUTION_POINTS + | OID_AUTHORITY_INFO_ACCESS + | OID_SUBJECT_INFO_ACCESS + | OID_CERTIFICATE_POLICIES + | OID_IP_ADDR_BLOCKS + | OID_AUTONOMOUS_SYS_IDS + ) || (role == ResourceCertificateRole::RouterEe && oid == OID_EXTENDED_KEY_USAGE) +} + fn validate_strict_rpki_name( name: &X509NameDer, role: &'static str, @@ -744,13 +903,24 @@ mod strict_name_tests { #[test] fn profile_rejects_rfc8360_v2_policy_oid() { let extensions = RcExtensionsParsed { - basic_constraints_ca: vec![true], + basic_constraints: vec![BasicConstraintsProfile { + ca: true, + critical: true, + path_len_constraint: None, + }], subject_key_identifier: Vec::new(), authority_key_identifier: Vec::new(), crl_distribution_points: Vec::new(), authority_info_access: Vec::new(), subject_info_access: Vec::new(), - certificate_policies: vec![(vec!["1.3.6.1.5.5.7.14.3".to_string()], true)], + certificate_policies: vec![( + vec![CertificatePoliciesProfile { + policy_oid: "1.3.6.1.5.5.7.14.3".to_string(), + qualifier_oids: Vec::new(), + }], + true, + )], + extension_oids: Vec::new(), ip_resources: Vec::new(), as_resources: Vec::new(), }; @@ -821,23 +991,25 @@ impl RcExtensionsParsed { // NOTE(perf): `self` is consumed. Prefer moving decoded fields out rather than cloning, // especially for large resource sets and URI lists. let RcExtensionsParsed { - basic_constraints_ca, + basic_constraints, subject_key_identifier, authority_key_identifier, crl_distribution_points, authority_info_access, subject_info_access, certificate_policies, + extension_oids, ip_resources, as_resources, } = self; - if basic_constraints_ca.len() > 1 { + if basic_constraints.len() > 1 { return Err(ResourceCertificateProfileError::DuplicateExtension( "basicConstraints", )); } - let basic_constraints_ca = basic_constraints_ca.first().copied().unwrap_or(false); + let basic_constraints = basic_constraints.into_iter().next(); + let basic_constraints_ca = basic_constraints.as_ref().is_some_and(|bc| bc.ca); let subject_key_identifier = match subject_key_identifier.len() { 0 => None, @@ -1032,25 +1204,25 @@ impl RcExtensionsParsed { } }; - let certificate_policies_oid = match certificate_policies.len() { + let certificate_policies = match certificate_policies.len() { 0 => None, 1 => { - let (oids, critical) = certificate_policies.into_iter().next().expect("len==1"); + let (policies, critical) = certificate_policies.into_iter().next().expect("len==1"); if !critical { return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality); } - if oids.len() != 1 { + if policies.len() != 1 { return Err(ResourceCertificateProfileError::InvalidCertificatePolicy( "expected exactly one policy".into(), )); } - let policy_oid = oids.into_iter().next().expect("len==1"); - if policy_oid != OID_CP_IPADDR_ASNUMBER { + let policy = policies.into_iter().next().expect("len==1"); + if policy.policy_oid != OID_CP_IPADDR_ASNUMBER { return Err(ResourceCertificateProfileError::InvalidCertificatePolicy( - policy_oid, + policy.policy_oid, )); } - Some(OID_CP_IPADDR_ASNUMBER.to_string()) + Some(policy) } _ => { return Err(ResourceCertificateProfileError::DuplicateExtension( @@ -1093,12 +1265,17 @@ impl RcExtensionsParsed { Ok(RcExtensions { basic_constraints_ca, + basic_constraints, subject_key_identifier, authority_key_identifier, crl_distribution_points_uris, ca_issuers_uris, subject_info_access, - certificate_policies_oid, + certificate_policies_oid: certificate_policies + .as_ref() + .map(|_| OID_CP_IPADDR_ASNUMBER.to_string()), + certificate_policies, + extension_oids, ip_resources, as_resources, }) @@ -1127,26 +1304,32 @@ fn algorithm_identifier_value( fn parse_extensions_parse( exts: &[X509Extension<'_>], ) -> Result { - let mut basic_constraints_ca: Vec = Vec::new(); + let mut basic_constraints: Vec = Vec::new(); let mut ski: Vec<(Vec, bool)> = Vec::new(); let mut aki: Vec<(AuthorityKeyIdentifierParsed, bool)> = Vec::new(); let mut crldp: Vec<(CrlDistributionPointsParsed, bool)> = Vec::new(); let mut aia: Vec<(AuthorityInfoAccessParsed, bool)> = Vec::new(); let mut sia: Vec<(SubjectInfoAccessParsed, bool)> = Vec::new(); - let mut cert_policies: Vec<(Vec, bool)> = Vec::new(); + let mut cert_policies: Vec<(Vec, bool)> = Vec::new(); + let mut extension_oids: Vec = Vec::with_capacity(exts.len()); let mut ip_resources: Vec<(IpResourceSet, bool)> = Vec::new(); let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new(); for ext in exts { let oid = ext.oid.as_bytes(); + extension_oids.push(ext.oid.to_id_string()); if oid == OID_BASIC_CONSTRAINTS_RAW { let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else { return Err(ResourceCertificateParseError::Parse( "basicConstraints parse failed".into(), )); }; - basic_constraints_ca.push(bc.ca); + basic_constraints.push(BasicConstraintsProfile { + ca: bc.ca, + critical: ext.critical, + path_len_constraint: bc.path_len_constraint, + }); } else if oid == OID_SUBJECT_KEY_IDENTIFIER_RAW { let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else { return Err(ResourceCertificateParseError::Parse( @@ -1195,16 +1378,30 @@ fn parse_extensions_parse( "certificatePolicies parse failed".into(), )); }; - let mut oids: Vec = Vec::with_capacity(cp.len()); + let mut policies: Vec = Vec::with_capacity(cp.len()); for p in cp.iter() { let b = p.policy_id.as_bytes(); - if b == OID_CP_IPADDR_ASNUMBER_RAW { - oids.push(OID_CP_IPADDR_ASNUMBER.to_string()); + let policy_oid = if b == OID_CP_IPADDR_ASNUMBER_RAW { + OID_CP_IPADDR_ASNUMBER.to_string() } else { - oids.push(p.policy_id.to_id_string()); - } + p.policy_id.to_id_string() + }; + let qualifier_oids = p + .policy_qualifiers + .as_ref() + .map(|qualifiers| { + qualifiers + .iter() + .map(|qualifier| qualifier.policy_qualifier_id.to_id_string()) + .collect() + }) + .unwrap_or_default(); + policies.push(CertificatePoliciesProfile { + policy_oid, + qualifier_oids, + }); } - cert_policies.push((oids, ext.critical)); + cert_policies.push((policies, ext.critical)); } else if oid == OID_IP_ADDR_BLOCKS_RAW { let parsed = IpResourceSet::decode_extn_value(ext.value) .map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?; @@ -1217,13 +1414,14 @@ fn parse_extensions_parse( } Ok(RcExtensionsParsed { - basic_constraints_ca, + basic_constraints, subject_key_identifier: ski, authority_key_identifier: aki, crl_distribution_points: crldp, authority_info_access: aia, subject_info_access: sia, certificate_policies: cert_policies, + extension_oids, ip_resources, as_resources, }) diff --git a/src/data_model/router_cert.rs b/src/data_model/router_cert.rs index 79a14d8..279706c 100644 --- a/src/data_model/router_cert.rs +++ b/src/data_model/router_cert.rs @@ -4,6 +4,7 @@ use crate::data_model::oid::{ use crate::data_model::rc::{ AsIdOrRange, AsIdentifierChoice, ResourceCertKind, ResourceCertificate, ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError, + ResourceCertificateRole, }; use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_predecoded_ee}; use x509_parser::extensions::ParsedExtension; @@ -202,6 +203,7 @@ impl BgpsecRouterCertificateParsed { self, ) -> Result { let rc = self.rc_parsed.validate_profile()?; + rc.validate_rfc6487_profile(ResourceCertificateRole::RouterEe)?; if rc.kind != ResourceCertKind::Ee { return Err(BgpsecRouterCertificateProfileError::NotEe); } diff --git a/src/data_model/signed_object.rs b/src/data_model/signed_object.rs index 3c8a377..a0e6437 100644 --- a/src/data_model/signed_object.rs +++ b/src/data_model/signed_object.rs @@ -8,7 +8,7 @@ use crate::data_model::oid::{ OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SIGNED_DATA, OID_SIGNED_DATA_RAW, OID_SUBJECT_INFO_ACCESS, }; -use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess}; +use crate::data_model::rc::{ResourceCertificate, ResourceCertificateRole, SubjectInfoAccess}; use asn1_rs::{Any, Class, FromBer, FromDer as Asn1FromDer, Header, Tag}; use ring::digest; use x509_parser::extensions::ParsedExtension; @@ -822,6 +822,8 @@ fn validate_ee_certificate( }; } }; + rc.validate_rfc6487_profile(ResourceCertificateRole::SignedObjectEe) + .map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?; if strict_name { rc.validate_strict_name_profile() .map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?; diff --git a/src/data_model/ta.rs b/src/data_model/ta.rs index 826979d..cd2c77a 100644 --- a/src/data_model/ta.rs +++ b/src/data_model/ta.rs @@ -5,6 +5,7 @@ use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER; use crate::data_model::rc::{ AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate, ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError, + ResourceCertificateRole, }; use crate::data_model::tal::Tal; @@ -218,6 +219,7 @@ impl TaCertificateParsed { if rc_ca.kind != ResourceCertKind::Ca { return Err(TaCertificateProfileError::NotCa); } + rc_ca.validate_rfc6487_profile(ResourceCertificateRole::TrustAnchor)?; if rc_ca.tbs.issuer_name != rc_ca.tbs.subject_name { return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject); diff --git a/src/validation/ca_path.rs b/src/validation/ca_path.rs index 2db4166..245e334 100644 --- a/src/validation/ca_path.rs +++ b/src/validation/ca_path.rs @@ -3,7 +3,8 @@ use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl}; use crate::data_model::oid::OID_KEY_USAGE_RAW; use crate::data_model::rc::{ AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind, - ResourceCertificate, ResourceCertificateDecodeError, + ResourceCertificate, ResourceCertificateDecodeError, ResourceCertificateProfileError, + ResourceCertificateRole, }; use crate::policy::ResourceValidationMode; use x509_parser::prelude::{FromDer, X509Certificate}; @@ -108,6 +109,12 @@ pub enum CaPathError { #[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")] IssuerDecode(ResourceCertificateDecodeError), + #[error("child CA certificate profile validation failed: {0} (RFC 6487 §4.8)")] + ChildProfile(ResourceCertificateProfileError), + + #[error("issuer CA certificate profile validation failed: {0} (RFC 6487 §4.8)")] + IssuerProfile(ResourceCertificateProfileError), + #[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")] CrlDecode(#[from] CrlDecodeError), @@ -244,12 +251,18 @@ pub fn validate_subordinate_ca_cert_with_resource_validation_mode( if child_ca.kind != ResourceCertKind::Ca { return Err(CaPathError::ChildNotCa); } + child_ca + .validate_rfc6487_profile(ResourceCertificateRole::Ca) + .map_err(CaPathError::ChildProfile)?; let issuer_ca = ResourceCertificate::decode_der(issuer_ca_der).map_err(CaPathError::IssuerDecode)?; if issuer_ca.kind != ResourceCertKind::Ca { return Err(CaPathError::IssuerNotCa); } + issuer_ca + .validate_rfc6487_profile(ResourceCertificateRole::Ca) + .map_err(CaPathError::IssuerProfile)?; let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?; if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) { @@ -379,6 +392,12 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources( if issuer_ca.kind != ResourceCertKind::Ca { return Err(CaPathError::IssuerNotCa); } + child_ca + .validate_rfc6487_profile(ResourceCertificateRole::Ca) + .map_err(CaPathError::ChildProfile)?; + issuer_ca + .validate_rfc6487_profile(ResourceCertificateRole::Ca) + .map_err(CaPathError::IssuerProfile)?; if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) { return Err(CaPathError::IssuerSubjectMismatch { @@ -1375,12 +1394,14 @@ fn bytes_leq(a: &[u8], b: &[u8]) -> bool { mod tests { use super::*; use crate::data_model::common::X509NameDer; + use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER; use crate::data_model::rc::{ Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix, IpResourceSet, }; use crate::data_model::rc::{ - RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, + BasicConstraintsProfile, CertificatePoliciesProfile, RcExtensions, ResourceCertKind, + ResourceCertificate, RpkixTbsCertificate, }; use der_parser::num_bigint::BigUint; use std::process::Command; @@ -1410,12 +1431,27 @@ mod tests { subject_public_key_info: Vec::new(), extensions: RcExtensions { basic_constraints_ca: kind == ResourceCertKind::Ca, + basic_constraints: (kind == ResourceCertKind::Ca).then_some( + BasicConstraintsProfile { + ca: true, + critical: true, + path_len_constraint: None, + }, + ), subject_key_identifier: ski, authority_key_identifier: aki, crl_distribution_points_uris: crldp, ca_issuers_uris: aia, subject_info_access: None, - certificate_policies_oid: None, + certificate_policies_oid: (kind == ResourceCertKind::Ca) + .then_some(OID_CP_IPADDR_ASNUMBER.to_string()), + certificate_policies: (kind == ResourceCertKind::Ca).then_some( + CertificatePoliciesProfile { + policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(), + qualifier_oids: Vec::new(), + }, + ), + extension_oids: Vec::new(), ip_resources: None, as_resources: None, }, diff --git a/src/validation/cert_path.rs b/src/validation/cert_path.rs index 7ef72b3..0bfb36b 100644 --- a/src/validation/cert_path.rs +++ b/src/validation/cert_path.rs @@ -2,6 +2,7 @@ use crate::data_model::common::BigUnsigned; use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl}; use crate::data_model::rc::{ ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError, + ResourceCertificateProfileError, ResourceCertificateRole, }; use crate::data_model::signed_object::{EeKeyUsageSummary, ResourceEeCertificate}; use ring::signature; @@ -34,6 +35,9 @@ pub enum CertPathError { )] IssuerNotCa, + #[error("issuer CA certificate profile validation failed: {0} (RFC 6487 §4.8)")] + IssuerProfile(ResourceCertificateProfileError), + #[error("EE certificate must be an EE resource certificate (RFC 6487 §4)")] EeNotEe, @@ -128,6 +132,9 @@ pub fn validate_ee_cert_path( if issuer_ca.kind != ResourceCertKind::Ca { return Err(CertPathError::IssuerNotCa); } + issuer_ca + .validate_rfc6487_profile(ResourceCertificateRole::Ca) + .map_err(CertPathError::IssuerProfile)?; let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?; if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) { @@ -311,6 +318,9 @@ fn validate_ee_cert_path_bindings_and_status( if issuer_ca.kind != ResourceCertKind::Ca { return Err(CertPathError::IssuerNotCa); } + issuer_ca + .validate_rfc6487_profile(ResourceCertificateRole::Ca) + .map_err(CertPathError::IssuerProfile)?; if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) { return Err(CertPathError::IssuerSubjectMismatch { @@ -515,8 +525,10 @@ fn is_serial_revoked_by_crl(ee: &ResourceCertificate, crl: &RpkixCrl) -> bool { mod tests { use super::*; use crate::data_model::common::X509NameDer; + use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER; use crate::data_model::rc::{ - RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, + BasicConstraintsProfile, CertificatePoliciesProfile, RcExtensions, ResourceCertKind, + ResourceCertificate, RpkixTbsCertificate, }; use der_parser::num_bigint::BigUint; @@ -545,12 +557,27 @@ mod tests { subject_public_key_info: Vec::new(), extensions: RcExtensions { basic_constraints_ca: kind == ResourceCertKind::Ca, + basic_constraints: (kind == ResourceCertKind::Ca).then_some( + BasicConstraintsProfile { + ca: true, + critical: true, + path_len_constraint: None, + }, + ), subject_key_identifier: ski, authority_key_identifier: aki, crl_distribution_points_uris: crldp, ca_issuers_uris: aia, subject_info_access: None, - certificate_policies_oid: None, + certificate_policies_oid: (kind == ResourceCertKind::Ca) + .then_some(OID_CP_IPADDR_ASNUMBER.to_string()), + certificate_policies: (kind == ResourceCertKind::Ca).then_some( + CertificatePoliciesProfile { + policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(), + qualifier_oids: Vec::new(), + }, + ), + extension_oids: Vec::new(), ip_resources: None, as_resources: None, }, diff --git a/src/validation/tree_runner.rs b/src/validation/tree_runner.rs index c1ffd3e..a1a40f2 100644 --- a/src/validation/tree_runner.rs +++ b/src/validation/tree_runner.rs @@ -2314,8 +2314,18 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der< let issuer_ca_decode_error: Option; let issuer_ca = match crate::data_model::rc::ResourceCertificate::decode_der(issuer_ca_der) { Ok(v) => { - issuer_ca_decode_error = None; - Some(v) + match v.validate_rfc6487_profile(crate::data_model::rc::ResourceCertificateRole::Ca) { + Ok(()) => { + issuer_ca_decode_error = None; + Some(v) + } + Err(e) => { + issuer_ca_decode_error = Some(format!( + "issuer CA profile validation failed: {e} (RFC 6487 §4.8)" + )); + None + } + } } Err(e) => { issuer_ca_decode_error = Some(format!( diff --git a/tests/bench_stage2_collect_selected_der_v2.rs b/tests/bench_stage2_collect_selected_der_v2.rs index df3c8f5..31b1ee0 100644 --- a/tests/bench_stage2_collect_selected_der_v2.rs +++ b/tests/bench_stage2_collect_selected_der_v2.rs @@ -383,7 +383,7 @@ fn metrics_for(obj_type: ObjType, der: &[u8]) -> Result { ObjType::Cer => { let parsed = ResourceCertificate::parse_der(der).map_err(|e| e.to_string())?; let spki_len = parsed.subject_public_key_info.len() as u32; - let ext_count = (parsed.extensions.basic_constraints_ca.len() + let ext_count = (parsed.extensions.basic_constraints.len() + parsed.extensions.subject_key_identifier.len() + parsed.extensions.authority_key_identifier.len() + parsed.extensions.crl_distribution_points.len() diff --git a/tests/test_aspa_validate_ee_resources.rs b/tests/test_aspa_validate_ee_resources.rs index 3c8feb6..56eab76 100644 --- a/tests/test_aspa_validate_ee_resources.rs +++ b/tests/test_aspa_validate_ee_resources.rs @@ -25,6 +25,7 @@ fn dummy_ee( subject_public_key_info: vec![], extensions: RcExtensions { basic_constraints_ca: false, + basic_constraints: None, subject_key_identifier: Some(vec![0x01]), authority_key_identifier: None, crl_distribution_points_uris: None, @@ -35,6 +36,8 @@ fn dummy_ee( }, )), certificate_policies_oid: None, + certificate_policies: None, + extension_oids: Vec::new(), ip_resources, as_resources, }, diff --git a/tests/test_roa_validate_ee_resources.rs b/tests/test_roa_validate_ee_resources.rs index c6f9cd2..5e6a9e5 100644 --- a/tests/test_roa_validate_ee_resources.rs +++ b/tests/test_roa_validate_ee_resources.rs @@ -28,6 +28,7 @@ fn dummy_ee( subject_public_key_info: vec![], extensions: RcExtensions { basic_constraints_ca: false, + basic_constraints: None, subject_key_identifier: Some(vec![0x01]), authority_key_identifier: None, crl_distribution_points_uris: None, @@ -38,6 +39,8 @@ fn dummy_ee( }, )), certificate_policies_oid: None, + certificate_policies: None, + extension_oids: Vec::new(), ip_resources, as_resources, }, diff --git a/tests/test_ta_certificate.rs b/tests/test_ta_certificate.rs index 491b6b9..7feec2f 100644 --- a/tests/test_ta_certificate.rs +++ b/tests/test_ta_certificate.rs @@ -68,12 +68,15 @@ fn ta_certificate_rejects_non_self_signed_ca() { fn ta_constraints_require_policies_and_ski() { let rc = dummy_rc_ca(RcExtensions { basic_constraints_ca: true, + basic_constraints: None, subject_key_identifier: None, authority_key_identifier: None, crl_distribution_points_uris: None, ca_issuers_uris: None, subject_info_access: None, certificate_policies_oid: None, + certificate_policies: None, + extension_oids: Vec::new(), ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }), as_resources: None, }); @@ -97,12 +100,15 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() { // Missing both IP and AS resources. let rc = dummy_rc_ca(RcExtensions { basic_constraints_ca: true, + basic_constraints: None, subject_key_identifier: Some(vec![1]), authority_key_identifier: None, crl_distribution_points_uris: None, ca_issuers_uris: None, subject_info_access: None, certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()), + certificate_policies: None, + extension_oids: Vec::new(), ip_resources: None, as_resources: None, });