将fetch pp cache改成使用vcir结构,跑通apnic全量同步

This commit is contained in:
2026-03-13 14:45:41 +08:00
parent e3339533b8
commit cf764c35bb
53 changed files with 8931 additions and 2201 deletions

View File

@@ -10,8 +10,10 @@ use crate::data_model::roa::{IpPrefix, RoaAfi, RoaDecodeError, RoaObject, RoaVal
use crate::data_model::signed_object::SignedObjectVerifyError;
use crate::policy::{Policy, SignedObjectFailurePolicy};
use crate::report::{RfcRef, Warning};
use crate::storage::{FetchCachePpPack, PackFile};
use crate::storage::PackFile;
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_prevalidated_issuer};
use crate::validation::manifest::PublicationPointData;
use crate::validation::publication_point::PublicationPointSnapshot;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
@@ -81,10 +83,10 @@ pub struct ObjectsStats {
pub publication_point_dropped: bool,
}
/// Process objects from a fetch_cache_pp publication point pack using a known issuer CA certificate
/// Process objects from a publication point snapshot using a known issuer CA certificate
/// and its effective resources (resolved via the resource-path, RFC 6487 §7.2).
pub fn process_fetch_cache_pp_pack_for_issuer(
pack: &FetchCachePpPack,
pub fn process_publication_point_for_issuer<P: PublicationPointData>(
publication_point: &P,
policy: &Policy,
issuer_ca_der: &[u8],
issuer_ca_rsync_uri: Option<&str>,
@@ -93,23 +95,24 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
validation_time: time::OffsetDateTime,
timing: Option<&TimingHandle>,
) -> ObjectsOutput {
let manifest_rsync_uri = publication_point.manifest_rsync_uri();
let manifest_bytes = publication_point.manifest_bytes();
let locked_files = publication_point.files();
let mut warnings: Vec<Warning> = Vec::new();
let mut stats = ObjectsStats::default();
stats.roa_total = pack
.files
stats.roa_total = locked_files
.iter()
.filter(|f| f.rsync_uri.ends_with(".roa"))
.count();
stats.aspa_total = pack
.files
stats.aspa_total = locked_files
.iter()
.filter(|f| f.rsync_uri.ends_with(".asa"))
.count();
let mut audit: Vec<ObjectAuditEntry> = Vec::new();
// Enforce that `manifest_bytes` is actually a manifest object.
let _manifest =
ManifestObject::decode_der(&pack.manifest_bytes).expect("fetch_cache_pp manifest decodes");
let _manifest = ManifestObject::decode_der(manifest_bytes)
.expect("publication point snapshot manifest decodes");
// Decode issuer CA once; if it fails we cannot validate ROA/ASPA EE certificates.
let issuer_ca = match ResourceCertificate::decode_der(issuer_ca_der) {
@@ -121,9 +124,9 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
"dropping publication point: issuer CA decode failed: {e}"
))
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2"), RfcRef("RFC 5280 §6.1")])
.with_context(&pack.manifest_rsync_uri),
.with_context(manifest_rsync_uri),
);
for f in &pack.files {
for f in locked_files {
if f.rsync_uri.ends_with(".roa") {
audit.push(ObjectAuditEntry {
rsync_uri: f.rsync_uri.clone(),
@@ -163,7 +166,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
rem.len()
))
.with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
.with_context(&pack.manifest_rsync_uri),
.with_context(manifest_rsync_uri),
);
return ObjectsOutput {
vrps: Vec::new(),
@@ -180,7 +183,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
"dropping publication point: issuer SPKI parse failed: {e}"
))
.with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
.with_context(&pack.manifest_rsync_uri),
.with_context(manifest_rsync_uri),
);
return ObjectsOutput {
vrps: Vec::new(),
@@ -192,8 +195,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
}
};
let mut crl_cache: std::collections::HashMap<String, CachedIssuerCrl> = pack
.files
let mut crl_cache: std::collections::HashMap<String, CachedIssuerCrl> = locked_files
.iter()
.filter(|f| f.rsync_uri.ends_with(".crl"))
.map(|f| {
@@ -207,23 +209,26 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
let issuer_resources_index =
build_issuer_resources_index(issuer_effective_ip, issuer_effective_as);
// 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 the snapshot 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 snapshot).
if crl_cache.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) {
stats.publication_point_dropped = true;
warnings.push(
Warning::new("dropping publication point: no CRL files in fetch_cache_pp")
Warning::new("dropping publication point: no CRL files in validated publication point")
.with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")])
.with_context(&pack.manifest_rsync_uri),
.with_context(manifest_rsync_uri),
);
for f in &pack.files {
for f in locked_files {
if f.rsync_uri.ends_with(".roa") {
audit.push(ObjectAuditEntry {
rsync_uri: f.rsync_uri.clone(),
sha256_hex: sha256_hex_from_32(&f.sha256),
kind: AuditObjectKind::Roa,
result: AuditObjectResult::Skipped,
detail: Some("skipped due to missing CRL files in fetch_cache_pp".to_string()),
detail: Some(
"skipped due to missing CRL files in validated publication point"
.to_string(),
),
});
} else if f.rsync_uri.ends_with(".asa") {
audit.push(ObjectAuditEntry {
@@ -231,7 +236,10 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
sha256_hex: sha256_hex_from_32(&f.sha256),
kind: AuditObjectKind::Aspa,
result: AuditObjectResult::Skipped,
detail: Some("skipped due to missing CRL files in fetch_cache_pp".to_string()),
detail: Some(
"skipped due to missing CRL files in validated publication point"
.to_string(),
),
});
}
}
@@ -247,7 +255,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
let mut vrps: Vec<Vrp> = Vec::new();
let mut aspas: Vec<AspaAttestation> = Vec::new();
for (idx, file) in pack.files.iter().enumerate() {
for (idx, file) in locked_files.iter().enumerate() {
if file.rsync_uri.ends_with(".roa") {
let _t = timing.as_ref().map(|t| t.span_phase("objects_roa_total"));
match process_roa_with_issuer(
@@ -300,7 +308,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
result: AuditObjectResult::Error,
detail: Some(e.to_string()),
});
for f in pack.files.iter().skip(idx + 1) {
for f in locked_files.iter().skip(idx + 1) {
if f.rsync_uri.ends_with(".roa") {
audit.push(ObjectAuditEntry {
rsync_uri: f.rsync_uri.clone(),
@@ -333,7 +341,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
file.rsync_uri
))
.with_rfc_refs(&refs)
.with_context(&pack.manifest_rsync_uri),
.with_context(manifest_rsync_uri),
);
return ObjectsOutput {
vrps: Vec::new(),
@@ -397,7 +405,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
result: AuditObjectResult::Error,
detail: Some(e.to_string()),
});
for f in pack.files.iter().skip(idx + 1) {
for f in locked_files.iter().skip(idx + 1) {
if f.rsync_uri.ends_with(".roa") {
audit.push(ObjectAuditEntry {
rsync_uri: f.rsync_uri.clone(),
@@ -430,7 +438,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
file.rsync_uri
))
.with_rfc_refs(&refs)
.with_context(&pack.manifest_rsync_uri),
.with_context(manifest_rsync_uri),
);
return ObjectsOutput {
vrps: Vec::new(),
@@ -453,6 +461,28 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
audit,
}
}
/// Compatibility wrapper that processes a publication point snapshot.
pub fn process_publication_point_snapshot_for_issuer(
pack: &PublicationPointSnapshot,
policy: &Policy,
issuer_ca_der: &[u8],
issuer_ca_rsync_uri: Option<&str>,
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
validation_time: time::OffsetDateTime,
timing: Option<&TimingHandle>,
) -> ObjectsOutput {
process_publication_point_for_issuer(
pack,
policy,
issuer_ca_der,
issuer_ca_rsync_uri,
issuer_effective_ip,
issuer_effective_as,
validation_time,
timing,
)
}
#[derive(Debug, thiserror::Error)]
enum ObjectValidateError {
@@ -480,12 +510,12 @@ enum ObjectValidateError {
MissingCrlDpUris,
#[error(
"no CRL available in fetch_cache_pp (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
"no CRL available in publication point snapshot (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
)]
MissingCrlInPack,
#[error(
"CRL referenced by CRLDistributionPoints not found in fetch_cache_pp: {0} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)"
"CRL referenced by CRLDistributionPoints not found in publication point snapshot: {0} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)"
)]
CrlNotFound(String),
@@ -1283,7 +1313,7 @@ mod tests {
}
#[test]
fn choose_crl_for_certificate_reports_missing_crl_in_pack() {
fn choose_crl_for_certificate_reports_missing_crl_in_snapshot() {
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");
@@ -1335,7 +1365,7 @@ mod tests {
}
#[test]
fn choose_crl_for_certificate_reports_not_found_when_crldp_does_not_match_pack() {
fn choose_crl_for_certificate_reports_not_found_when_crldp_does_not_match_snapshot() {
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");
@@ -1421,4 +1451,225 @@ mod tests {
let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None, &idx).unwrap_err();
assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset));
}
#[test]
fn extra_rfc_refs_for_crl_selection_distinguishes_crl_errors() {
assert_eq!(
extra_rfc_refs_for_crl_selection(&ObjectValidateError::MissingCrlDpUris),
RFC_CRLDP
);
assert_eq!(
extra_rfc_refs_for_crl_selection(&ObjectValidateError::CrlNotFound(
"rsync://example.test/x.crl".to_string(),
)),
RFC_CRLDP_AND_LOCKED_PACK
);
assert!(
extra_rfc_refs_for_crl_selection(&ObjectValidateError::MissingCrlInPack).is_empty()
);
}
#[test]
fn as_subset_helpers_cover_success_and_failure_paths() {
let child = AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Id(5),
AsIdOrRange::Range { min: 7, max: 9 },
]);
let parent =
AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 10 }]);
let parent_intervals = [(1, 10)];
assert!(as_choice_subset(None, Some(&parent)));
assert!(!as_choice_subset(Some(&child), None));
assert!(!as_choice_subset(
Some(&AsIdentifierChoice::Inherit),
Some(&parent)
));
assert!(!as_choice_subset(
Some(&child),
Some(&AsIdentifierChoice::Inherit)
));
assert!(as_choice_subset(Some(&child), Some(&parent)));
assert!(as_choice_subset_indexed(None, Some(&parent_intervals)));
assert!(!as_choice_subset_indexed(Some(&child), None));
assert!(!as_choice_subset_indexed(
Some(&AsIdentifierChoice::Inherit),
Some(&parent_intervals),
));
assert!(as_choice_subset_indexed(
Some(&child),
Some(&parent_intervals)
));
assert!(!as_choice_subset_indexed(
Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Range { min: 11, max: 12 }
])),
Some(&parent_intervals),
));
let child_set = AsResourceSet {
asnum: Some(child.clone()),
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(42)])),
};
let parent_set = AsResourceSet {
asnum: Some(parent.clone()),
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Range { min: 40, max: 50 },
])),
};
assert!(as_resources_is_subset(&child_set, &parent_set));
assert!(as_resources_is_subset_indexed(
&child_set,
&parent_set,
&IssuerResourcesIndex {
asnum: Some(vec![(1, 10)]),
rdi: Some(vec![(40, 50)]),
..IssuerResourcesIndex::default()
},
));
}
#[test]
fn ip_subset_helpers_cover_strict_and_indexed_paths() {
let parent = IpResourceSet {
families: vec![
IpAddressFamily {
afi: Afi::Ipv4,
choice: IpAddressChoice::AddressesOrRanges(vec![
IpAddressOrRange::Prefix(IpPrefix {
afi: Afi::Ipv4,
prefix_len: 8,
addr: vec![10, 0, 0, 0],
}),
IpAddressOrRange::Range(IpAddressRange {
min: vec![192, 0, 2, 0],
max: vec![192, 0, 2, 255],
}),
]),
},
IpAddressFamily {
afi: Afi::Ipv6,
choice: IpAddressChoice::Inherit,
},
],
};
let child = IpResourceSet {
families: vec![IpAddressFamily {
afi: Afi::Ipv4,
choice: IpAddressChoice::AddressesOrRanges(vec![
IpAddressOrRange::Prefix(IpPrefix {
afi: Afi::Ipv4,
prefix_len: 16,
addr: vec![10, 1, 0, 0],
}),
IpAddressOrRange::Range(IpAddressRange {
min: vec![192, 0, 2, 10],
max: vec![192, 0, 2, 20],
}),
]),
}],
};
let strict_bad = IpResourceSet {
families: vec![IpAddressFamily {
afi: Afi::Ipv4,
choice: IpAddressChoice::Inherit,
}],
};
assert!(ip_resources_is_subset(&child, &parent));
assert!(ip_resources_to_merged_intervals(&parent).contains_key(&AfiKey::V4));
assert!(ip_resources_to_merged_intervals_strict(&child).is_ok());
assert!(ip_resources_to_merged_intervals_strict(&strict_bad).is_err());
let idx = build_issuer_resources_index(
Some(&parent),
Some(&AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Range {
min: 64500,
max: 64510,
},
])),
rdi: Some(AsIdentifierChoice::Inherit),
}),
);
assert!(idx.ip_v4.is_some());
assert!(idx.ip_v6.is_none());
assert!(idx.asnum.is_some());
assert!(idx.rdi.is_none());
assert!(ip_resources_is_subset_indexed(&child, &parent, &idx));
assert!(!ip_resources_is_subset_indexed(&strict_bad, &parent, &idx));
}
#[test]
fn interval_and_byte_helpers_cover_edge_cases() {
let parent = vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 10])];
assert!(interval_is_covered(&parent, &[0, 0, 0, 1], &[0, 0, 0, 2]));
assert!(!interval_is_covered(
&parent,
&[0, 0, 0, 11],
&[0, 0, 0, 12]
));
assert!(intervals_are_covered(
&parent,
&[(vec![0, 0, 0, 1], vec![0, 0, 0, 2])]
));
assert!(!intervals_are_covered(
&parent,
&[(vec![0, 0, 0, 9], vec![0, 0, 0, 11])],
));
let prefix = RcIpPrefix {
afi: Afi::Ipv4,
prefix_len: 24,
addr: vec![203, 0, 113, 7],
};
assert_eq!(
prefix_to_range(&prefix),
(vec![203, 0, 113, 0], vec![203, 0, 113, 255])
);
assert_eq!(increment_bytes(&[0, 0, 0, 255]), vec![0, 0, 1, 0]);
assert!(bytes_is_next(&[0, 0, 1, 0], &[0, 0, 0, 255]));
assert!(!bytes_is_next(&[1, 2], &[1]));
}
#[test]
fn merged_interval_helpers_cover_empty_and_break_paths() {
let mut empty: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
merge_ip_intervals_in_place(&mut empty);
assert!(empty.is_empty());
let mut v = vec![
(vec![0, 0, 0, 20], vec![0, 0, 0, 30]),
(vec![0, 0, 0, 0], vec![0, 0, 0, 10]),
(vec![0, 0, 0, 11], vec![0, 0, 0, 19]),
];
v.sort_by(|(a, _), (b, _)| a.cmp(b));
merge_ip_intervals_in_place(&mut v);
assert_eq!(v, vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 30])]);
assert_eq!(
as_choice_to_merged_intervals(&AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Id(1),
AsIdOrRange::Range { min: 2, max: 3 },
AsIdOrRange::Range { min: 7, max: 9 },
])),
vec![(1, 3), (7, 9)]
);
assert!(as_interval_is_covered(&[(1, 3), (7, 9)], 2, 3));
assert!(!as_interval_is_covered(&[(7, 9)], 2, 3));
}
#[test]
fn roa_output_helpers_cover_vrps_and_afi_strings() {
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 vrps = roa_to_vrps(&roa);
assert!(!vrps.is_empty());
assert!(vrps.iter().all(|vrp| vrp.asn == roa.roa.as_id));
assert_eq!(roa_afi_to_string(RoaAfi::Ipv4), "ipv4");
assert_eq!(roa_afi_to_string(RoaAfi::Ipv6), "ipv6");
}
}