use crate::audit::{AuditObjectKind, AuditObjectResult, ObjectAuditEntry, sha256_hex_from_32}; use crate::data_model::aspa::{AspaDecodeError, AspaObject, AspaValidateError}; use crate::data_model::manifest::ManifestObject; use crate::data_model::rc::{ AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressOrRange, IpPrefix as RcIpPrefix, ResourceCertificate, }; use crate::data_model::roa::{IpPrefix, RoaAfi, RoaDecodeError, RoaObject, RoaValidateError}; use crate::data_model::signed_object::SignedObjectVerifyError; use crate::policy::{Policy, SignedObjectFailurePolicy}; use crate::report::{RfcRef, Warning}; use crate::storage::{FetchCachePpPack, PackFile}; use crate::validation::cert_path::{CertPathError, validate_ee_cert_path}; const RFC_NONE: &[RfcRef] = &[]; const RFC_CRLDP: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6")]; const RFC_CRLDP_AND_LOCKED_PACK: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §4.2.1")]; fn extra_rfc_refs_for_crl_selection(e: &ObjectValidateError) -> &'static [RfcRef] { match e { ObjectValidateError::MissingCrlDpUris => RFC_CRLDP, ObjectValidateError::CrlNotFound(_) => RFC_CRLDP_AND_LOCKED_PACK, _ => RFC_NONE, } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Vrp { pub asn: u32, pub prefix: IpPrefix, pub max_length: u16, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct AspaAttestation { pub customer_as_id: u32, pub provider_as_ids: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ObjectsOutput { pub vrps: Vec, pub aspas: Vec, pub warnings: Vec, pub stats: ObjectsStats, pub audit: Vec, } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct ObjectsStats { pub roa_total: usize, pub roa_ok: usize, pub aspa_total: usize, pub aspa_ok: usize, /// Whether this publication point was dropped due to an unrecoverable objects-processing error /// (e.g., missing issuer CRL in the pack, or `signed_object_failure_policy=drop_publication_point`). pub publication_point_dropped: bool, } /// Process objects from a fetch_cache_pp publication point pack using a known issuer CA certificate /// and its effective resources (resolved via the resource-path, RFC 6487 §7.2). pub fn process_fetch_cache_pp_pack_for_issuer( pack: &FetchCachePpPack, policy: &Policy, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>, issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, validation_time: time::OffsetDateTime, ) -> ObjectsOutput { let mut warnings: Vec = Vec::new(); let mut stats = ObjectsStats::default(); stats.roa_total = pack .files .iter() .filter(|f| f.rsync_uri.ends_with(".roa")) .count(); stats.aspa_total = pack .files .iter() .filter(|f| f.rsync_uri.ends_with(".asa")) .count(); let mut audit: Vec = Vec::new(); // Enforce that `manifest_bytes` is actually a manifest object. let _manifest = ManifestObject::decode_der(&pack.manifest_bytes).expect("fetch_cache_pp manifest decodes"); let crl_files = pack .files .iter() .filter(|f| f.rsync_uri.ends_with(".crl")) .map(|f| (f.rsync_uri.clone(), f.bytes.clone())) .collect::>(); // If the pack has signed objects but no CRLs at all, we cannot validate any embedded EE // certificate paths deterministically (EE CRLDP must reference an rsync URI in the pack). if crl_files.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) { stats.publication_point_dropped = true; warnings.push( Warning::new("dropping publication point: no CRL files in fetch_cache_pp") .with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")]) .with_context(&pack.manifest_rsync_uri), ); for f in &pack.files { if f.rsync_uri.ends_with(".roa") { audit.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Skipped, detail: Some( "skipped due to missing CRL files in fetch_cache_pp".to_string(), ), }); } else if f.rsync_uri.ends_with(".asa") { audit.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Skipped, detail: Some( "skipped due to missing CRL files in fetch_cache_pp".to_string(), ), }); } } return ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), warnings, stats, audit, }; } let mut vrps: Vec = Vec::new(); let mut aspas: Vec = Vec::new(); for (idx, file) in pack.files.iter().enumerate() { if file.rsync_uri.ends_with(".roa") { match process_roa_with_issuer( file, issuer_ca_der, issuer_ca_rsync_uri, &crl_files, issuer_effective_ip, issuer_effective_as, validation_time, ) { Ok(mut out) => { stats.roa_ok += 1; vrps.append(&mut out); audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Ok, detail: None, }); } Err(e) => match policy.signed_object_failure_policy { SignedObjectFailurePolicy::DropObject => { audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Error, detail: Some(e.to_string()), }); let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")]; refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e)); warnings.push( Warning::new(format!("dropping invalid ROA: {}: {e}", file.rsync_uri)) .with_rfc_refs(&refs) .with_context(&file.rsync_uri), ) } SignedObjectFailurePolicy::DropPublicationPoint => { stats.publication_point_dropped = true; audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Error, detail: Some(e.to_string()), }); for f in pack.files.iter().skip(idx + 1) { if f.rsync_uri.ends_with(".roa") { audit.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Skipped, detail: Some( "skipped due to policy=signed_object_failure_policy=drop_publication_point" .to_string(), ), }); } else if f.rsync_uri.ends_with(".asa") { audit.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Skipped, detail: Some( "skipped due to policy=signed_object_failure_policy=drop_publication_point" .to_string(), ), }); } } let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")]; refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e)); warnings.push( Warning::new(format!( "dropping publication point due to invalid ROA: {}: {e}", file.rsync_uri )) .with_rfc_refs(&refs) .with_context(&pack.manifest_rsync_uri), ); return ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), warnings, stats, audit, }; } }, } } else if file.rsync_uri.ends_with(".asa") { match process_aspa_with_issuer( file, issuer_ca_der, issuer_ca_rsync_uri, &crl_files, issuer_effective_ip, issuer_effective_as, validation_time, ) { Ok(att) => { stats.aspa_ok += 1; aspas.push(att); audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Ok, detail: None, }); } Err(e) => match policy.signed_object_failure_policy { SignedObjectFailurePolicy::DropObject => { audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Error, detail: Some(e.to_string()), }); let mut refs = vec![RfcRef("RFC 6488 §3")]; refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e)); warnings.push( Warning::new(format!("dropping invalid ASPA: {}: {e}", file.rsync_uri)) .with_rfc_refs(&refs) .with_context(&file.rsync_uri), ) } SignedObjectFailurePolicy::DropPublicationPoint => { stats.publication_point_dropped = true; audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Error, detail: Some(e.to_string()), }); for f in pack.files.iter().skip(idx + 1) { if f.rsync_uri.ends_with(".roa") { audit.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Skipped, detail: Some( "skipped due to policy=signed_object_failure_policy=drop_publication_point" .to_string(), ), }); } else if f.rsync_uri.ends_with(".asa") { audit.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Skipped, detail: Some( "skipped due to policy=signed_object_failure_policy=drop_publication_point" .to_string(), ), }); } } let mut refs = vec![RfcRef("RFC 6488 §3")]; refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e)); warnings.push( Warning::new(format!( "dropping publication point due to invalid ASPA: {}: {e}", file.rsync_uri )) .with_rfc_refs(&refs) .with_context(&pack.manifest_rsync_uri), ); return ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), warnings, stats, audit, }; } }, } } } ObjectsOutput { vrps, aspas, warnings, stats, audit, } } #[derive(Debug, thiserror::Error)] enum ObjectValidateError { #[error("ROA decode failed: {0}")] RoaDecode(#[from] RoaDecodeError), #[error("ROA embedded EE resource validation failed: {0}")] RoaEeResources(#[from] RoaValidateError), #[error("ASPA decode failed: {0}")] AspaDecode(#[from] AspaDecodeError), #[error("ASPA embedded EE resource validation failed: {0}")] AspaEeResources(#[from] AspaValidateError), #[error("CMS signature verification failed: {0}")] Signature(#[from] SignedObjectVerifyError), #[error("EE certificate path validation failed: {0}")] CertPath(#[from] CertPathError), #[error( "certificate CRLDistributionPoints URIs missing (cannot select issuer CRL) (RFC 6487 §4.8.6)" )] MissingCrlDpUris, #[error( "no CRL available in fetch_cache_pp (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)" )] MissingCrlInPack, #[error( "CRL referenced by CRLDistributionPoints not found in fetch_cache_pp: {0} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)" )] CrlNotFound(String), #[error( "issuer effective IP resources missing (cannot validate EE IP resources subset) (RFC 6487 §7.2; RFC 3779 §2.3)" )] MissingIssuerEffectiveIp, #[error( "issuer effective AS resources missing (cannot validate EE AS resources subset) (RFC 6487 §7.2; RFC 3779 §3.3)" )] MissingIssuerEffectiveAs, #[error( "EE certificate resources are not a subset of issuer effective resources (RFC 6487 §7.2; RFC 3779)" )] EeResourcesNotSubset, } fn process_roa_with_issuer( file: &PackFile, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, crl_files: &[(String, Vec)], issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>, issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, validation_time: time::OffsetDateTime, ) -> Result, ObjectValidateError> { let roa = RoaObject::decode_der(&file.bytes)?; roa.validate_embedded_ee_cert()?; roa.signed_object.verify()?; let ee_der = &roa.signed_object.signed_data.certificates[0].raw_der; let ee_crldp_uris = roa.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .crl_distribution_points_uris .as_ref(); let (issuer_crl_rsync_uri, issuer_crl_der) = choose_crl_for_certificate(ee_crldp_uris, crl_files)?; let validated = validate_ee_cert_path( ee_der, issuer_ca_der, &issuer_crl_der, issuer_ca_rsync_uri, Some(issuer_crl_rsync_uri.as_str()), validation_time, )?; validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?; Ok(roa_to_vrps(&roa)) } fn process_aspa_with_issuer( file: &PackFile, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, crl_files: &[(String, Vec)], issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>, issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, validation_time: time::OffsetDateTime, ) -> Result { let aspa = AspaObject::decode_der(&file.bytes)?; aspa.validate_embedded_ee_cert()?; aspa.signed_object.verify()?; let ee_der = &aspa.signed_object.signed_data.certificates[0].raw_der; let ee_crldp_uris = aspa.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .crl_distribution_points_uris .as_ref(); let (issuer_crl_rsync_uri, issuer_crl_der) = choose_crl_for_certificate(ee_crldp_uris, crl_files)?; let validated = validate_ee_cert_path( ee_der, issuer_ca_der, &issuer_crl_der, issuer_ca_rsync_uri, Some(issuer_crl_rsync_uri.as_str()), validation_time, )?; validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?; Ok(AspaAttestation { customer_as_id: aspa.aspa.customer_as_id, provider_as_ids: aspa.aspa.provider_as_ids.clone(), }) } fn choose_crl_for_certificate( crldp_uris: Option<&Vec>, crl_files: &[(String, Vec)], ) -> Result<(String, Vec), ObjectValidateError> { if crl_files.is_empty() { return Err(ObjectValidateError::MissingCrlInPack); } let Some(crldp_uris) = crldp_uris else { return Err(ObjectValidateError::MissingCrlDpUris); }; for u in crldp_uris { let s = u.as_str(); if let Some((uri, bytes)) = crl_files.iter().find(|(uri, _)| uri.as_str() == s) { return Ok((uri.clone(), bytes.clone())); } } Err(ObjectValidateError::CrlNotFound( crldp_uris .iter() .map(|u| u.as_str()) .collect::>() .join(", "), )) } fn validate_ee_resources_subset( ee: &ResourceCertificate, issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>, issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, ) -> Result<(), ObjectValidateError> { if let Some(child_ip) = ee.tbs.extensions.ip_resources.as_ref() { let Some(parent_ip) = issuer_effective_ip else { return Err(ObjectValidateError::MissingIssuerEffectiveIp); }; if !ip_resources_is_subset(child_ip, parent_ip) { return Err(ObjectValidateError::EeResourcesNotSubset); } } if let Some(child_as) = ee.tbs.extensions.as_resources.as_ref() { let Some(parent_as) = issuer_effective_as else { return Err(ObjectValidateError::MissingIssuerEffectiveAs); }; if !as_resources_is_subset(child_as, parent_as) { return Err(ObjectValidateError::EeResourcesNotSubset); } } Ok(()) } fn as_resources_is_subset(child: &AsResourceSet, parent: &AsResourceSet) -> bool { as_choice_subset(child.asnum.as_ref(), parent.asnum.as_ref()) && as_choice_subset(child.rdi.as_ref(), parent.rdi.as_ref()) } fn as_choice_subset( child: Option<&AsIdentifierChoice>, parent: Option<&AsIdentifierChoice>, ) -> bool { let Some(child) = child else { return true; }; let Some(parent) = parent else { return false; }; match (child, parent) { (AsIdentifierChoice::Inherit, _) => return false, (_, AsIdentifierChoice::Inherit) => return false, _ => {} } let child_intervals = as_choice_to_merged_intervals(child); let parent_intervals = as_choice_to_merged_intervals(parent); 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, _)| *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 } fn ip_resources_is_subset( child: &crate::data_model::rc::IpResourceSet, parent: &crate::data_model::rc::IpResourceSet, ) -> bool { let parent_by_afi = ip_resources_to_merged_intervals(parent); let child_by_afi = match ip_resources_to_merged_intervals_strict(child) { Ok(v) => v, Err(()) => return false, }; for (afi, child_intervals) in child_by_afi { let Some(parent_intervals) = parent_by_afi.get(&afi) else { return false; }; for (cmin, cmax) in &child_intervals { if !interval_is_covered(parent_intervals, cmin, cmax) { return false; } } } true } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum AfiKey { V4, V6, } fn ip_resources_to_merged_intervals( set: &crate::data_model::rc::IpResourceSet, ) -> std::collections::HashMap, Vec)>> { let mut m: std::collections::HashMap, Vec)>> = std::collections::HashMap::new(); for fam in &set.families { let afi = match fam.afi { crate::data_model::rc::Afi::Ipv4 => AfiKey::V4, crate::data_model::rc::Afi::Ipv6 => AfiKey::V6, }; match &fam.choice { IpAddressChoice::Inherit => { // Effective resource sets should not contain inherit, but if they do we treat it // as "unknown" by leaving it empty here (subset checks will fail). } IpAddressChoice::AddressesOrRanges(items) => { let ent = m.entry(afi).or_default(); for item in items { match item { IpAddressOrRange::Prefix(p) => ent.push(prefix_to_range(p)), 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_resources_to_merged_intervals_strict( set: &crate::data_model::rc::IpResourceSet, ) -> Result, Vec)>>, ()> { let mut m: std::collections::HashMap, Vec)>> = std::collections::HashMap::new(); for fam in &set.families { let afi = match fam.afi { crate::data_model::rc::Afi::Ipv4 => AfiKey::V4, crate::data_model::rc::Afi::Ipv6 => AfiKey::V6, }; match &fam.choice { IpAddressChoice::Inherit => return Err(()), IpAddressChoice::AddressesOrRanges(items) => { let ent = m.entry(afi).or_default(); for item in items { match item { IpAddressOrRange::Prefix(p) => ent.push(prefix_to_range(p)), 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); } Ok(m) } 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: &RcIpPrefix) -> (Vec, Vec) { let mut min = prefix.addr.clone(); let mut max = prefix.addr.clone(); let bitlen = prefix.afi.ub(); 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 } 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()] } fn roa_to_vrps(roa: &RoaObject) -> Vec { let asn = roa.roa.as_id; let mut out = Vec::new(); for fam in &roa.roa.ip_addr_blocks { for entry in &fam.addresses { let max_length = entry.max_length.unwrap_or(entry.prefix.prefix_len); out.push(Vrp { asn, prefix: entry.prefix.clone(), max_length, }); } } out } #[allow(dead_code)] fn roa_afi_to_string(afi: RoaAfi) -> &'static str { match afi { RoaAfi::Ipv4 => "ipv4", RoaAfi::Ipv6 => "ipv6", } } #[cfg(test)] mod tests { use super::*; use crate::data_model::rc::{ Afi, AsIdOrRange, AsIdentifierChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, IpResourceSet, }; fn fixture_bytes(path: &str) -> Vec { std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path)) .unwrap_or_else(|e| panic!("read fixture {path}: {e}")) } #[test] fn merge_as_intervals_merges_overlapping_and_adjacent() { let v = vec![(1, 2), (3, 5), (10, 10), (11, 12)]; let merged = merge_as_intervals(&v); assert_eq!(merged, vec![(1, 5), (10, 12)]); } #[test] fn as_choice_subset_rejects_inherit() { let child = Some(&AsIdentifierChoice::Inherit); let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![ AsIdOrRange::Range { min: 1, max: 10 }, ])); assert!(!as_choice_subset(child, parent)); } #[test] fn as_choice_subset_checks_ranges() { let child = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![ AsIdOrRange::Id(5), AsIdOrRange::Range { min: 7, max: 9 }, ])); let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![ AsIdOrRange::Range { min: 1, max: 10 }, ])); assert!(as_choice_subset(child, parent)); } #[test] fn ip_resources_is_subset_accepts_prefixes_and_ranges() { let parent = 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], }), IpAddressOrRange::Range(IpAddressRange { min: vec![192, 0, 2, 0], max: vec![192, 0, 2, 255], }), ]), }], }; let child = 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], }), IpAddressOrRange::Range(IpAddressRange { min: vec![192, 0, 2, 10], max: vec![192, 0, 2, 20], }), ]), }], }; assert!(ip_resources_is_subset(&child, &parent)); } #[test] fn ip_resources_is_subset_rejects_inherit_in_child() { let parent = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv6, choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( IpPrefix { afi: Afi::Ipv6, prefix_len: 32, addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }, )]), }], }; let child = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv6, choice: IpAddressChoice::Inherit, }], }; assert!(!ip_resources_is_subset(&child, &parent)); } #[test] fn increment_bytes_wraps_all_ff_to_zero() { assert_eq!(increment_bytes(&[0xFF, 0xFF]), vec![0x00, 0x00]); } #[test] fn merge_ip_intervals_merges_contiguous() { let v = vec![ (vec![0, 0, 0, 0], vec![0, 0, 0, 10]), (vec![0, 0, 0, 11], vec![0, 0, 0, 20]), ]; let merged = merge_ip_intervals(&v); assert_eq!(merged, vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 20])]); } #[test] fn choose_crl_for_certificate_reports_missing_crl_in_pack() { let roa_der = fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); let roa = RoaObject::decode_der(&roa_der).expect("decode roa"); let ee_crldp_uris = roa.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .crl_distribution_points_uris .as_ref(); let err = choose_crl_for_certificate(ee_crldp_uris, &[]).unwrap_err(); assert!(matches!(err, ObjectValidateError::MissingCrlInPack)); } #[test] fn choose_crl_for_certificate_reports_missing_crldp_uris() { let crl_a = ("rsync://example.test/a.crl".to_string(), vec![0x01]); let err = choose_crl_for_certificate(None, &[crl_a]).unwrap_err(); assert!(matches!(err, ObjectValidateError::MissingCrlDpUris)); } #[test] fn choose_crl_for_certificate_prefers_matching_crldp_uri_in_order() { let roa_der = fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); let roa = RoaObject::decode_der(&roa_der).expect("decode roa"); let ee_crldp_uris = roa.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .crl_distribution_points_uris .as_ref() .expect("fixture ee has crldp"); // Use two CRLs, only one matches the first CRLDP URI. let other_crl_der = fixture_bytes( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", ); let matching_uri = ee_crldp_uris[0].as_str().to_string(); let matching_crl_der = vec![0x01, 0x02, 0x03]; let (uri, bytes) = choose_crl_for_certificate( Some(ee_crldp_uris), &[ ("rsync://example.test/other.crl".to_string(), other_crl_der), (matching_uri.clone(), matching_crl_der.clone()), ], ) .unwrap(); assert_eq!(uri, matching_uri); assert_eq!(bytes, matching_crl_der); } #[test] fn choose_crl_for_certificate_reports_not_found_when_crldp_does_not_match_pack() { let roa_der = fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); let roa = RoaObject::decode_der(&roa_der).expect("decode roa"); let ee_crldp_uris = roa.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .crl_distribution_points_uris .as_ref(); let err = choose_crl_for_certificate( ee_crldp_uris, &[("rsync://example.test/other.crl".to_string(), vec![0x01])], ) .unwrap_err(); assert!(matches!(err, ObjectValidateError::CrlNotFound(_))); } #[test] fn validate_ee_resources_subset_reports_missing_issuer_effective_ip() { let roa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); let roa_der = std::fs::read(roa_path).expect("read roa"); let roa = RoaObject::decode_der(&roa_der).expect("decode roa"); let ee = &roa.signed_object.signed_data.certificates[0].resource_cert; let err = validate_ee_resources_subset(ee, None, None).unwrap_err(); assert!(matches!(err, ObjectValidateError::MissingIssuerEffectiveIp)); } #[test] fn validate_ee_resources_subset_reports_missing_issuer_effective_as() { let aspa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", ); let aspa_der = std::fs::read(aspa_path).expect("read aspa"); let aspa = AspaObject::decode_der(&aspa_der).expect("decode aspa"); let ee = &aspa.signed_object.signed_data.certificates[0].resource_cert; let issuer_ip = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv6, choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( IpPrefix { afi: Afi::Ipv6, prefix_len: 32, addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }, )]), }], }; let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None).unwrap_err(); assert!(matches!(err, ObjectValidateError::MissingIssuerEffectiveAs)); } #[test] fn validate_ee_resources_subset_reports_not_subset() { let roa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); let roa_der = std::fs::read(roa_path).expect("read roa"); let roa = RoaObject::decode_der(&roa_der).expect("decode roa"); let ee = &roa.signed_object.signed_data.certificates[0].resource_cert; // Unrelated parent resources. let issuer_ip = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv6, choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( IpPrefix { afi: Afi::Ipv6, prefix_len: 32, addr: vec![0x26, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }, )]), }], }; let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None).unwrap_err(); assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset)); } }