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};

View File

@@ -193,6 +193,59 @@ pub fn validate_ee_cert_path_with_prevalidated_issuer(
validation_time: time::OffsetDateTime,
) -> Result<ResourceCertificate, CertPathError> {
let ee = ResourceCertificate::decode_der(ee_cert_der)?;
validate_ee_cert_path_components(
&ee,
ee_cert_der,
issuer_ca,
issuer_spki,
issuer_crl,
issuer_crl_revoked_serials,
issuer_ca_rsync_uri,
issuer_crl_rsync_uri,
validation_time,
)?;
Ok(ee)
}
/// Validate the EE certificate path using a pre-decoded EE certificate and a pre-validated issuer.
///
/// This avoids re-decoding the embedded EE certificate when the caller already parsed it while
/// decoding a signed object (e.g. ROA / ASPA).
pub fn validate_ee_cert_path_with_predecoded_ee(
ee: &ResourceCertificate,
ee_cert_der: &[u8],
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: Option<&str>,
validation_time: time::OffsetDateTime,
) -> Result<(), CertPathError> {
validate_ee_cert_path_components(
ee,
ee_cert_der,
issuer_ca,
issuer_spki,
issuer_crl,
issuer_crl_revoked_serials,
issuer_ca_rsync_uri,
issuer_crl_rsync_uri,
validation_time,
)
}
fn validate_ee_cert_path_components(
ee: &ResourceCertificate,
ee_cert_der: &[u8],
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: Option<&str>,
validation_time: time::OffsetDateTime,
) -> Result<(), CertPathError> {
if ee.kind != ResourceCertKind::Ee {
return Err(CertPathError::EeNotEe);
}
@@ -207,12 +260,12 @@ pub fn validate_ee_cert_path_with_prevalidated_issuer(
});
}
validate_ee_aki_matches_issuer_ski(&ee, issuer_ca)?;
validate_ee_aki_matches_issuer_ski(ee, issuer_ca)?;
if let Some(expected_issuer_uri) = issuer_ca_rsync_uri {
validate_ee_aia_points_to_issuer_uri(&ee, expected_issuer_uri)?;
validate_ee_aia_points_to_issuer_uri(ee, expected_issuer_uri)?;
}
if let Some(expected_crl_uri) = issuer_crl_rsync_uri {
validate_ee_crldp_contains_issuer_crl_uri(&ee, expected_crl_uri)?;
validate_ee_crldp_contains_issuer_crl_uri(ee, expected_crl_uri)?;
}
if !time_within_validity(
@@ -240,7 +293,7 @@ pub fn validate_ee_cert_path_with_prevalidated_issuer(
return Err(CertPathError::EeRevoked);
}
Ok(ee)
Ok(())
}
fn parse_subject_pki_from_der(der: &[u8]) -> Result<SubjectPublicKeyInfo<'_>, CertPathError> {

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::PackFile;
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_prevalidated_issuer};
use crate::storage::{PackFile, PackTime, VcirLocalOutput, VcirOutputType};
use crate::validation::cert_path::{
CertPathError, validate_ee_cert_path_with_predecoded_ee,
};
use crate::validation::manifest::PublicationPointData;
use crate::validation::publication_point::PublicationPointSnapshot;
use x509_parser::prelude::FromDer;
@@ -67,6 +69,7 @@ pub struct AspaAttestation {
pub struct ObjectsOutput {
pub vrps: Vec<Vrp>,
pub aspas: Vec<AspaAttestation>,
pub local_outputs_cache: Vec<VcirLocalOutput>,
pub warnings: Vec<Warning>,
pub stats: ObjectsStats,
pub audit: Vec<ObjectAuditEntry>,
@@ -148,6 +151,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
audit,
@@ -171,6 +175,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
audit,
@@ -188,6 +193,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
audit,
@@ -246,6 +252,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
audit,
@@ -254,12 +261,14 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
let mut vrps: Vec<Vrp> = Vec::new();
let mut aspas: Vec<AspaAttestation> = Vec::new();
let mut local_outputs_cache: Vec<VcirLocalOutput> = Vec::new();
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(
file,
manifest_rsync_uri,
issuer_ca_der,
&issuer_ca,
&issuer_spki,
@@ -271,9 +280,10 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
validation_time,
timing,
) {
Ok(mut out) => {
Ok((mut out, local_outputs)) => {
stats.roa_ok += 1;
vrps.append(&mut out);
local_outputs_cache.extend(local_outputs);
audit.push(ObjectAuditEntry {
rsync_uri: file.rsync_uri.clone(),
sha256_hex: sha256_hex_from_32(&file.sha256),
@@ -346,6 +356,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
audit,
@@ -357,6 +368,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
let _t = timing.as_ref().map(|t| t.span_phase("objects_aspa_total"));
match process_aspa_with_issuer(
file,
manifest_rsync_uri,
issuer_ca_der,
&issuer_ca,
&issuer_spki,
@@ -368,9 +380,10 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
validation_time,
timing,
) {
Ok(att) => {
Ok((att, local_output)) => {
stats.aspa_ok += 1;
aspas.push(att);
local_outputs_cache.push(local_output);
audit.push(ObjectAuditEntry {
rsync_uri: file.rsync_uri.clone(),
sha256_hex: sha256_hex_from_32(&file.sha256),
@@ -443,6 +456,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
audit,
@@ -456,6 +470,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
ObjectsOutput {
vrps,
aspas,
local_outputs_cache,
warnings,
stats,
audit,
@@ -537,6 +552,7 @@ enum ObjectValidateError {
fn process_roa_with_issuer(
file: &PackFile,
manifest_rsync_uri: &str,
issuer_ca_der: &[u8],
issuer_ca: &ResourceCertificate,
issuer_spki: &SubjectPublicKeyInfo<'_>,
@@ -547,7 +563,7 @@ fn process_roa_with_issuer(
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
validation_time: time::OffsetDateTime,
timing: Option<&TimingHandle>,
) -> Result<Vec<Vrp>, ObjectValidateError> {
) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>), ObjectValidateError> {
let _decode = timing
.as_ref()
.map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
@@ -566,9 +582,9 @@ fn process_roa_with_issuer(
roa.signed_object.verify()?;
drop(_verify);
let ee = &roa.signed_object.signed_data.certificates[0].resource_cert;
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
let ee_crldp_uris = ee
.tbs
.extensions
.crl_distribution_points_uris
@@ -579,7 +595,8 @@ fn process_roa_with_issuer(
let _cert_path = timing
.as_ref()
.map(|t| t.span_phase("objects_roa_validate_ee_cert_path_total"));
let ee = validate_ee_cert_path_with_prevalidated_issuer(
validate_ee_cert_path_with_predecoded_ee(
ee,
ee_der,
issuer_ca,
issuer_spki,
@@ -602,11 +619,52 @@ fn process_roa_with_issuer(
)?;
drop(_subset);
Ok(roa_to_vrps(&roa))
let vrps = roa_to_vrps(&roa);
let source_object_hash = sha256_hex_from_32(&file.sha256);
let source_ee_cert_hash = crate::audit::sha256_hex(ee.raw_der.as_slice());
let item_effective_until = PackTime::from_utc_offset_datetime(ee.tbs.validity_not_after);
let local_outputs = vrps
.iter()
.map(|vrp| {
let prefix = vrp_prefix_to_string(vrp);
let payload_json = serde_json::json!({
"asn": vrp.asn,
"prefix": prefix,
"max_length": vrp.max_length,
})
.to_string();
let rule_hash = crate::audit::sha256_hex(
format!(
"roa-rule:{}:{}:{}:{}",
source_object_hash, vrp.asn, prefix, vrp.max_length
)
.as_bytes(),
);
VcirLocalOutput {
output_id: rule_hash.clone(),
output_type: VcirOutputType::Vrp,
item_effective_until: item_effective_until.clone(),
source_object_uri: file.rsync_uri.clone(),
source_object_type: "roa".to_string(),
source_object_hash: source_object_hash.clone(),
source_ee_cert_hash: source_ee_cert_hash.clone(),
payload_json,
rule_hash,
validation_path_hint: vec![
manifest_rsync_uri.to_string(),
file.rsync_uri.clone(),
source_object_hash.clone(),
],
}
})
.collect();
Ok((vrps, local_outputs))
}
fn process_aspa_with_issuer(
file: &PackFile,
manifest_rsync_uri: &str,
issuer_ca_der: &[u8],
issuer_ca: &ResourceCertificate,
issuer_spki: &SubjectPublicKeyInfo<'_>,
@@ -617,7 +675,7 @@ fn process_aspa_with_issuer(
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
validation_time: time::OffsetDateTime,
timing: Option<&TimingHandle>,
) -> Result<AspaAttestation, ObjectValidateError> {
) -> Result<(AspaAttestation, VcirLocalOutput), ObjectValidateError> {
let _decode = timing
.as_ref()
.map(|t| t.span_phase("objects_aspa_decode_and_validate_total"));
@@ -636,9 +694,9 @@ fn process_aspa_with_issuer(
aspa.signed_object.verify()?;
drop(_verify);
let ee = &aspa.signed_object.signed_data.certificates[0].resource_cert;
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
let ee_crldp_uris = ee
.tbs
.extensions
.crl_distribution_points_uris
@@ -649,7 +707,8 @@ fn process_aspa_with_issuer(
let _cert_path = timing
.as_ref()
.map(|t| t.span_phase("objects_aspa_validate_ee_cert_path_total"));
let ee = validate_ee_cert_path_with_prevalidated_issuer(
validate_ee_cert_path_with_predecoded_ee(
ee,
ee_der,
issuer_ca,
issuer_spki,
@@ -672,10 +731,70 @@ fn process_aspa_with_issuer(
)?;
drop(_subset);
Ok(AspaAttestation {
let attestation = AspaAttestation {
customer_as_id: aspa.aspa.customer_as_id,
provider_as_ids: aspa.aspa.provider_as_ids.clone(),
})
};
let source_object_hash = sha256_hex_from_32(&file.sha256);
let source_ee_cert_hash = crate::audit::sha256_hex(ee.raw_der.as_slice());
let item_effective_until = PackTime::from_utc_offset_datetime(ee.tbs.validity_not_after);
let providers = attestation
.provider_as_ids
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(",");
let rule_hash = crate::audit::sha256_hex(
format!(
"aspa-rule:{}:{}:{}",
source_object_hash, attestation.customer_as_id, providers
)
.as_bytes(),
);
let local_output = VcirLocalOutput {
output_id: rule_hash.clone(),
output_type: VcirOutputType::Aspa,
item_effective_until,
source_object_uri: file.rsync_uri.clone(),
source_object_type: "aspa".to_string(),
source_object_hash: source_object_hash.clone(),
source_ee_cert_hash,
payload_json: serde_json::json!({
"customer_as_id": attestation.customer_as_id,
"provider_as_ids": attestation.provider_as_ids,
})
.to_string(),
rule_hash,
validation_path_hint: vec![
manifest_rsync_uri.to_string(),
file.rsync_uri.clone(),
source_object_hash,
],
};
Ok((attestation, local_output))
}
fn vrp_prefix_to_string(vrp: &Vrp) -> String {
let prefix = &vrp.prefix;
match prefix.afi {
RoaAfi::Ipv4 => {
let addr = std::net::Ipv4Addr::new(
prefix.addr[0],
prefix.addr[1],
prefix.addr[2],
prefix.addr[3],
);
format!("{addr}/{}", prefix.prefix_len)
}
RoaAfi::Ipv6 => {
let mut octets = [0u8; 16];
octets.copy_from_slice(&prefix.addr[..16]);
let addr = std::net::Ipv6Addr::from(octets);
format!("{addr}/{}", prefix.prefix_len)
}
}
}
fn choose_crl_uri_for_certificate<'a>(

View File

@@ -15,7 +15,7 @@ use crate::replay::archive::ReplayArchiveIndex;
use crate::replay::delta_archive::ReplayDeltaArchiveIndex;
use crate::report::{RfcRef, Warning};
use crate::storage::{
AuditRuleIndexEntry, AuditRuleKind, PackFile, PackTime, RawByHashEntry, RocksStore,
PackFile, PackTime, RawByHashEntry, RocksStore,
ValidatedCaInstanceResult, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus,
VcirAuditSummary, VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirOutputType,
VcirRelatedArtifact, VcirSummary,
@@ -26,7 +26,8 @@ use crate::sync::repo::{
use crate::sync::rrdp::Fetcher;
use crate::validation::ca_instance::ca_instance_uris_from_ca_certificate;
use crate::validation::ca_path::{
CaPathError, ValidatedSubordinateCaLite, validate_subordinate_ca_cert_with_prevalidated_issuer,
CaPathError, IssuerEffectiveResourcesIndex, ValidatedSubordinateCaLite,
validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources,
};
use crate::validation::manifest::{
ManifestFreshError, PublicationPointData, PublicationPointSource,
@@ -483,6 +484,11 @@ fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
let mut out: Vec<DiscoveredChildCaInstance> = Vec::new();
let mut audits: Vec<ObjectAuditEntry> = Vec::new();
let issuer_resources_index = IssuerEffectiveResourcesIndex::from_effective_resources(
issuer.effective_ip_resources.as_ref(),
issuer.effective_as_resources.as_ref(),
)
.map_err(|e| format!("build issuer effective resources index failed: {e}"))?;
let mut cer_seen: u64 = 0;
let mut ca_skipped_not_ca: u64 = 0;
@@ -628,6 +634,7 @@ fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
issuer.ca_certificate_rsync_uri.as_deref(),
issuer.effective_ip_resources.as_ref(),
issuer.effective_as_resources.as_ref(),
&issuer_resources_index,
validation_time,
) {
Ok(v) => v,
@@ -839,11 +846,12 @@ fn validate_subordinate_ca_cert_with_cached_issuer(
issuer_ca_rsync_uri: Option<&str>,
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
issuer_resources_index: &IssuerEffectiveResourcesIndex,
validation_time: time::OffsetDateTime,
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?;
validate_subordinate_ca_cert_with_prevalidated_issuer(
validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
child_ca_der,
child_ca,
issuer_ca,
@@ -854,6 +862,7 @@ fn validate_subordinate_ca_cert_with_cached_issuer(
issuer_crl_rsync_uri,
issuer_effective_ip,
issuer_effective_as,
issuer_resources_index,
validation_time,
)
}
@@ -1172,6 +1181,7 @@ fn empty_objects_output() -> crate::validation::objects::ObjectsOutput {
crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: Vec::new(),
@@ -1669,36 +1679,8 @@ fn persist_vcir_for_fresh_result(
.map_err(|e| format!("load existing VCIR failed: {e}"))?;
store
.put_vcir(&vcir)
.map_err(|e| format!("store VCIR failed: {e}"))?;
if let Some(previous) = previous.as_ref() {
for output in &previous.local_outputs {
if let Some(kind) = audit_rule_kind_for_output(output.output_type) {
store
.delete_audit_rule_index_entry(kind, &output.rule_hash)
.map_err(|e| format!("delete stale audit rule index failed: {e}"))?;
}
}
}
for output in &vcir.local_outputs {
let Some(kind) = audit_rule_kind_for_output(output.output_type) else {
continue;
};
let entry = AuditRuleIndexEntry {
kind,
rule_hash: output.rule_hash.clone(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
source_object_uri: output.source_object_uri.clone(),
source_object_hash: output.source_object_hash.clone(),
output_id: output.output_id.clone(),
item_effective_until: output.item_effective_until.clone(),
};
store
.put_audit_rule_index_entry(&entry)
.map_err(|e| format!("store audit rule index failed: {e}"))?;
}
.replace_vcir_and_audit_rule_indexes(previous.as_ref(), &vcir)
.map_err(|e| format!("store VCIR and audit rule index failed: {e}"))?;
Ok(())
}
@@ -1847,6 +1829,10 @@ fn build_vcir_local_outputs(
pack: &PublicationPointSnapshot,
objects: &crate::validation::objects::ObjectsOutput,
) -> Result<Vec<VcirLocalOutput>, String> {
if !objects.local_outputs_cache.is_empty() {
return Ok(objects.local_outputs_cache.clone());
}
let accepted_roa_uris: HashSet<&str> = objects
.audit
.iter()
@@ -2250,12 +2236,6 @@ fn audit_result_to_vcir_status(result: &AuditObjectResult) -> VcirArtifactValida
}
}
fn audit_rule_kind_for_output(output_type: VcirOutputType) -> Option<AuditRuleKind> {
match output_type {
VcirOutputType::Vrp => Some(AuditRuleKind::Roa),
VcirOutputType::Aspa => Some(AuditRuleKind::Aspa),
}
}
fn roa_to_vrps_for_vcir(roa: &RoaObject) -> Vec<Vrp> {
let asn = roa.roa.as_id;
@@ -2298,7 +2278,7 @@ mod tests {
use crate::fetch::rsync::LocalDirRsyncFetcher;
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
use crate::storage::{
PackFile, PackTime, RawByHashEntry, ValidatedCaInstanceResult, ValidatedManifestMeta,
PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirOutputType, VcirRelatedArtifact,
VcirSummary,
@@ -2529,6 +2509,64 @@ authorityKeyIdentifier = keyid:always
}
}
fn cernet_publication_point_snapshot_for_vcir_tests(
) -> (PublicationPointSnapshot, Vec<u8>, time::OffsetDateTime) {
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/";
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
let manifest_bytes = std::fs::read(dir.join(manifest_file)).expect("read manifest fixture");
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
let candidate = manifest.manifest.this_update + time::Duration::seconds(60);
let validation_time = if candidate < manifest.manifest.next_update {
candidate
} else {
manifest.manifest.this_update
};
let issuer_ca_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
)
.expect("read issuer ca fixture");
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy {
sync_preference: crate::policy::SyncPreference::RsyncOnly,
..Policy::default()
};
sync_publication_point(
&store,
&policy,
None,
rsync_base_uri,
&NeverHttpFetcher,
&LocalDirRsyncFetcher::new(&dir),
None,
None,
)
.expect("sync cernet fixture");
let pp = crate::validation::manifest::process_manifest_publication_point(
&store,
&policy,
&manifest_rsync_uri,
rsync_base_uri,
issuer_ca_der.as_slice(),
Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
),
validation_time,
)
.expect("process manifest publication point");
(pp.snapshot, issuer_ca_der, validation_time)
}
fn sample_vcir_for_projection(
now: time::OffsetDateTime,
child_cert_hash: &str,
@@ -2693,6 +2731,327 @@ authorityKeyIdentifier = keyid:always
);
}
#[test]
fn build_vcir_local_outputs_prefers_cached_outputs() {
let pack = dummy_pack_with_files(vec![]);
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: vec![1],
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let cached = vec![VcirLocalOutput {
output_id: "cached-output".to_string(),
output_type: VcirOutputType::Vrp,
item_effective_until: pack.next_update.clone(),
source_object_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
source_object_type: "roa".to_string(),
source_object_hash: sha256_hex(b"cached-roa"),
source_ee_cert_hash: sha256_hex(b"cached-ee"),
payload_json: "{\"asn\":64500}".to_string(),
rule_hash: sha256_hex(b"cached-rule"),
validation_path_hint: vec![pack.manifest_rsync_uri.clone()],
}];
let outputs = build_vcir_local_outputs(
&ca,
&pack,
&crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: cached.clone(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: Vec::new(),
},
)
.expect("reuse cached outputs");
assert_eq!(outputs, cached);
}
#[test]
fn collect_and_persist_vcir_embedded_evidence_for_real_signed_objects() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
&pack,
&Policy::default(),
issuer_ca_der.as_slice(),
Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"),
issuer_ca.tbs.extensions.ip_resources.as_ref(),
issuer_ca.tbs.extensions.as_resources.as_ref(),
validation_time,
None,
);
assert!(!objects.local_outputs_cache.is_empty(), "expected local outputs from signed objects");
let evidence = collect_vcir_embedded_evidence(&pack, &objects).expect("collect embedded evidence");
assert!(evidence.len() >= 2, "expected manifest EE and signed-object EE evidence");
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: issuer_ca_der.clone(),
ca_certificate_rsync_uri: Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
persist_vcir_non_repository_evidence(&store, &ca, &evidence)
.expect("persist embedded evidence");
let issuer_hash = sha256_hex(&issuer_ca_der);
let issuer_entry = store
.get_raw_by_hash_entry(&issuer_hash)
.expect("load issuer raw entry")
.expect("issuer raw entry present");
assert!(issuer_entry
.origin_uris
.iter()
.any(|uri| uri.ends_with("BfycW4hQb3wNP4YsiJW-1n6fjro.cer")));
for entry in &evidence {
assert!(store
.get_raw_by_hash_entry(&entry.raw_entry.sha256_hex)
.expect("load evidence raw entry")
.is_some());
}
}
#[test]
fn build_vcir_local_outputs_falls_back_to_decoding_accepted_objects_when_cache_is_empty() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
&pack,
&Policy::default(),
issuer_ca_der.as_slice(),
Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"),
issuer_ca.tbs.extensions.ip_resources.as_ref(),
issuer_ca.tbs.extensions.as_resources.as_ref(),
validation_time,
None,
);
let mut objects_without_cache = objects.clone();
objects_without_cache.local_outputs_cache.clear();
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: issuer_ca_der,
ca_certificate_rsync_uri: Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let local_outputs = build_vcir_local_outputs(&ca, &pack, &objects_without_cache)
.expect("rebuild vcir local outputs");
assert!(!local_outputs.is_empty());
assert_eq!(local_outputs.len(), objects.vrps.len());
assert!(local_outputs
.iter()
.all(|output| output.output_type == VcirOutputType::Vrp));
}
#[test]
fn persist_vcir_for_fresh_result_stores_vcir_and_audit_indexes_for_real_snapshot() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
&pack,
&Policy::default(),
issuer_ca_der.as_slice(),
Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"),
issuer_ca.tbs.extensions.ip_resources.as_ref(),
issuer_ca.tbs.extensions.as_resources.as_ref(),
validation_time,
None,
);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: issuer_ca_der.clone(),
ca_certificate_rsync_uri: Some(
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
persist_vcir_for_fresh_result(
&store,
&ca,
&pack,
&objects,
&[],
&[],
&[],
validation_time,
)
.expect("persist vcir for fresh result");
let vcir = store
.get_vcir(&pack.manifest_rsync_uri)
.expect("get vcir")
.expect("vcir exists");
assert_eq!(vcir.manifest_rsync_uri, pack.manifest_rsync_uri);
assert_eq!(vcir.summary.local_vrp_count as usize, objects.vrps.len());
let first_output = vcir.local_outputs.first().expect("local outputs stored");
assert!(store
.get_audit_rule_index_entry(crate::storage::AuditRuleKind::Roa, &first_output.rule_hash)
.expect("get audit rule index entry")
.is_some());
}
#[test]
fn build_vcir_related_artifacts_classifies_snapshot_files_and_audit_statuses() {
let manifest_bytes = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
),
)
.expect("read manifest fixture");
let crl_bytes = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
),
)
.expect("read crl fixture");
let pack = PublicationPointSnapshot {
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_number_be: vec![1],
this_update: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
next_update: PackTime::from_utc_offset_datetime(
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
),
verified_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
manifest_bytes,
files: vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
crl_bytes,
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/child.cer",
vec![1u8, 2],
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/a.roa",
vec![3u8, 4],
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/a.asa",
vec![5u8, 6],
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/a.gbr",
vec![7u8, 8],
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/extra.bin",
vec![9u8],
),
],
};
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: vec![0x11, 0x22],
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
rrdp_notification_uri: None,
};
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: vec![
ObjectAuditEntry {
rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
sha256_hex: sha256_hex_from_32(&pack.files[2].sha256),
kind: AuditObjectKind::Roa,
result: AuditObjectResult::Error,
detail: Some("bad roa".to_string()),
},
ObjectAuditEntry {
rsync_uri: "rsync://example.test/repo/issuer/a.asa".to_string(),
sha256_hex: sha256_hex_from_32(&pack.files[3].sha256),
kind: AuditObjectKind::Aspa,
result: AuditObjectResult::Skipped,
detail: Some("skipped aspa".to_string()),
},
],
};
let embedded = vec![VcirEmbeddedEvidence {
artifact: VcirRelatedArtifact {
artifact_role: VcirArtifactRole::EeCert,
artifact_kind: VcirArtifactKind::Cer,
uri: None,
sha256: sha256_hex(b"embedded-ee"),
object_type: Some("cer".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
},
raw_entry: embedded_raw_entry(sha256_hex(b"embedded-ee"), vec![1u8, 2, 3]),
}];
let artifacts = build_vcir_related_artifacts(
&ca,
&pack,
"rsync://example.test/repo/issuer/issuer.crl",
&objects,
&[],
&embedded,
);
assert!(artifacts.iter().any(|artifact| artifact.artifact_role == VcirArtifactRole::Manifest));
assert!(artifacts.iter().any(|artifact| artifact.artifact_role == VcirArtifactRole::TrustAnchorCert));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/issuer.crl")
&& artifact.artifact_role == VcirArtifactRole::CurrentCrl));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/child.cer")
&& artifact.artifact_role == VcirArtifactRole::ChildCaCert));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/a.roa")
&& artifact.validation_status == VcirArtifactValidationStatus::Rejected));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/a.asa")
&& artifact.validation_status == VcirArtifactValidationStatus::WarningOnly));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/a.gbr")
&& artifact.artifact_kind == VcirArtifactKind::Gbr));
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/extra.bin")
&& artifact.artifact_kind == VcirArtifactKind::Other));
assert!(artifacts.iter().any(|artifact| artifact.uri.is_none() && artifact.sha256 == sha256_hex(b"embedded-ee")));
}
#[test]
fn select_issuer_crl_from_snapshot_reports_missing_crldp_for_self_signed_cert() {
let ta_der = std::fs::read(
@@ -3207,6 +3566,7 @@ authorityKeyIdentifier = keyid:always
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: Vec::new(),
@@ -3267,6 +3627,7 @@ authorityKeyIdentifier = keyid:always
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
audit: vec![ObjectAuditEntry {
@@ -3872,6 +4233,7 @@ authorityKeyIdentifier = keyid:always
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: vec![Warning::new("objects warning")],
stats: crate::validation::objects::ObjectsStats::default(),
audit: vec![ObjectAuditEntry {
@@ -3956,6 +4318,7 @@ authorityKeyIdentifier = keyid:always
&crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: vec![Warning::new("object warning")],
stats: crate::validation::objects::ObjectsStats::default(),
audit: Vec::new(),