20260324 完成和routinator对齐snapshot delta replay correctness一致

This commit is contained in:
2026-03-24 10:10:04 +08:00
parent 557a69cbd2
commit d6d44669b4
23 changed files with 2043 additions and 223 deletions

View File

@@ -8,7 +8,7 @@ use crate::data_model::rc::{
use x509_parser::prelude::{FromDer, X509Certificate};
use crate::validation::x509_name::x509_names_equivalent;
use std::collections::HashSet;
use std::collections::{BTreeMap, HashMap, HashSet};
use x509_parser::x509::SubjectPublicKeyInfo;
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -27,6 +27,44 @@ pub struct ValidatedSubordinateCaLite {
pub effective_as_resources: Option<AsResourceSet>,
}
#[derive(Clone, Debug, Default)]
pub struct IssuerEffectiveResourcesIndex {
parent_ip_by_afi_items: Option<BTreeMap<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>>>,
parent_ip_merged_intervals: HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>>,
parent_asnum_intervals: Option<Vec<(u32, u32)>>,
parent_rdi_intervals: Option<Vec<(u32, u32)>>,
}
impl IssuerEffectiveResourcesIndex {
pub fn from_effective_resources(
issuer_effective_ip: Option<&IpResourceSet>,
issuer_effective_as: Option<&AsResourceSet>,
) -> Result<Self, CaPathError> {
let parent_ip_by_afi_items = issuer_effective_ip.map(ip_resources_by_afi_items).transpose()?;
let parent_ip_merged_intervals = issuer_effective_ip
.map(ip_resources_to_merged_intervals_by_afi)
.unwrap_or_default();
let parent_asnum_intervals = issuer_effective_as.and_then(|resources| {
resources
.asnum
.as_ref()
.map(as_choice_to_merged_intervals)
});
let parent_rdi_intervals = issuer_effective_as.and_then(|resources| {
resources.rdi.as_ref().map(as_choice_to_merged_intervals)
});
Ok(Self {
parent_ip_by_afi_items,
parent_ip_merged_intervals,
parent_asnum_intervals,
parent_rdi_intervals,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum CaPathError {
#[error("child CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
@@ -231,6 +269,40 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer(
issuer_effective_ip: Option<&IpResourceSet>,
issuer_effective_as: Option<&AsResourceSet>,
validation_time: time::OffsetDateTime,
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
let issuer_resources_index = IssuerEffectiveResourcesIndex::from_effective_resources(
issuer_effective_ip,
issuer_effective_as,
)?;
validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
child_ca_der,
child_ca,
issuer_ca,
issuer_spki,
issuer_crl,
issuer_crl_revoked_serials,
issuer_ca_rsync_uri,
issuer_crl_rsync_uri,
issuer_effective_ip,
issuer_effective_as,
&issuer_resources_index,
validation_time,
)
}
pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
child_ca_der: &[u8],
child_ca: ResourceCertificate,
issuer_ca: &ResourceCertificate,
issuer_spki: &SubjectPublicKeyInfo<'_>,
issuer_crl: &RpkixCrl,
issuer_crl_revoked_serials: &HashSet<Vec<u8>>,
issuer_ca_rsync_uri: Option<&str>,
issuer_crl_rsync_uri: &str,
issuer_effective_ip: Option<&IpResourceSet>,
issuer_effective_as: Option<&AsResourceSet>,
issuer_resources_index: &IssuerEffectiveResourcesIndex,
validation_time: time::OffsetDateTime,
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
if child_ca.kind != ResourceCertKind::Ca {
return Err(CaPathError::ChildNotCa);
@@ -277,13 +349,17 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer(
return Err(CaPathError::ChildRevoked);
}
let effective_ip_resources = resolve_child_ip_resources(
let effective_ip_resources = resolve_child_ip_resources_indexed(
child_ca.tbs.extensions.ip_resources.as_ref(),
issuer_effective_ip,
issuer_resources_index.parent_ip_by_afi_items.as_ref(),
&issuer_resources_index.parent_ip_merged_intervals,
)?;
let effective_as_resources = resolve_child_as_resources(
let effective_as_resources = resolve_child_as_resources_indexed(
child_ca.tbs.extensions.as_resources.as_ref(),
issuer_effective_as,
issuer_resources_index.parent_asnum_intervals.as_deref(),
issuer_resources_index.parent_rdi_intervals.as_deref(),
)?;
if effective_ip_resources.is_none() && effective_as_resources.is_none() {
return Err(CaPathError::ResourcesMissing);
@@ -435,12 +511,30 @@ fn is_serial_revoked_by_crl(cert: &ResourceCertificate, crl: &RpkixCrl) -> bool
fn resolve_child_ip_resources(
child_ip: Option<&IpResourceSet>,
issuer_effective: Option<&IpResourceSet>,
) -> Result<Option<IpResourceSet>, CaPathError> {
let precomputed_parent_by_afi = issuer_effective.map(ip_resources_by_afi_items).transpose()?;
let precomputed_parent_intervals = issuer_effective
.map(ip_resources_to_merged_intervals_by_afi)
.unwrap_or_default();
resolve_child_ip_resources_indexed(
child_ip,
issuer_effective,
precomputed_parent_by_afi.as_ref(),
&precomputed_parent_intervals,
)
}
fn resolve_child_ip_resources_indexed(
child_ip: Option<&IpResourceSet>,
issuer_effective: Option<&IpResourceSet>,
parent_by_afi: Option<&BTreeMap<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>>>,
parent_intervals_by_afi: &HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>>,
) -> Result<Option<IpResourceSet>, CaPathError> {
let Some(child_ip) = child_ip else {
return Ok(None);
};
let Some(parent) = issuer_effective else {
let Some(_parent) = issuer_effective else {
if child_ip.has_any_inherit() {
return Err(CaPathError::InheritWithoutParentResources);
}
@@ -449,7 +543,7 @@ fn resolve_child_ip_resources(
};
// Resolve per-AFI inherit, producing an effective set with no inherit.
let parent_by_afi = ip_resources_by_afi_items(parent)?;
let parent_by_afi = parent_by_afi.ok_or(CaPathError::InheritWithoutParentResources)?;
let mut out_families: Vec<crate::data_model::rc::IpAddressFamily> = Vec::new();
for fam in &child_ip.families {
@@ -465,9 +559,11 @@ 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));
if !ip_family_items_subset(items, &parent_set) {
let parent_intervals = parent_intervals_by_afi
.get(&fam.afi)
.map(Vec::as_slice)
.unwrap_or(&[]);
if !ip_family_items_subset_with_parent_intervals(items, parent_intervals) {
return Err(CaPathError::ResourcesNotSubset);
}
out_families.push(crate::data_model::rc::IpAddressFamily {
@@ -486,6 +582,29 @@ fn resolve_child_ip_resources(
fn resolve_child_as_resources(
child_as: Option<&AsResourceSet>,
issuer_effective: Option<&AsResourceSet>,
) -> Result<Option<AsResourceSet>, CaPathError> {
let precomputed_asnum = issuer_effective.and_then(|resources| {
resources
.asnum
.as_ref()
.map(as_choice_to_merged_intervals)
});
let precomputed_rdi = issuer_effective.and_then(|resources| {
resources.rdi.as_ref().map(as_choice_to_merged_intervals)
});
resolve_child_as_resources_indexed(
child_as,
issuer_effective,
precomputed_asnum.as_deref(),
precomputed_rdi.as_deref(),
)
}
fn resolve_child_as_resources_indexed(
child_as: Option<&AsResourceSet>,
issuer_effective: Option<&AsResourceSet>,
parent_asnum_intervals: Option<&[(u32, u32)]>,
parent_rdi_intervals: Option<&[(u32, u32)]>,
) -> Result<Option<AsResourceSet>, CaPathError> {
let Some(child_as) = child_as else {
return Ok(None);
@@ -507,7 +626,11 @@ fn resolve_child_as_resources(
.ok_or(CaPathError::InheritWithoutParentResources)
.map(Some)?,
Some(_) => {
if !as_choice_subset(child_as.asnum.as_ref(), parent.asnum.as_ref()) {
if !as_choice_subset_with_parent_intervals(
child_as.asnum.as_ref(),
parent.asnum.as_ref(),
parent_asnum_intervals,
) {
return Err(CaPathError::ResourcesNotSubset);
}
child_as.asnum.clone()
@@ -522,7 +645,11 @@ fn resolve_child_as_resources(
.ok_or(CaPathError::InheritWithoutParentResources)
.map(Some)?,
Some(_) => {
if !as_choice_subset(child_as.rdi.as_ref(), parent.rdi.as_ref()) {
if !as_choice_subset_with_parent_intervals(
child_as.rdi.as_ref(),
parent.rdi.as_ref(),
parent_rdi_intervals,
) {
return Err(CaPathError::ResourcesNotSubset);
}
child_as.rdi.clone()
@@ -535,6 +662,14 @@ fn resolve_child_as_resources(
fn as_choice_subset(
child: Option<&AsIdentifierChoice>,
parent: Option<&AsIdentifierChoice>,
) -> bool {
as_choice_subset_with_parent_intervals(child, parent, None)
}
fn as_choice_subset_with_parent_intervals(
child: Option<&AsIdentifierChoice>,
parent: Option<&AsIdentifierChoice>,
parent_intervals_hint: Option<&[(u32, u32)]>,
) -> bool {
let Some(child) = child else {
return true;
@@ -552,9 +687,16 @@ fn as_choice_subset(
}
let child_intervals = as_choice_to_merged_intervals(child);
let parent_intervals = as_choice_to_merged_intervals(parent);
let owned_parent_intervals;
let parent_intervals = match parent_intervals_hint {
Some(intervals) => intervals,
None => {
owned_parent_intervals = as_choice_to_merged_intervals(parent);
owned_parent_intervals.as_slice()
}
};
for (cmin, cmax) in &child_intervals {
if !as_interval_is_covered(&parent_intervals, *cmin, *cmax) {
if !as_interval_is_covered(parent_intervals, *cmin, *cmax) {
return false;
}
}
@@ -692,21 +834,33 @@ fn ip_family_items_subset(
fn ip_resources_to_merged_intervals(
set: &IpResourceSet,
) -> std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> {
let mut m: std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> =
std::collections::HashMap::new();
let m = ip_resources_to_merged_intervals_by_afi(set);
m.into_iter()
.map(|(afi, ranges)| {
(
match afi {
crate::data_model::rc::Afi::Ipv4 => AfiKey::V4,
crate::data_model::rc::Afi::Ipv6 => AfiKey::V6,
},
ranges,
)
})
.collect()
}
fn ip_resources_to_merged_intervals_by_afi(
set: &IpResourceSet,
) -> HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>> {
let mut m: HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>> = HashMap::new();
for fam in &set.families {
let afi = match fam.afi {
crate::data_model::rc::Afi::Ipv4 => AfiKey::V4,
crate::data_model::rc::Afi::Ipv6 => AfiKey::V6,
};
match &fam.choice {
IpAddressChoice::Inherit => {
// When used in subset checks, treat inherit as "all" by leaving it absent.
// Resolution should have happened earlier.
}
IpAddressChoice::AddressesOrRanges(items) => {
let ent = m.entry(afi).or_default();
let ent = m.entry(fam.afi).or_default();
for item in items {
match item {
crate::data_model::rc::IpAddressOrRange::Prefix(p) => {
@@ -730,6 +884,36 @@ fn ip_resources_to_merged_intervals(
m
}
fn ip_family_items_subset_with_parent_intervals(
child_items: &[crate::data_model::rc::IpAddressOrRange],
parent_intervals: &[(Vec<u8>, Vec<u8>)],
) -> bool {
if parent_intervals.is_empty() {
return false;
}
let mut child_intervals: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
for item in child_items {
match item {
crate::data_model::rc::IpAddressOrRange::Prefix(p) => {
child_intervals.push(prefix_to_range(p))
}
crate::data_model::rc::IpAddressOrRange::Range(r) => {
child_intervals.push((r.min.clone(), r.max.clone()))
}
}
}
child_intervals.sort_by(|(a, _), (b, _)| a.cmp(b));
let child_intervals = merge_ip_intervals(&child_intervals);
for (cmin, cmax) in &child_intervals {
if !interval_is_covered(parent_intervals, cmin, cmax) {
return false;
}
}
true
}
fn merge_ip_intervals(v: &[(Vec<u8>, Vec<u8>)]) -> Vec<(Vec<u8>, Vec<u8>)> {
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
for (min, max) in v {
@@ -790,7 +974,8 @@ mod tests {
use super::*;
use crate::data_model::common::X509NameDer;
use crate::data_model::rc::{
Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpResourceSet,
Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
IpAddressOrRange, IpResourceSet,
};
use crate::data_model::rc::{
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
@@ -1268,6 +1453,105 @@ mod tests {
);
}
#[test]
fn issuer_effective_resources_index_and_indexed_resolvers_cover_success_and_failure_paths() {
use crate::data_model::rc::{AsIdOrRange, IpPrefix};
let parent_ip = IpResourceSet {
families: vec![IpAddressFamily {
afi: Afi::Ipv4,
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
IpPrefix {
afi: Afi::Ipv4,
prefix_len: 8,
addr: vec![10, 0, 0, 0],
},
)]),
}],
};
let parent_as = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
min: 64500,
max: 64599,
}])),
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(65000)])),
};
let idx = IssuerEffectiveResourcesIndex::from_effective_resources(Some(&parent_ip), Some(&parent_as))
.expect("index builds");
assert_eq!(idx.parent_ip_by_afi_items.as_ref().map(|v| v.len()), Some(1));
assert_eq!(idx.parent_ip_merged_intervals.len(), 1);
assert_eq!(idx.parent_asnum_intervals.as_ref().map(|v| v.len()), Some(1));
assert_eq!(idx.parent_rdi_intervals.as_ref().map(|v| v.len()), Some(1));
let child_ip_subset = IpResourceSet {
families: vec![IpAddressFamily {
afi: Afi::Ipv4,
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
IpPrefix {
afi: Afi::Ipv4,
prefix_len: 16,
addr: vec![10, 1, 0, 0],
},
)]),
}],
};
assert!(resolve_child_ip_resources_indexed(
Some(&child_ip_subset),
Some(&parent_ip),
idx.parent_ip_by_afi_items.as_ref(),
&idx.parent_ip_merged_intervals,
)
.expect("subset should resolve")
.is_some());
let child_ip_bad = IpResourceSet {
families: vec![IpAddressFamily {
afi: Afi::Ipv4,
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
IpPrefix {
afi: Afi::Ipv4,
prefix_len: 16,
addr: vec![11, 0, 0, 0],
},
)]),
}],
};
let err = resolve_child_ip_resources_indexed(
Some(&child_ip_bad),
Some(&parent_ip),
idx.parent_ip_by_afi_items.as_ref(),
&idx.parent_ip_merged_intervals,
)
.unwrap_err();
assert!(matches!(err, CaPathError::ResourcesNotSubset));
let child_as_subset = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64542)])),
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(65000)])),
};
assert!(resolve_child_as_resources_indexed(
Some(&child_as_subset),
Some(&parent_as),
idx.parent_asnum_intervals.as_deref(),
idx.parent_rdi_intervals.as_deref(),
)
.expect("subset as resolves")
.is_some());
let child_as_bad = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(65123)])),
rdi: None,
};
let err = resolve_child_as_resources_indexed(
Some(&child_as_bad),
Some(&parent_as),
idx.parent_asnum_intervals.as_deref(),
idx.parent_rdi_intervals.as_deref(),
)
.unwrap_err();
assert!(matches!(err, CaPathError::ResourcesNotSubset));
}
#[test]
fn resolve_child_ip_and_as_resources_success_paths() {
use crate::data_model::rc::{AsIdOrRange, IpAddressOrRange, IpPrefix};