20260324 完成和routinator对齐snapshot delta replay correctness一致
This commit is contained in:
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user