diff --git a/src/audit.rs b/src/audit.rs index 03e1a71..bfd9f98 100644 --- a/src/audit.rs +++ b/src/audit.rs @@ -52,6 +52,18 @@ impl From<&crate::report::Warning> for AuditWarning { #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] pub struct PublicationPointAudit { + /// Monotonic node ID assigned by the traversal engine. + /// + /// Present when running via the Stage2 tree engine; may be absent in ad-hoc runs. + #[serde(skip_serializing_if = "Option::is_none")] + pub node_id: Option, + /// Parent node ID in the traversal tree. + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_node_id: Option, + /// Provenance metadata for non-root nodes (how this CA instance was discovered). + #[serde(skip_serializing_if = "Option::is_none")] + pub discovered_from: Option, + pub rsync_base_uri: String, pub manifest_rsync_uri: String, pub publication_point_rsync_uri: String, @@ -67,6 +79,13 @@ pub struct PublicationPointAudit { pub objects: Vec, } +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct DiscoveredFrom { + pub parent_manifest_rsync_uri: String, + pub child_ca_certificate_rsync_uri: String, + pub child_ca_certificate_sha256_hex: String, +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct TreeSummary { pub instances_processed: usize, diff --git a/src/cli.rs b/src/cli.rs index 5dc715e..fef4621 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -205,7 +205,10 @@ fn unique_rrdp_repos(report: &AuditReportV1) -> usize { fn print_summary(report: &AuditReportV1) { let rrdp_repos = unique_rrdp_repos(report); println!("RPKI stage2 serial run summary"); - println!("validation_time={}", report.meta.validation_time_rfc3339_utc); + println!( + "validation_time={}", + report.meta.validation_time_rfc3339_utc + ); println!( "publication_points_processed={} publication_points_failed={}", report.tree.instances_processed, report.tree.instances_failed @@ -213,7 +216,10 @@ fn print_summary(report: &AuditReportV1) { println!("rrdp_repos_unique={rrdp_repos}"); println!("vrps={}", report.vrps.len()); println!("aspas={}", report.aspas.len()); - println!("audit_publication_points={}", report.publication_points.len()); + println!( + "audit_publication_points={}", + report.publication_points.len() + ); println!( "warnings_total={}", report.tree.warnings.len() @@ -278,7 +284,9 @@ pub fn run(argv: &[String]) -> Result<(), String> { let args = parse_args(argv)?; let policy = read_policy(args.policy_path.as_deref())?; - let validation_time = args.validation_time.unwrap_or_else(time::OffsetDateTime::now_utc); + let validation_time = args + .validation_time + .unwrap_or_else(time::OffsetDateTime::now_utc); let store = RocksStore::open(&args.db_path).map_err(|e| e.to_string())?; let http = BlockingHttpFetcher::new(HttpFetcherConfig::default()).map_err(|e| e.to_string())?; @@ -290,7 +298,11 @@ pub fn run(argv: &[String]) -> Result<(), String> { let out = if let Some(dir) = args.rsync_local_dir.as_ref() { let rsync = LocalDirRsyncFetcher::new(dir); - match (args.tal_url.as_ref(), args.tal_path.as_ref(), args.ta_path.as_ref()) { + match ( + args.tal_url.as_ref(), + args.tal_path.as_ref(), + args.ta_path.as_ref(), + ) { (Some(url), _, _) => run_tree_from_tal_url_serial_audit( &store, &policy, @@ -323,7 +335,11 @@ pub fn run(argv: &[String]) -> Result<(), String> { } } else { let rsync = SystemRsyncFetcher::new(SystemRsyncConfig::default()); - match (args.tal_url.as_ref(), args.tal_path.as_ref(), args.ta_path.as_ref()) { + match ( + args.tal_url.as_ref(), + args.tal_path.as_ref(), + args.ta_path.as_ref(), + ) { (Some(url), _, _) => run_tree_from_tal_url_serial_audit( &store, &policy, @@ -404,7 +420,10 @@ mod tests { "x.tal".to_string(), ]; let err = parse_args(&argv).unwrap_err(); - assert!(err.contains("exactly one of --tal-url or --tal-path"), "{err}"); + assert!( + err.contains("exactly one of --tal-url or --tal-path"), + "{err}" + ); } #[test] @@ -482,7 +501,10 @@ mod tests { fn parse_rejects_missing_tal_mode() { let argv = vec!["rpki".to_string(), "--db".to_string(), "db".to_string()]; let err = parse_args(&argv).unwrap_err(); - assert!(err.contains("--tal-url") || err.contains("--tal-path"), "{err}"); + assert!( + err.contains("--tal-url") || err.contains("--tal-path"), + "{err}" + ); } #[test] @@ -573,18 +595,18 @@ mod tests { .expect("read ta fixture"); let discovery = crate::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der( - &tal_bytes, - &ta_der, - None, + &tal_bytes, &ta_der, None, ) .expect("discover root"); let tree = crate::validation::tree::TreeRunOutput { instances_processed: 1, instances_failed: 0, - warnings: vec![crate::report::Warning::new("synthetic warning") - .with_rfc_refs(&[crate::report::RfcRef("RFC 6487 §4.8.8.1")]) - .with_context("rsync://example.test/repo/pp/")], + warnings: vec![ + crate::report::Warning::new("synthetic warning") + .with_rfc_refs(&[crate::report::RfcRef("RFC 6487 §4.8.8.1")]) + .with_context("rsync://example.test/repo/pp/"), + ], vrps: vec![crate::validation::objects::Vrp { asn: 64496, prefix: crate::data_model::roa::IpPrefix { diff --git a/src/data_model/rc.rs b/src/data_model/rc.rs index 7f71bde..5212f25 100644 --- a/src/data_model/rc.rs +++ b/src/data_model/rc.rs @@ -11,8 +11,8 @@ use crate::data_model::common::{ }; use crate::data_model::oid::{ OID_AD_CA_ISSUERS, OID_AD_SIGNED_OBJECT, OID_AUTHORITY_INFO_ACCESS, - OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTONOMOUS_SYS_IDS, OID_CRL_DISTRIBUTION_POINTS, - OID_CP_IPADDR_ASNUMBER, OID_IP_ADDR_BLOCKS, OID_SHA256_WITH_RSA_ENCRYPTION, + OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTONOMOUS_SYS_IDS, OID_CP_IPADDR_ASNUMBER, + OID_CRL_DISTRIBUTION_POINTS, OID_IP_ADDR_BLOCKS, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER, }; @@ -451,19 +451,13 @@ pub enum ResourceCertificateProfileError { )] CrlDistributionPointsCriticality, - #[error( - "CRLDistributionPoints MUST be omitted in self-signed certificates (RFC 6487 §4.8.6)" - )] + #[error("CRLDistributionPoints MUST be omitted in self-signed certificates (RFC 6487 §4.8.6)")] CrlDistributionPointsSelfSignedMustOmit, - #[error( - "CRLDistributionPoints must contain exactly one DistributionPoint (RFC 6487 §4.8.6)" - )] + #[error("CRLDistributionPoints must contain exactly one DistributionPoint (RFC 6487 §4.8.6)")] CrlDistributionPointsNotSingle, - #[error( - "CRLDistributionPoints distributionPoint field MUST be present (RFC 6487 §4.8.6)" - )] + #[error("CRLDistributionPoints distributionPoint field MUST be present (RFC 6487 §4.8.6)")] CrlDistributionPointsNoDistributionPoint, #[error("CRLDistributionPoints reasons field MUST be omitted (RFC 6487 §4.8.6)")] @@ -495,9 +489,7 @@ pub enum ResourceCertificateProfileError { )] AuthorityInfoAccessCriticality, - #[error( - "authorityInfoAccess MUST be omitted in self-signed certificates (RFC 6487 §4.8.7)" - )] + #[error("authorityInfoAccess MUST be omitted in self-signed certificates (RFC 6487 §4.8.7)")] AuthorityInfoAccessSelfSignedMustOmit, #[error( @@ -505,9 +497,7 @@ pub enum ResourceCertificateProfileError { )] AuthorityInfoAccessCaIssuersNotUri, - #[error( - "authorityInfoAccess must include at least one id-ad-caIssuers URI (RFC 6487 §4.8.7)" - )] + #[error("authorityInfoAccess must include at least one id-ad-caIssuers URI (RFC 6487 §4.8.7)")] AuthorityInfoAccessMissingCaIssuers, #[error("authorityInfoAccess must include at least one rsync:// URI (RFC 6487 §4.8.7)")] @@ -674,7 +664,8 @@ impl RcExtensionsParsed { } let keyid = aki.key_identifier.clone(); if is_self_signed { - if let (Some(keyid), Some(ski)) = (keyid.as_ref(), subject_key_identifier.as_ref()) + if let (Some(keyid), Some(ski)) = + (keyid.as_ref(), subject_key_identifier.as_ref()) { if keyid != ski { return Err(ResourceCertificateProfileError::AkiSelfSignedNotEqualSki); @@ -705,7 +696,9 @@ impl RcExtensionsParsed { return Err(ResourceCertificateProfileError::CrlDistributionPointsCriticality); } if is_self_signed { - return Err(ResourceCertificateProfileError::CrlDistributionPointsSelfSignedMustOmit); + return Err( + ResourceCertificateProfileError::CrlDistributionPointsSelfSignedMustOmit, + ); } if crldp.distribution_points.len() != 1 { return Err(ResourceCertificateProfileError::CrlDistributionPointsNotSingle); @@ -718,13 +711,17 @@ impl RcExtensionsParsed { return Err(ResourceCertificateProfileError::CrlDistributionPointsHasCrlIssuer); } if !dp.distribution_point_present { - return Err(ResourceCertificateProfileError::CrlDistributionPointsNoDistributionPoint); + return Err( + ResourceCertificateProfileError::CrlDistributionPointsNoDistributionPoint, + ); } if dp.name_relative_to_crl_issuer_present || !dp.full_name_present { return Err(ResourceCertificateProfileError::CrlDistributionPointsInvalidName); } if dp.full_name_not_uri { - return Err(ResourceCertificateProfileError::CrlDistributionPointsFullNameNotUri); + return Err( + ResourceCertificateProfileError::CrlDistributionPointsFullNameNotUri, + ); } if !dp.full_name_uris.iter().any(|u| u.scheme() == "rsync") { return Err(ResourceCertificateProfileError::CrlDistributionPointsNoRsync); @@ -751,13 +748,19 @@ impl RcExtensionsParsed { return Err(ResourceCertificateProfileError::AuthorityInfoAccessCriticality); } if is_self_signed { - return Err(ResourceCertificateProfileError::AuthorityInfoAccessSelfSignedMustOmit); + return Err( + ResourceCertificateProfileError::AuthorityInfoAccessSelfSignedMustOmit, + ); } if aia.ca_issuers_access_location_not_uri { - return Err(ResourceCertificateProfileError::AuthorityInfoAccessCaIssuersNotUri); + return Err( + ResourceCertificateProfileError::AuthorityInfoAccessCaIssuersNotUri, + ); } if aia.ca_issuers_uris.is_empty() { - return Err(ResourceCertificateProfileError::AuthorityInfoAccessMissingCaIssuers); + return Err( + ResourceCertificateProfileError::AuthorityInfoAccessMissingCaIssuers, + ); } if !aia.ca_issuers_uris.iter().any(|u| u.scheme() == "rsync") { return Err(ResourceCertificateProfileError::AuthorityInfoAccessNoRsync); diff --git a/src/fetch/http.rs b/src/fetch/http.rs index 6f836c4..db3da97 100644 --- a/src/fetch/http.rs +++ b/src/fetch/http.rs @@ -62,4 +62,3 @@ impl Fetcher for BlockingHttpFetcher { self.fetch_bytes(uri) } } - diff --git a/src/fetch/rsync_system.rs b/src/fetch/rsync_system.rs index dc1b3cb..5245136 100644 --- a/src/fetch/rsync_system.rs +++ b/src/fetch/rsync_system.rs @@ -50,7 +50,9 @@ impl SystemRsyncFetcher { .arg(src) .arg(dst); - let out = cmd.output().map_err(|e| format!("rsync spawn failed: {e}"))?; + let out = cmd + .output() + .map_err(|e| format!("rsync spawn failed: {e}"))?; if !out.status.success() { let stderr = String::from_utf8_lossy(&out.stderr); let stdout = String::from_utf8_lossy(&out.stdout); diff --git a/src/storage.rs b/src/storage.rs index e79b9c2..bfa7a15 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -168,10 +168,7 @@ impl RocksStore { ) -> StorageResult, Box<[u8]>)> + 'a> { let cf = self.cf(CF_RAW_OBJECTS)?; let mode = IteratorMode::Start; - Ok(self - .db - .iterator_cf(cf, mode) - .filter_map(|res| res.ok())) + Ok(self.db.iterator_cf(cf, mode).filter_map(|res| res.ok())) } #[allow(dead_code)] @@ -180,10 +177,7 @@ impl RocksStore { ) -> StorageResult, Box<[u8]>)> + 'a> { let cf = self.cf(CF_VERIFIED_PUBLICATION_POINTS)?; let mode = IteratorMode::Start; - Ok(self - .db - .iterator_cf(cf, mode) - .filter_map(|res| res.ok())) + Ok(self.db.iterator_cf(cf, mode).filter_map(|res| res.ok())) } #[allow(dead_code)] diff --git a/src/validation/ca_instance.rs b/src/validation/ca_instance.rs index 267db83..23119ca 100644 --- a/src/validation/ca_instance.rs +++ b/src/validation/ca_instance.rs @@ -98,7 +98,8 @@ pub fn ca_instance_uris_from_ca_certificate( } } - let mut publication_point_rsync_uri = ca_repo.ok_or(CaInstanceUrisError::MissingCaRepository)?; + let mut publication_point_rsync_uri = + ca_repo.ok_or(CaInstanceUrisError::MissingCaRepository)?; if !publication_point_rsync_uri.ends_with('/') { publication_point_rsync_uri.push('/'); } @@ -120,4 +121,3 @@ pub fn ca_instance_uris_from_ca_certificate( rrdp_notification_uri: notify, }) } - diff --git a/src/validation/ca_path.rs b/src/validation/ca_path.rs index 9caeb49..2af638a 100644 --- a/src/validation/ca_path.rs +++ b/src/validation/ca_path.rs @@ -51,17 +51,13 @@ pub enum CaPathError { #[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)" - )] + #[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)" - )] + #[error("child CA KeyUsage must have only keyCertSign and cRLSign set (RFC 6487 §4.8.4)")] KeyUsageInvalidBits, #[error( @@ -370,7 +366,8 @@ fn resolve_child_ip_resources( } IpAddressChoice::AddressesOrRanges(items) => { // Subset check against parent union for that AFI. - let parent_set = ip_resources_single_afi(parent, fam.afi, parent_by_afi.get(&fam.afi)); + let parent_set = + ip_resources_single_afi(parent, fam.afi, parent_by_afi.get(&fam.afi)); if !ip_family_items_subset(items, &parent_set) { return Err(CaPathError::ResourcesNotSubset); } @@ -382,7 +379,9 @@ fn resolve_child_ip_resources( } } - Ok(Some(IpResourceSet { families: out_families })) + Ok(Some(IpResourceSet { + families: out_families, + })) } fn resolve_child_as_resources( @@ -403,7 +402,11 @@ fn resolve_child_as_resources( let asnum = match child_as.asnum.as_ref() { None => None, - Some(AsIdentifierChoice::Inherit) => parent.asnum.clone().ok_or(CaPathError::InheritWithoutParentResources).map(Some)?, + Some(AsIdentifierChoice::Inherit) => parent + .asnum + .clone() + .ok_or(CaPathError::InheritWithoutParentResources) + .map(Some)?, Some(_) => { if !as_choice_subset(child_as.asnum.as_ref(), parent.asnum.as_ref()) { return Err(CaPathError::ResourcesNotSubset); @@ -414,7 +417,11 @@ fn resolve_child_as_resources( let rdi = match child_as.rdi.as_ref() { None => None, - Some(AsIdentifierChoice::Inherit) => parent.rdi.clone().ok_or(CaPathError::InheritWithoutParentResources).map(Some)?, + Some(AsIdentifierChoice::Inherit) => parent + .rdi + .clone() + .ok_or(CaPathError::InheritWithoutParentResources) + .map(Some)?, Some(_) => { if !as_choice_subset(child_as.rdi.as_ref(), parent.rdi.as_ref()) { return Err(CaPathError::ResourcesNotSubset); @@ -426,10 +433,10 @@ fn resolve_child_as_resources( Ok(Some(AsResourceSet { asnum, rdi })) } - fn as_choice_subset( - child: Option<&AsIdentifierChoice>, - parent: Option<&AsIdentifierChoice>, - ) -> bool { +fn as_choice_subset( + child: Option<&AsIdentifierChoice>, + parent: Option<&AsIdentifierChoice>, +) -> bool { let Some(child) = child else { return true; }; @@ -508,9 +515,17 @@ enum AfiKey { fn ip_resources_by_afi_items( set: &IpResourceSet, -) -> Result>, CaPathError> { - let mut m: std::collections::BTreeMap> = - std::collections::BTreeMap::new(); +) -> 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), @@ -555,8 +570,12 @@ fn ip_family_items_subset( 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())), + 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)); @@ -673,7 +692,9 @@ mod tests { use crate::data_model::rc::{ Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpResourceSet, }; - use crate::data_model::rc::{RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate}; + use crate::data_model::rc::{ + RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, + }; use der_parser::num_bigint::BigUint; use url::Url; @@ -882,11 +903,9 @@ mod tests { 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(); + 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( @@ -898,15 +917,10 @@ mod tests { 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}" - ); + 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( diff --git a/src/validation/cert_path.rs b/src/validation/cert_path.rs index d53be45..a453b4b 100644 --- a/src/validation/cert_path.rs +++ b/src/validation/cert_path.rs @@ -42,17 +42,13 @@ pub enum CertPathError { #[error("EE certificate signature verification failed: {0} (RFC 5280 §6.1)")] EeSignatureInvalid(String), - #[error( - "EE KeyUsage extension missing (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)" - )] + #[error("EE KeyUsage extension missing (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")] KeyUsageMissing, #[error("EE KeyUsage criticality must be critical (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")] KeyUsageNotCritical, - #[error( - "EE KeyUsage must have only digitalSignature set (RFC 6487 §4.8.4)" - )] + #[error("EE KeyUsage must have only digitalSignature set (RFC 6487 §4.8.4)")] KeyUsageInvalidBits, #[error("issuer CA subjectKeyIdentifier missing (RFC 6487 §4.8.2)")] @@ -303,7 +299,9 @@ fn is_serial_revoked_by_crl(ee: &ResourceCertificate, crl: &RpkixCrl) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::data_model::rc::{RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate}; + use crate::data_model::rc::{ + RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, + }; use der_parser::num_bigint::BigUint; use url::Url; @@ -410,9 +408,11 @@ mod tests { None, Some(vec!["rsync://example.test/issuer.crl"]), ); - let err = - validate_ee_aia_points_to_issuer_uri(&ee_missing_aia, "rsync://example.test/issuer.cer") - .unwrap_err(); + let err = validate_ee_aia_points_to_issuer_uri( + &ee_missing_aia, + "rsync://example.test/issuer.cer", + ) + .unwrap_err(); assert!(matches!(err, CertPathError::EeAiaMissing), "{err}"); let ee_wrong_aia = dummy_cert( @@ -462,10 +462,7 @@ mod tests { "rsync://example.test/issuer.crl", ) .unwrap_err(); - assert!( - matches!(err, CertPathError::EeCrlDpUriMismatch), - "{err}" - ); + assert!(matches!(err, CertPathError::EeCrlDpUriMismatch), "{err}"); } #[test] diff --git a/src/validation/from_tal.rs b/src/validation/from_tal.rs index 99d1590..948ae16 100644 --- a/src/validation/from_tal.rs +++ b/src/validation/from_tal.rs @@ -3,8 +3,9 @@ use url::Url; use crate::data_model::ta::{TrustAnchor, TrustAnchorError}; use crate::data_model::tal::{Tal, TalDecodeError}; use crate::sync::rrdp::Fetcher; -use crate::validation::ca_instance::{CaInstanceUris, CaInstanceUrisError, ca_instance_uris_from_ca_certificate}; -use crate::validation::objects::IssuerCaCertificateResolver; +use crate::validation::ca_instance::{ + CaInstanceUris, CaInstanceUrisError, ca_instance_uris_from_ca_certificate, +}; use crate::validation::run::{RunError, RunOutput, run_publication_point_once}; #[derive(Clone, Debug, PartialEq, Eq)] @@ -82,14 +83,14 @@ pub fn discover_root_ca_instance_from_tal( } }; - let ca_instance = match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) - { - Ok(v) => v, - Err(e) => { - last_err = Some(format!("CA instance discovery failed: {e}")); - continue; - } - }; + let ca_instance = + match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) { + Ok(v) => v, + Err(e) => { + last_err = Some(format!("CA instance discovery failed: {e}")); + continue; + } + }; return Ok(DiscoveredRootCaInstance { tal_url, @@ -98,9 +99,9 @@ pub fn discover_root_ca_instance_from_tal( }); } - Err(FromTalError::TaFetch( - last_err.unwrap_or_else(|| "unknown TA candidate error".to_string()), - )) + Err(FromTalError::TaFetch(last_err.unwrap_or_else(|| { + "unknown TA candidate error".to_string() + }))) } pub fn discover_root_ca_instance_from_tal_and_ta_der( @@ -124,7 +125,6 @@ pub fn run_root_from_tal_url_once( tal_url: &str, http_fetcher: &dyn Fetcher, rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher, - issuer_resolver: &dyn IssuerCaCertificateResolver, validation_time: time::OffsetDateTime, ) -> Result { let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?; @@ -138,7 +138,24 @@ pub fn run_root_from_tal_url_once( &discovery.ca_instance.publication_point_rsync_uri, http_fetcher, rsync_fetcher, - issuer_resolver, + &discovery.trust_anchor.ta_certificate.raw_der, + None, + discovery + .trust_anchor + .ta_certificate + .rc_ca + .tbs + .extensions + .ip_resources + .as_ref(), + discovery + .trust_anchor + .ta_certificate + .rc_ca + .tbs + .extensions + .as_resources + .as_ref(), validation_time, )?; diff --git a/src/validation/mod.rs b/src/validation/mod.rs index ca223d2..220b237 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -1,10 +1,10 @@ -pub mod cert_path; pub mod ca_instance; pub mod ca_path; +pub mod cert_path; pub mod from_tal; pub mod manifest; pub mod objects; pub mod run; +pub mod run_tree_from_tal; pub mod tree; pub mod tree_runner; -pub mod run_tree_from_tal; diff --git a/src/validation/objects.rs b/src/validation/objects.rs index 1b076cc..4c83d21 100644 --- a/src/validation/objects.rs +++ b/src/validation/objects.rs @@ -1,18 +1,29 @@ +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, - ResourceCertKind, ResourceCertificate, + ResourceCertificate, }; use crate::data_model::roa::{IpPrefix, RoaAfi, RoaDecodeError, RoaObject, RoaValidateError}; use crate::data_model::signed_object::SignedObjectVerifyError; -use crate::audit::{AuditObjectKind, AuditObjectResult, ObjectAuditEntry, sha256_hex_from_32}; use crate::policy::{Policy, SignedObjectFailurePolicy}; use crate::report::{RfcRef, Warning}; use crate::storage::{PackFile, VerifiedPublicationPointPack}; use crate::validation::cert_path::{CertPathError, validate_ee_cert_path}; -use std::collections::HashMap; +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 { @@ -60,8 +71,16 @@ pub fn process_verified_publication_point_pack_for_issuer( ) -> 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(); + 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. @@ -75,43 +94,42 @@ pub fn process_verified_publication_point_pack_for_issuer( .map(|f| (f.rsync_uri.clone(), f.bytes.clone())) .collect::>(); - let (issuer_crl_uri, issuer_crl_der) = match choose_crl_for_issuer(issuer_ca_der, &crl_files) { - Ok((uri, der)) => (uri, der), - Err(e) => { - stats.publication_point_dropped = true; - warnings.push( - Warning::new(format!("dropping publication point: {e}")) - .with_rfc_refs(&[RfcRef("RFC 6487 §5")]) - .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 issuer CRL".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 issuer CRL".to_string()), - }); - } + // 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 verified pack") + .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 verified pack".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 verified pack".to_string()), + }); } - return ObjectsOutput { - vrps: Vec::new(), - aspas: Vec::new(), - warnings, - stats, - audit, - }; } - }; + return ObjectsOutput { + vrps: Vec::new(), + aspas: Vec::new(), + warnings, + stats, + audit, + }; + } let mut vrps: Vec = Vec::new(); let mut aspas: Vec = Vec::new(); @@ -121,9 +139,8 @@ pub fn process_verified_publication_point_pack_for_issuer( match process_roa_with_issuer( file, issuer_ca_der, - &issuer_crl_der, issuer_ca_rsync_uri, - Some(issuer_crl_uri.as_str()), + &crl_files, issuer_effective_ip, issuer_effective_as, validation_time, @@ -148,9 +165,11 @@ pub fn process_verified_publication_point_pack_for_issuer( 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(&[RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")]) + .with_rfc_refs(&refs) .with_context(&file.rsync_uri), ) } @@ -188,12 +207,14 @@ pub fn process_verified_publication_point_pack_for_issuer( }); } } + 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(&[RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")]) + .with_rfc_refs(&refs) .with_context(&pack.manifest_rsync_uri), ); return ObjectsOutput { @@ -210,9 +231,8 @@ pub fn process_verified_publication_point_pack_for_issuer( match process_aspa_with_issuer( file, issuer_ca_der, - &issuer_crl_der, issuer_ca_rsync_uri, - Some(issuer_crl_uri.as_str()), + &crl_files, issuer_effective_ip, issuer_effective_as, validation_time, @@ -237,9 +257,11 @@ pub fn process_verified_publication_point_pack_for_issuer( 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(&[RfcRef("RFC 6488 §3")]) + .with_rfc_refs(&refs) .with_context(&file.rsync_uri), ) } @@ -277,12 +299,14 @@ pub fn process_verified_publication_point_pack_for_issuer( }); } } + 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(&[RfcRef("RFC 6488 §3")]) + .with_rfc_refs(&refs) .with_context(&pack.manifest_rsync_uri), ); return ObjectsOutput { @@ -307,221 +331,6 @@ pub fn process_verified_publication_point_pack_for_issuer( } } -#[derive(Debug, thiserror::Error)] -pub enum ObjectsProcessError { - #[error( - "publication point dropped due to invalid signed object: {rsync_uri}: {detail} (policy=signed_object_failure_policy=drop_publication_point)" - )] - PublicationPointDropped { rsync_uri: String, detail: String }, -} - -pub trait IssuerCaCertificateResolver { - fn resolve_by_subject_dn(&self, subject_dn: &str) -> Option>; -} - -pub fn process_verified_publication_point_pack( - pack: &VerifiedPublicationPointPack, - policy: &Policy, - issuer_resolver: &dyn IssuerCaCertificateResolver, - validation_time: time::OffsetDateTime, -) -> Result { - let mut warnings = 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(); - - // Parse manifest once (primarily to enforce that `manifest_bytes` really is a manifest object). - let _manifest = - ManifestObject::decode_der(&pack.manifest_bytes).expect("verified pack manifest decodes"); - - // Index CA certs found in the pack by subject DN (best-effort; packs may be incomplete). - let mut ca_certs_by_subject: HashMap> = HashMap::new(); - for f in &pack.files { - if !f.rsync_uri.ends_with(".cer") { - continue; - } - let Ok(cert) = ResourceCertificate::decode_der(&f.bytes) else { - continue; - }; - if cert.kind != ResourceCertKind::Ca { - continue; - } - ca_certs_by_subject.insert(cert.tbs.subject_dn.clone(), f.bytes.clone()); - } - - // Decode CRLs present in the pack (may be none in synthetic tests). - let crl_files = pack - .files - .iter() - .filter(|f| f.rsync_uri.ends_with(".crl")) - .map(|f| (f.rsync_uri.clone(), f.bytes.clone())) - .collect::>(); - - let mut vrps = Vec::new(); - let mut aspas = Vec::new(); - - for (idx, file) in pack.files.iter().enumerate() { - if file.rsync_uri.ends_with(".roa") { - match process_roa( - file, - &ca_certs_by_subject, - issuer_resolver, - &crl_files, - 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()), - }); - warnings.push( - Warning::new(format!("dropping invalid ROA: {}: {e}", file.rsync_uri)) - .with_rfc_refs(&[RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")]) - .with_context(&file.rsync_uri), - ) - } - SignedObjectFailurePolicy::DropPublicationPoint => { - 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(), - ), - }); - } - } - return Err(ObjectsProcessError::PublicationPointDropped { - rsync_uri: file.rsync_uri.clone(), - detail: e.to_string(), - }); - } - }, - } - } else if file.rsync_uri.ends_with(".asa") { - match process_aspa( - file, - &ca_certs_by_subject, - issuer_resolver, - &crl_files, - 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()), - }); - warnings.push( - Warning::new(format!("dropping invalid ASPA: {}: {e}", file.rsync_uri)) - .with_rfc_refs(&[RfcRef("RFC 6488 §3")]) - .with_context(&file.rsync_uri), - ) - } - SignedObjectFailurePolicy::DropPublicationPoint => { - 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(), - ), - }); - } - } - return Err(ObjectsProcessError::PublicationPointDropped { - rsync_uri: file.rsync_uri.clone(), - detail: e.to_string(), - }); - } - }, - } - } - } - - Ok(ObjectsOutput { - vrps, - aspas, - warnings, - stats, - audit, - }) -} - #[derive(Debug, thiserror::Error)] enum ObjectValidateError { #[error("ROA decode failed: {0}")] @@ -542,11 +351,20 @@ enum ObjectValidateError { #[error("EE certificate path validation failed: {0}")] CertPath(#[from] CertPathError), - #[error("missing issuer CA certificate for subject DN: {0}")] - MissingIssuerCaCert(String), + #[error( + "certificate CRLDistributionPoints URIs missing (cannot select issuer CRL) (RFC 6487 §4.8.6)" + )] + MissingCrlDpUris, - #[error("no CRL available for issuer CA")] - MissingCrl, + #[error( + "no CRL available in verified pack (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)" + )] + MissingCrlInPack, + + #[error( + "CRL referenced by CRLDistributionPoints not found in verified pack: {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)" @@ -564,49 +382,11 @@ enum ObjectValidateError { EeResourcesNotSubset, } -fn process_roa( - file: &PackFile, - ca_certs_by_subject: &HashMap>, - issuer_resolver: &dyn IssuerCaCertificateResolver, - crl_files: &[(String, Vec)], - 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_issuer_dn = roa.signed_object.signed_data.certificates[0] - .resource_cert - .tbs - .issuer_dn - .clone(); - - let issuer_ca_der = ca_certs_by_subject - .get(&ee_issuer_dn) - .cloned() - .or_else(|| issuer_resolver.resolve_by_subject_dn(&ee_issuer_dn)) - .ok_or_else(|| ObjectValidateError::MissingIssuerCaCert(ee_issuer_dn.clone()))?; - - let (crl_uri, crl_der) = choose_crl_for_issuer(&issuer_ca_der, crl_files)?; - validate_ee_cert_path( - ee_der, - &issuer_ca_der, - &crl_der, - None, - Some(crl_uri.as_str()), - validation_time, - )?; - - Ok(roa_to_vrps(&roa)) -} - fn process_roa_with_issuer( file: &PackFile, issuer_ca_der: &[u8], - issuer_crl_der: &[u8], issuer_ca_rsync_uri: Option<&str>, - issuer_crl_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, @@ -616,70 +396,33 @@ fn process_roa_with_issuer( 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_crl_der, issuer_ca_rsync_uri, - issuer_crl_rsync_uri, + Some(issuer_crl_rsync_uri.as_str()), validation_time, )?; - validate_ee_resources_subset( - &validated.ee, - issuer_effective_ip, - issuer_effective_as, - )?; + validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?; Ok(roa_to_vrps(&roa)) } -fn process_aspa( - file: &PackFile, - ca_certs_by_subject: &HashMap>, - issuer_resolver: &dyn IssuerCaCertificateResolver, - crl_files: &[(String, Vec)], - 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_issuer_dn = aspa.signed_object.signed_data.certificates[0] - .resource_cert - .tbs - .issuer_dn - .clone(); - - let issuer_ca_der = ca_certs_by_subject - .get(&ee_issuer_dn) - .cloned() - .or_else(|| issuer_resolver.resolve_by_subject_dn(&ee_issuer_dn)) - .ok_or_else(|| ObjectValidateError::MissingIssuerCaCert(ee_issuer_dn.clone()))?; - - let (crl_uri, crl_der) = choose_crl_for_issuer(&issuer_ca_der, crl_files)?; - validate_ee_cert_path( - ee_der, - &issuer_ca_der, - &crl_der, - None, - Some(crl_uri.as_str()), - validation_time, - )?; - - Ok(AspaAttestation { - customer_as_id: aspa.aspa.customer_as_id, - provider_as_ids: aspa.aspa.provider_as_ids.clone(), - }) -} - fn process_aspa_with_issuer( file: &PackFile, issuer_ca_der: &[u8], - issuer_crl_der: &[u8], issuer_ca_rsync_uri: Option<&str>, - issuer_crl_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, @@ -689,20 +432,24 @@ fn process_aspa_with_issuer( 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_crl_der, issuer_ca_rsync_uri, - issuer_crl_rsync_uri, + Some(issuer_crl_rsync_uri.as_str()), validation_time, )?; - validate_ee_resources_subset( - &validated.ee, - issuer_effective_ip, - issuer_effective_as, - )?; + validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?; Ok(AspaAttestation { customer_as_id: aspa.aspa.customer_as_id, @@ -710,42 +457,31 @@ fn process_aspa_with_issuer( }) } -fn choose_crl_for_issuer( - issuer_ca_der: &[u8], +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::MissingCrl); + return Err(ObjectValidateError::MissingCrlInPack); } - let issuer_tbs = ResourceCertificate::decode_der(issuer_ca_der) - .ok() - .map(|c| c.tbs); - let Some(issuer_tbs) = issuer_tbs else { - return Ok(crl_files[0].clone()); + let Some(crldp_uris) = crldp_uris else { + return Err(ObjectValidateError::MissingCrlDpUris); }; - if let Some(uris) = issuer_tbs.extensions.crl_distribution_points_uris.as_ref() { - for u in 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())); - } - } - } - - for (uri, bytes) in crl_files { - let Ok(crl) = crate::data_model::crl::RpkixCrl::decode_der(bytes) else { - continue; - }; - if crl.issuer_dn == issuer_tbs.subject_dn { + 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())); } } - - // Fall back to the first CRL when the pack is incomplete or uses a different DN string - // representation. Signature binding is still validated in `validate_ee_cert_path`. - Ok(crl_files[0].clone()) + Err(ObjectValidateError::CrlNotFound( + crldp_uris + .iter() + .map(|u| u.as_str()) + .collect::>() + .join(", "), + )) } fn validate_ee_resources_subset( @@ -1039,7 +775,10 @@ fn roa_afi_to_string(afi: RoaAfi) -> &'static str { #[cfg(test)] mod tests { use super::*; - use crate::data_model::rc::{Afi, AsIdOrRange, AsIdentifierChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, IpResourceSet}; + 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)) @@ -1056,10 +795,9 @@ mod tests { #[test] fn as_choice_subset_rejects_inherit() { let child = Some(&AsIdentifierChoice::Inherit); - let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { - min: 1, - max: 10, - }])); + let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { min: 1, max: 10 }, + ])); assert!(!as_choice_subset(child, parent)); } @@ -1069,10 +807,9 @@ mod tests { AsIdOrRange::Id(5), AsIdOrRange::Range { min: 7, max: 9 }, ])); - let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { - min: 1, - max: 10, - }])); + let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { min: 1, max: 10 }, + ])); assert!(as_choice_subset(child, parent)); } @@ -1153,82 +890,84 @@ mod tests { } #[test] - fn choose_crl_for_issuer_reports_missing_crl() { - let issuer_ca_der = fixture_bytes( - "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", - ); - let err = choose_crl_for_issuer(&issuer_ca_der, &[]).unwrap_err(); - assert!(matches!(err, ObjectValidateError::MissingCrl)); + 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_issuer_falls_back_to_first_when_issuer_ca_is_not_decodable() { - let invalid_issuer_ca_der = vec![0x01, 0x02, 0x03]; + fn choose_crl_for_certificate_reports_missing_crldp_uris() { let crl_a = ("rsync://example.test/a.crl".to_string(), vec![0x01]); - let crl_b = ("rsync://example.test/b.crl".to_string(), vec![0x02]); - let (uri, bytes) = - choose_crl_for_issuer(&invalid_issuer_ca_der, &[crl_a.clone(), crl_b]).unwrap(); - assert_eq!(uri, crl_a.0); - assert_eq!(bytes, crl_a.1); + let err = choose_crl_for_certificate(None, &[crl_a]).unwrap_err(); + assert!(matches!(err, ObjectValidateError::MissingCrlDpUris)); } #[test] - fn choose_crl_for_issuer_prefers_matching_crldp_uri() { - let issuer_ca_der = fixture_bytes( - "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", - ); - let matching_crl_der = fixture_bytes( - "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl", - ); + 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_issuer( - &issuer_ca_der, + let (uri, bytes) = choose_crl_for_certificate( + Some(ee_crldp_uris), &[ - ( - "rsync://example.test/other.crl".to_string(), - other_crl_der, - ), - ( - "rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl".to_string(), - matching_crl_der.clone(), - ), + ("rsync://example.test/other.crl".to_string(), other_crl_der), + (matching_uri.clone(), matching_crl_der.clone()), ], ) .unwrap(); - assert_eq!( - uri, - "rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl" - ); + assert_eq!(uri, matching_uri); assert_eq!(bytes, matching_crl_der); } #[test] - fn choose_crl_for_issuer_falls_back_to_first_when_no_dn_match() { - let issuer_ca_der = fixture_bytes( - "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", - ); + 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 (uri, bytes) = choose_crl_for_issuer( - &issuer_ca_der, - &[ - ("rsync://example.test/a.crl".to_string(), vec![0x01]), - ("rsync://example.test/b.crl".to_string(), vec![0x02]), - ], + let err = choose_crl_for_certificate( + ee_crldp_uris, + &[("rsync://example.test/other.crl".to_string(), vec![0x01])], ) - .unwrap(); - assert_eq!(uri, "rsync://example.test/a.crl"); - assert_eq!(bytes, 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_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; @@ -1265,9 +1004,8 @@ mod tests { #[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_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; @@ -1286,8 +1024,7 @@ mod tests { }], }; - let err = - validate_ee_resources_subset(ee, Some(&issuer_ip), None).unwrap_err(); + let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None).unwrap_err(); assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset)); } } diff --git a/src/validation/run.rs b/src/validation/run.rs index 3cf5b6d..4a4150d 100644 --- a/src/validation/run.rs +++ b/src/validation/run.rs @@ -1,3 +1,4 @@ +use crate::data_model::rc::{AsResourceSet, IpResourceSet}; use crate::fetch::rsync::RsyncFetcher; use crate::policy::Policy; use crate::storage::{RocksStore, VerifiedKey}; @@ -5,8 +6,7 @@ use crate::sync::repo::{RepoSyncResult, sync_publication_point}; use crate::sync::rrdp::Fetcher as HttpFetcher; use crate::validation::manifest::{PublicationPointResult, process_manifest_publication_point}; use crate::validation::objects::{ - IssuerCaCertificateResolver, ObjectsOutput, ObjectsProcessError, - process_verified_publication_point_pack, + ObjectsOutput, process_verified_publication_point_pack_for_issuer, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -23,9 +23,6 @@ pub enum RunError { #[error("manifest processing failed: {0}")] Manifest(#[from] crate::validation::manifest::ManifestProcessError), - - #[error("objects processing failed: {0}")] - Objects(#[from] ObjectsProcessError), } /// v1 serial offline-friendly end-to-end execution for a single publication point. @@ -43,7 +40,10 @@ pub fn run_publication_point_once( publication_point_rsync_uri: &str, http_fetcher: &dyn HttpFetcher, rsync_fetcher: &dyn RsyncFetcher, - issuer_resolver: &dyn IssuerCaCertificateResolver, + issuer_ca_der: &[u8], + issuer_ca_rsync_uri: Option<&str>, + issuer_effective_ip: Option<&IpResourceSet>, + issuer_effective_as: Option<&AsResourceSet>, validation_time: time::OffsetDateTime, ) -> Result { let repo_sync = sync_publication_point( @@ -63,12 +63,15 @@ pub fn run_publication_point_once( validation_time, )?; - let objects = process_verified_publication_point_pack( + let objects = process_verified_publication_point_pack_for_issuer( &publication_point.pack, policy, - issuer_resolver, + issuer_ca_der, + issuer_ca_rsync_uri, + issuer_effective_ip, + issuer_effective_as, validation_time, - )?; + ); Ok(RunOutput { repo_sync, diff --git a/src/validation/run_tree_from_tal.rs b/src/validation/run_tree_from_tal.rs index d8a7219..f07beb0 100644 --- a/src/validation/run_tree_from_tal.rs +++ b/src/validation/run_tree_from_tal.rs @@ -1,13 +1,16 @@ use url::Url; +use crate::audit::PublicationPointAudit; use crate::data_model::ta::TrustAnchor; use crate::sync::rrdp::Fetcher; -use crate::audit::PublicationPointAudit; use crate::validation::from_tal::{ DiscoveredRootCaInstance, FromTalError, discover_root_ca_instance_from_tal_and_ta_der, discover_root_ca_instance_from_tal_url, }; -use crate::validation::tree::{CaInstanceHandle, TreeRunConfig, TreeRunError, TreeRunOutput, run_tree_serial}; +use crate::validation::tree::{ + CaInstanceHandle, TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput, + run_tree_serial, run_tree_serial_audit, +}; use crate::validation::tree_runner::Rpkiv1PublicationPointRunner; #[derive(Clone, Debug, PartialEq, Eq)] @@ -95,26 +98,11 @@ pub fn run_tree_from_tal_url_serial_audit( validation_time, }; - let audits: std::cell::RefCell> = std::cell::RefCell::new(Vec::new()); - struct AuditingRunner<'a> { - inner: &'a Rpkiv1PublicationPointRunner<'a>, - audits: &'a std::cell::RefCell>, - } - impl<'a> crate::validation::tree::PublicationPointRunner for AuditingRunner<'a> { - fn run_publication_point( - &self, - ca: &crate::validation::tree::CaInstanceHandle, - ) -> Result { - let res = self.inner.run_publication_point(ca)?; - self.audits.borrow_mut().push(res.audit.clone()); - Ok(res) - } - } - let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance); - let auditing_runner = AuditingRunner { inner: &runner, audits: &audits }; - let tree = run_tree_serial(root, &auditing_runner, config)?; - let publication_points = audits.into_inner(); + let TreeRunAuditOutput { + tree, + publication_points, + } = run_tree_serial_audit(root, &runner, config)?; Ok(RunTreeFromTalAuditOutput { discovery, @@ -173,26 +161,11 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit( validation_time, }; - let audits: std::cell::RefCell> = std::cell::RefCell::new(Vec::new()); - struct AuditingRunner<'a> { - inner: &'a Rpkiv1PublicationPointRunner<'a>, - audits: &'a std::cell::RefCell>, - } - impl<'a> crate::validation::tree::PublicationPointRunner for AuditingRunner<'a> { - fn run_publication_point( - &self, - ca: &crate::validation::tree::CaInstanceHandle, - ) -> Result { - let res = self.inner.run_publication_point(ca)?; - self.audits.borrow_mut().push(res.audit.clone()); - Ok(res) - } - } - let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance); - let auditing_runner = AuditingRunner { inner: &runner, audits: &audits }; - let tree = run_tree_serial(root, &auditing_runner, config)?; - let publication_points = audits.into_inner(); + let TreeRunAuditOutput { + tree, + publication_points, + } = run_tree_serial_audit(root, &runner, config)?; Ok(RunTreeFromTalAuditOutput { discovery, diff --git a/src/validation/tree.rs b/src/validation/tree.rs index c2e2a1b..70af84d 100644 --- a/src/validation/tree.rs +++ b/src/validation/tree.rs @@ -1,8 +1,9 @@ +use crate::audit::DiscoveredFrom; +use crate::audit::PublicationPointAudit; +use crate::data_model::rc::{AsResourceSet, IpResourceSet}; use crate::report::{RfcRef, Warning}; use crate::storage::VerifiedPublicationPointPack; use crate::validation::manifest::PublicationPointSource; -use crate::audit::PublicationPointAudit; -use crate::data_model::rc::{AsResourceSet, IpResourceSet}; use crate::validation::objects::{AspaAttestation, ObjectsOutput, Vrp}; #[derive(Clone, Debug, PartialEq, Eq)] @@ -62,7 +63,13 @@ pub struct PublicationPointRunResult { /// RFC 9286 §6.6 restriction is enforced by the tree engine: if this /// publication point used verified cache due to failed fetch, children MUST NOT /// be enqueued/processed in this run. - pub discovered_children: Vec, + pub discovered_children: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DiscoveredChildCaInstance { + pub handle: CaInstanceHandle, + pub discovered_from: DiscoveredFrom, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -87,22 +94,54 @@ pub trait PublicationPointRunner { ) -> Result; } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TreeRunAuditOutput { + pub tree: TreeRunOutput, + pub publication_points: Vec, +} + pub fn run_tree_serial( root: CaInstanceHandle, runner: &dyn PublicationPointRunner, config: &TreeRunConfig, ) -> Result { - let mut queue: std::collections::VecDeque = std::collections::VecDeque::new(); - queue.push_back(root); + Ok(run_tree_serial_audit(root, runner, config)?.tree) +} - let mut visited_manifest_uris: std::collections::HashSet = std::collections::HashSet::new(); +pub fn run_tree_serial_audit( + root: CaInstanceHandle, + runner: &dyn PublicationPointRunner, + config: &TreeRunConfig, +) -> Result { + #[derive(Clone, Debug)] + struct QueuedCaInstance { + id: u64, + handle: CaInstanceHandle, + parent_id: Option, + discovered_from: Option, + } + + let mut next_id: u64 = 0; + let mut queue: std::collections::VecDeque = std::collections::VecDeque::new(); + queue.push_back(QueuedCaInstance { + id: next_id, + handle: root, + parent_id: None, + discovered_from: None, + }); + next_id += 1; + + let mut visited_manifest_uris: std::collections::HashSet = + std::collections::HashSet::new(); let mut instances_processed = 0usize; let mut instances_failed = 0usize; let mut warnings: Vec = Vec::new(); let mut vrps: Vec = Vec::new(); let mut aspas: Vec = Vec::new(); + let mut publication_points: Vec = Vec::new(); - while let Some(ca) = queue.pop_front() { + while let Some(node) = queue.pop_front() { + let ca = &node.handle; if !visited_manifest_uris.insert(ca.manifest_rsync_uri.clone()) { continue; } @@ -119,7 +158,7 @@ pub fn run_tree_serial( } } - let res = match runner.run_publication_point(&ca) { + let res = match runner.run_publication_point(ca) { Ok(v) => v, Err(e) => { instances_failed += 1; @@ -137,6 +176,12 @@ pub fn run_tree_serial( vrps.extend(res.objects.vrps.clone()); aspas.extend(res.objects.aspas.clone()); + let mut audit = res.audit.clone(); + audit.node_id = Some(node.id); + audit.parent_node_id = node.parent_id; + audit.discovered_from = node.discovered_from.clone(); + publication_points.push(audit); + let enqueue_children = res.source == PublicationPointSource::Fresh; if !enqueue_children && !res.discovered_children.is_empty() { warnings.push( @@ -147,17 +192,37 @@ pub fn run_tree_serial( } if enqueue_children { - for child in res.discovered_children { - queue.push_back(child.with_depth(ca.depth + 1)); + let mut children = res.discovered_children; + children.sort_by(|a, b| { + a.handle + .manifest_rsync_uri + .cmp(&b.handle.manifest_rsync_uri) + .then_with(|| { + a.discovered_from + .child_ca_certificate_rsync_uri + .cmp(&b.discovered_from.child_ca_certificate_rsync_uri) + }) + }); + for child in children { + queue.push_back(QueuedCaInstance { + id: next_id, + handle: child.handle.with_depth(ca.depth + 1), + parent_id: Some(node.id), + discovered_from: Some(child.discovered_from), + }); + next_id += 1; } } } - Ok(TreeRunOutput { - instances_processed, - instances_failed, - warnings, - vrps, - aspas, + Ok(TreeRunAuditOutput { + tree: TreeRunOutput { + instances_processed, + instances_failed, + warnings, + vrps, + aspas, + }, + publication_points, }) } diff --git a/src/validation/tree_runner.rs b/src/validation/tree_runner.rs index 874439d..7d06cd6 100644 --- a/src/validation/tree_runner.rs +++ b/src/validation/tree_runner.rs @@ -1,18 +1,20 @@ +use crate::audit::{ + AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit, + sha256_hex, sha256_hex_from_32, +}; use crate::fetch::rsync::RsyncFetcher; use crate::policy::Policy; use crate::report::{RfcRef, Warning}; use crate::storage::RocksStore; use crate::sync::repo::sync_publication_point; use crate::sync::rrdp::Fetcher; -use crate::audit::{ - AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit, - sha256_hex, sha256_hex_from_32, -}; use crate::validation::ca_instance::ca_instance_uris_from_ca_certificate; use crate::validation::ca_path::{CaPathError, validate_subordinate_ca_cert}; use crate::validation::manifest::{PublicationPointSource, process_manifest_publication_point}; use crate::validation::objects::process_verified_publication_point_pack_for_issuer; -use crate::validation::tree::{CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner}; +use crate::validation::tree::{ + CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner, +}; pub struct Rpkiv1PublicationPointRunner<'a> { pub store: &'a RocksStore, @@ -38,9 +40,11 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> { self.rsync_fetcher, ) { warnings.push( - Warning::new(format!("repo sync failed (continuing with cached/raw data): {e}")) - .with_rfc_refs(&[RfcRef("RFC 8182 §3.4.5"), RfcRef("RFC 9286 §6.6")]) - .with_context(&ca.rsync_base_uri), + Warning::new(format!( + "repo sync failed (continuing with cached/raw data): {e}" + )) + .with_rfc_refs(&[RfcRef("RFC 8182 §3.4.5"), RfcRef("RFC 9286 §6.6")]) + .with_context(&ca.rsync_base_uri), ); } @@ -97,7 +101,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> { } struct ChildDiscoveryOutput { - children: Vec, + children: Vec, audits: Vec, } @@ -107,9 +111,8 @@ fn discover_children_from_fresh_pack_with_audit( validation_time: time::OffsetDateTime, ) -> Result { let issuer_ca_der = issuer.ca_certificate_der.as_slice(); - let (issuer_crl_uri, issuer_crl_der) = select_issuer_crl_from_pack(issuer, pack)?; - let mut out: Vec = Vec::new(); + let mut out: Vec = Vec::new(); let mut audits: Vec = Vec::new(); for f in &pack.files { if !f.rsync_uri.ends_with(".cer") { @@ -117,6 +120,22 @@ fn discover_children_from_fresh_pack_with_audit( } let child_der = f.bytes.as_slice(); + let (issuer_crl_uri, issuer_crl_der) = match select_issuer_crl_from_pack(child_der, pack) { + Ok(v) => v, + Err(e) => { + audits.push(ObjectAuditEntry { + rsync_uri: f.rsync_uri.clone(), + sha256_hex: sha256_hex_from_32(&f.sha256), + kind: AuditObjectKind::Certificate, + result: AuditObjectResult::Error, + detail: Some(format!( + "cannot select issuer CRL for child certificate: {e}" + )), + }); + continue; + } + }; + let validated = match validate_subordinate_ca_cert( child_der, issuer_ca_der, @@ -144,7 +163,7 @@ fn discover_children_from_fresh_pack_with_audit( sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Error, - detail: Some(e.to_string()), + detail: Some(format!("child CA validation failed: {e}")), }); continue; } @@ -164,16 +183,23 @@ fn discover_children_from_fresh_pack_with_audit( } }; - out.push(CaInstanceHandle { - depth: 0, - ca_certificate_der: child_der.to_vec(), - ca_certificate_rsync_uri: Some(f.rsync_uri.clone()), - effective_ip_resources: validated.effective_ip_resources.clone(), - effective_as_resources: validated.effective_as_resources.clone(), - rsync_base_uri: uris.rsync_base_uri, - manifest_rsync_uri: uris.manifest_rsync_uri, - publication_point_rsync_uri: uris.publication_point_rsync_uri, - rrdp_notification_uri: uris.rrdp_notification_uri, + out.push(DiscoveredChildCaInstance { + handle: CaInstanceHandle { + depth: 0, + ca_certificate_der: child_der.to_vec(), + ca_certificate_rsync_uri: Some(f.rsync_uri.clone()), + effective_ip_resources: validated.effective_ip_resources.clone(), + effective_as_resources: validated.effective_as_resources.clone(), + rsync_base_uri: uris.rsync_base_uri, + manifest_rsync_uri: uris.manifest_rsync_uri, + publication_point_rsync_uri: uris.publication_point_rsync_uri, + rrdp_notification_uri: uris.rrdp_notification_uri, + }, + discovered_from: crate::audit::DiscoveredFrom { + parent_manifest_rsync_uri: issuer.manifest_rsync_uri.clone(), + child_ca_certificate_rsync_uri: f.rsync_uri.clone(), + child_ca_certificate_sha256_hex: sha256_hex_from_32(&f.sha256), + }, }); audits.push(ObjectAuditEntry { @@ -185,38 +211,39 @@ fn discover_children_from_fresh_pack_with_audit( }); } - Ok(ChildDiscoveryOutput { children: out, audits }) + Ok(ChildDiscoveryOutput { + children: out, + audits, + }) } fn select_issuer_crl_from_pack<'a>( - issuer: &CaInstanceHandle, + child_cert_der: &[u8], pack: &'a crate::storage::VerifiedPublicationPointPack, ) -> Result<(&'a str, &'a [u8]), String> { - let issuer_ca = crate::data_model::rc::ResourceCertificate::decode_der(&issuer.ca_certificate_der) - .map_err(|e| e.to_string())?; - let subject_dn = issuer_ca.tbs.subject_dn; + let child = crate::data_model::rc::ResourceCertificate::decode_der(child_cert_der) + .map_err(|e| format!("child certificate decode failed: {e}"))?; + let Some(crldp_uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else { + return Err( + "child certificate CRLDistributionPoints missing (RFC 6487 §4.8.6)".to_string(), + ); + }; - if let Some(uris) = issuer_ca.tbs.extensions.crl_distribution_points_uris.as_ref() { - for u in uris { - let s = u.as_str(); - if let Some(f) = pack.files.iter().find(|f| f.rsync_uri == s) { - return Ok((f.rsync_uri.as_str(), f.bytes.as_slice())); - } - } - } - - for f in &pack.files { - if !f.rsync_uri.ends_with(".crl") { - continue; - } - let Ok(crl) = crate::data_model::crl::RpkixCrl::decode_der(&f.bytes) else { - continue; - }; - if crl.issuer_dn == subject_dn { + for u in crldp_uris { + let s = u.as_str(); + if let Some(f) = pack.files.iter().find(|f| f.rsync_uri == s) { return Ok((f.rsync_uri.as_str(), f.bytes.as_slice())); } } - Err("issuer CRL not found in verified pack (RFC 9286 §7)".to_string()) + + Err(format!( + "CRL referenced by child certificate CRLDistributionPoints not found in verified pack: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)", + crldp_uris + .iter() + .map(|u| u.as_str()) + .collect::>() + .join(", ") + )) } fn kind_from_rsync_uri(uri: &str) -> AuditObjectKind { @@ -323,6 +350,9 @@ fn build_publication_point_audit( warnings.extend(objects.warnings.iter().map(AuditWarning::from)); PublicationPointAudit { + node_id: None, + parent_node_id: None, + discovered_from: None, rsync_base_uri: ca.rsync_base_uri.clone(), manifest_rsync_uri: ca.manifest_rsync_uri.clone(), publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(), @@ -455,104 +485,86 @@ authorityKeyIdentifier = keyid:always ); std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf"); - run( - Command::new("openssl") - .arg("genrsa") - .arg("-out") - .arg(dir.join("issuer.key")) - .arg("2048"), - ); - run( - Command::new("openssl") - .arg("req") - .arg("-new") - .arg("-x509") - .arg("-sha256") - .arg("-days") - .arg("365") - .arg("-key") - .arg(dir.join("issuer.key")) - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-extensions") - .arg("v3_issuer_ca") - .arg("-out") - .arg(dir.join("issuer.pem")), - ); + run(Command::new("openssl") + .arg("genrsa") + .arg("-out") + .arg(dir.join("issuer.key")) + .arg("2048")); + run(Command::new("openssl") + .arg("req") + .arg("-new") + .arg("-x509") + .arg("-sha256") + .arg("-days") + .arg("365") + .arg("-key") + .arg(dir.join("issuer.key")) + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-extensions") + .arg("v3_issuer_ca") + .arg("-out") + .arg(dir.join("issuer.pem"))); - run( - Command::new("openssl") - .arg("genrsa") - .arg("-out") - .arg(dir.join("child.key")) - .arg("2048"), - ); - run( - Command::new("openssl") - .arg("req") - .arg("-new") - .arg("-key") - .arg(dir.join("child.key")) - .arg("-subj") - .arg("/CN=Test Child CA") - .arg("-out") - .arg(dir.join("child.csr")), - ); + run(Command::new("openssl") + .arg("genrsa") + .arg("-out") + .arg(dir.join("child.key")) + .arg("2048")); + run(Command::new("openssl") + .arg("req") + .arg("-new") + .arg("-key") + .arg(dir.join("child.key")) + .arg("-subj") + .arg("/CN=Test Child CA") + .arg("-out") + .arg(dir.join("child.csr"))); - run( - Command::new("openssl") - .arg("ca") - .arg("-batch") - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-in") - .arg(dir.join("child.csr")) - .arg("-extensions") - .arg("v3_child_ca") - .arg("-out") - .arg(dir.join("child.pem")), - ); + run(Command::new("openssl") + .arg("ca") + .arg("-batch") + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-in") + .arg(dir.join("child.csr")) + .arg("-extensions") + .arg("v3_child_ca") + .arg("-out") + .arg(dir.join("child.pem"))); - run( - Command::new("openssl") - .arg("x509") - .arg("-in") - .arg(dir.join("issuer.pem")) - .arg("-outform") - .arg("DER") - .arg("-out") - .arg(dir.join("issuer.cer")), - ); - run( - Command::new("openssl") - .arg("x509") - .arg("-in") - .arg(dir.join("child.pem")) - .arg("-outform") - .arg("DER") - .arg("-out") - .arg(dir.join("child.cer")), - ); + run(Command::new("openssl") + .arg("x509") + .arg("-in") + .arg(dir.join("issuer.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("issuer.cer"))); + run(Command::new("openssl") + .arg("x509") + .arg("-in") + .arg(dir.join("child.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("child.cer"))); - run( - Command::new("openssl") - .arg("ca") - .arg("-gencrl") - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-out") - .arg(dir.join("issuer.crl.pem")), - ); - run( - Command::new("openssl") - .arg("crl") - .arg("-in") - .arg(dir.join("issuer.crl.pem")) - .arg("-outform") - .arg("DER") - .arg("-out") - .arg(dir.join("issuer.crl")), - ); + run(Command::new("openssl") + .arg("ca") + .arg("-gencrl") + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-out") + .arg(dir.join("issuer.crl.pem"))); + run(Command::new("openssl") + .arg("crl") + .arg("-in") + .arg(dir.join("issuer.crl.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("issuer.crl"))); Generated { issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"), @@ -577,29 +589,29 @@ authorityKeyIdentifier = keyid:always #[test] fn select_issuer_crl_from_pack_finds_matching_crl() { - let g = generate_chain_and_crl(); + // Use real fixtures to ensure child cert has CRLDP rsync URI and CRL exists. + let child_cert_der = + std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", + )) + .expect("read child cert fixture"); + let crl_der = std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl", + )) + .expect("read crl fixture"); let pack = dummy_pack_with_files(vec![PackFile::from_bytes_compute_sha256( - "rsync://example.test/repo/issuer/issuer.crl", - g.issuer_crl_der.clone(), + "rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl", + crl_der.clone(), )]); - let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); - let issuer = CaInstanceHandle { - depth: 0, - ca_certificate_der: g.issuer_ca_der.clone(), - ca_certificate_rsync_uri: None, - effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), - effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), - rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), - manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), - publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), - rrdp_notification_uri: None, - }; - - let (uri, found) = select_issuer_crl_from_pack(&issuer, &pack).expect("find crl"); - assert_eq!(uri, "rsync://example.test/repo/issuer/issuer.crl"); - assert_eq!(found, g.issuer_crl_der.as_slice()); + let (uri, found) = + select_issuer_crl_from_pack(child_cert_der.as_slice(), &pack).expect("find crl"); + assert_eq!( + uri, + "rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl" + ); + assert_eq!(found, crl_der.as_slice()); } #[test] @@ -635,26 +647,84 @@ authorityKeyIdentifier = keyid:always .expect("discover children") .children; assert_eq!(children.len(), 1); - assert_eq!(children[0].rsync_base_uri, "rsync://example.test/repo/child/".to_string()); assert_eq!( - children[0].manifest_rsync_uri, - "rsync://example.test/repo/child/child.mft".to_string() + children[0].discovered_from.parent_manifest_rsync_uri, + issuer.manifest_rsync_uri ); assert_eq!( - children[0].publication_point_rsync_uri, + children[0].discovered_from.child_ca_certificate_rsync_uri, + "rsync://example.test/repo/issuer/child.cer" + ); + assert_eq!( + children[0].handle.rsync_base_uri, "rsync://example.test/repo/child/".to_string() ); assert_eq!( - children[0].rrdp_notification_uri.as_deref(), + children[0].handle.manifest_rsync_uri, + "rsync://example.test/repo/child/child.mft".to_string() + ); + assert_eq!( + children[0].handle.publication_point_rsync_uri, + "rsync://example.test/repo/child/".to_string() + ); + assert_eq!( + children[0].handle.rrdp_notification_uri.as_deref(), Some("https://example.test/notification.xml") ); } #[test] - fn runner_offline_rsync_fixture_produces_pack_and_warnings() { - let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( - "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0", + fn discover_children_with_audit_records_missing_crl_for_child_certificate() { + let now = time::OffsetDateTime::now_utc(); + + let child_ca_der = + std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", + )) + .expect("read child ca fixture"); + + // Pack contains the child CA cert but does not contain the CRL referenced by the child + // certificate CRLDistributionPoints extension. + let pack = dummy_pack_with_files(vec![PackFile::from_bytes_compute_sha256( + "rsync://ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", + child_ca_der, + )]); + + let issuer = CaInstanceHandle { + depth: 0, + ca_certificate_der: vec![1], + ca_certificate_rsync_uri: None, + effective_ip_resources: None, + effective_as_resources: None, + rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), + manifest_rsync_uri: pack.manifest_rsync_uri.clone(), + publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), + rrdp_notification_uri: None, + }; + + let out = discover_children_from_fresh_pack_with_audit(&issuer, &pack, now) + .expect("discovery should succeed with audit error"); + assert_eq!(out.children.len(), 0); + assert_eq!(out.audits.len(), 1); + assert_eq!( + out.audits[0].rsync_uri, + "rsync://ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer" ); + assert_eq!(out.audits[0].result, AuditObjectResult::Error); + assert!( + out.audits[0] + .detail + .as_deref() + .unwrap_or("") + .contains("cannot select issuer CRL"), + "expected deterministic CRL selection failure to be recorded" + ); + } + + #[test] + fn runner_offline_rsync_fixture_produces_pack_and_warnings() { + let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); assert!(fixture_dir.is_dir(), "fixture directory must exist"); let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string(); diff --git a/tests/test_apnic_stats_live_stage2.rs b/tests/test_apnic_stats_live_stage2.rs index db0156d..0f19bee 100644 --- a/tests/test_apnic_stats_live_stage2.rs +++ b/tests/test_apnic_stats_live_stage2.rs @@ -1,5 +1,6 @@ use std::cell::RefCell; use std::collections::{BTreeMap, HashSet}; +use std::time::Duration; use rpki::data_model::crl::RpkixCrl; use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig}; @@ -59,7 +60,9 @@ impl LiveStats { fn record(&mut self, res: &PublicationPointRunResult) { self.publication_points_processed += 1; match res.source { - rpki::validation::manifest::PublicationPointSource::Fresh => self.publication_points_fresh += 1, + rpki::validation::manifest::PublicationPointSource::Fresh => { + self.publication_points_fresh += 1 + } rpki::validation::manifest::PublicationPointSource::VerifiedCache => { self.publication_points_cached += 1 } @@ -139,8 +142,15 @@ impl<'a> PublicationPointRunner for CountingRunner<'a> { #[test] #[ignore = "live network + rsync full-tree stats (APNIC TAL); may take minutes"] fn apnic_tree_full_stats_serial() { - let http = BlockingHttpFetcher::new(HttpFetcherConfig::default()).expect("http fetcher"); - let rsync = SystemRsyncFetcher::new(SystemRsyncConfig::default()); + let http = BlockingHttpFetcher::new(HttpFetcherConfig { + timeout: Duration::from_secs(30 * 60), + ..HttpFetcherConfig::default() + }) + .expect("http fetcher"); + let rsync = SystemRsyncFetcher::new(SystemRsyncConfig { + timeout: Duration::from_secs(30 * 60), + ..SystemRsyncConfig::default() + }); let validation_time = time::OffsetDateTime::now_utc(); let temp = tempfile::tempdir().expect("tempdir"); @@ -202,7 +212,12 @@ fn apnic_tree_full_stats_serial() { println!("APNIC Stage2 full-tree serial stats"); println!("tal_url={APNIC_TAL_URL}"); - println!("validation_time={}", validation_time.format(&time::format_description::well_known::Rfc3339).unwrap()); + println!( + "validation_time={}", + validation_time + .format(&time::format_description::well_known::Rfc3339) + .unwrap() + ); println!(); println!( "publication_points_processed={} publication_points_failed={} fresh={} cached={}", @@ -212,7 +227,10 @@ fn apnic_tree_full_stats_serial() { stats.publication_points_cached ); println!("rrdp_repos_unique={}", stats.rrdp_repos_unique.len()); - println!("objects_dropped_publication_points={}", stats.objects_dropped_publication_points); + println!( + "objects_dropped_publication_points={}", + stats.objects_dropped_publication_points + ); println!(); println!( "pack_uris_total={} pack_uris_unique={}", @@ -242,12 +260,24 @@ fn apnic_tree_full_stats_serial() { out.aspas.len() ); println!(); - println!("rocksdb_raw_objects_total={} raw_by_ext={:?}", raw_total, raw_by_ext); + println!( + "rocksdb_raw_objects_total={} raw_by_ext={:?}", + raw_total, raw_by_ext + ); println!("rocksdb_verified_packs_total={}", verified_total); // Loose sanity assertions (avoid flakiness due to repository churn). + // + // This test supports bounded manual runs via env vars: + // - `RPKI_APNIC_MAX_DEPTH=0` or `RPKI_APNIC_MAX_INSTANCES=1` implies root-only. + // Otherwise we expect to reach at least one child. + let min_expected = if max_depth == Some(0) || max_instances.is_some_and(|n| n <= 1) { + 1 + } else { + 2 + }; assert!( - out.instances_processed >= 2, - "expected to process root + at least one child" + out.instances_processed >= min_expected, + "expected to process at least {min_expected} publication point(s)" ); } diff --git a/tests/test_apnic_tree_live_m15.rs b/tests/test_apnic_tree_live_m15.rs index cbde251..36fa14e 100644 --- a/tests/test_apnic_tree_live_m15.rs +++ b/tests/test_apnic_tree_live_m15.rs @@ -1,8 +1,11 @@ +use std::time::Duration; + use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig}; use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher}; use rpki::policy::Policy; use rpki::storage::RocksStore; use rpki::validation::run_tree_from_tal::run_tree_from_tal_url_serial; +use rpki::validation::run_tree_from_tal::run_tree_from_tal_url_serial_audit; use rpki::validation::tree::TreeRunConfig; const APNIC_TAL_URL: &str = "https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal"; @@ -10,8 +13,15 @@ const APNIC_TAL_URL: &str = "https://tal.apnic.net/tal-archive/apnic-rfc7730-htt #[test] #[ignore = "live network + rsync smoke test (APNIC TAL)"] fn apnic_tree_depth1_processes_more_than_root() { - let http = BlockingHttpFetcher::new(HttpFetcherConfig::default()).expect("http fetcher"); - let rsync = SystemRsyncFetcher::new(SystemRsyncConfig::default()); + let http = BlockingHttpFetcher::new(HttpFetcherConfig { + timeout: Duration::from_secs(30 * 60), + ..HttpFetcherConfig::default() + }) + .expect("http fetcher"); + let rsync = SystemRsyncFetcher::new(SystemRsyncConfig { + timeout: Duration::from_secs(30 * 60), + ..SystemRsyncConfig::default() + }); let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); @@ -35,3 +45,46 @@ fn apnic_tree_depth1_processes_more_than_root() { "expected to process root + at least one child" ); } + +#[test] +#[ignore = "live network + rsync root-only smoke test (APNIC TAL); 30min timeouts"] +fn apnic_tree_root_only_processes_root_with_long_timeouts() { + let http = BlockingHttpFetcher::new(HttpFetcherConfig { + timeout: Duration::from_secs(30 * 60), + ..HttpFetcherConfig::default() + }) + .expect("http fetcher"); + let rsync = SystemRsyncFetcher::new(SystemRsyncConfig { + timeout: Duration::from_secs(30 * 60), + ..SystemRsyncConfig::default() + }); + + let temp = tempfile::tempdir().expect("tempdir"); + let store = RocksStore::open(temp.path()).expect("open rocksdb"); + let policy = Policy::default(); + let validation_time = time::OffsetDateTime::now_utc(); + + let out = run_tree_from_tal_url_serial_audit( + &store, + &policy, + APNIC_TAL_URL, + &http, + &rsync, + validation_time, + &TreeRunConfig { + max_depth: Some(0), + max_instances: Some(1), + }, + ) + .expect("run APNIC root-only"); + + assert_eq!( + out.tree.instances_processed, 1, + "expected to process exactly the root publication point" + ); + assert_eq!( + out.publication_points.len(), + out.tree.instances_processed, + "audit should include one publication point" + ); +} diff --git a/tests/test_ca_instance_discovery.rs b/tests/test_ca_instance_discovery.rs index a663939..4c1b6c6 100644 --- a/tests/test_ca_instance_discovery.rs +++ b/tests/test_ca_instance_discovery.rs @@ -9,8 +9,7 @@ fn load_tal_and_ta_fixture(tal_name: &str, ta_name: &str) -> TrustAnchor { std::fs::read(format!("tests/fixtures/tal/{tal_name}")).expect("read TAL fixture"); let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL"); - let ta_der = - std::fs::read(format!("tests/fixtures/ta/{ta_name}")).expect("read TA fixture"); + let ta_der = std::fs::read(format!("tests/fixtures/ta/{ta_name}")).expect("read TA fixture"); let resolved = tal.ta_uris[0].clone(); TrustAnchor::bind_der(tal, &ta_der, Some(&resolved)).expect("bind TAL and TA") diff --git a/tests/test_ca_path_m15.rs b/tests/test_ca_path_m15.rs index 21b3e1c..30bba0a 100644 --- a/tests/test_ca_path_m15.rs +++ b/tests/test_ca_path_m15.rs @@ -106,122 +106,102 @@ authorityKeyIdentifier = keyid:always write(&dir.join("openssl.cnf"), &cnf); // Issuer CA key + self-signed CA cert (DER later). - run( - Command::new("openssl") - .arg("genrsa") - .arg("-out") - .arg(dir.join("issuer.key")) - .arg("2048"), - ); - run( - Command::new("openssl") - .arg("req") - .arg("-new") - .arg("-x509") - .arg("-sha256") - .arg("-days") - .arg("365") - .arg("-key") - .arg(dir.join("issuer.key")) - .arg("-out") - .arg(dir.join("issuer.pem")) - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-extensions") - .arg("v3_issuer_ca"), - ); + run(Command::new("openssl") + .arg("genrsa") + .arg("-out") + .arg(dir.join("issuer.key")) + .arg("2048")); + run(Command::new("openssl") + .arg("req") + .arg("-new") + .arg("-x509") + .arg("-sha256") + .arg("-days") + .arg("365") + .arg("-key") + .arg(dir.join("issuer.key")) + .arg("-out") + .arg(dir.join("issuer.pem")) + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-extensions") + .arg("v3_issuer_ca")); // Child CA key + CSR. - run( - Command::new("openssl") - .arg("genrsa") - .arg("-out") - .arg(dir.join("child.key")) - .arg("2048"), - ); - run( - Command::new("openssl") - .arg("req") - .arg("-new") - .arg("-key") - .arg(dir.join("child.key")) - .arg("-subj") - .arg("/CN=Child CA") - .arg("-out") - .arg(dir.join("child.csr")) - .arg("-config") - .arg(dir.join("openssl.cnf")), - ); + run(Command::new("openssl") + .arg("genrsa") + .arg("-out") + .arg(dir.join("child.key")) + .arg("2048")); + run(Command::new("openssl") + .arg("req") + .arg("-new") + .arg("-key") + .arg(dir.join("child.key")) + .arg("-subj") + .arg("/CN=Child CA") + .arg("-out") + .arg(dir.join("child.csr")) + .arg("-config") + .arg(dir.join("openssl.cnf"))); // Issue child CA cert using openssl ca (so it appears in the CA database for CRL). - run( - Command::new("openssl") - .arg("ca") - .arg("-batch") - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-extensions") - .arg("v3_child_ca") - .arg("-in") - .arg(dir.join("child.csr")) - .arg("-out") - .arg(dir.join("child.pem")) - .arg("-notext"), - ); + run(Command::new("openssl") + .arg("ca") + .arg("-batch") + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-extensions") + .arg("v3_child_ca") + .arg("-in") + .arg(dir.join("child.csr")) + .arg("-out") + .arg(dir.join("child.pem")) + .arg("-notext")); if revoke_child { - run( - Command::new("openssl") - .arg("ca") - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-revoke") - .arg(dir.join("child.pem")), - ); + run(Command::new("openssl") + .arg("ca") + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-revoke") + .arg(dir.join("child.pem"))); } // Generate CRL. - run( - Command::new("openssl") - .arg("ca") - .arg("-gencrl") - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-out") - .arg(dir.join("issuer.crl.pem")), - ); + run(Command::new("openssl") + .arg("ca") + .arg("-gencrl") + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-out") + .arg(dir.join("issuer.crl.pem"))); // Convert to DER. - run( - Command::new("openssl") - .arg("x509") - .arg("-in") - .arg(dir.join("issuer.pem")) - .arg("-outform") - .arg("DER") - .arg("-out") - .arg(dir.join("issuer.cer")), - ); - run( - Command::new("openssl") - .arg("x509") - .arg("-in") - .arg(dir.join("child.pem")) - .arg("-outform") - .arg("DER") - .arg("-out") - .arg(dir.join("child.cer")), - ); - run( - Command::new("openssl") - .arg("crl") - .arg("-in") - .arg(dir.join("issuer.crl.pem")) - .arg("-outform") - .arg("DER") - .arg("-out") - .arg(dir.join("issuer.crl")), - ); + run(Command::new("openssl") + .arg("x509") + .arg("-in") + .arg(dir.join("issuer.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("issuer.cer"))); + run(Command::new("openssl") + .arg("x509") + .arg("-in") + .arg(dir.join("child.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("child.cer"))); + run(Command::new("openssl") + .arg("crl") + .arg("-in") + .arg(dir.join("issuer.crl.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("issuer.crl"))); Generated { issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"), diff --git a/tests/test_cert_path_key_usage.rs b/tests/test_cert_path_key_usage.rs index 3471f9d..69deee0 100644 --- a/tests/test_cert_path_key_usage.rs +++ b/tests/test_cert_path_key_usage.rs @@ -93,104 +93,86 @@ authorityKeyIdentifier = keyid:always ); std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf"); - run( - Command::new("openssl") - .arg("genrsa") - .arg("-out") - .arg(dir.join("issuer.key")) - .arg("2048"), - ); - run( - Command::new("openssl") - .arg("req") - .arg("-new") - .arg("-x509") - .arg("-sha256") - .arg("-days") - .arg("365") - .arg("-key") - .arg(dir.join("issuer.key")) - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-extensions") - .arg("v3_issuer_ca") - .arg("-out") - .arg(dir.join("issuer.pem")), - ); + run(Command::new("openssl") + .arg("genrsa") + .arg("-out") + .arg(dir.join("issuer.key")) + .arg("2048")); + run(Command::new("openssl") + .arg("req") + .arg("-new") + .arg("-x509") + .arg("-sha256") + .arg("-days") + .arg("365") + .arg("-key") + .arg(dir.join("issuer.key")) + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-extensions") + .arg("v3_issuer_ca") + .arg("-out") + .arg(dir.join("issuer.pem"))); - run( - Command::new("openssl") - .arg("genrsa") - .arg("-out") - .arg(dir.join("ee.key")) - .arg("2048"), - ); - run( - Command::new("openssl") - .arg("req") - .arg("-new") - .arg("-key") - .arg(dir.join("ee.key")) - .arg("-subj") - .arg("/CN=Test EE") - .arg("-out") - .arg(dir.join("ee.csr")), - ); + run(Command::new("openssl") + .arg("genrsa") + .arg("-out") + .arg(dir.join("ee.key")) + .arg("2048")); + run(Command::new("openssl") + .arg("req") + .arg("-new") + .arg("-key") + .arg(dir.join("ee.key")) + .arg("-subj") + .arg("/CN=Test EE") + .arg("-out") + .arg(dir.join("ee.csr"))); - run( - Command::new("openssl") - .arg("ca") - .arg("-batch") - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-in") - .arg(dir.join("ee.csr")) - .arg("-extensions") - .arg("v3_ee") - .arg("-out") - .arg(dir.join("ee.pem")), - ); + run(Command::new("openssl") + .arg("ca") + .arg("-batch") + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-in") + .arg(dir.join("ee.csr")) + .arg("-extensions") + .arg("v3_ee") + .arg("-out") + .arg(dir.join("ee.pem"))); - run( - Command::new("openssl") - .arg("x509") - .arg("-in") - .arg(dir.join("issuer.pem")) - .arg("-outform") - .arg("DER") - .arg("-out") - .arg(dir.join("issuer.cer")), - ); - run( - Command::new("openssl") - .arg("x509") - .arg("-in") - .arg(dir.join("ee.pem")) - .arg("-outform") - .arg("DER") - .arg("-out") - .arg(dir.join("ee.cer")), - ); + run(Command::new("openssl") + .arg("x509") + .arg("-in") + .arg(dir.join("issuer.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("issuer.cer"))); + run(Command::new("openssl") + .arg("x509") + .arg("-in") + .arg(dir.join("ee.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("ee.cer"))); - run( - Command::new("openssl") - .arg("ca") - .arg("-gencrl") - .arg("-config") - .arg(dir.join("openssl.cnf")) - .arg("-out") - .arg(dir.join("issuer.crl.pem")), - ); - run( - Command::new("openssl") - .arg("crl") - .arg("-in") - .arg(dir.join("issuer.crl.pem")) - .arg("-outform") - .arg("DER") - .arg("-out") - .arg(dir.join("issuer.crl")), - ); + run(Command::new("openssl") + .arg("ca") + .arg("-gencrl") + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-out") + .arg(dir.join("issuer.crl.pem"))); + run(Command::new("openssl") + .arg("crl") + .arg("-in") + .arg(dir.join("issuer.crl.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("issuer.crl"))); Generated { issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"), @@ -203,16 +185,30 @@ authorityKeyIdentifier = keyid:always fn ee_key_usage_digital_signature_only_is_accepted() { let g = generate_issuer_ca_ee_and_crl("keyUsage = critical, digitalSignature\n"); let now = time::OffsetDateTime::now_utc(); - validate_ee_cert_path(&g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now) - .expect("valid EE path"); + validate_ee_cert_path( + &g.ee_der, + &g.issuer_ca_der, + &g.issuer_crl_der, + None, + None, + now, + ) + .expect("valid EE path"); } #[test] fn ee_key_usage_missing_is_rejected() { let g = generate_issuer_ca_ee_and_crl(""); let now = time::OffsetDateTime::now_utc(); - let err = validate_ee_cert_path(&g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now) - .unwrap_err(); + let err = validate_ee_cert_path( + &g.ee_der, + &g.issuer_ca_der, + &g.issuer_crl_der, + None, + None, + now, + ) + .unwrap_err(); assert!(matches!(err, CertPathError::KeyUsageMissing), "{err}"); } @@ -220,16 +216,31 @@ fn ee_key_usage_missing_is_rejected() { fn ee_key_usage_not_critical_is_rejected() { let g = generate_issuer_ca_ee_and_crl("keyUsage = digitalSignature\n"); let now = time::OffsetDateTime::now_utc(); - let err = validate_ee_cert_path(&g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now) - .unwrap_err(); + let err = validate_ee_cert_path( + &g.ee_der, + &g.issuer_ca_der, + &g.issuer_crl_der, + None, + None, + now, + ) + .unwrap_err(); assert!(matches!(err, CertPathError::KeyUsageNotCritical), "{err}"); } #[test] fn ee_key_usage_wrong_bits_is_rejected() { - let g = generate_issuer_ca_ee_and_crl("keyUsage = critical, digitalSignature, keyEncipherment\n"); + let g = + generate_issuer_ca_ee_and_crl("keyUsage = critical, digitalSignature, keyEncipherment\n"); let now = time::OffsetDateTime::now_utc(); - let err = validate_ee_cert_path(&g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now) - .unwrap_err(); + let err = validate_ee_cert_path( + &g.ee_der, + &g.issuer_ca_der, + &g.issuer_crl_der, + None, + None, + now, + ) + .unwrap_err(); assert!(matches!(err, CertPathError::KeyUsageInvalidBits), "{err}"); } diff --git a/tests/test_cli_run_offline_m18.rs b/tests/test_cli_run_offline_m18.rs index 1db9911..c60a333 100644 --- a/tests/test_cli_run_offline_m18.rs +++ b/tests/test_cli_run_offline_m18.rs @@ -10,8 +10,8 @@ fn cli_run_offline_mode_executes_and_writes_json() { let tal_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/tal/apnic-rfc7730-https.tal"); - let ta_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("tests/fixtures/ta/apnic-ta.cer"); + let ta_path = + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"); let argv = vec![ "rpki".to_string(), @@ -39,4 +39,3 @@ fn cli_run_offline_mode_executes_and_writes_json() { let v: serde_json::Value = serde_json::from_slice(&bytes).expect("parse report json"); assert_eq!(v["format_version"], 1); } - diff --git a/tests/test_cli_smoke_m18.rs b/tests/test_cli_smoke_m18.rs index 783219b..c1f6839 100644 --- a/tests/test_cli_smoke_m18.rs +++ b/tests/test_cli_smoke_m18.rs @@ -11,8 +11,8 @@ fn cli_offline_smoke_writes_report_json() { let tal_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/tal/apnic-rfc7730-https.tal"); - let ta_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("tests/fixtures/ta/apnic-ta.cer"); + let ta_path = + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"); let out = Command::new(bin) .args([ @@ -51,4 +51,3 @@ fn cli_offline_smoke_writes_report_json() { assert!(v.get("vrps").is_some()); assert!(v.get("aspas").is_some()); } - diff --git a/tests/test_deterministic_semantics_m4.rs b/tests/test_deterministic_semantics_m4.rs new file mode 100644 index 0000000..e9c342a --- /dev/null +++ b/tests/test_deterministic_semantics_m4.rs @@ -0,0 +1,136 @@ +use rpki::audit::PublicationPointAudit; +use rpki::policy::{Policy, SignedObjectFailurePolicy}; +use rpki::storage::{PackFile, PackTime, VerifiedPublicationPointPack}; +use rpki::validation::manifest::PublicationPointSource; +use rpki::validation::objects::process_verified_publication_point_pack_for_issuer; +use rpki::validation::tree::{ + CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig, + run_tree_serial_audit, +}; + +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}")) +} + +fn dummy_pack(files: Vec) -> VerifiedPublicationPointPack { + let now = time::OffsetDateTime::now_utc(); + let manifest_rsync_uri = + "rsync://rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; + VerifiedPublicationPointPack { + format_version: VerifiedPublicationPointPack::FORMAT_VERSION_V1, + manifest_rsync_uri: manifest_rsync_uri.to_string(), + publication_point_rsync_uri: "rsync://rpki.cernet.net/repo/cernet/0/".to_string(), + this_update: PackTime::from_utc_offset_datetime(now), + next_update: PackTime::from_utc_offset_datetime(now + time::Duration::hours(1)), + verified_at: PackTime::from_utc_offset_datetime(now), + manifest_bytes: fixture_bytes( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ), + files, + } +} + +struct SinglePackRunner { + policy: Policy, + pack: VerifiedPublicationPointPack, +} + +impl PublicationPointRunner for SinglePackRunner { + fn run_publication_point( + &self, + ca: &CaInstanceHandle, + ) -> Result { + let objects = process_verified_publication_point_pack_for_issuer( + &self.pack, + &self.policy, + &ca.ca_certificate_der, + ca.ca_certificate_rsync_uri.as_deref(), + ca.effective_ip_resources.as_ref(), + ca.effective_as_resources.as_ref(), + time::OffsetDateTime::now_utc(), + ); + + Ok(PublicationPointRunResult { + source: PublicationPointSource::Fresh, + pack: self.pack.clone(), + warnings: Vec::new(), + objects, + audit: PublicationPointAudit::default(), + discovered_children: Vec::new(), + }) + } +} + +#[test] +fn crl_mismatch_drops_publication_point_and_cites_rfc_sections() { + let roa_bytes = + fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); + + // Include at least one CRL file but with a URI that does NOT match the EE certificate's CRLDP. + let pack = dummy_pack(vec![ + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/not-it.crl", vec![0x01]), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/a.roa", roa_bytes), + ]); + + let mut policy = Policy::default(); + policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropPublicationPoint; + + let runner = SinglePackRunner { policy, pack }; + + let root = CaInstanceHandle { + depth: 0, + ca_certificate_der: vec![0x01, 0x02, 0x03], + ca_certificate_rsync_uri: None, + effective_ip_resources: None, + effective_as_resources: None, + rsync_base_uri: "rsync://example.test/repo/".to_string(), + manifest_rsync_uri: + "rsync://rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft" + .to_string(), + publication_point_rsync_uri: "rsync://example.test/repo/".to_string(), + rrdp_notification_uri: None, + }; + + let out = run_tree_serial_audit( + root, + &runner, + &TreeRunConfig { + max_depth: Some(0), + max_instances: Some(1), + }, + ) + .expect("run tree audit"); + + assert_eq!(out.tree.instances_processed, 1); + assert_eq!(out.tree.instances_failed, 0); + + assert!( + out.tree.warnings.iter().any(|w| w + .message + .contains("dropping publication point due to invalid ROA")), + "expected publication point drop warning" + ); + let w = out + .tree + .warnings + .iter() + .find(|w| { + w.message + .contains("dropping publication point due to invalid ROA") + }) + .expect("warning present"); + let refs = w.rfc_refs.iter().map(|r| r.0).collect::>(); + assert!( + refs.contains(&"RFC 6487 §4.8.6"), + "expected CRLDP RFC reference in warning: {refs:?}" + ); + assert!( + refs.contains(&"RFC 9286 §4.2.1"), + "expected manifest locked-pack RFC reference in warning: {refs:?}" + ); + + assert_eq!(out.publication_points.len(), 1); + assert_eq!(out.publication_points[0].node_id, Some(0)); + assert_eq!(out.publication_points[0].parent_node_id, None); +} diff --git a/tests/test_fetch_rsync_localdir.rs b/tests/test_fetch_rsync_localdir.rs index c93bc16..37e9854 100644 --- a/tests/test_fetch_rsync_localdir.rs +++ b/tests/test_fetch_rsync_localdir.rs @@ -22,4 +22,3 @@ fn local_dir_rsync_fetcher_normalizes_base_and_skips_non_files() { assert!(uris.contains(&"rsync://example.test/repo/a.txt")); assert!(!uris.iter().any(|u| u.ends_with("/sock"))); } - diff --git a/tests/test_from_tal_offline.rs b/tests/test_from_tal_offline.rs index 2bc8068..439de99 100644 --- a/tests/test_from_tal_offline.rs +++ b/tests/test_from_tal_offline.rs @@ -6,10 +6,10 @@ use rpki::policy::{Policy, SyncPreference}; use rpki::storage::RocksStore; use rpki::sync::rrdp::Fetcher; use rpki::validation::from_tal::{ - FromTalError, discover_root_ca_instance_from_tal, discover_root_ca_instance_from_tal_and_ta_der, - discover_root_ca_instance_from_tal_url, run_root_from_tal_url_once, + FromTalError, discover_root_ca_instance_from_tal, + discover_root_ca_instance_from_tal_and_ta_der, discover_root_ca_instance_from_tal_url, + run_root_from_tal_url_once, }; -use rpki::validation::objects::IssuerCaCertificateResolver; use url::Url; struct MapFetcher { @@ -34,19 +34,14 @@ impl Fetcher for MapFetcher { struct EmptyRsync; impl RsyncFetcher for EmptyRsync { - fn fetch_objects(&self, _rsync_base_uri: &str) -> Result)>, RsyncFetchError> { + fn fetch_objects( + &self, + _rsync_base_uri: &str, + ) -> Result)>, RsyncFetchError> { Ok(Vec::new()) } } -struct NullResolver; - -impl IssuerCaCertificateResolver for NullResolver { - fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option> { - None - } -} - fn apnic_tal_bytes() -> Vec { std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic TAL fixture") } @@ -94,7 +89,8 @@ fn discover_root_tries_multiple_ta_uris_until_one_succeeds() { let tal_bytes = apnic_tal_bytes(); let mut tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL"); let good_uri = tal.ta_uris[0].clone(); - tal.ta_uris.insert(0, Url::parse("https://example.invalid/bad.cer").unwrap()); + tal.ta_uris + .insert(0, Url::parse("https://example.invalid/bad.cer").unwrap()); let mut map = HashMap::new(); map.insert(good_uri.as_str().to_string(), apnic_ta_der()); @@ -171,11 +167,9 @@ fn run_root_from_tal_url_once_propagates_run_error_when_repo_is_empty() { "https://example.test/apnic.tal", &fetcher, &EmptyRsync, - &NullResolver, time::OffsetDateTime::now_utc(), ) .unwrap_err(); assert!(matches!(err, FromTalError::Run(_))); } - diff --git a/tests/test_objects_errors_more.rs b/tests/test_objects_errors_more.rs index c9c3477..0fff655 100644 --- a/tests/test_objects_errors_more.rs +++ b/tests/test_objects_errors_more.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::path::Path; use rpki::data_model::crl::RpkixCrl; @@ -7,9 +6,7 @@ use rpki::data_model::rc::ResourceCertificate; use rpki::policy::{Policy, SignedObjectFailurePolicy}; use rpki::storage::{PackFile, RocksStore}; use rpki::validation::manifest::process_manifest_publication_point; -use rpki::validation::objects::{ - IssuerCaCertificateResolver, process_verified_publication_point_pack, -}; +use rpki::validation::objects::process_verified_publication_point_pack_for_issuer; fn fixture_to_rsync_uri(path: &Path) -> String { let rel = path @@ -33,28 +30,11 @@ fn fixture_dir_to_rsync_uri(dir: &Path) -> String { s } -struct EmptyResolver; - -impl IssuerCaCertificateResolver for EmptyResolver { - fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option> { - None - } -} - -struct MapResolver { - by_subject_dn: HashMap>, -} - -impl IssuerCaCertificateResolver for MapResolver { - fn resolve_by_subject_dn(&self, subject_dn: &str) -> Option> { - self.by_subject_dn.get(subject_dn).cloned() - } -} - fn build_cernet_pack_and_validation_time() -> ( rpki::storage::VerifiedPublicationPointPack, time::OffsetDateTime, - MapResolver, + Vec, + ResourceCertificate, ) { let manifest_path = Path::new( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", @@ -112,44 +92,60 @@ fn build_cernet_pack_and_validation_time() -> ( } t += time::Duration::seconds(1); - let resolver = MapResolver { - by_subject_dn: HashMap::from([(issuer_ca.tbs.subject_dn, issuer_ca_der)]), - }; - - (out.pack, t, resolver) + (out.pack, t, issuer_ca_der, issuer_ca) } #[test] fn missing_crl_causes_roas_to_be_dropped_under_drop_object_policy() { - let (mut pack, validation_time, resolver) = build_cernet_pack_and_validation_time(); + let (mut pack, validation_time, issuer_ca_der, issuer_ca) = + build_cernet_pack_and_validation_time(); pack.files.retain(|f| !f.rsync_uri.ends_with(".crl")); let mut policy = Policy::default(); policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject; - let out = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time) - .expect("drop_object should not fail the publication point"); + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &issuer_ca_der, + None, + issuer_ca.tbs.extensions.ip_resources.as_ref(), + issuer_ca.tbs.extensions.as_resources.as_ref(), + validation_time, + ); assert!(out.vrps.is_empty()); assert!(!out.warnings.is_empty()); + assert!(out.stats.publication_point_dropped); } #[test] -fn missing_issuer_ca_cert_causes_roas_to_be_dropped_under_drop_object_policy() { - let (pack, validation_time, _resolver) = build_cernet_pack_and_validation_time(); +fn wrong_issuer_ca_cert_causes_roas_to_be_dropped_under_drop_object_policy() { + let (pack, validation_time, _issuer_ca_der, _issuer_ca) = + build_cernet_pack_and_validation_time(); let mut policy = Policy::default(); policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject; - let out = - process_verified_publication_point_pack(&pack, &policy, &EmptyResolver, validation_time) - .expect("drop_object should not fail the publication point"); + // Use an unrelated trust anchor certificate as the issuer to force EE cert path validation to fail. + let wrong_issuer_ca_der = + std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read wrong issuer ca"); + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &wrong_issuer_ca_der, + None, + None, + None, + validation_time, + ); assert!(out.vrps.is_empty()); assert!(!out.warnings.is_empty()); } #[test] fn invalid_aspa_object_is_reported_as_warning_under_drop_object_policy() { - let (mut pack, validation_time, resolver) = build_cernet_pack_and_validation_time(); + let (mut pack, validation_time, issuer_ca_der, issuer_ca) = + build_cernet_pack_and_validation_time(); let uri = "rsync://rpki.cernet.net/repo/cernet/0/INVALID.asa".to_string(); pack.files.push(PackFile::from_bytes_compute_sha256( @@ -160,8 +156,15 @@ fn invalid_aspa_object_is_reported_as_warning_under_drop_object_policy() { let mut policy = Policy::default(); policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject; - let out = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time) - .expect("drop_object should not fail"); + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &issuer_ca_der, + None, + issuer_ca.tbs.extensions.ip_resources.as_ref(), + issuer_ca.tbs.extensions.as_resources.as_ref(), + validation_time, + ); assert!( out.warnings diff --git a/tests/test_objects_policy_m8.rs b/tests/test_objects_policy_m8.rs index c2551b8..ff6fd97 100644 --- a/tests/test_objects_policy_m8.rs +++ b/tests/test_objects_policy_m8.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::path::Path; use rpki::data_model::crl::RpkixCrl; @@ -7,9 +6,7 @@ use rpki::data_model::rc::ResourceCertificate; use rpki::policy::{Policy, SignedObjectFailurePolicy}; use rpki::storage::{PackFile, RocksStore}; use rpki::validation::manifest::process_manifest_publication_point; -use rpki::validation::objects::{ - IssuerCaCertificateResolver, ObjectsProcessError, process_verified_publication_point_pack, -}; +use rpki::validation::objects::process_verified_publication_point_pack_for_issuer; fn fixture_to_rsync_uri(path: &Path) -> String { let rel = path @@ -33,20 +30,11 @@ fn fixture_dir_to_rsync_uri(dir: &Path) -> String { s } -struct MapResolver { - by_subject_dn: HashMap>, -} - -impl IssuerCaCertificateResolver for MapResolver { - fn resolve_by_subject_dn(&self, subject_dn: &str) -> Option> { - self.by_subject_dn.get(subject_dn).cloned() - } -} - fn build_cernet_pack_and_validation_time() -> ( rpki::storage::VerifiedPublicationPointPack, time::OffsetDateTime, - MapResolver, + Vec, + ResourceCertificate, ) { let manifest_path = Path::new( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", @@ -109,19 +97,13 @@ fn build_cernet_pack_and_validation_time() -> ( } t += time::Duration::seconds(1); - let mut resolver = MapResolver { - by_subject_dn: HashMap::new(), - }; - resolver - .by_subject_dn - .insert(issuer_ca.tbs.subject_dn, issuer_ca_der); - - (out.pack, t, resolver) + (out.pack, t, issuer_ca_der, issuer_ca) } #[test] fn drop_object_policy_drops_only_failing_object() { - let (mut pack, validation_time, resolver) = build_cernet_pack_and_validation_time(); + let (mut pack, validation_time, issuer_ca_der, issuer_ca) = + build_cernet_pack_and_validation_time(); let valid_roa_uri = pack .files @@ -145,8 +127,15 @@ fn drop_object_policy_drops_only_failing_object() { let mut policy = Policy::default(); policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropObject; - let out = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time) - .expect("drop_object should succeed"); + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &issuer_ca_der, + None, + issuer_ca.tbs.extensions.ip_resources.as_ref(), + issuer_ca.tbs.extensions.as_resources.as_ref(), + validation_time, + ); assert!( out.vrps.iter().any(|v| v.asn == 4538), @@ -161,8 +150,9 @@ fn drop_object_policy_drops_only_failing_object() { } #[test] -fn drop_publication_point_policy_fails_the_publication_point() { - let (mut pack, validation_time, resolver) = build_cernet_pack_and_validation_time(); +fn drop_publication_point_policy_drops_the_publication_point() { + let (mut pack, validation_time, issuer_ca_der, issuer_ca) = + build_cernet_pack_and_validation_time(); let tamper_idx = pack .files @@ -179,11 +169,19 @@ fn drop_publication_point_policy_fails_the_publication_point() { let mut policy = Policy::default(); policy.signed_object_failure_policy = SignedObjectFailurePolicy::DropPublicationPoint; - let err = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time) - .expect_err("drop_publication_point should fail"); - match err { - ObjectsProcessError::PublicationPointDropped { rsync_uri, .. } => { - assert_eq!(rsync_uri, victim_uri); - } - } + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &issuer_ca_der, + None, + issuer_ca.tbs.extensions.ip_resources.as_ref(), + issuer_ca.tbs.extensions.as_resources.as_ref(), + validation_time, + ); + assert!(out.stats.publication_point_dropped); + assert!(out.vrps.is_empty(), "expected publication point dropped"); + assert!( + out.audit.iter().any(|a| a.rsync_uri == victim_uri), + "expected audit entry for victim object" + ); } diff --git a/tests/test_objects_process_pack_for_issuer.rs b/tests/test_objects_process_pack_for_issuer.rs index f11c987..b86da52 100644 --- a/tests/test_objects_process_pack_for_issuer.rs +++ b/tests/test_objects_process_pack_for_issuer.rs @@ -4,10 +4,7 @@ use rpki::storage::{PackFile, PackTime, RocksStore, VerifiedPublicationPointPack use rpki::sync::repo::sync_publication_point; use rpki::sync::rrdp::Fetcher; use rpki::validation::manifest::process_manifest_publication_point; -use rpki::validation::objects::{ - IssuerCaCertificateResolver, process_verified_publication_point_pack, - process_verified_publication_point_pack_for_issuer, -}; +use rpki::validation::objects::process_verified_publication_point_pack_for_issuer; struct NoopHttpFetcher; impl Fetcher for NoopHttpFetcher { @@ -24,13 +21,20 @@ fn cernet_fixture() -> (std::path::PathBuf, String, String) { (dir, rsync_base_uri, manifest_file) } -fn validation_time_from_manifest_fixture(dir: &std::path::Path, manifest_file: &str) -> time::OffsetDateTime { +fn validation_time_from_manifest_fixture( + dir: &std::path::Path, + manifest_file: &str, +) -> time::OffsetDateTime { let bytes = std::fs::read(dir.join(manifest_file)).expect("read manifest fixture"); let mft = rpki::data_model::manifest::ManifestObject::decode_der(&bytes).expect("decode mft"); let this_update = mft.manifest.this_update; let next_update = mft.manifest.next_update; let candidate = this_update + time::Duration::seconds(60); - if candidate < next_update { candidate } else { this_update } + if candidate < next_update { + candidate + } else { + this_update + } } fn issuer_ca_fixture() -> Vec { @@ -129,7 +133,11 @@ fn process_pack_for_issuer_extracts_vrps_from_real_cernet_fixture() { validation_time, ); - assert!(out.vrps.len() > 10, "expected many VRPs, got {}", out.vrps.len()); + assert!( + out.vrps.len() > 10, + "expected many VRPs, got {}", + out.vrps.len() + ); assert!(out.aspas.is_empty()); } @@ -187,7 +195,10 @@ fn signed_object_failure_policy_drop_object_drops_only_bad_object() { "expected one audit entry per ROA" ); assert!( - out.audit.iter().any(|e| e.rsync_uri == pack.files[bad_idx].rsync_uri && matches!(e.result, rpki::audit::AuditObjectResult::Error)), + out.audit + .iter() + .any(|e| e.rsync_uri == pack.files[bad_idx].rsync_uri + && matches!(e.result, rpki::audit::AuditObjectResult::Error)), "expected audit error for the corrupted ROA" ); } @@ -303,10 +314,8 @@ fn process_pack_for_issuer_handles_invalid_aspa_bytes() { let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file); let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft"); - let crl_bytes = std::fs::read(dir.join( - "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", - )) - .expect("read crl"); + let crl_bytes = + std::fs::read(dir.join("05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl")).expect("read crl"); let pack = minimal_pack( &manifest_rsync_uri, @@ -345,10 +354,8 @@ fn process_pack_for_issuer_drop_publication_point_on_invalid_aspa_bytes() { let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file); let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft"); - let crl_bytes = std::fs::read(dir.join( - "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", - )) - .expect("read crl"); + let crl_bytes = + std::fs::read(dir.join("05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl")).expect("read crl"); let pack = minimal_pack( &manifest_rsync_uri, @@ -384,80 +391,4 @@ fn process_pack_for_issuer_drop_publication_point_on_invalid_aspa_bytes() { assert!(!out.warnings.is_empty()); } -struct NoIssuerResolver; -impl IssuerCaCertificateResolver for NoIssuerResolver { - fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option> { - None - } -} - -struct AlwaysIssuerResolver { - issuer_ca_der: Vec, -} - -impl IssuerCaCertificateResolver for AlwaysIssuerResolver { - fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option> { - Some(self.issuer_ca_der.clone()) - } -} - -#[test] -fn process_verified_pack_indexes_ca_certs_by_subject() { - let (dir, rsync_base_uri, manifest_file) = cernet_fixture(); - let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); - let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file); - let manifest_bytes = std::fs::read(dir.join(&manifest_file)).expect("read mft"); - - // Add a real CA certificate to exercise CA indexing logic. - let ca_der = issuer_ca_fixture(); - - let pack = minimal_pack( - &manifest_rsync_uri, - &rsync_base_uri, - manifest_bytes, - vec![PackFile::from_bytes_compute_sha256( - format!("{rsync_base_uri}some-ca.cer"), - ca_der, - )], - validation_time, - ); - - let policy = Policy::default(); - let out = process_verified_publication_point_pack( - &pack, - &policy, - &NoIssuerResolver, - validation_time, - ) - .expect("process pack"); - assert!(out.vrps.is_empty()); - assert!(out.aspas.is_empty()); -} - -#[test] -fn process_pack_with_resolver_extracts_vrps_from_real_cernet_fixture() { - let (dir, rsync_base_uri, manifest_file) = cernet_fixture(); - let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); - let validation_time = validation_time_from_manifest_fixture(&dir, &manifest_file); - - let pack = build_verified_pack_from_local_rsync_fixture( - &dir, - &rsync_base_uri, - &manifest_rsync_uri, - validation_time, - ); - - let policy = Policy::default(); - let issuer_ca_der = issuer_ca_fixture(); - let resolver = AlwaysIssuerResolver { - issuer_ca_der: issuer_ca_der.clone(), - }; - - let out = process_verified_publication_point_pack(&pack, &policy, &resolver, validation_time) - .expect("process pack"); - assert!(out.vrps.len() > 10, "expected many VRPs, got {}", out.vrps.len()); - assert!( - out.audit.len() >= pack.files.iter().filter(|f| f.rsync_uri.ends_with(".roa")).count(), - "expected ROA audit entries" - ); -} +// NOTE: DN-based issuer resolution and pack-local CA indexing have been removed for determinism. diff --git a/tests/test_objects_processing_coverage_m18.rs b/tests/test_objects_processing_coverage_m18.rs index 6a8921c..3354c76 100644 --- a/tests/test_objects_processing_coverage_m18.rs +++ b/tests/test_objects_processing_coverage_m18.rs @@ -1,9 +1,6 @@ use rpki::policy::{Policy, SignedObjectFailurePolicy}; use rpki::storage::{PackFile, PackTime, VerifiedPublicationPointPack}; -use rpki::validation::objects::{ - IssuerCaCertificateResolver, ObjectsProcessError, process_verified_publication_point_pack, - process_verified_publication_point_pack_for_issuer, -}; +use rpki::validation::objects::process_verified_publication_point_pack_for_issuer; fn fixture_bytes(path: &str) -> Vec { std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path)) @@ -24,28 +21,36 @@ fn dummy_pack(manifest_bytes: Vec, files: Vec) -> VerifiedPublicat } } -struct NoneResolver; - -impl IssuerCaCertificateResolver for NoneResolver { - fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option> { - None - } -} - #[test] -fn process_pack_drop_object_on_missing_issuer_ca_for_roa() { +fn process_pack_drop_object_on_wrong_issuer_ca_for_roa() { let manifest_bytes = fixture_bytes( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", ); let roa_bytes = fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); + let roa = rpki::data_model::roa::RoaObject::decode_der(&roa_bytes).expect("decode roa"); + let ee_crldp = roa.signed_object.signed_data.certificates[0] + .resource_cert + .tbs + .extensions + .crl_distribution_points_uris + .as_ref() + .expect("ee crldp")[0] + .as_str() + .to_string(); + let crl_bytes = fixture_bytes( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", + ); let pack = dummy_pack( manifest_bytes, - vec![PackFile::from_bytes_compute_sha256( - "rsync://rpki.cernet.net/repo/cernet/0/AS4538.roa", - roa_bytes, - )], + vec![ + PackFile::from_bytes_compute_sha256(ee_crldp, crl_bytes), + PackFile::from_bytes_compute_sha256( + "rsync://rpki.cernet.net/repo/cernet/0/AS4538.roa", + roa_bytes, + ), + ], ); let policy = Policy { @@ -53,9 +58,16 @@ fn process_pack_drop_object_on_missing_issuer_ca_for_roa() { ..Policy::default() }; - let out = - process_verified_publication_point_pack(&pack, &policy, &NoneResolver, time::OffsetDateTime::now_utc()) - .expect("drop_object should not error"); + let wrong_issuer_ca_der = fixture_bytes("tests/fixtures/ta/arin-ta.cer"); + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &wrong_issuer_ca_der, + None, + None, + None, + time::OffsetDateTime::now_utc(), + ); assert_eq!(out.stats.roa_total, 1); assert_eq!(out.stats.roa_ok, 0); @@ -64,12 +76,25 @@ fn process_pack_drop_object_on_missing_issuer_ca_for_roa() { } #[test] -fn process_pack_drop_publication_point_on_missing_issuer_ca_for_roa_skips_rest() { +fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_roa_skips_rest() { let manifest_bytes = fixture_bytes( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", ); let roa_bytes = fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); + let roa = rpki::data_model::roa::RoaObject::decode_der(&roa_bytes).expect("decode roa"); + let ee_crldp = roa.signed_object.signed_data.certificates[0] + .resource_cert + .tbs + .extensions + .crl_distribution_points_uris + .as_ref() + .expect("ee crldp")[0] + .as_str() + .to_string(); + let crl_bytes = fixture_bytes( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", + ); let aspa_bytes = fixture_bytes( "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", ); @@ -77,6 +102,7 @@ fn process_pack_drop_publication_point_on_missing_issuer_ca_for_roa_skips_rest() let pack = dummy_pack( manifest_bytes, vec![ + PackFile::from_bytes_compute_sha256(ee_crldp, crl_bytes), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/pp/first.roa", roa_bytes.clone(), @@ -85,10 +111,7 @@ fn process_pack_drop_publication_point_on_missing_issuer_ca_for_roa_skips_rest() "rsync://example.test/repo/pp/second.roa", roa_bytes, ), - PackFile::from_bytes_compute_sha256( - "rsync://example.test/repo/pp/x.asa", - aspa_bytes, - ), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/x.asa", aspa_bytes), ], ); @@ -97,28 +120,48 @@ fn process_pack_drop_publication_point_on_missing_issuer_ca_for_roa_skips_rest() ..Policy::default() }; - let err = - process_verified_publication_point_pack(&pack, &policy, &NoneResolver, time::OffsetDateTime::now_utc()) - .unwrap_err(); - assert!(matches!(err, ObjectsProcessError::PublicationPointDropped { .. })); - assert!(err.to_string().contains("drop_publication_point")); + let wrong_issuer_ca_der = fixture_bytes("tests/fixtures/ta/arin-ta.cer"); + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &wrong_issuer_ca_der, + None, + None, + None, + time::OffsetDateTime::now_utc(), + ); + assert!(out.stats.publication_point_dropped); + assert_eq!(out.warnings.len(), 1); } #[test] -fn process_pack_drop_object_on_missing_issuer_ca_for_aspa() { +fn process_pack_drop_object_on_wrong_issuer_ca_for_aspa() { let manifest_bytes = fixture_bytes( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", ); let aspa_bytes = fixture_bytes( "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", ); + let aspa = rpki::data_model::aspa::AspaObject::decode_der(&aspa_bytes).expect("decode aspa"); + let ee_crldp = aspa.signed_object.signed_data.certificates[0] + .resource_cert + .tbs + .extensions + .crl_distribution_points_uris + .as_ref() + .expect("ee crldp")[0] + .as_str() + .to_string(); + let crl_bytes = fixture_bytes( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", + ); let pack = dummy_pack( manifest_bytes, - vec![PackFile::from_bytes_compute_sha256( - "rsync://example.test/repo/pp/x.asa", - aspa_bytes, - )], + vec![ + PackFile::from_bytes_compute_sha256(ee_crldp, crl_bytes), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/x.asa", aspa_bytes), + ], ); let policy = Policy { @@ -126,9 +169,16 @@ fn process_pack_drop_object_on_missing_issuer_ca_for_aspa() { ..Policy::default() }; - let out = - process_verified_publication_point_pack(&pack, &policy, &NoneResolver, time::OffsetDateTime::now_utc()) - .expect("drop_object should not error"); + let wrong_issuer_ca_der = fixture_bytes("tests/fixtures/ta/arin-ta.cer"); + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &wrong_issuer_ca_der, + None, + None, + None, + time::OffsetDateTime::now_utc(), + ); assert_eq!(out.stats.aspa_total, 1); assert_eq!(out.stats.aspa_ok, 0); @@ -137,7 +187,7 @@ fn process_pack_drop_object_on_missing_issuer_ca_for_aspa() { } #[test] -fn process_pack_drop_publication_point_on_missing_issuer_ca_for_aspa_skips_rest() { +fn process_pack_drop_publication_point_on_wrong_issuer_ca_for_aspa_skips_rest() { let manifest_bytes = fixture_bytes( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", ); @@ -146,18 +196,26 @@ fn process_pack_drop_publication_point_on_missing_issuer_ca_for_aspa_skips_rest( let aspa_bytes = fixture_bytes( "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", ); + let aspa = rpki::data_model::aspa::AspaObject::decode_der(&aspa_bytes).expect("decode aspa"); + let ee_crldp = aspa.signed_object.signed_data.certificates[0] + .resource_cert + .tbs + .extensions + .crl_distribution_points_uris + .as_ref() + .expect("ee crldp")[0] + .as_str() + .to_string(); + let crl_bytes = fixture_bytes( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", + ); let pack = dummy_pack( manifest_bytes, vec![ - PackFile::from_bytes_compute_sha256( - "rsync://example.test/repo/pp/x.asa", - aspa_bytes, - ), - PackFile::from_bytes_compute_sha256( - "rsync://example.test/repo/pp/y.roa", - roa_bytes, - ), + PackFile::from_bytes_compute_sha256(ee_crldp, crl_bytes), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/x.asa", aspa_bytes), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/y.roa", roa_bytes), ], ); @@ -166,10 +224,17 @@ fn process_pack_drop_publication_point_on_missing_issuer_ca_for_aspa_skips_rest( ..Policy::default() }; - let err = - process_verified_publication_point_pack(&pack, &policy, &NoneResolver, time::OffsetDateTime::now_utc()) - .unwrap_err(); - assert!(matches!(err, ObjectsProcessError::PublicationPointDropped { .. })); + let wrong_issuer_ca_der = fixture_bytes("tests/fixtures/ta/arin-ta.cer"); + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &wrong_issuer_ca_der, + None, + None, + None, + time::OffsetDateTime::now_utc(), + ); + assert!(out.stats.publication_point_dropped); } #[test] @@ -186,14 +251,8 @@ fn process_pack_for_issuer_marks_objects_skipped_when_missing_issuer_crl() { let pack = dummy_pack( manifest_bytes, vec![ - PackFile::from_bytes_compute_sha256( - "rsync://example.test/repo/pp/a.roa", - roa_bytes, - ), - PackFile::from_bytes_compute_sha256( - "rsync://example.test/repo/pp/a.asa", - aspa_bytes, - ), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/a.roa", roa_bytes), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/a.asa", aspa_bytes), ], ); @@ -302,3 +361,147 @@ fn process_pack_for_issuer_drop_publication_point_records_skips_for_rest() { assert_eq!(out.warnings.len(), 1); } +#[test] +fn process_pack_for_issuer_selects_crl_by_ee_crldp_uri_roa() { + let manifest_bytes = fixture_bytes( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ); + let roa_bytes = + fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); + let roa = rpki::data_model::roa::RoaObject::decode_der(&roa_bytes).expect("decode roa"); + let ee_crldp = roa.signed_object.signed_data.certificates[0] + .resource_cert + .tbs + .extensions + .crl_distribution_points_uris + .as_ref() + .expect("ee crldp")[0] + .as_str() + .to_string(); + + // Provide a CRL file with the *exact* rsync URI referenced by the embedded EE certificate. + // Bytes need not be valid for this test: we just want to cover deterministic selection. + let pack = dummy_pack( + manifest_bytes, + vec![ + PackFile::from_bytes_compute_sha256(ee_crldp, vec![0x01]), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/a.roa", roa_bytes), + ], + ); + + let policy = Policy { + signed_object_failure_policy: SignedObjectFailurePolicy::DropObject, + ..Policy::default() + }; + + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &[0x01, 0x02, 0x03], + None, + None, + None, + time::OffsetDateTime::now_utc(), + ); + + assert_eq!(out.stats.roa_total, 1); + assert_eq!(out.stats.roa_ok, 0, "expected validation to fail later"); + assert_eq!(out.audit.len(), 1); + assert_eq!(out.warnings.len(), 1); +} + +#[test] +fn process_pack_for_issuer_rejects_roa_when_crldp_crl_missing() { + let manifest_bytes = fixture_bytes( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ); + let roa_bytes = + fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); + + // Pack has a CRL, but its URI does not match the embedded EE certificate CRLDP. + let pack = dummy_pack( + manifest_bytes, + vec![ + PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/pp/not-it.crl", + vec![0x01], + ), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/a.roa", roa_bytes), + ], + ); + + let policy = Policy { + signed_object_failure_policy: SignedObjectFailurePolicy::DropObject, + ..Policy::default() + }; + + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &[0x01, 0x02, 0x03], + None, + None, + None, + time::OffsetDateTime::now_utc(), + ); + + assert_eq!(out.stats.roa_total, 1); + assert_eq!(out.stats.roa_ok, 0); + assert_eq!(out.audit.len(), 1); + assert_eq!(out.warnings.len(), 1); + assert!( + out.warnings[0].message.contains("dropping invalid ROA") + || out.warnings[0] + .message + .contains("dropping publication point"), + "expected deterministic CRL selection failure to surface as an invalid ROA warning" + ); +} + +#[test] +fn process_pack_for_issuer_selects_crl_by_ee_crldp_uri_aspa() { + let manifest_bytes = fixture_bytes( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ); + let aspa_bytes = fixture_bytes( + "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", + ); + let aspa = rpki::data_model::aspa::AspaObject::decode_der(&aspa_bytes).expect("decode aspa"); + let ee_crldp = aspa.signed_object.signed_data.certificates[0] + .resource_cert + .tbs + .extensions + .crl_distribution_points_uris + .as_ref() + .expect("ee crldp")[0] + .as_str() + .to_string(); + + let pack = dummy_pack( + manifest_bytes, + vec![ + PackFile::from_bytes_compute_sha256(ee_crldp, vec![0x01]), + PackFile::from_bytes_compute_sha256("rsync://example.test/repo/pp/a.asa", aspa_bytes), + ], + ); + + let policy = Policy { + signed_object_failure_policy: SignedObjectFailurePolicy::DropObject, + ..Policy::default() + }; + + let out = process_verified_publication_point_pack_for_issuer( + &pack, + &policy, + &[0x01, 0x02, 0x03], + None, + None, + None, + time::OffsetDateTime::now_utc(), + ); + + assert_eq!(out.stats.aspa_total, 1); + assert_eq!(out.stats.aspa_ok, 0); + assert_eq!(out.audit.len(), 1); + assert_eq!(out.warnings.len(), 1); +} diff --git a/tests/test_run_m9.rs b/tests/test_run_m9.rs index e3ca55d..373128f 100644 --- a/tests/test_run_m9.rs +++ b/tests/test_run_m9.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::path::Path; use rpki::data_model::crl::RpkixCrl; @@ -8,7 +7,6 @@ use rpki::fetch::rsync::LocalDirRsyncFetcher; use rpki::policy::{Policy, SyncPreference}; use rpki::storage::RocksStore; use rpki::sync::rrdp::Fetcher; -use rpki::validation::objects::IssuerCaCertificateResolver; use rpki::validation::run::{run_publication_point_once, verified_pack_exists}; fn fixture_to_rsync_uri(path: &Path) -> String { @@ -41,16 +39,6 @@ impl Fetcher for NeverHttpFetcher { } } -struct MapResolver { - by_subject_dn: HashMap>, -} - -impl IssuerCaCertificateResolver for MapResolver { - fn resolve_by_subject_dn(&self, subject_dn: &str) -> Option> { - self.by_subject_dn.get(subject_dn).cloned() - } -} - #[test] fn e2e_offline_uses_rsync_then_writes_verified_pack_then_outputs_vrps() { let fixture_dir = Path::new("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); @@ -89,10 +77,6 @@ fn e2e_offline_uses_rsync_then_writes_verified_pack_then_outputs_vrps() { let rsync_fetcher = LocalDirRsyncFetcher::new(fixture_dir); let http_fetcher = NeverHttpFetcher; - let resolver = MapResolver { - by_subject_dn: HashMap::from([(issuer_ca.tbs.subject_dn, issuer_ca_der)]), - }; - let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); @@ -113,7 +97,10 @@ fn e2e_offline_uses_rsync_then_writes_verified_pack_then_outputs_vrps() { &publication_point_rsync_uri, &http_fetcher, &rsync_fetcher, - &resolver, + &issuer_ca_der, + None, + issuer_ca.tbs.extensions.ip_resources.as_ref(), + issuer_ca.tbs.extensions.as_resources.as_ref(), t, ) .expect("run publication point once"); diff --git a/tests/test_run_tree_from_tal_offline_m17.rs b/tests/test_run_tree_from_tal_offline_m17.rs index 0f3880d..c784ecd 100644 --- a/tests/test_run_tree_from_tal_offline_m17.rs +++ b/tests/test_run_tree_from_tal_offline_m17.rs @@ -1,8 +1,8 @@ use rpki::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der; use rpki::validation::run_tree_from_tal::root_handle_from_trust_anchor; use rpki::validation::run_tree_from_tal::{ - run_tree_from_tal_and_ta_der_serial, run_tree_from_tal_url_serial, - run_tree_from_tal_and_ta_der_serial_audit, run_tree_from_tal_url_serial_audit, + run_tree_from_tal_and_ta_der_serial, run_tree_from_tal_and_ta_der_serial_audit, + run_tree_from_tal_url_serial, run_tree_from_tal_url_serial_audit, }; use rpki::validation::tree::TreeRunConfig; @@ -23,7 +23,10 @@ impl rpki::sync::rrdp::Fetcher for MapHttpFetcher { struct EmptyRsyncFetcher; impl rpki::fetch::rsync::RsyncFetcher for EmptyRsyncFetcher { - fn fetch_objects(&self, _rsync_base_uri: &str) -> Result)>, rpki::fetch::rsync::RsyncFetchError> { + fn fetch_objects( + &self, + _rsync_base_uri: &str, + ) -> Result)>, rpki::fetch::rsync::RsyncFetchError> { Ok(Vec::new()) } } @@ -37,13 +40,18 @@ fn root_handle_is_constructible_from_fixture_tal_and_ta() { let discovery = discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None).expect("discover"); - let root = - root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance); + let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance); assert_eq!(root.depth, 0); - assert_eq!(root.manifest_rsync_uri, discovery.ca_instance.manifest_rsync_uri); + assert_eq!( + root.manifest_rsync_uri, + discovery.ca_instance.manifest_rsync_uri + ); assert_eq!(root.rsync_base_uri, discovery.ca_instance.rsync_base_uri); - assert!(root.ca_certificate_der.len() > 100, "TA der should be non-empty"); + assert!( + root.ca_certificate_der.len() > 100, + "TA der should be non-empty" + ); } #[test] @@ -103,7 +111,9 @@ fn run_tree_from_tal_and_ta_der_entry_executes_and_records_failure_when_repo_emp .expect("read apnic tal fixture"); let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture"); - let http = MapHttpFetcher { map: HashMap::new() }; + let http = MapHttpFetcher { + map: HashMap::new(), + }; let rsync = EmptyRsyncFetcher; let temp = tempfile::tempdir().expect("tempdir"); let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb"); @@ -190,7 +200,9 @@ fn run_tree_from_tal_and_ta_der_audit_entry_collects_no_publication_points_when_ .expect("read apnic tal fixture"); let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture"); - let http = MapHttpFetcher { map: HashMap::new() }; + let http = MapHttpFetcher { + map: HashMap::new(), + }; let rsync = EmptyRsyncFetcher; let temp = tempfile::tempdir().expect("tempdir"); diff --git a/tests/test_storage_iter_all.rs b/tests/test_storage_iter_all.rs index 6ee6435..0303936 100644 --- a/tests/test_storage_iter_all.rs +++ b/tests/test_storage_iter_all.rs @@ -31,4 +31,3 @@ fn storage_iter_all_lists_raw_and_verified_entries() { .collect::>(); assert_eq!(verified_keys, vec![key.as_str().to_string()]); } - diff --git a/tests/test_ta_validate_rc_constraints.rs b/tests/test_ta_validate_rc_constraints.rs index 1236193..59f2f8c 100644 --- a/tests/test_ta_validate_rc_constraints.rs +++ b/tests/test_ta_validate_rc_constraints.rs @@ -100,4 +100,3 @@ fn ta_rc_constraints_reject_as_inherit() { Err(TaCertificateProfileError::AsResourcesInherit) )); } - diff --git a/tests/test_ta_verify_self_signature.rs b/tests/test_ta_verify_self_signature.rs index 032b797..42d12a1 100644 --- a/tests/test_ta_verify_self_signature.rs +++ b/tests/test_ta_verify_self_signature.rs @@ -44,4 +44,3 @@ fn ta_verify_self_signature_rejects_tampered_signature() { TaCertificateVerifyError::InvalidSelfSignature(_) | TaCertificateVerifyError::Parse(_) )); } - diff --git a/tests/test_tree_failure_handling.rs b/tests/test_tree_failure_handling.rs index 323e139..dd2d7a3 100644 --- a/tests/test_tree_failure_handling.rs +++ b/tests/test_tree_failure_handling.rs @@ -1,14 +1,14 @@ use std::collections::HashMap; +use rpki::audit::{DiscoveredFrom, PublicationPointAudit}; use rpki::report::Warning; use rpki::storage::{PackTime, VerifiedPublicationPointPack}; use rpki::validation::manifest::PublicationPointSource; use rpki::validation::objects::{ObjectsOutput, ObjectsStats}; use rpki::validation::tree::{ - CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig, - run_tree_serial, + CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner, + TreeRunConfig, run_tree_serial, }; -use rpki::audit::PublicationPointAudit; fn empty_pack(manifest_uri: &str, pp_uri: &str) -> VerifiedPublicationPointPack { VerifiedPublicationPointPack { @@ -43,6 +43,25 @@ fn ca_handle(manifest_uri: &str) -> CaInstanceHandle { } } +fn discovered_child( + parent_manifest_uri: &str, + child_manifest_uri: &str, +) -> DiscoveredChildCaInstance { + let name = child_manifest_uri + .rsplit('/') + .next() + .unwrap_or("child.mft") + .trim_end_matches(".mft"); + DiscoveredChildCaInstance { + handle: ca_handle(child_manifest_uri), + discovered_from: DiscoveredFrom { + parent_manifest_rsync_uri: parent_manifest_uri.to_string(), + child_ca_certificate_rsync_uri: format!("rsync://example.test/repo/{name}.cer"), + child_ca_certificate_sha256_hex: "00".repeat(32), + }, + } +} + #[derive(Default)] struct ResultRunner { by_manifest: HashMap>, @@ -94,7 +113,10 @@ fn tree_continues_when_a_publication_point_fails() { audit: Vec::new(), }, audit: PublicationPointAudit::default(), - discovered_children: vec![ca_handle(bad_child_manifest), ca_handle(ok_child_manifest)], + discovered_children: vec![ + discovered_child(root_manifest, bad_child_manifest), + discovered_child(root_manifest, ok_child_manifest), + ], }, ) .with_err(bad_child_manifest, "synthetic failure") diff --git a/tests/test_tree_traversal_m14.rs b/tests/test_tree_traversal_m14.rs index b18e48e..91ec597 100644 --- a/tests/test_tree_traversal_m14.rs +++ b/tests/test_tree_traversal_m14.rs @@ -1,14 +1,14 @@ use std::collections::HashMap; +use rpki::audit::{DiscoveredFrom, PublicationPointAudit}; use rpki::report::Warning; use rpki::storage::{PackFile, PackTime, VerifiedPublicationPointPack}; use rpki::validation::manifest::PublicationPointSource; -use rpki::validation::tree::{ - CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig, - run_tree_serial, -}; use rpki::validation::objects::{ObjectsOutput, ObjectsStats}; -use rpki::audit::PublicationPointAudit; +use rpki::validation::tree::{ + CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner, + TreeRunConfig, run_tree_serial, run_tree_serial_audit, +}; #[derive(Default)] struct MockRunner { @@ -58,10 +58,7 @@ fn empty_pack(manifest_uri: &str, pp_uri: &str) -> VerifiedPublicationPointPack rfc3339_utc: "2026-02-06T00:00:00Z".to_string(), }, manifest_bytes: vec![1, 2, 3], - files: vec![PackFile::from_bytes_compute_sha256( - manifest_uri, - vec![1], - )], + files: vec![PackFile::from_bytes_compute_sha256(manifest_uri, vec![1])], } } @@ -79,6 +76,25 @@ fn ca_handle(manifest_uri: &str) -> CaInstanceHandle { } } +fn discovered_child( + parent_manifest_uri: &str, + child_manifest_uri: &str, +) -> DiscoveredChildCaInstance { + let name = child_manifest_uri + .rsplit('/') + .next() + .unwrap_or("child.mft") + .trim_end_matches(".mft"); + DiscoveredChildCaInstance { + handle: ca_handle(child_manifest_uri), + discovered_from: DiscoveredFrom { + parent_manifest_rsync_uri: parent_manifest_uri.to_string(), + child_ca_certificate_rsync_uri: format!("rsync://example.test/repo/{name}.cer"), + child_ca_certificate_sha256_hex: "00".repeat(32), + }, + } +} + #[test] fn tree_enqueues_children_only_for_fresh_publication_points() { let root_manifest = "rsync://example.test/repo/root.mft"; @@ -86,8 +102,11 @@ fn tree_enqueues_children_only_for_fresh_publication_points() { let child2_manifest = "rsync://example.test/repo/child2.mft"; let grandchild_manifest = "rsync://example.test/repo/grandchild.mft"; - let root_children = vec![ca_handle(child1_manifest), ca_handle(child2_manifest)]; - let child1_children = vec![ca_handle(grandchild_manifest)]; + let root_children = vec![ + discovered_child(root_manifest, child1_manifest), + discovered_child(root_manifest, child2_manifest), + ]; + let child1_children = vec![discovered_child(child1_manifest, grandchild_manifest)]; let runner = MockRunner::default() .with( @@ -150,10 +169,15 @@ fn tree_enqueues_children_only_for_fresh_publication_points() { assert_eq!(out.instances_failed, 0); let called = runner.called(); - assert_eq!(called, vec![root_manifest, child1_manifest, child2_manifest]); + assert_eq!( + called, + vec![root_manifest, child1_manifest, child2_manifest] + ); assert!( - out.warnings.iter().any(|w| w.message.contains("child1 warning")), + out.warnings + .iter() + .any(|w| w.message.contains("child1 warning")), "expected child1 warning propagated" ); assert!( @@ -184,7 +208,7 @@ fn tree_respects_max_depth_and_max_instances() { audit: Vec::new(), }, audit: PublicationPointAudit::default(), - discovered_children: vec![ca_handle(child_manifest)], + discovered_children: vec![discovered_child(root_manifest, child_manifest)], }, ) .with( @@ -229,3 +253,65 @@ fn tree_respects_max_depth_and_max_instances() { assert_eq!(out.instances_processed, 1); assert_eq!(out.instances_failed, 0); } + +#[test] +fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() { + let root_manifest = "rsync://example.test/repo/root.mft"; + let child_manifest = "rsync://example.test/repo/child.mft"; + + let runner = MockRunner::default() + .with( + root_manifest, + PublicationPointRunResult { + source: PublicationPointSource::Fresh, + pack: empty_pack(root_manifest, "rsync://example.test/repo/"), + warnings: Vec::new(), + objects: ObjectsOutput { + vrps: Vec::new(), + aspas: Vec::new(), + warnings: Vec::new(), + stats: ObjectsStats::default(), + audit: Vec::new(), + }, + audit: PublicationPointAudit::default(), + discovered_children: vec![discovered_child(root_manifest, child_manifest)], + }, + ) + .with( + child_manifest, + PublicationPointRunResult { + source: PublicationPointSource::Fresh, + pack: empty_pack(child_manifest, "rsync://example.test/repo/child/"), + warnings: Vec::new(), + objects: ObjectsOutput { + vrps: Vec::new(), + aspas: Vec::new(), + warnings: Vec::new(), + stats: ObjectsStats::default(), + audit: Vec::new(), + }, + audit: PublicationPointAudit::default(), + discovered_children: Vec::new(), + }, + ); + + let out = run_tree_serial_audit(ca_handle(root_manifest), &runner, &TreeRunConfig::default()) + .expect("run tree audit"); + + assert_eq!(out.tree.instances_processed, 2); + assert_eq!(out.publication_points.len(), 2); + + let root_audit = &out.publication_points[0]; + assert_eq!(root_audit.node_id, Some(0)); + assert_eq!(root_audit.parent_node_id, None); + assert!(root_audit.discovered_from.is_none()); + + let child_audit = &out.publication_points[1]; + assert_eq!(child_audit.node_id, Some(1)); + assert_eq!(child_audit.parent_node_id, Some(0)); + let df = child_audit + .discovered_from + .as_ref() + .expect("child discovered_from"); + assert_eq!(df.parent_manifest_rsync_uri, root_manifest); +} diff --git a/tests/test_uncovered_lines_fillers.rs b/tests/test_uncovered_lines_fillers.rs index 84bd097..1270c39 100644 --- a/tests/test_uncovered_lines_fillers.rs +++ b/tests/test_uncovered_lines_fillers.rs @@ -1,8 +1,8 @@ use rpki::data_model::manifest::ManifestObject; use rpki::data_model::rc::{AsIdOrRange, AsIdentifierChoice, AsResourceSet, ResourceCertificate}; +use rpki::data_model::roa::{IpPrefix, RoaAfi}; use rpki::data_model::ta::{TaCertificate, TaCertificateParsed, TaCertificateProfileError}; use rpki::data_model::tal::{Tal, TalDecodeError, TalProfileError}; -use rpki::data_model::roa::{IpPrefix, RoaAfi}; #[test] fn tal_validate_profile_noop_is_callable() {