初始化 Panda RPKI v0.1.0 开源候选版本
This commit is contained in:
132
src/validation/ca_instance.rs
Normal file
132
src/validation/ca_instance.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use crate::model::oid::{OID_AD_CA_REPOSITORY, OID_AD_RPKI_MANIFEST, OID_AD_RPKI_NOTIFY};
|
||||
use crate::model::rc::{ResourceCertKind, ResourceCertificate, SubjectInfoAccess};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CaInstanceUris {
|
||||
/// rsync sync base URI used for live/materialized publication point fetches (must end with `/`).
|
||||
///
|
||||
/// This is the parent directory of `manifest_rsync_uri`, not necessarily the full
|
||||
/// `id-ad-caRepository` URI. Using the manifest parent keeps sync scope narrow while
|
||||
/// `publication_point_rsync_uri` preserves the full RFC 9286 publication-point value.
|
||||
pub rsync_base_uri: String,
|
||||
/// rsync URI for the manifest object (`.mft`).
|
||||
pub manifest_rsync_uri: String,
|
||||
/// Publication point rsync URI (RFC 9286 terminology).
|
||||
pub publication_point_rsync_uri: String,
|
||||
/// Optional RRDP notification URI (https).
|
||||
pub rrdp_notification_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CaInstanceUrisError {
|
||||
#[error("certificate must be a CA certificate (RFC 6487 §4.8.1)")]
|
||||
NotCa,
|
||||
|
||||
#[error(
|
||||
"CA certificate must contain Subject Information Access extension (RFC 6487 §4.8.8; RFC 5280 §4.2.2.2)"
|
||||
)]
|
||||
MissingSia,
|
||||
|
||||
#[error("CA certificate SIA must contain id-ad-caRepository rsync URI (RFC 6487 §4.8.8.1)")]
|
||||
MissingCaRepository,
|
||||
|
||||
#[error("CA certificate SIA must contain id-ad-rpkiManifest rsync URI (RFC 6487 §4.8.8.2)")]
|
||||
MissingRpkiManifest,
|
||||
|
||||
#[error(
|
||||
"SIA id-ad-caRepository accessLocation must be rsync:// URI, got {0} (RFC 6487 §4.8.8.1)"
|
||||
)]
|
||||
CaRepositoryNotRsync(String),
|
||||
|
||||
#[error(
|
||||
"SIA id-ad-rpkiManifest accessLocation must be rsync:// URI, got {0} (RFC 6487 §4.8.8.2)"
|
||||
)]
|
||||
RpkiManifestNotRsync(String),
|
||||
|
||||
#[error(
|
||||
"SIA id-ad-rpkiNotify accessLocation must be https:// URI, got {0} (RFC 8182 §3.4.1; RFC 6487 §4.8.8.3)"
|
||||
)]
|
||||
RpkiNotifyNotHttps(String),
|
||||
|
||||
#[error(
|
||||
"manifest rsync URI must be under CA publication point: manifest={manifest_rsync_uri} publication_point={publication_point_rsync_uri} (RFC 9286 §6.1)"
|
||||
)]
|
||||
ManifestNotUnderPublicationPoint {
|
||||
manifest_rsync_uri: String,
|
||||
publication_point_rsync_uri: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn ca_instance_uris_from_ca_certificate(
|
||||
cert: &ResourceCertificate,
|
||||
) -> Result<CaInstanceUris, CaInstanceUrisError> {
|
||||
if cert.kind != ResourceCertKind::Ca {
|
||||
return Err(CaInstanceUrisError::NotCa);
|
||||
}
|
||||
|
||||
let sia = cert
|
||||
.tbs
|
||||
.extensions
|
||||
.subject_info_access
|
||||
.as_ref()
|
||||
.ok_or(CaInstanceUrisError::MissingSia)?;
|
||||
|
||||
let access_descriptions = match sia {
|
||||
SubjectInfoAccess::Ca(ca) => &ca.access_descriptions,
|
||||
SubjectInfoAccess::Ee(_ee) => return Err(CaInstanceUrisError::MissingSia),
|
||||
};
|
||||
|
||||
let mut ca_repo: Option<String> = None;
|
||||
let mut manifest: Option<String> = None;
|
||||
let mut notify: Option<String> = None;
|
||||
|
||||
for ad in access_descriptions {
|
||||
if ad.access_method_oid == OID_AD_CA_REPOSITORY {
|
||||
let u = ad.access_location.as_str();
|
||||
if !u.starts_with("rsync://") {
|
||||
return Err(CaInstanceUrisError::CaRepositoryNotRsync(u.to_string()));
|
||||
}
|
||||
ca_repo.get_or_insert(u.to_string());
|
||||
} else if ad.access_method_oid == OID_AD_RPKI_MANIFEST {
|
||||
let u = ad.access_location.as_str();
|
||||
if !u.starts_with("rsync://") {
|
||||
return Err(CaInstanceUrisError::RpkiManifestNotRsync(u.to_string()));
|
||||
}
|
||||
manifest.get_or_insert(u.to_string());
|
||||
} else if ad.access_method_oid == OID_AD_RPKI_NOTIFY {
|
||||
let u = ad.access_location.as_str();
|
||||
if !u.starts_with("https://") {
|
||||
return Err(CaInstanceUrisError::RpkiNotifyNotHttps(u.to_string()));
|
||||
}
|
||||
notify.get_or_insert(u.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let mut publication_point_rsync_uri =
|
||||
ca_repo.ok_or(CaInstanceUrisError::MissingCaRepository)?;
|
||||
if !publication_point_rsync_uri.ends_with('/') {
|
||||
publication_point_rsync_uri.push('/');
|
||||
}
|
||||
|
||||
let manifest_rsync_uri = manifest.ok_or(CaInstanceUrisError::MissingRpkiManifest)?;
|
||||
if !manifest_rsync_uri.starts_with(&publication_point_rsync_uri) {
|
||||
return Err(CaInstanceUrisError::ManifestNotUnderPublicationPoint {
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
});
|
||||
}
|
||||
let manifest_parent = manifest_rsync_uri
|
||||
.rsplit_once('/')
|
||||
.map(|(parent, _)| format!("{parent}/"))
|
||||
.ok_or_else(|| CaInstanceUrisError::ManifestNotUnderPublicationPoint {
|
||||
manifest_rsync_uri: manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: publication_point_rsync_uri.clone(),
|
||||
})?;
|
||||
|
||||
Ok(CaInstanceUris {
|
||||
rsync_base_uri: manifest_parent,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
rrdp_notification_uri: notify,
|
||||
})
|
||||
}
|
||||
11
src/validation/ca_path.rs
Normal file
11
src/validation/ca_path.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
#![allow(clippy::too_many_arguments, clippy::type_complexity)]
|
||||
|
||||
include!("ca_path/types_and_validation.rs");
|
||||
include!("ca_path/certificate_checks.rs");
|
||||
include!("ca_path/resource_resolution.rs");
|
||||
include!("ca_path/ip_resources.rs");
|
||||
include!("ca_path/increment.rs");
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "ca_path/tests.rs"]
|
||||
mod tests;
|
||||
138
src/validation/ca_path/certificate_checks.rs
Normal file
138
src/validation/ca_path/certificate_checks.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
// Certificate profile, signature, CRL, and validity checks.
|
||||
|
||||
fn parse_subject_pki_from_der(der: &[u8]) -> Result<SubjectPublicKeyInfo<'_>, CaPathError> {
|
||||
let (rem, spki) = SubjectPublicKeyInfo::from_der(der)
|
||||
.map_err(|e| CaPathError::IssuerSpkiParse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CaPathError::IssuerSpkiTrailingBytes(rem.len()));
|
||||
}
|
||||
Ok(spki)
|
||||
}
|
||||
|
||||
fn parse_x509_cert(der: &[u8]) -> Result<X509Certificate<'_>, CaPathError> {
|
||||
let (rem, cert) = X509Certificate::from_der(der)
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CaPathError::ChildSignatureInvalid(
|
||||
"trailing bytes after child certificate".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(cert)
|
||||
}
|
||||
|
||||
fn verify_child_signature(
|
||||
child: &X509Certificate<'_>,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
) -> Result<(), CaPathError> {
|
||||
|
||||
child
|
||||
.verify_signature(Some(issuer_spki))
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
||||
}
|
||||
|
||||
fn validate_child_aki_matches_issuer_ski(
|
||||
child: &ResourceCertificate,
|
||||
issuer: &ResourceCertificate,
|
||||
) -> Result<(), CaPathError> {
|
||||
let Some(issuer_ski) = issuer.tbs.extensions.subject_key_identifier.as_deref() else {
|
||||
return Err(CaPathError::IssuerSkiMissing);
|
||||
};
|
||||
let Some(child_aki) = child.tbs.extensions.authority_key_identifier.as_deref() else {
|
||||
return Err(CaPathError::ChildAkiMissing);
|
||||
};
|
||||
if child_aki != issuer_ski {
|
||||
return Err(CaPathError::ChildAkiMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_child_aia_points_to_issuer_uri(
|
||||
child: &ResourceCertificate,
|
||||
issuer_ca_rsync_uri: &str,
|
||||
) -> Result<(), CaPathError> {
|
||||
let Some(uris) = child.tbs.extensions.ca_issuers_uris.as_ref() else {
|
||||
return Err(CaPathError::ChildAiaMissing);
|
||||
};
|
||||
if !uris.iter().any(|u| u.as_str() == issuer_ca_rsync_uri) {
|
||||
return Err(CaPathError::ChildAiaIssuerUriMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_child_crldp_contains_issuer_crl_uri(
|
||||
child: &ResourceCertificate,
|
||||
issuer_crl_rsync_uri: &str,
|
||||
) -> Result<(), CaPathError> {
|
||||
let Some(uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else {
|
||||
return Err(CaPathError::ChildCrlDpMissing);
|
||||
};
|
||||
if !uris.iter().any(|u| u.as_str() == issuer_crl_rsync_uri) {
|
||||
return Err(CaPathError::ChildCrlDpUriMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_child_ca_key_usage(cert: &X509Certificate<'_>) -> Result<(), CaPathError> {
|
||||
let mut ku_critical: Option<bool> = None;
|
||||
for ext in cert.extensions() {
|
||||
if ext.oid.as_bytes() == OID_KEY_USAGE_RAW {
|
||||
ku_critical = Some(ext.critical);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(critical) = ku_critical else {
|
||||
return Err(CaPathError::KeyUsageMissing);
|
||||
};
|
||||
if !critical {
|
||||
return Err(CaPathError::KeyUsageNotCritical);
|
||||
}
|
||||
|
||||
let Some(ku) = cert
|
||||
.key_usage()
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?
|
||||
else {
|
||||
return Err(CaPathError::KeyUsageMissing);
|
||||
};
|
||||
|
||||
let v = &ku.value;
|
||||
let ok = v.key_cert_sign()
|
||||
&& v.crl_sign()
|
||||
&& !v.digital_signature()
|
||||
&& !v.non_repudiation()
|
||||
&& !v.key_encipherment()
|
||||
&& !v.data_encipherment()
|
||||
&& !v.key_agreement()
|
||||
&& !v.encipher_only()
|
||||
&& !v.decipher_only();
|
||||
if !ok {
|
||||
return Err(CaPathError::KeyUsageInvalidBits);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn time_within_validity(
|
||||
t: time::OffsetDateTime,
|
||||
not_before: time::OffsetDateTime,
|
||||
not_after: time::OffsetDateTime,
|
||||
) -> bool {
|
||||
let t = t.to_offset(time::UtcOffset::UTC);
|
||||
let not_before = not_before.to_offset(time::UtcOffset::UTC);
|
||||
let not_after = not_after.to_offset(time::UtcOffset::UTC);
|
||||
t >= not_before && t <= not_after
|
||||
}
|
||||
|
||||
fn crl_valid_at_time(crl: &RpkixCrl, t: time::OffsetDateTime) -> bool {
|
||||
let t = t.to_offset(time::UtcOffset::UTC);
|
||||
let this_update = crl.this_update.utc.to_offset(time::UtcOffset::UTC);
|
||||
let next_update = crl.next_update.utc.to_offset(time::UtcOffset::UTC);
|
||||
t >= this_update && t < next_update
|
||||
}
|
||||
|
||||
fn is_serial_revoked_by_crl(cert: &ResourceCertificate, crl: &RpkixCrl) -> bool {
|
||||
let serial = BigUnsigned::from_biguint(&cert.tbs.serial_number);
|
||||
crl.revoked_certs
|
||||
.iter()
|
||||
.any(|rc| rc.serial_number == serial)
|
||||
}
|
||||
13
src/validation/ca_path/increment.rs
Normal file
13
src/validation/ca_path/increment.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Small byte-vector helper used by resource interval tests.
|
||||
|
||||
fn increment_bytes(v: &[u8]) -> Vec<u8> {
|
||||
let mut out = v.to_vec();
|
||||
for i in (0..out.len()).rev() {
|
||||
if out[i] != 0xFF {
|
||||
out[i] += 1;
|
||||
out[i + 1..].fill(0);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
vec![0u8; out.len()]
|
||||
}
|
||||
274
src/validation/ca_path/ip_resources.rs
Normal file
274
src/validation/ca_path/ip_resources.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
// IP interval normalization, intersection, and coverage helpers.
|
||||
|
||||
fn ip_resources_by_afi_items(
|
||||
set: &IpResourceSet,
|
||||
) -> Result<
|
||||
std::collections::BTreeMap<
|
||||
crate::model::rc::Afi,
|
||||
Vec<crate::model::rc::IpAddressOrRange>,
|
||||
>,
|
||||
CaPathError,
|
||||
> {
|
||||
let mut m: std::collections::BTreeMap<
|
||||
crate::model::rc::Afi,
|
||||
Vec<crate::model::rc::IpAddressOrRange>,
|
||||
> = std::collections::BTreeMap::new();
|
||||
for fam in &set.families {
|
||||
match &fam.choice {
|
||||
IpAddressChoice::Inherit => return Err(CaPathError::InheritWithoutParentResources),
|
||||
IpAddressChoice::AddressesOrRanges(items) => {
|
||||
m.insert(fam.afi, items.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
fn ip_resources_to_merged_intervals_by_afi(
|
||||
set: &IpResourceSet,
|
||||
) -> HashMap<crate::model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>> {
|
||||
let mut m: HashMap<crate::model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>> = HashMap::new();
|
||||
|
||||
for fam in &set.families {
|
||||
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(fam.afi).or_default();
|
||||
for item in items {
|
||||
match item {
|
||||
crate::model::rc::IpAddressOrRange::Prefix(p) => {
|
||||
let (min, max) = prefix_to_range(p);
|
||||
ent.push((min, max));
|
||||
}
|
||||
crate::model::rc::IpAddressOrRange::Range(r) => {
|
||||
ent.push((r.min.clone(), r.max.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (_afi, v) in m.iter_mut() {
|
||||
v.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
*v = merge_ip_intervals(v);
|
||||
}
|
||||
|
||||
m
|
||||
}
|
||||
|
||||
fn ip_family_items_subset_with_parent_intervals(
|
||||
child_items: &[crate::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::model::rc::IpAddressOrRange::Prefix(p) => {
|
||||
child_intervals.push(prefix_to_range(p))
|
||||
}
|
||||
crate::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 ip_items_to_merged_intervals(
|
||||
items: &[crate::model::rc::IpAddressOrRange],
|
||||
) -> Vec<(Vec<u8>, Vec<u8>)> {
|
||||
let mut intervals = Vec::new();
|
||||
for item in items {
|
||||
match item {
|
||||
crate::model::rc::IpAddressOrRange::Prefix(p) => {
|
||||
intervals.push(prefix_to_range(p))
|
||||
}
|
||||
crate::model::rc::IpAddressOrRange::Range(r) => {
|
||||
intervals.push((r.min.clone(), r.max.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
intervals.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
merge_ip_intervals(&intervals)
|
||||
}
|
||||
|
||||
fn intersect_ip_items_with_parent_intervals(
|
||||
items: &[crate::model::rc::IpAddressOrRange],
|
||||
parent_intervals: &[(Vec<u8>, Vec<u8>)],
|
||||
) -> Vec<(Vec<u8>, Vec<u8>)> {
|
||||
let child_intervals = ip_items_to_merged_intervals(items);
|
||||
intersect_ip_intervals(&child_intervals, parent_intervals)
|
||||
}
|
||||
|
||||
fn intersect_ip_intervals(
|
||||
child_intervals: &[(Vec<u8>, Vec<u8>)],
|
||||
parent_intervals: &[(Vec<u8>, Vec<u8>)],
|
||||
) -> Vec<(Vec<u8>, Vec<u8>)> {
|
||||
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(&out)
|
||||
}
|
||||
|
||||
fn ip_intervals_to_ranges(
|
||||
afi: crate::model::rc::Afi,
|
||||
intervals: &[(Vec<u8>, Vec<u8>)],
|
||||
) -> Vec<crate::model::rc::IpAddressOrRange> {
|
||||
intervals
|
||||
.iter()
|
||||
.map(|(min, max)| {
|
||||
crate::model::rc::IpAddressOrRange::Range(crate::model::rc::IpAddressRange {
|
||||
min: normalize_ip_bytes(afi, min),
|
||||
max: normalize_ip_bytes(afi, max),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalize_ip_bytes(afi: crate::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 intervals_changed<T: PartialEq>(left: &[T], right: &[T]) -> bool {
|
||||
left != right
|
||||
}
|
||||
|
||||
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::model::rc::AsIdOrRange> {
|
||||
intervals
|
||||
.iter()
|
||||
.map(|(min, max)| {
|
||||
if min == max {
|
||||
crate::model::rc::AsIdOrRange::Id(*min)
|
||||
} else {
|
||||
crate::model::rc::AsIdOrRange::Range {
|
||||
min: *min,
|
||||
max: *max,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
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 {
|
||||
let Some(last) = out.last_mut() else {
|
||||
out.push((min.clone(), max.clone()));
|
||||
continue;
|
||||
};
|
||||
|
||||
if bytes_leq(min, &increment_bytes(&last.1)) {
|
||||
if bytes_leq(&last.1, max) {
|
||||
last.1 = max.clone();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push((min.clone(), max.clone()));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn interval_is_covered(parent: &[(Vec<u8>, Vec<u8>)], min: &[u8], max: &[u8]) -> bool {
|
||||
for (pmin, pmax) in parent {
|
||||
if bytes_leq(pmin, min) && bytes_leq(max, pmax) {
|
||||
return true;
|
||||
}
|
||||
if pmin.as_slice() > min {
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn prefix_to_range(prefix: &crate::model::rc::IpPrefix) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut min = prefix.addr.clone();
|
||||
let mut max = prefix.addr.clone();
|
||||
|
||||
let bitlen = match prefix.afi {
|
||||
crate::model::rc::Afi::Ipv4 => 32u16,
|
||||
crate::model::rc::Afi::Ipv6 => 128u16,
|
||||
};
|
||||
let plen = prefix.prefix_len.min(bitlen);
|
||||
for bit in plen..bitlen {
|
||||
let byte = (bit / 8) as usize;
|
||||
let offset = 7 - (bit % 8);
|
||||
let mask = 1u8 << offset;
|
||||
min[byte] &= !mask;
|
||||
max[byte] |= mask;
|
||||
}
|
||||
(min, max)
|
||||
}
|
||||
|
||||
fn bytes_leq(a: &[u8], b: &[u8]) -> bool {
|
||||
a <= b
|
||||
}
|
||||
440
src/validation/ca_path/resource_resolution.rs
Normal file
440
src/validation/ca_path/resource_resolution.rs
Normal file
@@ -0,0 +1,440 @@
|
||||
// Effective IP/AS resource resolution and interval helpers.
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ResourceResolution {
|
||||
effective_ip_resources: Option<IpResourceSet>,
|
||||
effective_as_resources: Option<AsResourceSet>,
|
||||
warnings: ResourceValidationWarnings,
|
||||
}
|
||||
|
||||
fn resolve_child_resources(
|
||||
child_ip: Option<&IpResourceSet>,
|
||||
issuer_effective_ip: Option<&IpResourceSet>,
|
||||
child_as: Option<&AsResourceSet>,
|
||||
issuer_effective_as: Option<&AsResourceSet>,
|
||||
issuer_resources_index: &IssuerEffectiveResourcesIndex,
|
||||
mode: ResourceValidationMode,
|
||||
) -> Result<ResourceResolution, CaPathError> {
|
||||
match mode {
|
||||
ResourceValidationMode::Rfc6487 => Ok(ResourceResolution {
|
||||
effective_ip_resources: resolve_child_ip_resources_indexed(
|
||||
child_ip,
|
||||
issuer_effective_ip,
|
||||
issuer_resources_index.parent_ip_by_afi_items.as_ref(),
|
||||
&issuer_resources_index.parent_ip_merged_intervals,
|
||||
)?,
|
||||
effective_as_resources: resolve_child_as_resources_indexed(
|
||||
child_as,
|
||||
issuer_effective_as,
|
||||
issuer_resources_index.parent_asnum_intervals.as_deref(),
|
||||
issuer_resources_index.parent_rdi_intervals.as_deref(),
|
||||
)?,
|
||||
warnings: ResourceValidationWarnings::default(),
|
||||
}),
|
||||
ResourceValidationMode::ValidationUpdate03 => {
|
||||
let (effective_ip_resources, ip_reduced_by_vrs, ip_vrs_empty) =
|
||||
resolve_child_ip_resources_vrs(
|
||||
child_ip,
|
||||
issuer_effective_ip,
|
||||
issuer_resources_index.parent_ip_by_afi_items.as_ref(),
|
||||
&issuer_resources_index.parent_ip_merged_intervals,
|
||||
)?;
|
||||
let (effective_as_resources, as_reduced_by_vrs, as_vrs_empty) =
|
||||
resolve_child_as_resources_vrs(
|
||||
child_as,
|
||||
issuer_effective_as,
|
||||
issuer_resources_index.parent_asnum_intervals.as_deref(),
|
||||
issuer_resources_index.parent_rdi_intervals.as_deref(),
|
||||
)?;
|
||||
Ok(ResourceResolution {
|
||||
effective_ip_resources,
|
||||
effective_as_resources,
|
||||
warnings: ResourceValidationWarnings {
|
||||
ip_reduced_by_vrs,
|
||||
as_reduced_by_vrs,
|
||||
ip_vrs_empty,
|
||||
as_vrs_empty,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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::model::rc::Afi, Vec<crate::model::rc::IpAddressOrRange>>,
|
||||
>,
|
||||
parent_intervals_by_afi: &HashMap<crate::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 {
|
||||
if child_ip.has_any_inherit() {
|
||||
return Err(CaPathError::InheritWithoutParentResources);
|
||||
}
|
||||
// With no parent effective resources, we cannot validate subset.
|
||||
return Err(CaPathError::ResourcesNotSubset);
|
||||
};
|
||||
|
||||
// Resolve per-AFI inherit, producing an effective set with no inherit.
|
||||
let parent_by_afi = parent_by_afi.ok_or(CaPathError::InheritWithoutParentResources)?;
|
||||
let mut out_families: Vec<crate::model::rc::IpAddressFamily> = Vec::new();
|
||||
|
||||
for fam in &child_ip.families {
|
||||
match &fam.choice {
|
||||
IpAddressChoice::Inherit => {
|
||||
let items = parent_by_afi
|
||||
.get(&fam.afi)
|
||||
.ok_or(CaPathError::InheritWithoutParentResources)?;
|
||||
out_families.push(crate::model::rc::IpAddressFamily {
|
||||
afi: fam.afi,
|
||||
choice: IpAddressChoice::AddressesOrRanges(items.clone()),
|
||||
});
|
||||
}
|
||||
IpAddressChoice::AddressesOrRanges(items) => {
|
||||
// Subset check against parent union for that AFI.
|
||||
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::model::rc::IpAddressFamily {
|
||||
afi: fam.afi,
|
||||
choice: IpAddressChoice::AddressesOrRanges(items.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(IpResourceSet {
|
||||
families: out_families,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_child_ip_resources_vrs(
|
||||
child_ip: Option<&IpResourceSet>,
|
||||
issuer_effective: Option<&IpResourceSet>,
|
||||
parent_by_afi: Option<
|
||||
&BTreeMap<crate::model::rc::Afi, Vec<crate::model::rc::IpAddressOrRange>>,
|
||||
>,
|
||||
parent_intervals_by_afi: &HashMap<crate::model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>>,
|
||||
) -> Result<(Option<IpResourceSet>, bool, bool), CaPathError> {
|
||||
let Some(child_ip) = child_ip else {
|
||||
return Ok((None, false, false));
|
||||
};
|
||||
if child_ip.has_any_inherit() && issuer_effective.is_none() {
|
||||
return Err(CaPathError::InheritWithoutParentResources);
|
||||
}
|
||||
|
||||
let parent_by_afi = parent_by_afi.unwrap_or_else(|| {
|
||||
static EMPTY: std::sync::OnceLock<
|
||||
BTreeMap<crate::model::rc::Afi, Vec<crate::model::rc::IpAddressOrRange>>,
|
||||
> = std::sync::OnceLock::new();
|
||||
EMPTY.get_or_init(BTreeMap::new)
|
||||
});
|
||||
let mut out_families = Vec::new();
|
||||
let mut reduced = false;
|
||||
let mut saw_declared = false;
|
||||
|
||||
for fam in &child_ip.families {
|
||||
match &fam.choice {
|
||||
IpAddressChoice::Inherit => {
|
||||
let items = parent_by_afi
|
||||
.get(&fam.afi)
|
||||
.ok_or(CaPathError::InheritWithoutParentResources)?;
|
||||
out_families.push(crate::model::rc::IpAddressFamily {
|
||||
afi: fam.afi,
|
||||
choice: IpAddressChoice::AddressesOrRanges(items.clone()),
|
||||
});
|
||||
}
|
||||
IpAddressChoice::AddressesOrRanges(items) => {
|
||||
saw_declared = saw_declared || !items.is_empty();
|
||||
let parent_intervals = parent_intervals_by_afi
|
||||
.get(&fam.afi)
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]);
|
||||
let intersections =
|
||||
intersect_ip_items_with_parent_intervals(items, parent_intervals);
|
||||
let child_intervals = ip_items_to_merged_intervals(items);
|
||||
if intervals_changed(&child_intervals, &intersections) {
|
||||
reduced = true;
|
||||
}
|
||||
if !intersections.is_empty() {
|
||||
out_families.push(crate::model::rc::IpAddressFamily {
|
||||
afi: fam.afi,
|
||||
choice: IpAddressChoice::AddressesOrRanges(ip_intervals_to_ranges(
|
||||
fam.afi,
|
||||
&intersections,
|
||||
)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let empty = saw_declared && out_families.is_empty();
|
||||
Ok((
|
||||
Some(IpResourceSet {
|
||||
families: out_families,
|
||||
}),
|
||||
reduced,
|
||||
empty,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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);
|
||||
};
|
||||
let Some(parent) = issuer_effective else {
|
||||
if matches!(child_as.asnum, Some(AsIdentifierChoice::Inherit))
|
||||
|| matches!(child_as.rdi, Some(AsIdentifierChoice::Inherit))
|
||||
{
|
||||
return Err(CaPathError::InheritWithoutParentResources);
|
||||
}
|
||||
return Err(CaPathError::ResourcesNotSubset);
|
||||
};
|
||||
|
||||
let asnum = match child_as.asnum.as_ref() {
|
||||
None => None,
|
||||
Some(AsIdentifierChoice::Inherit) => parent
|
||||
.asnum
|
||||
.clone()
|
||||
.ok_or(CaPathError::InheritWithoutParentResources)
|
||||
.map(Some)?,
|
||||
Some(_) => {
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
let rdi = match child_as.rdi.as_ref() {
|
||||
None => None,
|
||||
Some(AsIdentifierChoice::Inherit) => parent
|
||||
.rdi
|
||||
.clone()
|
||||
.ok_or(CaPathError::InheritWithoutParentResources)
|
||||
.map(Some)?,
|
||||
Some(_) => {
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(AsResourceSet { asnum, rdi }))
|
||||
}
|
||||
|
||||
fn resolve_child_as_resources_vrs(
|
||||
child_as: Option<&AsResourceSet>,
|
||||
issuer_effective: Option<&AsResourceSet>,
|
||||
parent_asnum_intervals: Option<&[(u32, u32)]>,
|
||||
parent_rdi_intervals: Option<&[(u32, u32)]>,
|
||||
) -> Result<(Option<AsResourceSet>, bool, bool), CaPathError> {
|
||||
let Some(child_as) = child_as else {
|
||||
return Ok((None, false, false));
|
||||
};
|
||||
if issuer_effective.is_none()
|
||||
&& (matches!(child_as.asnum, Some(AsIdentifierChoice::Inherit))
|
||||
|| matches!(child_as.rdi, Some(AsIdentifierChoice::Inherit)))
|
||||
{
|
||||
return Err(CaPathError::InheritWithoutParentResources);
|
||||
}
|
||||
|
||||
let (asnum, asnum_reduced, asnum_empty) = resolve_as_choice_vrs(
|
||||
child_as.asnum.as_ref(),
|
||||
issuer_effective.and_then(|p| p.asnum.as_ref()),
|
||||
parent_asnum_intervals,
|
||||
)?;
|
||||
let (rdi, rdi_reduced, rdi_empty) = resolve_as_choice_vrs(
|
||||
child_as.rdi.as_ref(),
|
||||
issuer_effective.and_then(|p| p.rdi.as_ref()),
|
||||
parent_rdi_intervals,
|
||||
)?;
|
||||
Ok((
|
||||
Some(AsResourceSet { asnum, rdi }),
|
||||
asnum_reduced || rdi_reduced,
|
||||
asnum_empty || rdi_empty,
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_as_choice_vrs(
|
||||
child: Option<&AsIdentifierChoice>,
|
||||
parent: Option<&AsIdentifierChoice>,
|
||||
parent_intervals_hint: Option<&[(u32, u32)]>,
|
||||
) -> Result<(Option<AsIdentifierChoice>, bool, bool), CaPathError> {
|
||||
let Some(child) = child else {
|
||||
return Ok((None, false, false));
|
||||
};
|
||||
match child {
|
||||
AsIdentifierChoice::Inherit => {
|
||||
let parent = parent
|
||||
.cloned()
|
||||
.ok_or(CaPathError::InheritWithoutParentResources)?;
|
||||
Ok((Some(parent), false, false))
|
||||
}
|
||||
AsIdentifierChoice::AsIdsOrRanges(_) => {
|
||||
let child_intervals = as_choice_to_merged_intervals(child);
|
||||
let parent_intervals;
|
||||
let parent_intervals = match parent_intervals_hint {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
parent_intervals = parent
|
||||
.map(as_choice_to_merged_intervals)
|
||||
.unwrap_or_default();
|
||||
parent_intervals.as_slice()
|
||||
}
|
||||
};
|
||||
let intersections = intersect_as_intervals(&child_intervals, parent_intervals);
|
||||
let reduced = child_intervals != intersections;
|
||||
let empty = !child_intervals.is_empty() && intersections.is_empty();
|
||||
Ok((
|
||||
Some(AsIdentifierChoice::AsIdsOrRanges(as_intervals_to_items(
|
||||
&intersections,
|
||||
))),
|
||||
reduced,
|
||||
empty,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
let Some(parent) = parent else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Treat inherit as "all of parent" here; actual resolution is handled elsewhere.
|
||||
if matches!(child, AsIdentifierChoice::Inherit) {
|
||||
return true;
|
||||
}
|
||||
if matches!(parent, AsIdentifierChoice::Inherit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let child_intervals = as_choice_to_merged_intervals(child);
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn as_choice_to_merged_intervals(choice: &AsIdentifierChoice) -> Vec<(u32, u32)> {
|
||||
let mut v = Vec::new();
|
||||
match choice {
|
||||
AsIdentifierChoice::Inherit => {}
|
||||
AsIdentifierChoice::AsIdsOrRanges(items) => {
|
||||
for item in items {
|
||||
match item {
|
||||
crate::model::rc::AsIdOrRange::Id(id) => v.push((*id, *id)),
|
||||
crate::model::rc::AsIdOrRange::Range { min, max } => v.push((*min, *max)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v.sort_by_key(|(a, _b)| *a);
|
||||
merge_as_intervals(&v)
|
||||
}
|
||||
|
||||
fn merge_as_intervals(v: &[(u32, u32)]) -> Vec<(u32, u32)> {
|
||||
let mut out: Vec<(u32, u32)> = Vec::new();
|
||||
for (min, max) in v {
|
||||
let Some(last) = out.last_mut() else {
|
||||
out.push((*min, *max));
|
||||
continue;
|
||||
};
|
||||
if *min <= last.1.saturating_add(1) {
|
||||
last.1 = last.1.max(*max);
|
||||
continue;
|
||||
}
|
||||
out.push((*min, *max));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn as_interval_is_covered(parent: &[(u32, u32)], min: u32, max: u32) -> bool {
|
||||
for (pmin, pmax) in parent {
|
||||
if *pmin <= min && max <= *pmax {
|
||||
return true;
|
||||
}
|
||||
if *pmin > min {
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
913
src/validation/ca_path/tests.rs
Normal file
913
src/validation/ca_path/tests.rs
Normal file
@@ -0,0 +1,913 @@
|
||||
// CA path resource and certificate validation tests.
|
||||
|
||||
use super::*;
|
||||
use crate::model::common::X509NameDer;
|
||||
use crate::model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use crate::model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
|
||||
IpAddressOrRange, IpPrefix, IpResourceSet,
|
||||
};
|
||||
use crate::model::rc::{
|
||||
BasicConstraintsProfile, CertificatePoliciesProfile, RcExtensions, ResourceCertKind,
|
||||
ResourceCertificate, RpkixTbsCertificate,
|
||||
};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use std::process::Command;
|
||||
fn dummy_cert(
|
||||
kind: ResourceCertKind,
|
||||
subject_dn: &str,
|
||||
issuer_dn: &str,
|
||||
ski: Option<Vec<u8>>,
|
||||
aki: Option<Vec<u8>>,
|
||||
aia: Option<Vec<&str>>,
|
||||
crldp: Option<Vec<&str>>,
|
||||
) -> ResourceCertificate {
|
||||
let aia = aia.map(|v| v.into_iter().map(|s| s.to_string()).collect::<Vec<_>>());
|
||||
let crldp = crldp.map(|v| v.into_iter().map(|s| s.to_string()).collect::<Vec<_>>());
|
||||
|
||||
ResourceCertificate {
|
||||
raw_der: Vec::new(),
|
||||
kind,
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_name: X509NameDer(issuer_dn.as_bytes().to_vec()),
|
||||
subject_name: X509NameDer(subject_dn.as_bytes().to_vec()),
|
||||
validity_not_before: time::OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: time::OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: kind == ResourceCertKind::Ca,
|
||||
basic_constraints: (kind == ResourceCertKind::Ca).then_some(
|
||||
BasicConstraintsProfile {
|
||||
ca: true,
|
||||
critical: true,
|
||||
path_len_constraint: None,
|
||||
},
|
||||
),
|
||||
subject_key_identifier: ski,
|
||||
authority_key_identifier: aki,
|
||||
crl_distribution_points_uris: crldp,
|
||||
ca_issuers_uris: aia,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: (kind == ResourceCertKind::Ca)
|
||||
.then_some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
certificate_policies: (kind == ResourceCertKind::Ca).then_some(
|
||||
CertificatePoliciesProfile {
|
||||
policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(),
|
||||
qualifier_oids: Vec::new(),
|
||||
},
|
||||
),
|
||||
extension_oids: Vec::new(),
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn openssl_available() -> bool {
|
||||
Command::new("openssl")
|
||||
.arg("version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn write_cert_der_with_addext(dir: &std::path::Path, addext: Option<&str>) -> Vec<u8> {
|
||||
assert!(openssl_available(), "openssl is required for this test");
|
||||
let key = dir.join("k.pem");
|
||||
let cert = dir.join("c.pem");
|
||||
let der = dir.join("c.der");
|
||||
|
||||
let mut cmd = Command::new("openssl");
|
||||
cmd.arg("req")
|
||||
.arg("-x509")
|
||||
.arg("-newkey")
|
||||
.arg("rsa:2048")
|
||||
.arg("-nodes")
|
||||
.arg("-keyout")
|
||||
.arg(&key)
|
||||
.arg("-subj")
|
||||
.arg("/CN=ku")
|
||||
.arg("-days")
|
||||
.arg("1")
|
||||
.arg("-out")
|
||||
.arg(&cert);
|
||||
if let Some(ext) = addext {
|
||||
cmd.arg("-addext").arg(ext);
|
||||
}
|
||||
let out = cmd.output().expect("openssl req");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"openssl req failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
|
||||
let out = Command::new("openssl")
|
||||
.arg("x509")
|
||||
.arg("-in")
|
||||
.arg(&cert)
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(&der)
|
||||
.output()
|
||||
.expect("openssl x509");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"openssl x509 failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
std::fs::read(&der).expect("read der")
|
||||
}
|
||||
|
||||
fn gen_issuer_and_child_der(dir: &std::path::Path) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
|
||||
assert!(openssl_available(), "openssl is required for this test");
|
||||
let issuer_key = dir.join("issuer.key");
|
||||
let issuer_csr = dir.join("issuer.csr");
|
||||
let issuer_pem = dir.join("issuer.pem");
|
||||
let issuer_der = dir.join("issuer.der");
|
||||
|
||||
let child_key = dir.join("child.key");
|
||||
let child_csr = dir.join("child.csr");
|
||||
let child_pem = dir.join("child.pem");
|
||||
let child_der = dir.join("child.der");
|
||||
|
||||
let other_key = dir.join("other.key");
|
||||
let other_csr = dir.join("other.csr");
|
||||
let other_pem = dir.join("other.pem");
|
||||
let other_der = dir.join("other.der");
|
||||
|
||||
let run = |cmd: &mut Command| {
|
||||
let out = cmd.output().expect("run openssl");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"command failed: {:?}\nstderr={}",
|
||||
cmd,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
};
|
||||
|
||||
// Issuer self-signed.
|
||||
run(Command::new("openssl")
|
||||
.args(["genrsa", "-out"])
|
||||
.arg(&issuer_key)
|
||||
.arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["req", "-new", "-key"])
|
||||
.arg(&issuer_key)
|
||||
.args(["-subj", "/CN=issuer", "-out"])
|
||||
.arg(&issuer_csr));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-req", "-in"])
|
||||
.arg(&issuer_csr)
|
||||
.args(["-signkey"])
|
||||
.arg(&issuer_key)
|
||||
.args(["-days", "1", "-out"])
|
||||
.arg(&issuer_pem));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-in"])
|
||||
.arg(&issuer_pem)
|
||||
.args(["-outform", "DER", "-out"])
|
||||
.arg(&issuer_der));
|
||||
|
||||
// Child signed by issuer.
|
||||
run(Command::new("openssl")
|
||||
.args(["genrsa", "-out"])
|
||||
.arg(&child_key)
|
||||
.arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["req", "-new", "-key"])
|
||||
.arg(&child_key)
|
||||
.args(["-subj", "/CN=child", "-out"])
|
||||
.arg(&child_csr));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-req", "-in"])
|
||||
.arg(&child_csr)
|
||||
.args(["-CA"])
|
||||
.arg(&issuer_pem)
|
||||
.args(["-CAkey"])
|
||||
.arg(&issuer_key)
|
||||
.args(["-CAcreateserial", "-days", "1", "-out"])
|
||||
.arg(&child_pem));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-in"])
|
||||
.arg(&child_pem)
|
||||
.args(["-outform", "DER", "-out"])
|
||||
.arg(&child_der));
|
||||
|
||||
// Other self-signed issuer.
|
||||
run(Command::new("openssl")
|
||||
.args(["genrsa", "-out"])
|
||||
.arg(&other_key)
|
||||
.arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["req", "-new", "-key"])
|
||||
.arg(&other_key)
|
||||
.args(["-subj", "/CN=other", "-out"])
|
||||
.arg(&other_csr));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-req", "-in"])
|
||||
.arg(&other_csr)
|
||||
.args(["-signkey"])
|
||||
.arg(&other_key)
|
||||
.args(["-days", "1", "-out"])
|
||||
.arg(&other_pem));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-in"])
|
||||
.arg(&other_pem)
|
||||
.args(["-outform", "DER", "-out"])
|
||||
.arg(&other_der));
|
||||
|
||||
(
|
||||
std::fs::read(&issuer_der).expect("read issuer der"),
|
||||
std::fs::read(&child_der).expect("read child der"),
|
||||
std::fs::read(&other_der).expect("read other der"),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_child_ip_resources_rejects_inherit_without_parent_effective_resources() {
|
||||
let child = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
};
|
||||
let err = resolve_child_ip_resources(Some(&child), None).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::InheritWithoutParentResources));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_child_ip_resources_rejects_non_inherit_without_parent_effective_resources() {
|
||||
let child = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![]),
|
||||
}],
|
||||
};
|
||||
let err = resolve_child_ip_resources(Some(&child), None).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ResourcesNotSubset));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_resources_by_afi_items_rejects_inherit_families() {
|
||||
let parent = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
};
|
||||
let err = ip_resources_by_afi_items(&parent).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::InheritWithoutParentResources));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_child_as_resources_rejects_inherit_without_parent_effective_resources() {
|
||||
let child = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
};
|
||||
let err = resolve_child_as_resources(Some(&child), None).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::InheritWithoutParentResources));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_update_03_intersects_overclaiming_child_ip_resources() {
|
||||
let issuer = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
let child = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 23,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
let index = IssuerEffectiveResourcesIndex::from_effective_resources(Some(&issuer), None)
|
||||
.expect("index");
|
||||
let resolved = resolve_child_resources(
|
||||
Some(&child),
|
||||
Some(&issuer),
|
||||
None,
|
||||
None,
|
||||
&index,
|
||||
ResourceValidationMode::ValidationUpdate03,
|
||||
)
|
||||
.expect("vrs resolution");
|
||||
assert!(resolved.warnings.ip_reduced_by_vrs);
|
||||
assert!(!resolved.warnings.ip_vrs_empty);
|
||||
let effective = resolved.effective_ip_resources.expect("effective ip");
|
||||
assert!(effective.families[0].contains_prefix(&IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
}));
|
||||
assert!(!effective.families[0].contains_prefix(&IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![10, 0, 1, 0],
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_update_03_keeps_empty_vrs_instead_of_rejecting_child_ca() {
|
||||
let issuer = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
let child = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![192, 0, 2, 0],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
let index = IssuerEffectiveResourcesIndex::from_effective_resources(Some(&issuer), None)
|
||||
.expect("index");
|
||||
let strict_err = resolve_child_resources(
|
||||
Some(&child),
|
||||
Some(&issuer),
|
||||
None,
|
||||
None,
|
||||
&index,
|
||||
ResourceValidationMode::Rfc6487,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(strict_err, CaPathError::ResourcesNotSubset));
|
||||
|
||||
let resolved = resolve_child_resources(
|
||||
Some(&child),
|
||||
Some(&issuer),
|
||||
None,
|
||||
None,
|
||||
&index,
|
||||
ResourceValidationMode::ValidationUpdate03,
|
||||
)
|
||||
.expect("vrs resolution");
|
||||
assert!(resolved.warnings.ip_reduced_by_vrs);
|
||||
assert!(resolved.warnings.ip_vrs_empty);
|
||||
assert!(
|
||||
resolved
|
||||
.effective_ip_resources
|
||||
.expect("effective ip")
|
||||
.families
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_update_03_intersects_overclaiming_child_as_resources() {
|
||||
let issuer = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range {
|
||||
min: 64500,
|
||||
max: 64510,
|
||||
},
|
||||
])),
|
||||
rdi: None,
|
||||
};
|
||||
let child = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range {
|
||||
min: 64505,
|
||||
max: 64520,
|
||||
},
|
||||
])),
|
||||
rdi: None,
|
||||
};
|
||||
let index = IssuerEffectiveResourcesIndex::from_effective_resources(None, Some(&issuer))
|
||||
.expect("index");
|
||||
let resolved = resolve_child_resources(
|
||||
None,
|
||||
None,
|
||||
Some(&child),
|
||||
Some(&issuer),
|
||||
&index,
|
||||
ResourceValidationMode::ValidationUpdate03,
|
||||
)
|
||||
.expect("vrs resolution");
|
||||
assert!(resolved.warnings.as_reduced_by_vrs);
|
||||
assert!(!resolved.warnings.as_vrs_empty);
|
||||
let effective = resolved.effective_as_resources.expect("effective as");
|
||||
assert_eq!(
|
||||
effective.asnum,
|
||||
Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range {
|
||||
min: 64505,
|
||||
max: 64510,
|
||||
},
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_update_03_records_all_resource_warning_summary_parts() {
|
||||
let issuer_ip = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
let child_ip = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![192, 0, 2, 0],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
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: 64520,
|
||||
max: 64530,
|
||||
},
|
||||
])),
|
||||
rdi: None,
|
||||
};
|
||||
let index =
|
||||
IssuerEffectiveResourcesIndex::from_effective_resources(Some(&issuer_ip), Some(&issuer_as))
|
||||
.expect("index");
|
||||
|
||||
let resolved = resolve_child_resources(
|
||||
Some(&child_ip),
|
||||
Some(&issuer_ip),
|
||||
Some(&child_as),
|
||||
Some(&issuer_as),
|
||||
&index,
|
||||
ResourceValidationMode::ValidationUpdate03,
|
||||
)
|
||||
.expect("vrs resolution");
|
||||
|
||||
assert!(resolved.warnings.ip_reduced_by_vrs);
|
||||
assert!(resolved.warnings.ip_vrs_empty);
|
||||
assert!(resolved.warnings.as_reduced_by_vrs);
|
||||
assert!(resolved.warnings.as_vrs_empty);
|
||||
assert_eq!(
|
||||
resolved.warnings.summary(),
|
||||
"ip_reduced_by_vrs,as_reduced_by_vrs,ip_vrs_empty,as_vrs_empty"
|
||||
);
|
||||
assert!(!resolved.warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_aki_mismatch_is_rejected() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![9]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_child_aki_matches_issuer_ski(&child, &issuer).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildAkiMismatch), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_aia_missing_is_rejected() {
|
||||
let _issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
None,
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_child_aia_points_to_issuer_uri(&child, "rsync://example.test/issuer.cer")
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildAiaMissing), "{err}");
|
||||
|
||||
// Also cover issuer ski missing.
|
||||
let issuer_missing_ski = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let err = validate_child_aki_matches_issuer_ski(&child, &issuer_missing_ski).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::IssuerSkiMissing), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_aia_issuer_uri_mismatch_is_rejected() {
|
||||
let _issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/other.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_child_aia_points_to_issuer_uri(&child, "rsync://example.test/issuer.cer")
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CaPathError::ChildAiaIssuerUriMismatch),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_crldp_mismatch_is_rejected() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
None,
|
||||
);
|
||||
let err =
|
||||
validate_child_crldp_contains_issuer_crl_uri(&child, "rsync://example.test/issuer.crl")
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildCrlDpMissing), "{err}");
|
||||
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/other.crl"]),
|
||||
);
|
||||
let err =
|
||||
validate_child_crldp_contains_issuer_crl_uri(&child, "rsync://example.test/issuer.crl")
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildCrlDpUriMismatch), "{err}");
|
||||
|
||||
// Cover child AKI missing.
|
||||
let child_missing_aki = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
None,
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_child_aki_matches_issuer_ski(&child_missing_aki, &issuer).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildAkiMissing), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_binding_checks_accept_when_matching() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
validate_child_aki_matches_issuer_ski(&child, &issuer).expect("aki ok");
|
||||
validate_child_aia_points_to_issuer_uri(&child, "rsync://example.test/issuer.cer")
|
||||
.expect("aia ok");
|
||||
validate_child_crldp_contains_issuer_crl_uri(&child, "rsync://example.test/issuer.crl")
|
||||
.expect("crldp ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_child_ca_key_usage_accepts_only_keycertsign_and_crlsign_critical() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let der =
|
||||
write_cert_der_with_addext(td.path(), Some("keyUsage = critical, keyCertSign, cRLSign"));
|
||||
let cert = parse_x509_cert(&der).expect("x509 parse ok");
|
||||
validate_child_ca_key_usage(&cert).expect("key usage ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_child_ca_key_usage_rejects_missing_noncritical_and_invalid_bits() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let missing = write_cert_der_with_addext(td.path(), None);
|
||||
let cert = parse_x509_cert(&missing).expect("x509 parse ok");
|
||||
let err = validate_child_ca_key_usage(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageMissing), "{err}");
|
||||
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let noncritical =
|
||||
write_cert_der_with_addext(td.path(), Some("keyUsage = keyCertSign, cRLSign"));
|
||||
let cert = parse_x509_cert(&noncritical).expect("x509 parse ok");
|
||||
let err = validate_child_ca_key_usage(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageNotCritical), "{err}");
|
||||
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let invalid = write_cert_der_with_addext(
|
||||
td.path(),
|
||||
Some("keyUsage = critical, keyCertSign, cRLSign, digitalSignature"),
|
||||
);
|
||||
let cert = parse_x509_cert(&invalid).expect("x509 parse ok");
|
||||
let err = validate_child_ca_key_usage(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageInvalidBits), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_cert_signature_with_issuer_accepts_valid_chain_and_rejects_wrong_issuer() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let (issuer, child, other) = gen_issuer_and_child_der(td.path());
|
||||
let issuer_cert = parse_x509_cert(&issuer).expect("x509 parse issuer");
|
||||
let child_cert = parse_x509_cert(&child).expect("x509 parse child");
|
||||
verify_child_signature(&child_cert, &issuer_cert.tbs_certificate.subject_pki)
|
||||
.expect("signature ok");
|
||||
let other_cert = parse_x509_cert(&other).expect("x509 parse other");
|
||||
let err =
|
||||
verify_child_signature(&child_cert, &other_cert.tbs_certificate.subject_pki).unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CaPathError::ChildSignatureInvalid(_)),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issuer_effective_resources_index_and_indexed_resolvers_cover_success_and_failure_paths() {
|
||||
use crate::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::model::rc::{AsIdOrRange, IpAddressOrRange, 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 child_ip_inherit = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
};
|
||||
let eff = resolve_child_ip_resources(Some(&child_ip_inherit), Some(&parent_ip))
|
||||
.expect("inherit resolves")
|
||||
.expect("some ip");
|
||||
assert_eq!(eff.families.len(), 1);
|
||||
assert!(matches!(
|
||||
eff.families[0].choice,
|
||||
IpAddressChoice::AddressesOrRanges(_)
|
||||
));
|
||||
|
||||
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],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
resolve_child_ip_resources(Some(&child_ip_subset), Some(&parent_ip))
|
||||
.expect("subset ok")
|
||||
.expect("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(Some(&child_ip_bad), Some(&parent_ip)).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}");
|
||||
|
||||
let parent_as = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range { min: 1, max: 100 },
|
||||
])),
|
||||
rdi: None,
|
||||
};
|
||||
let child_as_inherit = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
};
|
||||
let eff_as = resolve_child_as_resources(Some(&child_as_inherit), Some(&parent_as))
|
||||
.expect("inherit as")
|
||||
.expect("some");
|
||||
assert_eq!(eff_as.asnum, parent_as.asnum);
|
||||
|
||||
let child_as_subset = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(50)])),
|
||||
rdi: None,
|
||||
};
|
||||
resolve_child_as_resources(Some(&child_as_subset), Some(&parent_as))
|
||||
.expect("subset as")
|
||||
.expect("some");
|
||||
|
||||
let child_as_bad = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
||||
200,
|
||||
)])),
|
||||
rdi: None,
|
||||
};
|
||||
let err = resolve_child_as_resources(Some(&child_as_bad), Some(&parent_as)).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}");
|
||||
}
|
||||
464
src/validation/ca_path/types_and_validation.rs
Normal file
464
src/validation/ca_path/types_and_validation.rs
Normal file
@@ -0,0 +1,464 @@
|
||||
// Resource certificate path types and public validation entry points.
|
||||
|
||||
use crate::model::common::BigUnsigned;
|
||||
use crate::model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
|
||||
use crate::model::oid::OID_KEY_USAGE_RAW;
|
||||
use crate::model::rc::{
|
||||
AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind,
|
||||
ResourceCertificate, ResourceCertificateDecodeError, ResourceCertificateProfileError,
|
||||
ResourceCertificateRole,
|
||||
};
|
||||
use crate::validation::policy::ResourceValidationMode;
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
use crate::validation::x509_name::x509_names_equivalent;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ValidatedSubordinateCa {
|
||||
pub child_ca: ResourceCertificate,
|
||||
pub issuer_ca: ResourceCertificate,
|
||||
pub issuer_crl: RpkixCrl,
|
||||
pub effective_ip_resources: Option<IpResourceSet>,
|
||||
pub effective_as_resources: Option<AsResourceSet>,
|
||||
pub resource_warnings: ResourceValidationWarnings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ValidatedSubordinateCaLite {
|
||||
pub child_ca: ResourceCertificate,
|
||||
pub effective_ip_resources: Option<IpResourceSet>,
|
||||
pub effective_as_resources: Option<AsResourceSet>,
|
||||
pub resource_warnings: ResourceValidationWarnings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ResourceValidationWarnings {
|
||||
pub ip_reduced_by_vrs: bool,
|
||||
pub as_reduced_by_vrs: bool,
|
||||
pub ip_vrs_empty: bool,
|
||||
pub as_vrs_empty: bool,
|
||||
}
|
||||
|
||||
impl ResourceValidationWarnings {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.ip_reduced_by_vrs
|
||||
&& !self.as_reduced_by_vrs
|
||||
&& !self.ip_vrs_empty
|
||||
&& !self.as_vrs_empty
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> String {
|
||||
let mut parts = Vec::new();
|
||||
if self.ip_reduced_by_vrs {
|
||||
parts.push("ip_reduced_by_vrs");
|
||||
}
|
||||
if self.as_reduced_by_vrs {
|
||||
parts.push("as_reduced_by_vrs");
|
||||
}
|
||||
if self.ip_vrs_empty {
|
||||
parts.push("ip_vrs_empty");
|
||||
}
|
||||
if self.as_vrs_empty {
|
||||
parts.push("as_vrs_empty");
|
||||
}
|
||||
parts.join(",")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct IssuerEffectiveResourcesIndex {
|
||||
parent_ip_by_afi_items:
|
||||
Option<BTreeMap<crate::model::rc::Afi, Vec<crate::model::rc::IpAddressOrRange>>>,
|
||||
parent_ip_merged_intervals: HashMap<crate::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)")]
|
||||
ChildDecode(#[from] ResourceCertificateDecodeError),
|
||||
|
||||
#[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
|
||||
IssuerDecode(ResourceCertificateDecodeError),
|
||||
|
||||
#[error("child CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
|
||||
ChildProfile(ResourceCertificateProfileError),
|
||||
|
||||
#[error("issuer CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
|
||||
IssuerProfile(ResourceCertificateProfileError),
|
||||
|
||||
#[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")]
|
||||
CrlDecode(#[from] CrlDecodeError),
|
||||
|
||||
#[error(
|
||||
"child certificate must be a CA resource certificate (RFC 6487 §4.8.1; RFC 5280 §4.2.1.9)"
|
||||
)]
|
||||
ChildNotCa,
|
||||
|
||||
#[error(
|
||||
"issuer certificate must be a CA resource certificate (RFC 6487 §4.8.1; RFC 5280 §4.2.1.9)"
|
||||
)]
|
||||
IssuerNotCa,
|
||||
|
||||
#[error(
|
||||
"child issuer DN does not match issuer CA subject DN: child.issuer={child_issuer_dn} issuer.subject={issuer_subject_dn} (RFC 5280 §6.1)"
|
||||
)]
|
||||
IssuerSubjectMismatch {
|
||||
child_issuer_dn: String,
|
||||
issuer_subject_dn: String,
|
||||
},
|
||||
|
||||
#[error("child CA certificate signature verification failed: {0} (RFC 5280 §6.1)")]
|
||||
ChildSignatureInvalid(String),
|
||||
|
||||
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
|
||||
IssuerSpkiParse(String),
|
||||
|
||||
#[error(
|
||||
"trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)"
|
||||
)]
|
||||
IssuerSpkiTrailingBytes(usize),
|
||||
|
||||
#[error("certificate not valid at validation_time (RFC 5280 §4.1.2.5; RFC 5280 §6.1)")]
|
||||
CertificateNotValidAtTime,
|
||||
|
||||
#[error("child CA KeyUsage extension missing (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")]
|
||||
KeyUsageMissing,
|
||||
|
||||
#[error("child CA KeyUsage criticality must be critical (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")]
|
||||
KeyUsageNotCritical,
|
||||
|
||||
#[error("child CA KeyUsage must have only keyCertSign and cRLSign set (RFC 6487 §4.8.4)")]
|
||||
KeyUsageInvalidBits,
|
||||
|
||||
#[error(
|
||||
"CRL signature/binding verification failed: {0} (RFC 5280 §6.3.3; RFC 6487 §5; RFC 9829 §3.1)"
|
||||
)]
|
||||
CrlVerify(#[from] CrlVerifyError),
|
||||
|
||||
#[error(
|
||||
"CRL not valid at validation_time (RFC 5280 §6.3.3(g); RFC 5280 §5.1.2.4-§5.1.2.5; RFC 6487 §5)"
|
||||
)]
|
||||
CrlNotValidAtTime,
|
||||
|
||||
#[error("child CA certificate is revoked by issuer CRL (RFC 5280 §6.3.3; RFC 6487 §5)")]
|
||||
ChildRevoked,
|
||||
|
||||
#[error(
|
||||
"child CA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11)"
|
||||
)]
|
||||
ResourcesMissing,
|
||||
|
||||
#[error(
|
||||
"resource extension inheritance cannot be resolved (parent missing resources) (RFC 6487 §7.2)"
|
||||
)]
|
||||
InheritWithoutParentResources,
|
||||
|
||||
#[error("child CA resources are not a subset of issuer resources (RFC 6487 §7.2)")]
|
||||
ResourcesNotSubset,
|
||||
|
||||
#[error("issuer CA subjectKeyIdentifier missing (RFC 6487 §4.8.2)")]
|
||||
IssuerSkiMissing,
|
||||
|
||||
#[error("child CA authorityKeyIdentifier missing (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)")]
|
||||
ChildAkiMissing,
|
||||
|
||||
#[error(
|
||||
"child CA authorityKeyIdentifier does not match issuer subjectKeyIdentifier (RFC 6487 §4.8.3)"
|
||||
)]
|
||||
ChildAkiMismatch,
|
||||
|
||||
#[error("child CA authorityInfoAccess missing (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)")]
|
||||
ChildAiaMissing,
|
||||
|
||||
#[error(
|
||||
"child CA authorityInfoAccess does not reference issuer certificate rsync URI (RFC 6487 §4.8.7)"
|
||||
)]
|
||||
ChildAiaIssuerUriMismatch,
|
||||
|
||||
#[error("child CA CRLDistributionPoints missing (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)")]
|
||||
ChildCrlDpMissing,
|
||||
|
||||
#[error(
|
||||
"child CA CRLDistributionPoints does not reference issuer CRL rsync URI (RFC 6487 §4.8.6)"
|
||||
)]
|
||||
ChildCrlDpUriMismatch,
|
||||
}
|
||||
|
||||
pub fn validate_subordinate_ca_cert(
|
||||
child_ca_der: &[u8],
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_crl_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_crl_rsync_uri: &str,
|
||||
issuer_effective_ip: Option<&IpResourceSet>,
|
||||
issuer_effective_as: Option<&AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<ValidatedSubordinateCa, CaPathError> {
|
||||
validate_subordinate_ca_cert_with_resource_validation_mode(
|
||||
child_ca_der,
|
||||
issuer_ca_der,
|
||||
issuer_crl_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_crl_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
ResourceValidationMode::Rfc6487,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn validate_subordinate_ca_cert_with_resource_validation_mode(
|
||||
child_ca_der: &[u8],
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_crl_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_crl_rsync_uri: &str,
|
||||
issuer_effective_ip: Option<&IpResourceSet>,
|
||||
issuer_effective_as: Option<&AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
resource_validation_mode: ResourceValidationMode,
|
||||
) -> Result<ValidatedSubordinateCa, CaPathError> {
|
||||
let child_ca = ResourceCertificate::decode_der(child_ca_der)?;
|
||||
if child_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::ChildNotCa);
|
||||
}
|
||||
child_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CaPathError::ChildProfile)?;
|
||||
|
||||
let issuer_ca =
|
||||
ResourceCertificate::decode_der(issuer_ca_der).map_err(CaPathError::IssuerDecode)?;
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::IssuerNotCa);
|
||||
}
|
||||
issuer_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CaPathError::IssuerProfile)?;
|
||||
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
|
||||
|
||||
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CaPathError::IssuerSubjectMismatch {
|
||||
child_issuer_dn: child_ca.tbs.issuer_name.to_string(),
|
||||
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
validate_child_aki_matches_issuer_ski(&child_ca, &issuer_ca)?;
|
||||
if let Some(expected_issuer_uri) = issuer_ca_rsync_uri {
|
||||
validate_child_aia_points_to_issuer_uri(&child_ca, expected_issuer_uri)?;
|
||||
}
|
||||
validate_child_crldp_contains_issuer_crl_uri(&child_ca, issuer_crl_rsync_uri)?;
|
||||
|
||||
if !time_within_validity(
|
||||
validation_time,
|
||||
child_ca.tbs.validity_not_before,
|
||||
child_ca.tbs.validity_not_after,
|
||||
) || !time_within_validity(
|
||||
validation_time,
|
||||
issuer_ca.tbs.validity_not_before,
|
||||
issuer_ca.tbs.validity_not_after,
|
||||
) {
|
||||
return Err(CaPathError::CertificateNotValidAtTime);
|
||||
}
|
||||
|
||||
let child_x509 = parse_x509_cert(child_ca_der)?;
|
||||
verify_child_signature(&child_x509, &issuer_spki)?;
|
||||
validate_child_ca_key_usage(&child_x509)?;
|
||||
|
||||
let issuer_crl = RpkixCrl::decode_der(issuer_crl_der)?;
|
||||
issuer_crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
|
||||
if !crl_valid_at_time(&issuer_crl, validation_time) {
|
||||
return Err(CaPathError::CrlNotValidAtTime);
|
||||
}
|
||||
|
||||
if is_serial_revoked_by_crl(&child_ca, &issuer_crl) {
|
||||
return Err(CaPathError::ChildRevoked);
|
||||
}
|
||||
|
||||
let ResourceResolution {
|
||||
effective_ip_resources,
|
||||
effective_as_resources,
|
||||
warnings: resource_warnings,
|
||||
} = resolve_child_resources(
|
||||
child_ca.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer_effective_ip,
|
||||
child_ca.tbs.extensions.as_resources.as_ref(),
|
||||
issuer_effective_as,
|
||||
&IssuerEffectiveResourcesIndex::from_effective_resources(
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
)?,
|
||||
resource_validation_mode,
|
||||
)?;
|
||||
if effective_ip_resources.is_none() && effective_as_resources.is_none() {
|
||||
return Err(CaPathError::ResourcesMissing);
|
||||
}
|
||||
|
||||
Ok(ValidatedSubordinateCa {
|
||||
child_ca,
|
||||
issuer_ca,
|
||||
issuer_crl,
|
||||
effective_ip_resources,
|
||||
effective_as_resources,
|
||||
resource_warnings,
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate a subordinate child CA using *pre-decoded issuer CA* and *pre-decoded+verified issuer CRL*.
|
||||
///
|
||||
/// This avoids repeating issuer CA decode and issuer CRL decode+signature verification for every
|
||||
/// child CA certificate discovered in a publication point.
|
||||
pub fn validate_subordinate_ca_cert_with_prevalidated_issuer(
|
||||
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>,
|
||||
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,
|
||||
ResourceValidationMode::Rfc6487,
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
resource_validation_mode: ResourceValidationMode,
|
||||
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
|
||||
if child_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::ChildNotCa);
|
||||
}
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::IssuerNotCa);
|
||||
}
|
||||
child_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CaPathError::ChildProfile)?;
|
||||
issuer_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CaPathError::IssuerProfile)?;
|
||||
|
||||
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CaPathError::IssuerSubjectMismatch {
|
||||
child_issuer_dn: child_ca.tbs.issuer_name.to_string(),
|
||||
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
validate_child_aki_matches_issuer_ski(&child_ca, issuer_ca)?;
|
||||
if let Some(expected_issuer_uri) = issuer_ca_rsync_uri {
|
||||
validate_child_aia_points_to_issuer_uri(&child_ca, expected_issuer_uri)?;
|
||||
}
|
||||
validate_child_crldp_contains_issuer_crl_uri(&child_ca, issuer_crl_rsync_uri)?;
|
||||
|
||||
if !time_within_validity(
|
||||
validation_time,
|
||||
child_ca.tbs.validity_not_before,
|
||||
child_ca.tbs.validity_not_after,
|
||||
) || !time_within_validity(
|
||||
validation_time,
|
||||
issuer_ca.tbs.validity_not_before,
|
||||
issuer_ca.tbs.validity_not_after,
|
||||
) {
|
||||
return Err(CaPathError::CertificateNotValidAtTime);
|
||||
}
|
||||
|
||||
let child_x509 = parse_x509_cert(child_ca_der)?;
|
||||
verify_child_signature(&child_x509, issuer_spki)?;
|
||||
validate_child_ca_key_usage(&child_x509)?;
|
||||
|
||||
if !crl_valid_at_time(issuer_crl, validation_time) {
|
||||
return Err(CaPathError::CrlNotValidAtTime);
|
||||
}
|
||||
|
||||
let serial = BigUnsigned::from_biguint(&child_ca.tbs.serial_number);
|
||||
if issuer_crl_revoked_serials.contains(&serial.bytes_be) {
|
||||
return Err(CaPathError::ChildRevoked);
|
||||
}
|
||||
|
||||
let ResourceResolution {
|
||||
effective_ip_resources,
|
||||
effective_as_resources,
|
||||
warnings: resource_warnings,
|
||||
} = resolve_child_resources(
|
||||
child_ca.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer_effective_ip,
|
||||
child_ca.tbs.extensions.as_resources.as_ref(),
|
||||
issuer_effective_as,
|
||||
issuer_resources_index,
|
||||
resource_validation_mode,
|
||||
)?;
|
||||
if effective_ip_resources.is_none() && effective_as_resources.is_none() {
|
||||
return Err(CaPathError::ResourcesMissing);
|
||||
}
|
||||
|
||||
Ok(ValidatedSubordinateCaLite {
|
||||
child_ca,
|
||||
effective_ip_resources,
|
||||
effective_as_resources,
|
||||
resource_warnings,
|
||||
})
|
||||
}
|
||||
725
src/validation/cert_path.rs
Normal file
725
src/validation/cert_path.rs
Normal file
@@ -0,0 +1,725 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::model::common::BigUnsigned;
|
||||
use crate::model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
|
||||
use crate::model::rc::{
|
||||
ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError,
|
||||
ResourceCertificateProfileError, ResourceCertificateRole,
|
||||
};
|
||||
use crate::model::signed_object::{EeKeyUsageSummary, ResourceEeCertificate};
|
||||
use ring::signature;
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
use crate::validation::x509_name::x509_names_equivalent;
|
||||
use std::collections::HashSet;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ValidatedEeCertPath {
|
||||
pub ee: ResourceCertificate,
|
||||
pub issuer_ca: ResourceCertificate,
|
||||
pub issuer_crl: RpkixCrl,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CertPathError {
|
||||
#[error("EE certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
|
||||
EeDecode(#[from] ResourceCertificateDecodeError),
|
||||
|
||||
#[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
|
||||
IssuerDecode(ResourceCertificateDecodeError),
|
||||
|
||||
#[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")]
|
||||
CrlDecode(#[from] CrlDecodeError),
|
||||
|
||||
#[error(
|
||||
"issuer certificate must be a CA resource certificate (RFC 6487 §4.8.1; RFC 5280 §4.2.1.9)"
|
||||
)]
|
||||
IssuerNotCa,
|
||||
|
||||
#[error("issuer CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
|
||||
IssuerProfile(ResourceCertificateProfileError),
|
||||
|
||||
#[error("EE certificate must be an EE resource certificate (RFC 6487 §4)")]
|
||||
EeNotEe,
|
||||
|
||||
#[error(
|
||||
"EE issuer DN does not match issuer CA subject DN: ee.issuer={ee_issuer_dn} issuer.subject={issuer_subject_dn} (RFC 5280 §6.1)"
|
||||
)]
|
||||
IssuerSubjectMismatch {
|
||||
ee_issuer_dn: String,
|
||||
issuer_subject_dn: String,
|
||||
},
|
||||
|
||||
#[error("EE certificate signature verification failed: {0} (RFC 5280 §6.1)")]
|
||||
EeSignatureInvalid(String),
|
||||
|
||||
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
|
||||
IssuerSpkiParse(String),
|
||||
|
||||
#[error(
|
||||
"trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)"
|
||||
)]
|
||||
IssuerSpkiTrailingBytes(usize),
|
||||
|
||||
#[error("EE KeyUsage extension missing (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")]
|
||||
KeyUsageMissing,
|
||||
|
||||
#[error("EE KeyUsage criticality must be critical (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")]
|
||||
KeyUsageNotCritical,
|
||||
|
||||
#[error("EE KeyUsage must have only digitalSignature set (RFC 6487 §4.8.4)")]
|
||||
KeyUsageInvalidBits,
|
||||
|
||||
#[error("EE KeyUsage parse failed: {0} (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")]
|
||||
KeyUsageParse(String),
|
||||
|
||||
#[error("issuer CA subjectKeyIdentifier missing (RFC 6487 §4.8.2)")]
|
||||
IssuerSkiMissing,
|
||||
|
||||
#[error("EE authorityKeyIdentifier missing (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)")]
|
||||
EeAkiMissing,
|
||||
|
||||
#[error(
|
||||
"EE authorityKeyIdentifier does not match issuer subjectKeyIdentifier (RFC 6487 §4.8.3)"
|
||||
)]
|
||||
EeAkiMismatch,
|
||||
|
||||
#[error("EE authorityInfoAccess missing (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)")]
|
||||
EeAiaMissing,
|
||||
|
||||
#[error(
|
||||
"EE authorityInfoAccess does not reference issuer certificate rsync URI (RFC 6487 §4.8.7)"
|
||||
)]
|
||||
EeAiaIssuerUriMismatch,
|
||||
|
||||
#[error("EE CRLDistributionPoints missing (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)")]
|
||||
EeCrlDpMissing,
|
||||
|
||||
#[error("EE CRLDistributionPoints does not reference issuer CRL rsync URI (RFC 6487 §4.8.6)")]
|
||||
EeCrlDpUriMismatch,
|
||||
|
||||
#[error("certificate not valid at validation_time (RFC 5280 §4.1.2.5; RFC 5280 §6.1)")]
|
||||
CertificateNotValidAtTime,
|
||||
|
||||
#[error(
|
||||
"CRL signature/binding verification failed: {0} (RFC 5280 §6.3.3; RFC 6487 §5; RFC 9829 §3.1)"
|
||||
)]
|
||||
CrlVerify(#[from] CrlVerifyError),
|
||||
|
||||
#[error(
|
||||
"CRL not valid at validation_time (RFC 5280 §6.3.3(g); RFC 5280 §5.1.2.4-§5.1.2.5; RFC 6487 §5)"
|
||||
)]
|
||||
CrlNotValidAtTime,
|
||||
|
||||
#[error("EE certificate is revoked by issuer CRL (RFC 5280 §6.3.3; RFC 6487 §5)")]
|
||||
EeRevoked,
|
||||
}
|
||||
|
||||
pub fn validate_ee_cert_path(
|
||||
ee_cert_der: &[u8],
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_crl_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_crl_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<ValidatedEeCertPath, CertPathError> {
|
||||
let ee = ResourceCertificate::decode_der(ee_cert_der)?;
|
||||
if ee.kind != ResourceCertKind::Ee {
|
||||
return Err(CertPathError::EeNotEe);
|
||||
}
|
||||
|
||||
let issuer_ca =
|
||||
ResourceCertificate::decode_der(issuer_ca_der).map_err(CertPathError::IssuerDecode)?;
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CertPathError::IssuerNotCa);
|
||||
}
|
||||
issuer_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CertPathError::IssuerProfile)?;
|
||||
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
|
||||
|
||||
if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CertPathError::IssuerSubjectMismatch {
|
||||
ee_issuer_dn: ee.tbs.issuer_name.to_string(),
|
||||
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
if let Some(expected_crl_uri) = issuer_crl_rsync_uri {
|
||||
validate_ee_crldp_contains_issuer_crl_uri(&ee, expected_crl_uri)?;
|
||||
}
|
||||
|
||||
if !time_within_validity(
|
||||
validation_time,
|
||||
ee.tbs.validity_not_before,
|
||||
ee.tbs.validity_not_after,
|
||||
) || !time_within_validity(
|
||||
validation_time,
|
||||
issuer_ca.tbs.validity_not_before,
|
||||
issuer_ca.tbs.validity_not_after,
|
||||
) {
|
||||
return Err(CertPathError::CertificateNotValidAtTime);
|
||||
}
|
||||
|
||||
let ee_x509 = parse_x509_cert(ee_cert_der)?;
|
||||
verify_ee_cert_signature(&ee_x509, &issuer_spki)?;
|
||||
validate_ee_key_usage(&ee_x509)?;
|
||||
|
||||
let issuer_crl = RpkixCrl::decode_der(issuer_crl_der)?;
|
||||
issuer_crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
|
||||
if !crl_valid_at_time(&issuer_crl, validation_time) {
|
||||
return Err(CertPathError::CrlNotValidAtTime);
|
||||
}
|
||||
|
||||
if is_serial_revoked_by_crl(&ee, &issuer_crl) {
|
||||
return Err(CertPathError::EeRevoked);
|
||||
}
|
||||
|
||||
Ok(ValidatedEeCertPath {
|
||||
ee,
|
||||
issuer_ca,
|
||||
issuer_crl,
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate the EE certificate path using a *pre-decoded issuer CA* and a *pre-decoded and
|
||||
/// pre-verified issuer CRL*.
|
||||
///
|
||||
/// This is a performance-oriented helper for serial runs: it avoids repeating issuer CA
|
||||
/// decode and issuer CRL decode+signature verification for every signed object in a publication point.
|
||||
///
|
||||
/// The caller must ensure:
|
||||
/// - `issuer_ca` corresponds to `issuer_ca_der`
|
||||
/// - `issuer_crl` cryptographic signature/binding has already been verified against `issuer_ca_der`
|
||||
pub fn validate_ee_cert_path_with_prevalidated_issuer(
|
||||
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<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,
|
||||
)
|
||||
}
|
||||
|
||||
/// Validate a signed-object embedded EE certificate path using signed-object cached fields.
|
||||
///
|
||||
/// This avoids reparsing the EE certificate as `X509Certificate` and bypasses the
|
||||
/// x509-parser signature wrapper by verifying the cached TBS/signature bytes directly.
|
||||
pub fn validate_signed_object_ee_cert_path_fast(
|
||||
ee: &ResourceEeCertificate,
|
||||
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_bindings_and_status(
|
||||
&ee.resource_cert,
|
||||
issuer_ca,
|
||||
issuer_crl,
|
||||
issuer_crl_revoked_serials,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_crl_rsync_uri,
|
||||
validation_time,
|
||||
)?;
|
||||
validate_ee_key_usage_summary(&ee.key_usage_summary)?;
|
||||
verify_ee_cert_signature_fast(&ee.tbs_certificate_der, &ee.signature_bytes, issuer_spki)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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> {
|
||||
validate_ee_cert_path_bindings_and_status(
|
||||
ee,
|
||||
issuer_ca,
|
||||
issuer_crl,
|
||||
issuer_crl_revoked_serials,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_crl_rsync_uri,
|
||||
validation_time,
|
||||
)?;
|
||||
|
||||
let ee_x509 = parse_x509_cert(ee_cert_der)?;
|
||||
verify_ee_cert_signature(&ee_x509, issuer_spki)?;
|
||||
validate_ee_key_usage(&ee_x509)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ee_cert_path_bindings_and_status(
|
||||
ee: &ResourceCertificate,
|
||||
issuer_ca: &ResourceCertificate,
|
||||
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);
|
||||
}
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CertPathError::IssuerNotCa);
|
||||
}
|
||||
issuer_ca
|
||||
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
||||
.map_err(CertPathError::IssuerProfile)?;
|
||||
|
||||
if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CertPathError::IssuerSubjectMismatch {
|
||||
ee_issuer_dn: ee.tbs.issuer_name.to_string(),
|
||||
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
if let Some(expected_crl_uri) = issuer_crl_rsync_uri {
|
||||
validate_ee_crldp_contains_issuer_crl_uri(ee, expected_crl_uri)?;
|
||||
}
|
||||
|
||||
if !time_within_validity(
|
||||
validation_time,
|
||||
ee.tbs.validity_not_before,
|
||||
ee.tbs.validity_not_after,
|
||||
) || !time_within_validity(
|
||||
validation_time,
|
||||
issuer_ca.tbs.validity_not_before,
|
||||
issuer_ca.tbs.validity_not_after,
|
||||
) {
|
||||
return Err(CertPathError::CertificateNotValidAtTime);
|
||||
}
|
||||
|
||||
if !crl_valid_at_time(issuer_crl, validation_time) {
|
||||
return Err(CertPathError::CrlNotValidAtTime);
|
||||
}
|
||||
|
||||
let serial = BigUnsigned::from_biguint(&ee.tbs.serial_number);
|
||||
if issuer_crl_revoked_serials.contains(&serial.bytes_be) {
|
||||
return Err(CertPathError::EeRevoked);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_subject_pki_from_der(der: &[u8]) -> Result<SubjectPublicKeyInfo<'_>, CertPathError> {
|
||||
let (rem, spki) = SubjectPublicKeyInfo::from_der(der)
|
||||
.map_err(|e| CertPathError::IssuerSpkiParse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CertPathError::IssuerSpkiTrailingBytes(rem.len()));
|
||||
}
|
||||
Ok(spki)
|
||||
}
|
||||
|
||||
fn parse_x509_cert(der: &[u8]) -> Result<X509Certificate<'_>, CertPathError> {
|
||||
let (rem, cert) = X509Certificate::from_der(der)
|
||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CertPathError::EeSignatureInvalid(
|
||||
"trailing bytes after EE certificate".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(cert)
|
||||
}
|
||||
|
||||
fn verify_ee_cert_signature(
|
||||
ee: &X509Certificate<'_>,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
) -> Result<(), CertPathError> {
|
||||
ee.verify_signature(Some(issuer_spki))
|
||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
||||
}
|
||||
|
||||
fn verify_ee_cert_signature_fast(
|
||||
ee_tbs_der: &[u8],
|
||||
ee_signature_bytes: &[u8],
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
) -> Result<(), CertPathError> {
|
||||
let key = signature::UnparsedPublicKey::new(
|
||||
&signature::RSA_PKCS1_2048_8192_SHA256,
|
||||
&issuer_spki.subject_public_key.data,
|
||||
);
|
||||
key.verify(ee_tbs_der, ee_signature_bytes)
|
||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
||||
}
|
||||
|
||||
fn validate_ee_aki_matches_issuer_ski(
|
||||
ee: &ResourceCertificate,
|
||||
issuer_ca: &ResourceCertificate,
|
||||
) -> Result<(), CertPathError> {
|
||||
let Some(issuer_ski) = issuer_ca.tbs.extensions.subject_key_identifier.as_deref() else {
|
||||
return Err(CertPathError::IssuerSkiMissing);
|
||||
};
|
||||
let Some(ee_aki) = ee.tbs.extensions.authority_key_identifier.as_deref() else {
|
||||
return Err(CertPathError::EeAkiMissing);
|
||||
};
|
||||
if ee_aki != issuer_ski {
|
||||
return Err(CertPathError::EeAkiMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ee_aia_points_to_issuer_uri(
|
||||
ee: &ResourceCertificate,
|
||||
issuer_ca_rsync_uri: &str,
|
||||
) -> Result<(), CertPathError> {
|
||||
let Some(uris) = ee.tbs.extensions.ca_issuers_uris.as_ref() else {
|
||||
return Err(CertPathError::EeAiaMissing);
|
||||
};
|
||||
if uris.iter().any(|u| u.as_str() == issuer_ca_rsync_uri) {
|
||||
return Ok(());
|
||||
}
|
||||
if uris.iter().any(|u| u.starts_with("rsync://")) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(CertPathError::EeAiaIssuerUriMismatch)
|
||||
}
|
||||
|
||||
fn validate_ee_crldp_contains_issuer_crl_uri(
|
||||
ee: &ResourceCertificate,
|
||||
issuer_crl_rsync_uri: &str,
|
||||
) -> Result<(), CertPathError> {
|
||||
let Some(uris) = ee.tbs.extensions.crl_distribution_points_uris.as_ref() else {
|
||||
return Err(CertPathError::EeCrlDpMissing);
|
||||
};
|
||||
if !uris.iter().any(|u| u.as_str() == issuer_crl_rsync_uri) {
|
||||
return Err(CertPathError::EeCrlDpUriMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ee_key_usage(cert: &X509Certificate<'_>) -> Result<(), CertPathError> {
|
||||
let mut ku_critical: Option<bool> = None;
|
||||
for ext in cert.extensions() {
|
||||
if ext.oid.as_bytes() == crate::model::oid::OID_KEY_USAGE_RAW {
|
||||
ku_critical = Some(ext.critical);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(critical) = ku_critical else {
|
||||
return Err(CertPathError::KeyUsageMissing);
|
||||
};
|
||||
if !critical {
|
||||
return Err(CertPathError::KeyUsageNotCritical);
|
||||
}
|
||||
|
||||
let Some(ku) = cert
|
||||
.key_usage()
|
||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?
|
||||
else {
|
||||
return Err(CertPathError::KeyUsageMissing);
|
||||
};
|
||||
|
||||
let v = &ku.value;
|
||||
let ok = v.digital_signature()
|
||||
&& !v.key_cert_sign()
|
||||
&& !v.crl_sign()
|
||||
&& !v.non_repudiation()
|
||||
&& !v.key_encipherment()
|
||||
&& !v.data_encipherment()
|
||||
&& !v.key_agreement()
|
||||
&& !v.encipher_only()
|
||||
&& !v.decipher_only();
|
||||
if !ok {
|
||||
return Err(CertPathError::KeyUsageInvalidBits);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ee_key_usage_summary(summary: &EeKeyUsageSummary) -> Result<(), CertPathError> {
|
||||
match summary {
|
||||
EeKeyUsageSummary::DigitalSignatureOnly => Ok(()),
|
||||
EeKeyUsageSummary::Missing => Err(CertPathError::KeyUsageMissing),
|
||||
EeKeyUsageSummary::NotCritical => Err(CertPathError::KeyUsageNotCritical),
|
||||
EeKeyUsageSummary::InvalidBits => Err(CertPathError::KeyUsageInvalidBits),
|
||||
EeKeyUsageSummary::ParseError(err) => Err(CertPathError::KeyUsageParse(err.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
fn time_within_validity(
|
||||
t: time::OffsetDateTime,
|
||||
not_before: time::OffsetDateTime,
|
||||
not_after: time::OffsetDateTime,
|
||||
) -> bool {
|
||||
let t = t.to_offset(time::UtcOffset::UTC);
|
||||
let not_before = not_before.to_offset(time::UtcOffset::UTC);
|
||||
let not_after = not_after.to_offset(time::UtcOffset::UTC);
|
||||
t >= not_before && t <= not_after
|
||||
}
|
||||
|
||||
fn crl_valid_at_time(crl: &RpkixCrl, t: time::OffsetDateTime) -> bool {
|
||||
let t = t.to_offset(time::UtcOffset::UTC);
|
||||
let this_update = crl.this_update.utc.to_offset(time::UtcOffset::UTC);
|
||||
let next_update = crl.next_update.utc.to_offset(time::UtcOffset::UTC);
|
||||
t >= this_update && t < next_update
|
||||
}
|
||||
|
||||
fn is_serial_revoked_by_crl(ee: &ResourceCertificate, crl: &RpkixCrl) -> bool {
|
||||
let serial = BigUnsigned::from_biguint(&ee.tbs.serial_number);
|
||||
crl.revoked_certs
|
||||
.iter()
|
||||
.any(|rc| rc.serial_number == serial)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::model::common::X509NameDer;
|
||||
use crate::model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use crate::model::rc::{
|
||||
BasicConstraintsProfile, CertificatePoliciesProfile, RcExtensions, ResourceCertKind,
|
||||
ResourceCertificate, RpkixTbsCertificate,
|
||||
};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
|
||||
fn dummy_cert(
|
||||
kind: ResourceCertKind,
|
||||
subject_dn: &str,
|
||||
issuer_dn: &str,
|
||||
ski: Option<Vec<u8>>,
|
||||
aki: Option<Vec<u8>>,
|
||||
aia: Option<Vec<&str>>,
|
||||
crldp: Option<Vec<&str>>,
|
||||
) -> ResourceCertificate {
|
||||
let aia = aia.map(|v| v.into_iter().map(|s| s.to_string()).collect::<Vec<_>>());
|
||||
let crldp = crldp.map(|v| v.into_iter().map(|s| s.to_string()).collect::<Vec<_>>());
|
||||
ResourceCertificate {
|
||||
raw_der: Vec::new(),
|
||||
kind,
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_name: X509NameDer(issuer_dn.as_bytes().to_vec()),
|
||||
subject_name: X509NameDer(subject_dn.as_bytes().to_vec()),
|
||||
validity_not_before: time::OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: time::OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: kind == ResourceCertKind::Ca,
|
||||
basic_constraints: (kind == ResourceCertKind::Ca).then_some(
|
||||
BasicConstraintsProfile {
|
||||
ca: true,
|
||||
critical: true,
|
||||
path_len_constraint: None,
|
||||
},
|
||||
),
|
||||
subject_key_identifier: ski,
|
||||
authority_key_identifier: aki,
|
||||
crl_distribution_points_uris: crldp,
|
||||
ca_issuers_uris: aia,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: (kind == ResourceCertKind::Ca)
|
||||
.then_some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
certificate_policies: (kind == ResourceCertKind::Ca).then_some(
|
||||
CertificatePoliciesProfile {
|
||||
policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(),
|
||||
qualifier_oids: Vec::new(),
|
||||
},
|
||||
),
|
||||
extension_oids: Vec::new(),
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_aki_and_aia_and_crldp_binding_checks_report_errors() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let ee = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![9]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_ee_aki_matches_issuer_ski(&ee, &issuer).unwrap_err();
|
||||
assert!(matches!(err, CertPathError::EeAkiMismatch), "{err}");
|
||||
|
||||
let issuer_missing_ski = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let err = validate_ee_aki_matches_issuer_ski(&ee, &issuer_missing_ski).unwrap_err();
|
||||
assert!(matches!(err, CertPathError::IssuerSkiMissing), "{err}");
|
||||
|
||||
let ee_missing_aki = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
None,
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_ee_aki_matches_issuer_ski(&ee_missing_aki, &issuer).unwrap_err();
|
||||
assert!(matches!(err, CertPathError::EeAkiMissing), "{err}");
|
||||
|
||||
let ee_missing_aia = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
None,
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_ee_aia_points_to_issuer_uri(
|
||||
&ee_missing_aia,
|
||||
"rsync://example.test/issuer.cer",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CertPathError::EeAiaMissing), "{err}");
|
||||
|
||||
let ee_wrong_aia = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/other.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
validate_ee_aia_points_to_issuer_uri(&ee_wrong_aia, "rsync://example.test/issuer.cer")
|
||||
.expect("non-matching rsync AIA is currently accepted");
|
||||
|
||||
let ee_missing_crldp = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
None,
|
||||
);
|
||||
let err = validate_ee_crldp_contains_issuer_crl_uri(
|
||||
&ee_missing_crldp,
|
||||
"rsync://example.test/issuer.crl",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CertPathError::EeCrlDpMissing), "{err}");
|
||||
|
||||
let ee_wrong_crldp = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/other.crl"]),
|
||||
);
|
||||
let err = validate_ee_crldp_contains_issuer_crl_uri(
|
||||
&ee_wrong_crldp,
|
||||
"rsync://example.test/issuer.crl",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CertPathError::EeCrlDpUriMismatch), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_binding_checks_accept_when_matching() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let ee = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
validate_ee_aki_matches_issuer_ski(&ee, &issuer).expect("aki ok");
|
||||
validate_ee_aia_points_to_issuer_uri(&ee, "rsync://example.test/issuer.cer")
|
||||
.expect("aia ok");
|
||||
validate_ee_crldp_contains_issuer_crl_uri(&ee, "rsync://example.test/issuer.crl")
|
||||
.expect("crldp ok");
|
||||
}
|
||||
}
|
||||
533
src/validation/from_tal.rs
Normal file
533
src/validation/from_tal.rs
Normal file
@@ -0,0 +1,533 @@
|
||||
use url::Url;
|
||||
|
||||
use crate::model::ta::{TrustAnchor, TrustAnchorError};
|
||||
use crate::model::tal::{Tal, TalDecodeError};
|
||||
use crate::repository::fetch::rsync::RsyncFetcher;
|
||||
use crate::repository::sync::rrdp::Fetcher;
|
||||
use crate::validation::ca_instance::{
|
||||
CaInstanceUris, CaInstanceUrisError, ca_instance_uris_from_ca_certificate,
|
||||
};
|
||||
use crate::validation::run::{RunError, RunOutput, run_publication_point_once};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DiscoveredRootCaInstance {
|
||||
pub tal_url: Option<String>,
|
||||
pub trust_anchor: TrustAnchor,
|
||||
pub ca_instance: CaInstanceUris,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunFromTalOutput {
|
||||
pub discovery: DiscoveredRootCaInstance,
|
||||
pub run: RunOutput,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FromTalError {
|
||||
#[error("TAL fetch failed: {0} (RFC 8630 §2.2)")]
|
||||
TalFetch(String),
|
||||
|
||||
#[error("TAL decode failed: {0} (RFC 8630 §2.2)")]
|
||||
TalDecode(#[from] TalDecodeError),
|
||||
|
||||
#[error("failed to fetch TA certificate from TAL: {0} (RFC 8630 §2.3)")]
|
||||
TaFetch(String),
|
||||
|
||||
#[error("failed to bind TAL and TA certificate: {0} (RFC 8630 §2.3)")]
|
||||
Bind(#[from] TrustAnchorError),
|
||||
|
||||
#[error("failed to discover CA instance URIs from TA certificate: {0}")]
|
||||
CaInstanceUris(#[from] CaInstanceUrisError),
|
||||
|
||||
#[error("run failed: {0}")]
|
||||
Run(#[from] RunError),
|
||||
|
||||
#[error("TAL contains no TA URIs (RFC 8630 §2.2)")]
|
||||
NoTaUris,
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_url(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
tal_url: &str,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
let tal_bytes = http_fetcher
|
||||
.fetch(tal_url)
|
||||
.map_err(FromTalError::TalFetch)?;
|
||||
let tal = Tal::decode_bytes(&tal_bytes)?;
|
||||
discover_root_ca_instance_from_tal(http_fetcher, tal, Some(tal_url.to_string()))
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_url_with_strict_name(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
tal_url: &str,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
let tal_bytes = http_fetcher
|
||||
.fetch(tal_url)
|
||||
.map_err(FromTalError::TalFetch)?;
|
||||
let tal = Tal::decode_bytes(&tal_bytes)?;
|
||||
discover_root_ca_instance_from_tal_with_strict_name(
|
||||
http_fetcher,
|
||||
tal,
|
||||
Some(tal_url.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_url_with_fetchers(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn RsyncFetcher,
|
||||
tal_url: &str,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
let tal_bytes = http_fetcher
|
||||
.fetch(tal_url)
|
||||
.map_err(FromTalError::TalFetch)?;
|
||||
let tal = Tal::decode_bytes(&tal_bytes)?;
|
||||
discover_root_ca_instance_from_tal_with_fetchers_impl(
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal,
|
||||
Some(tal_url.to_string()),
|
||||
false,
|
||||
TaUriOrder::HttpFirst,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_url_with_fetchers_strict_name(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn RsyncFetcher,
|
||||
tal_url: &str,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
let tal_bytes = http_fetcher
|
||||
.fetch(tal_url)
|
||||
.map_err(FromTalError::TalFetch)?;
|
||||
let tal = Tal::decode_bytes(&tal_bytes)?;
|
||||
discover_root_ca_instance_from_tal_with_fetchers_impl(
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal,
|
||||
Some(tal_url.to_string()),
|
||||
true,
|
||||
TaUriOrder::HttpFirst,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
tal: Tal,
|
||||
tal_url: Option<String>,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
discover_root_ca_instance_from_tal_impl(http_fetcher, tal, tal_url, false)
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_with_strict_name(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
tal: Tal,
|
||||
tal_url: Option<String>,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
discover_root_ca_instance_from_tal_impl(http_fetcher, tal, tal_url, true)
|
||||
}
|
||||
|
||||
fn discover_root_ca_instance_from_tal_impl(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
tal: Tal,
|
||||
tal_url: Option<String>,
|
||||
strict_name: bool,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
if tal.ta_uris.is_empty() {
|
||||
return Err(FromTalError::NoTaUris);
|
||||
}
|
||||
|
||||
let mut last_err: Option<String> = None;
|
||||
for ta_uri in tal.ta_uris.iter() {
|
||||
let ta_der = match http_fetcher.fetch(ta_uri.as_str()) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
last_err = Some(format!("fetch {ta_uri} failed: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let trust_anchor =
|
||||
match bind_trust_anchor_der(tal.clone(), &ta_der, Some(ta_uri), strict_name) {
|
||||
Ok(ta) => ta,
|
||||
Err(e) => {
|
||||
last_err = Some(format!("bind {ta_uri} failed: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let ca_instance =
|
||||
match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
last_err = Some(format!("CA instance discovery failed: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(DiscoveredRootCaInstance {
|
||||
tal_url,
|
||||
trust_anchor,
|
||||
ca_instance,
|
||||
});
|
||||
}
|
||||
|
||||
Err(FromTalError::TaFetch(last_err.unwrap_or_else(|| {
|
||||
"unknown TA candidate error".to_string()
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_with_fetchers(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn RsyncFetcher,
|
||||
tal: Tal,
|
||||
tal_url: Option<String>,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
discover_root_ca_instance_from_tal_with_fetchers_impl(
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal,
|
||||
tal_url,
|
||||
false,
|
||||
TaUriOrder::RsyncFirst,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_with_fetchers_strict_name(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn RsyncFetcher,
|
||||
tal: Tal,
|
||||
tal_url: Option<String>,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
discover_root_ca_instance_from_tal_with_fetchers_impl(
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal,
|
||||
tal_url,
|
||||
true,
|
||||
TaUriOrder::RsyncFirst,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum TaUriOrder {
|
||||
HttpFirst,
|
||||
RsyncFirst,
|
||||
}
|
||||
|
||||
fn discover_root_ca_instance_from_tal_with_fetchers_impl(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn RsyncFetcher,
|
||||
tal: Tal,
|
||||
tal_url: Option<String>,
|
||||
strict_name: bool,
|
||||
ta_uri_order: TaUriOrder,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
if tal.ta_uris.is_empty() {
|
||||
return Err(FromTalError::NoTaUris);
|
||||
}
|
||||
|
||||
let mut last_err: Option<String> = None;
|
||||
let mut ta_uris = tal.ta_uris.clone();
|
||||
ta_uris.sort_by_key(|uri| ta_uri_priority(uri, ta_uri_order));
|
||||
for ta_uri in ta_uris.iter() {
|
||||
let ta_der = match fetch_ta_der(http_fetcher, rsync_fetcher, ta_uri) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
last_err = Some(format!("fetch {ta_uri} failed: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let trust_anchor =
|
||||
match bind_trust_anchor_der(tal.clone(), &ta_der, Some(ta_uri), strict_name) {
|
||||
Ok(ta) => ta,
|
||||
Err(e) => {
|
||||
last_err = Some(format!("bind {ta_uri} failed: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let ca_instance =
|
||||
match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
last_err = Some(format!("CA instance discovery failed: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(DiscoveredRootCaInstance {
|
||||
tal_url,
|
||||
trust_anchor,
|
||||
ca_instance,
|
||||
});
|
||||
}
|
||||
|
||||
Err(FromTalError::TaFetch(last_err.unwrap_or_else(|| {
|
||||
"unknown TA candidate error".to_string()
|
||||
})))
|
||||
}
|
||||
|
||||
fn ta_uri_priority(uri: &Url, order: TaUriOrder) -> u8 {
|
||||
match (order, uri.scheme()) {
|
||||
(TaUriOrder::HttpFirst, "https" | "http") => 0,
|
||||
(TaUriOrder::HttpFirst, "rsync") => 1,
|
||||
(TaUriOrder::RsyncFirst, "rsync") => 0,
|
||||
(TaUriOrder::RsyncFirst, "https" | "http") => 1,
|
||||
_ => 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_ta_der(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn RsyncFetcher,
|
||||
ta_uri: &Url,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
match ta_uri.scheme() {
|
||||
"https" | "http" => http_fetcher.fetch(ta_uri.as_str()),
|
||||
"rsync" => fetch_ta_der_via_rsync(rsync_fetcher, ta_uri.as_str()),
|
||||
scheme => Err(format!("unsupported TA URI scheme: {scheme}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_trust_anchor_der(
|
||||
tal: Tal,
|
||||
ta_der: &[u8],
|
||||
resolved_uri: Option<&Url>,
|
||||
strict_name: bool,
|
||||
) -> Result<TrustAnchor, TrustAnchorError> {
|
||||
if strict_name {
|
||||
TrustAnchor::bind_der_with_strict_name(tal, ta_der, resolved_uri)
|
||||
} else {
|
||||
TrustAnchor::bind_der(tal, ta_der, resolved_uri)
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_ta_der_via_rsync(
|
||||
rsync_fetcher: &dyn RsyncFetcher,
|
||||
ta_rsync_uri: &str,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
rsync_fetcher
|
||||
.fetch_object(ta_rsync_uri)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_and_ta_der(
|
||||
tal_bytes: &[u8],
|
||||
ta_der: &[u8],
|
||||
resolved_ta_uri: Option<&Url>,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
discover_root_ca_instance_from_tal_and_ta_der_impl(tal_bytes, ta_der, resolved_ta_uri, false)
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
|
||||
tal_bytes: &[u8],
|
||||
ta_der: &[u8],
|
||||
resolved_ta_uri: Option<&Url>,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
discover_root_ca_instance_from_tal_and_ta_der_impl(tal_bytes, ta_der, resolved_ta_uri, true)
|
||||
}
|
||||
|
||||
fn discover_root_ca_instance_from_tal_and_ta_der_impl(
|
||||
tal_bytes: &[u8],
|
||||
ta_der: &[u8],
|
||||
resolved_ta_uri: Option<&Url>,
|
||||
strict_name: bool,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
let tal = Tal::decode_bytes(tal_bytes)?;
|
||||
let trust_anchor = bind_trust_anchor_der(tal, ta_der, resolved_ta_uri, strict_name)?;
|
||||
let ca_instance = ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca)?;
|
||||
Ok(DiscoveredRootCaInstance {
|
||||
tal_url: None,
|
||||
trust_anchor,
|
||||
ca_instance,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn canonical_tal_rsync_uri_from_bytes(tal_bytes: &[u8]) -> Result<Url, FromTalError> {
|
||||
let tal = Tal::decode_bytes(tal_bytes)?;
|
||||
tal.ta_uris
|
||||
.iter()
|
||||
.find(|uri| uri.scheme() == "rsync")
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
FromTalError::TaFetch("TAL contains no rsync TA URI for offline TA binding".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_root_from_tal_url_once(
|
||||
store: &crate::repository::storage::RocksStore,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_url: &str,
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn crate::repository::fetch::rsync::RsyncFetcher,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<RunFromTalOutput, FromTalError> {
|
||||
let discovery =
|
||||
discover_root_ca_instance_from_tal_url_with_fetchers(http_fetcher, rsync_fetcher, tal_url)?;
|
||||
|
||||
let run = run_publication_point_once(
|
||||
store,
|
||||
policy,
|
||||
discovery.ca_instance.rrdp_notification_uri.as_deref(),
|
||||
&discovery.ca_instance.rsync_base_uri,
|
||||
&discovery.ca_instance.manifest_rsync_uri,
|
||||
&discovery.ca_instance.publication_point_rsync_uri,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
&discovery.trust_anchor.ta_certificate.raw_der,
|
||||
None,
|
||||
discovery
|
||||
.trust_anchor
|
||||
.ta_certificate
|
||||
.rc_ca
|
||||
.tbs
|
||||
.extensions
|
||||
.ip_resources
|
||||
.as_ref(),
|
||||
discovery
|
||||
.trust_anchor
|
||||
.ta_certificate
|
||||
.rc_ca
|
||||
.tbs
|
||||
.extensions
|
||||
.as_resources
|
||||
.as_ref(),
|
||||
validation_time,
|
||||
)?;
|
||||
|
||||
Ok(RunFromTalOutput { discovery, run })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::repository::fetch::rsync::LocalDirRsyncFetcher;
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct MapHttpFetcher {
|
||||
map: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Fetcher for MapHttpFetcher {
|
||||
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
||||
self.map
|
||||
.get(uri)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("no fixture mapped for {uri}"))
|
||||
}
|
||||
}
|
||||
|
||||
struct FailingHttpFetcher;
|
||||
|
||||
impl Fetcher for FailingHttpFetcher {
|
||||
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
||||
Err(format!("blocked test HTTP fetch: {uri}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_ca_instance_from_tal_with_fetchers_supports_rsync_ta_uri() {
|
||||
let fixture = crate::test_support::synthetic_repository();
|
||||
let tal_bytes = std::fs::read(fixture.tal()).unwrap();
|
||||
let ta_der = std::fs::read(fixture.trust_anchor()).unwrap();
|
||||
let tal = Tal::decode_bytes(&tal_bytes).unwrap();
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let mirror_root = td.path().join("mirror");
|
||||
std::fs::create_dir_all(&mirror_root).unwrap();
|
||||
std::fs::write(mirror_root.join("ta.cer"), ta_der).unwrap();
|
||||
|
||||
let rsync = LocalDirRsyncFetcher::new(mirror_root);
|
||||
let discovery = discover_root_ca_instance_from_tal_with_fetchers(
|
||||
&FailingHttpFetcher,
|
||||
&rsync,
|
||||
tal,
|
||||
None,
|
||||
)
|
||||
.expect("discover via rsync TA fallback");
|
||||
assert!(
|
||||
discovery
|
||||
.trust_anchor
|
||||
.resolved_ta_uri
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.starts_with("rsync://")
|
||||
);
|
||||
}
|
||||
|
||||
struct PanicRsyncFetcher;
|
||||
|
||||
impl RsyncFetcher for PanicRsyncFetcher {
|
||||
fn fetch_objects(
|
||||
&self,
|
||||
rsync_base_uri: &str,
|
||||
) -> crate::repository::fetch::rsync::RsyncFetchResult<Vec<(String, Vec<u8>)>> {
|
||||
panic!("rsync should not be used when HTTPS TA URI is available: {rsync_base_uri}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_ca_instance_from_tal_url_with_fetchers_prefers_https_ta_uri() {
|
||||
let tal_url = "https://example.test/apnic.tal";
|
||||
let fixture = crate::test_support::synthetic_repository();
|
||||
let tal_bytes = std::fs::read(fixture.tal()).unwrap();
|
||||
let ta_der = std::fs::read(fixture.trust_anchor()).unwrap();
|
||||
let https_uri = Tal::decode_bytes(&tal_bytes)
|
||||
.unwrap()
|
||||
.ta_uris
|
||||
.into_iter()
|
||||
.find(|uri| uri.scheme() == "https")
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let mut map = HashMap::new();
|
||||
map.insert(tal_url.to_string(), tal_bytes);
|
||||
map.insert(https_uri.clone(), ta_der);
|
||||
let discovery = discover_root_ca_instance_from_tal_url_with_fetchers(
|
||||
&MapHttpFetcher { map },
|
||||
&PanicRsyncFetcher,
|
||||
tal_url,
|
||||
)
|
||||
.expect("discover via HTTPS TA URI");
|
||||
assert_eq!(
|
||||
discovery.trust_anchor.resolved_ta_uri.unwrap().as_str(),
|
||||
https_uri
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_ca_instance_from_tal_url_with_fetchers_supports_rsync_only_tal() {
|
||||
let tal_url = "https://example.test/apnic-rsync-only.tal";
|
||||
let fixture = crate::test_support::synthetic_repository();
|
||||
let tal_bytes = std::fs::read_to_string(fixture.tal()).unwrap();
|
||||
let rsync_uri = Tal::decode_bytes(tal_bytes.as_bytes())
|
||||
.unwrap()
|
||||
.ta_uris
|
||||
.into_iter()
|
||||
.find(|uri| uri.scheme() == "rsync")
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let key_material = tal_bytes
|
||||
.split_once("\n\n")
|
||||
.map(|(_, key)| key)
|
||||
.expect("fixture contains TAL key material");
|
||||
let rsync_only_tal = format!("{rsync_uri}\n\n{key_material}");
|
||||
let ta_der = std::fs::read(fixture.trust_anchor()).unwrap();
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let mirror_root = td.path().join("mirror");
|
||||
std::fs::create_dir_all(&mirror_root).unwrap();
|
||||
std::fs::write(mirror_root.join("ta.cer"), ta_der).unwrap();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(tal_url.to_string(), rsync_only_tal.into_bytes());
|
||||
let discovery = discover_root_ca_instance_from_tal_url_with_fetchers(
|
||||
&MapHttpFetcher { map },
|
||||
&LocalDirRsyncFetcher::new(mirror_root),
|
||||
tal_url,
|
||||
)
|
||||
.expect("discover via rsync-only TAL");
|
||||
assert_eq!(
|
||||
discovery.trust_anchor.resolved_ta_uri.unwrap().as_str(),
|
||||
rsync_uri
|
||||
);
|
||||
}
|
||||
}
|
||||
19
src/validation/manifest.rs
Normal file
19
src/validation/manifest.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::model::manifest::{ManifestDecodeError, ManifestObject, ManifestValidateError};
|
||||
use crate::model::signed_object::SignedObjectVerifyError;
|
||||
use crate::output::report::Warning;
|
||||
use crate::repository::current_repo_index::CurrentRepoIndexHandle;
|
||||
use crate::repository::storage::{PackFile, PackTime, RocksStore, StorageError};
|
||||
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
|
||||
use crate::validation::policy::Policy;
|
||||
use crate::validation::publication_point::PublicationPointSnapshot;
|
||||
use sha2::Digest;
|
||||
use std::cmp::Ordering;
|
||||
use x509_parser::prelude::FromDer;
|
||||
|
||||
include!("manifest/models_and_process.rs");
|
||||
include!("manifest/helpers.rs");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
96
src/validation/manifest/helpers.rs
Normal file
96
src/validation/manifest/helpers.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
// Manifest URI and embedded certificate validation helpers.
|
||||
|
||||
fn cmp_minimal_be_unsigned(a: &[u8], b: &[u8]) -> Ordering {
|
||||
// Compare two minimal big-endian byte strings as unsigned integers.
|
||||
// (Leading zeros are not expected; callers store minimal big-endian.)
|
||||
a.len().cmp(&b.len()).then_with(|| a.cmp(b))
|
||||
}
|
||||
|
||||
fn join_rsync_dir_and_file(base: &str, file_name: &str) -> String {
|
||||
if base.ends_with('/') {
|
||||
format!("{base}{file_name}")
|
||||
} else {
|
||||
format!("{base}/{file_name}")
|
||||
}
|
||||
}
|
||||
|
||||
fn rsync_uri_is_under_publication_point(uri: &str, publication_point_rsync_uri: &str) -> bool {
|
||||
let pp = if publication_point_rsync_uri.ends_with('/') {
|
||||
publication_point_rsync_uri.to_string()
|
||||
} else {
|
||||
format!("{publication_point_rsync_uri}/")
|
||||
};
|
||||
uri.starts_with(&pp)
|
||||
}
|
||||
|
||||
fn validate_manifest_embedded_ee_cert_path(
|
||||
manifest: &ManifestObject,
|
||||
files: &[crate::repository::storage::PackFile],
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<(), ManifestFreshError> {
|
||||
let ee = &manifest.signed_object.signed_data.certificates[0];
|
||||
|
||||
let crl_files = files
|
||||
.iter()
|
||||
.filter(|f| f.rsync_uri.ends_with(".crl"))
|
||||
.collect::<Vec<_>>();
|
||||
if crl_files.is_empty() {
|
||||
return Err(ManifestFreshError::NoCrlFiles);
|
||||
}
|
||||
|
||||
let Some(crldp_uris) = ee
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.crl_distribution_points_uris
|
||||
.as_ref()
|
||||
else {
|
||||
return Err(ManifestFreshError::EeCrlDpMissing);
|
||||
};
|
||||
|
||||
for u in crldp_uris {
|
||||
let s = u.as_str();
|
||||
if let Some(f) = crl_files.iter().find(|f| f.rsync_uri == s) {
|
||||
let crl_bytes = f.bytes().map_err(|e| ManifestFreshError::MissingFile {
|
||||
rsync_uri: format!("{s} ({e})"),
|
||||
})?;
|
||||
let issuer_ca = crate::model::rc::ResourceCertificate::decode_der(issuer_ca_der)
|
||||
.map_err(CertPathError::IssuerDecode)?;
|
||||
let (rem, issuer_spki) = x509_parser::x509::SubjectPublicKeyInfo::from_der(
|
||||
&issuer_ca.tbs.subject_public_key_info,
|
||||
)
|
||||
.map_err(|e| CertPathError::IssuerSpkiParse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CertPathError::IssuerSpkiTrailingBytes(rem.len()).into());
|
||||
}
|
||||
let issuer_crl = crate::model::crl::RpkixCrl::decode_der(crl_bytes)
|
||||
.map_err(CertPathError::from)?;
|
||||
let revoked_serials = issuer_crl
|
||||
.revoked_certs
|
||||
.iter()
|
||||
.map(|rc| rc.serial_number.bytes_be.clone())
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
validate_signed_object_ee_cert_path_fast(
|
||||
ee,
|
||||
&issuer_ca,
|
||||
&issuer_spki,
|
||||
&issuer_crl,
|
||||
&revoked_serials,
|
||||
issuer_ca_rsync_uri,
|
||||
Some(f.rsync_uri.as_str()),
|
||||
validation_time,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ManifestFreshError::EeCrlNotFound(
|
||||
crldp_uris
|
||||
.iter()
|
||||
.map(|u| u.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
))
|
||||
}
|
||||
679
src/validation/manifest/models_and_process.rs
Normal file
679
src/validation/manifest/models_and_process.rs
Normal file
@@ -0,0 +1,679 @@
|
||||
// Publication-point models and manifest processing pipeline.
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PublicationPointSource {
|
||||
Fresh,
|
||||
FailedFetchNoCache,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublicationPointResult {
|
||||
pub source: PublicationPointSource,
|
||||
pub snapshot: PublicationPointSnapshot,
|
||||
pub warnings: Vec<Warning>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FreshValidatedPublicationPoint {
|
||||
pub manifest_rsync_uri: String,
|
||||
pub publication_point_rsync_uri: String,
|
||||
pub manifest_number_be: Vec<u8>,
|
||||
pub this_update: PackTime,
|
||||
pub next_update: PackTime,
|
||||
pub verified_at: PackTime,
|
||||
pub manifest_bytes: Vec<u8>,
|
||||
pub files: Vec<PackFile>,
|
||||
}
|
||||
|
||||
pub trait PublicationPointData {
|
||||
fn manifest_rsync_uri(&self) -> &str;
|
||||
fn publication_point_rsync_uri(&self) -> &str;
|
||||
fn manifest_number_be(&self) -> &[u8];
|
||||
fn this_update(&self) -> &PackTime;
|
||||
fn next_update(&self) -> &PackTime;
|
||||
fn verified_at(&self) -> &PackTime;
|
||||
fn manifest_bytes(&self) -> &[u8];
|
||||
fn files(&self) -> &[PackFile];
|
||||
}
|
||||
|
||||
impl FreshValidatedPublicationPoint {
|
||||
pub fn to_publication_point_snapshot(&self) -> PublicationPointSnapshot {
|
||||
PublicationPointSnapshot {
|
||||
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
|
||||
manifest_rsync_uri: self.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: self.publication_point_rsync_uri.clone(),
|
||||
manifest_number_be: self.manifest_number_be.clone(),
|
||||
this_update: self.this_update.clone(),
|
||||
next_update: self.next_update.clone(),
|
||||
verified_at: self.verified_at.clone(),
|
||||
manifest_bytes: self.manifest_bytes.clone(),
|
||||
files: self.files.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicationPointData for FreshValidatedPublicationPoint {
|
||||
fn manifest_rsync_uri(&self) -> &str {
|
||||
&self.manifest_rsync_uri
|
||||
}
|
||||
|
||||
fn publication_point_rsync_uri(&self) -> &str {
|
||||
&self.publication_point_rsync_uri
|
||||
}
|
||||
|
||||
fn manifest_number_be(&self) -> &[u8] {
|
||||
self.manifest_number_be.as_slice()
|
||||
}
|
||||
|
||||
fn this_update(&self) -> &PackTime {
|
||||
&self.this_update
|
||||
}
|
||||
|
||||
fn next_update(&self) -> &PackTime {
|
||||
&self.next_update
|
||||
}
|
||||
|
||||
fn verified_at(&self) -> &PackTime {
|
||||
&self.verified_at
|
||||
}
|
||||
|
||||
fn manifest_bytes(&self) -> &[u8] {
|
||||
self.manifest_bytes.as_slice()
|
||||
}
|
||||
|
||||
fn files(&self) -> &[PackFile] {
|
||||
self.files.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicationPointData for PublicationPointSnapshot {
|
||||
fn manifest_rsync_uri(&self) -> &str {
|
||||
&self.manifest_rsync_uri
|
||||
}
|
||||
|
||||
fn publication_point_rsync_uri(&self) -> &str {
|
||||
&self.publication_point_rsync_uri
|
||||
}
|
||||
|
||||
fn manifest_number_be(&self) -> &[u8] {
|
||||
self.manifest_number_be.as_slice()
|
||||
}
|
||||
|
||||
fn this_update(&self) -> &PackTime {
|
||||
&self.this_update
|
||||
}
|
||||
|
||||
fn next_update(&self) -> &PackTime {
|
||||
&self.next_update
|
||||
}
|
||||
|
||||
fn verified_at(&self) -> &PackTime {
|
||||
&self.verified_at
|
||||
}
|
||||
|
||||
fn manifest_bytes(&self) -> &[u8] {
|
||||
self.manifest_bytes.as_slice()
|
||||
}
|
||||
|
||||
fn files(&self) -> &[PackFile] {
|
||||
self.files.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestFreshError {
|
||||
#[error("manifest freshness state unavailable: {0}")]
|
||||
Storage(#[from] StorageError),
|
||||
#[error("repo sync failed: {detail} (RFC 8182 §3.4.5; RFC 9286 §6.6)")]
|
||||
RepoSyncFailed { detail: String },
|
||||
|
||||
#[error(
|
||||
"manifest not found in current repository view: {manifest_rsync_uri} (RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
MissingManifest { manifest_rsync_uri: String },
|
||||
|
||||
#[error("manifest decode failed: {0} (RFC 9286 §4; RFC 9286 §6.2; RFC 9286 §6.6)")]
|
||||
Decode(#[from] ManifestDecodeError),
|
||||
|
||||
#[error(
|
||||
"manifest embedded EE certificate resources invalid: {0} (RFC 9286 §5.1; RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
EeResources(#[from] ManifestValidateError),
|
||||
|
||||
#[error(
|
||||
"manifest CMS signature verification failed: {0} (RFC 6488 §3; RFC 9589 §4; RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
Signature(#[from] SignedObjectVerifyError),
|
||||
|
||||
#[error(
|
||||
"manifest embedded EE certificate path validation failed: {0} (RFC 6488 §3; RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
EeCertPath(#[from] CertPathError),
|
||||
|
||||
#[error(
|
||||
"manifest embedded EE certificate CRLDistributionPoints missing (cannot validate EE certificate) (RFC 6487 §4.8.6; RFC 6488 §3; RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
EeCrlDpMissing,
|
||||
|
||||
#[error(
|
||||
"publication point contains no CRL files (cannot validate manifest EE certificate) (RFC 9286 §7; RFC 6487 §4.8.6; RFC 6488 §3; RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
NoCrlFiles,
|
||||
|
||||
#[error(
|
||||
"CRL referenced by manifest embedded EE certificate CRLDistributionPoints not found at publication point: {0} (RFC 6487 §4.8.6; RFC 9286 §4.2.1; RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
EeCrlNotFound(String),
|
||||
|
||||
#[error(
|
||||
"manifest is not valid at validation_time: this_update={this_update_rfc3339_utc} next_update={next_update_rfc3339_utc} validation_time={validation_time_rfc3339_utc} (RFC 9286 §6.3; RFC 9286 §6.6)"
|
||||
)]
|
||||
StaleOrEarly {
|
||||
this_update_rfc3339_utc: String,
|
||||
next_update_rfc3339_utc: String,
|
||||
validation_time_rfc3339_utc: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"manifest must reside at the same publication point as id-ad-caRepository: manifest={manifest_rsync_uri} publication_point={publication_point_rsync_uri} (RFC 9286 §6.1; RFC 9286 §6.6)"
|
||||
)]
|
||||
ManifestOutsidePublicationPoint {
|
||||
manifest_rsync_uri: String,
|
||||
publication_point_rsync_uri: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"manifestNumber not higher than previously validated manifest: old={old_hex} new={new_hex} (RFC 9286 §4.2.1; RFC 9286 §6.6)"
|
||||
)]
|
||||
ManifestNumberNotIncreasing { old_hex: String, new_hex: String },
|
||||
|
||||
#[error(
|
||||
"thisUpdate not more recent than previously validated manifest: old={old_rfc3339_utc} new={new_rfc3339_utc} (RFC 9286 §4.2.1; RFC 9286 §6.6)"
|
||||
)]
|
||||
ThisUpdateNotIncreasing {
|
||||
old_rfc3339_utc: String,
|
||||
new_rfc3339_utc: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"manifest referenced file missing in current repository view: {rsync_uri} (RFC 9286 §6.4; RFC 9286 §6.6)"
|
||||
)]
|
||||
MissingFile { rsync_uri: String },
|
||||
|
||||
#[error("manifest file hash mismatch: {rsync_uri} (RFC 9286 §6.5; RFC 9286 §6.6)")]
|
||||
HashMismatch { rsync_uri: String },
|
||||
|
||||
#[error("issuer CA certificate bytes unavailable: {detail} (RFC 6487 §4; RFC 9286 §6.2)")]
|
||||
IssuerCaLoadFailed { detail: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestProcessError {
|
||||
#[error("manifest processing failed: {0}")]
|
||||
StopAllOutput(#[from] ManifestFreshError),
|
||||
|
||||
#[error("storage error during manifest processing: {0}")]
|
||||
Storage(#[from] StorageError),
|
||||
}
|
||||
|
||||
pub fn process_manifest_publication_point(
|
||||
store: &RocksStore,
|
||||
policy: &Policy,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<PublicationPointResult, ManifestProcessError> {
|
||||
process_manifest_publication_point_after_repo_sync(
|
||||
store,
|
||||
policy,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
validation_time,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn process_manifest_publication_point_fresh_after_repo_sync(
|
||||
store: &RocksStore,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
repo_sync_ok: bool,
|
||||
repo_sync_error: Option<&str>,
|
||||
) -> Result<FreshValidatedPublicationPoint, ManifestFreshError> {
|
||||
process_manifest_publication_point_fresh_after_repo_sync_with_timing(
|
||||
store,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
None,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
validation_time,
|
||||
repo_sync_ok,
|
||||
repo_sync_error,
|
||||
)
|
||||
.map(|(fresh, _timing)| fresh)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct FreshPublicationPointTimingBreakdown {
|
||||
pub current_index_lock_ms: u64,
|
||||
pub manifest_load_ms: u64,
|
||||
pub manifest_index_lookup_ms: u64,
|
||||
pub manifest_blob_load_ms: u64,
|
||||
pub manifest_decode_ms: u64,
|
||||
pub anti_rollback_guard_ms: u64,
|
||||
pub anti_rollback_meta_hit: bool,
|
||||
pub anti_rollback_meta_miss: bool,
|
||||
pub manifest_entries_ms: u64,
|
||||
pub pack_files_ms: u64,
|
||||
pub pack_files_index_lookup_ms: u64,
|
||||
pub pack_files_blob_load_ms: u64,
|
||||
pub ee_path_validate_ms: u64,
|
||||
pub manifest_file_count: usize,
|
||||
}
|
||||
|
||||
pub fn process_manifest_publication_point_fresh_after_repo_sync_with_timing(
|
||||
store: &RocksStore,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
current_repo_index: Option<&CurrentRepoIndexHandle>,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
repo_sync_ok: bool,
|
||||
repo_sync_error: Option<&str>,
|
||||
) -> Result<
|
||||
(
|
||||
FreshValidatedPublicationPoint,
|
||||
FreshPublicationPointTimingBreakdown,
|
||||
),
|
||||
ManifestFreshError,
|
||||
> {
|
||||
if repo_sync_ok {
|
||||
try_build_fresh_publication_point_with_timing(
|
||||
store,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
current_repo_index,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
} else {
|
||||
Err(ManifestFreshError::RepoSyncFailed {
|
||||
detail: repo_sync_error.unwrap_or("repo sync failed").to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_manifest_publication_point_after_repo_sync(
|
||||
store: &RocksStore,
|
||||
policy: &Policy,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
repo_sync_ok: bool,
|
||||
repo_sync_error: Option<&str>,
|
||||
) -> Result<PublicationPointResult, ManifestProcessError> {
|
||||
let fresh = if repo_sync_ok {
|
||||
try_build_fresh_publication_point(
|
||||
store,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
} else {
|
||||
Err(ManifestFreshError::RepoSyncFailed {
|
||||
detail: repo_sync_error.unwrap_or("repo sync failed").to_string(),
|
||||
})
|
||||
};
|
||||
|
||||
match fresh {
|
||||
Ok(fresh_point) => Ok(PublicationPointResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
snapshot: fresh_point.to_publication_point_snapshot(),
|
||||
warnings: Vec::new(),
|
||||
}),
|
||||
Err(fresh_err) => {
|
||||
let _ = policy;
|
||||
Err(ManifestProcessError::StopAllOutput(fresh_err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_and_validate_manifest_with_current_time(
|
||||
manifest_bytes: &[u8],
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<ManifestObject, ManifestFreshError> {
|
||||
let manifest = ManifestObject::decode_der(manifest_bytes)?;
|
||||
manifest.validate_embedded_ee_cert()?;
|
||||
manifest.signed_object.verify()?;
|
||||
|
||||
let this_update = manifest
|
||||
.manifest
|
||||
.this_update
|
||||
.to_offset(time::UtcOffset::UTC);
|
||||
let next_update = manifest
|
||||
.manifest
|
||||
.next_update
|
||||
.to_offset(time::UtcOffset::UTC);
|
||||
let now = validation_time.to_offset(time::UtcOffset::UTC);
|
||||
if now < this_update || now > next_update {
|
||||
return Err(ManifestFreshError::StaleOrEarly {
|
||||
this_update_rfc3339_utc: this_update
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("format thisUpdate"),
|
||||
next_update_rfc3339_utc: next_update
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("format nextUpdate"),
|
||||
validation_time_rfc3339_utc: now
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("format validation_time"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
pub(crate) fn try_build_fresh_publication_point(
|
||||
store: &RocksStore,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<FreshValidatedPublicationPoint, ManifestFreshError> {
|
||||
try_build_fresh_publication_point_with_timing(
|
||||
store,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
None,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
.map(|(fresh, _timing)| fresh)
|
||||
}
|
||||
|
||||
pub(crate) fn try_build_fresh_publication_point_with_timing(
|
||||
store: &RocksStore,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
current_repo_index: Option<&CurrentRepoIndexHandle>,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<
|
||||
(
|
||||
FreshValidatedPublicationPoint,
|
||||
FreshPublicationPointTimingBreakdown,
|
||||
),
|
||||
ManifestFreshError,
|
||||
> {
|
||||
let mut timing = FreshPublicationPointTimingBreakdown::default();
|
||||
let current_index_lock_started = std::time::Instant::now();
|
||||
let current_index_guard = current_repo_index.and_then(|handle| handle.read().ok());
|
||||
timing.current_index_lock_ms = current_index_lock_started.elapsed().as_millis() as u64;
|
||||
|
||||
if !rsync_uri_is_under_publication_point(manifest_rsync_uri, publication_point_rsync_uri) {
|
||||
return Err(ManifestFreshError::ManifestOutsidePublicationPoint {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
publication_point_rsync_uri: publication_point_rsync_uri.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let manifest_load_started = std::time::Instant::now();
|
||||
let manifest_bytes = if let Some(index) = current_index_guard.as_ref() {
|
||||
let manifest_lookup_started = std::time::Instant::now();
|
||||
let current = index.get_by_uri(manifest_rsync_uri).ok_or_else(|| {
|
||||
ManifestFreshError::MissingManifest {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
}
|
||||
})?;
|
||||
timing.manifest_index_lookup_ms = manifest_lookup_started.elapsed().as_millis() as u64;
|
||||
let manifest_blob_load_started = std::time::Instant::now();
|
||||
store
|
||||
.get_blob_bytes(¤t.current_hash_hex)
|
||||
.map_err(|e| ManifestFreshError::MissingManifest {
|
||||
manifest_rsync_uri: format!("{manifest_rsync_uri} ({e})"),
|
||||
})?
|
||||
.ok_or_else(|| ManifestFreshError::MissingManifest {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
})
|
||||
.inspect(|_| {
|
||||
timing.manifest_blob_load_ms =
|
||||
manifest_blob_load_started.elapsed().as_millis() as u64;
|
||||
})?
|
||||
} else {
|
||||
let manifest_blob_load_started = std::time::Instant::now();
|
||||
store
|
||||
.load_current_object_bytes_by_uri(manifest_rsync_uri)
|
||||
.map_err(|e| ManifestFreshError::MissingManifest {
|
||||
manifest_rsync_uri: format!("{manifest_rsync_uri} ({e})"),
|
||||
})?
|
||||
.ok_or_else(|| ManifestFreshError::MissingManifest {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
})
|
||||
.inspect(|_| {
|
||||
timing.manifest_blob_load_ms =
|
||||
manifest_blob_load_started.elapsed().as_millis() as u64;
|
||||
})?
|
||||
};
|
||||
timing.manifest_load_ms = manifest_load_started.elapsed().as_millis() as u64;
|
||||
|
||||
let manifest_decode_started = std::time::Instant::now();
|
||||
let manifest =
|
||||
decode_and_validate_manifest_with_current_time(&manifest_bytes, validation_time)?;
|
||||
timing.manifest_decode_ms = manifest_decode_started.elapsed().as_millis() as u64;
|
||||
|
||||
let this_update = manifest
|
||||
.manifest
|
||||
.this_update
|
||||
.to_offset(time::UtcOffset::UTC);
|
||||
let next_update = manifest
|
||||
.manifest
|
||||
.next_update
|
||||
.to_offset(time::UtcOffset::UTC);
|
||||
let now = validation_time.to_offset(time::UtcOffset::UTC);
|
||||
|
||||
// RFC 9286 §4.2.1: rollback detection for manifestNumber and thisUpdate.
|
||||
//
|
||||
// Important nuance for revalidation across runs:
|
||||
// - If the manifestNumber is equal to the previously validated manifestNumber *and* the
|
||||
// manifest bytes are identical, then this is the same manifest being revalidated and MUST
|
||||
// be accepted (otherwise, RPs would incorrectly treat stable repositories as "failed fetch"
|
||||
// and continue the current validation run with a clear failure if the
|
||||
// repository cannot provide a valid manifest.
|
||||
// - If manifestNumber is equal but the manifest bytes differ, treat this as invalid (a
|
||||
// repository is not allowed to change the manifest while keeping the manifestNumber).
|
||||
// - If manifestNumber is lower, treat as rollback and reject.
|
||||
// - If manifestNumber is higher, require thisUpdate to be more recent than the previously
|
||||
// validated thisUpdate.
|
||||
let anti_rollback_started = std::time::Instant::now();
|
||||
if let Some(old_meta) = store
|
||||
.get_manifest_anti_rollback_meta(manifest_rsync_uri)?
|
||||
{
|
||||
timing.anti_rollback_meta_hit = true;
|
||||
if old_meta.manifest_rsync_uri == manifest_rsync_uri {
|
||||
let new_num = manifest.manifest.manifest_number.bytes_be.as_slice();
|
||||
let old_num = old_meta.manifest_number_be.as_slice();
|
||||
match cmp_minimal_be_unsigned(new_num, old_num) {
|
||||
Ordering::Greater => {
|
||||
let old_this_update = old_meta
|
||||
.manifest_this_update
|
||||
.parse()
|
||||
.expect("manifest anti-rollback metadata ensures thisUpdate parses");
|
||||
if this_update <= old_this_update {
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
return Err(ManifestFreshError::ThisUpdateNotIncreasing {
|
||||
old_rfc3339_utc: old_this_update
|
||||
.to_offset(time::UtcOffset::UTC)
|
||||
.format(&Rfc3339)
|
||||
.expect("format old thisUpdate"),
|
||||
new_rfc3339_utc: this_update
|
||||
.format(&Rfc3339)
|
||||
.expect("format new thisUpdate"),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ordering::Equal => {
|
||||
let new_manifest_hash = sha2::Sha256::digest(&manifest_bytes);
|
||||
if old_meta.manifest_sha256.as_slice() != new_manifest_hash.as_slice() {
|
||||
return Err(ManifestFreshError::ManifestNumberNotIncreasing {
|
||||
old_hex: hex::encode_upper(old_num),
|
||||
new_hex: hex::encode_upper(new_num),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ordering::Less => {
|
||||
return Err(ManifestFreshError::ManifestNumberNotIncreasing {
|
||||
old_hex: hex::encode_upper(old_num),
|
||||
new_hex: hex::encode_upper(new_num),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
timing.anti_rollback_meta_miss = true;
|
||||
}
|
||||
timing.anti_rollback_guard_ms = anti_rollback_started.elapsed().as_millis() as u64;
|
||||
|
||||
let manifest_entries_started = std::time::Instant::now();
|
||||
let entries = manifest
|
||||
.manifest
|
||||
.parse_files()
|
||||
.map_err(ManifestDecodeError::Validate)?;
|
||||
timing.manifest_entries_ms = manifest_entries_started.elapsed().as_millis() as u64;
|
||||
timing.manifest_file_count = entries.len();
|
||||
let mut files = Vec::with_capacity(manifest.manifest.file_count());
|
||||
let pack_files_started = std::time::Instant::now();
|
||||
let external_raw_store = store
|
||||
.external_raw_store_ref()
|
||||
.cloned()
|
||||
.map(std::sync::Arc::new);
|
||||
let external_repo_bytes = store
|
||||
.external_repo_bytes_ref()
|
||||
.cloned()
|
||||
.map(std::sync::Arc::new);
|
||||
let mut pack_files_index_lookup_duration = std::time::Duration::ZERO;
|
||||
let mut pack_files_blob_load_duration = std::time::Duration::ZERO;
|
||||
for entry in &entries {
|
||||
let rsync_uri =
|
||||
join_rsync_dir_and_file(publication_point_rsync_uri, entry.file_name.as_str());
|
||||
let current_object = if let Some(index) = current_index_guard.as_ref() {
|
||||
let index_lookup_started = std::time::Instant::now();
|
||||
let current =
|
||||
index
|
||||
.get_by_uri(&rsync_uri)
|
||||
.ok_or_else(|| ManifestFreshError::MissingFile {
|
||||
rsync_uri: rsync_uri.clone(),
|
||||
})?;
|
||||
pack_files_index_lookup_duration += index_lookup_started.elapsed();
|
||||
crate::repository::storage::CurrentObjectWithHash {
|
||||
current_hash_hex: current.current_hash_hex.clone(),
|
||||
current_hash: current.current_hash,
|
||||
bytes: Vec::new(),
|
||||
}
|
||||
} else {
|
||||
let blob_load_started = std::time::Instant::now();
|
||||
store
|
||||
.load_current_object_with_hash_by_uri(&rsync_uri)
|
||||
.map_err(|_e| ManifestFreshError::MissingFile {
|
||||
rsync_uri: rsync_uri.clone(),
|
||||
})?
|
||||
.ok_or_else(|| ManifestFreshError::MissingFile {
|
||||
rsync_uri: rsync_uri.clone(),
|
||||
})
|
||||
.inspect(|_| {
|
||||
pack_files_blob_load_duration += blob_load_started.elapsed();
|
||||
})?
|
||||
};
|
||||
|
||||
if current_object.current_hash != entry.hash_bytes {
|
||||
return Err(ManifestFreshError::HashMismatch { rsync_uri });
|
||||
}
|
||||
|
||||
if let (Some(_), Some(repo_bytes)) =
|
||||
(current_index_guard.as_ref(), external_repo_bytes.as_ref())
|
||||
{
|
||||
files.push(PackFile::from_lazy_repo_bytes(
|
||||
rsync_uri,
|
||||
current_object.current_hash_hex,
|
||||
current_object.current_hash,
|
||||
repo_bytes.clone(),
|
||||
));
|
||||
} else if let (Some(_), Some(raw_store)) =
|
||||
(current_index_guard.as_ref(), external_raw_store.as_ref())
|
||||
{
|
||||
files.push(PackFile::from_lazy_external_raw_store(
|
||||
rsync_uri,
|
||||
current_object.current_hash_hex,
|
||||
current_object.current_hash,
|
||||
raw_store.clone(),
|
||||
));
|
||||
} else {
|
||||
let bytes = if current_object.bytes.is_empty() {
|
||||
let blob_load_started = std::time::Instant::now();
|
||||
store
|
||||
.get_blob_bytes(¤t_object.current_hash_hex)
|
||||
.map_err(|_e| ManifestFreshError::MissingFile {
|
||||
rsync_uri: rsync_uri.clone(),
|
||||
})?
|
||||
.ok_or_else(|| ManifestFreshError::MissingFile {
|
||||
rsync_uri: rsync_uri.clone(),
|
||||
})
|
||||
.inspect(|_| {
|
||||
pack_files_blob_load_duration += blob_load_started.elapsed();
|
||||
})?
|
||||
} else {
|
||||
current_object.bytes
|
||||
};
|
||||
files.push(PackFile::from_bytes_with_sha256(
|
||||
rsync_uri,
|
||||
bytes,
|
||||
current_object.current_hash,
|
||||
));
|
||||
}
|
||||
}
|
||||
timing.pack_files_index_lookup_ms = pack_files_index_lookup_duration.as_millis() as u64;
|
||||
timing.pack_files_blob_load_ms = pack_files_blob_load_duration.as_millis() as u64;
|
||||
timing.pack_files_ms = pack_files_started.elapsed().as_millis() as u64;
|
||||
|
||||
// RFC 6488 §3: manifest (signed object) validity includes a valid EE cert path.
|
||||
// We validate this after §6.4/§6.5 so the issuer CRL can be selected from the publication point.
|
||||
let ee_path_validate_started = std::time::Instant::now();
|
||||
validate_manifest_embedded_ee_cert_path(
|
||||
&manifest,
|
||||
&files,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
validation_time,
|
||||
)?;
|
||||
timing.ee_path_validate_ms = ee_path_validate_started.elapsed().as_millis() as u64;
|
||||
|
||||
Ok((
|
||||
FreshValidatedPublicationPoint {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
publication_point_rsync_uri: publication_point_rsync_uri.to_string(),
|
||||
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||
this_update: PackTime::from_utc_offset_datetime(this_update),
|
||||
next_update: PackTime::from_utc_offset_datetime(next_update),
|
||||
verified_at: PackTime::from_utc_offset_datetime(now),
|
||||
manifest_bytes,
|
||||
files,
|
||||
},
|
||||
timing,
|
||||
))
|
||||
}
|
||||
606
src/validation/manifest/tests.rs
Normal file
606
src/validation/manifest/tests.rs
Normal file
@@ -0,0 +1,606 @@
|
||||
//! Manifest processing regression tests.
|
||||
use super::*;
|
||||
use crate::repository::current_repo_index::CurrentRepoIndex;
|
||||
use crate::repository::storage::{RawByHashEntry, RocksStore};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn fixture_repository() -> PathBuf {
|
||||
crate::test_support::synthetic_repository().case_repository("baseline-v1")
|
||||
}
|
||||
|
||||
fn manifest_fixture_path() -> PathBuf {
|
||||
fixture_repository().join("child/child.mft")
|
||||
}
|
||||
|
||||
fn issuer_ca_fixture_der() -> Vec<u8> {
|
||||
std::fs::read(fixture_repository().join("root/child.cer")).expect("read synthetic issuer CA")
|
||||
}
|
||||
|
||||
fn issuer_ca_rsync_uri() -> &'static str {
|
||||
"rsync://rpki-local-rsync:873/custom/root/child.cer"
|
||||
}
|
||||
|
||||
fn load_manifest_fixture() -> (
|
||||
ManifestObject,
|
||||
Vec<u8>,
|
||||
String,
|
||||
String,
|
||||
time::OffsetDateTime,
|
||||
) {
|
||||
let manifest_path = manifest_fixture_path();
|
||||
let manifest_bytes = std::fs::read(&manifest_path).expect("read synthetic manifest");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode synthetic manifest");
|
||||
let manifest_rsync_uri = "rsync://rpki-local-rsync:873/custom/child/child.mft".to_string();
|
||||
let publication_point_rsync_uri = "rsync://rpki-local-rsync:873/custom/child/".to_string();
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
(
|
||||
manifest,
|
||||
manifest_bytes,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
}
|
||||
|
||||
fn raw_by_hash_entry(uri: &str, bytes: Vec<u8>, object_type: &str) -> RawByHashEntry {
|
||||
let mut entry = RawByHashEntry::from_bytes(hex::encode(sha2::Sha256::digest(&bytes)), bytes);
|
||||
entry.origin_uris.push(uri.to_string());
|
||||
entry.object_type = Some(object_type.to_string());
|
||||
entry.encoding = Some("der".to_string());
|
||||
entry
|
||||
}
|
||||
|
||||
fn put_current_object(store: &RocksStore, rsync_uri: &str, bytes: Vec<u8>, object_type: &str) {
|
||||
let hash = hex::encode(sha2::Sha256::digest(&bytes));
|
||||
store
|
||||
.put_raw_by_hash_entry(&raw_by_hash_entry(rsync_uri, bytes, object_type))
|
||||
.expect("put raw_by_hash entry");
|
||||
store
|
||||
.put_repository_view_entry(&crate::repository::storage::RepositoryViewEntry {
|
||||
rsync_uri: rsync_uri.to_string(),
|
||||
current_hash: Some(hash),
|
||||
repository_source: Some("https://example.test/notification.xml".to_string()),
|
||||
object_type: Some(object_type.to_string()),
|
||||
state: crate::repository::storage::RepositoryViewState::Present,
|
||||
})
|
||||
.expect("put repository view entry");
|
||||
}
|
||||
|
||||
fn put_complete_publication_point_current_objects(
|
||||
store: &RocksStore,
|
||||
manifest: &ManifestObject,
|
||||
manifest_rsync_uri: &str,
|
||||
manifest_bytes: Vec<u8>,
|
||||
publication_point_rsync_uri: &str,
|
||||
) {
|
||||
put_current_object(store, manifest_rsync_uri, manifest_bytes, "mft");
|
||||
for entry in manifest.manifest.parse_files().expect("parse files") {
|
||||
let file_path = manifest_fixture_path()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(entry.file_name.as_str());
|
||||
let bytes = std::fs::read(&file_path).expect("read fixture file");
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin");
|
||||
put_current_object(store, &rsync_uri, bytes, object_type);
|
||||
}
|
||||
}
|
||||
|
||||
fn put_raw_only(store: &RocksStore, rsync_uri: &str, bytes: Vec<u8>, object_type: &str) {
|
||||
store
|
||||
.put_raw_by_hash_entry(&raw_by_hash_entry(rsync_uri, bytes, object_type))
|
||||
.expect("put raw_by_hash entry");
|
||||
}
|
||||
|
||||
fn locked_files_for_manifest(
|
||||
manifest: &ManifestObject,
|
||||
publication_point_rsync_uri: &str,
|
||||
) -> Vec<PackFile> {
|
||||
let manifest_path = manifest_fixture_path();
|
||||
manifest
|
||||
.manifest
|
||||
.parse_files()
|
||||
.expect("parse files")
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
let file_path = manifest_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(entry.file_name.as_str());
|
||||
let bytes = std::fs::read(&file_path).unwrap_or_else(|_| {
|
||||
panic!("read fixture file referenced by manifest: {file_path:?}")
|
||||
});
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
format!("{publication_point_rsync_uri}{}", entry.file_name),
|
||||
bytes,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_build_fresh_publication_point_rejects_manifest_outside_publication_point() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let (_, _, manifest_rsync_uri, _, validation_time) = load_manifest_fixture();
|
||||
let err = try_build_fresh_publication_point(
|
||||
&store,
|
||||
&manifest_rsync_uri,
|
||||
"rsync://example.test/other/",
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
ManifestFreshError::ManifestOutsidePublicationPoint { .. }
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_build_fresh_publication_point_reports_missing_manifest_when_raw_store_is_empty() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let (_, _, manifest_rsync_uri, publication_point_rsync_uri, validation_time) =
|
||||
load_manifest_fixture();
|
||||
let err = try_build_fresh_publication_point(
|
||||
&store,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, ManifestFreshError::MissingManifest { .. }),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_build_fresh_publication_point_reports_missing_locked_file() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let (
|
||||
manifest,
|
||||
manifest_bytes,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
validation_time,
|
||||
) = load_manifest_fixture();
|
||||
put_current_object(&store, &manifest_rsync_uri, manifest_bytes, "mft");
|
||||
let first_non_crl = manifest
|
||||
.manifest
|
||||
.parse_files()
|
||||
.expect("parse files")
|
||||
.into_iter()
|
||||
.find(|entry| !entry.file_name.ends_with(".crl"))
|
||||
.expect("fixture non-crl entry");
|
||||
let file_path = manifest_fixture_path()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(first_non_crl.file_name.as_str());
|
||||
let bytes = std::fs::read(&file_path).expect("read fixture file");
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", first_non_crl.file_name);
|
||||
let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin");
|
||||
put_current_object(&store, &rsync_uri, bytes, object_type);
|
||||
|
||||
let err = try_build_fresh_publication_point(
|
||||
&store,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, ManifestFreshError::MissingFile { .. }),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_build_fresh_publication_point_detects_hash_mismatch_via_repository_view_hash() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let (
|
||||
manifest,
|
||||
manifest_bytes,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
validation_time,
|
||||
) = load_manifest_fixture();
|
||||
put_current_object(&store, &manifest_rsync_uri, manifest_bytes, "mft");
|
||||
|
||||
let entries = manifest
|
||||
.manifest
|
||||
.parse_files()
|
||||
.expect("parse files")
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let first = entries
|
||||
.iter()
|
||||
.find(|entry| !entry.file_name.ends_with(".crl"))
|
||||
.expect("synthetic manifest non-CRL entry");
|
||||
|
||||
let first_uri = format!("{publication_point_rsync_uri}{}", first.file_name);
|
||||
let object_type = first_uri.rsplit('.').next().unwrap_or("bin");
|
||||
put_current_object(
|
||||
&store,
|
||||
&first_uri,
|
||||
b"wrong object bytes".to_vec(),
|
||||
object_type,
|
||||
);
|
||||
|
||||
for entry in entries
|
||||
.iter()
|
||||
.filter(|entry| entry.file_name != first.file_name)
|
||||
{
|
||||
let file_path = manifest_fixture_path()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(entry.file_name.as_str());
|
||||
let bytes = std::fs::read(&file_path).expect("read fixture file");
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin");
|
||||
put_current_object(&store, &rsync_uri, bytes, object_type);
|
||||
}
|
||||
|
||||
let err = try_build_fresh_publication_point(
|
||||
&store,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, ManifestFreshError::HashMismatch { .. }),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_build_fresh_publication_point_uses_current_repo_index_without_repository_view() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let (
|
||||
manifest,
|
||||
manifest_bytes,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
validation_time,
|
||||
) = load_manifest_fixture();
|
||||
|
||||
put_raw_only(&store, &manifest_rsync_uri, manifest_bytes.clone(), "mft");
|
||||
let current_index = CurrentRepoIndex::shared();
|
||||
let mut entries = vec![crate::repository::storage::RepositoryViewEntry {
|
||||
rsync_uri: manifest_rsync_uri.clone(),
|
||||
current_hash: Some(hex::encode(sha2::Sha256::digest(&manifest_bytes))),
|
||||
repository_source: Some("https://example.test/notification.xml".to_string()),
|
||||
object_type: Some("mft".to_string()),
|
||||
state: crate::repository::storage::RepositoryViewState::Present,
|
||||
}];
|
||||
|
||||
for entry in manifest.manifest.parse_files().expect("parse files") {
|
||||
let file_path = manifest_fixture_path()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(entry.file_name.as_str());
|
||||
let bytes = std::fs::read(&file_path).expect("read fixture file");
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin").to_string();
|
||||
put_raw_only(&store, &rsync_uri, bytes.clone(), &object_type);
|
||||
entries.push(crate::repository::storage::RepositoryViewEntry {
|
||||
rsync_uri,
|
||||
current_hash: Some(hex::encode(sha2::Sha256::digest(&bytes))),
|
||||
repository_source: Some("https://example.test/notification.xml".to_string()),
|
||||
object_type: Some(object_type),
|
||||
state: crate::repository::storage::RepositoryViewState::Present,
|
||||
});
|
||||
}
|
||||
|
||||
current_index
|
||||
.write()
|
||||
.expect("index write lock")
|
||||
.apply_repository_view_entries(&entries)
|
||||
.expect("apply current index");
|
||||
|
||||
assert!(
|
||||
store
|
||||
.get_repository_view_entry(&manifest_rsync_uri)
|
||||
.expect("get repository view")
|
||||
.is_none()
|
||||
);
|
||||
|
||||
let (fresh, _timing) = try_build_fresh_publication_point_with_timing(
|
||||
&store,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
Some(¤t_index),
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("fresh publication point via current index");
|
||||
|
||||
assert_eq!(fresh.manifest_rsync_uri, manifest_rsync_uri);
|
||||
assert_eq!(fresh.files.len(), manifest.manifest.file_count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_build_fresh_publication_point_records_anti_rollback_meta_miss() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let (
|
||||
manifest,
|
||||
manifest_bytes,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
validation_time,
|
||||
) = load_manifest_fixture();
|
||||
put_complete_publication_point_current_objects(
|
||||
&store,
|
||||
&manifest,
|
||||
&manifest_rsync_uri,
|
||||
manifest_bytes,
|
||||
&publication_point_rsync_uri,
|
||||
);
|
||||
|
||||
let (_fresh, timing) = try_build_fresh_publication_point_with_timing(
|
||||
&store,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
None,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("fresh publication point without freshness metadata");
|
||||
|
||||
assert!(timing.anti_rollback_meta_miss);
|
||||
assert!(!timing.anti_rollback_meta_hit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_build_fresh_publication_point_uses_anti_rollback_meta_hit_for_same_manifest() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
let (
|
||||
manifest,
|
||||
manifest_bytes,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
validation_time,
|
||||
) = load_manifest_fixture();
|
||||
let previous_meta = sample_manifest_meta(
|
||||
&manifest,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&manifest_bytes,
|
||||
validation_time,
|
||||
);
|
||||
store
|
||||
.put_manifest_anti_rollback_meta(&previous_meta)
|
||||
.expect("persist freshness");
|
||||
put_complete_publication_point_current_objects(
|
||||
&store,
|
||||
&manifest,
|
||||
&manifest_rsync_uri,
|
||||
manifest_bytes,
|
||||
&publication_point_rsync_uri,
|
||||
);
|
||||
|
||||
let (_fresh, timing) = try_build_fresh_publication_point_with_timing(
|
||||
&store,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
None,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("fresh publication point with matching freshness metadata");
|
||||
|
||||
assert!(timing.anti_rollback_meta_hit);
|
||||
assert!(!timing.anti_rollback_meta_miss);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_manifest_embedded_ee_cert_path_rejects_missing_crl_files() {
|
||||
let (manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture();
|
||||
let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri)
|
||||
.into_iter()
|
||||
.filter(|f| !f.rsync_uri.ends_with(".crl"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let err = validate_manifest_embedded_ee_cert_path(
|
||||
&manifest,
|
||||
&files,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ManifestFreshError::NoCrlFiles), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_manifest_embedded_ee_cert_path_rejects_missing_ee_crldp() {
|
||||
let (mut manifest, _, _, publication_point_rsync_uri, validation_time) =
|
||||
load_manifest_fixture();
|
||||
manifest.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.crl_distribution_points_uris = None;
|
||||
let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri);
|
||||
|
||||
let err = validate_manifest_embedded_ee_cert_path(
|
||||
&manifest,
|
||||
&files,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ManifestFreshError::EeCrlDpMissing), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_manifest_embedded_ee_cert_path_rejects_unlisted_crldp_uri() {
|
||||
let (manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture();
|
||||
let mut files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri)
|
||||
.into_iter()
|
||||
.filter(|f| !f.rsync_uri.ends_with(".crl"))
|
||||
.collect::<Vec<_>>();
|
||||
files.push(PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/unrelated.crl",
|
||||
b"dummy".to_vec(),
|
||||
));
|
||||
|
||||
let err = validate_manifest_embedded_ee_cert_path(
|
||||
&manifest,
|
||||
&files,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ManifestFreshError::EeCrlNotFound(_)), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_manifest_embedded_ee_cert_path_rejects_expired_crl() {
|
||||
let (manifest, _, _, publication_point_rsync_uri, _) = load_manifest_fixture();
|
||||
let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri);
|
||||
let ee = &manifest.signed_object.signed_data.certificates[0];
|
||||
let crldp_uri = ee
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.crl_distribution_points_uris
|
||||
.as_ref()
|
||||
.and_then(|uris| uris.first())
|
||||
.expect("fixture manifest EE CRLDP")
|
||||
.as_str()
|
||||
.to_string();
|
||||
let crl_file = files
|
||||
.iter()
|
||||
.find(|file| file.rsync_uri == crldp_uri)
|
||||
.expect("fixture CRL referenced by manifest EE");
|
||||
let crl =
|
||||
crate::model::crl::RpkixCrl::decode_der(crl_file.bytes().expect("read fixture crl bytes"))
|
||||
.expect("decode fixture crl");
|
||||
let validation_time = crl.next_update.utc;
|
||||
|
||||
let err = validate_manifest_embedded_ee_cert_path(
|
||||
&manifest,
|
||||
&files,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
ManifestFreshError::EeCertPath(
|
||||
crate::validation::cert_path::CertPathError::CrlNotValidAtTime
|
||||
)
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
fn sample_manifest_meta(
|
||||
manifest: &ManifestObject,
|
||||
uri: &str,
|
||||
_base: &str,
|
||||
bytes: &[u8],
|
||||
now: time::OffsetDateTime,
|
||||
) -> crate::repository::storage::ManifestAntiRollbackMeta {
|
||||
crate::repository::storage::ManifestAntiRollbackMeta {
|
||||
manifest_rsync_uri: uri.into(),
|
||||
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
|
||||
manifest_sha256: sha2::Sha256::digest(bytes).to_vec(),
|
||||
updated_at_validation_time: PackTime::from_utc_offset_datetime(now),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn history_survives_restart_and_rejects_rollback_without_advancing() {
|
||||
for case in 0..4 {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let (manifest, bytes, uri, base, now) = load_manifest_fixture();
|
||||
let mut meta = sample_manifest_meta(&manifest, &uri, &base, &bytes, now);
|
||||
match case {
|
||||
0 => {}
|
||||
1 => {
|
||||
meta.manifest_number_be = vec![0x7f; 20];
|
||||
}
|
||||
2 => {
|
||||
meta.manifest_sha256 = vec![0x55; 32];
|
||||
}
|
||||
3 => {
|
||||
meta.manifest_number_be = vec![0];
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
{
|
||||
let store = RocksStore::open(dir.path()).unwrap();
|
||||
store.put_manifest_anti_rollback_meta(&meta).unwrap();
|
||||
put_complete_publication_point_current_objects(
|
||||
&store,
|
||||
&manifest,
|
||||
&uri,
|
||||
bytes.clone(),
|
||||
&base,
|
||||
);
|
||||
}
|
||||
let store = RocksStore::open(dir.path()).unwrap();
|
||||
assert_eq!(
|
||||
store.get_manifest_anti_rollback_meta(&uri).unwrap(),
|
||||
Some(meta.clone())
|
||||
);
|
||||
let result = try_build_fresh_publication_point_with_timing(
|
||||
&store,
|
||||
&uri,
|
||||
&base,
|
||||
None,
|
||||
&issuer_ca_fixture_der(),
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
now,
|
||||
);
|
||||
match case {
|
||||
0 => {
|
||||
assert!(result.unwrap().1.anti_rollback_meta_hit);
|
||||
}
|
||||
1 | 2 => {
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ManifestFreshError::ManifestNumberNotIncreasing { .. })
|
||||
));
|
||||
}
|
||||
3 => {
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ManifestFreshError::ThisUpdateNotIncreasing { .. })
|
||||
));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert_eq!(
|
||||
store.get_manifest_anti_rollback_meta(&uri).unwrap(),
|
||||
Some(meta)
|
||||
);
|
||||
}
|
||||
}
|
||||
14
src/validation/mod.rs
Normal file
14
src/validation/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub mod ca_instance;
|
||||
pub mod ca_path;
|
||||
pub mod cert_path;
|
||||
pub mod from_tal;
|
||||
pub mod manifest;
|
||||
pub mod objects;
|
||||
pub mod policy;
|
||||
pub mod publication_point;
|
||||
pub mod run;
|
||||
pub mod run_tree_from_tal;
|
||||
pub mod tree;
|
||||
pub mod tree_parallel;
|
||||
pub mod tree_runner;
|
||||
pub mod x509_name;
|
||||
36
src/validation/objects.rs
Normal file
36
src/validation/objects.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::model::aspa::{AspaDecodeError, AspaObject, AspaValidateError};
|
||||
use crate::model::manifest::ManifestObject;
|
||||
use crate::model::rc::{
|
||||
AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressOrRange, IpPrefix as RcIpPrefix,
|
||||
ResourceCertificate,
|
||||
};
|
||||
use crate::model::roa::{IpPrefix, RoaAfi, RoaDecodeError, RoaObject, RoaValidateError};
|
||||
use crate::model::signed_object::{RpkiSignedObject, SignedObjectVerifyError};
|
||||
use crate::output::analysis::timing::TimingHandle;
|
||||
use crate::output::audit::{
|
||||
AuditObjectKind, AuditObjectResult, ObjectAuditEntry, sha256_hex_from_32,
|
||||
};
|
||||
use crate::output::report::{RfcRef, Warning, WarningCategory};
|
||||
use crate::repository::storage::{PackFile, PackTime};
|
||||
use crate::scheduler::config::ParallelPhase2Config;
|
||||
use crate::scheduler::object_worker::{
|
||||
ObjectTaskExecutor, ObjectWorkerPool, ObjectWorkerSubmitError,
|
||||
};
|
||||
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
|
||||
use crate::validation::manifest::PublicationPointData;
|
||||
use crate::validation::policy::{Policy, ResourceValidationMode, SignedObjectFailurePolicy};
|
||||
use crate::validation::publication_point::PublicationPointSnapshot;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
|
||||
include!("objects/outputs.rs");
|
||||
include!("objects/serial_processing.rs");
|
||||
include!("objects/parallel_processing.rs");
|
||||
include!("objects/parallel_stage.rs");
|
||||
include!("objects/object_validation.rs");
|
||||
include!("objects/resource_validation.rs");
|
||||
100
src/validation/objects/object_validation.rs
Normal file
100
src/validation/objects/object_validation.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
fn process_roa_with_issuer(
|
||||
file: &PackFile,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca: &ResourceCertificate,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
crl_states: &mut std::collections::HashMap<String, IssuerCrlState>,
|
||||
issuer_resources_index: &IssuerResourcesIndex,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
strict_cms_der: bool,
|
||||
strict_name: bool,
|
||||
resource_validation_mode: ResourceValidationMode,
|
||||
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
||||
) -> Result<Vec<Vrp>, ObjectValidateError> {
|
||||
let roa = {
|
||||
let _span = timing.as_ref().map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
|
||||
RoaObject::decode_der_with_strict_options(file.bytes().map_err(ObjectValidateError::BytesLoad)?, strict_cms_der, strict_name)?
|
||||
};
|
||||
roa.validate_embedded_ee_cert()?;
|
||||
roa.signed_object.verify()?;
|
||||
let ee = &roa.signed_object.signed_data.certificates[0];
|
||||
let crl_uri = choose_crl_uri_for_certificate(ee.resource_cert.tbs.extensions.crl_distribution_points_uris.as_ref(), crl_states)?;
|
||||
let verified_crl = ensure_issuer_crl_verified(crl_uri, crl_states, issuer_ca_der)?;
|
||||
validate_signed_object_ee_cert_path_fast(ee, issuer_ca, issuer_spki, &verified_crl.crl, &verified_crl.revoked_serials, issuer_ca_rsync_uri, Some(crl_uri), validation_time)?;
|
||||
let ee_vrs = validate_ee_resources_for_mode(&ee.resource_cert, issuer_effective_ip, issuer_effective_as, issuer_resources_index, resource_validation_mode)?;
|
||||
if let Some(constraints) = ta_constraints { constraints.validate_ee_certificate(&ee.resource_cert)?; }
|
||||
roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())
|
||||
}
|
||||
|
||||
fn process_roa_with_issuer_parallel(
|
||||
file: &PackFile,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca: &ResourceCertificate,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
crl_states: &Mutex<std::collections::HashMap<String, IssuerCrlState>>,
|
||||
issuer_resources_index: &IssuerResourcesIndex,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
strict_cms_der: bool,
|
||||
strict_name: bool,
|
||||
resource_validation_mode: ResourceValidationMode,
|
||||
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
||||
) -> Result<Vec<Vrp>, ObjectValidateError> {
|
||||
let roa = {
|
||||
let _span = timing.as_ref().map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
|
||||
RoaObject::decode_der_with_strict_options(file.bytes().map_err(ObjectValidateError::BytesLoad)?, strict_cms_der, strict_name)?
|
||||
};
|
||||
roa.validate_embedded_ee_cert()?;
|
||||
roa.signed_object.verify()?;
|
||||
let ee = &roa.signed_object.signed_data.certificates[0];
|
||||
let (crl_uri, verified_crl) = {
|
||||
let mut states = crl_states.lock().expect("parallel issuer CRL state lock");
|
||||
let uri = choose_crl_uri_for_certificate(ee.resource_cert.tbs.extensions.crl_distribution_points_uris.as_ref(), &states)?.to_string();
|
||||
let value = ensure_issuer_crl_verified(&uri, &mut states, issuer_ca_der)?;
|
||||
(uri, value)
|
||||
};
|
||||
validate_signed_object_ee_cert_path_fast(ee, issuer_ca, issuer_spki, &verified_crl.crl, &verified_crl.revoked_serials, issuer_ca_rsync_uri, Some(crl_uri.as_str()), validation_time)?;
|
||||
let ee_vrs = validate_ee_resources_for_mode(&ee.resource_cert, issuer_effective_ip, issuer_effective_as, issuer_resources_index, resource_validation_mode)?;
|
||||
if let Some(constraints) = ta_constraints { constraints.validate_ee_certificate(&ee.resource_cert)?; }
|
||||
roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())
|
||||
}
|
||||
|
||||
fn process_aspa_with_issuer(
|
||||
file: &PackFile,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca: &ResourceCertificate,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
crl_states: &mut std::collections::HashMap<String, IssuerCrlState>,
|
||||
issuer_resources_index: &IssuerResourcesIndex,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
strict_cms_der: bool,
|
||||
strict_name: bool,
|
||||
resource_validation_mode: ResourceValidationMode,
|
||||
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
||||
) -> Result<AspaAttestation, ObjectValidateError> {
|
||||
let aspa = {
|
||||
let _span = timing.as_ref().map(|t| t.span_phase("objects_aspa_decode_and_validate_total"));
|
||||
AspaObject::decode_der_with_strict_options(file.bytes().map_err(ObjectValidateError::BytesLoad)?, strict_cms_der, strict_name)?
|
||||
};
|
||||
aspa.validate_embedded_ee_cert()?;
|
||||
aspa.signed_object.verify()?;
|
||||
let ee = &aspa.signed_object.signed_data.certificates[0];
|
||||
let crl_uri = choose_crl_uri_for_certificate(ee.resource_cert.tbs.extensions.crl_distribution_points_uris.as_ref(), crl_states)?;
|
||||
let verified_crl = ensure_issuer_crl_verified(crl_uri, crl_states, issuer_ca_der)?;
|
||||
validate_signed_object_ee_cert_path_fast(ee, issuer_ca, issuer_spki, &verified_crl.crl, &verified_crl.revoked_serials, issuer_ca_rsync_uri, Some(crl_uri), validation_time)?;
|
||||
let ee_vrs = validate_ee_resources_for_mode(&ee.resource_cert, issuer_effective_ip, issuer_effective_as, issuer_resources_index, resource_validation_mode)?;
|
||||
if let Some(constraints) = ta_constraints { constraints.validate_ee_certificate(&ee.resource_cert)?; }
|
||||
validate_aspa_customer_in_vrs(&aspa, ee_vrs.asn.as_ref())?;
|
||||
Ok(AspaAttestation { customer_as_id: aspa.aspa.customer_as_id, provider_as_ids: aspa.aspa.provider_as_ids.clone() })
|
||||
}
|
||||
88
src/validation/objects/outputs.rs
Normal file
88
src/validation/objects/outputs.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
// Shared in-run validation types used by the fresh validation pipeline.
|
||||
|
||||
const RFC_NONE: &[RfcRef] = &[];
|
||||
const RFC_CRLDP: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6")];
|
||||
const RFC_CRLDP_AND_LOCKED_PACK: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §4.2.1")];
|
||||
|
||||
fn ber_compatible_cms_warning(der: &[u8], rsync_uri: &str, object_kind: &str) -> Option<Warning> {
|
||||
let strict_error = RpkiSignedObject::strict_cms_der_error(der)?;
|
||||
Some(Warning::new(format!("accepted BER-compatible CMS encoding for {object_kind}: {rsync_uri}: {strict_error}"))
|
||||
.with_category(WarningCategory::BerCompatibleCmsEncoding)
|
||||
.with_rfc_refs(&[RfcRef("X.690 §10"), RfcRef("RFC 6488 §2")])
|
||||
.with_context(rsync_uri))
|
||||
}
|
||||
fn ber_compatible_cms_warning_for_file(file: &PackFile, object_kind: &str) -> Option<Warning> {
|
||||
ber_compatible_cms_warning(file.bytes().ok()?, &file.rsync_uri, object_kind)
|
||||
}
|
||||
|
||||
fn decode_resource_certificate_with_policy(der: &[u8], policy: &Policy) -> Result<ResourceCertificate, crate::model::rc::ResourceCertificateDecodeError> {
|
||||
if policy.strict.name { ResourceCertificate::decode_der_with_strict_name(der) } else { ResourceCertificate::decode_der(der) }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct VerifiedIssuerCrl {
|
||||
pub(crate) crl: crate::model::crl::RpkixCrl,
|
||||
pub(crate) revoked_serials: std::collections::HashSet<Vec<u8>>,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum IssuerCrlState {
|
||||
Pending { bytes: Vec<u8> },
|
||||
Verified(Arc<VerifiedIssuerCrl>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct IssuerResourcesIndex {
|
||||
pub(crate) ip_v4: Option<Vec<(Vec<u8>, Vec<u8>)>>,
|
||||
pub(crate) ip_v6: Option<Vec<(Vec<u8>, Vec<u8>)>>,
|
||||
pub(crate) asnum: Option<Vec<(u32, u32)>>,
|
||||
pub(crate) rdi: Option<Vec<(u32, u32)>>,
|
||||
}
|
||||
|
||||
fn extra_rfc_refs_for_crl_selection(error: &ObjectValidateError) -> &'static [RfcRef] {
|
||||
match error { ObjectValidateError::MissingCrlDpUris => RFC_CRLDP, ObjectValidateError::CrlNotFound(_) => RFC_CRLDP_AND_LOCKED_PACK, _ => RFC_NONE }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Vrp { pub asn: u32, pub prefix: IpPrefix, pub max_length: u16 }
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AspaAttestation { pub customer_as_id: u32, pub provider_as_ids: Vec<u32> }
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RouterKeyPayload {
|
||||
pub as_id: u32,
|
||||
pub ski: Vec<u8>,
|
||||
pub spki_der: Vec<u8>,
|
||||
pub source_object_uri: String,
|
||||
pub source_object_hash: String,
|
||||
pub source_ee_cert_hash: String,
|
||||
pub item_effective_until: PackTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ObjectsOutput {
|
||||
pub vrps: Vec<Vrp>,
|
||||
pub aspas: Vec<AspaAttestation>,
|
||||
pub router_keys: Vec<RouterKeyPayload>,
|
||||
pub warnings: Vec<Warning>,
|
||||
pub stats: ObjectsStats,
|
||||
pub audit: Vec<ObjectAuditEntry>,
|
||||
}
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ObjectsStats {
|
||||
pub roa_total: usize,
|
||||
pub roa_ok: usize,
|
||||
pub aspa_total: usize,
|
||||
pub aspa_ok: usize,
|
||||
pub publication_point_dropped: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RoaTaskOk { pub(crate) vrps: Vec<Vrp> }
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RoaTaskResult {
|
||||
pub(crate) publication_point_id: u64,
|
||||
pub(crate) index: usize,
|
||||
pub(crate) worker_index: usize,
|
||||
pub(crate) queue_wait_ms: u64,
|
||||
pub(crate) worker_ms: u64,
|
||||
pub(crate) outcome: Result<RoaTaskOk, ObjectValidateError>,
|
||||
}
|
||||
331
src/validation/objects/parallel_processing.rs
Normal file
331
src/validation/objects/parallel_processing.rs
Normal file
@@ -0,0 +1,331 @@
|
||||
/// Bounded parallel ROA validation.
|
||||
///
|
||||
/// Workers share only immutable certificate/resource state and an in-run CRL
|
||||
/// verification map. Nothing in this path is persisted or reused by a later
|
||||
/// validation run.
|
||||
pub fn process_publication_point_for_issuer_parallel_roa<P: PublicationPointData>(
|
||||
publication_point: &P,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
config: &ParallelPhase2Config,
|
||||
) -> ObjectsOutput {
|
||||
if config.object_workers <= 1
|
||||
|| policy.signed_object_failure_policy == SignedObjectFailurePolicy::DropPublicationPoint
|
||||
{
|
||||
return process_publication_point_for_issuer_with_ta_constraints(
|
||||
publication_point,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
None,
|
||||
);
|
||||
}
|
||||
let Ok(pool) = ParallelRoaWorkerPool::new(config) else {
|
||||
return process_publication_point_for_issuer_with_ta_constraints(
|
||||
publication_point,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
None,
|
||||
);
|
||||
};
|
||||
process_publication_point_for_issuer_parallel_roa_with_pool(
|
||||
publication_point,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
&pool,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn process_publication_point_for_issuer_parallel_roa_with_pool<P: PublicationPointData>(
|
||||
publication_point: &P,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
pool: &ParallelRoaWorkerPool,
|
||||
) -> ObjectsOutput {
|
||||
if policy.signed_object_failure_policy == SignedObjectFailurePolicy::DropPublicationPoint {
|
||||
return process_publication_point_for_issuer_with_ta_constraints(
|
||||
publication_point,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
None,
|
||||
);
|
||||
}
|
||||
process_publication_point_for_issuer_parallel_roa_inner(
|
||||
publication_point,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
pool,
|
||||
None,
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
process_publication_point_for_issuer_with_ta_constraints(
|
||||
publication_point,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
None,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RoaTaskShared {
|
||||
locked_files: Arc<[PackFile]>,
|
||||
issuer_ca_der: Arc<[u8]>,
|
||||
issuer_ca: Arc<ResourceCertificate>,
|
||||
issuer_spki_der: Arc<[u8]>,
|
||||
issuer_ca_rsync_uri: Option<Arc<str>>,
|
||||
crl_states: Arc<Mutex<HashMap<String, IssuerCrlState>>>,
|
||||
issuer_resources_index: Arc<IssuerResourcesIndex>,
|
||||
issuer_effective_ip: Option<Arc<crate::model::rc::IpResourceSet>>,
|
||||
issuer_effective_as: Option<Arc<crate::model::rc::AsResourceSet>>,
|
||||
resource_validation_mode: ResourceValidationMode,
|
||||
ta_constraints: Option<Arc<crate::ta_constraints::TaConstraints>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct OwnedRoaTask {
|
||||
pub(crate) publication_point_id: u64,
|
||||
index: usize,
|
||||
shared: Arc<RoaTaskShared>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
strict_cms_der: bool,
|
||||
strict_name: bool,
|
||||
pub(crate) submitted_at: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RoaTaskExecutor;
|
||||
|
||||
impl ObjectTaskExecutor<OwnedRoaTask, RoaTaskResult> for RoaTaskExecutor {
|
||||
fn execute(&self, worker_index: usize, task: OwnedRoaTask) -> RoaTaskResult {
|
||||
validate_owned_roa_task(worker_index, task)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ParallelRoaWorkerPool {
|
||||
pool: Mutex<ObjectWorkerPool<OwnedRoaTask, RoaTaskResult, RoaTaskExecutor>>,
|
||||
}
|
||||
|
||||
pub(crate) enum ParallelObjectsPrepare {
|
||||
Complete(ObjectsOutput),
|
||||
Staged(ParallelObjectsStage),
|
||||
}
|
||||
|
||||
pub(crate) struct ParallelObjectsStage {
|
||||
pub(crate) publication_point_id: u64,
|
||||
pub(crate) shared: Arc<RoaTaskShared>,
|
||||
pub(crate) validation_time: time::OffsetDateTime,
|
||||
pub(crate) strict_cms_der: bool,
|
||||
pub(crate) strict_name: bool,
|
||||
pub(crate) roa_task_indices: Vec<usize>,
|
||||
pub(crate) ready_roa_results: Vec<RoaTaskResult>,
|
||||
pub(crate) warnings: Vec<Warning>,
|
||||
pub(crate) stats: ObjectsStats,
|
||||
pub(crate) audit: Vec<ObjectAuditEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ObjectValidateError {
|
||||
#[error("object bytes load failed: {0}")]
|
||||
BytesLoad(String),
|
||||
#[error("ROA decode failed: {0}")]
|
||||
RoaDecode(#[from] RoaDecodeError),
|
||||
#[error("ROA embedded EE resource validation failed: {0}")]
|
||||
RoaEeResources(#[from] RoaValidateError),
|
||||
#[error("ASPA decode failed: {0}")]
|
||||
AspaDecode(#[from] AspaDecodeError),
|
||||
#[error("ASPA embedded EE resource validation failed: {0}")]
|
||||
AspaEeResources(#[from] AspaValidateError),
|
||||
#[error("CMS signature verification failed: {0}")]
|
||||
Signature(#[from] SignedObjectVerifyError),
|
||||
#[error("EE certificate path validation failed: {0}")]
|
||||
CertPath(#[from] CertPathError),
|
||||
#[error("certificate CRLDistributionPoints URIs missing (RFC 6487 §4.8.6)")]
|
||||
MissingCrlDpUris,
|
||||
#[error("no CRL available in publication point snapshot (RFC 9286 §7)")]
|
||||
MissingCrlInPack,
|
||||
#[error("CRL referenced by CRLDistributionPoints not found in publication point snapshot: {0}")]
|
||||
CrlNotFound(String),
|
||||
#[error("issuer effective IP resources missing")]
|
||||
MissingIssuerEffectiveIp,
|
||||
#[error("issuer effective AS resources missing")]
|
||||
MissingIssuerEffectiveAs,
|
||||
#[error("EE certificate resources are not a subset of issuer effective resources")]
|
||||
EeResourcesNotSubset,
|
||||
#[error("EE certificate violates locally configured TA constraints: {0}")]
|
||||
TaConstraints(#[from] crate::ta_constraints::TaConstraintsViolation),
|
||||
}
|
||||
|
||||
impl ParallelRoaWorkerPool {
|
||||
pub fn new(config: &ParallelPhase2Config) -> Result<Self, String> {
|
||||
if config.object_workers <= 1 {
|
||||
return Err("parallel ROA worker pool requires object_workers > 1".to_string());
|
||||
}
|
||||
Ok(Self {
|
||||
pool: Mutex::new(ObjectWorkerPool::new(
|
||||
config.object_workers,
|
||||
config.worker_queue_capacity,
|
||||
RoaTaskExecutor,
|
||||
)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn try_submit_round_robin(
|
||||
&self,
|
||||
task: OwnedRoaTask,
|
||||
) -> Result<usize, ObjectWorkerSubmitError<OwnedRoaTask>> {
|
||||
self.pool
|
||||
.lock()
|
||||
.expect("parallel ROA worker pool lock")
|
||||
.try_submit_round_robin(task)
|
||||
}
|
||||
|
||||
pub(crate) fn recv_result_timeout(
|
||||
&self,
|
||||
timeout: Duration,
|
||||
) -> Result<Option<RoaTaskResult>, String> {
|
||||
self.pool
|
||||
.lock()
|
||||
.expect("parallel ROA worker pool lock")
|
||||
.recv_result_timeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskResult {
|
||||
let worker_started = Instant::now();
|
||||
let queue_wait_ms = task
|
||||
.submitted_at
|
||||
.map(|submitted_at| worker_started.saturating_duration_since(submitted_at))
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let shared = task.shared.as_ref();
|
||||
let file = shared
|
||||
.locked_files
|
||||
.get(task.index)
|
||||
.expect("ROA task index must reference locked file");
|
||||
let outcome = match SubjectPublicKeyInfo::from_der(shared.issuer_spki_der.as_ref()) {
|
||||
Ok(([], issuer_spki)) => process_roa_with_issuer_parallel(
|
||||
file,
|
||||
shared.issuer_ca_der.as_ref(),
|
||||
shared.issuer_ca.as_ref(),
|
||||
&issuer_spki,
|
||||
shared.issuer_ca_rsync_uri.as_deref(),
|
||||
shared.crl_states.as_ref(),
|
||||
shared.issuer_resources_index.as_ref(),
|
||||
shared.issuer_effective_ip.as_deref(),
|
||||
shared.issuer_effective_as.as_deref(),
|
||||
task.validation_time,
|
||||
None,
|
||||
task.strict_cms_der,
|
||||
task.strict_name,
|
||||
shared.resource_validation_mode,
|
||||
shared.ta_constraints.as_deref(),
|
||||
)
|
||||
.map(|vrps| RoaTaskOk { vrps }),
|
||||
Ok((rem, _)) => Err(ObjectValidateError::CertPath(
|
||||
CertPathError::IssuerSpkiTrailingBytes(rem.len()),
|
||||
)),
|
||||
Err(error) => Err(ObjectValidateError::CertPath(
|
||||
CertPathError::IssuerSpkiParse(error.to_string()),
|
||||
)),
|
||||
};
|
||||
RoaTaskResult {
|
||||
publication_point_id: task.publication_point_id,
|
||||
index: task.index,
|
||||
worker_index,
|
||||
queue_wait_ms,
|
||||
worker_ms: worker_started.elapsed().as_millis() as u64,
|
||||
outcome,
|
||||
}
|
||||
}
|
||||
|
||||
fn process_publication_point_for_issuer_parallel_roa_inner<P: PublicationPointData>(
|
||||
publication_point: &P,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
pool: &ParallelRoaWorkerPool,
|
||||
ta_constraints: Option<Arc<crate::ta_constraints::TaConstraints>>,
|
||||
) -> Result<ObjectsOutput, String> {
|
||||
let stage = match prepare_publication_point_for_parallel_roa_and_ta_constraints(
|
||||
0,
|
||||
publication_point,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
ta_constraints,
|
||||
) {
|
||||
ParallelObjectsPrepare::Complete(output) => return Ok(output),
|
||||
ParallelObjectsPrepare::Staged(stage) => stage,
|
||||
};
|
||||
let task_count = stage.roa_task_count();
|
||||
let mut pending = std::collections::VecDeque::with_capacity(task_count);
|
||||
stage.append_roa_tasks_to(&mut pending);
|
||||
while let Some(mut task) = pending.pop_front() {
|
||||
task.submitted_at = Some(Instant::now());
|
||||
match pool.try_submit_round_robin(task) {
|
||||
Ok(_) => {}
|
||||
Err(ObjectWorkerSubmitError::QueueFull { task, .. }) => {
|
||||
pending.push_front(task);
|
||||
std::thread::yield_now();
|
||||
}
|
||||
Err(ObjectWorkerSubmitError::Disconnected { .. }) => {
|
||||
return Err("parallel ROA worker queue disconnected".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut results = Vec::with_capacity(task_count);
|
||||
while results.len() < task_count {
|
||||
let Some(result) = pool.recv_result_timeout(Duration::from_secs(30))? else {
|
||||
return Err("parallel ROA worker timed out".to_string());
|
||||
};
|
||||
results.push(result);
|
||||
}
|
||||
reduce_parallel_roa_stage(stage, results, timing)
|
||||
}
|
||||
377
src/validation/objects/parallel_stage.rs
Normal file
377
src/validation/objects/parallel_stage.rs
Normal file
@@ -0,0 +1,377 @@
|
||||
impl ParallelObjectsStage {
|
||||
pub(crate) fn append_roa_tasks_to(
|
||||
&self,
|
||||
pending: &mut std::collections::VecDeque<OwnedRoaTask>,
|
||||
) {
|
||||
self.extend_roa_tasks(|task| pending.push_back(task));
|
||||
}
|
||||
|
||||
fn extend_roa_tasks<F>(&self, mut push: F)
|
||||
where
|
||||
F: FnMut(OwnedRoaTask),
|
||||
{
|
||||
let shared = self.shared.clone();
|
||||
for index in &self.roa_task_indices {
|
||||
push(OwnedRoaTask {
|
||||
publication_point_id: self.publication_point_id,
|
||||
index: *index,
|
||||
shared: shared.clone(),
|
||||
validation_time: self.validation_time,
|
||||
strict_cms_der: self.strict_cms_der,
|
||||
strict_name: self.strict_name,
|
||||
submitted_at: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn roa_task_count(&self) -> usize {
|
||||
self.roa_task_indices.len()
|
||||
}
|
||||
|
||||
pub(crate) fn aspa_task_count(&self) -> usize {
|
||||
self.stats.aspa_total
|
||||
}
|
||||
|
||||
pub(crate) fn locked_file_count(&self) -> usize {
|
||||
self.shared.locked_files.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_publication_point_for_parallel_roa_and_ta_constraints<
|
||||
P: PublicationPointData,
|
||||
>(
|
||||
publication_point_id: u64,
|
||||
publication_point: &P,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
ta_constraints: Option<Arc<crate::ta_constraints::TaConstraints>>,
|
||||
) -> ParallelObjectsPrepare {
|
||||
let manifest_uri = publication_point.manifest_rsync_uri();
|
||||
let locked_files = publication_point.files();
|
||||
let mut stats = ObjectsStats {
|
||||
roa_total: locked_files
|
||||
.iter()
|
||||
.filter(|f| f.rsync_uri.ends_with(".roa"))
|
||||
.count(),
|
||||
aspa_total: locked_files
|
||||
.iter()
|
||||
.filter(|f| f.rsync_uri.ends_with(".asa"))
|
||||
.count(),
|
||||
..ObjectsStats::default()
|
||||
};
|
||||
let mut warnings = Vec::new();
|
||||
let audit = Vec::new();
|
||||
let empty = |stats: ObjectsStats, warnings: Vec<Warning>| ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
warnings,
|
||||
stats,
|
||||
audit: Vec::new(),
|
||||
};
|
||||
if let Err(error) = ManifestObject::decode_der_with_strict_options(
|
||||
publication_point.manifest_bytes(),
|
||||
policy.strict.cms_der,
|
||||
policy.strict.name,
|
||||
) {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(
|
||||
Warning::new(format!(
|
||||
"dropping publication point: manifest decode failed: {error}"
|
||||
))
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §4"), RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(manifest_uri),
|
||||
);
|
||||
return ParallelObjectsPrepare::Complete(empty(stats, warnings));
|
||||
}
|
||||
if let Some(warning) = ber_compatible_cms_warning(
|
||||
publication_point.manifest_bytes(),
|
||||
manifest_uri,
|
||||
"manifest",
|
||||
) {
|
||||
warnings.push(warning);
|
||||
}
|
||||
let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(
|
||||
Warning::new(format!(
|
||||
"dropping publication point: issuer CA decode failed: {error}"
|
||||
))
|
||||
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2"), RfcRef("RFC 5280 §6.1")])
|
||||
.with_context(manifest_uri),
|
||||
);
|
||||
return ParallelObjectsPrepare::Complete(empty(stats, warnings));
|
||||
}
|
||||
};
|
||||
let issuer_spki_der = issuer_ca.tbs.subject_public_key_info.clone();
|
||||
match SubjectPublicKeyInfo::from_der(&issuer_spki_der) {
|
||||
Ok(([], _)) => {}
|
||||
Ok((remaining, _)) => {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(
|
||||
Warning::new(format!(
|
||||
"dropping publication point: issuer SPKI has {} trailing bytes",
|
||||
remaining.len()
|
||||
))
|
||||
.with_context(manifest_uri),
|
||||
);
|
||||
return ParallelObjectsPrepare::Complete(empty(stats, warnings));
|
||||
}
|
||||
Err(error) => {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(
|
||||
Warning::new(format!(
|
||||
"dropping publication point: issuer SPKI parse failed: {error}"
|
||||
))
|
||||
.with_context(manifest_uri),
|
||||
);
|
||||
return ParallelObjectsPrepare::Complete(empty(stats, warnings));
|
||||
}
|
||||
}
|
||||
let crl_states = locked_files
|
||||
.iter()
|
||||
.filter(|file| file.rsync_uri.ends_with(".crl"))
|
||||
.filter_map(|file| {
|
||||
file.bytes_cloned().ok().map(|bytes| {
|
||||
(
|
||||
file.rsync_uri.clone(),
|
||||
IssuerCrlState::Pending { bytes },
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
if crl_states.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(
|
||||
Warning::new(
|
||||
"dropping publication point: no CRL files in validated publication point",
|
||||
)
|
||||
.with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")])
|
||||
.with_context(manifest_uri),
|
||||
);
|
||||
return ParallelObjectsPrepare::Complete(empty(stats, warnings));
|
||||
}
|
||||
let roa_task_indices = locked_files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, file)| file.rsync_uri.ends_with(".roa").then_some(index))
|
||||
.collect::<Vec<_>>();
|
||||
ParallelObjectsPrepare::Staged(ParallelObjectsStage {
|
||||
publication_point_id,
|
||||
shared: Arc::new(RoaTaskShared {
|
||||
locked_files: Arc::<[PackFile]>::from(locked_files.to_vec()),
|
||||
issuer_ca_der: Arc::<[u8]>::from(issuer_ca_der.to_vec()),
|
||||
issuer_ca: Arc::new(issuer_ca),
|
||||
issuer_spki_der: Arc::<[u8]>::from(issuer_spki_der),
|
||||
issuer_ca_rsync_uri: issuer_ca_rsync_uri.map(Arc::<str>::from),
|
||||
crl_states: Arc::new(Mutex::new(crl_states)),
|
||||
issuer_resources_index: Arc::new(build_issuer_resources_index(
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
)),
|
||||
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,
|
||||
ta_constraints,
|
||||
}),
|
||||
validation_time,
|
||||
strict_cms_der: policy.strict.cms_der,
|
||||
strict_name: policy.strict.name,
|
||||
roa_task_indices,
|
||||
ready_roa_results: Vec::new(),
|
||||
warnings,
|
||||
stats,
|
||||
audit,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn reduce_parallel_roa_stage(
|
||||
stage: ParallelObjectsStage,
|
||||
mut roa_results: Vec<RoaTaskResult>,
|
||||
timing: Option<&TimingHandle>,
|
||||
) -> Result<ObjectsOutput, String> {
|
||||
roa_results.extend(stage.ready_roa_results);
|
||||
roa_results.sort_by_key(|result| result.index);
|
||||
let mut result_iter = roa_results.into_iter().peekable();
|
||||
let shared = stage.shared.clone();
|
||||
let strict_cms_der = stage.strict_cms_der;
|
||||
let strict_name = stage.strict_name;
|
||||
let issuer_spki = SubjectPublicKeyInfo::from_der(shared.issuer_spki_der.as_ref())
|
||||
.map_err(|error| error.to_string())?
|
||||
.1;
|
||||
let mut stats = stage.stats;
|
||||
let mut warnings = stage.warnings;
|
||||
let mut audit = stage.audit;
|
||||
let mut vrps = Vec::new();
|
||||
let mut aspas = Vec::new();
|
||||
let mut crl_states = shared
|
||||
.crl_states
|
||||
.lock()
|
||||
.map_err(|_| "issuer CRL state lock poisoned")?;
|
||||
for (index, file) in shared.locked_files.iter().enumerate() {
|
||||
if file.rsync_uri.ends_with(".roa") {
|
||||
let result = match result_iter.peek() {
|
||||
Some(result) if result.index == index => {
|
||||
result_iter.next().expect("peeked result")
|
||||
}
|
||||
Some(result) => {
|
||||
return Err(format!(
|
||||
"unexpected ROA task result index {} while reducing {}",
|
||||
result.index, file.rsync_uri
|
||||
));
|
||||
}
|
||||
None => return Err(format!("missing ROA task result for {}", file.rsync_uri)),
|
||||
};
|
||||
match result.outcome {
|
||||
Ok(mut output) => {
|
||||
stats.roa_ok += 1;
|
||||
vrps.append(&mut output.vrps);
|
||||
audit.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Roa,
|
||||
result: AuditObjectResult::Ok,
|
||||
detail: None,
|
||||
});
|
||||
if let Some(warning) = ber_compatible_cms_warning_for_file(file, "ROA") {
|
||||
warnings.push(warning);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
audit.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Roa,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(error.to_string()),
|
||||
});
|
||||
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
|
||||
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&error));
|
||||
warnings.push(
|
||||
Warning::new(format!(
|
||||
"dropping invalid ROA: {}: {error}",
|
||||
file.rsync_uri
|
||||
))
|
||||
.with_rfc_refs(&refs)
|
||||
.with_context(&file.rsync_uri),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if file.rsync_uri.ends_with(".asa") {
|
||||
match process_aspa_with_issuer(
|
||||
file,
|
||||
shared.issuer_ca_der.as_ref(),
|
||||
shared.issuer_ca.as_ref(),
|
||||
&issuer_spki,
|
||||
shared.issuer_ca_rsync_uri.as_deref(),
|
||||
&mut crl_states,
|
||||
shared.issuer_resources_index.as_ref(),
|
||||
shared.issuer_effective_ip.as_deref(),
|
||||
shared.issuer_effective_as.as_deref(),
|
||||
stage.validation_time,
|
||||
timing,
|
||||
strict_cms_der,
|
||||
strict_name,
|
||||
shared.resource_validation_mode,
|
||||
shared.ta_constraints.as_deref(),
|
||||
) {
|
||||
Ok(attestation) => {
|
||||
stats.aspa_ok += 1;
|
||||
aspas.push(attestation);
|
||||
audit.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Aspa,
|
||||
result: AuditObjectResult::Ok,
|
||||
detail: None,
|
||||
});
|
||||
if let Some(warning) = ber_compatible_cms_warning_for_file(file, "ASPA") {
|
||||
warnings.push(warning);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
audit.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Aspa,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(error.to_string()),
|
||||
});
|
||||
warnings.push(
|
||||
Warning::new(format!(
|
||||
"dropping invalid ASPA: {}: {error}",
|
||||
file.rsync_uri
|
||||
))
|
||||
.with_context(&file.rsync_uri),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(result) = result_iter.next() {
|
||||
return Err(format!(
|
||||
"unexpected trailing ROA task result at index {}",
|
||||
result.index
|
||||
));
|
||||
}
|
||||
Ok(ObjectsOutput {
|
||||
vrps,
|
||||
aspas,
|
||||
router_keys: Vec::new(),
|
||||
warnings,
|
||||
stats,
|
||||
audit,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn process_publication_point_snapshot_for_issuer(
|
||||
pack: &PublicationPointSnapshot,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
) -> ObjectsOutput {
|
||||
process_publication_point_for_issuer(
|
||||
pack,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn process_publication_point_snapshot_for_issuer_parallel_roa(
|
||||
pack: &PublicationPointSnapshot,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
config: &ParallelPhase2Config,
|
||||
) -> ObjectsOutput {
|
||||
process_publication_point_for_issuer_parallel_roa(
|
||||
pack,
|
||||
policy,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
config,
|
||||
)
|
||||
}
|
||||
642
src/validation/objects/resource_validation.rs
Normal file
642
src/validation/objects/resource_validation.rs
Normal file
@@ -0,0 +1,642 @@
|
||||
fn choose_crl_uri_for_certificate<'a>(
|
||||
crldp_uris: Option<&'a Vec<String>>,
|
||||
crl_states: &std::collections::HashMap<String, IssuerCrlState>,
|
||||
) -> Result<&'a str, ObjectValidateError> {
|
||||
if crl_states.is_empty() {
|
||||
return Err(ObjectValidateError::MissingCrlInPack);
|
||||
}
|
||||
|
||||
let Some(crldp_uris) = crldp_uris else {
|
||||
return Err(ObjectValidateError::MissingCrlDpUris);
|
||||
};
|
||||
|
||||
for u in crldp_uris {
|
||||
let s = u.as_str();
|
||||
if crl_states.contains_key(s) {
|
||||
return Ok(s);
|
||||
}
|
||||
}
|
||||
Err(ObjectValidateError::CrlNotFound(
|
||||
crldp_uris
|
||||
.iter()
|
||||
.map(|u| u.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
))
|
||||
}
|
||||
|
||||
fn ensure_issuer_crl_verified(
|
||||
crl_rsync_uri: &str,
|
||||
crl_states: &mut std::collections::HashMap<String, IssuerCrlState>,
|
||||
issuer_ca_der: &[u8],
|
||||
) -> Result<Arc<VerifiedIssuerCrl>, CertPathError> {
|
||||
let entry = crl_states
|
||||
.get_mut(crl_rsync_uri)
|
||||
.expect("CRL must exist in snapshot");
|
||||
match entry {
|
||||
IssuerCrlState::Verified(v) => Ok(Arc::clone(v)),
|
||||
IssuerCrlState::Pending { bytes } => {
|
||||
let der = std::mem::take(bytes);
|
||||
let crl = crate::model::crl::RpkixCrl::decode_der(&der)
|
||||
.map_err(CertPathError::CrlDecode)?;
|
||||
crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)
|
||||
.map_err(CertPathError::CrlVerify)?;
|
||||
|
||||
let mut revoked_serials: std::collections::HashSet<Vec<u8>> =
|
||||
std::collections::HashSet::with_capacity(crl.revoked_certs.len());
|
||||
for rc in &crl.revoked_certs {
|
||||
revoked_serials.insert(rc.serial_number.bytes_be.clone());
|
||||
}
|
||||
|
||||
*entry = IssuerCrlState::Verified(Arc::new(VerifiedIssuerCrl {
|
||||
crl,
|
||||
revoked_serials,
|
||||
}));
|
||||
match entry {
|
||||
IssuerCrlState::Verified(v) => Ok(Arc::clone(v)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_ee_resources_subset(
|
||||
ee: &ResourceCertificate,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
issuer_resources_index: &IssuerResourcesIndex,
|
||||
) -> Result<(), ObjectValidateError> {
|
||||
if let Some(child_ip) = ee.tbs.extensions.ip_resources.as_ref() {
|
||||
let Some(parent_ip) = issuer_effective_ip else {
|
||||
return Err(ObjectValidateError::MissingIssuerEffectiveIp);
|
||||
};
|
||||
if !ip_resources_is_subset_indexed(child_ip, parent_ip, issuer_resources_index) {
|
||||
return Err(ObjectValidateError::EeResourcesNotSubset);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(child_as) = ee.tbs.extensions.as_resources.as_ref() {
|
||||
let Some(parent_as) = issuer_effective_as else {
|
||||
return Err(ObjectValidateError::MissingIssuerEffectiveAs);
|
||||
};
|
||||
if !as_resources_is_subset_indexed(child_as, parent_as, issuer_resources_index) {
|
||||
return Err(ObjectValidateError::EeResourcesNotSubset);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EeVerifiedResources {
|
||||
ip: Option<crate::model::rc::IpResourceSet>,
|
||||
asn: Option<crate::model::rc::AsResourceSet>,
|
||||
}
|
||||
|
||||
fn validate_ee_resources_for_mode(
|
||||
ee: &ResourceCertificate,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::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::model::rc::IpResourceSet,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_resources_index: &IssuerResourcesIndex,
|
||||
) -> Result<crate::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::model::rc::Afi::Ipv4 => issuer_resources_index.ip_v4.as_deref(),
|
||||
crate::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::model::rc::IpAddressFamily {
|
||||
afi: fam.afi,
|
||||
choice: IpAddressChoice::AddressesOrRanges(ip_intervals_to_ranges(
|
||||
fam.afi,
|
||||
&intersections,
|
||||
)),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(crate::model::rc::IpResourceSet { families })
|
||||
}
|
||||
|
||||
fn intersect_ee_as_resources_vrs(
|
||||
child_as: &crate::model::rc::AsResourceSet,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
issuer_resources_index: &IssuerResourcesIndex,
|
||||
) -> Result<crate::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::model::rc::AsResourceSet { asnum, rdi })
|
||||
}
|
||||
|
||||
fn roa_to_vrps_with_vrs(
|
||||
roa: &RoaObject,
|
||||
ee_vrs_ip: Option<&crate::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::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::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::model::rc::AsIdOrRange::Id(id) => *id == asn,
|
||||
crate::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::model::rc::Afi::Ipv4,
|
||||
RoaAfi::Ipv6 => crate::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_indexed(
|
||||
child: &AsResourceSet,
|
||||
parent: &AsResourceSet,
|
||||
idx: &IssuerResourcesIndex,
|
||||
) -> bool {
|
||||
let _ = parent;
|
||||
as_choice_subset_indexed(child.asnum.as_ref(), idx.asnum.as_deref())
|
||||
&& as_choice_subset_indexed(child.rdi.as_ref(), idx.rdi.as_deref())
|
||||
}
|
||||
|
||||
fn as_choice_subset_indexed(
|
||||
child: Option<&AsIdentifierChoice>,
|
||||
parent_intervals: Option<&[(u32, u32)]>,
|
||||
) -> bool {
|
||||
let Some(child) = child else {
|
||||
return true;
|
||||
};
|
||||
let Some(parent_intervals) = parent_intervals else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if matches!(child, AsIdentifierChoice::Inherit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let child_intervals = as_choice_to_merged_intervals(child);
|
||||
for (cmin, cmax) in &child_intervals {
|
||||
if !as_interval_is_covered(parent_intervals, *cmin, *cmax) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn as_choice_to_merged_intervals(choice: &AsIdentifierChoice) -> Vec<(u32, u32)> {
|
||||
let mut v = Vec::new();
|
||||
match choice {
|
||||
AsIdentifierChoice::Inherit => {}
|
||||
AsIdentifierChoice::AsIdsOrRanges(items) => {
|
||||
for item in items {
|
||||
match item {
|
||||
crate::model::rc::AsIdOrRange::Id(id) => v.push((*id, *id)),
|
||||
crate::model::rc::AsIdOrRange::Range { min, max } => v.push((*min, *max)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v.sort_by_key(|(a, _)| *a);
|
||||
merge_as_intervals(&v)
|
||||
}
|
||||
|
||||
fn merge_as_intervals(v: &[(u32, u32)]) -> Vec<(u32, u32)> {
|
||||
let mut out: Vec<(u32, u32)> = Vec::new();
|
||||
for (min, max) in v {
|
||||
let Some(last) = out.last_mut() else {
|
||||
out.push((*min, *max));
|
||||
continue;
|
||||
};
|
||||
if *min <= last.1.saturating_add(1) {
|
||||
last.1 = last.1.max(*max);
|
||||
continue;
|
||||
}
|
||||
out.push((*min, *max));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn as_interval_is_covered(parent: &[(u32, u32)], min: u32, max: u32) -> bool {
|
||||
for (pmin, pmax) in parent {
|
||||
if *pmin <= min && max <= *pmax {
|
||||
return true;
|
||||
}
|
||||
if *pmin > min {
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn ip_resources_is_subset_indexed(
|
||||
child: &crate::model::rc::IpResourceSet,
|
||||
parent: &crate::model::rc::IpResourceSet,
|
||||
idx: &IssuerResourcesIndex,
|
||||
) -> bool {
|
||||
let _ = parent;
|
||||
|
||||
for fam in &child.families {
|
||||
let parent_intervals = match fam.afi {
|
||||
crate::model::rc::Afi::Ipv4 => idx.ip_v4.as_deref(),
|
||||
crate::model::rc::Afi::Ipv6 => idx.ip_v6.as_deref(),
|
||||
};
|
||||
let Some(parent_intervals) = parent_intervals else {
|
||||
return false;
|
||||
};
|
||||
let items = match &fam.choice {
|
||||
IpAddressChoice::Inherit => return false,
|
||||
IpAddressChoice::AddressesOrRanges(items) => items,
|
||||
};
|
||||
|
||||
let mut child_intervals: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
||||
for item in items {
|
||||
match item {
|
||||
IpAddressOrRange::Prefix(p) => child_intervals.push(prefix_to_range(p)),
|
||||
IpAddressOrRange::Range(r) => child_intervals.push((r.min.clone(), r.max.clone())),
|
||||
}
|
||||
}
|
||||
if child_intervals.is_empty() {
|
||||
continue;
|
||||
}
|
||||
child_intervals.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
merge_ip_intervals_in_place(&mut child_intervals);
|
||||
if !intervals_are_covered(parent_intervals, &child_intervals) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn ip_items_to_merged_intervals(
|
||||
items: &[crate::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::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::model::rc::Afi,
|
||||
intervals: &[(Vec<u8>, Vec<u8>)],
|
||||
) -> Vec<crate::model::rc::IpAddressOrRange> {
|
||||
intervals
|
||||
.iter()
|
||||
.map(|(min, max)| {
|
||||
IpAddressOrRange::Range(crate::model::rc::IpAddressRange {
|
||||
min: normalize_ip_bytes(afi, min),
|
||||
max: normalize_ip_bytes(afi, max),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalize_ip_bytes(afi: crate::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::model::rc::AsIdOrRange> {
|
||||
intervals
|
||||
.iter()
|
||||
.map(|(min, max)| {
|
||||
if min == max {
|
||||
crate::model::rc::AsIdOrRange::Id(*min)
|
||||
} else {
|
||||
crate::model::rc::AsIdOrRange::Range {
|
||||
min: *min,
|
||||
max: *max,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn merge_ip_intervals_in_place(v: &mut Vec<(Vec<u8>, Vec<u8>)>) {
|
||||
if v.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::with_capacity(v.len());
|
||||
for (min, max) in v.drain(..) {
|
||||
let Some(last) = out.last_mut() else {
|
||||
out.push((min, max));
|
||||
continue;
|
||||
};
|
||||
if bytes_leq(&min, &last.1) || bytes_is_next(&min, &last.1) {
|
||||
if bytes_leq(&last.1, &max) {
|
||||
last.1 = max;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.push((min, max));
|
||||
}
|
||||
*v = out;
|
||||
}
|
||||
|
||||
fn intervals_are_covered(parent: &[(Vec<u8>, Vec<u8>)], child: &[(Vec<u8>, Vec<u8>)]) -> bool {
|
||||
let mut i = 0usize;
|
||||
for (cmin, cmax) in child {
|
||||
while i < parent.len() && parent[i].1.as_slice() < cmin.as_slice() {
|
||||
i += 1;
|
||||
}
|
||||
if i >= parent.len() {
|
||||
return false;
|
||||
}
|
||||
let (pmin, pmax) = &parent[i];
|
||||
if !bytes_leq(pmin, cmin) || !bytes_leq(cmax, pmax) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn prefix_to_range(prefix: &RcIpPrefix) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut min = prefix.addr.clone();
|
||||
let mut max = prefix.addr.clone();
|
||||
let bitlen = prefix.afi.ub();
|
||||
let plen = prefix.prefix_len.min(bitlen);
|
||||
for bit in plen..bitlen {
|
||||
let byte = (bit / 8) as usize;
|
||||
let offset = 7 - (bit % 8);
|
||||
let mask = 1u8 << offset;
|
||||
min[byte] &= !mask;
|
||||
max[byte] |= mask;
|
||||
}
|
||||
(min, max)
|
||||
}
|
||||
|
||||
fn bytes_leq(a: &[u8], b: &[u8]) -> bool {
|
||||
a <= b
|
||||
}
|
||||
|
||||
fn bytes_is_next(a: &[u8], b: &[u8]) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
let mut carry: u16 = 1;
|
||||
for i in (0..b.len()).rev() {
|
||||
let sum = (b[i] as u16) + carry;
|
||||
let expected = (sum & 0xFF) as u8;
|
||||
carry = sum >> 8;
|
||||
if a[i] != expected {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn build_issuer_resources_index(
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
) -> IssuerResourcesIndex {
|
||||
let mut idx = IssuerResourcesIndex::default();
|
||||
|
||||
if let Some(ip) = issuer_effective_ip {
|
||||
let mut v4: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
||||
let mut v6: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
||||
for fam in &ip.families {
|
||||
let ent = match fam.afi {
|
||||
crate::model::rc::Afi::Ipv4 => &mut v4,
|
||||
crate::model::rc::Afi::Ipv6 => &mut v6,
|
||||
};
|
||||
match &fam.choice {
|
||||
IpAddressChoice::Inherit => {
|
||||
// Effective resources should not contain inherit; leave empty so subset fails.
|
||||
}
|
||||
IpAddressChoice::AddressesOrRanges(items) => {
|
||||
for item in items {
|
||||
match item {
|
||||
IpAddressOrRange::Prefix(p) => ent.push(prefix_to_range(p)),
|
||||
IpAddressOrRange::Range(r) => ent.push((r.min.clone(), r.max.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !v4.is_empty() {
|
||||
v4.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
merge_ip_intervals_in_place(&mut v4);
|
||||
idx.ip_v4 = Some(v4);
|
||||
}
|
||||
if !v6.is_empty() {
|
||||
v6.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
merge_ip_intervals_in_place(&mut v6);
|
||||
idx.ip_v6 = Some(v6);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(asr) = issuer_effective_as {
|
||||
if let Some(choice) = asr.asnum.as_ref()
|
||||
&& !matches!(choice, AsIdentifierChoice::Inherit) {
|
||||
idx.asnum = Some(as_choice_to_merged_intervals(choice));
|
||||
}
|
||||
if let Some(choice) = asr.rdi.as_ref()
|
||||
&& !matches!(choice, AsIdentifierChoice::Inherit) {
|
||||
idx.rdi = Some(as_choice_to_merged_intervals(choice));
|
||||
}
|
||||
}
|
||||
|
||||
idx
|
||||
}
|
||||
|
||||
fn roa_to_vrps(roa: &RoaObject) -> Vec<Vrp> {
|
||||
let asn = roa.roa.as_id;
|
||||
let mut out = Vec::new();
|
||||
for fam in &roa.roa.ip_addr_blocks {
|
||||
for entry in &fam.addresses {
|
||||
let max_length = entry.max_length.unwrap_or(entry.prefix.prefix_len);
|
||||
out.push(Vrp {
|
||||
asn,
|
||||
prefix: entry.prefix.clone(),
|
||||
max_length,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn roa_afi_to_string(afi: RoaAfi) -> &'static str {
|
||||
match afi {
|
||||
RoaAfi::Ipv4 => "ipv4",
|
||||
RoaAfi::Ipv6 => "ipv6",
|
||||
}
|
||||
}
|
||||
117
src/validation/objects/serial_processing.rs
Normal file
117
src/validation/objects/serial_processing.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
pub fn process_publication_point_for_issuer<P: PublicationPointData>(
|
||||
publication_point: &P,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
) -> ObjectsOutput {
|
||||
process_publication_point_for_issuer_with_ta_constraints(
|
||||
publication_point, policy, issuer_ca_der, issuer_ca_rsync_uri,
|
||||
issuer_effective_ip, issuer_effective_as, validation_time, timing, None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn process_publication_point_for_issuer_with_ta_constraints<P: PublicationPointData>(
|
||||
publication_point: &P,
|
||||
policy: &Policy,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
||||
) -> ObjectsOutput {
|
||||
let manifest_uri = publication_point.manifest_rsync_uri();
|
||||
let files = publication_point.files();
|
||||
let mut stats = ObjectsStats {
|
||||
roa_total: files.iter().filter(|f| f.rsync_uri.ends_with(".roa")).count(),
|
||||
aspa_total: files.iter().filter(|f| f.rsync_uri.ends_with(".asa")).count(),
|
||||
..ObjectsStats::default()
|
||||
};
|
||||
let mut warnings = Vec::new();
|
||||
let mut audit = Vec::new();
|
||||
let empty = |stats: ObjectsStats, warnings: Vec<Warning>, audit: Vec<ObjectAuditEntry>| ObjectsOutput {
|
||||
vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), warnings, stats, audit,
|
||||
};
|
||||
if let Err(error) = ManifestObject::decode_der_with_strict_options(
|
||||
publication_point.manifest_bytes(), policy.strict.cms_der, policy.strict.name,
|
||||
) {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(Warning::new(format!("dropping publication point: manifest decode failed: {error}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §4"), RfcRef("RFC 9286 §6.6")]).with_context(manifest_uri));
|
||||
return empty(stats, warnings, audit);
|
||||
}
|
||||
if let Some(warning) = ber_compatible_cms_warning(publication_point.manifest_bytes(), manifest_uri, "manifest") { warnings.push(warning); }
|
||||
let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(Warning::new(format!("dropping publication point: issuer CA decode failed: {error}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2"), RfcRef("RFC 5280 §6.1")]).with_context(manifest_uri));
|
||||
return empty(stats, warnings, audit);
|
||||
}
|
||||
};
|
||||
let issuer_spki = match SubjectPublicKeyInfo::from_der(&issuer_ca.tbs.subject_public_key_info) {
|
||||
Ok(([], spki)) => spki,
|
||||
Ok((remaining, _)) => {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(Warning::new(format!("dropping publication point: issuer SPKI has {} trailing bytes", remaining.len())).with_context(manifest_uri));
|
||||
return empty(stats, warnings, audit);
|
||||
}
|
||||
Err(error) => {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(Warning::new(format!("dropping publication point: issuer SPKI parse failed: {error}")).with_context(manifest_uri));
|
||||
return empty(stats, warnings, audit);
|
||||
}
|
||||
};
|
||||
let mut crl_states: std::collections::HashMap<String, IssuerCrlState> = match files.iter().filter(|f| f.rsync_uri.ends_with(".crl")).map(|file| {
|
||||
Ok((file.rsync_uri.clone(), IssuerCrlState::Pending { bytes: file.bytes_cloned().map_err(|e| format!("CRL bytes load failed: {e}"))? }))
|
||||
}).collect::<Result<_, String>>() { Ok(value) => value, Err(error) => { stats.publication_point_dropped = true; warnings.push(Warning::new(error).with_context(manifest_uri)); return empty(stats, warnings, audit); } };
|
||||
let issuer_resources_index = build_issuer_resources_index(issuer_effective_ip, issuer_effective_as);
|
||||
if crl_states.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(Warning::new("dropping publication point: no CRL files in validated publication point").with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")]).with_context(manifest_uri));
|
||||
return empty(stats, warnings, audit);
|
||||
}
|
||||
let mut vrps = Vec::new();
|
||||
let mut aspas = Vec::new();
|
||||
for (index, file) in files.iter().enumerate() {
|
||||
if file.rsync_uri.ends_with(".roa") {
|
||||
let result = process_roa_with_issuer(file, issuer_ca_der, &issuer_ca, &issuer_spki, issuer_ca_rsync_uri, &mut crl_states, &issuer_resources_index, issuer_effective_ip, issuer_effective_as, validation_time, timing, policy.strict.cms_der, policy.strict.name, policy.resource_validation_mode, ta_constraints);
|
||||
match result {
|
||||
Ok(mut values) => {
|
||||
stats.roa_ok += 1;
|
||||
vrps.append(&mut values);
|
||||
audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Ok, detail: None });
|
||||
if let Some(warning) = ber_compatible_cms_warning_for_file(file, "ROA") { warnings.push(warning); }
|
||||
}
|
||||
Err(error) => match policy.signed_object_failure_policy {
|
||||
SignedObjectFailurePolicy::DropObject => {
|
||||
audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Error, detail: Some(error.to_string()) });
|
||||
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")]; refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&error));
|
||||
warnings.push(Warning::new(format!("dropping invalid ROA: {}: {error}", file.rsync_uri)).with_rfc_refs(&refs).with_context(&file.rsync_uri));
|
||||
}
|
||||
SignedObjectFailurePolicy::DropPublicationPoint => {
|
||||
stats.publication_point_dropped = true;
|
||||
warnings.push(Warning::new(format!("dropping publication point due to invalid ROA: {}: {error}", file.rsync_uri)).with_context(manifest_uri));
|
||||
for later in files.iter().skip(index + 1) { if later.rsync_uri.ends_with(".roa") || later.rsync_uri.ends_with(".asa") { audit.push(ObjectAuditEntry { rsync_uri: later.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&later.sha256), kind: if later.rsync_uri.ends_with(".roa") { AuditObjectKind::Roa } else { AuditObjectKind::Aspa }, result: AuditObjectResult::Skipped, detail: Some("skipped due to signed_object_failure_policy=drop_publication_point".to_string()) }); } }
|
||||
return empty(stats, warnings, audit);
|
||||
}
|
||||
},
|
||||
}
|
||||
} else if file.rsync_uri.ends_with(".asa") {
|
||||
match process_aspa_with_issuer(file, issuer_ca_der, &issuer_ca, &issuer_spki, issuer_ca_rsync_uri, &mut crl_states, &issuer_resources_index, issuer_effective_ip, issuer_effective_as, validation_time, timing, policy.strict.cms_der, policy.strict.name, policy.resource_validation_mode, ta_constraints) {
|
||||
Ok(attestation) => { stats.aspa_ok += 1; aspas.push(attestation); audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Ok, detail: None }); if let Some(warning) = ber_compatible_cms_warning_for_file(file, "ASPA") { warnings.push(warning); } }
|
||||
Err(error) => match policy.signed_object_failure_policy {
|
||||
SignedObjectFailurePolicy::DropObject => { audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Error, detail: Some(error.to_string()) }); warnings.push(Warning::new(format!("dropping invalid ASPA: {}: {error}", file.rsync_uri)).with_context(&file.rsync_uri)); }
|
||||
SignedObjectFailurePolicy::DropPublicationPoint => { stats.publication_point_dropped = true; warnings.push(Warning::new(format!("dropping publication point due to invalid ASPA: {}: {error}", file.rsync_uri)).with_context(manifest_uri)); return empty(stats, warnings, audit); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ObjectsOutput { vrps, aspas, router_keys: Vec::new(), warnings, stats, audit }
|
||||
}
|
||||
196
src/validation/policy.rs
Normal file
196
src/validation/policy.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ta_constraints::TaConstraintsByTal;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[derive(Default)]
|
||||
pub enum SyncPreference {
|
||||
#[default]
|
||||
RrdpThenRsync,
|
||||
RsyncOnly,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[derive(Default)]
|
||||
pub enum SignedObjectFailurePolicy {
|
||||
#[default]
|
||||
DropObject,
|
||||
DropPublicationPoint,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum ResourceValidationMode {
|
||||
#[serde(rename = "validation-update-03")]
|
||||
#[default]
|
||||
ValidationUpdate03,
|
||||
#[serde(rename = "rfc6487")]
|
||||
Rfc6487,
|
||||
}
|
||||
|
||||
impl ResourceValidationMode {
|
||||
pub fn parse_cli_value(raw: &str) -> Result<Self, String> {
|
||||
match raw.trim() {
|
||||
"validation-update-03" => Ok(Self::ValidationUpdate03),
|
||||
"rfc6487" => Ok(Self::Rfc6487),
|
||||
value => Err(format!(
|
||||
"unknown resource validation mode: {value}; supported: validation-update-03,rfc6487"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct StrictPolicy {
|
||||
pub name: bool,
|
||||
pub cms_der: bool,
|
||||
pub signed_attrs: bool,
|
||||
}
|
||||
|
||||
impl StrictPolicy {
|
||||
pub fn none() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn all() -> Self {
|
||||
Self {
|
||||
name: true,
|
||||
cms_der: true,
|
||||
signed_attrs: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_cli_spec(spec: Option<&str>) -> Result<Self, String> {
|
||||
let Some(spec) = spec else {
|
||||
return Ok(Self::all());
|
||||
};
|
||||
let spec = spec.trim();
|
||||
if spec.is_empty() || spec == "all" {
|
||||
return Ok(Self::all());
|
||||
}
|
||||
if spec == "none" {
|
||||
return Ok(Self::none());
|
||||
}
|
||||
|
||||
let mut out = Self::none();
|
||||
for raw in spec.split(',') {
|
||||
let item = raw.trim();
|
||||
match item {
|
||||
"name" => out.name = true,
|
||||
"cms-der" | "cms_der" => out.cms_der = true,
|
||||
"signed-attrs" | "signed_attrs" => out.signed_attrs = true,
|
||||
"all" => out = Self::all(),
|
||||
"none" => out = Self::none(),
|
||||
"" => return Err("empty strict policy name".to_string()),
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"unknown strict policy: {item}; supported: name,cms-der,signed-attrs,all,none"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[derive(Default)]
|
||||
pub struct Policy {
|
||||
pub sync_preference: SyncPreference,
|
||||
pub signed_object_failure_policy: SignedObjectFailurePolicy,
|
||||
pub resource_validation_mode: ResourceValidationMode,
|
||||
pub strict: StrictPolicy,
|
||||
/// Locally configured, per-TAL EE certificate resource constraints. They
|
||||
/// are intentionally CLI/runtime-only rather than policy-file input.
|
||||
#[serde(skip)]
|
||||
pub ta_constraints: TaConstraintsByTal,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PolicyParseError {
|
||||
#[error("policy TOML parse error: {0}")]
|
||||
Toml(String),
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
pub fn from_toml_str(s: &str) -> Result<Self, PolicyParseError> {
|
||||
toml::from_str(s).map_err(|e| PolicyParseError::Toml(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn strict_policy_parses_cli_specs() {
|
||||
assert_eq!(
|
||||
StrictPolicy::parse_cli_spec(None).unwrap(),
|
||||
StrictPolicy::all()
|
||||
);
|
||||
assert_eq!(
|
||||
StrictPolicy::parse_cli_spec(Some("name,signed-attrs")).unwrap(),
|
||||
StrictPolicy {
|
||||
name: true,
|
||||
cms_der: false,
|
||||
signed_attrs: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
StrictPolicy::parse_cli_spec(Some("none")).unwrap(),
|
||||
StrictPolicy::none()
|
||||
);
|
||||
assert!(StrictPolicy::parse_cli_spec(Some("bogus")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_validation_mode_parses_cli_values() {
|
||||
assert_eq!(
|
||||
ResourceValidationMode::parse_cli_value("validation-update-03").unwrap(),
|
||||
ResourceValidationMode::ValidationUpdate03
|
||||
);
|
||||
assert_eq!(
|
||||
ResourceValidationMode::parse_cli_value("rfc6487").unwrap(),
|
||||
ResourceValidationMode::Rfc6487
|
||||
);
|
||||
assert!(ResourceValidationMode::parse_cli_value("bogus").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_toml_accepts_strict_table() {
|
||||
let policy = Policy::from_toml_str(
|
||||
r#"
|
||||
[strict]
|
||||
name = true
|
||||
cms_der = true
|
||||
signed_attrs = false
|
||||
"#,
|
||||
)
|
||||
.expect("parse policy");
|
||||
assert_eq!(
|
||||
policy.strict,
|
||||
StrictPolicy {
|
||||
name: true,
|
||||
cms_der: true,
|
||||
signed_attrs: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_toml_accepts_resource_validation_mode() {
|
||||
let policy = Policy::from_toml_str(
|
||||
r#"
|
||||
resource_validation_mode = "rfc6487"
|
||||
"#,
|
||||
)
|
||||
.expect("parse policy");
|
||||
assert_eq!(
|
||||
policy.resource_validation_mode,
|
||||
ResourceValidationMode::Rfc6487
|
||||
);
|
||||
}
|
||||
}
|
||||
18
src/validation/publication_point.rs
Normal file
18
src/validation/publication_point.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use crate::repository::storage::{PackFile, PackTime};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublicationPointSnapshot {
|
||||
pub format_version: u32,
|
||||
pub manifest_rsync_uri: String,
|
||||
pub publication_point_rsync_uri: String,
|
||||
pub manifest_number_be: Vec<u8>,
|
||||
pub this_update: PackTime,
|
||||
pub next_update: PackTime,
|
||||
pub verified_at: PackTime,
|
||||
pub manifest_bytes: Vec<u8>,
|
||||
pub files: Vec<PackFile>,
|
||||
}
|
||||
|
||||
impl PublicationPointSnapshot {
|
||||
pub const FORMAT_VERSION_V1: u32 = 1;
|
||||
}
|
||||
86
src/validation/run.rs
Normal file
86
src/validation/run.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::model::rc::{AsResourceSet, IpResourceSet};
|
||||
use crate::output::report::Warning;
|
||||
use crate::repository::fetch::rsync::RsyncFetcher;
|
||||
use crate::repository::storage::RocksStore;
|
||||
use crate::repository::sync::rrdp::Fetcher as HttpFetcher;
|
||||
use crate::validation::manifest::PublicationPointSource;
|
||||
use crate::validation::objects::ObjectsOutput;
|
||||
use crate::validation::policy::Policy;
|
||||
use crate::validation::tree::{CaCertificateRef, CaInstanceHandle, PublicationPointRunner};
|
||||
use crate::validation::tree_runner::Rpkiv1PublicationPointRunner;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunOutput {
|
||||
pub publication_point_source: PublicationPointSource,
|
||||
pub publication_point_warnings: Vec<Warning>,
|
||||
pub objects: ObjectsOutput,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RunError {
|
||||
#[error("publication point runner failed: {0}")]
|
||||
Runner(String),
|
||||
}
|
||||
|
||||
/// Offline-friendly end-to-end execution for a single publication point.
|
||||
///
|
||||
/// This reuses the same fresh-first runtime path as the tree runner:
|
||||
/// 1) repository sync (RRDP or rsync fallback) into the current repository view
|
||||
/// 2) manifest processing into a validated publication point result
|
||||
/// 3) signed object processing (ROA/ASPA) and public report/CCR output
|
||||
pub fn run_publication_point_once(
|
||||
store: &RocksStore,
|
||||
policy: &Policy,
|
||||
rrdp_notification_uri: Option<&str>,
|
||||
rsync_base_uri: &str,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
http_fetcher: &dyn HttpFetcher,
|
||||
rsync_fetcher: &dyn RsyncFetcher,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&IpResourceSet>,
|
||||
issuer_effective_as: Option<&AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<RunOutput, RunError> {
|
||||
let handle = CaInstanceHandle {
|
||||
depth: 0,
|
||||
tal_id: "single-publication-point".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.to_vec()),
|
||||
ca_certificate_rsync_uri: issuer_ca_rsync_uri.map(str::to_string),
|
||||
effective_ip_resources: issuer_effective_ip.cloned(),
|
||||
effective_as_resources: issuer_effective_as.cloned(),
|
||||
rsync_base_uri: rsync_base_uri.to_string(),
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
publication_point_rsync_uri: publication_point_rsync_uri.to_string(),
|
||||
rrdp_notification_uri: rrdp_notification_uri.map(str::to_string),
|
||||
};
|
||||
|
||||
let runner = Rpkiv1PublicationPointRunner {
|
||||
store,
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
timing: None,
|
||||
download_log: None,
|
||||
current_repo_index: None,
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let result = runner
|
||||
.run_publication_point(&handle)
|
||||
.map_err(RunError::Runner)?;
|
||||
|
||||
Ok(RunOutput {
|
||||
publication_point_source: result.source,
|
||||
publication_point_warnings: result.warnings,
|
||||
objects: result.objects,
|
||||
})
|
||||
}
|
||||
42
src/validation/run_tree_from_tal.rs
Normal file
42
src/validation/run_tree_from_tal.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#![allow(clippy::too_many_arguments, clippy::type_complexity)]
|
||||
|
||||
use url::Url;
|
||||
|
||||
use crate::ccr::CcrAccumulator;
|
||||
use crate::model::ta::TrustAnchor;
|
||||
use crate::output::analysis::timing::TimingHandle;
|
||||
use crate::output::audit::PublicationPointAudit;
|
||||
use crate::output::audit_downloads::DownloadLogHandle;
|
||||
use crate::repository::current_repo_index::{CurrentRepoIndexHandle, CurrentRepoObject};
|
||||
use crate::repository::sync::rrdp::Fetcher;
|
||||
use crate::scheduler::config::{ParallelPhase1Config, ParallelPhase2Config};
|
||||
use crate::scheduler::repo_runtime::{Phase1RepoSyncRuntime, RepoSyncRuntime};
|
||||
use crate::scheduler::repo_worker::{
|
||||
LiveRepoTransportExecutor, RepoTransportWorkerPool, RepoWorkerPoolConfig,
|
||||
};
|
||||
use crate::scheduler::run_coordinator::GlobalRunCoordinator;
|
||||
use crate::scheduler::types::{TalInputSpec, TalSource};
|
||||
use crate::validation::from_tal::{
|
||||
DiscoveredRootCaInstance, FromTalError, canonical_tal_rsync_uri_from_bytes,
|
||||
discover_root_ca_instance_from_tal_and_ta_der,
|
||||
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name,
|
||||
discover_root_ca_instance_from_tal_url_with_fetchers,
|
||||
discover_root_ca_instance_from_tal_url_with_fetchers_strict_name,
|
||||
discover_root_ca_instance_from_tal_with_fetchers,
|
||||
discover_root_ca_instance_from_tal_with_fetchers_strict_name,
|
||||
};
|
||||
use crate::validation::objects::ParallelRoaWorkerPool;
|
||||
use crate::validation::tree::CaCertificateRef;
|
||||
use crate::validation::tree::{
|
||||
CaInstanceHandle, TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput,
|
||||
run_tree_serial, run_tree_serial_audit, run_tree_serial_audit_multi_root,
|
||||
};
|
||||
use crate::validation::tree_parallel::{
|
||||
run_tree_parallel_phase2_audit, run_tree_parallel_phase2_audit_multi_root,
|
||||
};
|
||||
use crate::validation::tree_runner::Rpkiv1PublicationPointRunner;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
include!("run_tree_from_tal/discovery.rs");
|
||||
include!("run_tree_from_tal/serial.rs");
|
||||
include!("run_tree_from_tal/phase1.rs");
|
||||
360
src/validation/run_tree_from_tal/discovery.rs
Normal file
360
src/validation/run_tree_from_tal/discovery.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
fn tal_id_from_url_like(s: &str) -> Option<String> {
|
||||
let url = Url::parse(s).ok()?;
|
||||
if let Some(last) = url
|
||||
.path_segments()
|
||||
.and_then(|mut segments| segments.rfind(|seg| !seg.is_empty()))
|
||||
{
|
||||
let stem = last.rsplit_once('.').map(|(stem, _)| stem).unwrap_or(last);
|
||||
let trimmed = stem.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
url.host_str().map(|host| host.to_string())
|
||||
}
|
||||
|
||||
fn derive_tal_id(discovery: &DiscoveredRootCaInstance) -> String {
|
||||
discovery
|
||||
.tal_url
|
||||
.as_deref()
|
||||
.and_then(tal_id_from_url_like)
|
||||
.or_else(|| {
|
||||
discovery
|
||||
.trust_anchor
|
||||
.resolved_ta_uri
|
||||
.as_ref()
|
||||
.and_then(|uri| tal_id_from_url_like(uri.as_str()))
|
||||
})
|
||||
.or_else(|| {
|
||||
discovery
|
||||
.trust_anchor
|
||||
.tal
|
||||
.ta_uris
|
||||
.first()
|
||||
.and_then(|uri| tal_id_from_url_like(uri.as_str()))
|
||||
})
|
||||
.unwrap_or_else(|| "unknown-tal".to_string())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunTreeFromTalOutput {
|
||||
pub discovery: DiscoveredRootCaInstance,
|
||||
pub tree: TreeRunOutput,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunTreeFromTalAuditOutput {
|
||||
pub discovery: DiscoveredRootCaInstance,
|
||||
pub discoveries: Vec<DiscoveredRootCaInstance>,
|
||||
pub successful_tal_inputs: Vec<TalInputSpec>,
|
||||
pub tree: TreeRunOutput,
|
||||
pub publication_points: Vec<PublicationPointAudit>,
|
||||
pub downloads: Vec<crate::output::audit::AuditDownloadEvent>,
|
||||
pub download_stats: crate::output::audit::AuditDownloadStats,
|
||||
pub current_repo_objects: Vec<CurrentRepoObject>,
|
||||
pub ccr_accumulator: Option<CcrAccumulator>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TalRootDiscovery {
|
||||
pub tal_input: TalInputSpec,
|
||||
pub discovery: DiscoveredRootCaInstance,
|
||||
pub root_handle: CaInstanceHandle,
|
||||
}
|
||||
|
||||
fn snapshot_current_repo_objects(
|
||||
current_repo_index: Option<&CurrentRepoIndexHandle>,
|
||||
collect: bool,
|
||||
) -> Vec<CurrentRepoObject> {
|
||||
if !collect {
|
||||
return Vec::new();
|
||||
}
|
||||
current_repo_index
|
||||
.and_then(|handle| handle.read().ok().map(|idx| idx.snapshot_objects()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn make_live_runner<'a>(
|
||||
store: &'a crate::repository::storage::RocksStore,
|
||||
policy: &'a crate::validation::policy::Policy,
|
||||
http_fetcher: &'a dyn Fetcher,
|
||||
rsync_fetcher: &'a dyn crate::repository::fetch::rsync::RsyncFetcher,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<TimingHandle>,
|
||||
download_log: Option<DownloadLogHandle>,
|
||||
current_repo_index: Option<CurrentRepoIndexHandle>,
|
||||
repo_sync_runtime: Option<Arc<dyn RepoSyncRuntime>>,
|
||||
parallel_phase2_config: Option<ParallelPhase2Config>,
|
||||
ccr_accumulator: Option<CcrAccumulator>,
|
||||
) -> Rpkiv1PublicationPointRunner<'a> {
|
||||
let parallel_roa_worker_pool = parallel_phase2_config
|
||||
.as_ref()
|
||||
.and_then(|config| ParallelRoaWorkerPool::new(config).ok());
|
||||
Rpkiv1PublicationPointRunner {
|
||||
store,
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
timing,
|
||||
download_log,
|
||||
current_repo_index,
|
||||
repo_sync_runtime,
|
||||
parallel_phase2_config,
|
||||
parallel_roa_worker_pool,
|
||||
ccr_accumulator: ccr_accumulator.map(Mutex::new),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_phase1_repo_sync_runtime<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
timing: Option<TimingHandle>,
|
||||
download_log: Option<DownloadLogHandle>,
|
||||
tal_inputs: Vec<crate::scheduler::types::TalInputSpec>,
|
||||
) -> Result<(Arc<dyn RepoSyncRuntime>, CurrentRepoIndexHandle), RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
let coordinator = GlobalRunCoordinator::new(parallel_config.clone(), tal_inputs);
|
||||
let current_repo_index = coordinator.current_repo_index_handle();
|
||||
let rsync_fetcher_arc = Arc::new(rsync_fetcher.clone());
|
||||
let executor = LiveRepoTransportExecutor::new(
|
||||
Arc::clone(&store),
|
||||
current_repo_index.clone(),
|
||||
Arc::new(http_fetcher.clone()),
|
||||
Arc::clone(&rsync_fetcher_arc),
|
||||
timing.clone(),
|
||||
download_log,
|
||||
);
|
||||
let pool = RepoTransportWorkerPool::new(RepoWorkerPoolConfig::from(¶llel_config), executor)
|
||||
.map_err(RunTreeFromTalError::Setup)?;
|
||||
let resolver_rsync_fetcher_arc = Arc::clone(&rsync_fetcher_arc);
|
||||
let resolver: Arc<dyn Fn(&str) -> String + Send + Sync> =
|
||||
Arc::new(move |base: &str| resolver_rsync_fetcher_arc.dedup_key(base));
|
||||
let failure_rsync_fetcher_arc = Arc::clone(&rsync_fetcher_arc);
|
||||
let failure_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync> =
|
||||
Arc::new(move |base: &str| failure_rsync_fetcher_arc.failure_dedup_key(base));
|
||||
let _ = policy; // policy reserved for later runtime-level decisions
|
||||
let runtime = Arc::new(Phase1RepoSyncRuntime::new_with_failure_scope(
|
||||
coordinator,
|
||||
pool,
|
||||
resolver,
|
||||
failure_resolver,
|
||||
policy.sync_preference,
|
||||
));
|
||||
Ok((runtime, current_repo_index))
|
||||
}
|
||||
|
||||
fn root_discovery_from_tal_input(
|
||||
tal_input: &TalInputSpec,
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn crate::repository::fetch::rsync::RsyncFetcher,
|
||||
strict_name: bool,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
match &tal_input.source {
|
||||
TalSource::Url(url) => {
|
||||
if strict_name {
|
||||
discover_root_ca_instance_from_tal_url_with_fetchers_strict_name(
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
url,
|
||||
)
|
||||
} else {
|
||||
discover_root_ca_instance_from_tal_url_with_fetchers(
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
url,
|
||||
)
|
||||
}
|
||||
}
|
||||
TalSource::DerBytes {
|
||||
tal_bytes, ta_der, ..
|
||||
} => {
|
||||
if strict_name {
|
||||
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
|
||||
tal_bytes, ta_der, None,
|
||||
)
|
||||
} else {
|
||||
discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, None)
|
||||
}
|
||||
}
|
||||
TalSource::FilePath(path) => {
|
||||
let tal_bytes = std::fs::read(path).map_err(|e| {
|
||||
FromTalError::TalFetch(format!("read TAL file failed: {}: {e}", path.display()))
|
||||
})?;
|
||||
let tal = crate::model::tal::Tal::decode_bytes(&tal_bytes)
|
||||
.map_err(FromTalError::from)?;
|
||||
if strict_name {
|
||||
discover_root_ca_instance_from_tal_with_fetchers_strict_name(
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal,
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
discover_root_ca_instance_from_tal_with_fetchers(
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal,
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
TalSource::FilePathWithTa { tal_path, ta_path } => {
|
||||
let tal_bytes = std::fs::read(tal_path).map_err(|e| {
|
||||
FromTalError::TalFetch(format!("read TAL file failed: {}: {e}", tal_path.display()))
|
||||
})?;
|
||||
let ta_der = std::fs::read(ta_path).map_err(|e| {
|
||||
FromTalError::TaFetch(format!("read TA file failed: {}: {e}", ta_path.display()))
|
||||
})?;
|
||||
let resolved_ta_uri = canonical_tal_rsync_uri_from_bytes(&tal_bytes)?;
|
||||
if strict_name {
|
||||
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
|
||||
&tal_bytes,
|
||||
&ta_der,
|
||||
Some(&resolved_ta_uri),
|
||||
)
|
||||
} else {
|
||||
discover_root_ca_instance_from_tal_and_ta_der(
|
||||
&tal_bytes,
|
||||
&ta_der,
|
||||
Some(&resolved_ta_uri),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_root_ca_instance_from_tal_url_with_policy(
|
||||
policy: &crate::validation::policy::Policy,
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn crate::repository::fetch::rsync::RsyncFetcher,
|
||||
tal_url: &str,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
if policy.strict.name {
|
||||
discover_root_ca_instance_from_tal_url_with_fetchers_strict_name(
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal_url,
|
||||
)
|
||||
} else {
|
||||
discover_root_ca_instance_from_tal_url_with_fetchers(http_fetcher, rsync_fetcher, tal_url)
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_root_ca_instance_from_tal_and_ta_der_with_policy(
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_bytes: &[u8],
|
||||
ta_der: &[u8],
|
||||
resolved_ta_uri: Option<&Url>,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
if policy.strict.name {
|
||||
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
|
||||
tal_bytes,
|
||||
ta_der,
|
||||
resolved_ta_uri,
|
||||
)
|
||||
} else {
|
||||
discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_multiple_roots_from_tal_inputs(
|
||||
tal_inputs: &[TalInputSpec],
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn crate::repository::fetch::rsync::RsyncFetcher,
|
||||
strict_name: bool,
|
||||
) -> Result<Vec<TalRootDiscovery>, RunTreeFromTalError> {
|
||||
let mut roots = Vec::with_capacity(tal_inputs.len());
|
||||
for tal_input in tal_inputs {
|
||||
let discovery = match root_discovery_from_tal_input(
|
||||
tal_input,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
strict_name,
|
||||
) {
|
||||
Ok(discovery) => discovery,
|
||||
Err(error)
|
||||
if should_isolate_multi_tal_strict_name_failure(
|
||||
tal_inputs.len(),
|
||||
strict_name,
|
||||
&error,
|
||||
) =>
|
||||
{
|
||||
crate::logging::warning!(
|
||||
"warning: skipping TAL '{}' because strict name validation failed during trust anchor discovery: {error}",
|
||||
tal_input.tal_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let root_handle = root_handle_from_trust_anchor(
|
||||
&discovery.trust_anchor,
|
||||
tal_input.tal_id.clone(),
|
||||
None,
|
||||
&discovery.ca_instance,
|
||||
);
|
||||
roots.push(TalRootDiscovery {
|
||||
tal_input: tal_input.clone(),
|
||||
discovery,
|
||||
root_handle,
|
||||
});
|
||||
}
|
||||
if roots.is_empty() {
|
||||
return Err(RunTreeFromTalError::Setup(
|
||||
"multi-TAL root discovery returned no usable roots after strict name filtering"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(roots)
|
||||
}
|
||||
|
||||
fn should_isolate_multi_tal_strict_name_failure(
|
||||
tal_input_count: usize,
|
||||
strict_name: bool,
|
||||
error: &FromTalError,
|
||||
) -> bool {
|
||||
strict_name
|
||||
&& tal_input_count > 1
|
||||
&& error.to_string().contains("Name strict validation failed")
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RunTreeFromTalError {
|
||||
#[error("{0}")]
|
||||
FromTal(#[from] FromTalError),
|
||||
|
||||
#[error("validation setup failed: {0}")]
|
||||
Setup(String),
|
||||
|
||||
#[error("{0}")]
|
||||
Tree(#[from] TreeRunError),
|
||||
}
|
||||
|
||||
pub fn root_handle_from_trust_anchor(
|
||||
trust_anchor: &TrustAnchor,
|
||||
tal_id: String,
|
||||
ca_certificate_rsync_uri: Option<String>,
|
||||
ca_instance: &crate::validation::ca_instance::CaInstanceUris,
|
||||
) -> CaInstanceHandle {
|
||||
let ta_rc = trust_anchor.ta_certificate.rc_ca.clone();
|
||||
CaInstanceHandle {
|
||||
depth: 0,
|
||||
tal_id,
|
||||
parent_manifest_rsync_uri: None,
|
||||
ca_certificate: CaCertificateRef::inline_der(trust_anchor.ta_certificate.raw_der.clone()),
|
||||
ca_certificate_rsync_uri,
|
||||
effective_ip_resources: ta_rc.tbs.extensions.ip_resources.clone(),
|
||||
effective_as_resources: ta_rc.tbs.extensions.as_resources.clone(),
|
||||
rsync_base_uri: ca_instance.rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: ca_instance.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: ca_instance.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: ca_instance.rrdp_notification_uri.clone(),
|
||||
}
|
||||
}
|
||||
365
src/validation/run_tree_from_tal/phase1.rs
Normal file
365
src/validation/run_tree_from_tal/phase1.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
pub fn run_tree_from_tal_url_parallel_phase1_audit<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_url: &str,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
collect_current_repo_objects: bool,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
let discovery = discover_root_ca_instance_from_tal_url_with_policy(
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal_url,
|
||||
)?;
|
||||
run_single_root_parallel_audit_inner(
|
||||
store,
|
||||
policy,
|
||||
discovery,
|
||||
vec![TalInputSpec::from_url(tal_url.to_string())],
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
config,
|
||||
parallel_config,
|
||||
None,
|
||||
collect_current_repo_objects,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_and_ta_der_parallel_phase1_audit<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_bytes: &[u8],
|
||||
ta_der: &[u8],
|
||||
resolved_ta_uri: Option<&url::Url>,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
collect_current_repo_objects: bool,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
|
||||
policy,
|
||||
tal_bytes,
|
||||
ta_der,
|
||||
resolved_ta_uri,
|
||||
)?;
|
||||
let derived_tal_id = derive_tal_id(&discovery);
|
||||
let tal_inputs = vec![TalInputSpec {
|
||||
tal_id: derived_tal_id.clone(),
|
||||
rir_id: derived_tal_id,
|
||||
source: TalSource::DerBytes {
|
||||
tal_url: discovery
|
||||
.tal_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "embedded-tal".to_string()),
|
||||
tal_bytes: tal_bytes.to_vec(),
|
||||
ta_der: ta_der.to_vec(),
|
||||
},
|
||||
}];
|
||||
run_single_root_parallel_audit_inner(
|
||||
store,
|
||||
policy,
|
||||
discovery,
|
||||
tal_inputs,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
config,
|
||||
parallel_config,
|
||||
None,
|
||||
collect_current_repo_objects,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_tree_from_multiple_tals_parallel_phase1_audit<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_inputs: Vec<TalInputSpec>,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
collect_current_repo_objects: bool,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
run_multi_root_parallel_audit_inner(
|
||||
store,
|
||||
policy,
|
||||
tal_inputs,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
config,
|
||||
parallel_config,
|
||||
None,
|
||||
collect_current_repo_objects,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_url_parallel_phase2_audit<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_url: &str,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
phase2_config: ParallelPhase2Config,
|
||||
collect_current_repo_objects: bool,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
let discovery = discover_root_ca_instance_from_tal_url_with_policy(
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal_url,
|
||||
)?;
|
||||
run_single_root_parallel_audit_inner(
|
||||
store,
|
||||
policy,
|
||||
discovery,
|
||||
vec![TalInputSpec::from_url(tal_url.to_string())],
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
config,
|
||||
parallel_config,
|
||||
Some(phase2_config),
|
||||
collect_current_repo_objects,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_url_parallel_phase2_audit_with_timing<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_url: &str,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
phase2_config: ParallelPhase2Config,
|
||||
collect_current_repo_objects: bool,
|
||||
timing: &TimingHandle,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
let discovery = discover_root_ca_instance_from_tal_url_with_policy(
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal_url,
|
||||
)?;
|
||||
run_single_root_parallel_audit_inner(
|
||||
store,
|
||||
policy,
|
||||
discovery,
|
||||
vec![TalInputSpec::from_url(tal_url.to_string())],
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
config,
|
||||
parallel_config,
|
||||
Some(phase2_config),
|
||||
collect_current_repo_objects,
|
||||
Some(timing.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_and_ta_der_parallel_phase2_audit<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_bytes: &[u8],
|
||||
ta_der: &[u8],
|
||||
resolved_ta_uri: Option<&url::Url>,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
phase2_config: ParallelPhase2Config,
|
||||
collect_current_repo_objects: bool,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
|
||||
policy,
|
||||
tal_bytes,
|
||||
ta_der,
|
||||
resolved_ta_uri,
|
||||
)?;
|
||||
let derived_tal_id = derive_tal_id(&discovery);
|
||||
let tal_inputs = vec![TalInputSpec {
|
||||
tal_id: derived_tal_id.clone(),
|
||||
rir_id: derived_tal_id,
|
||||
source: TalSource::DerBytes {
|
||||
tal_url: discovery
|
||||
.tal_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "embedded-tal".to_string()),
|
||||
tal_bytes: tal_bytes.to_vec(),
|
||||
ta_der: ta_der.to_vec(),
|
||||
},
|
||||
}];
|
||||
run_single_root_parallel_audit_inner(
|
||||
store,
|
||||
policy,
|
||||
discovery,
|
||||
tal_inputs,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
config,
|
||||
parallel_config,
|
||||
Some(phase2_config),
|
||||
collect_current_repo_objects,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_and_ta_der_parallel_phase2_audit_with_timing<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_bytes: &[u8],
|
||||
ta_der: &[u8],
|
||||
resolved_ta_uri: Option<&url::Url>,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
phase2_config: ParallelPhase2Config,
|
||||
collect_current_repo_objects: bool,
|
||||
timing: &TimingHandle,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
|
||||
policy,
|
||||
tal_bytes,
|
||||
ta_der,
|
||||
resolved_ta_uri,
|
||||
)?;
|
||||
let derived_tal_id = derive_tal_id(&discovery);
|
||||
let tal_inputs = vec![TalInputSpec {
|
||||
tal_id: derived_tal_id.clone(),
|
||||
rir_id: derived_tal_id,
|
||||
source: TalSource::DerBytes {
|
||||
tal_url: discovery
|
||||
.tal_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "embedded-tal".to_string()),
|
||||
tal_bytes: tal_bytes.to_vec(),
|
||||
ta_der: ta_der.to_vec(),
|
||||
},
|
||||
}];
|
||||
run_single_root_parallel_audit_inner(
|
||||
store,
|
||||
policy,
|
||||
discovery,
|
||||
tal_inputs,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
config,
|
||||
parallel_config,
|
||||
Some(phase2_config),
|
||||
collect_current_repo_objects,
|
||||
Some(timing.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_tree_from_multiple_tals_parallel_phase2_audit<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_inputs: Vec<TalInputSpec>,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
phase2_config: ParallelPhase2Config,
|
||||
collect_current_repo_objects: bool,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
run_multi_root_parallel_audit_inner(
|
||||
store,
|
||||
policy,
|
||||
tal_inputs,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
config,
|
||||
parallel_config,
|
||||
Some(phase2_config),
|
||||
collect_current_repo_objects,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_tree_from_multiple_tals_parallel_phase2_audit_with_timing<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_inputs: Vec<TalInputSpec>,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
phase2_config: ParallelPhase2Config,
|
||||
collect_current_repo_objects: bool,
|
||||
timing: &TimingHandle,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
run_multi_root_parallel_audit_inner(
|
||||
store,
|
||||
policy,
|
||||
tal_inputs,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
config,
|
||||
parallel_config,
|
||||
Some(phase2_config),
|
||||
collect_current_repo_objects,
|
||||
Some(timing.clone()),
|
||||
)
|
||||
}
|
||||
344
src/validation/run_tree_from_tal/serial.rs
Normal file
344
src/validation/run_tree_from_tal/serial.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
pub fn run_tree_from_tal_url_serial(
|
||||
store: &crate::repository::storage::RocksStore,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_url: &str,
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn crate::repository::fetch::rsync::RsyncFetcher,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
) -> Result<RunTreeFromTalOutput, RunTreeFromTalError> {
|
||||
let discovery = discover_root_ca_instance_from_tal_url_with_policy(
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal_url,
|
||||
)?;
|
||||
|
||||
let runner = make_live_runner(
|
||||
store,
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
&discovery.trust_anchor,
|
||||
derive_tal_id(&discovery),
|
||||
None,
|
||||
&discovery.ca_instance,
|
||||
);
|
||||
let tree = run_tree_serial(root, &runner, config)?;
|
||||
|
||||
Ok(RunTreeFromTalOutput { discovery, tree })
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_url_serial_audit(
|
||||
store: &crate::repository::storage::RocksStore,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_url: &str,
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn crate::repository::fetch::rsync::RsyncFetcher,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
|
||||
let discovery = discover_root_ca_instance_from_tal_url_with_policy(
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal_url,
|
||||
)?;
|
||||
|
||||
let download_log = DownloadLogHandle::new();
|
||||
let runner = make_live_runner(
|
||||
store,
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
None,
|
||||
Some(download_log.clone()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
&discovery.trust_anchor,
|
||||
derive_tal_id(&discovery),
|
||||
None,
|
||||
&discovery.ca_instance,
|
||||
);
|
||||
let TreeRunAuditOutput {
|
||||
tree,
|
||||
publication_points,
|
||||
} = run_tree_serial_audit(root, &runner, config)?;
|
||||
|
||||
let downloads = download_log.snapshot_events();
|
||||
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
|
||||
Ok(RunTreeFromTalAuditOutput {
|
||||
discovery: discovery.clone(),
|
||||
discoveries: vec![discovery],
|
||||
successful_tal_inputs: Vec::new(),
|
||||
tree,
|
||||
publication_points,
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_url_serial_audit_with_timing(
|
||||
store: &crate::repository::storage::RocksStore,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_url: &str,
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn crate::repository::fetch::rsync::RsyncFetcher,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
timing: &TimingHandle,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
|
||||
let _tal = timing.span_phase("tal_bootstrap");
|
||||
let discovery = discover_root_ca_instance_from_tal_url_with_policy(
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
tal_url,
|
||||
)?;
|
||||
drop(_tal);
|
||||
|
||||
let download_log = DownloadLogHandle::new();
|
||||
let runner = make_live_runner(
|
||||
store,
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
Some(timing.clone()),
|
||||
Some(download_log.clone()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
&discovery.trust_anchor,
|
||||
derive_tal_id(&discovery),
|
||||
None,
|
||||
&discovery.ca_instance,
|
||||
);
|
||||
let _tree = timing.span_phase("tree_run_total");
|
||||
let TreeRunAuditOutput {
|
||||
tree,
|
||||
publication_points,
|
||||
} = run_tree_serial_audit(root, &runner, config)?;
|
||||
|
||||
let downloads = download_log.snapshot_events();
|
||||
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
|
||||
Ok(RunTreeFromTalAuditOutput {
|
||||
discovery: discovery.clone(),
|
||||
discoveries: vec![discovery],
|
||||
successful_tal_inputs: Vec::new(),
|
||||
tree,
|
||||
publication_points,
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_single_root_parallel_audit_inner<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
discovery: DiscoveredRootCaInstance,
|
||||
tal_inputs: Vec<TalInputSpec>,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
phase2_config: Option<ParallelPhase2Config>,
|
||||
collect_current_repo_objects: bool,
|
||||
timing: Option<TimingHandle>,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
let phase2_enabled = phase2_config.is_some();
|
||||
let download_log = DownloadLogHandle::new();
|
||||
let (runtime, current_repo_index) = build_phase1_repo_sync_runtime(
|
||||
Arc::clone(&store),
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
parallel_config,
|
||||
timing.clone(),
|
||||
Some(download_log.clone()),
|
||||
tal_inputs,
|
||||
)?;
|
||||
let current_repo_index_for_output = current_repo_index.clone();
|
||||
let runner = make_live_runner(
|
||||
store.as_ref(),
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
timing.clone(),
|
||||
Some(download_log.clone()),
|
||||
Some(current_repo_index),
|
||||
Some(Arc::clone(&runtime)),
|
||||
phase2_config,
|
||||
(phase2_enabled && config.build_ccr_accumulator)
|
||||
.then(|| CcrAccumulator::new(vec![discovery.trust_anchor.clone()])),
|
||||
);
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
&discovery.trust_anchor,
|
||||
derive_tal_id(&discovery),
|
||||
None,
|
||||
&discovery.ca_instance,
|
||||
);
|
||||
let TreeRunAuditOutput {
|
||||
tree,
|
||||
publication_points,
|
||||
} = if phase2_enabled {
|
||||
run_tree_parallel_phase2_audit(root, &runner, config)?
|
||||
} else {
|
||||
run_tree_serial_audit(root, &runner, config)?
|
||||
};
|
||||
let downloads = download_log.snapshot_events();
|
||||
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
|
||||
Ok(RunTreeFromTalAuditOutput {
|
||||
discovery: discovery.clone(),
|
||||
discoveries: vec![discovery],
|
||||
successful_tal_inputs: Vec::new(),
|
||||
tree,
|
||||
publication_points,
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: snapshot_current_repo_objects(
|
||||
Some(¤t_repo_index_for_output),
|
||||
collect_current_repo_objects,
|
||||
),
|
||||
ccr_accumulator: runner.ccr_accumulator_snapshot(),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_multi_root_parallel_audit_inner<H, R>(
|
||||
store: Arc<crate::repository::storage::RocksStore>,
|
||||
policy: &crate::validation::policy::Policy,
|
||||
tal_inputs: Vec<TalInputSpec>,
|
||||
http_fetcher: &H,
|
||||
rsync_fetcher: &R,
|
||||
validation_time: time::OffsetDateTime,
|
||||
config: &TreeRunConfig,
|
||||
parallel_config: ParallelPhase1Config,
|
||||
phase2_config: Option<ParallelPhase2Config>,
|
||||
collect_current_repo_objects: bool,
|
||||
timing: Option<TimingHandle>,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
|
||||
where
|
||||
H: Fetcher + Clone + 'static,
|
||||
R: crate::repository::fetch::rsync::RsyncFetcher + Clone + 'static,
|
||||
{
|
||||
// Constraints are an immutable per-run policy snapshot. The phase-2
|
||||
// ready-stage binds the snapshot to each CA's TAL and moves an Arc into
|
||||
// ROA/ASPA worker state, so constrained multi-TAL runs retain the same
|
||||
// parallel scheduler as unconstrained runs.
|
||||
let phase2_enabled = phase2_config.is_some();
|
||||
if tal_inputs.is_empty() {
|
||||
return Err(RunTreeFromTalError::Setup(
|
||||
"multi-TAL run requires at least one TAL input".to_string(),
|
||||
));
|
||||
}
|
||||
let roots = discover_multiple_roots_from_tal_inputs(
|
||||
&tal_inputs,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
policy.strict.name,
|
||||
)?;
|
||||
let primary = roots.first().cloned().ok_or_else(|| {
|
||||
RunTreeFromTalError::Setup("multi-TAL root discovery returned no roots".to_string())
|
||||
})?;
|
||||
let discoveries = roots
|
||||
.iter()
|
||||
.map(|item| item.discovery.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let successful_tal_inputs = roots
|
||||
.iter()
|
||||
.map(|item| item.tal_input.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let root_handles = roots
|
||||
.iter()
|
||||
.map(|item| item.root_handle.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let download_log = DownloadLogHandle::new();
|
||||
let (runtime, current_repo_index) = build_phase1_repo_sync_runtime(
|
||||
Arc::clone(&store),
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
parallel_config,
|
||||
timing.clone(),
|
||||
Some(download_log.clone()),
|
||||
successful_tal_inputs.clone(),
|
||||
)?;
|
||||
let current_repo_index_for_output = current_repo_index.clone();
|
||||
let runner = make_live_runner(
|
||||
store.as_ref(),
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
timing.clone(),
|
||||
Some(download_log.clone()),
|
||||
Some(current_repo_index),
|
||||
Some(Arc::clone(&runtime)),
|
||||
phase2_config,
|
||||
(phase2_enabled && config.build_ccr_accumulator).then(|| {
|
||||
CcrAccumulator::new(
|
||||
discoveries
|
||||
.iter()
|
||||
.map(|item| item.trust_anchor.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
let TreeRunAuditOutput {
|
||||
tree,
|
||||
publication_points,
|
||||
} = if phase2_enabled {
|
||||
run_tree_parallel_phase2_audit_multi_root(root_handles, &runner, config)?
|
||||
} else {
|
||||
run_tree_serial_audit_multi_root(root_handles, &runner, config)?
|
||||
};
|
||||
let downloads = download_log.snapshot_events();
|
||||
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
|
||||
Ok(RunTreeFromTalAuditOutput {
|
||||
discovery: primary.discovery.clone(),
|
||||
discoveries,
|
||||
successful_tal_inputs,
|
||||
tree,
|
||||
publication_points,
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: snapshot_current_repo_objects(
|
||||
Some(¤t_repo_index_for_output),
|
||||
collect_current_repo_objects,
|
||||
),
|
||||
ccr_accumulator: runner.ccr_accumulator_snapshot(),
|
||||
})
|
||||
}
|
||||
363
src/validation/tree.rs
Normal file
363
src/validation/tree.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
use crate::model::rc::{AsResourceSet, IpResourceSet};
|
||||
use crate::output::audit::{DiscoveredFrom, PublicationPointAudit};
|
||||
use crate::output::report::Warning;
|
||||
use crate::validation::manifest::PublicationPointSource;
|
||||
use crate::validation::objects::{AspaAttestation, ObjectsOutput, RouterKeyPayload, Vrp};
|
||||
use crate::validation::publication_point::PublicationPointSnapshot;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
pub const DEFAULT_MAX_CA_DEPTH: usize = 32;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TreeRunConfig {
|
||||
/// Max CA instance depth to process (0 = root only).
|
||||
pub max_depth: Option<usize>,
|
||||
/// Max number of CA instances to process.
|
||||
pub max_instances: Option<usize>,
|
||||
/// Drop per-object audit payload when the caller only needs validation outputs.
|
||||
pub compact_audit: bool,
|
||||
/// Build online CCR manifest projections during phase2 validation.
|
||||
pub build_ccr_accumulator: bool,
|
||||
}
|
||||
|
||||
impl Default for TreeRunConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_depth: Some(DEFAULT_MAX_CA_DEPTH),
|
||||
max_instances: None,
|
||||
compact_audit: false,
|
||||
build_ccr_accumulator: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ca_depth_is_allowed(config: &TreeRunConfig, depth: usize) -> bool {
|
||||
config.max_depth.is_none_or(|max_depth| depth <= max_depth)
|
||||
}
|
||||
|
||||
pub(crate) fn next_allowed_ca_depth(config: &TreeRunConfig, parent_depth: usize) -> Option<usize> {
|
||||
let child_depth = parent_depth.checked_add(1)?;
|
||||
ca_depth_is_allowed(config, child_depth).then_some(child_depth)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CaCertificateRef {
|
||||
InlineDer(Vec<u8>),
|
||||
RepoBytes {
|
||||
sha256_hex: String,
|
||||
cached_der: Arc<OnceLock<Arc<[u8]>>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl PartialEq for CaCertificateRef {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::InlineDer(left), Self::InlineDer(right)) => left == right,
|
||||
(
|
||||
Self::RepoBytes {
|
||||
sha256_hex: left, ..
|
||||
},
|
||||
Self::RepoBytes {
|
||||
sha256_hex: right, ..
|
||||
},
|
||||
) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for CaCertificateRef {}
|
||||
|
||||
impl CaCertificateRef {
|
||||
pub fn inline_der(bytes: Vec<u8>) -> Self {
|
||||
Self::InlineDer(bytes)
|
||||
}
|
||||
|
||||
pub fn repo_bytes(sha256_hex: String) -> Self {
|
||||
Self::RepoBytes {
|
||||
sha256_hex,
|
||||
cached_der: Arc::new(OnceLock::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sha256_hex(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::InlineDer(_) => None,
|
||||
Self::RepoBytes { sha256_hex, .. } => Some(sha256_hex.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn der<'a>(
|
||||
&'a self,
|
||||
store: &crate::repository::storage::RocksStore,
|
||||
) -> Result<Cow<'a, [u8]>, String> {
|
||||
match self {
|
||||
Self::InlineDer(bytes) => Ok(Cow::Borrowed(bytes.as_slice())),
|
||||
Self::RepoBytes {
|
||||
sha256_hex,
|
||||
cached_der,
|
||||
} => {
|
||||
if cached_der.get().is_none() {
|
||||
let bytes = store
|
||||
.get_blob_bytes(sha256_hex)
|
||||
.map_err(|e| format!("load CA certificate bytes failed: {e}"))?
|
||||
.ok_or_else(|| {
|
||||
format!("missing CA certificate repo bytes for sha256={sha256_hex}")
|
||||
})?;
|
||||
let _ = cached_der.set(Arc::from(bytes));
|
||||
}
|
||||
let bytes = cached_der.get().ok_or_else(|| {
|
||||
format!("missing cached CA certificate bytes for sha256={sha256_hex}")
|
||||
})?;
|
||||
Ok(Cow::Borrowed(bytes.as_ref()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CaInstanceHandle {
|
||||
pub depth: usize,
|
||||
pub tal_id: String,
|
||||
pub parent_manifest_rsync_uri: Option<String>,
|
||||
/// CA certificate bytes or a lazy repo-bytes reference for this CA instance.
|
||||
pub ca_certificate: CaCertificateRef,
|
||||
/// rsync URI of this CA certificate object (where it is published).
|
||||
///
|
||||
/// This is used for strict AIA binding checks (RFC 6487 §4.8.7) when validating
|
||||
/// subordinate CA and EE certificates. Trust anchor certificates downloaded from
|
||||
/// TAL URIs may not have an rsync publication URI, so this can be `None`.
|
||||
pub ca_certificate_rsync_uri: Option<String>,
|
||||
/// Effective (fully resolved) resources for this CA instance, used for resource-path
|
||||
/// validation of subordinate CA certificates.
|
||||
pub effective_ip_resources: Option<IpResourceSet>,
|
||||
pub effective_as_resources: Option<AsResourceSet>,
|
||||
pub rsync_base_uri: String,
|
||||
pub manifest_rsync_uri: String,
|
||||
pub publication_point_rsync_uri: String,
|
||||
pub rrdp_notification_uri: Option<String>,
|
||||
}
|
||||
|
||||
impl CaInstanceHandle {
|
||||
pub fn with_depth(mut self, depth: usize) -> Self {
|
||||
self.depth = depth;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ca_certificate_der<'a>(
|
||||
&'a self,
|
||||
store: &crate::repository::storage::RocksStore,
|
||||
) -> Result<Cow<'a, [u8]>, String> {
|
||||
self.ca_certificate.der(store)
|
||||
}
|
||||
|
||||
pub fn ca_certificate_sha256_hex(&self) -> Option<&str> {
|
||||
self.ca_certificate.sha256_hex()
|
||||
}
|
||||
|
||||
pub fn ca_certificate_sha256_32(&self) -> Option<[u8; 32]> {
|
||||
match &self.ca_certificate {
|
||||
CaCertificateRef::InlineDer(bytes) => {
|
||||
use sha2::Digest as _;
|
||||
let digest = sha2::Sha256::digest(bytes);
|
||||
Some(digest.into())
|
||||
}
|
||||
CaCertificateRef::RepoBytes { sha256_hex, .. } => {
|
||||
let mut out = [0u8; 32];
|
||||
hex::decode_to_slice(sha256_hex, &mut out).ok()?;
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublicationPointRunResult {
|
||||
pub source: PublicationPointSource,
|
||||
pub snapshot: Option<PublicationPointSnapshot>,
|
||||
pub warnings: Vec<Warning>,
|
||||
pub objects: ObjectsOutput,
|
||||
pub audit: PublicationPointAudit,
|
||||
/// Candidate child CA instances to enqueue after this publication point completes.
|
||||
///
|
||||
/// - For `Fresh`, these are discovered from the current validated publication point.
|
||||
/// - For failed synchronization, the result contains no child instances;
|
||||
/// callers may retry the repository on a later run.
|
||||
pub discovered_children: Vec<DiscoveredChildCaInstance>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DiscoveredChildEntryProjection {
|
||||
pub child_ski: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DiscoveredChildCaInstance {
|
||||
pub handle: CaInstanceHandle,
|
||||
pub discovered_from: DiscoveredFrom,
|
||||
pub child_entry_projection: Option<DiscoveredChildEntryProjection>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TreeRunOutput {
|
||||
pub instances_processed: usize,
|
||||
pub instances_failed: usize,
|
||||
pub warnings: Vec<Warning>,
|
||||
pub vrps: Vec<Vrp>,
|
||||
pub aspas: Vec<AspaAttestation>,
|
||||
pub router_keys: Vec<RouterKeyPayload>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TreeRunError {
|
||||
#[error("publication point runner failed: {0}")]
|
||||
Runner(String),
|
||||
}
|
||||
|
||||
pub trait PublicationPointRunner {
|
||||
fn run_publication_point(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
) -> Result<PublicationPointRunResult, String>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TreeRunAuditOutput {
|
||||
pub tree: TreeRunOutput,
|
||||
pub publication_points: Vec<PublicationPointAudit>,
|
||||
}
|
||||
|
||||
pub fn run_tree_serial(
|
||||
root: CaInstanceHandle,
|
||||
runner: &dyn PublicationPointRunner,
|
||||
config: &TreeRunConfig,
|
||||
) -> Result<TreeRunOutput, TreeRunError> {
|
||||
Ok(run_tree_serial_audit(root, runner, config)?.tree)
|
||||
}
|
||||
|
||||
pub fn run_tree_serial_audit(
|
||||
root: CaInstanceHandle,
|
||||
runner: &dyn PublicationPointRunner,
|
||||
config: &TreeRunConfig,
|
||||
) -> Result<TreeRunAuditOutput, TreeRunError> {
|
||||
run_tree_serial_audit_multi_root(vec![root], runner, config)
|
||||
}
|
||||
|
||||
pub fn run_tree_serial_audit_multi_root(
|
||||
roots: Vec<CaInstanceHandle>,
|
||||
runner: &dyn PublicationPointRunner,
|
||||
config: &TreeRunConfig,
|
||||
) -> Result<TreeRunAuditOutput, TreeRunError> {
|
||||
#[derive(Clone, Debug)]
|
||||
struct QueuedCaInstance {
|
||||
id: u64,
|
||||
handle: CaInstanceHandle,
|
||||
parent_id: Option<u64>,
|
||||
discovered_from: Option<DiscoveredFrom>,
|
||||
}
|
||||
|
||||
let mut next_id: u64 = 0;
|
||||
let mut queue: std::collections::VecDeque<QueuedCaInstance> = std::collections::VecDeque::new();
|
||||
for root in roots {
|
||||
queue.push_back(QueuedCaInstance {
|
||||
id: next_id,
|
||||
handle: root,
|
||||
parent_id: None,
|
||||
discovered_from: None,
|
||||
});
|
||||
next_id += 1;
|
||||
}
|
||||
|
||||
let mut visited_manifest_uris: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
let mut instances_processed = 0usize;
|
||||
let mut instances_failed = 0usize;
|
||||
let mut warnings: Vec<Warning> = Vec::new();
|
||||
let mut vrps: Vec<Vrp> = Vec::new();
|
||||
let mut aspas: Vec<AspaAttestation> = Vec::new();
|
||||
let mut router_keys: Vec<RouterKeyPayload> = Vec::new();
|
||||
let mut publication_points: Vec<PublicationPointAudit> = Vec::new();
|
||||
|
||||
while let Some(node) = queue.pop_front() {
|
||||
let ca = &node.handle;
|
||||
if !visited_manifest_uris.insert(ca.manifest_rsync_uri.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(max_instances) = config.max_instances
|
||||
&& instances_processed >= max_instances
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if !ca_depth_is_allowed(config, ca.depth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let res = match runner.run_publication_point(ca) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
instances_failed += 1;
|
||||
warnings.push(
|
||||
Warning::new(format!("publication point failed: {e}"))
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
instances_processed += 1;
|
||||
warnings.extend(res.warnings);
|
||||
warnings.extend(res.objects.warnings);
|
||||
vrps.extend(res.objects.vrps);
|
||||
aspas.extend(res.objects.aspas);
|
||||
router_keys.extend(res.objects.router_keys);
|
||||
|
||||
let mut audit = res.audit;
|
||||
audit.node_id = Some(node.id);
|
||||
audit.parent_node_id = node.parent_id;
|
||||
audit.discovered_from = node.discovered_from.clone();
|
||||
if config.compact_audit {
|
||||
audit.objects.clear();
|
||||
audit.warnings.clear();
|
||||
}
|
||||
publication_points.push(audit);
|
||||
|
||||
let Some(child_depth) = next_allowed_ca_depth(config, ca.depth) else {
|
||||
continue;
|
||||
};
|
||||
let mut children = res.discovered_children;
|
||||
children.sort_by(|a, b| {
|
||||
a.handle
|
||||
.manifest_rsync_uri
|
||||
.cmp(&b.handle.manifest_rsync_uri)
|
||||
.then_with(|| {
|
||||
a.discovered_from
|
||||
.child_ca_certificate_rsync_uri
|
||||
.cmp(&b.discovered_from.child_ca_certificate_rsync_uri)
|
||||
})
|
||||
});
|
||||
for child in children {
|
||||
queue.push_back(QueuedCaInstance {
|
||||
id: next_id,
|
||||
handle: child.handle.with_depth(child_depth),
|
||||
parent_id: Some(node.id),
|
||||
discovered_from: Some(child.discovered_from),
|
||||
});
|
||||
next_id += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TreeRunAuditOutput {
|
||||
tree: TreeRunOutput {
|
||||
instances_processed,
|
||||
instances_failed,
|
||||
warnings,
|
||||
vrps,
|
||||
aspas,
|
||||
router_keys,
|
||||
},
|
||||
publication_points,
|
||||
})
|
||||
}
|
||||
33
src/validation/tree_parallel.rs
Normal file
33
src/validation/tree_parallel.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
#![allow(clippy::too_many_arguments, clippy::large_enum_variant)]
|
||||
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::output::audit::{DiscoveredFrom, PublicationPointAudit};
|
||||
use crate::output::report::Warning;
|
||||
use crate::scheduler::object_worker::{
|
||||
ObjectTaskExecutor, ObjectWorkerSubmitError, ScopedObjectWorkerPool,
|
||||
};
|
||||
use crate::scheduler::repo_runtime::{RepoSyncRequestStatus, RepoSyncRuntimeOutcome};
|
||||
use crate::scheduler::types::RepoIdentity;
|
||||
use crate::validation::manifest::PublicationPointData;
|
||||
use crate::validation::objects::{
|
||||
ObjectsOutput, OwnedRoaTask, ParallelObjectsPrepare, ParallelObjectsStage,
|
||||
prepare_publication_point_for_parallel_roa_and_ta_constraints, reduce_parallel_roa_stage,
|
||||
};
|
||||
use crate::validation::policy::SignedObjectFailurePolicy;
|
||||
use crate::validation::tree::{
|
||||
CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
|
||||
TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput, ca_depth_is_allowed,
|
||||
next_allowed_ca_depth, run_tree_serial_audit_multi_root,
|
||||
};
|
||||
use crate::validation::tree_runner::{
|
||||
FreshPublicationPointFinalizeOutput, FreshPublicationPointStage, Rpkiv1PublicationPointRunner,
|
||||
};
|
||||
|
||||
include!("tree_parallel/state.rs");
|
||||
include!("tree_parallel/phase2.rs");
|
||||
include!("tree_parallel/ready_stage.rs");
|
||||
include!("tree_parallel/dispatch.rs");
|
||||
include!("tree_parallel/finalize.rs");
|
||||
450
src/validation/tree_parallel/dispatch.rs
Normal file
450
src/validation/tree_parallel/dispatch.rs
Normal file
@@ -0,0 +1,450 @@
|
||||
fn flush_pending_roa_dispatch(
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
pending_roa_dispatch: &mut VecDeque<OwnedRoaTask>,
|
||||
inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
|
||||
) -> Result<RoaDispatchMetrics, TreeRunError> {
|
||||
let started = Instant::now();
|
||||
let mut metrics = RoaDispatchMetrics::default();
|
||||
let Some(pool) = runner.parallel_roa_worker_pool.as_ref() else {
|
||||
return Ok(metrics);
|
||||
};
|
||||
while let Some(mut task) = pending_roa_dispatch.pop_front() {
|
||||
metrics.attempted += 1;
|
||||
let pp_id = task.publication_point_id;
|
||||
task.submitted_at = Some(Instant::now());
|
||||
match pool.try_submit_round_robin(task) {
|
||||
Ok(_) => {
|
||||
metrics.submitted += 1;
|
||||
if let Some(state) = inflight_publication_points.get_mut(&pp_id) {
|
||||
let now = Instant::now();
|
||||
state.tasks_submitted += 1;
|
||||
if state.first_task_submitted_at.is_none() {
|
||||
state.first_task_submitted_at = Some(now);
|
||||
}
|
||||
state.last_task_submitted_at = Some(now);
|
||||
}
|
||||
}
|
||||
Err(ObjectWorkerSubmitError::QueueFull { task, .. }) => {
|
||||
pending_roa_dispatch.push_front(task);
|
||||
metrics.queue_full = true;
|
||||
break;
|
||||
}
|
||||
Err(ObjectWorkerSubmitError::Disconnected { .. }) => {
|
||||
return Err(TreeRunError::Runner(
|
||||
"parallel ROA worker queue disconnected".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
metrics.pending_remaining = pending_roa_dispatch.len();
|
||||
metrics.duration_ms = elapsed_ms(started);
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
fn flush_pending_roa_dispatch_with_progress(
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
pending_roa_dispatch: &mut VecDeque<OwnedRoaTask>,
|
||||
inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
|
||||
pending_finalization: &VecDeque<FinalizeTask>,
|
||||
) -> Result<(), TreeRunError> {
|
||||
let dispatch_metrics =
|
||||
flush_pending_roa_dispatch(runner, pending_roa_dispatch, inflight_publication_points)?;
|
||||
if dispatch_metrics.attempted > 0 || dispatch_metrics.queue_full {
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_roa_dispatch_batch",
|
||||
serde_json::json!({
|
||||
"attempted": dispatch_metrics.attempted,
|
||||
"submitted": dispatch_metrics.submitted,
|
||||
"queue_full": dispatch_metrics.queue_full,
|
||||
"pending_remaining": dispatch_metrics.pending_remaining,
|
||||
"duration_ms": dispatch_metrics.duration_ms,
|
||||
"inflight_publication_points": inflight_publication_points.len(),
|
||||
"pending_finalization_len": pending_finalization.len(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn drain_object_results(
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
|
||||
pending_finalization: &mut VecDeque<FinalizeTask>,
|
||||
result_budget: usize,
|
||||
) -> Result<ObjectDrainMetrics, TreeRunError> {
|
||||
let started = Instant::now();
|
||||
let mut metrics = ObjectDrainMetrics::default();
|
||||
let Some(pool) = runner.parallel_roa_worker_pool.as_ref() else {
|
||||
return Ok(metrics);
|
||||
};
|
||||
let result_budget = result_budget.max(1);
|
||||
while metrics.results_drained < result_budget {
|
||||
let Some(result) = pool
|
||||
.recv_result_timeout(Duration::from_millis(0))
|
||||
.map_err(TreeRunError::Runner)?
|
||||
else {
|
||||
break;
|
||||
};
|
||||
metrics.results_drained += 1;
|
||||
let pp_id = result.publication_point_id;
|
||||
let _worker_index = result.worker_index;
|
||||
metrics.worker_ms_total += result.worker_ms;
|
||||
metrics.worker_ms_max = metrics.worker_ms_max.max(result.worker_ms);
|
||||
metrics.queue_wait_ms_total += result.queue_wait_ms;
|
||||
metrics.queue_wait_ms_max = metrics.queue_wait_ms_max.max(result.queue_wait_ms);
|
||||
let should_finalize = if let Some(state) = inflight_publication_points.get_mut(&pp_id) {
|
||||
let now = Instant::now();
|
||||
if state.first_result_at.is_none() {
|
||||
state.first_result_at = Some(now);
|
||||
}
|
||||
state.last_result_at = Some(now);
|
||||
state.worker_ms_total += result.worker_ms;
|
||||
state.worker_ms_max = state.worker_ms_max.max(result.worker_ms);
|
||||
state.queue_wait_ms_total += result.queue_wait_ms;
|
||||
state.queue_wait_ms_max = state.queue_wait_ms_max.max(result.queue_wait_ms);
|
||||
state.results.push(result);
|
||||
state.results.len() == state.task_count
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if should_finalize {
|
||||
let mut state = inflight_publication_points
|
||||
.remove(&pp_id)
|
||||
.expect("inflight publication point must exist");
|
||||
state.finalize_enqueued_at = Some(Instant::now());
|
||||
metrics.publication_points_completed += 1;
|
||||
pending_finalization.push_back(FinalizeTask { state });
|
||||
}
|
||||
}
|
||||
metrics.result_budget_exhausted = metrics.results_drained == result_budget;
|
||||
metrics.duration_ms = elapsed_ms(started);
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
fn drain_object_results_with_progress(
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
|
||||
pending_finalization: &mut VecDeque<FinalizeTask>,
|
||||
pending_roa_dispatch_len: usize,
|
||||
result_budget: usize,
|
||||
) -> Result<(), TreeRunError> {
|
||||
let drain_metrics = drain_object_results(
|
||||
runner,
|
||||
inflight_publication_points,
|
||||
pending_finalization,
|
||||
result_budget,
|
||||
)?;
|
||||
if drain_metrics.results_drained > 0 || drain_metrics.result_budget_exhausted {
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_object_results_drain",
|
||||
serde_json::json!({
|
||||
"results_drained": drain_metrics.results_drained,
|
||||
"publication_points_completed": drain_metrics.publication_points_completed,
|
||||
"result_budget_exhausted": drain_metrics.result_budget_exhausted,
|
||||
"result_drain_batch_size": result_budget,
|
||||
"worker_ms_total": drain_metrics.worker_ms_total,
|
||||
"worker_ms_max": drain_metrics.worker_ms_max,
|
||||
"queue_wait_ms_total": drain_metrics.queue_wait_ms_total,
|
||||
"queue_wait_ms_max": drain_metrics.queue_wait_ms_max,
|
||||
"duration_ms": drain_metrics.duration_ms,
|
||||
"pending_roa_dispatch_len": pending_roa_dispatch_len,
|
||||
"inflight_publication_points": inflight_publication_points.len(),
|
||||
"pending_finalization_len": pending_finalization.len(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Submission endpoint of the stage pool, abstracted so the dispatch loop can
|
||||
/// be tested with a deterministic backpressure source.
|
||||
trait ReadyStageSubmitter {
|
||||
// The error must hand the task back so the dispatch loop can requeue it;
|
||||
// boxing it away would only shuffle the same bytes around.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn try_submit_ready_stage(
|
||||
&mut self,
|
||||
task: ReadyStageTask,
|
||||
) -> Result<(), ObjectWorkerSubmitError<ReadyStageTask>>;
|
||||
}
|
||||
|
||||
impl ReadyStageSubmitter for ReadyStagePool<'_, '_> {
|
||||
fn try_submit_ready_stage(
|
||||
&mut self,
|
||||
task: ReadyStageTask,
|
||||
) -> Result<(), ObjectWorkerSubmitError<ReadyStageTask>> {
|
||||
self.try_submit_round_robin(task).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch loop of the pool-enabled ready batch: pop ready publication
|
||||
/// points and submit their compute tasks to the stage pool, following the
|
||||
/// `flush_pending_roa_dispatch` backpressure pattern — on a full worker queue
|
||||
/// the publication point goes back to the head of the ready queue so the next
|
||||
/// turn retries it first (nothing lost, nothing duplicated).
|
||||
fn submit_ready_batch_to_stage_pool(
|
||||
stage_pool: &mut impl ReadyStageSubmitter,
|
||||
ready_queue: &mut VecDeque<ReadyCaInstance>,
|
||||
staging_inflight: &mut usize,
|
||||
ready_batch_size: usize,
|
||||
ready_batch_wall_time_budget: Duration,
|
||||
) -> Result<StageDispatchMetrics, TreeRunError> {
|
||||
let started = Instant::now();
|
||||
let mut metrics = StageDispatchMetrics::default();
|
||||
while metrics.submitted < ready_batch_size {
|
||||
let Some(ready) = ready_queue.pop_front() else {
|
||||
break;
|
||||
};
|
||||
let task = ReadyStageTask {
|
||||
ready_queue_len_after_pop: ready_queue.len(),
|
||||
ready,
|
||||
submitted_at: Instant::now(),
|
||||
};
|
||||
match stage_pool.try_submit_ready_stage(task) {
|
||||
Ok(_) => {
|
||||
*staging_inflight += 1;
|
||||
metrics.submitted += 1;
|
||||
}
|
||||
Err(ObjectWorkerSubmitError::QueueFull { task, .. }) => {
|
||||
ready_queue.push_front(task.ready);
|
||||
metrics.queue_full = true;
|
||||
break;
|
||||
}
|
||||
Err(ObjectWorkerSubmitError::Disconnected { .. }) => {
|
||||
return Err(TreeRunError::Runner(
|
||||
"ready stage worker queue disconnected".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if metrics.submitted > 0 && started.elapsed() >= ready_batch_wall_time_budget {
|
||||
break;
|
||||
}
|
||||
}
|
||||
metrics.duration_ms = elapsed_ms(started);
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
/// Collect every available stage result without blocking and run the apply
|
||||
/// phase for each on the control thread, exactly like the inline path would
|
||||
/// have done right after compute. Drained metrics aggregate into the same
|
||||
/// `ReadyStageBatchMetrics`, keeping the `phase2_ready_queue_*` events
|
||||
/// unchanged.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn drain_stage_results(
|
||||
stage_pool: &ReadyStagePool<'_, '_>,
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
next_id: &mut u64,
|
||||
ca_queue: &mut VecDeque<QueuedCaInstance>,
|
||||
pending_roa_dispatch: &mut VecDeque<OwnedRoaTask>,
|
||||
inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
|
||||
pending_finalization: &mut VecDeque<FinalizeTask>,
|
||||
finished: &mut Vec<FinishedPublicationPoint>,
|
||||
staging_inflight: &mut usize,
|
||||
batch_metrics: &mut ReadyStageBatchMetrics,
|
||||
pool_metrics: &mut StageDrainMetrics,
|
||||
config: &TreeRunConfig,
|
||||
) -> Result<(), TreeRunError> {
|
||||
let started = Instant::now();
|
||||
loop {
|
||||
let Some(result) = stage_pool
|
||||
.recv_result_timeout(Duration::from_millis(0))
|
||||
.map_err(TreeRunError::Runner)?
|
||||
else {
|
||||
break;
|
||||
};
|
||||
pool_metrics.results_drained += 1;
|
||||
pool_metrics.queue_wait_ms_total += result.queue_wait_ms;
|
||||
pool_metrics.queue_wait_ms_max = pool_metrics.queue_wait_ms_max.max(result.queue_wait_ms);
|
||||
pool_metrics.worker_ms_total += result.worker_ms;
|
||||
pool_metrics.worker_ms_max = pool_metrics.worker_ms_max.max(result.worker_ms);
|
||||
*staging_inflight = staging_inflight.saturating_sub(1);
|
||||
let metrics = apply_ready_publication_point_stage(
|
||||
runner,
|
||||
next_id,
|
||||
ca_queue,
|
||||
pending_roa_dispatch,
|
||||
inflight_publication_points,
|
||||
pending_finalization,
|
||||
finished,
|
||||
result.outcome,
|
||||
result.metrics,
|
||||
config,
|
||||
config.compact_audit,
|
||||
);
|
||||
batch_metrics.record(metrics);
|
||||
}
|
||||
pool_metrics.duration_ms += elapsed_ms(started);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn submit_pending_finalization(
|
||||
finalize_task_tx: &SyncSender<FinalizeTask>,
|
||||
pending_finalization: &mut VecDeque<FinalizeTask>,
|
||||
finalize_inflight: &mut usize,
|
||||
) -> Result<FinalizeSubmitMetrics, TreeRunError> {
|
||||
let started = Instant::now();
|
||||
let mut metrics = FinalizeSubmitMetrics::default();
|
||||
while let Some(task) = pending_finalization.pop_front() {
|
||||
match finalize_task_tx.try_send(task) {
|
||||
Ok(()) => {
|
||||
metrics.submitted += 1;
|
||||
*finalize_inflight += 1;
|
||||
}
|
||||
Err(TrySendError::Full(task)) => {
|
||||
pending_finalization.push_front(task);
|
||||
metrics.queue_full = true;
|
||||
break;
|
||||
}
|
||||
Err(TrySendError::Disconnected(_task)) => {
|
||||
return Err(TreeRunError::Runner(
|
||||
"phase2 finalize worker queue disconnected".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
metrics.duration_ms = elapsed_ms(started);
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
fn submit_pending_finalization_with_progress(
|
||||
finalize_task_tx: &SyncSender<FinalizeTask>,
|
||||
pending_finalization: &mut VecDeque<FinalizeTask>,
|
||||
finalize_inflight: &mut usize,
|
||||
finalize_queue_capacity: usize,
|
||||
pending_roa_dispatch_len: usize,
|
||||
inflight_publication_points_len: usize,
|
||||
) -> Result<(), TreeRunError> {
|
||||
let submit_metrics =
|
||||
submit_pending_finalization(finalize_task_tx, pending_finalization, finalize_inflight)?;
|
||||
if submit_metrics.submitted > 0 || submit_metrics.queue_full {
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_finalize_task_submit",
|
||||
serde_json::json!({
|
||||
"submitted": submit_metrics.submitted,
|
||||
"queue_full": submit_metrics.queue_full,
|
||||
"duration_ms": submit_metrics.duration_ms,
|
||||
"finalize_queue_capacity": finalize_queue_capacity,
|
||||
"pending_finalization_len": pending_finalization.len(),
|
||||
"finalize_inflight": *finalize_inflight,
|
||||
"pending_roa_dispatch_len": pending_roa_dispatch_len,
|
||||
"inflight_publication_points": inflight_publication_points_len,
|
||||
}),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn drain_finalize_results(
|
||||
finalize_result_rx: &Receiver<FinalizeWorkerResult>,
|
||||
finished: &mut Vec<FinishedPublicationPoint>,
|
||||
finalize_inflight: &mut usize,
|
||||
) -> Result<FinalizeResultsDrainMetrics, TreeRunError> {
|
||||
let started = Instant::now();
|
||||
let mut metrics = FinalizeResultsDrainMetrics::default();
|
||||
loop {
|
||||
match finalize_result_rx.try_recv() {
|
||||
Ok(result) => {
|
||||
metrics.results_drained += 1;
|
||||
metrics.reduce_ms_total += result.metrics.reduce_ms;
|
||||
metrics.reduce_ms_max = metrics.reduce_ms_max.max(result.metrics.reduce_ms);
|
||||
metrics.finalize_ms_total += result.metrics.finalize_ms;
|
||||
metrics.finalize_ms_max = metrics.finalize_ms_max.max(result.metrics.finalize_ms);
|
||||
metrics.finalize_queue_wait_ms_max = metrics
|
||||
.finalize_queue_wait_ms_max
|
||||
.max(result.metrics.finalize_queue_wait_ms.unwrap_or(0));
|
||||
metrics.finalize_worker_ms_total += result.metrics.finalize_worker_ms;
|
||||
metrics.finalize_worker_ms_max = metrics
|
||||
.finalize_worker_ms_max
|
||||
.max(result.metrics.finalize_worker_ms);
|
||||
metrics.snapshot_pack_ms_total += result.metrics.snapshot_pack_ms;
|
||||
metrics.snapshot_pack_ms_max = metrics
|
||||
.snapshot_pack_ms_max
|
||||
.max(result.metrics.snapshot_pack_ms);
|
||||
metrics.ccr_projection_build_ms_total += result.metrics.ccr_projection_build_ms;
|
||||
metrics.ccr_projection_build_ms_max = metrics
|
||||
.ccr_projection_build_ms_max
|
||||
.max(result.metrics.ccr_projection_build_ms);
|
||||
metrics.ccr_append_ms_total += result.metrics.ccr_append_ms;
|
||||
metrics.ccr_append_ms_max =
|
||||
metrics.ccr_append_ms_max.max(result.metrics.ccr_append_ms);
|
||||
metrics.audit_build_ms_total += result.metrics.audit_build_ms;
|
||||
metrics.audit_build_ms_max = metrics
|
||||
.audit_build_ms_max
|
||||
.max(result.metrics.audit_build_ms);
|
||||
*finalize_inflight = finalize_inflight.saturating_sub(1);
|
||||
finished.push(result.finished);
|
||||
}
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
if *finalize_inflight == 0 {
|
||||
break;
|
||||
}
|
||||
return Err(TreeRunError::Runner(
|
||||
"phase2 finalize result channel disconnected".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
metrics.duration_ms = elapsed_ms(started);
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
fn drain_finalize_results_with_progress(
|
||||
finalize_result_rx: &Receiver<FinalizeWorkerResult>,
|
||||
finished: &mut Vec<FinishedPublicationPoint>,
|
||||
finalize_inflight: &mut usize,
|
||||
pending_finalization_len: usize,
|
||||
pending_roa_dispatch_len: usize,
|
||||
inflight_publication_points_len: usize,
|
||||
) -> Result<(), TreeRunError> {
|
||||
let drain_metrics = drain_finalize_results(finalize_result_rx, finished, finalize_inflight)?;
|
||||
if drain_metrics.results_drained >= 64
|
||||
|| (drain_metrics.results_drained > 0
|
||||
&& (*finalize_inflight == 0 || pending_finalization_len > 0))
|
||||
{
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_finalize_results_drain",
|
||||
serde_json::json!({
|
||||
"results_drained": drain_metrics.results_drained,
|
||||
"reduce_ms_total": drain_metrics.reduce_ms_total,
|
||||
"reduce_ms_max": drain_metrics.reduce_ms_max,
|
||||
"finalize_ms_total": drain_metrics.finalize_ms_total,
|
||||
"finalize_ms_max": drain_metrics.finalize_ms_max,
|
||||
"finalize_queue_wait_ms_max": drain_metrics.finalize_queue_wait_ms_max,
|
||||
"finalize_worker_ms_total": drain_metrics.finalize_worker_ms_total,
|
||||
"finalize_worker_ms_max": drain_metrics.finalize_worker_ms_max,
|
||||
"snapshot_pack_ms_total": drain_metrics.snapshot_pack_ms_total,
|
||||
"snapshot_pack_ms_max": drain_metrics.snapshot_pack_ms_max,
|
||||
"ccr_projection_build_ms_total": drain_metrics.ccr_projection_build_ms_total,
|
||||
"ccr_projection_build_ms_max": drain_metrics.ccr_projection_build_ms_max,
|
||||
"ccr_append_ms_total": drain_metrics.ccr_append_ms_total,
|
||||
"ccr_append_ms_max": drain_metrics.ccr_append_ms_max,
|
||||
"audit_build_ms_total": drain_metrics.audit_build_ms_total,
|
||||
"audit_build_ms_max": drain_metrics.audit_build_ms_max,
|
||||
"duration_ms": drain_metrics.duration_ms,
|
||||
"pending_finalization_len": pending_finalization_len,
|
||||
"finalize_inflight": *finalize_inflight,
|
||||
"pending_roa_dispatch_len": pending_roa_dispatch_len,
|
||||
"inflight_publication_points": inflight_publication_points_len,
|
||||
}),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_finalize_worker(
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
finalize_task_rx: Receiver<FinalizeTask>,
|
||||
finalize_result_tx: mpsc::Sender<FinalizeWorkerResult>,
|
||||
compact_audit: bool,
|
||||
) -> Result<(), TreeRunError> {
|
||||
while let Ok(task) = finalize_task_rx.recv() {
|
||||
let result = finalize_publication_point_state(runner, task.state, compact_audit);
|
||||
if finalize_result_tx.send(result).is_err() {
|
||||
return Err(TreeRunError::Runner(
|
||||
"phase2 finalize result receiver disconnected".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
488
src/validation/tree_parallel/finalize.rs
Normal file
488
src/validation/tree_parallel/finalize.rs
Normal file
@@ -0,0 +1,488 @@
|
||||
fn finalize_publication_point_state(
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
state: InflightPublicationPoint,
|
||||
compact_audit: bool,
|
||||
) -> FinalizeWorkerResult {
|
||||
let finalize_worker_started = Instant::now();
|
||||
let InflightPublicationPoint {
|
||||
node,
|
||||
fresh_stage,
|
||||
objects_prepare,
|
||||
repo_outcome,
|
||||
warnings,
|
||||
started_at,
|
||||
objects_started_at,
|
||||
task_count,
|
||||
tasks_submitted,
|
||||
first_task_submitted_at,
|
||||
last_task_submitted_at,
|
||||
first_result_at,
|
||||
last_result_at,
|
||||
worker_ms_total,
|
||||
worker_ms_max,
|
||||
queue_wait_ms_total,
|
||||
queue_wait_ms_max,
|
||||
finalize_enqueued_at,
|
||||
results,
|
||||
} = state;
|
||||
let finalize_queue_wait_ms = finalize_enqueued_at.or(last_result_at).map(|ready_at| {
|
||||
Instant::now()
|
||||
.saturating_duration_since(ready_at)
|
||||
.as_millis() as u64
|
||||
});
|
||||
let objects_processing_ms = objects_started_at.elapsed().as_millis() as u64;
|
||||
runner.record_publication_point_step_ms(
|
||||
&node.handle.manifest_rsync_uri,
|
||||
"fresh_objects_processing_lifetime",
|
||||
objects_processing_ms,
|
||||
);
|
||||
|
||||
let (result, mut metrics, reduce_ms) = match objects_prepare {
|
||||
ParallelObjectsPrepare::Staged(objects_stage) => {
|
||||
let reduce_started = Instant::now();
|
||||
let locked_files = objects_stage.locked_file_count();
|
||||
let reduce_result =
|
||||
reduce_parallel_roa_stage(objects_stage, results, runner.timing.as_ref());
|
||||
let reduce_ms = elapsed_ms(reduce_started);
|
||||
runner.record_publication_point_step_ms(
|
||||
&node.handle.manifest_rsync_uri,
|
||||
"fresh_roa_reduce",
|
||||
reduce_ms,
|
||||
);
|
||||
|
||||
let (result, metrics) = match reduce_result {
|
||||
Ok(mut objects) => {
|
||||
let finalize_started = Instant::now();
|
||||
objects
|
||||
.router_keys
|
||||
.extend(fresh_stage.discovered_router_keys.clone());
|
||||
let finalized = runner.finalize_fresh_publication_point_from_reducer(
|
||||
&node.handle,
|
||||
&fresh_stage.fresh_point,
|
||||
warnings,
|
||||
objects,
|
||||
fresh_stage.child_audits,
|
||||
fresh_stage.discovered_children,
|
||||
repo_outcome.repo_sync_source.as_deref(),
|
||||
repo_outcome.repo_sync_phase.as_deref(),
|
||||
repo_outcome.repo_sync_duration_ms,
|
||||
repo_outcome.repo_sync_err.as_deref(),
|
||||
);
|
||||
let finalize_ms = elapsed_ms(finalize_started);
|
||||
match finalized {
|
||||
Ok(output) => {
|
||||
let metrics = finalize_metrics_from_output(
|
||||
&output,
|
||||
reduce_ms,
|
||||
finalize_ms,
|
||||
finalize_queue_wait_ms,
|
||||
0,
|
||||
locked_files,
|
||||
);
|
||||
(
|
||||
compact_phase2_finished_result(output.result, compact_audit),
|
||||
metrics,
|
||||
)
|
||||
}
|
||||
Err(err) => (
|
||||
FinishedPublicationPointResult::Err(err),
|
||||
FinalizePublicationPointMetrics {
|
||||
reduce_ms,
|
||||
finalize_ms,
|
||||
finalize_queue_wait_ms,
|
||||
locked_files,
|
||||
..FinalizePublicationPointMetrics::default()
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
Err(err) => (
|
||||
FinishedPublicationPointResult::Err(err),
|
||||
FinalizePublicationPointMetrics {
|
||||
reduce_ms,
|
||||
finalize_queue_wait_ms,
|
||||
locked_files,
|
||||
..FinalizePublicationPointMetrics::default()
|
||||
},
|
||||
),
|
||||
};
|
||||
(result, metrics, reduce_ms)
|
||||
}
|
||||
ParallelObjectsPrepare::Complete(objects) => {
|
||||
// ROA prepare already produced complete objects for this publication
|
||||
// point, so there is nothing to reduce; finalize directly. This is
|
||||
// the former control-thread "direct finalize", now running on the
|
||||
// finalize worker through the regular task queue.
|
||||
let locked_files = fresh_stage.fresh_point.files().len();
|
||||
let finalize_started = Instant::now();
|
||||
let finalized = runner.finalize_fresh_publication_point_from_reducer(
|
||||
&node.handle,
|
||||
&fresh_stage.fresh_point,
|
||||
warnings,
|
||||
objects,
|
||||
fresh_stage.child_audits,
|
||||
fresh_stage.discovered_children,
|
||||
repo_outcome.repo_sync_source.as_deref(),
|
||||
repo_outcome.repo_sync_phase.as_deref(),
|
||||
repo_outcome.repo_sync_duration_ms,
|
||||
repo_outcome.repo_sync_err.as_deref(),
|
||||
);
|
||||
let finalize_ms = elapsed_ms(finalize_started);
|
||||
let (result, metrics) = match finalized {
|
||||
Ok(output) => {
|
||||
let metrics = finalize_metrics_from_output(
|
||||
&output,
|
||||
0,
|
||||
finalize_ms,
|
||||
finalize_queue_wait_ms,
|
||||
0,
|
||||
locked_files,
|
||||
);
|
||||
(
|
||||
compact_phase2_finished_result(output.result, compact_audit),
|
||||
metrics,
|
||||
)
|
||||
}
|
||||
Err(err) => (
|
||||
FinishedPublicationPointResult::Err(err),
|
||||
FinalizePublicationPointMetrics {
|
||||
finalize_ms,
|
||||
finalize_queue_wait_ms,
|
||||
locked_files,
|
||||
..FinalizePublicationPointMetrics::default()
|
||||
},
|
||||
),
|
||||
};
|
||||
(result, metrics, 0)
|
||||
}
|
||||
};
|
||||
let finalize_worker_ms = elapsed_ms(finalize_worker_started);
|
||||
metrics.finalize_worker_ms = finalize_worker_ms;
|
||||
runner.record_publication_point_step_ms(
|
||||
&node.handle.manifest_rsync_uri,
|
||||
"fresh_finalize_worker",
|
||||
finalize_worker_ms,
|
||||
);
|
||||
runner.record_publication_point_step_ms(
|
||||
&node.handle.manifest_rsync_uri,
|
||||
"fresh_finalize_queue_wait",
|
||||
finalize_queue_wait_ms.unwrap_or(0),
|
||||
);
|
||||
runner.record_publication_point_step_ms(
|
||||
&node.handle.manifest_rsync_uri,
|
||||
"fresh_finalize",
|
||||
metrics.finalize_ms,
|
||||
);
|
||||
runner.record_publication_point_total_ms(
|
||||
&node.handle.manifest_rsync_uri,
|
||||
started_at.elapsed().as_millis() as u64,
|
||||
);
|
||||
emit_finalize_breakdown(
|
||||
"phase2_finalize_worker_breakdown",
|
||||
node.handle.manifest_rsync_uri.as_str(),
|
||||
node.handle.publication_point_rsync_uri.as_str(),
|
||||
&metrics,
|
||||
);
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_publication_point_reduced",
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": node.handle.manifest_rsync_uri.as_str(),
|
||||
"publication_point_rsync_uri": node.handle.publication_point_rsync_uri.as_str(),
|
||||
"objects_processing_ms": objects_processing_ms,
|
||||
"task_count": task_count,
|
||||
"tasks_submitted": tasks_submitted,
|
||||
"first_task_submitted_ms": first_task_submitted_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
|
||||
"last_task_submitted_ms": last_task_submitted_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
|
||||
"task_submit_span_ms": match (first_task_submitted_at, last_task_submitted_at) {
|
||||
(Some(first), Some(last)) => Some(last.saturating_duration_since(first).as_millis() as u64),
|
||||
_ => None,
|
||||
},
|
||||
"first_result_ms": first_result_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
|
||||
"last_result_ms": last_result_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
|
||||
"all_results_ready_ms": last_result_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
|
||||
"finalize_queue_wait_ms": finalize_queue_wait_ms,
|
||||
"result_span_ms": match (first_result_at, last_result_at) {
|
||||
(Some(first), Some(last)) => Some(last.saturating_duration_since(first).as_millis() as u64),
|
||||
_ => None,
|
||||
},
|
||||
"worker_ms_total": worker_ms_total,
|
||||
"worker_ms_max": worker_ms_max,
|
||||
"worker_ms_avg": if task_count > 0 { worker_ms_total / task_count as u64 } else { 0 },
|
||||
"queue_wait_ms_total": queue_wait_ms_total,
|
||||
"queue_wait_ms_max": queue_wait_ms_max,
|
||||
"queue_wait_ms_avg": if task_count > 0 { queue_wait_ms_total / task_count as u64 } else { 0 },
|
||||
"reduce_ms": reduce_ms,
|
||||
"finalize_ms": metrics.finalize_ms,
|
||||
"finalize_worker_ms": finalize_worker_ms,
|
||||
"snapshot_pack_ms": metrics.snapshot_pack_ms,
|
||||
"ccr_projection_build_ms": metrics.ccr_projection_build_ms,
|
||||
"ccr_append_ms": metrics.ccr_append_ms,
|
||||
"audit_build_ms": metrics.audit_build_ms,
|
||||
"locked_files": metrics.locked_files,
|
||||
"child_count": metrics.child_count,
|
||||
"warning_count": metrics.warning_count,
|
||||
"vrp_count": metrics.vrp_count,
|
||||
"vap_count": metrics.vap_count,
|
||||
"router_key_count": metrics.router_key_count,
|
||||
"audit_object_count": metrics.audit_object_count,
|
||||
"total_duration_ms": started_at.elapsed().as_millis() as u64,
|
||||
}),
|
||||
);
|
||||
FinalizeWorkerResult {
|
||||
finished: FinishedPublicationPoint {
|
||||
node: FinishedPublicationPointNode::from_queued(node),
|
||||
result,
|
||||
},
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_repo_events(
|
||||
repo_runtime: &dyn crate::scheduler::repo_runtime::RepoSyncRuntime,
|
||||
ca_waiting_repo_by_identity: &mut HashMap<RepoIdentity, Vec<QueuedCaInstance>>,
|
||||
ready_queue: &mut VecDeque<ReadyCaInstance>,
|
||||
timeout: Duration,
|
||||
) -> Result<RepoDrainMetrics, TreeRunError> {
|
||||
let started = Instant::now();
|
||||
let mut metrics = RepoDrainMetrics::default();
|
||||
let events = repo_runtime
|
||||
.drain_repo_results_timeout(timeout, REPO_RESULT_DRAIN_MAX_EVENTS)
|
||||
.map_err(TreeRunError::Runner)?;
|
||||
for event in events {
|
||||
metrics.event_count += 1;
|
||||
metrics.completions += event.completions.len();
|
||||
for completion in event.completions {
|
||||
let mut outcome = completion.outcome;
|
||||
if completion.identity != event.transport_identity {
|
||||
// Shared RRDP/rsync transports release many publication points, but the transport
|
||||
// wall time should only be counted once in per-PP stage timing aggregation.
|
||||
outcome.repo_sync_duration_ms = 0;
|
||||
}
|
||||
if let Some(waiters) = ca_waiting_repo_by_identity.remove(&completion.identity) {
|
||||
metrics.ready_enqueued += waiters.len();
|
||||
for node in waiters {
|
||||
ready_queue.push_back(ReadyCaInstance {
|
||||
node,
|
||||
repo_outcome: outcome.clone(),
|
||||
ready_enqueued_at: Instant::now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
metrics.duration_ms = elapsed_ms(started);
|
||||
Ok(metrics)
|
||||
}
|
||||
|
||||
fn event_poll_timeout(
|
||||
ca_queue: &VecDeque<QueuedCaInstance>,
|
||||
ready_queue: &VecDeque<ReadyCaInstance>,
|
||||
pending_roa_dispatch: &VecDeque<OwnedRoaTask>,
|
||||
inflight_publication_points: &HashMap<u64, InflightPublicationPoint>,
|
||||
pending_finalization: &VecDeque<FinalizeTask>,
|
||||
finalize_inflight: usize,
|
||||
staging_inflight: usize,
|
||||
instances_started: usize,
|
||||
config: &TreeRunConfig,
|
||||
) -> Duration {
|
||||
// Stage results pending collection must not let the loop sleep: with the
|
||||
// stage pool enabled a 50ms nap per turn would collapse throughput.
|
||||
if !ready_queue.is_empty()
|
||||
|| !pending_roa_dispatch.is_empty()
|
||||
|| !inflight_publication_points.is_empty()
|
||||
|| !pending_finalization.is_empty()
|
||||
|| staging_inflight > 0
|
||||
|| (!ca_queue.is_empty() && can_start_more(instances_started, config))
|
||||
{
|
||||
Duration::from_millis(0)
|
||||
} else if finalize_inflight > 0 {
|
||||
Duration::from_millis(10)
|
||||
} else {
|
||||
Duration::from_millis(50)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete(
|
||||
ca_queue: &VecDeque<QueuedCaInstance>,
|
||||
ready_queue: &VecDeque<ReadyCaInstance>,
|
||||
ca_waiting_repo_by_identity: &HashMap<RepoIdentity, Vec<QueuedCaInstance>>,
|
||||
pending_roa_dispatch: &VecDeque<OwnedRoaTask>,
|
||||
inflight_publication_points: &HashMap<u64, InflightPublicationPoint>,
|
||||
pending_finalization: &VecDeque<FinalizeTask>,
|
||||
finalize_inflight: usize,
|
||||
staging_inflight: usize,
|
||||
instances_started: usize,
|
||||
config: &TreeRunConfig,
|
||||
) -> bool {
|
||||
// `staging_inflight == 0` additionally implies the stage result channel is
|
||||
// drained empty: the drain loop collects every available result each turn
|
||||
// and only decrements the counter while collecting.
|
||||
let ca_queue_done = ca_queue.is_empty() || !can_start_more(instances_started, config);
|
||||
ca_queue_done
|
||||
&& ready_queue.is_empty()
|
||||
&& ca_waiting_repo_by_identity.is_empty()
|
||||
&& pending_roa_dispatch.is_empty()
|
||||
&& inflight_publication_points.is_empty()
|
||||
&& pending_finalization.is_empty()
|
||||
&& finalize_inflight == 0
|
||||
&& staging_inflight == 0
|
||||
}
|
||||
|
||||
/// Minimum publication points per reduction shard; smaller runs stay
|
||||
/// single-threaded to avoid thread-spawn overhead dominating the reduction.
|
||||
const TREE_OUTPUT_MIN_SHARD_LEN: usize = 4096;
|
||||
/// Hard cap on reduction shards regardless of core count.
|
||||
const TREE_OUTPUT_MAX_SHARDS: usize = 8;
|
||||
|
||||
fn tree_output_shard_count(len: usize) -> usize {
|
||||
if len < TREE_OUTPUT_MIN_SHARD_LEN * 2 {
|
||||
return 1;
|
||||
}
|
||||
let parallel = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1);
|
||||
parallel
|
||||
.min(TREE_OUTPUT_MAX_SHARDS)
|
||||
.min(len / TREE_OUTPUT_MIN_SHARD_LEN)
|
||||
.max(1)
|
||||
}
|
||||
|
||||
/// Per-shard reduction result of the phase2 output merge. Shards cover
|
||||
/// contiguous ranges of the id-sorted finished list, so concatenating shard
|
||||
/// outputs in shard order reproduces the sequential single-threaded order
|
||||
/// exactly.
|
||||
#[derive(Default)]
|
||||
struct TreeOutputShardReduction {
|
||||
instances_processed: usize,
|
||||
instances_failed: usize,
|
||||
warnings: Vec<Warning>,
|
||||
vrps: Vec<crate::validation::objects::Vrp>,
|
||||
aspas: Vec<crate::validation::objects::AspaAttestation>,
|
||||
router_keys: Vec<crate::validation::objects::RouterKeyPayload>,
|
||||
publication_points: Vec<PublicationPointAudit>,
|
||||
}
|
||||
|
||||
fn reduce_finished_shard(items: Vec<FinishedPublicationPoint>) -> TreeOutputShardReduction {
|
||||
let mut reduction = TreeOutputShardReduction::default();
|
||||
for item in items {
|
||||
match item.result {
|
||||
FinishedPublicationPointResult::Ok {
|
||||
warnings: result_warnings,
|
||||
objects,
|
||||
audit,
|
||||
} => {
|
||||
reduction.instances_processed += 1;
|
||||
reduction.warnings.extend(result_warnings);
|
||||
reduction.warnings.extend(objects.warnings);
|
||||
reduction.vrps.extend(objects.vrps);
|
||||
reduction.aspas.extend(objects.aspas);
|
||||
reduction.router_keys.extend(objects.router_keys);
|
||||
|
||||
let mut audit: PublicationPointAudit = audit;
|
||||
audit.node_id = Some(item.node.id);
|
||||
audit.parent_node_id = item.node.parent_id;
|
||||
audit.discovered_from = item.node.discovered_from;
|
||||
reduction.publication_points.push(audit);
|
||||
}
|
||||
FinishedPublicationPointResult::Err(err) => {
|
||||
reduction.instances_failed += 1;
|
||||
reduction.warnings.push(
|
||||
Warning::new(format!("publication point failed: {err}"))
|
||||
.with_context(&item.node.manifest_rsync_uri),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
reduction
|
||||
}
|
||||
|
||||
fn merge_shard_reductions(reductions: Vec<TreeOutputShardReduction>) -> TreeOutputShardReduction {
|
||||
let mut iter = reductions.into_iter();
|
||||
let mut merged = iter.next().unwrap_or_default();
|
||||
for mut other in iter {
|
||||
merged.instances_processed += other.instances_processed;
|
||||
merged.instances_failed += other.instances_failed;
|
||||
merged.warnings.append(&mut other.warnings);
|
||||
merged.vrps.append(&mut other.vrps);
|
||||
merged.aspas.append(&mut other.aspas);
|
||||
merged.router_keys.append(&mut other.router_keys);
|
||||
merged
|
||||
.publication_points
|
||||
.append(&mut other.publication_points);
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
fn build_tree_output(mut finished: Vec<FinishedPublicationPoint>) -> TreeRunAuditOutput {
|
||||
let total_started = Instant::now();
|
||||
finished.sort_by_key(|item| item.node.id);
|
||||
let sort_ms = total_started.elapsed().as_millis() as u64;
|
||||
let shard_count = tree_output_shard_count(finished.len());
|
||||
|
||||
let reduce_started = Instant::now();
|
||||
let reductions = if shard_count <= 1 {
|
||||
vec![reduce_finished_shard(finished)]
|
||||
} else {
|
||||
let chunk_len = finished.len().div_ceil(shard_count);
|
||||
let mut shards: Vec<Vec<FinishedPublicationPoint>> = Vec::new();
|
||||
let mut rest = finished;
|
||||
while !rest.is_empty() {
|
||||
let split_at = chunk_len.min(rest.len());
|
||||
let tail = rest.split_off(split_at);
|
||||
shards.push(std::mem::replace(&mut rest, tail));
|
||||
}
|
||||
std::thread::scope(|scope| {
|
||||
let handles: Vec<_> = shards
|
||||
.into_iter()
|
||||
.map(|shard| scope.spawn(move || reduce_finished_shard(shard)))
|
||||
.collect();
|
||||
handles
|
||||
.into_iter()
|
||||
.map(|handle| handle.join().expect("tree output reduction shard panicked"))
|
||||
.collect()
|
||||
})
|
||||
};
|
||||
let reduce_ms = reduce_started.elapsed().as_millis() as u64;
|
||||
|
||||
let merge_started = Instant::now();
|
||||
let merged = merge_shard_reductions(reductions);
|
||||
let merge_ms = merge_started.elapsed().as_millis() as u64;
|
||||
|
||||
let finalize_started = Instant::now();
|
||||
let output = TreeRunAuditOutput {
|
||||
tree: TreeRunOutput {
|
||||
instances_processed: merged.instances_processed,
|
||||
instances_failed: merged.instances_failed,
|
||||
warnings: merged.warnings,
|
||||
vrps: merged.vrps,
|
||||
aspas: merged.aspas,
|
||||
router_keys: merged.router_keys,
|
||||
},
|
||||
publication_points: merged.publication_points,
|
||||
};
|
||||
let finalize_ms = finalize_started.elapsed().as_millis() as u64;
|
||||
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_build_tree_output",
|
||||
serde_json::json!({
|
||||
"sort_ms": sort_ms,
|
||||
"shard_count": shard_count,
|
||||
"reduce_ms": reduce_ms,
|
||||
"merge_ms": merge_ms,
|
||||
"finalize_ms": finalize_ms,
|
||||
"total_ms": total_started.elapsed().as_millis() as u64,
|
||||
"publication_points": output.publication_points.len(),
|
||||
"instances_processed": output.tree.instances_processed,
|
||||
"instances_failed": output.tree.instances_failed,
|
||||
}),
|
||||
);
|
||||
output
|
||||
}
|
||||
|
||||
pub fn run_tree_parallel_phase2_audit(
|
||||
root: CaInstanceHandle,
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
config: &TreeRunConfig,
|
||||
) -> Result<TreeRunAuditOutput, TreeRunError> {
|
||||
run_tree_parallel_phase2_audit_multi_root(vec![root], runner, config)
|
||||
}
|
||||
445
src/validation/tree_parallel/phase2.rs
Normal file
445
src/validation/tree_parallel/phase2.rs
Normal file
@@ -0,0 +1,445 @@
|
||||
pub fn run_tree_parallel_phase2_audit_multi_root(
|
||||
roots: Vec<CaInstanceHandle>,
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
config: &TreeRunConfig,
|
||||
) -> Result<TreeRunAuditOutput, TreeRunError> {
|
||||
if runner.policy.signed_object_failure_policy == SignedObjectFailurePolicy::DropPublicationPoint
|
||||
{
|
||||
return run_tree_serial_audit_multi_root(roots, runner, config);
|
||||
}
|
||||
|
||||
let Some(repo_runtime) = runner.repo_sync_runtime.as_ref() else {
|
||||
return run_tree_serial_audit_multi_root(roots, runner, config);
|
||||
};
|
||||
if runner.parallel_roa_worker_pool.is_none() {
|
||||
return run_tree_serial_audit_multi_root(roots, runner, config);
|
||||
}
|
||||
|
||||
let mut next_id: u64 = 0;
|
||||
let mut ca_queue: VecDeque<QueuedCaInstance> = VecDeque::new();
|
||||
for root in roots {
|
||||
ca_queue.push_back(QueuedCaInstance {
|
||||
id: next_id,
|
||||
handle: root,
|
||||
parent_id: None,
|
||||
discovered_from: None,
|
||||
});
|
||||
next_id += 1;
|
||||
}
|
||||
|
||||
let mut visited_manifest_uris: HashSet<String> = HashSet::new();
|
||||
let mut ca_waiting_repo_by_identity: HashMap<RepoIdentity, Vec<QueuedCaInstance>> =
|
||||
HashMap::new();
|
||||
let mut ready_queue: VecDeque<ReadyCaInstance> = VecDeque::new();
|
||||
let mut inflight_publication_points: HashMap<u64, InflightPublicationPoint> = HashMap::new();
|
||||
let mut pending_finalization: VecDeque<FinalizeTask> = VecDeque::new();
|
||||
let mut pending_roa_dispatch: VecDeque<OwnedRoaTask> = VecDeque::new();
|
||||
let mut finished: Vec<FinishedPublicationPoint> = Vec::new();
|
||||
let mut instances_started = 0usize;
|
||||
let phase2_config = runner.parallel_phase2_config.as_ref();
|
||||
let ready_batch_size = phase2_config
|
||||
.map(|cfg| cfg.ready_batch_size)
|
||||
.unwrap_or(256)
|
||||
.max(1);
|
||||
let ready_batch_wall_time_budget_ms = phase2_config
|
||||
.map(|cfg| cfg.ready_batch_wall_time_budget_ms)
|
||||
.unwrap_or(100)
|
||||
.max(1);
|
||||
let ready_batch_wall_time_budget = Duration::from_millis(ready_batch_wall_time_budget_ms);
|
||||
let object_result_drain_batch_size = phase2_config
|
||||
.map(|cfg| cfg.object_result_drain_batch_size)
|
||||
.unwrap_or(2048)
|
||||
.max(1);
|
||||
let publication_point_finalize_queue_capacity = phase2_config
|
||||
.map(|cfg| cfg.publication_point_finalize_queue_capacity)
|
||||
.unwrap_or(32768)
|
||||
.max(1);
|
||||
// Experimental ready-stage pool: `stage_workers == 0` keeps the inline
|
||||
// compute+apply staging path byte-for-byte; any positive value moves the
|
||||
// compute phase to a scoped worker pool.
|
||||
let stage_worker_count = phase2_config.map(|cfg| cfg.stage_workers).unwrap_or(0);
|
||||
let stage_queue_capacity = phase2_config
|
||||
.map(|cfg| cfg.worker_queue_capacity)
|
||||
.unwrap_or(256)
|
||||
.max(1);
|
||||
|
||||
let (finalize_task_tx, finalize_task_rx) =
|
||||
mpsc::sync_channel::<FinalizeTask>(publication_point_finalize_queue_capacity);
|
||||
let (finalize_result_tx, finalize_result_rx) = mpsc::channel::<FinalizeWorkerResult>();
|
||||
let mut finalize_inflight = 0usize;
|
||||
|
||||
std::thread::scope(|scope| {
|
||||
let finalize_worker = scope.spawn(move || {
|
||||
run_finalize_worker(
|
||||
runner,
|
||||
finalize_task_rx,
|
||||
finalize_result_tx,
|
||||
config.compact_audit,
|
||||
)
|
||||
});
|
||||
|
||||
// The stage pool borrows the runner like the finalize worker does; it
|
||||
// lives inside this scope and is dropped before the scope joins.
|
||||
let mut stage_pool = if stage_worker_count > 0 {
|
||||
Some(
|
||||
ReadyStagePool::new(
|
||||
scope,
|
||||
stage_worker_count,
|
||||
stage_queue_capacity,
|
||||
ReadyStageTaskExecutor { runner },
|
||||
)
|
||||
.map_err(TreeRunError::Runner)?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Submitted-but-not-yet-drained stage tasks. The drain loop collects
|
||||
// every available result each turn, so `staging_inflight == 0` also
|
||||
// implies the stage result channel is empty.
|
||||
let mut staging_inflight = 0usize;
|
||||
|
||||
let run_result: Result<(), TreeRunError> = (|| {
|
||||
loop {
|
||||
let control_loop_started = Instant::now();
|
||||
// With the stage pool enabled the batch wall clock covers the
|
||||
// whole turn (turn-head drain + dispatch + apply); the inline
|
||||
// path keeps its historical start point at the ready batch.
|
||||
let turn_stage_started = Instant::now();
|
||||
let mut ready_batch_metrics = ReadyStageBatchMetrics::default();
|
||||
let mut stage_drain_metrics = StageDrainMetrics::default();
|
||||
drain_finalize_results_with_progress(
|
||||
&finalize_result_rx,
|
||||
&mut finished,
|
||||
&mut finalize_inflight,
|
||||
pending_finalization.len(),
|
||||
pending_roa_dispatch.len(),
|
||||
inflight_publication_points.len(),
|
||||
)?;
|
||||
flush_pending_roa_dispatch_with_progress(
|
||||
runner,
|
||||
&mut pending_roa_dispatch,
|
||||
&mut inflight_publication_points,
|
||||
&pending_finalization,
|
||||
)?;
|
||||
drain_object_results_with_progress(
|
||||
runner,
|
||||
&mut inflight_publication_points,
|
||||
&mut pending_finalization,
|
||||
pending_roa_dispatch.len(),
|
||||
object_result_drain_batch_size,
|
||||
)?;
|
||||
if let Some(pool) = stage_pool.as_ref() {
|
||||
drain_stage_results(
|
||||
pool,
|
||||
runner,
|
||||
&mut next_id,
|
||||
&mut ca_queue,
|
||||
&mut pending_roa_dispatch,
|
||||
&mut inflight_publication_points,
|
||||
&mut pending_finalization,
|
||||
&mut finished,
|
||||
&mut staging_inflight,
|
||||
&mut ready_batch_metrics,
|
||||
&mut stage_drain_metrics,
|
||||
config,
|
||||
)?;
|
||||
}
|
||||
submit_pending_finalization_with_progress(
|
||||
&finalize_task_tx,
|
||||
&mut pending_finalization,
|
||||
&mut finalize_inflight,
|
||||
publication_point_finalize_queue_capacity,
|
||||
pending_roa_dispatch.len(),
|
||||
inflight_publication_points.len(),
|
||||
)?;
|
||||
|
||||
start_queued_ca_instances(
|
||||
repo_runtime.as_ref(),
|
||||
&mut ca_queue,
|
||||
&mut ready_queue,
|
||||
&mut ca_waiting_repo_by_identity,
|
||||
&mut finished,
|
||||
&mut visited_manifest_uris,
|
||||
&mut instances_started,
|
||||
config,
|
||||
);
|
||||
|
||||
let repo_poll_timeout = event_poll_timeout(
|
||||
&ca_queue,
|
||||
&ready_queue,
|
||||
&pending_roa_dispatch,
|
||||
&inflight_publication_points,
|
||||
&pending_finalization,
|
||||
finalize_inflight,
|
||||
staging_inflight,
|
||||
instances_started,
|
||||
config,
|
||||
);
|
||||
let repo_metrics = drain_repo_events(
|
||||
repo_runtime.as_ref(),
|
||||
&mut ca_waiting_repo_by_identity,
|
||||
&mut ready_queue,
|
||||
repo_poll_timeout,
|
||||
)?;
|
||||
if repo_metrics.event_count > 0 {
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_repo_events_drain",
|
||||
serde_json::json!({
|
||||
"event_count": repo_metrics.event_count,
|
||||
"completions": repo_metrics.completions,
|
||||
"ready_enqueued": repo_metrics.ready_enqueued,
|
||||
"duration_ms": repo_metrics.duration_ms,
|
||||
"ready_queue_len": ready_queue.len(),
|
||||
"ca_waiting_repo_identities": ca_waiting_repo_by_identity.len(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let ready_batch_started = Instant::now();
|
||||
let mut ready_time_budget_exhausted = false;
|
||||
let mut stage_dispatch_metrics = StageDispatchMetrics::default();
|
||||
if let Some(pool) = stage_pool.as_mut() {
|
||||
// Pool path: the ready batch becomes a dispatch loop that
|
||||
// submits compute tasks and applies whatever results are
|
||||
// already available; backpressure requeues for next turn.
|
||||
stage_dispatch_metrics = submit_ready_batch_to_stage_pool(
|
||||
pool,
|
||||
&mut ready_queue,
|
||||
&mut staging_inflight,
|
||||
ready_batch_size,
|
||||
ready_batch_wall_time_budget,
|
||||
)?;
|
||||
ready_time_budget_exhausted = stage_dispatch_metrics.queue_full
|
||||
|| (!ready_queue.is_empty()
|
||||
&& ready_batch_started.elapsed() >= ready_batch_wall_time_budget);
|
||||
drain_stage_results(
|
||||
pool,
|
||||
runner,
|
||||
&mut next_id,
|
||||
&mut ca_queue,
|
||||
&mut pending_roa_dispatch,
|
||||
&mut inflight_publication_points,
|
||||
&mut pending_finalization,
|
||||
&mut finished,
|
||||
&mut staging_inflight,
|
||||
&mut ready_batch_metrics,
|
||||
&mut stage_drain_metrics,
|
||||
config,
|
||||
)?;
|
||||
} else {
|
||||
while ready_batch_metrics.ready_count < ready_batch_size {
|
||||
let Some(ready) = ready_queue.pop_front() else {
|
||||
break;
|
||||
};
|
||||
let ready_queue_len_after_pop = ready_queue.len();
|
||||
let (outcome, metrics) = compute_ready_publication_point_stage(
|
||||
runner,
|
||||
ready,
|
||||
ready_queue_len_after_pop,
|
||||
);
|
||||
let metrics = apply_ready_publication_point_stage(
|
||||
runner,
|
||||
&mut next_id,
|
||||
&mut ca_queue,
|
||||
&mut pending_roa_dispatch,
|
||||
&mut inflight_publication_points,
|
||||
&mut pending_finalization,
|
||||
&mut finished,
|
||||
outcome,
|
||||
metrics,
|
||||
config,
|
||||
config.compact_audit,
|
||||
);
|
||||
ready_batch_metrics.record(metrics);
|
||||
if ready_batch_metrics.ready_count > 0
|
||||
&& ready_batch_started.elapsed() >= ready_batch_wall_time_budget
|
||||
{
|
||||
ready_time_budget_exhausted = !ready_queue.is_empty();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ready_batch_metrics.ready_count > 0 {
|
||||
ready_batch_metrics.total_ms = if stage_pool.is_some() {
|
||||
elapsed_ms(turn_stage_started)
|
||||
} else {
|
||||
elapsed_ms(ready_batch_started)
|
||||
};
|
||||
let ready_count_budget_exhausted = ready_batch_metrics.ready_count
|
||||
>= ready_batch_size
|
||||
&& !ready_queue.is_empty();
|
||||
ready_time_budget_exhausted = ready_time_budget_exhausted
|
||||
|| (!ready_queue.is_empty()
|
||||
&& ready_batch_metrics.total_ms >= ready_batch_wall_time_budget_ms);
|
||||
emit_ready_queue_batch_progress(
|
||||
&ready_batch_metrics,
|
||||
ready_batch_size,
|
||||
ready_batch_wall_time_budget_ms,
|
||||
ready_queue.len(),
|
||||
ready_count_budget_exhausted,
|
||||
ready_time_budget_exhausted,
|
||||
ca_queue.len(),
|
||||
pending_roa_dispatch.len(),
|
||||
inflight_publication_points.len(),
|
||||
pending_finalization.len(),
|
||||
finalize_inflight,
|
||||
);
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_ready_queue_stage_fresh_breakdown",
|
||||
serde_json::json!({
|
||||
"ready_count": ready_batch_metrics.ready_count,
|
||||
"stage_fresh_ms_total": ready_batch_metrics.stage_fresh_ms_total,
|
||||
"stage_fresh_ms_max": ready_batch_metrics.stage_fresh_ms_max,
|
||||
"stage_fresh_ms_max_manifest_rsync_uri": ready_batch_metrics.stage_fresh_ms_max_manifest_rsync_uri,
|
||||
"stage_fresh_ms_max_publication_point_rsync_uri": ready_batch_metrics.stage_fresh_ms_max_publication_point_rsync_uri,
|
||||
"snapshot_prepare_ms_total": ready_batch_metrics.snapshot_prepare_ms_total,
|
||||
"snapshot_prepare_ms_max": ready_batch_metrics.snapshot_prepare_ms_max,
|
||||
"snapshot_current_index_lock_ms_total": ready_batch_metrics.snapshot_current_index_lock_ms_total,
|
||||
"snapshot_current_index_lock_ms_max": ready_batch_metrics.snapshot_current_index_lock_ms_max,
|
||||
"snapshot_manifest_load_ms_total": ready_batch_metrics.snapshot_manifest_load_ms_total,
|
||||
"snapshot_manifest_load_ms_max": ready_batch_metrics.snapshot_manifest_load_ms_max,
|
||||
"snapshot_manifest_index_lookup_ms_total": ready_batch_metrics.snapshot_manifest_index_lookup_ms_total,
|
||||
"snapshot_manifest_index_lookup_ms_max": ready_batch_metrics.snapshot_manifest_index_lookup_ms_max,
|
||||
"snapshot_manifest_blob_load_ms_total": ready_batch_metrics.snapshot_manifest_blob_load_ms_total,
|
||||
"snapshot_manifest_blob_load_ms_max": ready_batch_metrics.snapshot_manifest_blob_load_ms_max,
|
||||
"snapshot_manifest_decode_ms_total": ready_batch_metrics.snapshot_manifest_decode_ms_total,
|
||||
"snapshot_manifest_decode_ms_max": ready_batch_metrics.snapshot_manifest_decode_ms_max,
|
||||
"snapshot_anti_rollback_ms_total": ready_batch_metrics.snapshot_anti_rollback_ms_total,
|
||||
"snapshot_anti_rollback_ms_max": ready_batch_metrics.snapshot_anti_rollback_ms_max,
|
||||
"anti_rollback_meta_hit_count": ready_batch_metrics.anti_rollback_meta_hit_count,
|
||||
"anti_rollback_meta_miss_count": ready_batch_metrics.anti_rollback_meta_miss_count,
|
||||
"snapshot_manifest_entries_ms_total": ready_batch_metrics.snapshot_manifest_entries_ms_total,
|
||||
"snapshot_manifest_entries_ms_max": ready_batch_metrics.snapshot_manifest_entries_ms_max,
|
||||
"snapshot_pack_files_ms_total": ready_batch_metrics.snapshot_pack_files_ms_total,
|
||||
"snapshot_pack_files_ms_max": ready_batch_metrics.snapshot_pack_files_ms_max,
|
||||
"snapshot_pack_files_index_lookup_ms_total": ready_batch_metrics.snapshot_pack_files_index_lookup_ms_total,
|
||||
"snapshot_pack_files_index_lookup_ms_max": ready_batch_metrics.snapshot_pack_files_index_lookup_ms_max,
|
||||
"snapshot_pack_files_blob_load_ms_total": ready_batch_metrics.snapshot_pack_files_blob_load_ms_total,
|
||||
"snapshot_pack_files_blob_load_ms_max": ready_batch_metrics.snapshot_pack_files_blob_load_ms_max,
|
||||
"snapshot_ee_path_validate_ms_total": ready_batch_metrics.snapshot_ee_path_validate_ms_total,
|
||||
"snapshot_ee_path_validate_ms_max": ready_batch_metrics.snapshot_ee_path_validate_ms_max,
|
||||
"snapshot_manifest_file_count_total": ready_batch_metrics.snapshot_manifest_file_count_total,
|
||||
"snapshot_manifest_file_count_max": ready_batch_metrics.snapshot_manifest_file_count_max,
|
||||
"child_discovery_ms_total": ready_batch_metrics.child_discovery_ms_total,
|
||||
"child_discovery_ms_max": ready_batch_metrics.child_discovery_ms_max,
|
||||
"batch_duration_ms": ready_batch_metrics.total_ms,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if stage_pool.is_some()
|
||||
&& (stage_dispatch_metrics.submitted > 0
|
||||
|| stage_drain_metrics.results_drained > 0
|
||||
|| stage_dispatch_metrics.queue_full)
|
||||
{
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_stage_pool_stats",
|
||||
serde_json::json!({
|
||||
"stage_workers": stage_worker_count,
|
||||
"submitted": stage_dispatch_metrics.submitted,
|
||||
"results_drained": stage_drain_metrics.results_drained,
|
||||
"queue_full": stage_dispatch_metrics.queue_full,
|
||||
"staging_inflight": staging_inflight,
|
||||
"ready_queue_len": ready_queue.len(),
|
||||
"queue_wait_ms_total": stage_drain_metrics.queue_wait_ms_total,
|
||||
"queue_wait_ms_max": stage_drain_metrics.queue_wait_ms_max,
|
||||
"worker_ms_total": stage_drain_metrics.worker_ms_total,
|
||||
"worker_ms_max": stage_drain_metrics.worker_ms_max,
|
||||
"dispatch_duration_ms": stage_dispatch_metrics.duration_ms,
|
||||
"drain_duration_ms": stage_drain_metrics.duration_ms,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
flush_pending_roa_dispatch_with_progress(
|
||||
runner,
|
||||
&mut pending_roa_dispatch,
|
||||
&mut inflight_publication_points,
|
||||
&pending_finalization,
|
||||
)?;
|
||||
drain_object_results_with_progress(
|
||||
runner,
|
||||
&mut inflight_publication_points,
|
||||
&mut pending_finalization,
|
||||
pending_roa_dispatch.len(),
|
||||
object_result_drain_batch_size,
|
||||
)?;
|
||||
submit_pending_finalization_with_progress(
|
||||
&finalize_task_tx,
|
||||
&mut pending_finalization,
|
||||
&mut finalize_inflight,
|
||||
publication_point_finalize_queue_capacity,
|
||||
pending_roa_dispatch.len(),
|
||||
inflight_publication_points.len(),
|
||||
)?;
|
||||
drain_finalize_results_with_progress(
|
||||
&finalize_result_rx,
|
||||
&mut finished,
|
||||
&mut finalize_inflight,
|
||||
pending_finalization.len(),
|
||||
pending_roa_dispatch.len(),
|
||||
inflight_publication_points.len(),
|
||||
)?;
|
||||
|
||||
emit_control_loop_slow(
|
||||
elapsed_ms(control_loop_started),
|
||||
repo_poll_timeout,
|
||||
&repo_metrics,
|
||||
&ready_batch_metrics,
|
||||
ca_queue.len(),
|
||||
ready_queue.len(),
|
||||
ca_waiting_repo_by_identity.len(),
|
||||
pending_roa_dispatch.len(),
|
||||
inflight_publication_points.len(),
|
||||
pending_finalization.len(),
|
||||
finalize_inflight,
|
||||
);
|
||||
|
||||
if is_complete(
|
||||
&ca_queue,
|
||||
&ready_queue,
|
||||
&ca_waiting_repo_by_identity,
|
||||
&pending_roa_dispatch,
|
||||
&inflight_publication_points,
|
||||
&pending_finalization,
|
||||
finalize_inflight,
|
||||
staging_inflight,
|
||||
instances_started,
|
||||
config,
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
repo_runtime
|
||||
.reset_run_state()
|
||||
.map_err(TreeRunError::Runner)?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
drop(finalize_task_tx);
|
||||
// Dropping the pool closes the stage task queues so the scoped stage
|
||||
// workers exit before the scope joins them below.
|
||||
drop(stage_pool);
|
||||
let worker_result = finalize_worker
|
||||
.join()
|
||||
.map_err(|_| TreeRunError::Runner("phase2 finalize worker panicked".to_string()))?;
|
||||
run_result?;
|
||||
worker_result?;
|
||||
drain_finalize_results_with_progress(
|
||||
&finalize_result_rx,
|
||||
&mut finished,
|
||||
&mut finalize_inflight,
|
||||
pending_finalization.len(),
|
||||
pending_roa_dispatch.len(),
|
||||
inflight_publication_points.len(),
|
||||
)?;
|
||||
if finalize_inflight != 0 || !pending_finalization.is_empty() {
|
||||
return Err(TreeRunError::Runner(format!(
|
||||
"phase2 finalize worker stopped with pending work: queued={} inflight={}",
|
||||
pending_finalization.len(),
|
||||
finalize_inflight
|
||||
)));
|
||||
}
|
||||
Ok(build_tree_output(finished))
|
||||
})
|
||||
}
|
||||
728
src/validation/tree_parallel/ready_stage.rs
Normal file
728
src/validation/tree_parallel/ready_stage.rs
Normal file
@@ -0,0 +1,728 @@
|
||||
fn emit_ready_queue_batch_progress(
|
||||
metrics: &ReadyStageBatchMetrics,
|
||||
ready_batch_size: usize,
|
||||
ready_batch_wall_time_budget_ms: u64,
|
||||
ready_queue_len_after_batch: usize,
|
||||
ready_count_budget_exhausted: bool,
|
||||
ready_time_budget_exhausted: bool,
|
||||
ca_queue_len_after_batch: usize,
|
||||
pending_roa_dispatch_len_after_batch: usize,
|
||||
inflight_publication_points_after_batch: usize,
|
||||
pending_finalization_len_after_batch: usize,
|
||||
finalize_inflight_after_batch: usize,
|
||||
) {
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_ready_queue_batch",
|
||||
serde_json::json!({
|
||||
"ready_count": metrics.ready_count,
|
||||
"fallback_count": metrics.fallback_count,
|
||||
"complete_count": metrics.complete_count,
|
||||
"staged_count": metrics.staged_count,
|
||||
"zero_task_count": metrics.zero_task_count,
|
||||
"error_count": metrics.error_count,
|
||||
"discovered_children": metrics.discovered_children,
|
||||
"locked_files": metrics.locked_files,
|
||||
"roa_tasks": metrics.roa_tasks,
|
||||
"aspa_objects": metrics.aspa_objects,
|
||||
"stage_fresh_ms_total": metrics.stage_fresh_ms_total,
|
||||
"stage_fresh_ms_max": metrics.stage_fresh_ms_max,
|
||||
"stage_fresh_ms_max_manifest_rsync_uri": metrics.stage_fresh_ms_max_manifest_rsync_uri,
|
||||
"stage_fresh_ms_max_publication_point_rsync_uri": metrics.stage_fresh_ms_max_publication_point_rsync_uri,
|
||||
"prepare_ms_total": metrics.prepare_ms_total,
|
||||
"prepare_ms_max": metrics.prepare_ms_max,
|
||||
"build_roa_tasks_ms_total": metrics.build_roa_tasks_ms_total,
|
||||
"build_roa_tasks_ms_max": metrics.build_roa_tasks_ms_max,
|
||||
"batch_duration_ms": metrics.total_ms,
|
||||
"ready_batch_size": ready_batch_size,
|
||||
"ready_batch_wall_time_budget_ms": ready_batch_wall_time_budget_ms,
|
||||
"ready_queue_len_after_batch": ready_queue_len_after_batch,
|
||||
"ready_queue_budget_exhausted": ready_queue_len_after_batch > 0,
|
||||
"ready_count_budget_exhausted": ready_count_budget_exhausted,
|
||||
"ready_time_budget_exhausted": ready_time_budget_exhausted,
|
||||
"ca_queue_len_after_batch": ca_queue_len_after_batch,
|
||||
"pending_roa_dispatch_len_after_batch": pending_roa_dispatch_len_after_batch,
|
||||
"inflight_publication_points_after_batch": inflight_publication_points_after_batch,
|
||||
"pending_finalization_len_after_batch": pending_finalization_len_after_batch,
|
||||
"finalize_inflight_after_batch": finalize_inflight_after_batch,
|
||||
}),
|
||||
);
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_ready_queue_control_breakdown",
|
||||
serde_json::json!({
|
||||
"ready_count": metrics.ready_count,
|
||||
"ready_queue_wait_ms_total": metrics.ready_queue_wait_ms_total,
|
||||
"ready_queue_wait_ms_max": metrics.ready_queue_wait_ms_max,
|
||||
"child_enqueue_ms_total": metrics.child_enqueue_ms_total,
|
||||
"child_enqueue_ms_max": metrics.child_enqueue_ms_max,
|
||||
"roa_presence_scan_ms_total": metrics.roa_presence_scan_ms_total,
|
||||
"roa_presence_scan_ms_max": metrics.roa_presence_scan_ms_max,
|
||||
"direct_finalize_ms_total": metrics.direct_finalize_ms_total,
|
||||
"direct_finalize_ms_max": metrics.direct_finalize_ms_max,
|
||||
"fallback_full_run_ms_total": metrics.fallback_full_run_ms_total,
|
||||
"fallback_full_run_ms_max": metrics.fallback_full_run_ms_max,
|
||||
"batch_duration_ms": metrics.total_ms,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn can_start_more(instances_started: usize, config: &TreeRunConfig) -> bool {
|
||||
config
|
||||
.max_instances
|
||||
.map(|max| instances_started < max)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn start_queued_ca_instances(
|
||||
repo_runtime: &dyn crate::scheduler::repo_runtime::RepoSyncRuntime,
|
||||
ca_queue: &mut VecDeque<QueuedCaInstance>,
|
||||
ready_queue: &mut VecDeque<ReadyCaInstance>,
|
||||
ca_waiting_repo_by_identity: &mut HashMap<RepoIdentity, Vec<QueuedCaInstance>>,
|
||||
finished: &mut Vec<FinishedPublicationPoint>,
|
||||
visited_manifest_uris: &mut HashSet<String>,
|
||||
instances_started: &mut usize,
|
||||
config: &TreeRunConfig,
|
||||
) {
|
||||
while can_start_more(*instances_started, config) {
|
||||
let Some(node) = ca_queue.pop_front() else {
|
||||
break;
|
||||
};
|
||||
if !visited_manifest_uris.insert(node.handle.manifest_rsync_uri.clone()) {
|
||||
continue;
|
||||
}
|
||||
if !ca_depth_is_allowed(config, node.handle.depth) {
|
||||
continue;
|
||||
}
|
||||
*instances_started += 1;
|
||||
match repo_runtime.request_publication_point_repo(&node.handle, 0) {
|
||||
Ok(RepoSyncRequestStatus::Ready { mut outcome, .. }) => {
|
||||
// Ready here means this CA is reusing repo work that has already completed
|
||||
// (often due to child request scheduling). Do not add the transport duration
|
||||
// again.
|
||||
outcome.repo_sync_duration_ms = 0;
|
||||
ready_queue.push_back(ReadyCaInstance {
|
||||
node,
|
||||
repo_outcome: outcome,
|
||||
ready_enqueued_at: Instant::now(),
|
||||
});
|
||||
}
|
||||
Ok(RepoSyncRequestStatus::Pending { identity, .. }) => {
|
||||
ca_waiting_repo_by_identity
|
||||
.entry(identity)
|
||||
.or_default()
|
||||
.push(node);
|
||||
}
|
||||
Err(err) => {
|
||||
finished.push(FinishedPublicationPoint {
|
||||
node: FinishedPublicationPointNode::from_queued(node),
|
||||
result: FinishedPublicationPointResult::Err(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_ready_publication_point_stage(
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
ready: ReadyCaInstance,
|
||||
ready_queue_len_after_pop: usize,
|
||||
) -> (StageOutcome, ReadyStageMetrics) {
|
||||
let publication_point_started = Instant::now();
|
||||
let ready_queue_wait_ms = publication_point_started
|
||||
.saturating_duration_since(ready.ready_enqueued_at)
|
||||
.as_millis() as u64;
|
||||
let mut metrics = ReadyStageMetrics {
|
||||
ready_count: 1,
|
||||
manifest_rsync_uri: Some(ready.node.handle.manifest_rsync_uri.clone()),
|
||||
publication_point_rsync_uri: Some(ready.node.handle.publication_point_rsync_uri.clone()),
|
||||
ready_queue_wait_ms,
|
||||
ready_queue_len_after_pop,
|
||||
..ReadyStageMetrics::default()
|
||||
};
|
||||
let mut warnings = ready.repo_outcome.warnings.clone();
|
||||
let repo_outcome = ready.repo_outcome.clone();
|
||||
let stage_fresh_started = Instant::now();
|
||||
let stage = runner.stage_fresh_publication_point_after_repo_ready(
|
||||
&ready.node.handle,
|
||||
repo_outcome.repo_sync_ok,
|
||||
repo_outcome.repo_sync_err.as_deref(),
|
||||
);
|
||||
metrics.stage_fresh_ms = elapsed_ms(stage_fresh_started);
|
||||
|
||||
let fresh_stage = match stage {
|
||||
Ok(stage) => stage,
|
||||
Err(err) => {
|
||||
if metrics.stage_fresh_ms >= crate::logging::progress::stage_fresh_slow_threshold_ms() {
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_stage_fresh_slow",
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": ready.node.handle.manifest_rsync_uri.as_str(),
|
||||
"publication_point_rsync_uri": ready.node.handle.publication_point_rsync_uri.as_str(),
|
||||
"status": "error",
|
||||
"error": err.error.to_string(),
|
||||
"stage_fresh_ms": metrics.stage_fresh_ms,
|
||||
"snapshot_prepare_ms": err.snapshot_prepare_ms,
|
||||
"repo_sync_source": repo_outcome.repo_sync_source.as_deref(),
|
||||
"repo_sync_phase": repo_outcome.repo_sync_phase.as_deref(),
|
||||
"repo_sync_duration_ms": repo_outcome.repo_sync_duration_ms,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// The blocking `run_publication_point` fallback stays on the control
|
||||
// thread; it is executed by the apply phase for this outcome.
|
||||
return (
|
||||
StageOutcome::FreshError(Box::new(FreshErrorOutcome {
|
||||
ready,
|
||||
publication_point_started,
|
||||
})),
|
||||
metrics,
|
||||
);
|
||||
}
|
||||
};
|
||||
metrics.snapshot_prepare_ms = fresh_stage.snapshot_prepare_ms;
|
||||
metrics.snapshot_current_index_lock_ms =
|
||||
fresh_stage.snapshot_prepare_timing.current_index_lock_ms;
|
||||
metrics.snapshot_manifest_load_ms = fresh_stage.snapshot_prepare_timing.manifest_load_ms;
|
||||
metrics.snapshot_manifest_index_lookup_ms =
|
||||
fresh_stage.snapshot_prepare_timing.manifest_index_lookup_ms;
|
||||
metrics.snapshot_manifest_blob_load_ms =
|
||||
fresh_stage.snapshot_prepare_timing.manifest_blob_load_ms;
|
||||
metrics.snapshot_manifest_decode_ms = fresh_stage.snapshot_prepare_timing.manifest_decode_ms;
|
||||
metrics.snapshot_anti_rollback_ms = fresh_stage.snapshot_prepare_timing.anti_rollback_guard_ms;
|
||||
metrics.anti_rollback_meta_hit_count =
|
||||
fresh_stage.snapshot_prepare_timing.anti_rollback_meta_hit as usize;
|
||||
metrics.anti_rollback_meta_miss_count =
|
||||
fresh_stage.snapshot_prepare_timing.anti_rollback_meta_miss as usize;
|
||||
metrics.snapshot_manifest_entries_ms = fresh_stage.snapshot_prepare_timing.manifest_entries_ms;
|
||||
metrics.snapshot_pack_files_ms = fresh_stage.snapshot_prepare_timing.pack_files_ms;
|
||||
metrics.snapshot_pack_files_index_lookup_ms = fresh_stage
|
||||
.snapshot_prepare_timing
|
||||
.pack_files_index_lookup_ms;
|
||||
metrics.snapshot_pack_files_blob_load_ms =
|
||||
fresh_stage.snapshot_prepare_timing.pack_files_blob_load_ms;
|
||||
metrics.snapshot_ee_path_validate_ms = fresh_stage.snapshot_prepare_timing.ee_path_validate_ms;
|
||||
metrics.snapshot_manifest_file_count = fresh_stage.snapshot_prepare_timing.manifest_file_count;
|
||||
metrics.child_discovery_ms = fresh_stage.child_discovery_ms;
|
||||
if metrics.stage_fresh_ms >= crate::logging::progress::stage_fresh_slow_threshold_ms() {
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_stage_fresh_slow",
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": ready.node.handle.manifest_rsync_uri.as_str(),
|
||||
"publication_point_rsync_uri": ready.node.handle.publication_point_rsync_uri.as_str(),
|
||||
"status": "ok",
|
||||
"stage_fresh_ms": metrics.stage_fresh_ms,
|
||||
"snapshot_prepare_ms": fresh_stage.snapshot_prepare_ms,
|
||||
"snapshot_current_index_lock_ms": fresh_stage.snapshot_prepare_timing.current_index_lock_ms,
|
||||
"snapshot_manifest_load_ms": fresh_stage.snapshot_prepare_timing.manifest_load_ms,
|
||||
"snapshot_manifest_index_lookup_ms": fresh_stage.snapshot_prepare_timing.manifest_index_lookup_ms,
|
||||
"snapshot_manifest_blob_load_ms": fresh_stage.snapshot_prepare_timing.manifest_blob_load_ms,
|
||||
"snapshot_manifest_decode_ms": fresh_stage.snapshot_prepare_timing.manifest_decode_ms,
|
||||
"snapshot_anti_rollback_ms": fresh_stage.snapshot_prepare_timing.anti_rollback_guard_ms,
|
||||
"anti_rollback_meta_hit": fresh_stage.snapshot_prepare_timing.anti_rollback_meta_hit,
|
||||
"anti_rollback_meta_miss": fresh_stage.snapshot_prepare_timing.anti_rollback_meta_miss,
|
||||
"snapshot_manifest_entries_ms": fresh_stage.snapshot_prepare_timing.manifest_entries_ms,
|
||||
"snapshot_pack_files_ms": fresh_stage.snapshot_prepare_timing.pack_files_ms,
|
||||
"snapshot_pack_files_index_lookup_ms": fresh_stage.snapshot_prepare_timing.pack_files_index_lookup_ms,
|
||||
"snapshot_pack_files_blob_load_ms": fresh_stage.snapshot_prepare_timing.pack_files_blob_load_ms,
|
||||
"snapshot_ee_path_validate_ms": fresh_stage.snapshot_prepare_timing.ee_path_validate_ms,
|
||||
"snapshot_manifest_file_count": fresh_stage.snapshot_prepare_timing.manifest_file_count,
|
||||
"child_discovery_ms": fresh_stage.child_discovery_ms,
|
||||
"child_count": fresh_stage.discovered_children.len(),
|
||||
"repo_sync_source": repo_outcome.repo_sync_source.as_deref(),
|
||||
"repo_sync_phase": repo_outcome.repo_sync_phase.as_deref(),
|
||||
"repo_sync_duration_ms": repo_outcome.repo_sync_duration_ms,
|
||||
}),
|
||||
);
|
||||
}
|
||||
warnings.extend(fresh_stage.warnings.clone());
|
||||
|
||||
metrics.discovered_children = fresh_stage.discovered_children.len();
|
||||
|
||||
let prepare_started = Instant::now();
|
||||
let ta_constraints = runner
|
||||
.policy
|
||||
.ta_constraints
|
||||
.shared_for_tal(&ready.node.handle.tal_id);
|
||||
if ta_constraints.is_some()
|
||||
&& let Some(timing) = runner.timing.as_ref() {
|
||||
timing.record_count("ta_constraints_parallel_publication_points", 1);
|
||||
}
|
||||
match prepare_publication_point_for_parallel_roa_and_ta_constraints(
|
||||
ready.node.id,
|
||||
&fresh_stage.fresh_point,
|
||||
runner.policy,
|
||||
fresh_stage.issuer_ca_der.as_ref(),
|
||||
ready.node.handle.ca_certificate_rsync_uri.as_deref(),
|
||||
ready.node.handle.effective_ip_resources.as_ref(),
|
||||
ready.node.handle.effective_as_resources.as_ref(),
|
||||
runner.validation_time,
|
||||
ta_constraints,
|
||||
) {
|
||||
ParallelObjectsPrepare::Complete(mut objects) => {
|
||||
metrics.prepare_ms = elapsed_ms(prepare_started);
|
||||
runner.record_publication_point_step_ms(
|
||||
&ready.node.handle.manifest_rsync_uri,
|
||||
"fresh_objects_prepare",
|
||||
metrics.prepare_ms,
|
||||
);
|
||||
metrics.complete_count = 1;
|
||||
metrics.roa_tasks = objects.stats.roa_total;
|
||||
metrics.aspa_objects = objects.stats.aspa_total;
|
||||
objects
|
||||
.router_keys
|
||||
.extend(fresh_stage.discovered_router_keys.clone());
|
||||
(
|
||||
StageOutcome::Complete(Box::new(CompleteOutcome {
|
||||
ready,
|
||||
publication_point_started,
|
||||
fresh_stage,
|
||||
warnings,
|
||||
objects,
|
||||
})),
|
||||
metrics,
|
||||
)
|
||||
}
|
||||
ParallelObjectsPrepare::Staged(objects_stage) => {
|
||||
metrics.prepare_ms = elapsed_ms(prepare_started);
|
||||
runner.record_publication_point_step_ms(
|
||||
&ready.node.handle.manifest_rsync_uri,
|
||||
"fresh_objects_prepare",
|
||||
metrics.prepare_ms,
|
||||
);
|
||||
metrics.staged_count = 1;
|
||||
metrics.locked_files = objects_stage.locked_file_count();
|
||||
metrics.aspa_objects = objects_stage.aspa_task_count();
|
||||
let task_count = objects_stage.roa_task_count();
|
||||
metrics.roa_tasks = task_count;
|
||||
let outcome = StagedOutcome {
|
||||
ready,
|
||||
publication_point_started,
|
||||
fresh_stage,
|
||||
warnings,
|
||||
objects_stage,
|
||||
};
|
||||
if task_count == 0 {
|
||||
metrics.zero_task_count = 1;
|
||||
(StageOutcome::ZeroTask(Box::new(outcome)), metrics)
|
||||
} else {
|
||||
(StageOutcome::Fresh(Box::new(outcome)), metrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_ready_publication_point_stage(
|
||||
runner: &Rpkiv1PublicationPointRunner<'_>,
|
||||
next_id: &mut u64,
|
||||
ca_queue: &mut VecDeque<QueuedCaInstance>,
|
||||
pending_roa_dispatch: &mut VecDeque<OwnedRoaTask>,
|
||||
inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
|
||||
pending_finalization: &mut VecDeque<FinalizeTask>,
|
||||
finished: &mut Vec<FinishedPublicationPoint>,
|
||||
outcome: StageOutcome,
|
||||
mut metrics: ReadyStageMetrics,
|
||||
config: &TreeRunConfig,
|
||||
compact_audit: bool,
|
||||
) -> ReadyStageMetrics {
|
||||
match outcome {
|
||||
StageOutcome::FreshError(outcome) => {
|
||||
let FreshErrorOutcome {
|
||||
ready,
|
||||
publication_point_started,
|
||||
} = *outcome;
|
||||
let repo_outcome = ready.repo_outcome.clone();
|
||||
metrics.fallback_count = 1;
|
||||
let fallback_started = Instant::now();
|
||||
let fallback = runner.run_publication_point(&ready.node.handle);
|
||||
metrics.fallback_full_run_ms = elapsed_ms(fallback_started);
|
||||
if let Ok(result) = fallback.as_ref() {
|
||||
metrics.discovered_children = result.discovered_children.len();
|
||||
let child_enqueue_started = Instant::now();
|
||||
enqueue_discovered_children(
|
||||
next_id,
|
||||
ca_queue,
|
||||
&ready.node,
|
||||
config,
|
||||
result.discovered_children.clone(),
|
||||
);
|
||||
metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
|
||||
}
|
||||
finished.push(FinishedPublicationPoint {
|
||||
node: FinishedPublicationPointNode::from_queued(ready.node),
|
||||
result: compact_phase2_finished_result_result(fallback, compact_audit),
|
||||
});
|
||||
metrics.total_ms = elapsed_ms(publication_point_started);
|
||||
emit_ready_publication_point_control_slow(
|
||||
metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
|
||||
metrics
|
||||
.publication_point_rsync_uri
|
||||
.as_deref()
|
||||
.unwrap_or_default(),
|
||||
&repo_outcome,
|
||||
&metrics,
|
||||
"fallback",
|
||||
true,
|
||||
);
|
||||
metrics
|
||||
}
|
||||
StageOutcome::Complete(outcome) => {
|
||||
let CompleteOutcome {
|
||||
ready,
|
||||
publication_point_started,
|
||||
fresh_stage,
|
||||
warnings,
|
||||
objects,
|
||||
} = *outcome;
|
||||
let repo_outcome = ready.repo_outcome.clone();
|
||||
let child_enqueue_started = Instant::now();
|
||||
enqueue_discovered_children(
|
||||
next_id,
|
||||
ca_queue,
|
||||
&ready.node,
|
||||
config,
|
||||
fresh_stage.discovered_children.clone(),
|
||||
);
|
||||
metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
|
||||
// The finalize no longer runs synchronously here: the publication
|
||||
// point is queued for the shared finalize worker through the same
|
||||
// pending_finalization path (queue capacity backpressure and
|
||||
// finalize_inflight accounting included) as zero-task staging. The
|
||||
// per-publication-point total timing is recorded by the finalize
|
||||
// worker, exactly like the zero-task and staged paths.
|
||||
let direct_finalize_started = Instant::now();
|
||||
pending_finalization.push_back(FinalizeTask {
|
||||
state: InflightPublicationPoint {
|
||||
node: ready.node,
|
||||
fresh_stage,
|
||||
objects_prepare: ParallelObjectsPrepare::Complete(objects),
|
||||
repo_outcome: repo_outcome.clone(),
|
||||
warnings,
|
||||
started_at: publication_point_started,
|
||||
objects_started_at: Instant::now(),
|
||||
task_count: 0,
|
||||
tasks_submitted: 0,
|
||||
first_task_submitted_at: None,
|
||||
last_task_submitted_at: None,
|
||||
first_result_at: None,
|
||||
last_result_at: None,
|
||||
worker_ms_total: 0,
|
||||
worker_ms_max: 0,
|
||||
queue_wait_ms_total: 0,
|
||||
queue_wait_ms_max: 0,
|
||||
finalize_enqueued_at: Some(Instant::now()),
|
||||
results: Vec::new(),
|
||||
},
|
||||
});
|
||||
metrics.direct_finalize_ms = elapsed_ms(direct_finalize_started);
|
||||
runner.record_publication_point_step_ms(
|
||||
&metrics.manifest_rsync_uri.clone().unwrap_or_default(),
|
||||
"fresh_direct_finalize",
|
||||
metrics.direct_finalize_ms,
|
||||
);
|
||||
metrics.total_ms = elapsed_ms(publication_point_started);
|
||||
emit_ready_publication_point_control_slow(
|
||||
metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
|
||||
metrics
|
||||
.publication_point_rsync_uri
|
||||
.as_deref()
|
||||
.unwrap_or_default(),
|
||||
&repo_outcome,
|
||||
&metrics,
|
||||
"complete",
|
||||
false,
|
||||
);
|
||||
metrics
|
||||
}
|
||||
StageOutcome::ZeroTask(outcome) => {
|
||||
let StagedOutcome {
|
||||
ready,
|
||||
publication_point_started,
|
||||
fresh_stage,
|
||||
warnings,
|
||||
objects_stage,
|
||||
} = *outcome;
|
||||
let repo_outcome = ready.repo_outcome.clone();
|
||||
let child_enqueue_started = Instant::now();
|
||||
enqueue_discovered_children(
|
||||
next_id,
|
||||
ca_queue,
|
||||
&ready.node,
|
||||
config,
|
||||
fresh_stage.discovered_children.clone(),
|
||||
);
|
||||
metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
|
||||
let build_tasks_started = Instant::now();
|
||||
objects_stage.append_roa_tasks_to(pending_roa_dispatch);
|
||||
metrics.build_roa_tasks_ms = elapsed_ms(build_tasks_started);
|
||||
runner.record_publication_point_step_ms(
|
||||
&ready.node.handle.manifest_rsync_uri,
|
||||
"fresh_build_roa_tasks",
|
||||
metrics.build_roa_tasks_ms,
|
||||
);
|
||||
let task_count = objects_stage.roa_task_count();
|
||||
pending_finalization.push_back(FinalizeTask {
|
||||
state: InflightPublicationPoint {
|
||||
node: ready.node,
|
||||
fresh_stage,
|
||||
objects_prepare: ParallelObjectsPrepare::Staged(objects_stage),
|
||||
repo_outcome: repo_outcome.clone(),
|
||||
warnings,
|
||||
started_at: publication_point_started,
|
||||
objects_started_at: Instant::now(),
|
||||
task_count,
|
||||
tasks_submitted: 0,
|
||||
first_task_submitted_at: None,
|
||||
last_task_submitted_at: None,
|
||||
first_result_at: None,
|
||||
last_result_at: None,
|
||||
worker_ms_total: 0,
|
||||
worker_ms_max: 0,
|
||||
queue_wait_ms_total: 0,
|
||||
queue_wait_ms_max: 0,
|
||||
finalize_enqueued_at: Some(Instant::now()),
|
||||
results: Vec::new(),
|
||||
},
|
||||
});
|
||||
metrics.total_ms = elapsed_ms(publication_point_started);
|
||||
emit_ready_publication_point_control_slow(
|
||||
metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
|
||||
metrics
|
||||
.publication_point_rsync_uri
|
||||
.as_deref()
|
||||
.unwrap_or_default(),
|
||||
&repo_outcome,
|
||||
&metrics,
|
||||
"zero_task",
|
||||
false,
|
||||
);
|
||||
metrics
|
||||
}
|
||||
StageOutcome::Fresh(outcome) => {
|
||||
let StagedOutcome {
|
||||
ready,
|
||||
publication_point_started,
|
||||
fresh_stage,
|
||||
warnings,
|
||||
objects_stage,
|
||||
} = *outcome;
|
||||
let repo_outcome = ready.repo_outcome.clone();
|
||||
let child_enqueue_started = Instant::now();
|
||||
enqueue_discovered_children(
|
||||
next_id,
|
||||
ca_queue,
|
||||
&ready.node,
|
||||
config,
|
||||
fresh_stage.discovered_children.clone(),
|
||||
);
|
||||
metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
|
||||
let build_tasks_started = Instant::now();
|
||||
objects_stage.append_roa_tasks_to(pending_roa_dispatch);
|
||||
metrics.build_roa_tasks_ms = elapsed_ms(build_tasks_started);
|
||||
runner.record_publication_point_step_ms(
|
||||
&ready.node.handle.manifest_rsync_uri,
|
||||
"fresh_build_roa_tasks",
|
||||
metrics.build_roa_tasks_ms,
|
||||
);
|
||||
let task_count = objects_stage.roa_task_count();
|
||||
inflight_publication_points.insert(
|
||||
ready.node.id,
|
||||
InflightPublicationPoint {
|
||||
node: ready.node,
|
||||
fresh_stage,
|
||||
objects_prepare: ParallelObjectsPrepare::Staged(objects_stage),
|
||||
repo_outcome: repo_outcome.clone(),
|
||||
warnings,
|
||||
started_at: publication_point_started,
|
||||
objects_started_at: Instant::now(),
|
||||
task_count,
|
||||
tasks_submitted: 0,
|
||||
first_task_submitted_at: None,
|
||||
last_task_submitted_at: None,
|
||||
first_result_at: None,
|
||||
last_result_at: None,
|
||||
worker_ms_total: 0,
|
||||
worker_ms_max: 0,
|
||||
queue_wait_ms_total: 0,
|
||||
queue_wait_ms_max: 0,
|
||||
finalize_enqueued_at: None,
|
||||
results: Vec::with_capacity(task_count),
|
||||
},
|
||||
);
|
||||
metrics.total_ms = elapsed_ms(publication_point_started);
|
||||
emit_ready_publication_point_control_slow(
|
||||
metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
|
||||
metrics
|
||||
.publication_point_rsync_uri
|
||||
.as_deref()
|
||||
.unwrap_or_default(),
|
||||
&repo_outcome,
|
||||
&metrics,
|
||||
"staged",
|
||||
false,
|
||||
);
|
||||
metrics
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_ready_publication_point_control_slow(
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
repo_outcome: &RepoSyncRuntimeOutcome,
|
||||
metrics: &ReadyStageMetrics,
|
||||
status: &str,
|
||||
force_error_path: bool,
|
||||
) {
|
||||
let threshold_ms = crate::logging::progress::pp_control_slow_threshold_ms();
|
||||
if !force_error_path && metrics.total_ms < threshold_ms {
|
||||
return;
|
||||
}
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_ready_publication_point_control_slow",
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": manifest_rsync_uri,
|
||||
"publication_point_rsync_uri": publication_point_rsync_uri,
|
||||
"status": status,
|
||||
"repo_sync_source": repo_outcome.repo_sync_source.as_deref(),
|
||||
"repo_sync_phase": repo_outcome.repo_sync_phase.as_deref(),
|
||||
"repo_sync_duration_ms": repo_outcome.repo_sync_duration_ms,
|
||||
"repo_sync_ok": repo_outcome.repo_sync_ok,
|
||||
"repo_sync_err": repo_outcome.repo_sync_err.as_deref(),
|
||||
"ready_queue_wait_ms": metrics.ready_queue_wait_ms,
|
||||
"ready_queue_len_after_pop": metrics.ready_queue_len_after_pop,
|
||||
"stage_fresh_ms": metrics.stage_fresh_ms,
|
||||
"child_discovery_ms": metrics.child_discovery_ms,
|
||||
"child_enqueue_ms": metrics.child_enqueue_ms,
|
||||
"discovered_children": metrics.discovered_children,
|
||||
"roa_presence_scan_ms": metrics.roa_presence_scan_ms,
|
||||
"prepare_ms": metrics.prepare_ms,
|
||||
"build_roa_tasks_ms": metrics.build_roa_tasks_ms,
|
||||
"direct_finalize_ms": metrics.direct_finalize_ms,
|
||||
"fallback_full_run_ms": metrics.fallback_full_run_ms,
|
||||
"locked_files": metrics.locked_files,
|
||||
"roa_tasks": metrics.roa_tasks,
|
||||
"aspa_objects": metrics.aspa_objects,
|
||||
"complete_count": metrics.complete_count,
|
||||
"staged_count": metrics.staged_count,
|
||||
"zero_task_count": metrics.zero_task_count,
|
||||
"fallback_count": metrics.fallback_count,
|
||||
"total_ms": metrics.total_ms,
|
||||
"slow_threshold_ms": threshold_ms,
|
||||
}),
|
||||
);
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_ready_publication_point_control_snapshot_breakdown",
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": manifest_rsync_uri,
|
||||
"publication_point_rsync_uri": publication_point_rsync_uri,
|
||||
"status": status,
|
||||
"snapshot_prepare_ms": metrics.snapshot_prepare_ms,
|
||||
"snapshot_current_index_lock_ms": metrics.snapshot_current_index_lock_ms,
|
||||
"snapshot_manifest_load_ms": metrics.snapshot_manifest_load_ms,
|
||||
"snapshot_manifest_index_lookup_ms": metrics.snapshot_manifest_index_lookup_ms,
|
||||
"snapshot_manifest_blob_load_ms": metrics.snapshot_manifest_blob_load_ms,
|
||||
"snapshot_manifest_decode_ms": metrics.snapshot_manifest_decode_ms,
|
||||
"snapshot_anti_rollback_ms": metrics.snapshot_anti_rollback_ms,
|
||||
"anti_rollback_meta_hit_count": metrics.anti_rollback_meta_hit_count,
|
||||
"anti_rollback_meta_miss_count": metrics.anti_rollback_meta_miss_count,
|
||||
"snapshot_manifest_entries_ms": metrics.snapshot_manifest_entries_ms,
|
||||
"snapshot_pack_files_ms": metrics.snapshot_pack_files_ms,
|
||||
"snapshot_pack_files_index_lookup_ms": metrics.snapshot_pack_files_index_lookup_ms,
|
||||
"snapshot_pack_files_blob_load_ms": metrics.snapshot_pack_files_blob_load_ms,
|
||||
"snapshot_ee_path_validate_ms": metrics.snapshot_ee_path_validate_ms,
|
||||
"snapshot_manifest_file_count": metrics.snapshot_manifest_file_count,
|
||||
"total_ms": metrics.total_ms,
|
||||
"slow_threshold_ms": threshold_ms,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn enqueue_discovered_children(
|
||||
next_id: &mut u64,
|
||||
ca_queue: &mut VecDeque<QueuedCaInstance>,
|
||||
parent: &QueuedCaInstance,
|
||||
config: &TreeRunConfig,
|
||||
mut children: Vec<DiscoveredChildCaInstance>,
|
||||
) {
|
||||
let Some(child_depth) = next_allowed_ca_depth(config, parent.handle.depth) else {
|
||||
return;
|
||||
};
|
||||
|
||||
children.sort_by(|a, b| {
|
||||
a.handle
|
||||
.manifest_rsync_uri
|
||||
.cmp(&b.handle.manifest_rsync_uri)
|
||||
.then_with(|| {
|
||||
a.discovered_from
|
||||
.child_ca_certificate_rsync_uri
|
||||
.cmp(&b.discovered_from.child_ca_certificate_rsync_uri)
|
||||
})
|
||||
});
|
||||
for child in children {
|
||||
let mut handle = child.handle.with_depth(child_depth);
|
||||
handle.parent_manifest_rsync_uri = Some(parent.handle.manifest_rsync_uri.clone());
|
||||
ca_queue.push_back(QueuedCaInstance {
|
||||
id: *next_id,
|
||||
handle,
|
||||
parent_id: Some(parent.id),
|
||||
discovered_from: Some(child.discovered_from),
|
||||
});
|
||||
*next_id += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_metrics_from_output(
|
||||
output: &FreshPublicationPointFinalizeOutput,
|
||||
reduce_ms: u64,
|
||||
finalize_ms: u64,
|
||||
finalize_queue_wait_ms: Option<u64>,
|
||||
finalize_worker_ms: u64,
|
||||
locked_files: usize,
|
||||
) -> FinalizePublicationPointMetrics {
|
||||
FinalizePublicationPointMetrics {
|
||||
reduce_ms,
|
||||
finalize_ms,
|
||||
finalize_queue_wait_ms,
|
||||
finalize_worker_ms,
|
||||
snapshot_pack_ms: output.snapshot_pack_ms,
|
||||
ccr_projection_build_ms: output.ccr_projection_build_ms,
|
||||
ccr_append_ms: output.ccr_append_ms,
|
||||
audit_build_ms: output.audit_build_ms,
|
||||
locked_files,
|
||||
child_count: output.result.discovered_children.len(),
|
||||
warning_count: output.result.warnings.len(),
|
||||
vrp_count: output.result.objects.vrps.len(),
|
||||
vap_count: output.result.objects.aspas.len(),
|
||||
router_key_count: output.result.objects.router_keys.len(),
|
||||
audit_object_count: output.result.audit.objects.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_finalize_breakdown(
|
||||
event_name: &str,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
metrics: &FinalizePublicationPointMetrics,
|
||||
) {
|
||||
crate::logging::progress::emit!(
|
||||
event_name,
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": manifest_rsync_uri,
|
||||
"publication_point_rsync_uri": publication_point_rsync_uri,
|
||||
"reduce_ms": metrics.reduce_ms,
|
||||
"finalize_ms": metrics.finalize_ms,
|
||||
"finalize_queue_wait_ms": metrics.finalize_queue_wait_ms,
|
||||
"finalize_worker_ms": metrics.finalize_worker_ms,
|
||||
"snapshot_pack_ms": metrics.snapshot_pack_ms,
|
||||
"ccr_projection_build_ms": metrics.ccr_projection_build_ms,
|
||||
"ccr_append_ms": metrics.ccr_append_ms,
|
||||
"audit_build_ms": metrics.audit_build_ms,
|
||||
"locked_files": metrics.locked_files,
|
||||
"child_count": metrics.child_count,
|
||||
"warning_count": metrics.warning_count,
|
||||
"vrp_count": metrics.vrp_count,
|
||||
"vap_count": metrics.vap_count,
|
||||
"router_key_count": metrics.router_key_count,
|
||||
"audit_object_count": metrics.audit_object_count,
|
||||
}),
|
||||
);
|
||||
}
|
||||
564
src/validation/tree_parallel/state.rs
Normal file
564
src/validation/tree_parallel/state.rs
Normal file
@@ -0,0 +1,564 @@
|
||||
#[derive(Clone, Debug)]
|
||||
struct QueuedCaInstance {
|
||||
id: u64,
|
||||
handle: CaInstanceHandle,
|
||||
parent_id: Option<u64>,
|
||||
discovered_from: Option<DiscoveredFrom>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ReadyCaInstance {
|
||||
node: QueuedCaInstance,
|
||||
repo_outcome: RepoSyncRuntimeOutcome,
|
||||
ready_enqueued_at: Instant,
|
||||
}
|
||||
|
||||
struct InflightPublicationPoint {
|
||||
node: QueuedCaInstance,
|
||||
fresh_stage: FreshPublicationPointStage,
|
||||
objects_prepare: ParallelObjectsPrepare,
|
||||
repo_outcome: RepoSyncRuntimeOutcome,
|
||||
warnings: Vec<Warning>,
|
||||
started_at: Instant,
|
||||
objects_started_at: Instant,
|
||||
task_count: usize,
|
||||
tasks_submitted: usize,
|
||||
first_task_submitted_at: Option<Instant>,
|
||||
last_task_submitted_at: Option<Instant>,
|
||||
first_result_at: Option<Instant>,
|
||||
last_result_at: Option<Instant>,
|
||||
worker_ms_total: u64,
|
||||
worker_ms_max: u64,
|
||||
queue_wait_ms_total: u64,
|
||||
queue_wait_ms_max: u64,
|
||||
finalize_enqueued_at: Option<Instant>,
|
||||
results: Vec<crate::validation::objects::RoaTaskResult>,
|
||||
}
|
||||
|
||||
struct FinishedPublicationPoint {
|
||||
node: FinishedPublicationPointNode,
|
||||
result: FinishedPublicationPointResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FinishedPublicationPointNode {
|
||||
id: u64,
|
||||
parent_id: Option<u64>,
|
||||
discovered_from: Option<DiscoveredFrom>,
|
||||
manifest_rsync_uri: String,
|
||||
}
|
||||
|
||||
impl FinishedPublicationPointNode {
|
||||
fn from_queued(node: QueuedCaInstance) -> Self {
|
||||
Self {
|
||||
id: node.id,
|
||||
parent_id: node.parent_id,
|
||||
discovered_from: node.discovered_from,
|
||||
manifest_rsync_uri: node.handle.manifest_rsync_uri,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FinishedPublicationPointResult {
|
||||
Ok {
|
||||
warnings: Vec<Warning>,
|
||||
objects: ObjectsOutput,
|
||||
audit: PublicationPointAudit,
|
||||
},
|
||||
Err(String),
|
||||
}
|
||||
|
||||
struct FinalizeTask {
|
||||
state: InflightPublicationPoint,
|
||||
}
|
||||
|
||||
/// Outcome of the pure compute phase for one ready publication point.
|
||||
///
|
||||
/// `compute_ready_publication_point_stage` only performs read-only validation
|
||||
/// work (repository-view lookup, fresh snapshot staging, ROA prepare)
|
||||
/// and returns this enum. `apply_ready_publication_point_stage` then performs
|
||||
/// every write to control-loop state (`ca_queue`/`next_id`, `finished`,
|
||||
/// `pending_roa_dispatch`, `pending_finalization`, `inflight_publication_points`)
|
||||
/// in the same per-publication-point order the monolithic staging function did.
|
||||
/// Each variant payload is boxed so the enum itself stays small.
|
||||
enum StageOutcome {
|
||||
/// A repository-view request was already completed during this run and only
|
||||
/// needs child enqueueing plus a `finished` entry.
|
||||
/// Fresh staging failed: apply runs the existing blocking
|
||||
/// `run_publication_point` fallback inline on the control thread.
|
||||
FreshError(Box<FreshErrorOutcome>),
|
||||
/// Fresh staging succeeded and ROA prepare returned complete objects (no
|
||||
/// ROA tasks to dispatch): apply hands the publication point to the
|
||||
/// finalize worker through `pending_finalization` instead of running the
|
||||
/// finalize synchronously on the control thread.
|
||||
Complete(Box<CompleteOutcome>),
|
||||
/// Fresh staging succeeded with a staged objects plan that contains zero
|
||||
/// ROA tasks: apply queues the finalize task directly.
|
||||
ZeroTask(Box<StagedOutcome>),
|
||||
/// Fresh staging succeeded with ROA tasks to dispatch: apply appends the
|
||||
/// tasks to `pending_roa_dispatch` and registers the inflight publication
|
||||
/// point.
|
||||
Fresh(Box<StagedOutcome>),
|
||||
}
|
||||
|
||||
|
||||
struct FreshErrorOutcome {
|
||||
ready: ReadyCaInstance,
|
||||
publication_point_started: Instant,
|
||||
}
|
||||
|
||||
struct CompleteOutcome {
|
||||
ready: ReadyCaInstance,
|
||||
publication_point_started: Instant,
|
||||
fresh_stage: FreshPublicationPointStage,
|
||||
warnings: Vec<Warning>,
|
||||
objects: ObjectsOutput,
|
||||
}
|
||||
|
||||
struct StagedOutcome {
|
||||
ready: ReadyCaInstance,
|
||||
publication_point_started: Instant,
|
||||
fresh_stage: FreshPublicationPointStage,
|
||||
warnings: Vec<Warning>,
|
||||
objects_stage: ParallelObjectsStage,
|
||||
}
|
||||
|
||||
struct FinalizeWorkerResult {
|
||||
finished: FinishedPublicationPoint,
|
||||
metrics: FinalizePublicationPointMetrics,
|
||||
}
|
||||
|
||||
/// Task submitted to the experimental ready-stage worker pool: everything
|
||||
/// `compute_ready_publication_point_stage` needs for one ready publication
|
||||
/// point.
|
||||
struct ReadyStageTask {
|
||||
ready: ReadyCaInstance,
|
||||
ready_queue_len_after_pop: usize,
|
||||
submitted_at: Instant,
|
||||
}
|
||||
|
||||
/// Result drained from the ready-stage worker pool: the compute outcome and
|
||||
/// its metrics plus per-task pool timing for the `phase2_stage_pool_stats`
|
||||
/// observability event.
|
||||
struct ReadyStageWorkerResult {
|
||||
outcome: StageOutcome,
|
||||
metrics: ReadyStageMetrics,
|
||||
queue_wait_ms: u64,
|
||||
worker_ms: u64,
|
||||
}
|
||||
|
||||
/// Executor borrowing the publication point runner so stage workers can run
|
||||
/// the read-only compute phase off the control thread. The runner is shared
|
||||
/// with the finalize worker and the ROA pool in the same way; the scoped pool
|
||||
/// guarantees all borrows end before the enclosing `std::thread::scope`.
|
||||
struct ReadyStageTaskExecutor<'a> {
|
||||
runner: &'a Rpkiv1PublicationPointRunner<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ObjectTaskExecutor<ReadyStageTask, ReadyStageWorkerResult> for ReadyStageTaskExecutor<'a> {
|
||||
fn execute(&self, _worker_index: usize, task: ReadyStageTask) -> ReadyStageWorkerResult {
|
||||
let worker_started = Instant::now();
|
||||
let queue_wait_ms = worker_started
|
||||
.saturating_duration_since(task.submitted_at)
|
||||
.as_millis() as u64;
|
||||
let (outcome, metrics) = compute_ready_publication_point_stage(
|
||||
self.runner,
|
||||
task.ready,
|
||||
task.ready_queue_len_after_pop,
|
||||
);
|
||||
ReadyStageWorkerResult {
|
||||
outcome,
|
||||
metrics,
|
||||
queue_wait_ms,
|
||||
worker_ms: elapsed_ms(worker_started),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ReadyStagePool<'scope, 'env> = ScopedObjectWorkerPool<
|
||||
'scope,
|
||||
'env,
|
||||
ReadyStageTask,
|
||||
ReadyStageWorkerResult,
|
||||
ReadyStageTaskExecutor<'env>,
|
||||
>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct StageDispatchMetrics {
|
||||
submitted: usize,
|
||||
queue_full: bool,
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StageDrainMetrics {
|
||||
results_drained: usize,
|
||||
queue_wait_ms_total: u64,
|
||||
queue_wait_ms_max: u64,
|
||||
worker_ms_total: u64,
|
||||
worker_ms_max: u64,
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ReadyStageMetrics {
|
||||
manifest_rsync_uri: Option<String>,
|
||||
publication_point_rsync_uri: Option<String>,
|
||||
ready_count: usize,
|
||||
fallback_count: usize,
|
||||
complete_count: usize,
|
||||
staged_count: usize,
|
||||
zero_task_count: usize,
|
||||
error_count: usize,
|
||||
discovered_children: usize,
|
||||
locked_files: usize,
|
||||
roa_tasks: usize,
|
||||
aspa_objects: usize,
|
||||
stage_fresh_ms: u64,
|
||||
snapshot_prepare_ms: u64,
|
||||
snapshot_current_index_lock_ms: u64,
|
||||
snapshot_manifest_load_ms: u64,
|
||||
snapshot_manifest_index_lookup_ms: u64,
|
||||
snapshot_manifest_blob_load_ms: u64,
|
||||
snapshot_manifest_decode_ms: u64,
|
||||
snapshot_anti_rollback_ms: u64,
|
||||
anti_rollback_meta_hit_count: usize,
|
||||
anti_rollback_meta_miss_count: usize,
|
||||
snapshot_manifest_entries_ms: u64,
|
||||
snapshot_pack_files_ms: u64,
|
||||
snapshot_pack_files_index_lookup_ms: u64,
|
||||
snapshot_pack_files_blob_load_ms: u64,
|
||||
snapshot_ee_path_validate_ms: u64,
|
||||
snapshot_manifest_file_count: usize,
|
||||
child_discovery_ms: u64,
|
||||
child_enqueue_ms: u64,
|
||||
ready_queue_wait_ms: u64,
|
||||
ready_queue_len_after_pop: usize,
|
||||
roa_presence_scan_ms: u64,
|
||||
direct_finalize_ms: u64,
|
||||
fallback_full_run_ms: u64,
|
||||
prepare_ms: u64,
|
||||
build_roa_tasks_ms: u64,
|
||||
total_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ReadyStageBatchMetrics {
|
||||
ready_count: usize,
|
||||
fallback_count: usize,
|
||||
complete_count: usize,
|
||||
staged_count: usize,
|
||||
zero_task_count: usize,
|
||||
error_count: usize,
|
||||
discovered_children: usize,
|
||||
locked_files: usize,
|
||||
roa_tasks: usize,
|
||||
aspa_objects: usize,
|
||||
stage_fresh_ms_total: u64,
|
||||
stage_fresh_ms_max: u64,
|
||||
stage_fresh_ms_max_manifest_rsync_uri: Option<String>,
|
||||
stage_fresh_ms_max_publication_point_rsync_uri: Option<String>,
|
||||
snapshot_prepare_ms_total: u64,
|
||||
snapshot_prepare_ms_max: u64,
|
||||
snapshot_current_index_lock_ms_total: u64,
|
||||
snapshot_current_index_lock_ms_max: u64,
|
||||
snapshot_manifest_load_ms_total: u64,
|
||||
snapshot_manifest_load_ms_max: u64,
|
||||
snapshot_manifest_index_lookup_ms_total: u64,
|
||||
snapshot_manifest_index_lookup_ms_max: u64,
|
||||
snapshot_manifest_blob_load_ms_total: u64,
|
||||
snapshot_manifest_blob_load_ms_max: u64,
|
||||
snapshot_manifest_decode_ms_total: u64,
|
||||
snapshot_manifest_decode_ms_max: u64,
|
||||
snapshot_anti_rollback_ms_total: u64,
|
||||
snapshot_anti_rollback_ms_max: u64,
|
||||
anti_rollback_meta_hit_count: usize,
|
||||
anti_rollback_meta_miss_count: usize,
|
||||
snapshot_manifest_entries_ms_total: u64,
|
||||
snapshot_manifest_entries_ms_max: u64,
|
||||
snapshot_pack_files_ms_total: u64,
|
||||
snapshot_pack_files_ms_max: u64,
|
||||
snapshot_pack_files_index_lookup_ms_total: u64,
|
||||
snapshot_pack_files_index_lookup_ms_max: u64,
|
||||
snapshot_pack_files_blob_load_ms_total: u64,
|
||||
snapshot_pack_files_blob_load_ms_max: u64,
|
||||
snapshot_ee_path_validate_ms_total: u64,
|
||||
snapshot_ee_path_validate_ms_max: u64,
|
||||
snapshot_manifest_file_count_total: usize,
|
||||
snapshot_manifest_file_count_max: usize,
|
||||
child_discovery_ms_total: u64,
|
||||
child_discovery_ms_max: u64,
|
||||
child_enqueue_ms_total: u64,
|
||||
child_enqueue_ms_max: u64,
|
||||
ready_queue_wait_ms_total: u64,
|
||||
ready_queue_wait_ms_max: u64,
|
||||
roa_presence_scan_ms_total: u64,
|
||||
roa_presence_scan_ms_max: u64,
|
||||
direct_finalize_ms_total: u64,
|
||||
direct_finalize_ms_max: u64,
|
||||
fallback_full_run_ms_total: u64,
|
||||
fallback_full_run_ms_max: u64,
|
||||
prepare_ms_total: u64,
|
||||
prepare_ms_max: u64,
|
||||
build_roa_tasks_ms_total: u64,
|
||||
build_roa_tasks_ms_max: u64,
|
||||
total_ms: u64,
|
||||
}
|
||||
|
||||
impl ReadyStageBatchMetrics {
|
||||
fn record(&mut self, metrics: ReadyStageMetrics) {
|
||||
self.ready_count += metrics.ready_count;
|
||||
self.fallback_count += metrics.fallback_count;
|
||||
self.complete_count += metrics.complete_count;
|
||||
self.staged_count += metrics.staged_count;
|
||||
self.zero_task_count += metrics.zero_task_count;
|
||||
self.error_count += metrics.error_count;
|
||||
self.discovered_children += metrics.discovered_children;
|
||||
self.locked_files += metrics.locked_files;
|
||||
self.roa_tasks += metrics.roa_tasks;
|
||||
self.aspa_objects += metrics.aspa_objects;
|
||||
if metrics.stage_fresh_ms >= self.stage_fresh_ms_max {
|
||||
self.stage_fresh_ms_max_manifest_rsync_uri = metrics.manifest_rsync_uri.clone();
|
||||
self.stage_fresh_ms_max_publication_point_rsync_uri =
|
||||
metrics.publication_point_rsync_uri.clone();
|
||||
}
|
||||
self.stage_fresh_ms_total += metrics.stage_fresh_ms;
|
||||
self.stage_fresh_ms_max = self.stage_fresh_ms_max.max(metrics.stage_fresh_ms);
|
||||
self.snapshot_prepare_ms_total += metrics.snapshot_prepare_ms;
|
||||
self.snapshot_prepare_ms_max = self
|
||||
.snapshot_prepare_ms_max
|
||||
.max(metrics.snapshot_prepare_ms);
|
||||
self.snapshot_current_index_lock_ms_total += metrics.snapshot_current_index_lock_ms;
|
||||
self.snapshot_current_index_lock_ms_max = self
|
||||
.snapshot_current_index_lock_ms_max
|
||||
.max(metrics.snapshot_current_index_lock_ms);
|
||||
self.snapshot_manifest_load_ms_total += metrics.snapshot_manifest_load_ms;
|
||||
self.snapshot_manifest_load_ms_max = self
|
||||
.snapshot_manifest_load_ms_max
|
||||
.max(metrics.snapshot_manifest_load_ms);
|
||||
self.snapshot_manifest_index_lookup_ms_total += metrics.snapshot_manifest_index_lookup_ms;
|
||||
self.snapshot_manifest_index_lookup_ms_max = self
|
||||
.snapshot_manifest_index_lookup_ms_max
|
||||
.max(metrics.snapshot_manifest_index_lookup_ms);
|
||||
self.snapshot_manifest_blob_load_ms_total += metrics.snapshot_manifest_blob_load_ms;
|
||||
self.snapshot_manifest_blob_load_ms_max = self
|
||||
.snapshot_manifest_blob_load_ms_max
|
||||
.max(metrics.snapshot_manifest_blob_load_ms);
|
||||
self.snapshot_manifest_decode_ms_total += metrics.snapshot_manifest_decode_ms;
|
||||
self.snapshot_manifest_decode_ms_max = self
|
||||
.snapshot_manifest_decode_ms_max
|
||||
.max(metrics.snapshot_manifest_decode_ms);
|
||||
self.snapshot_anti_rollback_ms_total += metrics.snapshot_anti_rollback_ms;
|
||||
self.snapshot_anti_rollback_ms_max = self
|
||||
.snapshot_anti_rollback_ms_max
|
||||
.max(metrics.snapshot_anti_rollback_ms);
|
||||
self.anti_rollback_meta_hit_count += metrics.anti_rollback_meta_hit_count;
|
||||
self.anti_rollback_meta_miss_count += metrics.anti_rollback_meta_miss_count;
|
||||
self.snapshot_manifest_entries_ms_total += metrics.snapshot_manifest_entries_ms;
|
||||
self.snapshot_manifest_entries_ms_max = self
|
||||
.snapshot_manifest_entries_ms_max
|
||||
.max(metrics.snapshot_manifest_entries_ms);
|
||||
self.snapshot_pack_files_ms_total += metrics.snapshot_pack_files_ms;
|
||||
self.snapshot_pack_files_ms_max = self
|
||||
.snapshot_pack_files_ms_max
|
||||
.max(metrics.snapshot_pack_files_ms);
|
||||
self.snapshot_pack_files_index_lookup_ms_total +=
|
||||
metrics.snapshot_pack_files_index_lookup_ms;
|
||||
self.snapshot_pack_files_index_lookup_ms_max = self
|
||||
.snapshot_pack_files_index_lookup_ms_max
|
||||
.max(metrics.snapshot_pack_files_index_lookup_ms);
|
||||
self.snapshot_pack_files_blob_load_ms_total += metrics.snapshot_pack_files_blob_load_ms;
|
||||
self.snapshot_pack_files_blob_load_ms_max = self
|
||||
.snapshot_pack_files_blob_load_ms_max
|
||||
.max(metrics.snapshot_pack_files_blob_load_ms);
|
||||
self.snapshot_ee_path_validate_ms_total += metrics.snapshot_ee_path_validate_ms;
|
||||
self.snapshot_ee_path_validate_ms_max = self
|
||||
.snapshot_ee_path_validate_ms_max
|
||||
.max(metrics.snapshot_ee_path_validate_ms);
|
||||
self.snapshot_manifest_file_count_total += metrics.snapshot_manifest_file_count;
|
||||
self.snapshot_manifest_file_count_max = self
|
||||
.snapshot_manifest_file_count_max
|
||||
.max(metrics.snapshot_manifest_file_count);
|
||||
self.child_discovery_ms_total += metrics.child_discovery_ms;
|
||||
self.child_discovery_ms_max = self.child_discovery_ms_max.max(metrics.child_discovery_ms);
|
||||
self.child_enqueue_ms_total += metrics.child_enqueue_ms;
|
||||
self.child_enqueue_ms_max = self.child_enqueue_ms_max.max(metrics.child_enqueue_ms);
|
||||
self.ready_queue_wait_ms_total += metrics.ready_queue_wait_ms;
|
||||
self.ready_queue_wait_ms_max = self
|
||||
.ready_queue_wait_ms_max
|
||||
.max(metrics.ready_queue_wait_ms);
|
||||
self.roa_presence_scan_ms_total += metrics.roa_presence_scan_ms;
|
||||
self.roa_presence_scan_ms_max = self
|
||||
.roa_presence_scan_ms_max
|
||||
.max(metrics.roa_presence_scan_ms);
|
||||
self.direct_finalize_ms_total += metrics.direct_finalize_ms;
|
||||
self.direct_finalize_ms_max = self.direct_finalize_ms_max.max(metrics.direct_finalize_ms);
|
||||
self.fallback_full_run_ms_total += metrics.fallback_full_run_ms;
|
||||
self.fallback_full_run_ms_max = self
|
||||
.fallback_full_run_ms_max
|
||||
.max(metrics.fallback_full_run_ms);
|
||||
self.prepare_ms_total += metrics.prepare_ms;
|
||||
self.prepare_ms_max = self.prepare_ms_max.max(metrics.prepare_ms);
|
||||
self.build_roa_tasks_ms_total += metrics.build_roa_tasks_ms;
|
||||
self.build_roa_tasks_ms_max = self.build_roa_tasks_ms_max.max(metrics.build_roa_tasks_ms);
|
||||
self.total_ms += metrics.total_ms;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RoaDispatchMetrics {
|
||||
attempted: usize,
|
||||
submitted: usize,
|
||||
queue_full: bool,
|
||||
pending_remaining: usize,
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ObjectDrainMetrics {
|
||||
results_drained: usize,
|
||||
publication_points_completed: usize,
|
||||
worker_ms_total: u64,
|
||||
worker_ms_max: u64,
|
||||
queue_wait_ms_total: u64,
|
||||
queue_wait_ms_max: u64,
|
||||
result_budget_exhausted: bool,
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FinalizeSubmitMetrics {
|
||||
submitted: usize,
|
||||
queue_full: bool,
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FinalizePublicationPointMetrics {
|
||||
reduce_ms: u64,
|
||||
finalize_ms: u64,
|
||||
finalize_queue_wait_ms: Option<u64>,
|
||||
finalize_worker_ms: u64,
|
||||
snapshot_pack_ms: u64,
|
||||
ccr_projection_build_ms: u64,
|
||||
ccr_append_ms: u64,
|
||||
audit_build_ms: u64,
|
||||
locked_files: usize,
|
||||
child_count: usize,
|
||||
warning_count: usize,
|
||||
vrp_count: usize,
|
||||
vap_count: usize,
|
||||
router_key_count: usize,
|
||||
audit_object_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FinalizeResultsDrainMetrics {
|
||||
results_drained: usize,
|
||||
reduce_ms_total: u64,
|
||||
reduce_ms_max: u64,
|
||||
finalize_ms_total: u64,
|
||||
finalize_ms_max: u64,
|
||||
finalize_queue_wait_ms_max: u64,
|
||||
finalize_worker_ms_total: u64,
|
||||
finalize_worker_ms_max: u64,
|
||||
snapshot_pack_ms_total: u64,
|
||||
snapshot_pack_ms_max: u64,
|
||||
ccr_projection_build_ms_total: u64,
|
||||
ccr_projection_build_ms_max: u64,
|
||||
ccr_append_ms_total: u64,
|
||||
ccr_append_ms_max: u64,
|
||||
audit_build_ms_total: u64,
|
||||
audit_build_ms_max: u64,
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RepoDrainMetrics {
|
||||
event_count: usize,
|
||||
completions: usize,
|
||||
ready_enqueued: usize,
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
const REPO_RESULT_DRAIN_MAX_EVENTS: usize = 64;
|
||||
|
||||
fn elapsed_ms(started: Instant) -> u64 {
|
||||
started.elapsed().as_millis() as u64
|
||||
}
|
||||
|
||||
fn emit_control_loop_slow(
|
||||
duration_ms: u64,
|
||||
repo_poll_timeout: Duration,
|
||||
repo_metrics: &RepoDrainMetrics,
|
||||
ready_batch_metrics: &ReadyStageBatchMetrics,
|
||||
ca_queue_len: usize,
|
||||
ready_queue_len: usize,
|
||||
ca_waiting_repo_identities: usize,
|
||||
pending_roa_dispatch_len: usize,
|
||||
inflight_publication_points_len: usize,
|
||||
pending_finalization_len: usize,
|
||||
finalize_inflight: usize,
|
||||
) {
|
||||
let threshold_ms = crate::logging::progress::control_loop_slow_threshold_ms();
|
||||
if duration_ms < threshold_ms {
|
||||
return;
|
||||
}
|
||||
crate::logging::progress::emit!(
|
||||
"phase2_control_loop_slow",
|
||||
serde_json::json!({
|
||||
"duration_ms": duration_ms,
|
||||
"slow_threshold_ms": threshold_ms,
|
||||
"repo_poll_timeout_ms": repo_poll_timeout.as_millis() as u64,
|
||||
"repo_event_count": repo_metrics.event_count,
|
||||
"repo_completions": repo_metrics.completions,
|
||||
"repo_ready_enqueued": repo_metrics.ready_enqueued,
|
||||
"repo_drain_duration_ms": repo_metrics.duration_ms,
|
||||
"ready_count": ready_batch_metrics.ready_count,
|
||||
"ready_batch_duration_ms": ready_batch_metrics.total_ms,
|
||||
"ready_batch_stage_fresh_ms_total": ready_batch_metrics.stage_fresh_ms_total,
|
||||
"ready_batch_stage_fresh_ms_max": ready_batch_metrics.stage_fresh_ms_max,
|
||||
"ready_batch_stage_fresh_ms_max_manifest_rsync_uri": ready_batch_metrics.stage_fresh_ms_max_manifest_rsync_uri,
|
||||
"ready_batch_child_discovery_ms_total": ready_batch_metrics.child_discovery_ms_total,
|
||||
"ready_batch_child_discovery_ms_max": ready_batch_metrics.child_discovery_ms_max,
|
||||
"ready_batch_prepare_ms_total": ready_batch_metrics.prepare_ms_total,
|
||||
"ready_batch_prepare_ms_max": ready_batch_metrics.prepare_ms_max,
|
||||
"ready_batch_direct_finalize_ms_total": ready_batch_metrics.direct_finalize_ms_total,
|
||||
"ready_batch_direct_finalize_ms_max": ready_batch_metrics.direct_finalize_ms_max,
|
||||
"ca_queue_len": ca_queue_len,
|
||||
"ready_queue_len": ready_queue_len,
|
||||
"ca_waiting_repo_identities": ca_waiting_repo_identities,
|
||||
"pending_roa_dispatch_len": pending_roa_dispatch_len,
|
||||
"inflight_publication_points_len": inflight_publication_points_len,
|
||||
"pending_finalization_len": pending_finalization_len,
|
||||
"finalize_inflight": finalize_inflight,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn compact_phase2_finished_result(
|
||||
mut result: PublicationPointRunResult,
|
||||
compact_audit: bool,
|
||||
) -> FinishedPublicationPointResult {
|
||||
result.objects.audit.clear();
|
||||
if compact_audit {
|
||||
result.audit.objects.clear();
|
||||
result.audit.warnings.clear();
|
||||
}
|
||||
FinishedPublicationPointResult::Ok {
|
||||
warnings: result.warnings,
|
||||
objects: result.objects,
|
||||
audit: result.audit,
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_phase2_finished_result_result(
|
||||
result: Result<PublicationPointRunResult, String>,
|
||||
compact_audit: bool,
|
||||
) -> FinishedPublicationPointResult {
|
||||
match result {
|
||||
Ok(result) => compact_phase2_finished_result(result, compact_audit),
|
||||
Err(err) => FinishedPublicationPointResult::Err(err),
|
||||
}
|
||||
}
|
||||
68
src/validation/tree_runner.rs
Normal file
68
src/validation/tree_runner.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
#![allow(
|
||||
clippy::large_enum_variant,
|
||||
clippy::too_many_arguments,
|
||||
clippy::type_complexity
|
||||
)]
|
||||
|
||||
mod labels;
|
||||
|
||||
use crate::ccr::CcrAccumulator;
|
||||
use crate::model::rc::ResourceCertificate;
|
||||
use crate::model::router_cert::{
|
||||
BgpsecRouterCertificate, BgpsecRouterCertificateDecodeError, BgpsecRouterCertificatePathError,
|
||||
BgpsecRouterCertificateProfileError,
|
||||
};
|
||||
use crate::output::analysis::timing::TimingHandle;
|
||||
use crate::output::audit::{
|
||||
AuditObjectKind, AuditObjectResult, AuditWarning, DiscoveredFrom, ObjectAuditEntry,
|
||||
PublicationPointAudit, sha256_hex, sha256_hex_from_32,
|
||||
};
|
||||
use crate::output::audit_downloads::DownloadLogHandle;
|
||||
use crate::output::report::{RfcRef, Warning};
|
||||
use crate::repository::current_repo_index::CurrentRepoIndexHandle;
|
||||
use crate::repository::fetch::rsync::RsyncFetcher;
|
||||
use crate::repository::storage::{CcrManifestProjection, PackFile, PackTime, RocksStore};
|
||||
use crate::repository::sync::repo::sync_publication_point;
|
||||
use crate::repository::sync::rrdp::Fetcher;
|
||||
use crate::scheduler::config::ParallelPhase2Config;
|
||||
use crate::scheduler::repo_runtime::RepoSyncRuntime;
|
||||
use crate::validation::ca_instance::ca_instance_uris_from_ca_certificate;
|
||||
use crate::validation::ca_path::{
|
||||
CaPathError, IssuerEffectiveResourcesIndex, ValidatedSubordinateCaLite,
|
||||
validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources,
|
||||
};
|
||||
use crate::validation::manifest::{
|
||||
FreshPublicationPointTimingBreakdown, FreshValidatedPublicationPoint, ManifestFreshError,
|
||||
PublicationPointData, PublicationPointSource,
|
||||
process_manifest_publication_point_fresh_after_repo_sync_with_timing,
|
||||
};
|
||||
use crate::validation::objects::{
|
||||
ParallelRoaWorkerPool, RouterKeyPayload, process_publication_point_for_issuer_parallel_roa,
|
||||
process_publication_point_for_issuer_parallel_roa_with_pool,
|
||||
};
|
||||
use crate::validation::policy::{Policy, ResourceValidationMode};
|
||||
use crate::validation::publication_point::PublicationPointSnapshot;
|
||||
use crate::validation::tree::{
|
||||
CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance, DiscoveredChildEntryProjection,
|
||||
PublicationPointRunResult, PublicationPointRunner,
|
||||
};
|
||||
use labels::{
|
||||
effective_repo_sync_duration_ms, kind_from_rsync_uri, repo_sync_failure_phase_label,
|
||||
repo_sync_phase_label, repo_sync_source_label, source_label, terminal_state_label,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
|
||||
include!("tree_runner/types.rs");
|
||||
|
||||
include!("tree_runner/timing.rs");
|
||||
include!("tree_runner/fresh_pipeline.rs");
|
||||
|
||||
include!("tree_runner/publication_point_runner.rs");
|
||||
include!("tree_runner/ephemeral_state.rs");
|
||||
include!("tree_runner/discovery.rs");
|
||||
include!("tree_runner/child_validation.rs");
|
||||
include!("tree_runner/audit_projection.rs");
|
||||
164
src/validation/tree_runner/audit_projection.rs
Normal file
164
src/validation/tree_runner/audit_projection.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
fn build_publication_point_audit_from_snapshot(
|
||||
ca: &CaInstanceHandle,
|
||||
source: PublicationPointSource,
|
||||
repo_sync_source: Option<&str>,
|
||||
repo_sync_phase: Option<&str>,
|
||||
repo_sync_duration_ms: Option<u64>,
|
||||
repo_sync_error: Option<&str>,
|
||||
pack: &PublicationPointSnapshot,
|
||||
runner_warnings: &[Warning],
|
||||
objects: &crate::validation::objects::ObjectsOutput,
|
||||
child_audits: &[ObjectAuditEntry],
|
||||
) -> PublicationPointAudit {
|
||||
use crate::model::crl::RpkixCrl;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = pack
|
||||
.files
|
||||
.iter()
|
||||
.map(|file| {
|
||||
(
|
||||
file.rsync_uri.clone(),
|
||||
ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: kind_from_rsync_uri(&file.rsync_uri),
|
||||
result: AuditObjectResult::Skipped,
|
||||
detail: Some("skipped: not processed in this run".to_string()),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for file in &pack.files {
|
||||
if !file.rsync_uri.ends_with(".crl") {
|
||||
continue;
|
||||
}
|
||||
let decoded = file
|
||||
.bytes()
|
||||
.ok()
|
||||
.and_then(|bytes| RpkixCrl::decode_der(bytes).ok())
|
||||
.is_some();
|
||||
audit_by_uri.insert(
|
||||
file.rsync_uri.clone(),
|
||||
ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Crl,
|
||||
result: if decoded {
|
||||
AuditObjectResult::Ok
|
||||
} else {
|
||||
AuditObjectResult::Error
|
||||
},
|
||||
detail: if decoded {
|
||||
None
|
||||
} else {
|
||||
Some("CRL decode failed".to_string())
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for entry in child_audits {
|
||||
audit_by_uri.insert(entry.rsync_uri.clone(), entry.clone());
|
||||
}
|
||||
for entry in &objects.audit {
|
||||
audit_by_uri.insert(entry.rsync_uri.clone(), entry.clone());
|
||||
}
|
||||
|
||||
let mut objects_out = Vec::with_capacity(pack.files.len() + 1);
|
||||
objects_out.push(ObjectAuditEntry {
|
||||
rsync_uri: pack.manifest_rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex(&pack.manifest_bytes),
|
||||
kind: AuditObjectKind::Manifest,
|
||||
result: AuditObjectResult::Ok,
|
||||
detail: None,
|
||||
});
|
||||
for file in &pack.files {
|
||||
objects_out.push(audit_by_uri.remove(&file.rsync_uri).unwrap_or_else(|| {
|
||||
ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: kind_from_rsync_uri(&file.rsync_uri),
|
||||
result: AuditObjectResult::Skipped,
|
||||
detail: Some("skipped: no audit entry".to_string()),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let mut warnings = runner_warnings.iter().map(AuditWarning::from).collect::<Vec<_>>();
|
||||
warnings.extend(objects.warnings.iter().map(AuditWarning::from));
|
||||
PublicationPointAudit {
|
||||
node_id: None,
|
||||
parent_node_id: None,
|
||||
discovered_from: None,
|
||||
rsync_base_uri: ca.rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
|
||||
source: source_label(source),
|
||||
repo_sync_source: repo_sync_source.map(ToString::to_string),
|
||||
repo_sync_phase: repo_sync_phase.map(ToString::to_string),
|
||||
repo_sync_duration_ms,
|
||||
repo_sync_error: repo_sync_error.map(ToString::to_string),
|
||||
repo_terminal_state: terminal_state_label(source).to_string(),
|
||||
this_update_rfc3339_utc: pack.this_update.rfc3339_utc.clone(),
|
||||
next_update_rfc3339_utc: pack.next_update.rfc3339_utc.clone(),
|
||||
verified_at_rfc3339_utc: pack.verified_at.rfc3339_utc.clone(),
|
||||
warnings,
|
||||
objects: objects_out,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_publication_point_audit_from_failed_fetch(
|
||||
ca: &CaInstanceHandle,
|
||||
repo_sync_source: Option<&str>,
|
||||
repo_sync_phase: Option<&str>,
|
||||
repo_sync_duration_ms: Option<u64>,
|
||||
repo_sync_error: Option<&str>,
|
||||
runner_warnings: &[Warning],
|
||||
fresh_error: &ManifestFreshError,
|
||||
) -> PublicationPointAudit {
|
||||
let mut warnings = runner_warnings.iter().map(AuditWarning::from).collect::<Vec<_>>();
|
||||
warnings.push(AuditWarning::from(
|
||||
&Warning::new(fresh_error.to_string()).with_context(&ca.manifest_rsync_uri),
|
||||
));
|
||||
PublicationPointAudit {
|
||||
node_id: None,
|
||||
parent_node_id: None,
|
||||
discovered_from: None,
|
||||
rsync_base_uri: ca.rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
|
||||
source: source_label(PublicationPointSource::FailedFetchNoCache),
|
||||
repo_sync_source: repo_sync_source.map(ToString::to_string),
|
||||
repo_sync_phase: repo_sync_phase.map(ToString::to_string),
|
||||
repo_sync_duration_ms,
|
||||
repo_sync_error: repo_sync_error.map(ToString::to_string),
|
||||
repo_terminal_state: terminal_state_label(PublicationPointSource::FailedFetchNoCache)
|
||||
.to_string(),
|
||||
this_update_rfc3339_utc: String::new(),
|
||||
next_update_rfc3339_utc: String::new(),
|
||||
verified_at_rfc3339_utc: String::new(),
|
||||
warnings,
|
||||
objects: vec![ObjectAuditEntry {
|
||||
rsync_uri: ca.manifest_rsync_uri.clone(),
|
||||
sha256_hex: String::new(),
|
||||
kind: AuditObjectKind::Manifest,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(fresh_error.to_string()),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_objects_output() -> crate::validation::objects::ObjectsOutput {
|
||||
crate::validation::objects::ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: crate::validation::objects::ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
}
|
||||
}
|
||||
43
src/validation/tree_runner/child_validation.rs
Normal file
43
src/validation/tree_runner/child_validation.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
fn is_non_router_certificate(err: &BgpsecRouterCertificatePathError) -> bool {
|
||||
matches!(
|
||||
err,
|
||||
BgpsecRouterCertificatePathError::Decode(BgpsecRouterCertificateDecodeError::Validate(
|
||||
BgpsecRouterCertificateProfileError::NotEe
|
||||
| BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage
|
||||
| BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_subordinate_ca_cert_with_issuer(
|
||||
child_ca_der: &[u8],
|
||||
child_ca: crate::model::rc::ResourceCertificate,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca: &crate::model::rc::ResourceCertificate,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
issuer_crl_uri: &str,
|
||||
crl_states: &mut HashMap<String, IssuerCrlState>,
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_effective_ip: Option<&crate::model::rc::IpResourceSet>,
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
issuer_resources_index: &IssuerEffectiveResourcesIndex,
|
||||
validation_time: time::OffsetDateTime,
|
||||
resource_validation_mode: ResourceValidationMode,
|
||||
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
|
||||
let verified_crl = ensure_issuer_crl_verified(issuer_crl_uri, crl_states, issuer_ca_der)?;
|
||||
validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
|
||||
child_ca_der,
|
||||
child_ca,
|
||||
issuer_ca,
|
||||
issuer_spki,
|
||||
&verified_crl.crl,
|
||||
&verified_crl.revoked_serials,
|
||||
issuer_ca_rsync_uri,
|
||||
issuer_crl_uri,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
issuer_resources_index,
|
||||
validation_time,
|
||||
resource_validation_mode,
|
||||
)
|
||||
}
|
||||
329
src/validation/tree_runner/discovery.rs
Normal file
329
src/validation/tree_runner/discovery.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
// Fresh child-certificate and router-certificate validation for one
|
||||
// publication-point snapshot. The CRL map below is deliberately scoped to
|
||||
// this call: it only avoids decoding the same CRL twice within one snapshot.
|
||||
|
||||
fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: PublicationPointData>(
|
||||
issuer: &CaInstanceHandle,
|
||||
issuer_ca_der: &[u8],
|
||||
publication_point: &P,
|
||||
validation_time: time::OffsetDateTime,
|
||||
timing: Option<&TimingHandle>,
|
||||
policy: &Policy,
|
||||
) -> Result<ChildDiscoveryOutput, String> {
|
||||
let locked_files = publication_point.files();
|
||||
let issuer_ca = match crate::model::rc::ResourceCertificate::decode_der(issuer_ca_der) {
|
||||
Ok(ca) => match ca.validate_rfc6487_profile(crate::model::rc::ResourceCertificateRole::Ca) {
|
||||
Ok(()) => Some(ca),
|
||||
Err(error) => {
|
||||
crate::logging::emit(crate::logging::Level::Debug, "issuer_ca_profile_error", || serde_json::json!({"error": error.to_string()}));
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
crate::logging::emit(crate::logging::Level::Debug, "issuer_ca_decode_error", || serde_json::json!({"error": error.to_string()}));
|
||||
None
|
||||
}
|
||||
};
|
||||
let issuer_spki = issuer_ca
|
||||
.as_ref()
|
||||
.and_then(|ca| SubjectPublicKeyInfo::from_der(&ca.tbs.subject_public_key_info).ok())
|
||||
.and_then(|(remaining, spki)| remaining.is_empty().then_some(spki));
|
||||
|
||||
let mut crl_states: HashMap<String, IssuerCrlState> = locked_files
|
||||
.iter()
|
||||
.filter(|file| file.rsync_uri.ends_with(".crl"))
|
||||
.map(|file| {
|
||||
let bytes = file
|
||||
.bytes_cloned()
|
||||
.map_err(|error| format!("snapshot CRL bytes load failed: {error}"))?;
|
||||
Ok((
|
||||
file.rsync_uri.clone(),
|
||||
IssuerCrlState::Pending { bytes },
|
||||
))
|
||||
})
|
||||
.collect::<Result<_, String>>()?;
|
||||
|
||||
let issuer_resources_index = IssuerEffectiveResourcesIndex::from_effective_resources(
|
||||
issuer.effective_ip_resources.as_ref(),
|
||||
issuer.effective_as_resources.as_ref(),
|
||||
)
|
||||
.map_err(|error| format!("build issuer effective resources index failed: {error}"))?;
|
||||
|
||||
let mut children = Vec::new();
|
||||
let mut audits = Vec::new();
|
||||
let mut router_keys = Vec::new();
|
||||
let mut ca_ok = 0u64;
|
||||
let mut ca_error = 0u64;
|
||||
let mut router_ok = 0u64;
|
||||
let mut router_error = 0u64;
|
||||
let mut uri_errors = 0u64;
|
||||
let mut decode_nanos = 0u64;
|
||||
let mut validate_nanos = 0u64;
|
||||
let mut uri_nanos = 0u64;
|
||||
let mut child_load_nanos = 0u64;
|
||||
let mut child_load_count = 0u64;
|
||||
|
||||
for file in locked_files.iter().filter(|file| file.rsync_uri.ends_with(".cer")) {
|
||||
let child_der = load_child_certificate_der_for_discovery(
|
||||
file,
|
||||
&mut child_load_nanos,
|
||||
&mut child_load_count,
|
||||
)?;
|
||||
let decode_started = std::time::Instant::now();
|
||||
let child_cert = match crate::model::rc::ResourceCertificate::decode_der(child_der) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
ca_error = ca_error.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(format!("child certificate decode failed: {error}")),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
decode_nanos = decode_nanos.saturating_add(
|
||||
decode_started
|
||||
.elapsed()
|
||||
.as_nanos()
|
||||
.min(u128::from(u64::MAX)) as u64,
|
||||
);
|
||||
let issuer_crl_uri = match select_issuer_crl_uri_for_child(&child_cert, &crl_states) {
|
||||
Ok(uri) => uri.to_string(),
|
||||
Err(error) => {
|
||||
ca_error = ca_error.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(format!("cannot select issuer CRL: {error}")),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let (Some(issuer_ca), Some(issuer_spki)) = (issuer_ca.as_ref(), issuer_spki.as_ref()) else {
|
||||
ca_error = ca_error.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some("issuer CA or SubjectPublicKeyInfo validation failed".to_string()),
|
||||
});
|
||||
continue;
|
||||
};
|
||||
|
||||
let validate_started = std::time::Instant::now();
|
||||
let validated = match validate_subordinate_ca_cert_with_issuer(
|
||||
child_der,
|
||||
child_cert,
|
||||
issuer_ca_der,
|
||||
issuer_ca,
|
||||
issuer_spki,
|
||||
&issuer_crl_uri,
|
||||
&mut crl_states,
|
||||
issuer.ca_certificate_rsync_uri.as_deref(),
|
||||
issuer.effective_ip_resources.as_ref(),
|
||||
issuer.effective_as_resources.as_ref(),
|
||||
&issuer_resources_index,
|
||||
validation_time,
|
||||
policy.resource_validation_mode,
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(CaPathError::ChildNotCa) => {
|
||||
let router_result = match ensure_issuer_crl_verified(
|
||||
&issuer_crl_uri,
|
||||
&mut crl_states,
|
||||
issuer_ca_der,
|
||||
) {
|
||||
Ok(verified_crl) => BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
|
||||
child_der,
|
||||
issuer_ca,
|
||||
issuer_spki,
|
||||
&verified_crl.crl,
|
||||
&verified_crl.revoked_serials,
|
||||
issuer.ca_certificate_rsync_uri.as_deref(),
|
||||
Some(issuer_crl_uri.as_str()),
|
||||
validation_time,
|
||||
),
|
||||
Err(error) => {
|
||||
router_error = router_error.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::RouterCertificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(format!("router certificate issuer CRL validation failed: {error}")),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match router_result {
|
||||
Ok(router) => {
|
||||
if let Some(constraints) = policy.ta_constraints.for_tal(&issuer.tal_id)
|
||||
&& let Err(error) = constraints.validate_ee_certificate(&router.resource_cert) {
|
||||
router_error = router_error.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::RouterCertificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(format!("router certificate violates TA constraints: {error}")),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let asns = match router_asns_for_resource_mode(
|
||||
&router.asns,
|
||||
issuer.effective_as_resources.as_ref(),
|
||||
policy.resource_validation_mode,
|
||||
) {
|
||||
Ok(asns) => asns,
|
||||
Err(error) => {
|
||||
router_error = router_error.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::RouterCertificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(error),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let source_hash = sha256_hex_from_32(&file.sha256);
|
||||
let until = PackTime::from_utc_offset_datetime(router.resource_cert.tbs.validity_not_after);
|
||||
for as_id in asns {
|
||||
router_keys.push(RouterKeyPayload {
|
||||
as_id,
|
||||
ski: router.subject_key_identifier.clone(),
|
||||
spki_der: router.spki_der.clone(),
|
||||
source_object_uri: file.rsync_uri.clone(),
|
||||
source_object_hash: source_hash.clone(),
|
||||
source_ee_cert_hash: source_hash.clone(),
|
||||
item_effective_until: until.clone(),
|
||||
});
|
||||
}
|
||||
router_ok = router_ok.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: source_hash,
|
||||
kind: AuditObjectKind::RouterCertificate,
|
||||
result: AuditObjectResult::Ok,
|
||||
detail: Some("validated BGPsec router certificate (RFC 8209)".to_string()),
|
||||
});
|
||||
}
|
||||
Err(error) if is_non_router_certificate(&error) => {
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Skipped,
|
||||
detail: Some("certificate is neither a CA nor a BGPsec router certificate".to_string()),
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
router_error = router_error.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::RouterCertificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(format!("router certificate validation failed: {error}")),
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
ca_error = ca_error.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(format!("child CA validation failed: {error}")),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
validate_nanos = validate_nanos.saturating_add(
|
||||
validate_started
|
||||
.elapsed()
|
||||
.as_nanos()
|
||||
.min(u128::from(u64::MAX)) as u64,
|
||||
);
|
||||
|
||||
let uri_started = std::time::Instant::now();
|
||||
let uris = match ca_instance_uris_from_ca_certificate(&validated.child_ca) {
|
||||
Ok(uris) => uris,
|
||||
Err(error) => {
|
||||
uri_errors = uri_errors.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(format!("CA instance URI discovery failed: {error}")),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
uri_nanos = uri_nanos.saturating_add(
|
||||
uri_started
|
||||
.elapsed()
|
||||
.as_nanos()
|
||||
.min(u128::from(u64::MAX)) as u64,
|
||||
);
|
||||
let child_ski = validated.child_ca.tbs.extensions.subject_key_identifier.clone();
|
||||
let child_hash = sha256_hex_from_32(&file.sha256);
|
||||
children.push(DiscoveredChildCaInstance {
|
||||
handle: CaInstanceHandle {
|
||||
depth: 0,
|
||||
tal_id: issuer.tal_id.clone(),
|
||||
parent_manifest_rsync_uri: Some(issuer.manifest_rsync_uri.clone()),
|
||||
ca_certificate: CaCertificateRef::inline_der(child_der.to_vec()),
|
||||
ca_certificate_rsync_uri: Some(file.rsync_uri.clone()),
|
||||
effective_ip_resources: validated.effective_ip_resources.clone(),
|
||||
effective_as_resources: validated.effective_as_resources.clone(),
|
||||
rsync_base_uri: uris.rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: uris.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: uris.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: uris.rrdp_notification_uri.clone(),
|
||||
},
|
||||
discovered_from: DiscoveredFrom {
|
||||
parent_manifest_rsync_uri: issuer.manifest_rsync_uri.clone(),
|
||||
child_ca_certificate_rsync_uri: file.rsync_uri.clone(),
|
||||
child_ca_certificate_sha256_hex: child_hash.clone(),
|
||||
},
|
||||
child_entry_projection: child_ski.as_ref().map(|ski| DiscoveredChildEntryProjection {
|
||||
child_ski: hex::encode(ski),
|
||||
}),
|
||||
});
|
||||
ca_ok = ca_ok.saturating_add(1);
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: file.rsync_uri.clone(),
|
||||
sha256_hex: child_hash,
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Ok,
|
||||
detail: Some("validated subordinate CA certificate; enqueued CA instance".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(timing) = timing {
|
||||
timing.record_count("child_cer_seen", locked_files.iter().filter(|file| file.rsync_uri.ends_with(".cer")).count() as u64);
|
||||
timing.record_count("child_ca_ok", ca_ok);
|
||||
timing.record_count("child_ca_error", ca_error);
|
||||
timing.record_count("child_router_ok", router_ok);
|
||||
timing.record_count("child_router_error", router_error);
|
||||
timing.record_count("child_uri_discovery_error", uri_errors);
|
||||
timing.record_phase_nanos("child_decode_certificate_total", decode_nanos);
|
||||
timing.record_phase_nanos("child_validate_subordinate_total", validate_nanos);
|
||||
timing.record_phase_nanos("child_ca_instance_uri_discovery_total", uri_nanos);
|
||||
timing.record_phase_nanos("child_certificate_der_load_total", child_load_nanos);
|
||||
timing.record_count("child_certificate_der_load_count", child_load_count);
|
||||
}
|
||||
Ok(ChildDiscoveryOutput { children, audits, router_keys })
|
||||
}
|
||||
152
src/validation/tree_runner/ephemeral_state.rs
Normal file
152
src/validation/tree_runner/ephemeral_state.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
// Per-publication-point validation state.
|
||||
//
|
||||
// This module intentionally contains only ephemeral state needed while a
|
||||
// snapshot is being validated. Nothing here is serialized or reused across
|
||||
// runs; in particular, it is not a validation cache.
|
||||
|
||||
struct ChildDiscoveryOutput {
|
||||
children: Vec<DiscoveredChildCaInstance>,
|
||||
audits: Vec<ObjectAuditEntry>,
|
||||
router_keys: Vec<RouterKeyPayload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct VerifiedIssuerCrl {
|
||||
crl: crate::model::crl::RpkixCrl,
|
||||
revoked_serials: std::collections::HashSet<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum IssuerCrlState {
|
||||
Pending { bytes: Vec<u8> },
|
||||
Verified(VerifiedIssuerCrl),
|
||||
}
|
||||
|
||||
|
||||
fn load_child_certificate_der_for_discovery<'a>(
|
||||
file: &'a PackFile,
|
||||
elapsed_nanos: &mut u64,
|
||||
count: &mut u64,
|
||||
) -> Result<&'a [u8], String> {
|
||||
let started = std::time::Instant::now();
|
||||
let bytes = file
|
||||
.bytes()
|
||||
.map_err(|e| format!("child certificate bytes load failed: {e}"))?;
|
||||
*elapsed_nanos = elapsed_nanos.saturating_add(
|
||||
started
|
||||
.elapsed()
|
||||
.as_nanos()
|
||||
.min(u128::from(u64::MAX)) as u64,
|
||||
);
|
||||
*count = count.saturating_add(1);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn ca_certificate_der_for_validation<'a>(
|
||||
ca: &'a CaInstanceHandle,
|
||||
store: &RocksStore,
|
||||
timing: Option<&TimingHandle>,
|
||||
) -> Result<std::borrow::Cow<'a, [u8]>, String> {
|
||||
let started = std::time::Instant::now();
|
||||
let was_lazy = ca.ca_certificate_sha256_hex().is_some();
|
||||
let der = ca.ca_certificate_der(store)?;
|
||||
if was_lazy {
|
||||
let elapsed = started
|
||||
.elapsed()
|
||||
.as_nanos()
|
||||
.min(u128::from(u64::MAX)) as u64;
|
||||
if let Some(timing) = timing {
|
||||
timing.record_count("ca_certificate_lazy_load_count", 1);
|
||||
timing.record_count("ca_certificate_lazy_load_bytes", der.len() as u64);
|
||||
timing.record_phase_nanos("ca_certificate_lazy_load_total", elapsed);
|
||||
}
|
||||
}
|
||||
Ok(der)
|
||||
}
|
||||
|
||||
fn select_issuer_crl_uri_for_child<'a>(
|
||||
child: &'a crate::model::rc::ResourceCertificate,
|
||||
crl_states: &HashMap<String, IssuerCrlState>,
|
||||
) -> Result<&'a str, String> {
|
||||
if crl_states.is_empty() {
|
||||
return Err(
|
||||
"no CRL available in publication point snapshot (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let Some(crldp_uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else {
|
||||
return Err(
|
||||
"child certificate CRLDistributionPoints missing (RFC 6487 §4.8.6)".to_string(),
|
||||
);
|
||||
};
|
||||
for uri in crldp_uris {
|
||||
if crl_states.contains_key(uri.as_str()) {
|
||||
return Ok(uri.as_str());
|
||||
}
|
||||
}
|
||||
Err(format!(
|
||||
"CRL referenced by child certificate CRLDistributionPoints not found in publication point snapshot: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)",
|
||||
crldp_uris.iter().map(|uri| uri.as_str()).collect::<Vec<_>>().join(", ")
|
||||
))
|
||||
}
|
||||
|
||||
fn ensure_issuer_crl_verified<'a>(
|
||||
crl_uri: &str,
|
||||
crl_states: &'a mut HashMap<String, IssuerCrlState>,
|
||||
issuer_ca_der: &[u8],
|
||||
) -> Result<&'a VerifiedIssuerCrl, CaPathError> {
|
||||
let state = crl_states
|
||||
.get_mut(crl_uri)
|
||||
.expect("selected CRL must be present in snapshot");
|
||||
match state {
|
||||
IssuerCrlState::Verified(verified) => Ok(verified),
|
||||
IssuerCrlState::Pending { bytes } => {
|
||||
let der = std::mem::take(bytes);
|
||||
let crl = crate::model::crl::RpkixCrl::decode_der(&der)?;
|
||||
crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
|
||||
let mut revoked_serials = std::collections::HashSet::with_capacity(crl.revoked_certs.len());
|
||||
for revoked in &crl.revoked_certs {
|
||||
revoked_serials.insert(revoked.serial_number.bytes_be.clone());
|
||||
}
|
||||
*state = IssuerCrlState::Verified(VerifiedIssuerCrl {
|
||||
crl,
|
||||
revoked_serials,
|
||||
});
|
||||
match state {
|
||||
IssuerCrlState::Verified(verified) => Ok(verified),
|
||||
IssuerCrlState::Pending { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn router_asns_for_resource_mode(
|
||||
router_asns: &[u32],
|
||||
issuer_effective_as: Option<&crate::model::rc::AsResourceSet>,
|
||||
mode: ResourceValidationMode,
|
||||
) -> Result<Vec<u32>, String> {
|
||||
let Some(resources) = issuer_effective_as else {
|
||||
return Err("issuer has no effective AS resources".to_string());
|
||||
};
|
||||
let contains = |asn: u32| {
|
||||
resources.asnum.as_ref().is_some_and(|choice| match choice {
|
||||
crate::model::rc::AsIdentifierChoice::Inherit => false,
|
||||
crate::model::rc::AsIdentifierChoice::AsIdsOrRanges(items) => items.iter().any(|item| match item {
|
||||
crate::model::rc::AsIdOrRange::Id(id) => *id == asn,
|
||||
crate::model::rc::AsIdOrRange::Range { min, max } => *min <= asn && asn <= *max,
|
||||
}),
|
||||
})
|
||||
};
|
||||
match mode {
|
||||
ResourceValidationMode::Rfc6487 => {
|
||||
let outside = router_asns.iter().copied().filter(|asn| !contains(*asn)).collect::<Vec<_>>();
|
||||
if outside.is_empty() { Ok(router_asns.to_vec()) } else {
|
||||
Err(format!("router AS resources are not a subset of issuer effective AS resources: {outside:?}"))
|
||||
}
|
||||
}
|
||||
ResourceValidationMode::ValidationUpdate03 => {
|
||||
let filtered = router_asns.iter().copied().filter(|asn| contains(*asn)).collect::<Vec<_>>();
|
||||
if filtered.is_empty() { Err("router AS resources have empty validated resource set".to_string()) } else { Ok(filtered) }
|
||||
}
|
||||
}
|
||||
}
|
||||
324
src/validation/tree_runner/fresh_pipeline.rs
Normal file
324
src/validation/tree_runner/fresh_pipeline.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
pub(crate) fn ccr_accumulator_snapshot(&self) -> Option<CcrAccumulator> {
|
||||
self.ccr_accumulator
|
||||
.as_ref()
|
||||
.and_then(|accumulator| accumulator.lock().ok().map(|guard| guard.clone()))
|
||||
}
|
||||
|
||||
pub(crate) fn append_ccr_manifest_projection(
|
||||
&self,
|
||||
projection: &CcrManifestProjection,
|
||||
) -> Result<(), String> {
|
||||
if let Some(accumulator) = self.ccr_accumulator.as_ref() {
|
||||
accumulator
|
||||
.lock()
|
||||
.map_err(|_| "lock CCR accumulator failed".to_string())?
|
||||
.append_manifest_projection(projection)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn stage_fresh_publication_point_after_repo_ready(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
repo_sync_ok: bool,
|
||||
repo_sync_err: Option<&str>,
|
||||
) -> Result<FreshPublicationPointStage, FreshPublicationPointStageError> {
|
||||
let snapshot_prepare_started = std::time::Instant::now();
|
||||
let issuer_ca_der = ca_certificate_der_for_validation(ca, self.store, self.timing.as_ref())
|
||||
.map_err(|detail| FreshPublicationPointStageError {
|
||||
error: ManifestFreshError::IssuerCaLoadFailed { detail },
|
||||
snapshot_prepare_ms: snapshot_prepare_started.elapsed().as_millis() as u64,
|
||||
})?;
|
||||
let issuer_ca_der: Arc<[u8]> = Arc::from(issuer_ca_der.as_ref());
|
||||
let fresh_publication_point = {
|
||||
let _manifest_total = self
|
||||
.timing
|
||||
.as_ref()
|
||||
.map(|t| t.span_phase("manifest_processing_total"));
|
||||
process_manifest_publication_point_fresh_after_repo_sync_with_timing(
|
||||
self.store,
|
||||
&ca.manifest_rsync_uri,
|
||||
&ca.publication_point_rsync_uri,
|
||||
self.current_repo_index.as_ref(),
|
||||
issuer_ca_der.as_ref(),
|
||||
ca.ca_certificate_rsync_uri.as_deref(),
|
||||
self.validation_time,
|
||||
repo_sync_ok,
|
||||
repo_sync_err,
|
||||
)
|
||||
};
|
||||
let snapshot_prepare_ms = snapshot_prepare_started.elapsed().as_millis() as u64;
|
||||
let (fresh_point, snapshot_prepare_timing) =
|
||||
fresh_publication_point.map_err(|error| FreshPublicationPointStageError {
|
||||
error,
|
||||
snapshot_prepare_ms,
|
||||
})?;
|
||||
if let Some(timing) = self.timing.as_ref() {
|
||||
timing.record_phase_nanos(
|
||||
"fresh_snapshot_prepare_total",
|
||||
snapshot_prepare_ms.saturating_mul(1_000_000),
|
||||
);
|
||||
timing.record_phase_nanos(
|
||||
"fresh_snapshot_manifest_load_total",
|
||||
snapshot_prepare_timing
|
||||
.manifest_load_ms
|
||||
.saturating_mul(1_000_000),
|
||||
);
|
||||
timing.record_phase_nanos(
|
||||
"fresh_snapshot_manifest_decode_total",
|
||||
snapshot_prepare_timing
|
||||
.manifest_decode_ms
|
||||
.saturating_mul(1_000_000),
|
||||
);
|
||||
timing.record_phase_nanos(
|
||||
"fresh_snapshot_anti_rollback_total",
|
||||
snapshot_prepare_timing
|
||||
.anti_rollback_guard_ms
|
||||
.saturating_mul(1_000_000),
|
||||
);
|
||||
timing.record_phase_nanos(
|
||||
"fresh_snapshot_manifest_entries_total",
|
||||
snapshot_prepare_timing
|
||||
.manifest_entries_ms
|
||||
.saturating_mul(1_000_000),
|
||||
);
|
||||
timing.record_phase_nanos(
|
||||
"fresh_snapshot_pack_files_total",
|
||||
snapshot_prepare_timing
|
||||
.pack_files_ms
|
||||
.saturating_mul(1_000_000),
|
||||
);
|
||||
timing.record_phase_nanos(
|
||||
"fresh_snapshot_ee_path_validate_total",
|
||||
snapshot_prepare_timing
|
||||
.ee_path_validate_ms
|
||||
.saturating_mul(1_000_000),
|
||||
);
|
||||
timing.record_count("fresh_publication_points", 1);
|
||||
timing.record_count(
|
||||
"fresh_manifest_files_total",
|
||||
snapshot_prepare_timing.manifest_file_count as u64,
|
||||
);
|
||||
}
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_snapshot_prepare",
|
||||
snapshot_prepare_ms,
|
||||
);
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_snapshot_manifest_load",
|
||||
snapshot_prepare_timing.manifest_load_ms,
|
||||
);
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_snapshot_manifest_decode",
|
||||
snapshot_prepare_timing.manifest_decode_ms,
|
||||
);
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_snapshot_anti_rollback",
|
||||
snapshot_prepare_timing.anti_rollback_guard_ms,
|
||||
);
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_snapshot_manifest_entries",
|
||||
snapshot_prepare_timing.manifest_entries_ms,
|
||||
);
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_snapshot_pack_files",
|
||||
snapshot_prepare_timing.pack_files_ms,
|
||||
);
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_snapshot_ee_path_validate",
|
||||
snapshot_prepare_timing.ee_path_validate_ms,
|
||||
);
|
||||
|
||||
let child_discovery_started = std::time::Instant::now();
|
||||
let out = {
|
||||
let _child_disc_total = self
|
||||
.timing
|
||||
.as_ref()
|
||||
.map(|t| t.span_phase("child_discovery_total"));
|
||||
discover_children_from_fresh_snapshot_with_audit_with_issuer_der(
|
||||
ca,
|
||||
issuer_ca_der.as_ref(),
|
||||
&fresh_point,
|
||||
self.validation_time,
|
||||
self.timing.as_ref(),
|
||||
self.policy,
|
||||
)
|
||||
};
|
||||
let (discovered_children, child_audits, discovered_router_keys, warnings) = match out {
|
||||
Ok(out) => (out.children, out.audits, out.router_keys, Vec::new()),
|
||||
Err(e) => (
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
vec![
|
||||
Warning::new(format!("child CA discovery failed: {e}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
],
|
||||
),
|
||||
};
|
||||
let child_discovery_ms = child_discovery_started.elapsed().as_millis() as u64;
|
||||
if let Some(timing) = self.timing.as_ref() {
|
||||
timing.record_phase_nanos(
|
||||
"fresh_child_discovery_total",
|
||||
child_discovery_ms.saturating_mul(1_000_000),
|
||||
);
|
||||
timing.record_count(
|
||||
"fresh_children_discovered",
|
||||
discovered_children.len() as u64,
|
||||
);
|
||||
timing.record_count("fresh_child_audits", child_audits.len() as u64);
|
||||
timing.record_count(
|
||||
"fresh_router_keys_discovered",
|
||||
discovered_router_keys.len() as u64,
|
||||
);
|
||||
}
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_child_discovery",
|
||||
child_discovery_ms,
|
||||
);
|
||||
|
||||
Ok(FreshPublicationPointStage {
|
||||
fresh_point,
|
||||
issuer_ca_der,
|
||||
snapshot_prepare_timing,
|
||||
snapshot_prepare_ms,
|
||||
discovered_children,
|
||||
child_audits,
|
||||
discovered_router_keys,
|
||||
child_discovery_ms,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn finalize_fresh_publication_point_from_reducer(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
fresh_point: &FreshValidatedPublicationPoint,
|
||||
warnings: Vec<Warning>,
|
||||
objects: crate::validation::objects::ObjectsOutput,
|
||||
child_audits: Vec<ObjectAuditEntry>,
|
||||
discovered_children: Vec<DiscoveredChildCaInstance>,
|
||||
repo_sync_source: Option<&str>,
|
||||
repo_sync_phase: Option<&str>,
|
||||
repo_sync_duration_ms: u64,
|
||||
repo_sync_err: Option<&str>,
|
||||
) -> Result<FreshPublicationPointFinalizeOutput, String> {
|
||||
let snapshot_pack_started = std::time::Instant::now();
|
||||
let pack = fresh_point.to_publication_point_snapshot();
|
||||
let snapshot_pack_ms = snapshot_pack_started.elapsed().as_millis() as u64;
|
||||
if let Some(timing) = self.timing.as_ref() {
|
||||
timing.record_phase_nanos(
|
||||
"fresh_snapshot_pack_total",
|
||||
snapshot_pack_ms.saturating_mul(1_000_000),
|
||||
);
|
||||
}
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_snapshot_pack",
|
||||
snapshot_pack_ms,
|
||||
);
|
||||
|
||||
let mut ccr_projection_build_ms = 0;
|
||||
let mut ccr_append_ms = 0;
|
||||
if self.ccr_accumulator.is_some() {
|
||||
let ccr_projection_build_started = std::time::Instant::now();
|
||||
let subordinate_skis = discovered_children.iter().map(|child| {
|
||||
if let Some(projection) = &child.child_entry_projection {
|
||||
return hex::decode(&projection.child_ski).map_err(|error| error.to_string());
|
||||
}
|
||||
let der = child.handle.ca_certificate_der(self.store)?;
|
||||
let certificate = ResourceCertificate::decode_der(der.as_ref()).map_err(|error| error.to_string())?;
|
||||
certificate.tbs.extensions.subject_key_identifier.clone()
|
||||
.ok_or_else(|| "child certificate missing SubjectKeyIdentifier".to_string())
|
||||
}).collect::<Result<Vec<_>, String>>()?;
|
||||
let ccr_manifest_projection = crate::ccr::projection::from_snapshot(ca, &pack, subordinate_skis)?;
|
||||
ccr_projection_build_ms = ccr_projection_build_started.elapsed().as_millis() as u64;
|
||||
let ccr_append_started = std::time::Instant::now();
|
||||
self.append_ccr_manifest_projection(&ccr_manifest_projection)?;
|
||||
ccr_append_ms = ccr_append_started.elapsed().as_millis() as u64;
|
||||
}
|
||||
if let Some(timing) = self.timing.as_ref() {
|
||||
timing.record_phase_nanos(
|
||||
"fresh_ccr_projection_build_total",
|
||||
ccr_projection_build_ms.saturating_mul(1_000_000),
|
||||
);
|
||||
timing.record_phase_nanos(
|
||||
"fresh_ccr_append_total",
|
||||
ccr_append_ms.saturating_mul(1_000_000),
|
||||
);
|
||||
}
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_ccr_projection_build",
|
||||
ccr_projection_build_ms,
|
||||
);
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_ccr_append",
|
||||
ccr_append_ms,
|
||||
);
|
||||
|
||||
let audit_build_started = std::time::Instant::now();
|
||||
let audit = build_publication_point_audit_from_snapshot(
|
||||
ca,
|
||||
PublicationPointSource::Fresh,
|
||||
repo_sync_source,
|
||||
repo_sync_phase,
|
||||
Some(repo_sync_duration_ms),
|
||||
repo_sync_err,
|
||||
&pack,
|
||||
&warnings,
|
||||
&objects,
|
||||
&child_audits,
|
||||
);
|
||||
let audit_build_ms = audit_build_started.elapsed().as_millis() as u64;
|
||||
if let Some(timing) = self.timing.as_ref() {
|
||||
timing.record_phase_nanos(
|
||||
"fresh_audit_build_total",
|
||||
audit_build_ms.saturating_mul(1_000_000),
|
||||
);
|
||||
}
|
||||
self.record_publication_point_step_ms(
|
||||
&ca.manifest_rsync_uri,
|
||||
"fresh_audit_build",
|
||||
audit_build_ms,
|
||||
);
|
||||
|
||||
// Commit protocol freshness only after successful publication-point
|
||||
// validation/finalization, independently of optional CCR output.
|
||||
self.store.put_manifest_anti_rollback_meta(
|
||||
&crate::repository::storage::ManifestAntiRollbackMeta {
|
||||
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
|
||||
manifest_number_be: pack.manifest_number_be.clone(),
|
||||
manifest_this_update: pack.this_update.clone(),
|
||||
manifest_sha256: <sha2::Sha256 as sha2::Digest>::digest(&pack.manifest_bytes).to_vec(),
|
||||
updated_at_validation_time: pack.verified_at.clone(),
|
||||
},
|
||||
).map_err(|error| format!("persist manifest freshness failed: {error}"))?;
|
||||
|
||||
Ok(FreshPublicationPointFinalizeOutput {
|
||||
result: PublicationPointRunResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
snapshot: Some(pack),
|
||||
warnings,
|
||||
objects,
|
||||
audit,
|
||||
discovered_children,
|
||||
},
|
||||
snapshot_pack_ms,
|
||||
ccr_projection_build_ms,
|
||||
ccr_append_ms,
|
||||
audit_build_ms,
|
||||
})
|
||||
}
|
||||
}
|
||||
86
src/validation/tree_runner/labels.rs
Normal file
86
src/validation/tree_runner/labels.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! Stable labels used by validation audit output.
|
||||
//!
|
||||
//! These helpers are pure mappings. Keeping them separate from publication
|
||||
//! point traversal makes the output vocabulary easy to review without
|
||||
//! changing traversal or validation control flow.
|
||||
|
||||
use crate::output::audit::AuditObjectKind;
|
||||
use crate::validation::manifest::PublicationPointSource;
|
||||
|
||||
pub(super) fn kind_from_rsync_uri(uri: &str) -> AuditObjectKind {
|
||||
if uri.ends_with(".crl") {
|
||||
AuditObjectKind::Crl
|
||||
} else if uri.ends_with(".cer") {
|
||||
AuditObjectKind::Certificate
|
||||
} else if uri.ends_with(".roa") {
|
||||
AuditObjectKind::Roa
|
||||
} else if uri.ends_with(".asa") {
|
||||
AuditObjectKind::Aspa
|
||||
} else {
|
||||
AuditObjectKind::Other
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn source_label(source: PublicationPointSource) -> String {
|
||||
match source {
|
||||
PublicationPointSource::Fresh => "fresh".to_string(),
|
||||
PublicationPointSource::FailedFetchNoCache => "failed_fetch_no_cache".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn repo_sync_phase_label(
|
||||
phase: crate::repository::sync::repo::RepoSyncPhase,
|
||||
) -> &'static str {
|
||||
match phase {
|
||||
crate::repository::sync::repo::RepoSyncPhase::RrdpOk => "rrdp_ok",
|
||||
crate::repository::sync::repo::RepoSyncPhase::RrdpFailedRsyncOk => "rrdp_failed_rsync_ok",
|
||||
crate::repository::sync::repo::RepoSyncPhase::RsyncOnlyOk => "rsync_only_ok",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn repo_sync_failure_phase_label(
|
||||
attempted_rrdp: bool,
|
||||
original_notification_uri: Option<&str>,
|
||||
effective_notification_uri: Option<&str>,
|
||||
) -> &'static str {
|
||||
if attempted_rrdp && original_notification_uri.is_some() && effective_notification_uri.is_some()
|
||||
{
|
||||
"rrdp_failed_rsync_failed"
|
||||
} else if attempted_rrdp
|
||||
&& original_notification_uri.is_some()
|
||||
&& effective_notification_uri.is_none()
|
||||
{
|
||||
"rsync_only_failed_after_rrdp_dedup"
|
||||
} else {
|
||||
"rsync_only_failed"
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn terminal_state_label(source: PublicationPointSource) -> &'static str {
|
||||
match source {
|
||||
PublicationPointSource::Fresh => "fresh",
|
||||
PublicationPointSource::FailedFetchNoCache => "failed_no_cache",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn repo_sync_source_label(
|
||||
source: crate::repository::sync::repo::RepoSyncSource,
|
||||
) -> &'static str {
|
||||
match source {
|
||||
crate::repository::sync::repo::RepoSyncSource::Rrdp => "rrdp",
|
||||
crate::repository::sync::repo::RepoSyncSource::Rsync => "rsync",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn effective_repo_sync_duration_ms(
|
||||
elapsed_ms: u64,
|
||||
runtime_reported_duration_ms: Option<u64>,
|
||||
repo_sync_ok: bool,
|
||||
) -> u64 {
|
||||
if repo_sync_ok {
|
||||
return elapsed_ms;
|
||||
}
|
||||
runtime_reported_duration_ms
|
||||
.map(|runtime_ms| elapsed_ms.max(runtime_ms))
|
||||
.unwrap_or(elapsed_ms)
|
||||
}
|
||||
223
src/validation/tree_runner/publication_point_runner.rs
Normal file
223
src/validation/tree_runner/publication_point_runner.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
fn run_publication_point(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
) -> Result<PublicationPointRunResult, String> {
|
||||
let started = std::time::Instant::now();
|
||||
let _span = self
|
||||
.timing
|
||||
.as_ref()
|
||||
.map(|timing| timing.span_publication_point(&ca.manifest_rsync_uri));
|
||||
if let Some(timing) = self.timing.as_ref() {
|
||||
timing.record_count("publication_points_seen", 1);
|
||||
}
|
||||
crate::logging::progress::emit!(
|
||||
"publication_point_start",
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": ca.manifest_rsync_uri,
|
||||
"publication_point_rsync_uri": ca.publication_point_rsync_uri,
|
||||
"rsync_base_uri": ca.rsync_base_uri,
|
||||
"rrdp_notification_uri": ca.rrdp_notification_uri,
|
||||
}),
|
||||
);
|
||||
|
||||
let attempted_rrdp =
|
||||
self.policy.sync_preference == crate::validation::policy::SyncPreference::RrdpThenRsync;
|
||||
let repo_started = std::time::Instant::now();
|
||||
let (repo_sync_ok, repo_sync_error, repo_sync_source, repo_sync_phase, runtime_duration) =
|
||||
if let Some(runtime) = self.repo_sync_runtime.as_ref() {
|
||||
let outcome = runtime.sync_publication_point_repo(ca)?;
|
||||
(
|
||||
outcome.repo_sync_ok,
|
||||
outcome.repo_sync_err,
|
||||
outcome.repo_sync_source,
|
||||
outcome.repo_sync_phase,
|
||||
Some(outcome.repo_sync_duration_ms),
|
||||
)
|
||||
} else {
|
||||
match sync_publication_point(
|
||||
self.store,
|
||||
self.policy,
|
||||
if attempted_rrdp {
|
||||
ca.rrdp_notification_uri.as_deref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
&ca.rsync_base_uri,
|
||||
self.http_fetcher,
|
||||
self.rsync_fetcher,
|
||||
self.timing.as_ref(),
|
||||
self.download_log.as_ref(),
|
||||
) {
|
||||
Ok(result) => (
|
||||
true,
|
||||
None,
|
||||
Some(repo_sync_source_label(result.source).to_string()),
|
||||
Some(repo_sync_phase_label(result.phase).to_string()),
|
||||
None,
|
||||
),
|
||||
Err(error) => (
|
||||
false,
|
||||
Some(error.to_string()),
|
||||
None,
|
||||
Some(
|
||||
repo_sync_failure_phase_label(
|
||||
attempted_rrdp,
|
||||
ca.rrdp_notification_uri.as_deref(),
|
||||
ca.rrdp_notification_uri.as_deref(),
|
||||
)
|
||||
.to_string(),
|
||||
),
|
||||
None,
|
||||
),
|
||||
}
|
||||
};
|
||||
let repo_sync_duration_ms = effective_repo_sync_duration_ms(
|
||||
repo_started.elapsed().as_millis() as u64,
|
||||
runtime_duration,
|
||||
repo_sync_ok,
|
||||
);
|
||||
crate::logging::progress::emit!(
|
||||
"publication_point_repo_sync_done",
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": ca.manifest_rsync_uri,
|
||||
"repo_sync_ok": repo_sync_ok,
|
||||
"repo_sync_source": repo_sync_source,
|
||||
"repo_sync_phase": repo_sync_phase,
|
||||
"repo_sync_error": repo_sync_error,
|
||||
"repo_sync_duration_ms": repo_sync_duration_ms,
|
||||
}),
|
||||
);
|
||||
|
||||
let stage = self.stage_fresh_publication_point_after_repo_ready(
|
||||
ca,
|
||||
repo_sync_ok,
|
||||
repo_sync_error.as_deref(),
|
||||
);
|
||||
let stage = match stage {
|
||||
Ok(stage) => stage,
|
||||
Err(stage_error) => {
|
||||
let mut warnings = Vec::new();
|
||||
warnings.push(
|
||||
Warning::new(format!("publication point processing failed: {}", stage_error.error))
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
let audit = build_publication_point_audit_from_failed_fetch(
|
||||
ca,
|
||||
repo_sync_source.as_deref(),
|
||||
repo_sync_phase.as_deref(),
|
||||
Some(repo_sync_duration_ms),
|
||||
repo_sync_error.as_deref(),
|
||||
&warnings,
|
||||
&stage_error.error,
|
||||
);
|
||||
crate::logging::progress::emit!(
|
||||
"repo_terminal_failure",
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": ca.manifest_rsync_uri,
|
||||
"repo_sync_source": repo_sync_source,
|
||||
"repo_sync_phase": repo_sync_phase,
|
||||
"repo_sync_error": repo_sync_error,
|
||||
"repo_sync_duration_ms": repo_sync_duration_ms,
|
||||
"terminal_state": "failed_no_cache",
|
||||
"error": stage_error.error.to_string(),
|
||||
}),
|
||||
);
|
||||
return Ok(PublicationPointRunResult {
|
||||
source: PublicationPointSource::FailedFetchNoCache,
|
||||
snapshot: None,
|
||||
warnings,
|
||||
objects: empty_objects_output(),
|
||||
audit,
|
||||
discovered_children: Vec::new(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let ta_constraints = self.policy.ta_constraints.for_tal(&ca.tal_id);
|
||||
let object_started = std::time::Instant::now();
|
||||
let mut objects = if let Some(constraints) = ta_constraints {
|
||||
crate::validation::objects::process_publication_point_for_issuer_with_ta_constraints(
|
||||
&stage.fresh_point,
|
||||
self.policy,
|
||||
stage.issuer_ca_der.as_ref(),
|
||||
ca.ca_certificate_rsync_uri.as_deref(),
|
||||
ca.effective_ip_resources.as_ref(),
|
||||
ca.effective_as_resources.as_ref(),
|
||||
self.validation_time,
|
||||
self.timing.as_ref(),
|
||||
Some(constraints),
|
||||
)
|
||||
} else if let Some(pool) = self.parallel_roa_worker_pool.as_ref() {
|
||||
process_publication_point_for_issuer_parallel_roa_with_pool(
|
||||
&stage.fresh_point,
|
||||
self.policy,
|
||||
stage.issuer_ca_der.as_ref(),
|
||||
ca.ca_certificate_rsync_uri.as_deref(),
|
||||
ca.effective_ip_resources.as_ref(),
|
||||
ca.effective_as_resources.as_ref(),
|
||||
self.validation_time,
|
||||
self.timing.as_ref(),
|
||||
pool,
|
||||
)
|
||||
} else if let Some(config) = self.parallel_phase2_config.as_ref() {
|
||||
process_publication_point_for_issuer_parallel_roa(
|
||||
&stage.fresh_point,
|
||||
self.policy,
|
||||
stage.issuer_ca_der.as_ref(),
|
||||
ca.ca_certificate_rsync_uri.as_deref(),
|
||||
ca.effective_ip_resources.as_ref(),
|
||||
ca.effective_as_resources.as_ref(),
|
||||
self.validation_time,
|
||||
self.timing.as_ref(),
|
||||
config,
|
||||
)
|
||||
} else {
|
||||
crate::validation::objects::process_publication_point_for_issuer(
|
||||
&stage.fresh_point,
|
||||
self.policy,
|
||||
stage.issuer_ca_der.as_ref(),
|
||||
ca.ca_certificate_rsync_uri.as_deref(),
|
||||
ca.effective_ip_resources.as_ref(),
|
||||
ca.effective_as_resources.as_ref(),
|
||||
self.validation_time,
|
||||
self.timing.as_ref(),
|
||||
)
|
||||
};
|
||||
let object_ms = object_started.elapsed().as_millis() as u64;
|
||||
self.record_publication_point_step_ms(&ca.manifest_rsync_uri, "objects_processing", object_ms);
|
||||
objects.router_keys.extend(stage.discovered_router_keys.clone());
|
||||
|
||||
let finalized = self.finalize_fresh_publication_point_from_reducer(
|
||||
ca,
|
||||
&stage.fresh_point,
|
||||
stage.warnings,
|
||||
objects,
|
||||
stage.child_audits,
|
||||
stage.discovered_children,
|
||||
repo_sync_source.as_deref(),
|
||||
repo_sync_phase.as_deref(),
|
||||
repo_sync_duration_ms,
|
||||
repo_sync_error.as_deref(),
|
||||
)?;
|
||||
let result = finalized.result;
|
||||
let total_ms = started.elapsed().as_millis() as u64;
|
||||
crate::logging::progress::emit!(
|
||||
"publication_point_finish",
|
||||
serde_json::json!({
|
||||
"manifest_rsync_uri": ca.manifest_rsync_uri,
|
||||
"repo_sync_source": repo_sync_source,
|
||||
"repo_sync_phase": repo_sync_phase,
|
||||
"repo_sync_duration_ms": repo_sync_duration_ms,
|
||||
"total_duration_ms": total_ms,
|
||||
"objects_processing_ms": object_ms,
|
||||
"warning_count": result.warnings.len(),
|
||||
"vrp_count": result.objects.vrps.len(),
|
||||
"aspa_count": result.objects.aspas.len(),
|
||||
"child_count": result.discovered_children.len(),
|
||||
}),
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
23
src/validation/tree_runner/timing.rs
Normal file
23
src/validation/tree_runner/timing.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
pub(crate) fn record_publication_point_total_ms(&self, manifest_rsync_uri: &str, ms: u64) {
|
||||
if let Some(timing) = self.timing.as_ref() {
|
||||
timing.record_publication_point_nanos(manifest_rsync_uri, ms.saturating_mul(1_000_000));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_publication_point_step_ms(
|
||||
&self,
|
||||
manifest_rsync_uri: &str,
|
||||
step: &'static str,
|
||||
ms: u64,
|
||||
) {
|
||||
if let Some(timing) = self.timing.as_ref() {
|
||||
timing.record_publication_point_step_nanos(
|
||||
manifest_rsync_uri,
|
||||
step,
|
||||
ms.saturating_mul(1_000_000),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
42
src/validation/tree_runner/types.rs
Normal file
42
src/validation/tree_runner/types.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct FreshPublicationPointStage {
|
||||
pub(crate) fresh_point: FreshValidatedPublicationPoint,
|
||||
pub(crate) issuer_ca_der: Arc<[u8]>,
|
||||
pub(crate) snapshot_prepare_timing: FreshPublicationPointTimingBreakdown,
|
||||
pub(crate) snapshot_prepare_ms: u64,
|
||||
pub(crate) discovered_children: Vec<DiscoveredChildCaInstance>,
|
||||
pub(crate) child_audits: Vec<ObjectAuditEntry>,
|
||||
pub(crate) discovered_router_keys: Vec<RouterKeyPayload>,
|
||||
pub(crate) child_discovery_ms: u64,
|
||||
pub(crate) warnings: Vec<Warning>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FreshPublicationPointStageError {
|
||||
pub(crate) error: ManifestFreshError,
|
||||
pub(crate) snapshot_prepare_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct FreshPublicationPointFinalizeOutput {
|
||||
pub(crate) result: PublicationPointRunResult,
|
||||
pub(crate) snapshot_pack_ms: u64,
|
||||
pub(crate) ccr_projection_build_ms: u64,
|
||||
pub(crate) ccr_append_ms: u64,
|
||||
pub(crate) audit_build_ms: u64,
|
||||
}
|
||||
|
||||
pub struct Rpkiv1PublicationPointRunner<'a> {
|
||||
pub store: &'a RocksStore,
|
||||
pub policy: &'a Policy,
|
||||
pub http_fetcher: &'a dyn Fetcher,
|
||||
pub rsync_fetcher: &'a dyn RsyncFetcher,
|
||||
pub validation_time: time::OffsetDateTime,
|
||||
pub timing: Option<TimingHandle>,
|
||||
pub download_log: Option<DownloadLogHandle>,
|
||||
pub current_repo_index: Option<CurrentRepoIndexHandle>,
|
||||
pub repo_sync_runtime: Option<Arc<dyn RepoSyncRuntime>>,
|
||||
pub parallel_phase2_config: Option<ParallelPhase2Config>,
|
||||
pub parallel_roa_worker_pool: Option<ParallelRoaWorkerPool>,
|
||||
pub ccr_accumulator: Option<Mutex<CcrAccumulator>>,
|
||||
}
|
||||
85
src/validation/x509_name.rs
Normal file
85
src/validation/x509_name.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use crate::model::common::X509NameDer;
|
||||
use x509_parser::prelude::FromDer;
|
||||
|
||||
fn canonicalize(name: &X509NameDer) -> Option<String> {
|
||||
let (rem, parsed) = x509_parser::x509::X509Name::from_der(name.as_raw()).ok()?;
|
||||
if !rem.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(parsed.to_string())
|
||||
}
|
||||
|
||||
/// Compare two X.509 distinguished names using a tolerant semantic comparison.
|
||||
///
|
||||
/// RPKI repositories in the wild sometimes encode the same name using different
|
||||
/// ASN.1 string types (e.g., PrintableString vs UTF8String) while remaining
|
||||
/// semantically equivalent. RFC 5280 path validation requires name matching, but
|
||||
/// DER byte equality is too strict for interoperability.
|
||||
pub fn x509_names_equivalent(a: &X509NameDer, b: &X509NameDer) -> bool {
|
||||
let Some(ca) = canonicalize(a) else {
|
||||
return a == b;
|
||||
};
|
||||
let Some(cb) = canonicalize(b) else {
|
||||
return a == b;
|
||||
};
|
||||
ca == cb
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn synthetic_certificate_der() -> Vec<u8> {
|
||||
std::fs::read(
|
||||
crate::test_support::synthetic_repository()
|
||||
.case_repository("baseline-v1")
|
||||
.join("root/child.cer"),
|
||||
)
|
||||
.expect("read synthetic child certificate")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x509_names_equivalent_falls_back_to_der_equality_when_parse_fails() {
|
||||
// Invalid tag (not a SEQUENCE) makes x509-parser fail and forces DER equality fallback.
|
||||
let a = X509NameDer(vec![0x01, 0x00]);
|
||||
let b = X509NameDer(vec![0x01, 0x00]);
|
||||
assert!(x509_names_equivalent(&a, &b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x509_names_equivalent_compares_semantic_names_when_parse_succeeds() {
|
||||
let cert_der = synthetic_certificate_der();
|
||||
|
||||
let (_rem, cert) =
|
||||
x509_parser::parse_x509_certificate(&cert_der).expect("parse certificate fixture");
|
||||
|
||||
let subject = X509NameDer(cert.tbs_certificate.subject.as_raw().to_vec());
|
||||
let issuer = X509NameDer(cert.tbs_certificate.issuer.as_raw().to_vec());
|
||||
|
||||
assert!(x509_names_equivalent(&subject, &subject));
|
||||
assert!(!x509_names_equivalent(&subject, &issuer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x509_names_equivalent_falls_back_when_name_has_trailing_bytes() {
|
||||
// Use a real name DER and append a trailing byte so parsing yields leftover `rem`.
|
||||
let cert_der = synthetic_certificate_der();
|
||||
let (_rem, cert) =
|
||||
x509_parser::parse_x509_certificate(&cert_der).expect("parse certificate fixture");
|
||||
let mut name = cert.tbs_certificate.subject.as_raw().to_vec();
|
||||
name.push(0x00);
|
||||
let a = X509NameDer(name.clone());
|
||||
let b = X509NameDer(name);
|
||||
assert!(x509_names_equivalent(&a, &b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x509_names_equivalent_falls_back_when_one_side_fails_to_parse() {
|
||||
let cert_der = synthetic_certificate_der();
|
||||
let (_rem, cert) =
|
||||
x509_parser::parse_x509_certificate(&cert_der).expect("parse certificate fixture");
|
||||
let good = X509NameDer(cert.tbs_certificate.subject.as_raw().to_vec());
|
||||
let bad = X509NameDer(vec![0x01, 0x00]);
|
||||
assert!(!x509_names_equivalent(&good, &bad));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user