use crate::data_model::common::BigUnsigned; 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, }; use x509_parser::prelude::{FromDer, X509Certificate}; use crate::validation::x509_name::x509_names_equivalent; use std::collections::{BTreeMap, HashMap, HashSet}; use x509_parser::x509::SubjectPublicKeyInfo; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ValidatedSubordinateCa { pub child_ca: ResourceCertificate, pub issuer_ca: ResourceCertificate, pub issuer_crl: RpkixCrl, pub effective_ip_resources: Option, pub effective_as_resources: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ValidatedSubordinateCaLite { pub child_ca: ResourceCertificate, pub effective_ip_resources: Option, pub effective_as_resources: Option, } #[derive(Clone, Debug, Default)] pub struct IssuerEffectiveResourcesIndex { parent_ip_by_afi_items: Option>>, parent_ip_merged_intervals: HashMap, Vec)>>, parent_asnum_intervals: Option>, parent_rdi_intervals: Option>, } impl IssuerEffectiveResourcesIndex { pub fn from_effective_resources( issuer_effective_ip: Option<&IpResourceSet>, issuer_effective_as: Option<&AsResourceSet>, ) -> Result { let parent_ip_by_afi_items = issuer_effective_ip .map(ip_resources_by_afi_items) .transpose()?; let parent_ip_merged_intervals = issuer_effective_ip .map(ip_resources_to_merged_intervals_by_afi) .unwrap_or_default(); let parent_asnum_intervals = issuer_effective_as .and_then(|resources| resources.asnum.as_ref().map(as_choice_to_merged_intervals)); let parent_rdi_intervals = issuer_effective_as .and_then(|resources| resources.rdi.as_ref().map(as_choice_to_merged_intervals)); Ok(Self { parent_ip_by_afi_items, parent_ip_merged_intervals, parent_asnum_intervals, parent_rdi_intervals, }) } } #[derive(Debug, thiserror::Error)] pub enum CaPathError { #[error("child CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")] ChildDecode(#[from] ResourceCertificateDecodeError), #[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")] IssuerDecode(ResourceCertificateDecodeError), #[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")] CrlDecode(#[from] CrlDecodeError), #[error( "child certificate must be a CA resource certificate (RFC 6487 §4.8.1; RFC 5280 §4.2.1.9)" )] ChildNotCa, #[error( "issuer certificate must be a CA resource certificate (RFC 6487 §4.8.1; RFC 5280 §4.2.1.9)" )] IssuerNotCa, #[error( "child issuer DN does not match issuer CA subject DN: child.issuer={child_issuer_dn} issuer.subject={issuer_subject_dn} (RFC 5280 §6.1)" )] IssuerSubjectMismatch { child_issuer_dn: String, issuer_subject_dn: String, }, #[error("child CA certificate signature verification failed: {0} (RFC 5280 §6.1)")] ChildSignatureInvalid(String), #[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")] IssuerSpkiParse(String), #[error( "trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)" )] IssuerSpkiTrailingBytes(usize), #[error("certificate not valid at validation_time (RFC 5280 §4.1.2.5; RFC 5280 §6.1)")] CertificateNotValidAtTime, #[error("child CA KeyUsage extension missing (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")] KeyUsageMissing, #[error("child CA KeyUsage criticality must be critical (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")] KeyUsageNotCritical, #[error("child CA KeyUsage must have only keyCertSign and cRLSign set (RFC 6487 §4.8.4)")] KeyUsageInvalidBits, #[error( "CRL signature/binding verification failed: {0} (RFC 5280 §6.3.3; RFC 6487 §5; RFC 9829 §3.1)" )] CrlVerify(#[from] CrlVerifyError), #[error( "CRL not valid at validation_time (RFC 5280 §6.3.3(g); RFC 5280 §5.1.2.4-§5.1.2.5; RFC 6487 §5)" )] CrlNotValidAtTime, #[error("child CA certificate is revoked by issuer CRL (RFC 5280 §6.3.3; RFC 6487 §5)")] ChildRevoked, #[error( "child CA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11)" )] ResourcesMissing, #[error( "resource extension inheritance cannot be resolved (parent missing resources) (RFC 6487 §7.2)" )] InheritWithoutParentResources, #[error("child CA resources are not a subset of issuer resources (RFC 6487 §7.2)")] ResourcesNotSubset, #[error("issuer CA subjectKeyIdentifier missing (RFC 6487 §4.8.2)")] IssuerSkiMissing, #[error("child CA authorityKeyIdentifier missing (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)")] ChildAkiMissing, #[error( "child CA authorityKeyIdentifier does not match issuer subjectKeyIdentifier (RFC 6487 §4.8.3)" )] ChildAkiMismatch, #[error("child CA authorityInfoAccess missing (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)")] ChildAiaMissing, #[error( "child CA authorityInfoAccess does not reference issuer certificate rsync URI (RFC 6487 §4.8.7)" )] ChildAiaIssuerUriMismatch, #[error("child CA CRLDistributionPoints missing (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)")] ChildCrlDpMissing, #[error( "child CA CRLDistributionPoints does not reference issuer CRL rsync URI (RFC 6487 §4.8.6)" )] ChildCrlDpUriMismatch, } pub fn validate_subordinate_ca_cert( child_ca_der: &[u8], issuer_ca_der: &[u8], issuer_crl_der: &[u8], issuer_ca_rsync_uri: Option<&str>, issuer_crl_rsync_uri: &str, issuer_effective_ip: Option<&IpResourceSet>, issuer_effective_as: Option<&AsResourceSet>, validation_time: time::OffsetDateTime, ) -> Result { let child_ca = ResourceCertificate::decode_der(child_ca_der)?; if child_ca.kind != ResourceCertKind::Ca { return Err(CaPathError::ChildNotCa); } let issuer_ca = ResourceCertificate::decode_der(issuer_ca_der).map_err(CaPathError::IssuerDecode)?; if issuer_ca.kind != ResourceCertKind::Ca { return Err(CaPathError::IssuerNotCa); } 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) { return Err(CaPathError::IssuerSubjectMismatch { child_issuer_dn: child_ca.tbs.issuer_name.to_string(), issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(), }); } validate_child_aki_matches_issuer_ski(&child_ca, &issuer_ca)?; if let Some(expected_issuer_uri) = issuer_ca_rsync_uri { validate_child_aia_points_to_issuer_uri(&child_ca, expected_issuer_uri)?; } validate_child_crldp_contains_issuer_crl_uri(&child_ca, issuer_crl_rsync_uri)?; if !time_within_validity( validation_time, child_ca.tbs.validity_not_before, child_ca.tbs.validity_not_after, ) || !time_within_validity( validation_time, issuer_ca.tbs.validity_not_before, issuer_ca.tbs.validity_not_after, ) { return Err(CaPathError::CertificateNotValidAtTime); } let child_x509 = parse_x509_cert(child_ca_der)?; verify_child_signature(&child_x509, &issuer_spki)?; validate_child_ca_key_usage(&child_x509)?; let issuer_crl = RpkixCrl::decode_der(issuer_crl_der)?; issuer_crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?; if !crl_valid_at_time(&issuer_crl, validation_time) { return Err(CaPathError::CrlNotValidAtTime); } if is_serial_revoked_by_crl(&child_ca, &issuer_crl) { return Err(CaPathError::ChildRevoked); } let effective_ip_resources = resolve_child_ip_resources( child_ca.tbs.extensions.ip_resources.as_ref(), issuer_effective_ip, )?; let effective_as_resources = resolve_child_as_resources( child_ca.tbs.extensions.as_resources.as_ref(), issuer_effective_as, )?; if effective_ip_resources.is_none() && effective_as_resources.is_none() { return Err(CaPathError::ResourcesMissing); } Ok(ValidatedSubordinateCa { child_ca, issuer_ca, issuer_crl, effective_ip_resources, effective_as_resources, }) } /// Validate a subordinate child CA using *pre-decoded issuer CA* and *pre-decoded+verified issuer CRL*. /// /// This avoids repeating issuer CA decode and issuer CRL decode+signature verification for every /// child CA certificate discovered in a publication point. pub fn validate_subordinate_ca_cert_with_prevalidated_issuer( child_ca_der: &[u8], child_ca: ResourceCertificate, issuer_ca: &ResourceCertificate, issuer_spki: &SubjectPublicKeyInfo<'_>, issuer_crl: &RpkixCrl, issuer_crl_revoked_serials: &HashSet>, issuer_ca_rsync_uri: Option<&str>, issuer_crl_rsync_uri: &str, issuer_effective_ip: Option<&IpResourceSet>, issuer_effective_as: Option<&AsResourceSet>, validation_time: time::OffsetDateTime, ) -> Result { let issuer_resources_index = IssuerEffectiveResourcesIndex::from_effective_resources( issuer_effective_ip, issuer_effective_as, )?; validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources( child_ca_der, child_ca, issuer_ca, issuer_spki, issuer_crl, issuer_crl_revoked_serials, issuer_ca_rsync_uri, issuer_crl_rsync_uri, issuer_effective_ip, issuer_effective_as, &issuer_resources_index, validation_time, ) } pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources( child_ca_der: &[u8], child_ca: ResourceCertificate, issuer_ca: &ResourceCertificate, issuer_spki: &SubjectPublicKeyInfo<'_>, issuer_crl: &RpkixCrl, issuer_crl_revoked_serials: &HashSet>, issuer_ca_rsync_uri: Option<&str>, issuer_crl_rsync_uri: &str, issuer_effective_ip: Option<&IpResourceSet>, issuer_effective_as: Option<&AsResourceSet>, issuer_resources_index: &IssuerEffectiveResourcesIndex, validation_time: time::OffsetDateTime, ) -> Result { if child_ca.kind != ResourceCertKind::Ca { return Err(CaPathError::ChildNotCa); } if issuer_ca.kind != ResourceCertKind::Ca { return Err(CaPathError::IssuerNotCa); } if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) { return Err(CaPathError::IssuerSubjectMismatch { child_issuer_dn: child_ca.tbs.issuer_name.to_string(), issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(), }); } validate_child_aki_matches_issuer_ski(&child_ca, issuer_ca)?; if let Some(expected_issuer_uri) = issuer_ca_rsync_uri { validate_child_aia_points_to_issuer_uri(&child_ca, expected_issuer_uri)?; } validate_child_crldp_contains_issuer_crl_uri(&child_ca, issuer_crl_rsync_uri)?; if !time_within_validity( validation_time, child_ca.tbs.validity_not_before, child_ca.tbs.validity_not_after, ) || !time_within_validity( validation_time, issuer_ca.tbs.validity_not_before, issuer_ca.tbs.validity_not_after, ) { return Err(CaPathError::CertificateNotValidAtTime); } let child_x509 = parse_x509_cert(child_ca_der)?; verify_child_signature(&child_x509, issuer_spki)?; validate_child_ca_key_usage(&child_x509)?; if !crl_valid_at_time(issuer_crl, validation_time) { return Err(CaPathError::CrlNotValidAtTime); } let serial = BigUnsigned::from_biguint(&child_ca.tbs.serial_number); if issuer_crl_revoked_serials.contains(&serial.bytes_be) { return Err(CaPathError::ChildRevoked); } let effective_ip_resources = resolve_child_ip_resources_indexed( child_ca.tbs.extensions.ip_resources.as_ref(), issuer_effective_ip, issuer_resources_index.parent_ip_by_afi_items.as_ref(), &issuer_resources_index.parent_ip_merged_intervals, )?; let effective_as_resources = resolve_child_as_resources_indexed( child_ca.tbs.extensions.as_resources.as_ref(), issuer_effective_as, issuer_resources_index.parent_asnum_intervals.as_deref(), issuer_resources_index.parent_rdi_intervals.as_deref(), )?; if effective_ip_resources.is_none() && effective_as_resources.is_none() { return Err(CaPathError::ResourcesMissing); } Ok(ValidatedSubordinateCaLite { child_ca, effective_ip_resources, effective_as_resources, }) } fn parse_subject_pki_from_der(der: &[u8]) -> Result, CaPathError> { let (rem, spki) = SubjectPublicKeyInfo::from_der(der) .map_err(|e| CaPathError::IssuerSpkiParse(e.to_string()))?; if !rem.is_empty() { return Err(CaPathError::IssuerSpkiTrailingBytes(rem.len())); } Ok(spki) } fn parse_x509_cert(der: &[u8]) -> Result, CaPathError> { let (rem, cert) = X509Certificate::from_der(der) .map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?; if !rem.is_empty() { return Err(CaPathError::ChildSignatureInvalid( "trailing bytes after child certificate".to_string(), )); } Ok(cert) } fn verify_child_signature( child: &X509Certificate<'_>, issuer_spki: &SubjectPublicKeyInfo<'_>, ) -> Result<(), CaPathError> { child .verify_signature(Some(issuer_spki)) .map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string())) } fn validate_child_aki_matches_issuer_ski( child: &ResourceCertificate, issuer: &ResourceCertificate, ) -> Result<(), CaPathError> { let Some(issuer_ski) = issuer.tbs.extensions.subject_key_identifier.as_deref() else { return Err(CaPathError::IssuerSkiMissing); }; let Some(child_aki) = child.tbs.extensions.authority_key_identifier.as_deref() else { return Err(CaPathError::ChildAkiMissing); }; if child_aki != issuer_ski { return Err(CaPathError::ChildAkiMismatch); } Ok(()) } fn validate_child_aia_points_to_issuer_uri( child: &ResourceCertificate, issuer_ca_rsync_uri: &str, ) -> Result<(), CaPathError> { let Some(uris) = child.tbs.extensions.ca_issuers_uris.as_ref() else { return Err(CaPathError::ChildAiaMissing); }; if !uris.iter().any(|u| u.as_str() == issuer_ca_rsync_uri) { return Err(CaPathError::ChildAiaIssuerUriMismatch); } Ok(()) } fn validate_child_crldp_contains_issuer_crl_uri( child: &ResourceCertificate, issuer_crl_rsync_uri: &str, ) -> Result<(), CaPathError> { let Some(uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else { return Err(CaPathError::ChildCrlDpMissing); }; if !uris.iter().any(|u| u.as_str() == issuer_crl_rsync_uri) { return Err(CaPathError::ChildCrlDpUriMismatch); } Ok(()) } fn validate_child_ca_key_usage(cert: &X509Certificate<'_>) -> Result<(), CaPathError> { let mut ku_critical: Option = None; for ext in cert.extensions() { if ext.oid.as_bytes() == OID_KEY_USAGE_RAW { ku_critical = Some(ext.critical); break; } } let Some(critical) = ku_critical else { return Err(CaPathError::KeyUsageMissing); }; if !critical { return Err(CaPathError::KeyUsageNotCritical); } let Some(ku) = cert .key_usage() .map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))? else { return Err(CaPathError::KeyUsageMissing); }; let v = &ku.value; let ok = v.key_cert_sign() && v.crl_sign() && !v.digital_signature() && !v.non_repudiation() && !v.key_encipherment() && !v.data_encipherment() && !v.key_agreement() && !v.encipher_only() && !v.decipher_only(); if !ok { return Err(CaPathError::KeyUsageInvalidBits); } Ok(()) } fn time_within_validity( t: time::OffsetDateTime, not_before: time::OffsetDateTime, not_after: time::OffsetDateTime, ) -> bool { let t = t.to_offset(time::UtcOffset::UTC); let not_before = not_before.to_offset(time::UtcOffset::UTC); let not_after = not_after.to_offset(time::UtcOffset::UTC); t >= not_before && t <= not_after } fn crl_valid_at_time(crl: &RpkixCrl, t: time::OffsetDateTime) -> bool { let t = t.to_offset(time::UtcOffset::UTC); let this_update = crl.this_update.utc.to_offset(time::UtcOffset::UTC); let next_update = crl.next_update.utc.to_offset(time::UtcOffset::UTC); t >= this_update && t < next_update } fn is_serial_revoked_by_crl(cert: &ResourceCertificate, crl: &RpkixCrl) -> bool { let serial = BigUnsigned::from_biguint(&cert.tbs.serial_number); crl.revoked_certs .iter() .any(|rc| rc.serial_number == serial) } fn resolve_child_ip_resources( child_ip: Option<&IpResourceSet>, issuer_effective: Option<&IpResourceSet>, ) -> Result, CaPathError> { let precomputed_parent_by_afi = issuer_effective .map(ip_resources_by_afi_items) .transpose()?; let precomputed_parent_intervals = issuer_effective .map(ip_resources_to_merged_intervals_by_afi) .unwrap_or_default(); resolve_child_ip_resources_indexed( child_ip, issuer_effective, precomputed_parent_by_afi.as_ref(), &precomputed_parent_intervals, ) } fn resolve_child_ip_resources_indexed( child_ip: Option<&IpResourceSet>, issuer_effective: Option<&IpResourceSet>, parent_by_afi: Option< &BTreeMap>, >, parent_intervals_by_afi: &HashMap, Vec)>>, ) -> Result, CaPathError> { let Some(child_ip) = child_ip else { return Ok(None); }; let Some(_parent) = issuer_effective else { if child_ip.has_any_inherit() { return Err(CaPathError::InheritWithoutParentResources); } // With no parent effective resources, we cannot validate subset. return Err(CaPathError::ResourcesNotSubset); }; // Resolve per-AFI inherit, producing an effective set with no inherit. let parent_by_afi = parent_by_afi.ok_or(CaPathError::InheritWithoutParentResources)?; let mut out_families: Vec = Vec::new(); for fam in &child_ip.families { match &fam.choice { IpAddressChoice::Inherit => { let items = parent_by_afi .get(&fam.afi) .ok_or(CaPathError::InheritWithoutParentResources)?; out_families.push(crate::data_model::rc::IpAddressFamily { afi: fam.afi, choice: IpAddressChoice::AddressesOrRanges(items.clone()), }); } IpAddressChoice::AddressesOrRanges(items) => { // Subset check against parent union for that AFI. let parent_intervals = parent_intervals_by_afi .get(&fam.afi) .map(Vec::as_slice) .unwrap_or(&[]); if !ip_family_items_subset_with_parent_intervals(items, parent_intervals) { return Err(CaPathError::ResourcesNotSubset); } out_families.push(crate::data_model::rc::IpAddressFamily { afi: fam.afi, choice: IpAddressChoice::AddressesOrRanges(items.clone()), }); } } } Ok(Some(IpResourceSet { families: out_families, })) } fn resolve_child_as_resources( child_as: Option<&AsResourceSet>, issuer_effective: Option<&AsResourceSet>, ) -> Result, CaPathError> { let precomputed_asnum = issuer_effective .and_then(|resources| resources.asnum.as_ref().map(as_choice_to_merged_intervals)); let precomputed_rdi = issuer_effective .and_then(|resources| resources.rdi.as_ref().map(as_choice_to_merged_intervals)); resolve_child_as_resources_indexed( child_as, issuer_effective, precomputed_asnum.as_deref(), precomputed_rdi.as_deref(), ) } fn resolve_child_as_resources_indexed( child_as: Option<&AsResourceSet>, issuer_effective: Option<&AsResourceSet>, parent_asnum_intervals: Option<&[(u32, u32)]>, parent_rdi_intervals: Option<&[(u32, u32)]>, ) -> Result, CaPathError> { let Some(child_as) = child_as else { return Ok(None); }; let Some(parent) = issuer_effective else { if matches!(child_as.asnum, Some(AsIdentifierChoice::Inherit)) || matches!(child_as.rdi, Some(AsIdentifierChoice::Inherit)) { return Err(CaPathError::InheritWithoutParentResources); } return Err(CaPathError::ResourcesNotSubset); }; let asnum = match child_as.asnum.as_ref() { None => None, Some(AsIdentifierChoice::Inherit) => parent .asnum .clone() .ok_or(CaPathError::InheritWithoutParentResources) .map(Some)?, Some(_) => { if !as_choice_subset_with_parent_intervals( child_as.asnum.as_ref(), parent.asnum.as_ref(), parent_asnum_intervals, ) { return Err(CaPathError::ResourcesNotSubset); } child_as.asnum.clone() } }; let rdi = match child_as.rdi.as_ref() { None => None, Some(AsIdentifierChoice::Inherit) => parent .rdi .clone() .ok_or(CaPathError::InheritWithoutParentResources) .map(Some)?, Some(_) => { if !as_choice_subset_with_parent_intervals( child_as.rdi.as_ref(), parent.rdi.as_ref(), parent_rdi_intervals, ) { return Err(CaPathError::ResourcesNotSubset); } child_as.rdi.clone() } }; Ok(Some(AsResourceSet { asnum, rdi })) } fn as_choice_subset( child: Option<&AsIdentifierChoice>, parent: Option<&AsIdentifierChoice>, ) -> bool { as_choice_subset_with_parent_intervals(child, parent, None) } fn as_choice_subset_with_parent_intervals( child: Option<&AsIdentifierChoice>, parent: Option<&AsIdentifierChoice>, parent_intervals_hint: Option<&[(u32, u32)]>, ) -> bool { let Some(child) = child else { return true; }; let Some(parent) = parent else { return false; }; // Treat inherit as "all of parent" here; actual resolution is handled elsewhere. if matches!(child, AsIdentifierChoice::Inherit) { return true; } if matches!(parent, AsIdentifierChoice::Inherit) { return true; } let child_intervals = as_choice_to_merged_intervals(child); let owned_parent_intervals; let parent_intervals = match parent_intervals_hint { Some(intervals) => intervals, None => { owned_parent_intervals = as_choice_to_merged_intervals(parent); owned_parent_intervals.as_slice() } }; for (cmin, cmax) in &child_intervals { if !as_interval_is_covered(parent_intervals, *cmin, *cmax) { return false; } } true } fn as_choice_to_merged_intervals(choice: &AsIdentifierChoice) -> Vec<(u32, u32)> { let mut v = Vec::new(); match choice { AsIdentifierChoice::Inherit => {} AsIdentifierChoice::AsIdsOrRanges(items) => { for item in items { match item { crate::data_model::rc::AsIdOrRange::Id(id) => v.push((*id, *id)), crate::data_model::rc::AsIdOrRange::Range { min, max } => v.push((*min, *max)), } } } } v.sort_by_key(|(a, _b)| *a); merge_as_intervals(&v) } fn merge_as_intervals(v: &[(u32, u32)]) -> Vec<(u32, u32)> { let mut out: Vec<(u32, u32)> = Vec::new(); for (min, max) in v { let Some(last) = out.last_mut() else { out.push((*min, *max)); continue; }; if *min <= last.1.saturating_add(1) { last.1 = last.1.max(*max); continue; } out.push((*min, *max)); } out } fn as_interval_is_covered(parent: &[(u32, u32)], min: u32, max: u32) -> bool { for (pmin, pmax) in parent { if *pmin <= min && max <= *pmax { return true; } if *pmin > min { break; } } false } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum AfiKey { V4, V6, } fn ip_resources_by_afi_items( set: &IpResourceSet, ) -> Result< std::collections::BTreeMap< crate::data_model::rc::Afi, Vec, >, CaPathError, > { let mut m: std::collections::BTreeMap< crate::data_model::rc::Afi, Vec, > = std::collections::BTreeMap::new(); for fam in &set.families { match &fam.choice { IpAddressChoice::Inherit => return Err(CaPathError::InheritWithoutParentResources), IpAddressChoice::AddressesOrRanges(items) => { m.insert(fam.afi, items.clone()); } } } Ok(m) } fn ip_resources_single_afi( parent: &IpResourceSet, afi: crate::data_model::rc::Afi, items: Option<&Vec>, ) -> IpResourceSet { let mut families = Vec::new(); if let Some(items) = items { families.push(crate::data_model::rc::IpAddressFamily { afi, choice: IpAddressChoice::AddressesOrRanges(items.clone()), }); } else { // If parent doesn't mention this AFI explicitly, treat as empty. // The subset check will fail. let _ = parent; } IpResourceSet { families } } fn ip_family_items_subset( child_items: &[crate::data_model::rc::IpAddressOrRange], parent_set: &IpResourceSet, ) -> bool { let parent_by_afi = ip_resources_to_merged_intervals(parent_set); // parent_set should contain exactly one AFI. let (afi_key, parent_intervals) = match parent_by_afi.into_iter().next() { None => return false, Some(v) => v, }; let mut child_intervals: Vec<(Vec, Vec)> = Vec::new(); for item in child_items { match item { crate::data_model::rc::IpAddressOrRange::Prefix(p) => { child_intervals.push(prefix_to_range(p)) } crate::data_model::rc::IpAddressOrRange::Range(r) => { child_intervals.push((r.min.clone(), r.max.clone())) } } } child_intervals.sort_by(|(a, _), (b, _)| a.cmp(b)); let child_intervals = merge_ip_intervals(&child_intervals); let _ = afi_key; for (cmin, cmax) in &child_intervals { if !interval_is_covered(&parent_intervals, cmin, cmax) { return false; } } true } fn ip_resources_to_merged_intervals( set: &IpResourceSet, ) -> std::collections::HashMap, Vec)>> { let m = ip_resources_to_merged_intervals_by_afi(set); m.into_iter() .map(|(afi, ranges)| { ( match afi { crate::data_model::rc::Afi::Ipv4 => AfiKey::V4, crate::data_model::rc::Afi::Ipv6 => AfiKey::V6, }, ranges, ) }) .collect() } fn ip_resources_to_merged_intervals_by_afi( set: &IpResourceSet, ) -> HashMap, Vec)>> { let mut m: HashMap, Vec)>> = HashMap::new(); for fam in &set.families { match &fam.choice { IpAddressChoice::Inherit => { // When used in subset checks, treat inherit as "all" by leaving it absent. // Resolution should have happened earlier. } IpAddressChoice::AddressesOrRanges(items) => { let ent = m.entry(fam.afi).or_default(); for item in items { match item { crate::data_model::rc::IpAddressOrRange::Prefix(p) => { let (min, max) = prefix_to_range(p); ent.push((min, max)); } crate::data_model::rc::IpAddressOrRange::Range(r) => { ent.push((r.min.clone(), r.max.clone())); } } } } } } for (_afi, v) in m.iter_mut() { v.sort_by(|(a, _), (b, _)| a.cmp(b)); *v = merge_ip_intervals(v); } m } fn ip_family_items_subset_with_parent_intervals( child_items: &[crate::data_model::rc::IpAddressOrRange], parent_intervals: &[(Vec, Vec)], ) -> bool { if parent_intervals.is_empty() { return false; } let mut child_intervals: Vec<(Vec, Vec)> = Vec::new(); for item in child_items { match item { crate::data_model::rc::IpAddressOrRange::Prefix(p) => { child_intervals.push(prefix_to_range(p)) } crate::data_model::rc::IpAddressOrRange::Range(r) => { child_intervals.push((r.min.clone(), r.max.clone())) } } } child_intervals.sort_by(|(a, _), (b, _)| a.cmp(b)); let child_intervals = merge_ip_intervals(&child_intervals); for (cmin, cmax) in &child_intervals { if !interval_is_covered(parent_intervals, cmin, cmax) { return false; } } true } fn merge_ip_intervals(v: &[(Vec, Vec)]) -> Vec<(Vec, Vec)> { let mut out: Vec<(Vec, Vec)> = Vec::new(); for (min, max) in v { let Some(last) = out.last_mut() else { out.push((min.clone(), max.clone())); continue; }; if bytes_leq(min, &increment_bytes(&last.1)) { if bytes_leq(&last.1, max) { last.1 = max.clone(); } continue; } out.push((min.clone(), max.clone())); } out } fn interval_is_covered(parent: &[(Vec, Vec)], min: &[u8], max: &[u8]) -> bool { for (pmin, pmax) in parent { if bytes_leq(pmin, min) && bytes_leq(max, pmax) { return true; } if pmin.as_slice() > min { break; } } false } fn prefix_to_range(prefix: &crate::data_model::rc::IpPrefix) -> (Vec, Vec) { let mut min = prefix.addr.clone(); let mut max = prefix.addr.clone(); let bitlen = match prefix.afi { crate::data_model::rc::Afi::Ipv4 => 32u16, crate::data_model::rc::Afi::Ipv6 => 128u16, }; let plen = prefix.prefix_len.min(bitlen); for bit in plen..bitlen { let byte = (bit / 8) as usize; let offset = 7 - (bit % 8); let mask = 1u8 << offset; min[byte] &= !mask; max[byte] |= mask; } (min, max) } fn bytes_leq(a: &[u8], b: &[u8]) -> bool { a <= b } #[cfg(test)] mod tests { use super::*; use crate::data_model::common::X509NameDer; use crate::data_model::rc::{ Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpResourceSet, }; use crate::data_model::rc::{ RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, }; use der_parser::num_bigint::BigUint; use std::process::Command; fn dummy_cert( kind: ResourceCertKind, subject_dn: &str, issuer_dn: &str, ski: Option>, aki: Option>, aia: Option>, crldp: Option>, ) -> ResourceCertificate { let aia = aia.map(|v| v.into_iter().map(|s| s.to_string()).collect::>()); let crldp = crldp.map(|v| v.into_iter().map(|s| s.to_string()).collect::>()); ResourceCertificate { raw_der: Vec::new(), kind, tbs: RpkixTbsCertificate { version: 2, serial_number: BigUint::from(1u8), signature_algorithm: "1.2.840.113549.1.1.11".to_string(), issuer_name: X509NameDer(issuer_dn.as_bytes().to_vec()), subject_name: X509NameDer(subject_dn.as_bytes().to_vec()), validity_not_before: time::OffsetDateTime::UNIX_EPOCH, validity_not_after: time::OffsetDateTime::UNIX_EPOCH, subject_public_key_info: Vec::new(), extensions: RcExtensions { basic_constraints_ca: kind == ResourceCertKind::Ca, 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, ip_resources: None, as_resources: None, }, }, } } fn openssl_available() -> bool { Command::new("openssl") .arg("version") .output() .map(|o| o.status.success()) .unwrap_or(false) } fn write_cert_der_with_addext(dir: &std::path::Path, addext: Option<&str>) -> Vec { assert!(openssl_available(), "openssl is required for this test"); let key = dir.join("k.pem"); let cert = dir.join("c.pem"); let der = dir.join("c.der"); let mut cmd = Command::new("openssl"); cmd.arg("req") .arg("-x509") .arg("-newkey") .arg("rsa:2048") .arg("-nodes") .arg("-keyout") .arg(&key) .arg("-subj") .arg("/CN=ku") .arg("-days") .arg("1") .arg("-out") .arg(&cert); if let Some(ext) = addext { cmd.arg("-addext").arg(ext); } let out = cmd.output().expect("openssl req"); assert!( out.status.success(), "openssl req failed: {}", String::from_utf8_lossy(&out.stderr) ); let out = Command::new("openssl") .arg("x509") .arg("-in") .arg(&cert) .arg("-outform") .arg("DER") .arg("-out") .arg(&der) .output() .expect("openssl x509"); assert!( out.status.success(), "openssl x509 failed: {}", String::from_utf8_lossy(&out.stderr) ); std::fs::read(&der).expect("read der") } fn gen_issuer_and_child_der(dir: &std::path::Path) -> (Vec, Vec, Vec) { assert!(openssl_available(), "openssl is required for this test"); let issuer_key = dir.join("issuer.key"); let issuer_csr = dir.join("issuer.csr"); let issuer_pem = dir.join("issuer.pem"); let issuer_der = dir.join("issuer.der"); let child_key = dir.join("child.key"); let child_csr = dir.join("child.csr"); let child_pem = dir.join("child.pem"); let child_der = dir.join("child.der"); let other_key = dir.join("other.key"); let other_csr = dir.join("other.csr"); let other_pem = dir.join("other.pem"); let other_der = dir.join("other.der"); let run = |cmd: &mut Command| { let out = cmd.output().expect("run openssl"); assert!( out.status.success(), "command failed: {:?}\nstderr={}", cmd, String::from_utf8_lossy(&out.stderr) ); }; // Issuer self-signed. run(Command::new("openssl") .args(["genrsa", "-out"]) .arg(&issuer_key) .arg("2048")); run(Command::new("openssl") .args(["req", "-new", "-key"]) .arg(&issuer_key) .args(["-subj", "/CN=issuer", "-out"]) .arg(&issuer_csr)); run(Command::new("openssl") .args(["x509", "-req", "-in"]) .arg(&issuer_csr) .args(["-signkey"]) .arg(&issuer_key) .args(["-days", "1", "-out"]) .arg(&issuer_pem)); run(Command::new("openssl") .args(["x509", "-in"]) .arg(&issuer_pem) .args(["-outform", "DER", "-out"]) .arg(&issuer_der)); // Child signed by issuer. run(Command::new("openssl") .args(["genrsa", "-out"]) .arg(&child_key) .arg("2048")); run(Command::new("openssl") .args(["req", "-new", "-key"]) .arg(&child_key) .args(["-subj", "/CN=child", "-out"]) .arg(&child_csr)); run(Command::new("openssl") .args(["x509", "-req", "-in"]) .arg(&child_csr) .args(["-CA"]) .arg(&issuer_pem) .args(["-CAkey"]) .arg(&issuer_key) .args(["-CAcreateserial", "-days", "1", "-out"]) .arg(&child_pem)); run(Command::new("openssl") .args(["x509", "-in"]) .arg(&child_pem) .args(["-outform", "DER", "-out"]) .arg(&child_der)); // Other self-signed issuer. run(Command::new("openssl") .args(["genrsa", "-out"]) .arg(&other_key) .arg("2048")); run(Command::new("openssl") .args(["req", "-new", "-key"]) .arg(&other_key) .args(["-subj", "/CN=other", "-out"]) .arg(&other_csr)); run(Command::new("openssl") .args(["x509", "-req", "-in"]) .arg(&other_csr) .args(["-signkey"]) .arg(&other_key) .args(["-days", "1", "-out"]) .arg(&other_pem)); run(Command::new("openssl") .args(["x509", "-in"]) .arg(&other_pem) .args(["-outform", "DER", "-out"]) .arg(&other_der)); ( std::fs::read(&issuer_der).expect("read issuer der"), std::fs::read(&child_der).expect("read child der"), std::fs::read(&other_der).expect("read other der"), ) } #[test] fn resolve_child_ip_resources_rejects_inherit_without_parent_effective_resources() { let child = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, choice: IpAddressChoice::Inherit, }], }; let err = resolve_child_ip_resources(Some(&child), None).unwrap_err(); assert!(matches!(err, CaPathError::InheritWithoutParentResources)); } #[test] fn resolve_child_ip_resources_rejects_non_inherit_without_parent_effective_resources() { let child = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, choice: IpAddressChoice::AddressesOrRanges(vec![]), }], }; let err = resolve_child_ip_resources(Some(&child), None).unwrap_err(); assert!(matches!(err, CaPathError::ResourcesNotSubset)); } #[test] fn ip_resources_by_afi_items_rejects_inherit_families() { let parent = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv6, choice: IpAddressChoice::Inherit, }], }; let err = ip_resources_by_afi_items(&parent).unwrap_err(); assert!(matches!(err, CaPathError::InheritWithoutParentResources)); } #[test] fn resolve_child_as_resources_rejects_inherit_without_parent_effective_resources() { let child = AsResourceSet { asnum: Some(AsIdentifierChoice::Inherit), rdi: None, }; let err = resolve_child_as_resources(Some(&child), None).unwrap_err(); assert!(matches!(err, CaPathError::InheritWithoutParentResources)); } #[test] fn child_aki_mismatch_is_rejected() { let issuer = dummy_cert( ResourceCertKind::Ca, "CN=issuer", "CN=issuer", Some(vec![1]), None, None, None, ); let child = dummy_cert( ResourceCertKind::Ca, "CN=child", "CN=issuer", Some(vec![2]), Some(vec![9]), Some(vec!["rsync://example.test/issuer.cer"]), Some(vec!["rsync://example.test/issuer.crl"]), ); let err = validate_child_aki_matches_issuer_ski(&child, &issuer).unwrap_err(); assert!(matches!(err, CaPathError::ChildAkiMismatch), "{err}"); } #[test] fn child_aia_missing_is_rejected() { let _issuer = dummy_cert( ResourceCertKind::Ca, "CN=issuer", "CN=issuer", Some(vec![1]), None, None, None, ); let child = dummy_cert( ResourceCertKind::Ca, "CN=child", "CN=issuer", Some(vec![2]), Some(vec![1]), None, Some(vec!["rsync://example.test/issuer.crl"]), ); let err = validate_child_aia_points_to_issuer_uri(&child, "rsync://example.test/issuer.cer") .unwrap_err(); assert!(matches!(err, CaPathError::ChildAiaMissing), "{err}"); // Also cover issuer ski missing. let issuer_missing_ski = dummy_cert( ResourceCertKind::Ca, "CN=issuer", "CN=issuer", None, None, None, None, ); let err = validate_child_aki_matches_issuer_ski(&child, &issuer_missing_ski).unwrap_err(); assert!(matches!(err, CaPathError::IssuerSkiMissing), "{err}"); } #[test] fn child_aia_issuer_uri_mismatch_is_rejected() { let _issuer = dummy_cert( ResourceCertKind::Ca, "CN=issuer", "CN=issuer", Some(vec![1]), None, None, None, ); let child = dummy_cert( ResourceCertKind::Ca, "CN=child", "CN=issuer", Some(vec![2]), Some(vec![1]), Some(vec!["rsync://example.test/other.cer"]), Some(vec!["rsync://example.test/issuer.crl"]), ); let err = validate_child_aia_points_to_issuer_uri(&child, "rsync://example.test/issuer.cer") .unwrap_err(); assert!( matches!(err, CaPathError::ChildAiaIssuerUriMismatch), "{err}" ); } #[test] fn child_crldp_mismatch_is_rejected() { let issuer = dummy_cert( ResourceCertKind::Ca, "CN=issuer", "CN=issuer", Some(vec![1]), None, None, None, ); let child = dummy_cert( ResourceCertKind::Ca, "CN=child", "CN=issuer", Some(vec![2]), Some(vec![1]), Some(vec!["rsync://example.test/issuer.cer"]), None, ); let err = validate_child_crldp_contains_issuer_crl_uri(&child, "rsync://example.test/issuer.crl") .unwrap_err(); assert!(matches!(err, CaPathError::ChildCrlDpMissing), "{err}"); let child = dummy_cert( ResourceCertKind::Ca, "CN=child", "CN=issuer", Some(vec![2]), Some(vec![1]), Some(vec!["rsync://example.test/issuer.cer"]), Some(vec!["rsync://example.test/other.crl"]), ); let err = validate_child_crldp_contains_issuer_crl_uri(&child, "rsync://example.test/issuer.crl") .unwrap_err(); assert!(matches!(err, CaPathError::ChildCrlDpUriMismatch), "{err}"); // Cover child AKI missing. let child_missing_aki = dummy_cert( ResourceCertKind::Ca, "CN=child", "CN=issuer", Some(vec![2]), None, Some(vec!["rsync://example.test/issuer.cer"]), Some(vec!["rsync://example.test/issuer.crl"]), ); let err = validate_child_aki_matches_issuer_ski(&child_missing_aki, &issuer).unwrap_err(); assert!(matches!(err, CaPathError::ChildAkiMissing), "{err}"); } #[test] fn child_binding_checks_accept_when_matching() { let issuer = dummy_cert( ResourceCertKind::Ca, "CN=issuer", "CN=issuer", Some(vec![1]), None, None, None, ); let child = dummy_cert( ResourceCertKind::Ca, "CN=child", "CN=issuer", Some(vec![2]), Some(vec![1]), Some(vec!["rsync://example.test/issuer.cer"]), Some(vec!["rsync://example.test/issuer.crl"]), ); validate_child_aki_matches_issuer_ski(&child, &issuer).expect("aki ok"); validate_child_aia_points_to_issuer_uri(&child, "rsync://example.test/issuer.cer") .expect("aia ok"); validate_child_crldp_contains_issuer_crl_uri(&child, "rsync://example.test/issuer.crl") .expect("crldp ok"); } #[test] fn validate_child_ca_key_usage_accepts_only_keycertsign_and_crlsign_critical() { let td = tempfile::tempdir().expect("tempdir"); let der = write_cert_der_with_addext( td.path(), Some("keyUsage = critical, keyCertSign, cRLSign"), ); let cert = parse_x509_cert(&der).expect("x509 parse ok"); validate_child_ca_key_usage(&cert).expect("key usage ok"); } #[test] fn validate_child_ca_key_usage_rejects_missing_noncritical_and_invalid_bits() { let td = tempfile::tempdir().expect("tempdir"); let missing = write_cert_der_with_addext(td.path(), None); let cert = parse_x509_cert(&missing).expect("x509 parse ok"); let err = validate_child_ca_key_usage(&cert).unwrap_err(); assert!(matches!(err, CaPathError::KeyUsageMissing), "{err}"); let td = tempfile::tempdir().expect("tempdir"); let noncritical = write_cert_der_with_addext(td.path(), Some("keyUsage = keyCertSign, cRLSign")); let cert = parse_x509_cert(&noncritical).expect("x509 parse ok"); let err = validate_child_ca_key_usage(&cert).unwrap_err(); assert!(matches!(err, CaPathError::KeyUsageNotCritical), "{err}"); let td = tempfile::tempdir().expect("tempdir"); let invalid = write_cert_der_with_addext( td.path(), Some("keyUsage = critical, keyCertSign, cRLSign, digitalSignature"), ); let cert = parse_x509_cert(&invalid).expect("x509 parse ok"); let err = validate_child_ca_key_usage(&cert).unwrap_err(); assert!(matches!(err, CaPathError::KeyUsageInvalidBits), "{err}"); } #[test] fn verify_cert_signature_with_issuer_accepts_valid_chain_and_rejects_wrong_issuer() { let td = tempfile::tempdir().expect("tempdir"); let (issuer, child, other) = gen_issuer_and_child_der(td.path()); let issuer_cert = parse_x509_cert(&issuer).expect("x509 parse issuer"); let child_cert = parse_x509_cert(&child).expect("x509 parse child"); verify_child_signature(&child_cert, &issuer_cert.tbs_certificate.subject_pki) .expect("signature ok"); let other_cert = parse_x509_cert(&other).expect("x509 parse other"); let err = verify_child_signature(&child_cert, &other_cert.tbs_certificate.subject_pki) .unwrap_err(); assert!( matches!(err, CaPathError::ChildSignatureInvalid(_)), "{err}" ); } #[test] fn issuer_effective_resources_index_and_indexed_resolvers_cover_success_and_failure_paths() { use crate::data_model::rc::{AsIdOrRange, IpPrefix}; let parent_ip = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( IpPrefix { afi: Afi::Ipv4, prefix_len: 8, addr: vec![10, 0, 0, 0], }, )]), }], }; let parent_as = AsResourceSet { asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ AsIdOrRange::Range { min: 64500, max: 64599, }, ])), rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( 65000, )])), }; let idx = IssuerEffectiveResourcesIndex::from_effective_resources( Some(&parent_ip), Some(&parent_as), ) .expect("index builds"); assert_eq!( idx.parent_ip_by_afi_items.as_ref().map(|v| v.len()), Some(1) ); assert_eq!(idx.parent_ip_merged_intervals.len(), 1); assert_eq!( idx.parent_asnum_intervals.as_ref().map(|v| v.len()), Some(1) ); assert_eq!(idx.parent_rdi_intervals.as_ref().map(|v| v.len()), Some(1)); let child_ip_subset = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( IpPrefix { afi: Afi::Ipv4, prefix_len: 16, addr: vec![10, 1, 0, 0], }, )]), }], }; assert!( resolve_child_ip_resources_indexed( Some(&child_ip_subset), Some(&parent_ip), idx.parent_ip_by_afi_items.as_ref(), &idx.parent_ip_merged_intervals, ) .expect("subset should resolve") .is_some() ); let child_ip_bad = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( IpPrefix { afi: Afi::Ipv4, prefix_len: 16, addr: vec![11, 0, 0, 0], }, )]), }], }; let err = resolve_child_ip_resources_indexed( Some(&child_ip_bad), Some(&parent_ip), idx.parent_ip_by_afi_items.as_ref(), &idx.parent_ip_merged_intervals, ) .unwrap_err(); assert!(matches!(err, CaPathError::ResourcesNotSubset)); let child_as_subset = AsResourceSet { asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( 64542, )])), rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( 65000, )])), }; assert!( resolve_child_as_resources_indexed( Some(&child_as_subset), Some(&parent_as), idx.parent_asnum_intervals.as_deref(), idx.parent_rdi_intervals.as_deref(), ) .expect("subset as resolves") .is_some() ); let child_as_bad = AsResourceSet { asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( 65123, )])), rdi: None, }; let err = resolve_child_as_resources_indexed( Some(&child_as_bad), Some(&parent_as), idx.parent_asnum_intervals.as_deref(), idx.parent_rdi_intervals.as_deref(), ) .unwrap_err(); assert!(matches!(err, CaPathError::ResourcesNotSubset)); } #[test] fn resolve_child_ip_and_as_resources_success_paths() { use crate::data_model::rc::{AsIdOrRange, IpAddressOrRange, IpPrefix}; let parent_ip = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( IpPrefix { afi: Afi::Ipv4, prefix_len: 8, addr: vec![10, 0, 0, 0], }, )]), }], }; let child_ip_inherit = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, choice: IpAddressChoice::Inherit, }], }; let eff = resolve_child_ip_resources(Some(&child_ip_inherit), Some(&parent_ip)) .expect("inherit resolves") .expect("some ip"); assert_eq!(eff.families.len(), 1); assert!(matches!( eff.families[0].choice, IpAddressChoice::AddressesOrRanges(_) )); let child_ip_subset = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( IpPrefix { afi: Afi::Ipv4, prefix_len: 16, addr: vec![10, 1, 0, 0], }, )]), }], }; resolve_child_ip_resources(Some(&child_ip_subset), Some(&parent_ip)) .expect("subset ok") .expect("some"); let child_ip_bad = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( IpPrefix { afi: Afi::Ipv4, prefix_len: 16, addr: vec![11, 0, 0, 0], }, )]), }], }; let err = resolve_child_ip_resources(Some(&child_ip_bad), Some(&parent_ip)).unwrap_err(); assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}"); let parent_as = AsResourceSet { asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ AsIdOrRange::Range { min: 1, max: 100 }, ])), rdi: None, }; let child_as_inherit = AsResourceSet { asnum: Some(AsIdentifierChoice::Inherit), rdi: None, }; let eff_as = resolve_child_as_resources(Some(&child_as_inherit), Some(&parent_as)) .expect("inherit as") .expect("some"); assert_eq!(eff_as.asnum, parent_as.asnum); let child_as_subset = AsResourceSet { asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(50)])), rdi: None, }; resolve_child_as_resources(Some(&child_as_subset), Some(&parent_as)) .expect("subset as") .expect("some"); let child_as_bad = AsResourceSet { asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( 200, )])), rdi: None, }; let err = resolve_child_as_resources(Some(&child_as_bad), Some(&parent_as)).unwrap_err(); assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}"); } } fn increment_bytes(v: &[u8]) -> Vec { let mut out = v.to_vec(); for i in (0..out.len()).rev() { if out[i] != 0xFF { out[i] += 1; for j in i + 1..out.len() { out[j] = 0; } return out; } } vec![0u8; out.len()] }