20260701 add validation-update-03 VRS mode

This commit is contained in:
2026-07-01 20:28:25 +08:00
parent a501cfb709
commit a5b194ce35
14 changed files with 1430 additions and 24 deletions

View File

@@ -13,7 +13,7 @@ use crate::parallel::config::ParallelPhase2Config;
use crate::parallel::object_worker::{
ObjectTaskExecutor, ObjectWorkerPool, ObjectWorkerSubmitError,
};
use crate::policy::{Policy, SignedObjectFailurePolicy};
use crate::policy::{Policy, ResourceValidationMode, SignedObjectFailurePolicy};
use crate::report::{RfcRef, Warning};
use crate::storage::{
PackFile, PackTime, RoaCacheObjectMeta, RoaCacheProjection, VcirLocalOutput,
@@ -1226,6 +1226,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
collect_vcir_local_outputs,
policy.strict.cms_der,
policy.strict.name,
policy.resource_validation_mode,
)
}
blocked @ (RoaCacheLookupResult::HashBlocked
@@ -1251,6 +1252,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
collect_vcir_local_outputs,
policy.strict.cms_der,
policy.strict.name,
policy.resource_validation_mode,
)
}
}
@@ -1273,6 +1275,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
collect_vcir_local_outputs,
policy.strict.cms_der,
policy.strict.name,
policy.resource_validation_mode,
)
};
match result.outcome {
@@ -1386,6 +1389,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
collect_vcir_local_outputs,
policy.strict.cms_der,
policy.strict.name,
policy.resource_validation_mode,
) {
Ok((att, local_output)) => {
stats.aspa_ok += 1;
@@ -1735,6 +1739,7 @@ pub(crate) struct RoaTaskShared {
issuer_resources_index: Arc<IssuerResourcesIndex>,
issuer_effective_ip: Option<Arc<crate::data_model::rc::IpResourceSet>>,
issuer_effective_as: Option<Arc<crate::data_model::rc::AsResourceSet>>,
resource_validation_mode: ResourceValidationMode,
}
#[derive(Clone)]
@@ -1746,6 +1751,7 @@ pub(crate) struct OwnedRoaTask {
collect_vcir_local_outputs: bool,
strict_cms_der: bool,
strict_name: bool,
resource_validation_mode: ResourceValidationMode,
pub(crate) submitted_at: Option<Instant>,
}
@@ -1854,6 +1860,7 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe
task.collect_vcir_local_outputs,
task.strict_cms_der,
task.strict_name,
task.resource_validation_mode,
)
.map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk {
vrps,
@@ -1884,6 +1891,7 @@ pub(crate) struct ParallelObjectsStage {
collect_vcir_local_outputs: bool,
strict_cms_der: bool,
strict_name: bool,
resource_validation_mode: ResourceValidationMode,
roa_task_indices: Vec<usize>,
cached_roa_results: Vec<RoaTaskResult>,
roa_cache_stats: RoaValidationCacheStats,
@@ -1921,6 +1929,7 @@ impl ParallelObjectsStage {
collect_vcir_local_outputs: self.collect_vcir_local_outputs,
strict_cms_der: self.strict_cms_der,
strict_name: self.strict_name,
resource_validation_mode: self.resource_validation_mode,
submitted_at: None,
});
});
@@ -2257,11 +2266,13 @@ pub(crate) fn prepare_publication_point_for_parallel_roa_with_cache<P: Publicati
)),
issuer_effective_ip: issuer_effective_ip.cloned().map(Arc::new),
issuer_effective_as: issuer_effective_as.cloned().map(Arc::new),
resource_validation_mode: policy.resource_validation_mode,
}),
validation_time,
collect_vcir_local_outputs,
strict_cms_der: policy.strict.cms_der,
strict_name: policy.strict.name,
resource_validation_mode: policy.resource_validation_mode,
roa_task_indices,
cached_roa_results,
roa_cache_stats,
@@ -2370,6 +2381,7 @@ pub(crate) fn reduce_parallel_roa_stage(
collect_vcir_local_outputs,
strict_cms_der,
strict_name,
shared.resource_validation_mode,
) {
Ok((att, local_output)) => {
stats.aspa_ok += 1;
@@ -2604,6 +2616,7 @@ pub(crate) fn validate_roa_task_serial(
collect_vcir_local_outputs: bool,
strict_cms_der: bool,
strict_name: bool,
resource_validation_mode: ResourceValidationMode,
) -> RoaTaskResult {
let outcome = process_roa_with_issuer(
task.file,
@@ -2621,6 +2634,7 @@ pub(crate) fn validate_roa_task_serial(
collect_vcir_local_outputs,
strict_cms_der,
strict_name,
resource_validation_mode,
)
.map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk {
vrps,
@@ -2655,6 +2669,7 @@ fn process_roa_with_issuer(
collect_vcir_local_outputs: bool,
strict_cms_der: bool,
strict_name: bool,
resource_validation_mode: ResourceValidationMode,
) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>, Option<RoaCacheObjectMeta>), ObjectValidateError> {
let _decode = timing
.as_ref()
@@ -2706,15 +2721,16 @@ fn process_roa_with_issuer(
let _subset = timing
.as_ref()
.map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total"));
validate_ee_resources_subset(
let ee_vrs = validate_ee_resources_for_mode(
&ee.resource_cert,
issuer_effective_ip,
issuer_effective_as,
issuer_resources_index,
resource_validation_mode,
)?;
drop(_subset);
let vrps = roa_to_vrps(&roa);
let vrps = roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())?;
let cache_object_meta = RoaCacheObjectMeta {
source_object_uri: file.rsync_uri.clone(),
source_object_hash: file.sha256,
@@ -2777,6 +2793,7 @@ fn process_roa_with_issuer_parallel_cached(
collect_vcir_local_outputs: bool,
strict_cms_der: bool,
strict_name: bool,
resource_validation_mode: ResourceValidationMode,
) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>, Option<RoaCacheObjectMeta>), ObjectValidateError> {
let _decode = timing
.as_ref()
@@ -2834,15 +2851,16 @@ fn process_roa_with_issuer_parallel_cached(
let _subset = timing
.as_ref()
.map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total"));
validate_ee_resources_subset(
let ee_vrs = validate_ee_resources_for_mode(
&ee.resource_cert,
issuer_effective_ip,
issuer_effective_as,
issuer_resources_index,
resource_validation_mode,
)?;
drop(_subset);
let vrps = roa_to_vrps(&roa);
let vrps = roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())?;
let cache_object_meta = RoaCacheObjectMeta {
source_object_uri: file.rsync_uri.clone(),
source_object_hash: file.sha256,
@@ -2905,6 +2923,7 @@ fn process_aspa_with_issuer(
collect_vcir_local_outputs: bool,
strict_cms_der: bool,
strict_name: bool,
resource_validation_mode: ResourceValidationMode,
) -> Result<(AspaAttestation, Option<VcirLocalOutput>), ObjectValidateError> {
let _decode = timing
.as_ref()
@@ -2956,14 +2975,17 @@ fn process_aspa_with_issuer(
let _subset = timing
.as_ref()
.map(|t| t.span_phase("objects_aspa_validate_ee_resources_subset_total"));
validate_ee_resources_subset(
let ee_vrs = validate_ee_resources_for_mode(
&ee.resource_cert,
issuer_effective_ip,
issuer_effective_as,
issuer_resources_index,
resource_validation_mode,
)?;
drop(_subset);
validate_aspa_customer_in_vrs(&aspa, ee_vrs.asn.as_ref())?;
let attestation = AspaAttestation {
customer_as_id: aspa.aspa.customer_as_id,
provider_as_ids: aspa.aspa.provider_as_ids.clone(),
@@ -3119,6 +3141,176 @@ fn validate_ee_resources_subset(
Ok(())
}
#[derive(Debug)]
struct EeVerifiedResources {
ip: Option<crate::data_model::rc::IpResourceSet>,
asn: Option<crate::data_model::rc::AsResourceSet>,
}
fn validate_ee_resources_for_mode(
ee: &ResourceCertificate,
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
issuer_resources_index: &IssuerResourcesIndex,
mode: ResourceValidationMode,
) -> Result<EeVerifiedResources, ObjectValidateError> {
match mode {
ResourceValidationMode::Rfc6487 => {
validate_ee_resources_subset(
ee,
issuer_effective_ip,
issuer_effective_as,
issuer_resources_index,
)?;
Ok(EeVerifiedResources {
ip: ee.tbs.extensions.ip_resources.clone(),
asn: ee.tbs.extensions.as_resources.clone(),
})
}
ResourceValidationMode::ValidationUpdate03 => {
let ip = match ee.tbs.extensions.ip_resources.as_ref() {
Some(child_ip) => Some(intersect_ee_ip_resources_vrs(
child_ip,
issuer_effective_ip,
issuer_resources_index,
)?),
None => None,
};
let asn = match ee.tbs.extensions.as_resources.as_ref() {
Some(child_as) => Some(intersect_ee_as_resources_vrs(
child_as,
issuer_effective_as,
issuer_resources_index,
)?),
None => None,
};
Ok(EeVerifiedResources { ip, asn })
}
}
}
fn intersect_ee_ip_resources_vrs(
child_ip: &crate::data_model::rc::IpResourceSet,
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
issuer_resources_index: &IssuerResourcesIndex,
) -> Result<crate::data_model::rc::IpResourceSet, ObjectValidateError> {
if child_ip.has_any_inherit() {
return Err(ObjectValidateError::EeResourcesNotSubset);
}
let _ = issuer_effective_ip;
let mut families = Vec::new();
for fam in &child_ip.families {
let parent_intervals = match fam.afi {
crate::data_model::rc::Afi::Ipv4 => issuer_resources_index.ip_v4.as_deref(),
crate::data_model::rc::Afi::Ipv6 => issuer_resources_index.ip_v6.as_deref(),
}
.unwrap_or(&[]);
let items = match &fam.choice {
IpAddressChoice::Inherit => return Err(ObjectValidateError::EeResourcesNotSubset),
IpAddressChoice::AddressesOrRanges(items) => items,
};
let intersections = intersect_ip_items_with_parent_intervals(items, parent_intervals);
if !intersections.is_empty() {
families.push(crate::data_model::rc::IpAddressFamily {
afi: fam.afi,
choice: IpAddressChoice::AddressesOrRanges(ip_intervals_to_ranges(
fam.afi,
&intersections,
)),
});
}
}
Ok(crate::data_model::rc::IpResourceSet { families })
}
fn intersect_ee_as_resources_vrs(
child_as: &crate::data_model::rc::AsResourceSet,
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
issuer_resources_index: &IssuerResourcesIndex,
) -> Result<crate::data_model::rc::AsResourceSet, ObjectValidateError> {
let _ = issuer_effective_as;
if matches!(child_as.asnum, Some(AsIdentifierChoice::Inherit))
|| matches!(child_as.rdi, Some(AsIdentifierChoice::Inherit))
{
return Err(ObjectValidateError::EeResourcesNotSubset);
}
let asnum = child_as.asnum.as_ref().map(|choice| {
let child_intervals = as_choice_to_merged_intervals(choice);
AsIdentifierChoice::AsIdsOrRanges(as_intervals_to_items(&intersect_as_intervals(
&child_intervals,
issuer_resources_index.asnum.as_deref().unwrap_or(&[]),
)))
});
let rdi = child_as.rdi.as_ref().map(|choice| {
let child_intervals = as_choice_to_merged_intervals(choice);
AsIdentifierChoice::AsIdsOrRanges(as_intervals_to_items(&intersect_as_intervals(
&child_intervals,
issuer_resources_index.rdi.as_deref().unwrap_or(&[]),
)))
});
Ok(crate::data_model::rc::AsResourceSet { asnum, rdi })
}
fn roa_to_vrps_with_vrs(
roa: &RoaObject,
ee_vrs_ip: Option<&crate::data_model::rc::IpResourceSet>,
) -> Result<Vec<Vrp>, ObjectValidateError> {
let vrps = roa_to_vrps(roa);
let Some(ee_vrs_ip) = ee_vrs_ip else {
return Err(ObjectValidateError::EeResourcesNotSubset);
};
for vrp in &vrps {
let rc_prefix = roa_prefix_to_rc_prefix(&vrp.prefix);
if !ee_vrs_ip.contains_prefix(&rc_prefix) {
return Err(ObjectValidateError::EeResourcesNotSubset);
}
}
Ok(vrps)
}
fn validate_aspa_customer_in_vrs(
aspa: &AspaObject,
ee_vrs_as: Option<&crate::data_model::rc::AsResourceSet>,
) -> Result<(), ObjectValidateError> {
let Some(ee_vrs_as) = ee_vrs_as else {
return Err(ObjectValidateError::EeResourcesNotSubset);
};
if !as_resource_set_contains_asn(ee_vrs_as, aspa.aspa.customer_as_id) {
return Err(ObjectValidateError::EeResourcesNotSubset);
}
Ok(())
}
fn as_resource_set_contains_asn(
resources: &crate::data_model::rc::AsResourceSet,
asn: u32,
) -> bool {
let Some(choice) = resources.asnum.as_ref() else {
return false;
};
match choice {
AsIdentifierChoice::Inherit => false,
AsIdentifierChoice::AsIdsOrRanges(items) => items.iter().any(|item| match item {
crate::data_model::rc::AsIdOrRange::Id(id) => *id == asn,
crate::data_model::rc::AsIdOrRange::Range { min, max } => *min <= asn && asn <= *max,
}),
}
}
fn roa_prefix_to_rc_prefix(prefix: &IpPrefix) -> RcIpPrefix {
let afi = match prefix.afi {
RoaAfi::Ipv4 => crate::data_model::rc::Afi::Ipv4,
RoaAfi::Ipv6 => crate::data_model::rc::Afi::Ipv6,
};
let mut addr = prefix.addr.to_vec();
addr.truncate(afi.octets_len());
RcIpPrefix {
afi,
prefix_len: prefix.prefix_len,
addr,
}
}
fn as_resources_is_subset(child: &AsResourceSet, parent: &AsResourceSet) -> bool {
as_choice_subset(child.asnum.as_ref(), parent.asnum.as_ref())
&& as_choice_subset(child.rdi.as_ref(), parent.rdi.as_ref())
@@ -3292,6 +3484,121 @@ fn ip_resources_is_subset_indexed(
true
}
fn ip_items_to_merged_intervals(
items: &[crate::data_model::rc::IpAddressOrRange],
) -> Vec<(Vec<u8>, Vec<u8>)> {
let mut intervals = Vec::new();
for item in items {
match item {
IpAddressOrRange::Prefix(p) => intervals.push(prefix_to_range(p)),
IpAddressOrRange::Range(r) => intervals.push((r.min.clone(), r.max.clone())),
}
}
intervals.sort_by(|(a, _), (b, _)| a.cmp(b));
merge_ip_intervals_in_place(&mut intervals);
intervals
}
fn intersect_ip_items_with_parent_intervals(
items: &[crate::data_model::rc::IpAddressOrRange],
parent_intervals: &[(Vec<u8>, Vec<u8>)],
) -> Vec<(Vec<u8>, Vec<u8>)> {
let child_intervals = ip_items_to_merged_intervals(items);
let mut out = Vec::new();
let mut parent_index = 0usize;
for (child_min, child_max) in &child_intervals {
while parent_index < parent_intervals.len()
&& parent_intervals[parent_index].1.as_slice() < child_min.as_slice()
{
parent_index += 1;
}
let mut scan = parent_index;
while scan < parent_intervals.len()
&& parent_intervals[scan].0.as_slice() <= child_max.as_slice()
{
let (parent_min, parent_max) = &parent_intervals[scan];
let min = if bytes_leq(child_min, parent_min) {
parent_min.clone()
} else {
child_min.clone()
};
let max = if bytes_leq(child_max, parent_max) {
child_max.clone()
} else {
parent_max.clone()
};
if bytes_leq(&min, &max) {
out.push((min, max));
}
scan += 1;
}
}
merge_ip_intervals_in_place(&mut out);
out
}
fn ip_intervals_to_ranges(
afi: crate::data_model::rc::Afi,
intervals: &[(Vec<u8>, Vec<u8>)],
) -> Vec<crate::data_model::rc::IpAddressOrRange> {
intervals
.iter()
.map(|(min, max)| {
IpAddressOrRange::Range(crate::data_model::rc::IpAddressRange {
min: normalize_ip_bytes(afi, min),
max: normalize_ip_bytes(afi, max),
})
})
.collect()
}
fn normalize_ip_bytes(afi: crate::data_model::rc::Afi, bytes: &[u8]) -> Vec<u8> {
let target_len = afi.octets_len();
if bytes.len() == target_len {
return bytes.to_vec();
}
let mut out = vec![0u8; target_len];
let copy_len = bytes.len().min(target_len);
out[..copy_len].copy_from_slice(&bytes[..copy_len]);
out
}
fn intersect_as_intervals(child: &[(u32, u32)], parent: &[(u32, u32)]) -> Vec<(u32, u32)> {
let mut out = Vec::new();
let mut parent_index = 0usize;
for (child_min, child_max) in child {
while parent_index < parent.len() && parent[parent_index].1 < *child_min {
parent_index += 1;
}
let mut scan = parent_index;
while scan < parent.len() && parent[scan].0 <= *child_max {
let min = (*child_min).max(parent[scan].0);
let max = (*child_max).min(parent[scan].1);
if min <= max {
out.push((min, max));
}
scan += 1;
}
}
merge_as_intervals(&out)
}
fn as_intervals_to_items(intervals: &[(u32, u32)]) -> Vec<crate::data_model::rc::AsIdOrRange> {
intervals
.iter()
.map(|(min, max)| {
if min == max {
crate::data_model::rc::AsIdOrRange::Id(*min)
} else {
crate::data_model::rc::AsIdOrRange::Range {
min: *min,
max: *max,
}
}
})
.collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum AfiKey {
V4,
@@ -4724,6 +5031,120 @@ mod tests {
assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset));
}
#[test]
fn validation_update_03_ee_resources_reduce_to_vrs_for_roa_checks() {
let roa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
let roa_der = std::fs::read(roa_path).expect("read roa");
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
let ee = &roa.signed_object.signed_data.certificates[0].resource_cert;
let ee_ip = ee
.tbs
.extensions
.ip_resources
.as_ref()
.expect("fixture EE has IP resources");
let first_family = ee_ip.families.first().expect("family");
let first_item = match &first_family.choice {
IpAddressChoice::AddressesOrRanges(items) => items.first().expect("item").clone(),
IpAddressChoice::Inherit => panic!("fixture should not inherit"),
};
let issuer_ip = IpResourceSet {
families: vec![IpAddressFamily {
afi: first_family.afi,
choice: IpAddressChoice::AddressesOrRanges(vec![first_item]),
}],
};
let idx = build_issuer_resources_index(Some(&issuer_ip), None);
let strict_err = validate_ee_resources_for_mode(
ee,
Some(&issuer_ip),
None,
&idx,
ResourceValidationMode::Rfc6487,
)
.unwrap_err();
assert!(matches!(
strict_err,
ObjectValidateError::EeResourcesNotSubset
));
let vrs = validate_ee_resources_for_mode(
ee,
Some(&issuer_ip),
None,
&idx,
ResourceValidationMode::ValidationUpdate03,
)
.expect("vrs resource resolution");
assert_eq!(vrs.ip.expect("vrs ip").families.len(), 1);
}
#[test]
fn validation_update_03_ee_as_vrs_filters_aspa_customer_resources() {
let issuer_as = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Range {
min: 64500,
max: 64510,
},
])),
rdi: None,
};
let child_as = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Range {
min: 64505,
max: 64520,
},
])),
rdi: None,
};
let idx = build_issuer_resources_index(None, Some(&issuer_as));
let vrs = intersect_ee_as_resources_vrs(&child_as, Some(&issuer_as), &idx)
.expect("intersect AS resources");
assert!(as_resource_set_contains_asn(&vrs, 64505));
assert!(as_resource_set_contains_asn(&vrs, 64510));
assert!(!as_resource_set_contains_asn(&vrs, 64511));
}
#[test]
fn validation_update_03_empty_ee_vrs_rejects_roa_and_aspa_outputs() {
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 empty_ip_vrs = IpResourceSet {
families: Vec::new(),
};
let roa_err = roa_to_vrps_with_vrs(&roa, Some(&empty_ip_vrs)).unwrap_err();
assert!(matches!(roa_err, ObjectValidateError::EeResourcesNotSubset));
let missing_ip_err = roa_to_vrps_with_vrs(&roa, None).unwrap_err();
assert!(matches!(
missing_ip_err,
ObjectValidateError::EeResourcesNotSubset
));
let aspa_der = fixture_bytes(
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
);
let aspa = AspaObject::decode_der(&aspa_der).expect("decode aspa");
let empty_as_vrs = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(Vec::new())),
rdi: None,
};
let aspa_err = validate_aspa_customer_in_vrs(&aspa, Some(&empty_as_vrs)).unwrap_err();
assert!(matches!(
aspa_err,
ObjectValidateError::EeResourcesNotSubset
));
let missing_as_err = validate_aspa_customer_in_vrs(&aspa, None).unwrap_err();
assert!(matches!(
missing_as_err,
ObjectValidateError::EeResourcesNotSubset
));
}
#[test]
fn extra_rfc_refs_for_crl_selection_distinguishes_crl_errors() {
assert_eq!(
@@ -4976,11 +5397,13 @@ mod tests {
issuer_resources_index: Arc::new(IssuerResourcesIndex::default()),
issuer_effective_ip: None,
issuer_effective_as: None,
resource_validation_mode: ResourceValidationMode::default(),
}),
validation_time: OffsetDateTime::now_utc(),
collect_vcir_local_outputs: false,
strict_cms_der: false,
strict_name: false,
resource_validation_mode: ResourceValidationMode::default(),
roa_task_indices: vec![0, 1],
cached_roa_results: Vec::new(),
roa_cache_stats: RoaValidationCacheStats::default(),