4302 lines
153 KiB
Rust
4302 lines
153 KiB
Rust
use crate::analysis::timing::TimingHandle;
|
|
use crate::audit::{AuditObjectKind, AuditObjectResult, ObjectAuditEntry, sha256_hex_from_32};
|
|
use crate::data_model::aspa::{AspaDecodeError, AspaObject, AspaValidateError};
|
|
use crate::data_model::manifest::ManifestObject;
|
|
use crate::data_model::rc::{
|
|
AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressOrRange, IpPrefix as RcIpPrefix,
|
|
ResourceCertificate,
|
|
};
|
|
use crate::data_model::roa::{IpPrefix, RoaAfi, RoaDecodeError, RoaObject, RoaValidateError};
|
|
use crate::data_model::signed_object::SignedObjectVerifyError;
|
|
use crate::parallel::config::ParallelPhase2Config;
|
|
use crate::parallel::object_worker::{
|
|
ObjectTaskExecutor, ObjectWorkerPool, ObjectWorkerSubmitError,
|
|
};
|
|
use crate::policy::{Policy, SignedObjectFailurePolicy};
|
|
use crate::report::{RfcRef, Warning};
|
|
use crate::storage::{
|
|
PackFile, PackTime, RoaCacheProjection, ValidatedCaInstanceResult, VcirArtifactKind,
|
|
VcirArtifactRole, VcirArtifactValidationStatus, VcirLocalOutput, VcirLocalOutputPayload,
|
|
VcirOutputType, VcirSourceObjectType,
|
|
};
|
|
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
|
|
use crate::validation::manifest::PublicationPointData;
|
|
use crate::validation::publication_point::PublicationPointSnapshot;
|
|
use sha2::Digest;
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::{Duration, Instant};
|
|
use x509_parser::prelude::FromDer;
|
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
|
|
|
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 sha256_hex_to_32(hex_value: &str) -> [u8; 32] {
|
|
let bytes = hex::decode(hex_value).expect("internal sha256 hex should decode");
|
|
let mut out = [0u8; 32];
|
|
out.copy_from_slice(&bytes);
|
|
out
|
|
}
|
|
|
|
fn sha256_hex(bytes: &[u8]) -> String {
|
|
hex::encode(sha2::Sha256::digest(bytes))
|
|
}
|
|
|
|
fn decode_resource_certificate_with_policy(
|
|
der: &[u8],
|
|
policy: &Policy,
|
|
) -> Result<ResourceCertificate, crate::data_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 {
|
|
crl: crate::data_model::crl::RpkixCrl,
|
|
revoked_serials: std::collections::HashSet<Vec<u8>>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) enum CachedIssuerCrl {
|
|
Pending(Vec<u8>),
|
|
Ok(Arc<VerifiedIssuerCrl>),
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(crate) struct IssuerResourcesIndex {
|
|
ip_v4: Option<Vec<(Vec<u8>, Vec<u8>)>>,
|
|
ip_v6: Option<Vec<(Vec<u8>, Vec<u8>)>>,
|
|
asnum: Option<Vec<(u32, u32)>>,
|
|
rdi: Option<Vec<(u32, u32)>>,
|
|
}
|
|
|
|
fn extra_rfc_refs_for_crl_selection(e: &ObjectValidateError) -> &'static [RfcRef] {
|
|
match e {
|
|
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 local_outputs_cache: Vec<VcirLocalOutput>,
|
|
pub warnings: Vec<Warning>,
|
|
pub stats: ObjectsStats,
|
|
pub audit: Vec<ObjectAuditEntry>,
|
|
pub roa_cache_stats: RoaValidationCacheStats,
|
|
}
|
|
|
|
#[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,
|
|
/// Whether this publication point was dropped due to an unrecoverable objects-processing error
|
|
/// (e.g., missing issuer CRL in the pack, or `signed_object_failure_policy=drop_publication_point`).
|
|
pub publication_point_dropped: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
|
|
pub struct RoaValidationCacheStats {
|
|
pub enabled_publication_points: usize,
|
|
pub vcir_hit_publication_points: usize,
|
|
pub vcir_miss_publication_points: usize,
|
|
pub hit_roas: usize,
|
|
pub miss_roas: usize,
|
|
pub blocked_roas: usize,
|
|
pub fresh_roas: usize,
|
|
pub context_gate_nanos: u64,
|
|
pub lookup_nanos: u64,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct RoaValidationCacheInput<'a> {
|
|
enabled: bool,
|
|
view: Option<&'a RoaValidationCacheView>,
|
|
}
|
|
|
|
impl<'a> RoaValidationCacheInput<'a> {
|
|
pub fn disabled() -> Self {
|
|
Self {
|
|
enabled: false,
|
|
view: None,
|
|
}
|
|
}
|
|
|
|
pub fn enabled(view: Option<&'a RoaValidationCacheView>) -> Self {
|
|
Self {
|
|
enabled: true,
|
|
view,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RoaValidationCacheStats {
|
|
pub fn add_assign(&mut self, other: &Self) {
|
|
self.enabled_publication_points += other.enabled_publication_points;
|
|
self.vcir_hit_publication_points += other.vcir_hit_publication_points;
|
|
self.vcir_miss_publication_points += other.vcir_miss_publication_points;
|
|
self.hit_roas += other.hit_roas;
|
|
self.miss_roas += other.miss_roas;
|
|
self.blocked_roas += other.blocked_roas;
|
|
self.fresh_roas += other.fresh_roas;
|
|
self.context_gate_nanos = self
|
|
.context_gate_nanos
|
|
.saturating_add(other.context_gate_nanos);
|
|
self.lookup_nanos = self.lookup_nanos.saturating_add(other.lookup_nanos);
|
|
}
|
|
|
|
fn for_input(input: RoaValidationCacheInput<'_>, roa_total: usize) -> Self {
|
|
let mut stats = Self::default();
|
|
if !input.enabled {
|
|
return stats;
|
|
}
|
|
|
|
stats.enabled_publication_points = 1;
|
|
if input.view.is_some() {
|
|
stats.vcir_hit_publication_points = 1;
|
|
} else {
|
|
stats.vcir_miss_publication_points = 1;
|
|
stats.miss_roas = roa_total;
|
|
stats.fresh_roas = roa_total;
|
|
}
|
|
stats
|
|
}
|
|
|
|
fn record_to_timing(&self, timing: Option<&TimingHandle>) {
|
|
let Some(timing) = timing else {
|
|
return;
|
|
};
|
|
record_non_zero(
|
|
timing,
|
|
"roa_validation_cache_enabled_publication_points",
|
|
self.enabled_publication_points,
|
|
);
|
|
record_non_zero(
|
|
timing,
|
|
"roa_validation_cache_vcir_hit_publication_points",
|
|
self.vcir_hit_publication_points,
|
|
);
|
|
record_non_zero(
|
|
timing,
|
|
"roa_validation_cache_vcir_miss_publication_points",
|
|
self.vcir_miss_publication_points,
|
|
);
|
|
record_non_zero(timing, "roa_validation_cache_hit_roas", self.hit_roas);
|
|
record_non_zero(timing, "roa_validation_cache_miss_roas", self.miss_roas);
|
|
record_non_zero(
|
|
timing,
|
|
"roa_validation_cache_blocked_roas",
|
|
self.blocked_roas,
|
|
);
|
|
record_non_zero(timing, "roa_validation_cache_fresh_roas", self.fresh_roas);
|
|
record_non_zero(
|
|
timing,
|
|
"roa_validation_cache_context_gate_nanos",
|
|
self.context_gate_nanos as usize,
|
|
);
|
|
record_non_zero(
|
|
timing,
|
|
"roa_validation_cache_lookup_nanos",
|
|
self.lookup_nanos as usize,
|
|
);
|
|
timing.record_phase_nanos(
|
|
"roa_validation_cache_context_gate_total",
|
|
self.context_gate_nanos,
|
|
);
|
|
timing.record_phase_nanos("roa_validation_cache_lookup_total", self.lookup_nanos);
|
|
}
|
|
}
|
|
|
|
fn record_non_zero(timing: &TimingHandle, key: &'static str, value: usize) {
|
|
if value > 0 {
|
|
timing.record_count(key, value as u64);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct RoaValidationCacheView {
|
|
entries_by_uri: HashMap<String, CachedRoaValidationResult>,
|
|
issuer_ca_sha256_hex: Option<String>,
|
|
crl_sha256_by_uri: HashMap<String, String>,
|
|
blocked: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct CachedRoaValidationResult {
|
|
source_object_hash: [u8; 32],
|
|
outputs: Vec<VcirLocalOutput>,
|
|
}
|
|
|
|
impl RoaValidationCacheView {
|
|
pub fn from_projection(
|
|
projection: &RoaCacheProjection,
|
|
validation_time: time::OffsetDateTime,
|
|
) -> Self {
|
|
let mut entries_by_uri: HashMap<String, CachedRoaValidationResult> =
|
|
HashMap::with_capacity(projection.entries.len());
|
|
let issuer_ca_sha256_hex = projection.issuer_ca_sha256_hex.clone();
|
|
let crl_sha256_by_uri = projection
|
|
.crl_sha256_by_uri
|
|
.iter()
|
|
.map(|crl| (crl.uri.clone(), crl.sha256.clone()))
|
|
.collect::<HashMap<_, _>>();
|
|
let blocked = projection
|
|
.instance_effective_until
|
|
.parse()
|
|
.map(|effective_until| effective_until <= validation_time)
|
|
.unwrap_or(true);
|
|
|
|
if blocked {
|
|
return Self {
|
|
entries_by_uri,
|
|
issuer_ca_sha256_hex,
|
|
crl_sha256_by_uri,
|
|
blocked,
|
|
};
|
|
}
|
|
|
|
for entry in &projection.entries {
|
|
let outputs = entry
|
|
.outputs
|
|
.iter()
|
|
.map(|output| VcirLocalOutput {
|
|
output_type: VcirOutputType::Vrp,
|
|
item_effective_until: output.item_effective_until.clone(),
|
|
source_object_uri: entry.source_object_uri.clone(),
|
|
source_object_type: VcirSourceObjectType::Roa,
|
|
source_object_hash: entry.source_object_hash,
|
|
source_ee_cert_hash: output.source_ee_cert_hash,
|
|
payload: output.payload.clone(),
|
|
rule_hash: output.rule_hash,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
entries_by_uri.insert(
|
|
entry.source_object_uri.clone(),
|
|
CachedRoaValidationResult {
|
|
source_object_hash: entry.source_object_hash,
|
|
outputs,
|
|
},
|
|
);
|
|
}
|
|
|
|
Self {
|
|
entries_by_uri,
|
|
issuer_ca_sha256_hex,
|
|
crl_sha256_by_uri,
|
|
blocked,
|
|
}
|
|
}
|
|
|
|
pub fn from_vcir(
|
|
vcir: &ValidatedCaInstanceResult,
|
|
validation_time: time::OffsetDateTime,
|
|
) -> Self {
|
|
let mut entries_by_uri: HashMap<String, CachedRoaValidationResult> = HashMap::new();
|
|
let mut issuer_ca_sha256_hex: Option<String> = None;
|
|
let mut crl_sha256_by_uri: HashMap<String, String> = HashMap::new();
|
|
let blocked = vcir
|
|
.instance_gate
|
|
.instance_effective_until
|
|
.parse()
|
|
.map(|effective_until| effective_until <= validation_time)
|
|
.unwrap_or(true);
|
|
|
|
if blocked {
|
|
return Self {
|
|
entries_by_uri,
|
|
issuer_ca_sha256_hex,
|
|
crl_sha256_by_uri,
|
|
blocked,
|
|
};
|
|
}
|
|
|
|
for artifact in &vcir.related_artifacts {
|
|
if artifact.validation_status != VcirArtifactValidationStatus::Accepted {
|
|
continue;
|
|
}
|
|
match (artifact.artifact_role, artifact.artifact_kind) {
|
|
(
|
|
VcirArtifactRole::IssuerCert | VcirArtifactRole::TrustAnchorCert,
|
|
VcirArtifactKind::Cer,
|
|
) => {
|
|
issuer_ca_sha256_hex = Some(artifact.sha256.clone());
|
|
}
|
|
(_, VcirArtifactKind::Crl) => {
|
|
if let Some(uri) = artifact.uri.as_ref() {
|
|
crl_sha256_by_uri.insert(uri.clone(), artifact.sha256.clone());
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
for output in &vcir.local_outputs {
|
|
if output.output_type != VcirOutputType::Vrp
|
|
|| output.source_object_type != VcirSourceObjectType::Roa
|
|
{
|
|
continue;
|
|
}
|
|
entries_by_uri
|
|
.entry(output.source_object_uri.clone())
|
|
.or_insert_with(|| CachedRoaValidationResult {
|
|
source_object_hash: output.source_object_hash,
|
|
outputs: Vec::new(),
|
|
})
|
|
.outputs
|
|
.push(output.clone());
|
|
}
|
|
|
|
Self {
|
|
entries_by_uri,
|
|
issuer_ca_sha256_hex,
|
|
crl_sha256_by_uri,
|
|
blocked,
|
|
}
|
|
}
|
|
|
|
fn matches_current_context(&self, issuer_ca_der: &[u8], locked_files: &[PackFile]) -> bool {
|
|
if self.blocked {
|
|
return false;
|
|
}
|
|
|
|
let Some(expected_issuer_hash) = self.issuer_ca_sha256_hex.as_ref() else {
|
|
return false;
|
|
};
|
|
if expected_issuer_hash != &sha256_hex(issuer_ca_der) {
|
|
return false;
|
|
}
|
|
|
|
let current_crl_hashes = locked_files
|
|
.iter()
|
|
.filter(|file| file.rsync_uri.ends_with(".crl"))
|
|
.map(|file| (file.rsync_uri.clone(), sha256_hex_from_32(&file.sha256)))
|
|
.collect::<HashMap<_, _>>();
|
|
self.crl_sha256_by_uri == current_crl_hashes
|
|
}
|
|
|
|
fn lookup(
|
|
&self,
|
|
file: &PackFile,
|
|
validation_time: time::OffsetDateTime,
|
|
) -> RoaCacheLookupResult {
|
|
if self.blocked {
|
|
return RoaCacheLookupResult::Blocked;
|
|
}
|
|
|
|
let Some(cached) = self.entries_by_uri.get(file.rsync_uri.as_str()) else {
|
|
return RoaCacheLookupResult::Miss;
|
|
};
|
|
if cached.source_object_hash != file.sha256 {
|
|
return RoaCacheLookupResult::Blocked;
|
|
}
|
|
if cached.outputs.is_empty() {
|
|
return RoaCacheLookupResult::Miss;
|
|
}
|
|
if cached.outputs.iter().any(|output| {
|
|
output
|
|
.item_effective_until
|
|
.parse()
|
|
.map_or(true, |t| t <= validation_time)
|
|
}) {
|
|
return RoaCacheLookupResult::Blocked;
|
|
}
|
|
|
|
let mut vrps = Vec::with_capacity(cached.outputs.len());
|
|
for output in &cached.outputs {
|
|
let VcirLocalOutputPayload::Vrp {
|
|
asn,
|
|
afi,
|
|
prefix_len,
|
|
addr,
|
|
max_length,
|
|
} = &output.payload
|
|
else {
|
|
return RoaCacheLookupResult::Blocked;
|
|
};
|
|
vrps.push(Vrp {
|
|
asn: *asn,
|
|
prefix: IpPrefix {
|
|
afi: *afi,
|
|
prefix_len: *prefix_len,
|
|
addr: *addr,
|
|
},
|
|
max_length: *max_length,
|
|
});
|
|
}
|
|
|
|
RoaCacheLookupResult::Hit(RoaTaskOk {
|
|
vrps,
|
|
local_outputs: cached.outputs.clone(),
|
|
reused_from_cache: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn active_roa_cache_view<'a>(
|
|
roa_cache: RoaValidationCacheInput<'a>,
|
|
issuer_ca_der: &[u8],
|
|
locked_files: &[PackFile],
|
|
stats: &mut RoaValidationCacheStats,
|
|
roa_total: usize,
|
|
) -> Option<&'a RoaValidationCacheView> {
|
|
let view = roa_cache.view?;
|
|
let gate_started = Instant::now();
|
|
if view.matches_current_context(issuer_ca_der, locked_files) {
|
|
stats.context_gate_nanos = stats
|
|
.context_gate_nanos
|
|
.saturating_add(gate_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);
|
|
Some(view)
|
|
} else {
|
|
stats.context_gate_nanos = stats
|
|
.context_gate_nanos
|
|
.saturating_add(gate_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);
|
|
stats.blocked_roas += roa_total;
|
|
stats.fresh_roas += roa_total;
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum RoaCacheLookupResult {
|
|
Hit(RoaTaskOk),
|
|
Miss,
|
|
Blocked,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct RoaTask<'a> {
|
|
pub(crate) index: usize,
|
|
pub(crate) file: &'a PackFile,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct RoaTaskOk {
|
|
pub(crate) vrps: Vec<Vrp>,
|
|
pub(crate) local_outputs: Vec<VcirLocalOutput>,
|
|
pub(crate) reused_from_cache: bool,
|
|
}
|
|
|
|
#[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>,
|
|
}
|
|
|
|
/// Process objects from a publication point snapshot using a known issuer CA certificate
|
|
/// and its effective resources (resolved via the resource-path, RFC 6487 §7.2).
|
|
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::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
) -> ObjectsOutput {
|
|
process_publication_point_for_issuer_with_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
true,
|
|
)
|
|
}
|
|
|
|
pub fn process_publication_point_for_issuer_with_options<P: PublicationPointData>(
|
|
publication_point: &P,
|
|
policy: &Policy,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
collect_vcir_local_outputs: bool,
|
|
) -> ObjectsOutput {
|
|
process_publication_point_for_issuer_with_cache_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
RoaValidationCacheInput::disabled(),
|
|
)
|
|
}
|
|
|
|
pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPointData>(
|
|
publication_point: &P,
|
|
policy: &Policy,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
collect_vcir_local_outputs: bool,
|
|
roa_cache: RoaValidationCacheInput<'_>,
|
|
) -> ObjectsOutput {
|
|
let manifest_rsync_uri = publication_point.manifest_rsync_uri();
|
|
let manifest_bytes = publication_point.manifest_bytes();
|
|
let locked_files = publication_point.files();
|
|
let mut warnings: Vec<Warning> = Vec::new();
|
|
let mut stats = ObjectsStats::default();
|
|
stats.roa_total = locked_files
|
|
.iter()
|
|
.filter(|f| f.rsync_uri.ends_with(".roa"))
|
|
.count();
|
|
stats.aspa_total = locked_files
|
|
.iter()
|
|
.filter(|f| f.rsync_uri.ends_with(".asa"))
|
|
.count();
|
|
let mut roa_cache_stats = RoaValidationCacheStats::for_input(roa_cache, stats.roa_total);
|
|
let mut audit: Vec<ObjectAuditEntry> = Vec::new();
|
|
|
|
// Enforce that `manifest_bytes` is actually a manifest object.
|
|
let _manifest = match ManifestObject::decode_der_with_strict_options(
|
|
manifest_bytes,
|
|
policy.strict.cms_der,
|
|
policy.strict.name,
|
|
) {
|
|
Ok(manifest) => manifest,
|
|
Err(e) => {
|
|
stats.publication_point_dropped = true;
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point: manifest decode failed: {e}"
|
|
))
|
|
.with_rfc_refs(&[
|
|
RfcRef("RFC 9286 §4"),
|
|
RfcRef("RFC 9286 §6.2"),
|
|
RfcRef("RFC 9286 §6.6"),
|
|
])
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
for f in locked_files {
|
|
if f.rsync_uri.ends_with(".roa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Roa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some("skipped: manifest decode failed".to_string()),
|
|
});
|
|
} else if f.rsync_uri.ends_with(".asa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Aspa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some("skipped: manifest decode failed".to_string()),
|
|
});
|
|
}
|
|
}
|
|
return ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
};
|
|
}
|
|
};
|
|
|
|
// Decode issuer CA once; if it fails we cannot validate ROA/ASPA EE certificates.
|
|
let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
stats.publication_point_dropped = true;
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point: issuer CA decode failed: {e}"
|
|
))
|
|
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2"), RfcRef("RFC 5280 §6.1")])
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
for f in locked_files {
|
|
if f.rsync_uri.ends_with(".roa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Roa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some("skipped: issuer CA decode failed".to_string()),
|
|
});
|
|
} else if f.rsync_uri.ends_with(".asa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Aspa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some("skipped: issuer CA decode failed".to_string()),
|
|
});
|
|
}
|
|
}
|
|
return ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
};
|
|
}
|
|
};
|
|
|
|
// Parse issuer SubjectPublicKeyInfo once and reuse for all EE certificate signature checks.
|
|
let issuer_spki = match SubjectPublicKeyInfo::from_der(&issuer_ca.tbs.subject_public_key_info) {
|
|
Ok((rem, spki)) if rem.is_empty() => spki,
|
|
Ok((rem, _)) => {
|
|
stats.publication_point_dropped = true;
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point: trailing bytes after issuer SPKI DER: {} bytes",
|
|
rem.len()
|
|
))
|
|
.with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
return ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
};
|
|
}
|
|
Err(e) => {
|
|
stats.publication_point_dropped = true;
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point: issuer SPKI parse failed: {e}"
|
|
))
|
|
.with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
return ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
};
|
|
}
|
|
};
|
|
|
|
let mut crl_cache: std::collections::HashMap<String, CachedIssuerCrl> = locked_files
|
|
.iter()
|
|
.filter(|f| f.rsync_uri.ends_with(".crl"))
|
|
.map(|f| {
|
|
let bytes = f
|
|
.bytes_cloned()
|
|
.expect("snapshot CRL bytes must be loadable");
|
|
(f.rsync_uri.clone(), CachedIssuerCrl::Pending(bytes))
|
|
})
|
|
.collect();
|
|
|
|
let issuer_resources_index =
|
|
build_issuer_resources_index(issuer_effective_ip, issuer_effective_as);
|
|
|
|
// If the snapshot has signed objects but no CRLs at all, we cannot validate any embedded EE
|
|
// certificate paths deterministically (EE CRLDP must reference an rsync URI in the snapshot).
|
|
if crl_cache.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_rsync_uri),
|
|
);
|
|
for f in locked_files {
|
|
if f.rsync_uri.ends_with(".roa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Roa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some(
|
|
"skipped due to missing CRL files in validated publication point"
|
|
.to_string(),
|
|
),
|
|
});
|
|
} else if f.rsync_uri.ends_with(".asa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Aspa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some(
|
|
"skipped due to missing CRL files in validated publication point"
|
|
.to_string(),
|
|
),
|
|
});
|
|
}
|
|
}
|
|
return ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
};
|
|
}
|
|
|
|
let mut vrps: Vec<Vrp> = Vec::new();
|
|
let mut aspas: Vec<AspaAttestation> = Vec::new();
|
|
let mut local_outputs_cache: Vec<VcirLocalOutput> = Vec::new();
|
|
let active_cache_view = active_roa_cache_view(
|
|
roa_cache,
|
|
issuer_ca_der,
|
|
locked_files,
|
|
&mut roa_cache_stats,
|
|
stats.roa_total,
|
|
);
|
|
|
|
for (idx, file) in locked_files.iter().enumerate() {
|
|
if file.rsync_uri.ends_with(".roa") {
|
|
let result = if let Some(cache_view) = active_cache_view {
|
|
let lookup_started = Instant::now();
|
|
let lookup_result = cache_view.lookup(file, validation_time);
|
|
roa_cache_stats.lookup_nanos = roa_cache_stats.lookup_nanos.saturating_add(
|
|
lookup_started
|
|
.elapsed()
|
|
.as_nanos()
|
|
.min(u128::from(u64::MAX)) as u64,
|
|
);
|
|
match lookup_result {
|
|
RoaCacheLookupResult::Hit(ok) => {
|
|
roa_cache_stats.hit_roas += 1;
|
|
RoaTaskResult {
|
|
publication_point_id: 0,
|
|
index: idx,
|
|
worker_index: 0,
|
|
queue_wait_ms: 0,
|
|
worker_ms: 0,
|
|
outcome: Ok(ok),
|
|
}
|
|
}
|
|
RoaCacheLookupResult::Miss => {
|
|
roa_cache_stats.miss_roas += 1;
|
|
roa_cache_stats.fresh_roas += 1;
|
|
let task = RoaTask { index: idx, file };
|
|
let _t = timing.as_ref().map(|t| t.span_phase("objects_roa_total"));
|
|
validate_roa_task_serial(
|
|
task,
|
|
manifest_rsync_uri,
|
|
issuer_ca_der,
|
|
&issuer_ca,
|
|
&issuer_spki,
|
|
issuer_ca_rsync_uri,
|
|
&mut crl_cache,
|
|
&issuer_resources_index,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
policy.strict.cms_der,
|
|
policy.strict.name,
|
|
)
|
|
}
|
|
RoaCacheLookupResult::Blocked => {
|
|
roa_cache_stats.blocked_roas += 1;
|
|
roa_cache_stats.fresh_roas += 1;
|
|
let task = RoaTask { index: idx, file };
|
|
let _t = timing.as_ref().map(|t| t.span_phase("objects_roa_total"));
|
|
validate_roa_task_serial(
|
|
task,
|
|
manifest_rsync_uri,
|
|
issuer_ca_der,
|
|
&issuer_ca,
|
|
&issuer_spki,
|
|
issuer_ca_rsync_uri,
|
|
&mut crl_cache,
|
|
&issuer_resources_index,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
policy.strict.cms_der,
|
|
policy.strict.name,
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
let task = RoaTask { index: idx, file };
|
|
let _t = timing.as_ref().map(|t| t.span_phase("objects_roa_total"));
|
|
validate_roa_task_serial(
|
|
task,
|
|
manifest_rsync_uri,
|
|
issuer_ca_der,
|
|
&issuer_ca,
|
|
&issuer_spki,
|
|
issuer_ca_rsync_uri,
|
|
&mut crl_cache,
|
|
&issuer_resources_index,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
policy.strict.cms_der,
|
|
policy.strict.name,
|
|
)
|
|
};
|
|
match result.outcome {
|
|
Ok(mut ok) => {
|
|
stats.roa_ok += 1;
|
|
vrps.append(&mut ok.vrps);
|
|
if collect_vcir_local_outputs || ok.reused_from_cache {
|
|
local_outputs_cache.extend(ok.local_outputs);
|
|
}
|
|
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,
|
|
});
|
|
}
|
|
Err(e) => 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(e.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(&e));
|
|
warnings.push(
|
|
Warning::new(format!("dropping invalid ROA: {}: {e}", file.rsync_uri))
|
|
.with_rfc_refs(&refs)
|
|
.with_context(&file.rsync_uri),
|
|
)
|
|
}
|
|
SignedObjectFailurePolicy::DropPublicationPoint => {
|
|
stats.publication_point_dropped = true;
|
|
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(e.to_string()),
|
|
});
|
|
for f in locked_files.iter().skip(idx + 1) {
|
|
if f.rsync_uri.ends_with(".roa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Roa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some(
|
|
"skipped due to policy=signed_object_failure_policy=drop_publication_point"
|
|
.to_string(),
|
|
),
|
|
});
|
|
} else if f.rsync_uri.ends_with(".asa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Aspa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some(
|
|
"skipped due to policy=signed_object_failure_policy=drop_publication_point"
|
|
.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(&e));
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point due to invalid ROA: {}: {e}",
|
|
file.rsync_uri
|
|
))
|
|
.with_rfc_refs(&refs)
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
return ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
};
|
|
}
|
|
},
|
|
}
|
|
} else if file.rsync_uri.ends_with(".asa") {
|
|
let _t = timing.as_ref().map(|t| t.span_phase("objects_aspa_total"));
|
|
match process_aspa_with_issuer(
|
|
file,
|
|
manifest_rsync_uri,
|
|
issuer_ca_der,
|
|
&issuer_ca,
|
|
&issuer_spki,
|
|
issuer_ca_rsync_uri,
|
|
&mut crl_cache,
|
|
&issuer_resources_index,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
policy.strict.cms_der,
|
|
policy.strict.name,
|
|
) {
|
|
Ok((att, local_output)) => {
|
|
stats.aspa_ok += 1;
|
|
aspas.push(att);
|
|
if let Some(local_output) = local_output {
|
|
local_outputs_cache.push(local_output);
|
|
}
|
|
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,
|
|
});
|
|
}
|
|
Err(e) => 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(e.to_string()),
|
|
});
|
|
let mut refs = vec![RfcRef("RFC 6488 §3")];
|
|
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
|
|
warnings.push(
|
|
Warning::new(format!("dropping invalid ASPA: {}: {e}", file.rsync_uri))
|
|
.with_rfc_refs(&refs)
|
|
.with_context(&file.rsync_uri),
|
|
)
|
|
}
|
|
SignedObjectFailurePolicy::DropPublicationPoint => {
|
|
stats.publication_point_dropped = true;
|
|
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(e.to_string()),
|
|
});
|
|
for f in locked_files.iter().skip(idx + 1) {
|
|
if f.rsync_uri.ends_with(".roa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Roa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some(
|
|
"skipped due to policy=signed_object_failure_policy=drop_publication_point"
|
|
.to_string(),
|
|
),
|
|
});
|
|
} else if f.rsync_uri.ends_with(".asa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Aspa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some(
|
|
"skipped due to policy=signed_object_failure_policy=drop_publication_point"
|
|
.to_string(),
|
|
),
|
|
});
|
|
}
|
|
}
|
|
let mut refs = vec![RfcRef("RFC 6488 §3")];
|
|
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point due to invalid ASPA: {}: {e}",
|
|
file.rsync_uri
|
|
))
|
|
.with_rfc_refs(&refs)
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
return ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
};
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
roa_cache_stats.record_to_timing(timing);
|
|
|
|
ObjectsOutput {
|
|
vrps,
|
|
aspas,
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache,
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
}
|
|
}
|
|
|
|
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::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
config: &ParallelPhase2Config,
|
|
) -> ObjectsOutput {
|
|
process_publication_point_for_issuer_parallel_roa_with_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
config,
|
|
true,
|
|
)
|
|
}
|
|
|
|
pub fn process_publication_point_for_issuer_parallel_roa_with_options<P: PublicationPointData>(
|
|
publication_point: &P,
|
|
policy: &Policy,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
config: &ParallelPhase2Config,
|
|
collect_vcir_local_outputs: bool,
|
|
) -> ObjectsOutput {
|
|
process_publication_point_for_issuer_parallel_roa_with_cache_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
config,
|
|
collect_vcir_local_outputs,
|
|
RoaValidationCacheInput::disabled(),
|
|
)
|
|
}
|
|
|
|
pub fn process_publication_point_for_issuer_parallel_roa_with_cache_options<
|
|
P: PublicationPointData,
|
|
>(
|
|
publication_point: &P,
|
|
policy: &Policy,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
config: &ParallelPhase2Config,
|
|
collect_vcir_local_outputs: bool,
|
|
roa_cache: RoaValidationCacheInput<'_>,
|
|
) -> ObjectsOutput {
|
|
if config.object_workers <= 1
|
|
|| policy.signed_object_failure_policy == SignedObjectFailurePolicy::DropPublicationPoint
|
|
{
|
|
return process_publication_point_for_issuer_with_cache_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
roa_cache,
|
|
);
|
|
}
|
|
|
|
let pool = match ParallelRoaWorkerPool::new(config) {
|
|
Ok(pool) => pool,
|
|
Err(_) => {
|
|
return process_publication_point_for_issuer_with_cache_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
roa_cache,
|
|
);
|
|
}
|
|
};
|
|
|
|
process_publication_point_for_issuer_parallel_roa_with_pool_cache_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
&pool,
|
|
collect_vcir_local_outputs,
|
|
roa_cache,
|
|
)
|
|
}
|
|
|
|
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::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
pool: &ParallelRoaWorkerPool,
|
|
) -> ObjectsOutput {
|
|
process_publication_point_for_issuer_parallel_roa_with_pool_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
pool,
|
|
true,
|
|
)
|
|
}
|
|
|
|
pub fn process_publication_point_for_issuer_parallel_roa_with_pool_options<
|
|
P: PublicationPointData,
|
|
>(
|
|
publication_point: &P,
|
|
policy: &Policy,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
pool: &ParallelRoaWorkerPool,
|
|
collect_vcir_local_outputs: bool,
|
|
) -> ObjectsOutput {
|
|
process_publication_point_for_issuer_parallel_roa_with_pool_cache_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
pool,
|
|
collect_vcir_local_outputs,
|
|
RoaValidationCacheInput::disabled(),
|
|
)
|
|
}
|
|
|
|
pub fn process_publication_point_for_issuer_parallel_roa_with_pool_cache_options<
|
|
P: PublicationPointData,
|
|
>(
|
|
publication_point: &P,
|
|
policy: &Policy,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
pool: &ParallelRoaWorkerPool,
|
|
collect_vcir_local_outputs: bool,
|
|
roa_cache: RoaValidationCacheInput<'_>,
|
|
) -> ObjectsOutput {
|
|
if policy.signed_object_failure_policy == SignedObjectFailurePolicy::DropPublicationPoint {
|
|
return process_publication_point_for_issuer_with_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
);
|
|
}
|
|
|
|
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,
|
|
collect_vcir_local_outputs,
|
|
roa_cache,
|
|
)
|
|
.unwrap_or_else(|_| {
|
|
process_publication_point_for_issuer_with_cache_options(
|
|
publication_point,
|
|
policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
roa_cache,
|
|
)
|
|
})
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct RoaTaskShared {
|
|
locked_files: Arc<[PackFile]>,
|
|
manifest_rsync_uri: Arc<str>,
|
|
issuer_ca_der: Arc<[u8]>,
|
|
issuer_ca: Arc<ResourceCertificate>,
|
|
issuer_spki_der: Arc<[u8]>,
|
|
issuer_ca_rsync_uri: Option<Arc<str>>,
|
|
crl_cache: Arc<Mutex<std::collections::HashMap<String, CachedIssuerCrl>>>,
|
|
issuer_resources_index: Arc<IssuerResourcesIndex>,
|
|
issuer_effective_ip: Option<Arc<crate::data_model::rc::IpResourceSet>>,
|
|
issuer_effective_as: Option<Arc<crate::data_model::rc::AsResourceSet>>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct OwnedRoaTask {
|
|
pub(crate) publication_point_id: u64,
|
|
index: usize,
|
|
shared: Arc<RoaTaskShared>,
|
|
validation_time: time::OffsetDateTime,
|
|
collect_vcir_local_outputs: bool,
|
|
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>>,
|
|
}
|
|
|
|
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 = task
|
|
.shared
|
|
.locked_files
|
|
.get(task.index)
|
|
.expect("ROA task index must reference locked file");
|
|
let issuer_spki = match SubjectPublicKeyInfo::from_der(shared.issuer_spki_der.as_ref()) {
|
|
Ok((rem, spki)) if rem.is_empty() => spki,
|
|
Ok((rem, _)) => {
|
|
return 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: Err(ObjectValidateError::CertPath(
|
|
CertPathError::IssuerSpkiTrailingBytes(rem.len()),
|
|
)),
|
|
};
|
|
}
|
|
Err(e) => {
|
|
return 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: Err(ObjectValidateError::CertPath(
|
|
CertPathError::IssuerSpkiParse(e.to_string()),
|
|
)),
|
|
};
|
|
}
|
|
};
|
|
let outcome = process_roa_with_issuer_parallel_cached(
|
|
file,
|
|
shared.manifest_rsync_uri.as_ref(),
|
|
shared.issuer_ca_der.as_ref(),
|
|
shared.issuer_ca.as_ref(),
|
|
&issuer_spki,
|
|
shared.issuer_ca_rsync_uri.as_deref(),
|
|
shared.crl_cache.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.collect_vcir_local_outputs,
|
|
task.strict_cms_der,
|
|
task.strict_name,
|
|
)
|
|
.map(|(vrps, local_outputs)| RoaTaskOk {
|
|
vrps,
|
|
local_outputs,
|
|
reused_from_cache: false,
|
|
});
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
pub(crate) enum ParallelObjectsPrepare {
|
|
Complete(ObjectsOutput),
|
|
Staged(ParallelObjectsStage),
|
|
}
|
|
|
|
pub(crate) struct ParallelObjectsStage {
|
|
pub(crate) publication_point_id: u64,
|
|
shared: Arc<RoaTaskShared>,
|
|
validation_time: time::OffsetDateTime,
|
|
collect_vcir_local_outputs: bool,
|
|
strict_cms_der: bool,
|
|
strict_name: bool,
|
|
roa_task_indices: Vec<usize>,
|
|
cached_roa_results: Vec<RoaTaskResult>,
|
|
roa_cache_stats: RoaValidationCacheStats,
|
|
warnings: Vec<Warning>,
|
|
stats: ObjectsStats,
|
|
audit: Vec<ObjectAuditEntry>,
|
|
}
|
|
|
|
impl ParallelObjectsStage {
|
|
#[cfg(test)]
|
|
pub(crate) fn build_roa_tasks(&self) -> Vec<OwnedRoaTask> {
|
|
let mut tasks = Vec::with_capacity(self.roa_task_count());
|
|
self.extend_roa_tasks(|task| tasks.push(task));
|
|
tasks
|
|
}
|
|
|
|
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();
|
|
self.roa_task_indices.iter().for_each(|index| {
|
|
push(OwnedRoaTask {
|
|
publication_point_id: self.publication_point_id,
|
|
index: *index,
|
|
shared: shared.clone(),
|
|
validation_time: self.validation_time,
|
|
collect_vcir_local_outputs: self.collect_vcir_local_outputs,
|
|
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_with_cache<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::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
collect_vcir_local_outputs: bool,
|
|
roa_cache: RoaValidationCacheInput<'_>,
|
|
) -> ParallelObjectsPrepare {
|
|
let manifest_rsync_uri = publication_point.manifest_rsync_uri();
|
|
let manifest_bytes = publication_point.manifest_bytes();
|
|
let locked_files = publication_point.files();
|
|
let mut warnings: Vec<Warning> = Vec::new();
|
|
let mut stats = ObjectsStats::default();
|
|
stats.roa_total = locked_files
|
|
.iter()
|
|
.filter(|f| f.rsync_uri.ends_with(".roa"))
|
|
.count();
|
|
stats.aspa_total = locked_files
|
|
.iter()
|
|
.filter(|f| f.rsync_uri.ends_with(".asa"))
|
|
.count();
|
|
let mut roa_cache_stats = RoaValidationCacheStats::for_input(roa_cache, stats.roa_total);
|
|
let mut audit: Vec<ObjectAuditEntry> = Vec::new();
|
|
|
|
let _manifest = match ManifestObject::decode_der_with_strict_options(
|
|
manifest_bytes,
|
|
policy.strict.cms_der,
|
|
policy.strict.name,
|
|
) {
|
|
Ok(manifest) => manifest,
|
|
Err(e) => {
|
|
stats.publication_point_dropped = true;
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point: manifest decode failed: {e}"
|
|
))
|
|
.with_rfc_refs(&[
|
|
RfcRef("RFC 9286 §4"),
|
|
RfcRef("RFC 9286 §6.2"),
|
|
RfcRef("RFC 9286 §6.6"),
|
|
])
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
for f in locked_files {
|
|
if f.rsync_uri.ends_with(".roa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Roa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some("skipped: manifest decode failed".to_string()),
|
|
});
|
|
} else if f.rsync_uri.ends_with(".asa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Aspa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some("skipped: manifest decode failed".to_string()),
|
|
});
|
|
}
|
|
}
|
|
return ParallelObjectsPrepare::Complete(ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
});
|
|
}
|
|
};
|
|
|
|
let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
stats.publication_point_dropped = true;
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point: issuer CA decode failed: {e}"
|
|
))
|
|
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2"), RfcRef("RFC 5280 §6.1")])
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
for f in locked_files {
|
|
if f.rsync_uri.ends_with(".roa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Roa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some("skipped: issuer CA decode failed".to_string()),
|
|
});
|
|
} else if f.rsync_uri.ends_with(".asa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Aspa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some("skipped: issuer CA decode failed".to_string()),
|
|
});
|
|
}
|
|
}
|
|
return ParallelObjectsPrepare::Complete(ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
});
|
|
}
|
|
};
|
|
|
|
match SubjectPublicKeyInfo::from_der(&issuer_ca.tbs.subject_public_key_info) {
|
|
Ok((rem, _)) if rem.is_empty() => {}
|
|
Ok((rem, _)) => {
|
|
stats.publication_point_dropped = true;
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point: trailing bytes after issuer SPKI DER: {} bytes",
|
|
rem.len()
|
|
))
|
|
.with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
return ParallelObjectsPrepare::Complete(ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
stats.publication_point_dropped = true;
|
|
warnings.push(
|
|
Warning::new(format!(
|
|
"dropping publication point: issuer SPKI parse failed: {e}"
|
|
))
|
|
.with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
|
|
.with_context(manifest_rsync_uri),
|
|
);
|
|
return ParallelObjectsPrepare::Complete(ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
});
|
|
}
|
|
}
|
|
|
|
let crl_cache: std::collections::HashMap<String, CachedIssuerCrl> = locked_files
|
|
.iter()
|
|
.filter(|f| f.rsync_uri.ends_with(".crl"))
|
|
.map(|f| {
|
|
let bytes = f
|
|
.bytes_cloned()
|
|
.expect("snapshot CRL bytes must be loadable");
|
|
(f.rsync_uri.clone(), CachedIssuerCrl::Pending(bytes))
|
|
})
|
|
.collect();
|
|
|
|
if crl_cache.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_rsync_uri),
|
|
);
|
|
for f in locked_files {
|
|
if f.rsync_uri.ends_with(".roa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Roa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some(
|
|
"skipped due to missing CRL files in validated publication point"
|
|
.to_string(),
|
|
),
|
|
});
|
|
} else if f.rsync_uri.ends_with(".asa") {
|
|
audit.push(ObjectAuditEntry {
|
|
rsync_uri: f.rsync_uri.clone(),
|
|
sha256_hex: sha256_hex_from_32(&f.sha256),
|
|
kind: AuditObjectKind::Aspa,
|
|
result: AuditObjectResult::Skipped,
|
|
detail: Some(
|
|
"skipped due to missing CRL files in validated publication point"
|
|
.to_string(),
|
|
),
|
|
});
|
|
}
|
|
}
|
|
return ParallelObjectsPrepare::Complete(ObjectsOutput {
|
|
vrps: Vec::new(),
|
|
aspas: Vec::new(),
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache: Vec::new(),
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
});
|
|
}
|
|
|
|
let active_cache_view = active_roa_cache_view(
|
|
roa_cache,
|
|
issuer_ca_der,
|
|
locked_files,
|
|
&mut roa_cache_stats,
|
|
stats.roa_total,
|
|
);
|
|
let mut roa_task_indices = Vec::new();
|
|
let mut cached_roa_results = Vec::new();
|
|
for (index, file) in locked_files.iter().enumerate() {
|
|
if !file.rsync_uri.ends_with(".roa") {
|
|
continue;
|
|
}
|
|
if let Some(cache_view) = active_cache_view {
|
|
let lookup_started = Instant::now();
|
|
let lookup_result = cache_view.lookup(file, validation_time);
|
|
roa_cache_stats.lookup_nanos = roa_cache_stats.lookup_nanos.saturating_add(
|
|
lookup_started
|
|
.elapsed()
|
|
.as_nanos()
|
|
.min(u128::from(u64::MAX)) as u64,
|
|
);
|
|
match lookup_result {
|
|
RoaCacheLookupResult::Hit(ok) => {
|
|
roa_cache_stats.hit_roas += 1;
|
|
cached_roa_results.push(RoaTaskResult {
|
|
publication_point_id,
|
|
index,
|
|
worker_index: usize::MAX,
|
|
queue_wait_ms: 0,
|
|
worker_ms: 0,
|
|
outcome: Ok(ok),
|
|
});
|
|
}
|
|
RoaCacheLookupResult::Miss => {
|
|
roa_cache_stats.miss_roas += 1;
|
|
roa_cache_stats.fresh_roas += 1;
|
|
roa_task_indices.push(index);
|
|
}
|
|
RoaCacheLookupResult::Blocked => {
|
|
roa_cache_stats.blocked_roas += 1;
|
|
roa_cache_stats.fresh_roas += 1;
|
|
roa_task_indices.push(index);
|
|
}
|
|
}
|
|
} else {
|
|
roa_task_indices.push(index);
|
|
}
|
|
}
|
|
|
|
ParallelObjectsPrepare::Staged(ParallelObjectsStage {
|
|
publication_point_id,
|
|
shared: Arc::new(RoaTaskShared {
|
|
locked_files: Arc::<[PackFile]>::from(locked_files.to_vec()),
|
|
manifest_rsync_uri: Arc::<str>::from(manifest_rsync_uri),
|
|
issuer_ca_der: Arc::<[u8]>::from(issuer_ca_der.to_vec()),
|
|
issuer_spki_der: Arc::<[u8]>::from(issuer_ca.tbs.subject_public_key_info.clone()),
|
|
issuer_ca: Arc::new(issuer_ca),
|
|
issuer_ca_rsync_uri: issuer_ca_rsync_uri.map(Arc::<str>::from),
|
|
crl_cache: Arc::new(Mutex::new(crl_cache)),
|
|
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),
|
|
}),
|
|
validation_time,
|
|
collect_vcir_local_outputs,
|
|
strict_cms_der: policy.strict.cms_der,
|
|
strict_name: policy.strict.name,
|
|
roa_task_indices,
|
|
cached_roa_results,
|
|
roa_cache_stats,
|
|
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.cached_roa_results);
|
|
roa_results.sort_by_key(|result| result.index);
|
|
let mut roa_results = roa_results.into_iter().peekable();
|
|
let shared = stage.shared.clone();
|
|
let mut aspa_crl_cache = shared
|
|
.crl_cache
|
|
.lock()
|
|
.expect("parallel ROA CRL cache lock")
|
|
.clone();
|
|
let issuer_spki = SubjectPublicKeyInfo::from_der(shared.issuer_spki_der.as_ref())
|
|
.map_err(|e| e.to_string())?
|
|
.1;
|
|
let collect_vcir_local_outputs = stage.collect_vcir_local_outputs;
|
|
let validation_time = stage.validation_time;
|
|
let strict_cms_der = stage.strict_cms_der;
|
|
let strict_name = stage.strict_name;
|
|
let roa_cache_stats = stage.roa_cache_stats;
|
|
let mut stats = stage.stats;
|
|
let mut warnings = stage.warnings;
|
|
let mut audit = stage.audit;
|
|
let mut vrps: Vec<Vrp> = Vec::new();
|
|
let mut aspas: Vec<AspaAttestation> = Vec::new();
|
|
let mut local_outputs_cache: Vec<VcirLocalOutput> = Vec::new();
|
|
|
|
for (idx, file) in shared.locked_files.iter().enumerate() {
|
|
if file.rsync_uri.ends_with(".roa") {
|
|
let result = match roa_results.peek() {
|
|
Some(result) if result.index == idx => roa_results
|
|
.next()
|
|
.expect("peeked ROA task result must be present"),
|
|
Some(result) => {
|
|
return Err(format!(
|
|
"unexpected ROA task result index {} while reducing {} at index {}",
|
|
result.index, file.rsync_uri, idx
|
|
));
|
|
}
|
|
None => {
|
|
return Err(format!("missing ROA task result for {}", file.rsync_uri));
|
|
}
|
|
};
|
|
match result.outcome {
|
|
Ok(mut ok) => {
|
|
stats.roa_ok += 1;
|
|
vrps.append(&mut ok.vrps);
|
|
if collect_vcir_local_outputs || ok.reused_from_cache {
|
|
local_outputs_cache.extend(ok.local_outputs);
|
|
}
|
|
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,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
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(e.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(&e));
|
|
warnings.push(
|
|
Warning::new(format!("dropping invalid ROA: {}: {e}", file.rsync_uri))
|
|
.with_rfc_refs(&refs)
|
|
.with_context(&file.rsync_uri),
|
|
)
|
|
}
|
|
}
|
|
} else if file.rsync_uri.ends_with(".asa") {
|
|
let _t = timing.as_ref().map(|t| t.span_phase("objects_aspa_total"));
|
|
match process_aspa_with_issuer(
|
|
file,
|
|
shared.manifest_rsync_uri.as_ref(),
|
|
shared.issuer_ca_der.as_ref(),
|
|
shared.issuer_ca.as_ref(),
|
|
&issuer_spki,
|
|
shared.issuer_ca_rsync_uri.as_deref(),
|
|
&mut aspa_crl_cache,
|
|
shared.issuer_resources_index.as_ref(),
|
|
shared.issuer_effective_ip.as_deref(),
|
|
shared.issuer_effective_as.as_deref(),
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
strict_cms_der,
|
|
strict_name,
|
|
) {
|
|
Ok((att, local_output)) => {
|
|
stats.aspa_ok += 1;
|
|
aspas.push(att);
|
|
if let Some(local_output) = local_output {
|
|
local_outputs_cache.push(local_output);
|
|
}
|
|
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,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
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(e.to_string()),
|
|
});
|
|
let mut refs = vec![RfcRef("RFC 6488 §3")];
|
|
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
|
|
warnings.push(
|
|
Warning::new(format!("dropping invalid ASPA: {}: {e}", file.rsync_uri))
|
|
.with_rfc_refs(&refs)
|
|
.with_context(&file.rsync_uri),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(result) = roa_results.next() {
|
|
return Err(format!(
|
|
"unexpected trailing ROA task result at index {}",
|
|
result.index
|
|
));
|
|
}
|
|
|
|
roa_cache_stats.record_to_timing(timing);
|
|
|
|
Ok(ObjectsOutput {
|
|
vrps,
|
|
aspas,
|
|
router_keys: Vec::new(),
|
|
local_outputs_cache,
|
|
warnings,
|
|
stats,
|
|
audit,
|
|
roa_cache_stats,
|
|
})
|
|
}
|
|
|
|
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::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
pool: &ParallelRoaWorkerPool,
|
|
collect_vcir_local_outputs: bool,
|
|
roa_cache: RoaValidationCacheInput<'_>,
|
|
) -> Result<ObjectsOutput, String> {
|
|
let stage = match prepare_publication_point_for_parallel_roa_with_cache(
|
|
0,
|
|
publication_point,
|
|
_policy,
|
|
issuer_ca_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
collect_vcir_local_outputs,
|
|
roa_cache,
|
|
) {
|
|
ParallelObjectsPrepare::Complete(out) => return Ok(out),
|
|
ParallelObjectsPrepare::Staged(stage) => stage,
|
|
};
|
|
|
|
let roa_task_count = stage.roa_task_count();
|
|
let mut pending = std::collections::VecDeque::with_capacity(roa_task_count);
|
|
stage.append_roa_tasks_to(&mut pending);
|
|
let mut worker_pool = pool
|
|
.pool
|
|
.lock()
|
|
.map_err(|_| "parallel ROA worker pool lock poisoned".to_string())?;
|
|
|
|
while let Some(task) = pending.pop_front() {
|
|
match worker_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 roa_results = Vec::with_capacity(roa_task_count);
|
|
while roa_results.len() < roa_task_count {
|
|
let Some(result) = worker_pool.recv_result_timeout(Duration::from_secs(30))? else {
|
|
return Err("parallel ROA worker timed out".to_string());
|
|
};
|
|
roa_results.push(result);
|
|
}
|
|
drop(worker_pool);
|
|
|
|
reduce_parallel_roa_stage(stage, roa_results, timing)
|
|
}
|
|
/// Compatibility wrapper that processes a publication point snapshot.
|
|
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::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_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::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_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,
|
|
)
|
|
}
|
|
|
|
#[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 (cannot select issuer CRL) (RFC 6487 §4.8.6)"
|
|
)]
|
|
MissingCrlDpUris,
|
|
|
|
#[error(
|
|
"no CRL available in publication point snapshot (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
|
|
)]
|
|
MissingCrlInPack,
|
|
|
|
#[error(
|
|
"CRL referenced by CRLDistributionPoints not found in publication point snapshot: {0} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)"
|
|
)]
|
|
CrlNotFound(String),
|
|
|
|
#[error(
|
|
"issuer effective IP resources missing (cannot validate EE IP resources subset) (RFC 6487 §7.2; RFC 3779 §2.3)"
|
|
)]
|
|
MissingIssuerEffectiveIp,
|
|
|
|
#[error(
|
|
"issuer effective AS resources missing (cannot validate EE AS resources subset) (RFC 6487 §7.2; RFC 3779 §3.3)"
|
|
)]
|
|
MissingIssuerEffectiveAs,
|
|
|
|
#[error(
|
|
"EE certificate resources are not a subset of issuer effective resources (RFC 6487 §7.2; RFC 3779)"
|
|
)]
|
|
EeResourcesNotSubset,
|
|
}
|
|
|
|
pub(crate) fn validate_roa_task_serial(
|
|
task: RoaTask<'_>,
|
|
manifest_rsync_uri: &str,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca: &ResourceCertificate,
|
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
|
|
issuer_resources_index: &IssuerResourcesIndex,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
collect_vcir_local_outputs: bool,
|
|
strict_cms_der: bool,
|
|
strict_name: bool,
|
|
) -> RoaTaskResult {
|
|
let outcome = process_roa_with_issuer(
|
|
task.file,
|
|
manifest_rsync_uri,
|
|
issuer_ca_der,
|
|
issuer_ca,
|
|
issuer_spki,
|
|
issuer_ca_rsync_uri,
|
|
crl_cache,
|
|
issuer_resources_index,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
timing,
|
|
collect_vcir_local_outputs,
|
|
strict_cms_der,
|
|
strict_name,
|
|
)
|
|
.map(|(vrps, local_outputs)| RoaTaskOk {
|
|
vrps,
|
|
local_outputs,
|
|
reused_from_cache: false,
|
|
});
|
|
|
|
RoaTaskResult {
|
|
publication_point_id: 0,
|
|
index: task.index,
|
|
worker_index: 0,
|
|
queue_wait_ms: 0,
|
|
worker_ms: 0,
|
|
outcome,
|
|
}
|
|
}
|
|
|
|
fn process_roa_with_issuer(
|
|
file: &PackFile,
|
|
_manifest_rsync_uri: &str,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca: &ResourceCertificate,
|
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
|
|
issuer_resources_index: &IssuerResourcesIndex,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
collect_vcir_local_outputs: bool,
|
|
strict_cms_der: bool,
|
|
strict_name: bool,
|
|
) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>), ObjectValidateError> {
|
|
let _decode = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
|
|
let roa = RoaObject::decode_der_with_strict_options(
|
|
file.bytes().map_err(ObjectValidateError::BytesLoad)?,
|
|
strict_cms_der,
|
|
strict_name,
|
|
)?;
|
|
drop(_decode);
|
|
|
|
let _ee_profile = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_validate_embedded_ee_total"));
|
|
roa.validate_embedded_ee_cert()?;
|
|
drop(_ee_profile);
|
|
|
|
let _verify = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_verify_signature_total"));
|
|
roa.signed_object.verify()?;
|
|
drop(_verify);
|
|
|
|
let ee = &roa.signed_object.signed_data.certificates[0];
|
|
let ee_crldp_uris = ee
|
|
.resource_cert
|
|
.tbs
|
|
.extensions
|
|
.crl_distribution_points_uris
|
|
.as_ref();
|
|
let issuer_crl_rsync_uri = choose_crl_uri_for_certificate(ee_crldp_uris, crl_cache)?;
|
|
let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?;
|
|
|
|
let _cert_path = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_validate_ee_cert_path_total"));
|
|
validate_signed_object_ee_cert_path_fast(
|
|
ee,
|
|
issuer_ca,
|
|
issuer_spki,
|
|
&verified_crl.crl,
|
|
&verified_crl.revoked_serials,
|
|
issuer_ca_rsync_uri,
|
|
Some(issuer_crl_rsync_uri),
|
|
validation_time,
|
|
)?;
|
|
drop(_cert_path);
|
|
|
|
let _subset = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total"));
|
|
validate_ee_resources_subset(
|
|
&ee.resource_cert,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
issuer_resources_index,
|
|
)?;
|
|
drop(_subset);
|
|
|
|
let vrps = roa_to_vrps(&roa);
|
|
if !collect_vcir_local_outputs {
|
|
return Ok((vrps, Vec::new()));
|
|
}
|
|
let source_object_hash = sha256_hex_from_32(&file.sha256);
|
|
let source_ee_cert_hash = crate::audit::sha256_hex(ee.raw_der.as_slice());
|
|
let item_effective_until =
|
|
PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
|
|
let local_outputs = vrps
|
|
.iter()
|
|
.map(|vrp| {
|
|
let prefix = vrp_prefix_to_string(vrp);
|
|
let rule_hash = crate::audit::sha256_hex(
|
|
format!(
|
|
"roa-rule:{}:{}:{}:{}",
|
|
source_object_hash, vrp.asn, prefix, vrp.max_length
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
VcirLocalOutput {
|
|
output_type: VcirOutputType::Vrp,
|
|
item_effective_until: item_effective_until.clone(),
|
|
source_object_uri: file.rsync_uri.clone(),
|
|
source_object_type: VcirSourceObjectType::Roa,
|
|
source_object_hash: file.sha256,
|
|
source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
|
|
payload: VcirLocalOutputPayload::Vrp {
|
|
asn: vrp.asn,
|
|
afi: vrp.prefix.afi,
|
|
prefix_len: vrp.prefix.prefix_len,
|
|
addr: vrp.prefix.addr,
|
|
max_length: vrp.max_length,
|
|
},
|
|
rule_hash: sha256_hex_to_32(&rule_hash),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok((vrps, local_outputs))
|
|
}
|
|
|
|
fn process_roa_with_issuer_parallel_cached(
|
|
file: &PackFile,
|
|
_manifest_rsync_uri: &str,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca: &ResourceCertificate,
|
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
crl_cache: &Mutex<std::collections::HashMap<String, CachedIssuerCrl>>,
|
|
issuer_resources_index: &IssuerResourcesIndex,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
collect_vcir_local_outputs: bool,
|
|
strict_cms_der: bool,
|
|
strict_name: bool,
|
|
) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>), ObjectValidateError> {
|
|
let _decode = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
|
|
let roa = RoaObject::decode_der_with_strict_options(
|
|
file.bytes().map_err(ObjectValidateError::BytesLoad)?,
|
|
strict_cms_der,
|
|
strict_name,
|
|
)?;
|
|
drop(_decode);
|
|
|
|
let _ee_profile = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_validate_embedded_ee_total"));
|
|
roa.validate_embedded_ee_cert()?;
|
|
drop(_ee_profile);
|
|
|
|
let _verify = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_verify_signature_total"));
|
|
roa.signed_object.verify()?;
|
|
drop(_verify);
|
|
|
|
let ee = &roa.signed_object.signed_data.certificates[0];
|
|
let ee_crldp_uris = ee
|
|
.resource_cert
|
|
.tbs
|
|
.extensions
|
|
.crl_distribution_points_uris
|
|
.as_ref();
|
|
let (issuer_crl_rsync_uri, verified_crl) = {
|
|
let mut crl_cache = crl_cache.lock().expect("parallel ROA CRL cache lock");
|
|
let issuer_crl_rsync_uri =
|
|
choose_crl_uri_for_certificate(ee_crldp_uris, &crl_cache)?.to_string();
|
|
let verified_crl =
|
|
ensure_issuer_crl_verified(&issuer_crl_rsync_uri, &mut crl_cache, issuer_ca_der)?;
|
|
(issuer_crl_rsync_uri, verified_crl)
|
|
};
|
|
|
|
let _cert_path = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_validate_ee_cert_path_total"));
|
|
validate_signed_object_ee_cert_path_fast(
|
|
ee,
|
|
issuer_ca,
|
|
issuer_spki,
|
|
&verified_crl.crl,
|
|
&verified_crl.revoked_serials,
|
|
issuer_ca_rsync_uri,
|
|
Some(issuer_crl_rsync_uri.as_str()),
|
|
validation_time,
|
|
)?;
|
|
drop(_cert_path);
|
|
|
|
let _subset = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total"));
|
|
validate_ee_resources_subset(
|
|
&ee.resource_cert,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
issuer_resources_index,
|
|
)?;
|
|
drop(_subset);
|
|
|
|
let vrps = roa_to_vrps(&roa);
|
|
if !collect_vcir_local_outputs {
|
|
return Ok((vrps, Vec::new()));
|
|
}
|
|
let source_object_hash = sha256_hex_from_32(&file.sha256);
|
|
let source_ee_cert_hash = crate::audit::sha256_hex(ee.raw_der.as_slice());
|
|
let item_effective_until =
|
|
PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
|
|
let local_outputs = vrps
|
|
.iter()
|
|
.map(|vrp| {
|
|
let prefix = vrp_prefix_to_string(vrp);
|
|
let rule_hash = crate::audit::sha256_hex(
|
|
format!(
|
|
"roa-rule:{}:{}:{}:{}",
|
|
source_object_hash, vrp.asn, prefix, vrp.max_length
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
VcirLocalOutput {
|
|
output_type: VcirOutputType::Vrp,
|
|
item_effective_until: item_effective_until.clone(),
|
|
source_object_uri: file.rsync_uri.clone(),
|
|
source_object_type: VcirSourceObjectType::Roa,
|
|
source_object_hash: file.sha256,
|
|
source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
|
|
payload: VcirLocalOutputPayload::Vrp {
|
|
asn: vrp.asn,
|
|
afi: vrp.prefix.afi,
|
|
prefix_len: vrp.prefix.prefix_len,
|
|
addr: vrp.prefix.addr,
|
|
max_length: vrp.max_length,
|
|
},
|
|
rule_hash: sha256_hex_to_32(&rule_hash),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok((vrps, local_outputs))
|
|
}
|
|
|
|
fn process_aspa_with_issuer(
|
|
file: &PackFile,
|
|
_manifest_rsync_uri: &str,
|
|
issuer_ca_der: &[u8],
|
|
issuer_ca: &ResourceCertificate,
|
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
|
|
issuer_resources_index: &IssuerResourcesIndex,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
timing: Option<&TimingHandle>,
|
|
collect_vcir_local_outputs: bool,
|
|
strict_cms_der: bool,
|
|
strict_name: bool,
|
|
) -> Result<(AspaAttestation, Option<VcirLocalOutput>), ObjectValidateError> {
|
|
let _decode = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_aspa_decode_and_validate_total"));
|
|
let aspa = AspaObject::decode_der_with_strict_options(
|
|
file.bytes().map_err(ObjectValidateError::BytesLoad)?,
|
|
strict_cms_der,
|
|
strict_name,
|
|
)?;
|
|
drop(_decode);
|
|
|
|
let _ee_profile = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_aspa_validate_embedded_ee_total"));
|
|
aspa.validate_embedded_ee_cert()?;
|
|
drop(_ee_profile);
|
|
|
|
let _verify = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_aspa_verify_signature_total"));
|
|
aspa.signed_object.verify()?;
|
|
drop(_verify);
|
|
|
|
let ee = &aspa.signed_object.signed_data.certificates[0];
|
|
let ee_crldp_uris = ee
|
|
.resource_cert
|
|
.tbs
|
|
.extensions
|
|
.crl_distribution_points_uris
|
|
.as_ref();
|
|
let issuer_crl_rsync_uri = choose_crl_uri_for_certificate(ee_crldp_uris, crl_cache)?;
|
|
let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?;
|
|
|
|
let _cert_path = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_aspa_validate_ee_cert_path_total"));
|
|
validate_signed_object_ee_cert_path_fast(
|
|
ee,
|
|
issuer_ca,
|
|
issuer_spki,
|
|
&verified_crl.crl,
|
|
&verified_crl.revoked_serials,
|
|
issuer_ca_rsync_uri,
|
|
Some(issuer_crl_rsync_uri),
|
|
validation_time,
|
|
)?;
|
|
drop(_cert_path);
|
|
|
|
let _subset = timing
|
|
.as_ref()
|
|
.map(|t| t.span_phase("objects_aspa_validate_ee_resources_subset_total"));
|
|
validate_ee_resources_subset(
|
|
&ee.resource_cert,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
issuer_resources_index,
|
|
)?;
|
|
drop(_subset);
|
|
|
|
let attestation = AspaAttestation {
|
|
customer_as_id: aspa.aspa.customer_as_id,
|
|
provider_as_ids: aspa.aspa.provider_as_ids.clone(),
|
|
};
|
|
if !collect_vcir_local_outputs {
|
|
return Ok((attestation, None));
|
|
}
|
|
let source_object_hash = sha256_hex_from_32(&file.sha256);
|
|
let source_ee_cert_hash = crate::audit::sha256_hex(ee.raw_der.as_slice());
|
|
let item_effective_until =
|
|
PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
|
|
let providers = attestation
|
|
.provider_as_ids
|
|
.iter()
|
|
.map(u32::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(",");
|
|
let rule_hash = crate::audit::sha256_hex(
|
|
format!(
|
|
"aspa-rule:{}:{}:{}",
|
|
source_object_hash, attestation.customer_as_id, providers
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
let local_output = VcirLocalOutput {
|
|
output_type: VcirOutputType::Aspa,
|
|
item_effective_until,
|
|
source_object_uri: file.rsync_uri.clone(),
|
|
source_object_type: VcirSourceObjectType::Aspa,
|
|
source_object_hash: file.sha256,
|
|
source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
|
|
payload: VcirLocalOutputPayload::Aspa {
|
|
customer_as_id: attestation.customer_as_id,
|
|
provider_as_ids: attestation.provider_as_ids.clone(),
|
|
},
|
|
rule_hash: sha256_hex_to_32(&rule_hash),
|
|
};
|
|
|
|
Ok((attestation, Some(local_output)))
|
|
}
|
|
|
|
fn vrp_prefix_to_string(vrp: &Vrp) -> String {
|
|
let prefix = &vrp.prefix;
|
|
match prefix.afi {
|
|
RoaAfi::Ipv4 => {
|
|
let addr = std::net::Ipv4Addr::new(
|
|
prefix.addr[0],
|
|
prefix.addr[1],
|
|
prefix.addr[2],
|
|
prefix.addr[3],
|
|
);
|
|
format!("{addr}/{}", prefix.prefix_len)
|
|
}
|
|
RoaAfi::Ipv6 => {
|
|
let mut octets = [0u8; 16];
|
|
octets.copy_from_slice(&prefix.addr[..16]);
|
|
let addr = std::net::Ipv6Addr::from(octets);
|
|
format!("{addr}/{}", prefix.prefix_len)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn choose_crl_uri_for_certificate<'a>(
|
|
crldp_uris: Option<&'a Vec<String>>,
|
|
crl_cache: &std::collections::HashMap<String, CachedIssuerCrl>,
|
|
) -> Result<&'a str, ObjectValidateError> {
|
|
if crl_cache.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_cache.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<'a>(
|
|
crl_rsync_uri: &str,
|
|
crl_cache: &'a mut std::collections::HashMap<String, CachedIssuerCrl>,
|
|
issuer_ca_der: &[u8],
|
|
) -> Result<Arc<VerifiedIssuerCrl>, CertPathError> {
|
|
let entry = crl_cache
|
|
.get_mut(crl_rsync_uri)
|
|
.expect("CRL must exist in cache");
|
|
match entry {
|
|
CachedIssuerCrl::Ok(v) => Ok(Arc::clone(v)),
|
|
CachedIssuerCrl::Pending(bytes) => {
|
|
let der = std::mem::take(bytes);
|
|
let crl = crate::data_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 = CachedIssuerCrl::Ok(Arc::new(VerifiedIssuerCrl {
|
|
crl,
|
|
revoked_serials,
|
|
}));
|
|
match entry {
|
|
CachedIssuerCrl::Ok(v) => Ok(Arc::clone(v)),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_ee_resources_subset(
|
|
ee: &ResourceCertificate,
|
|
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_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(())
|
|
}
|
|
|
|
fn as_resources_is_subset(child: &AsResourceSet, parent: &AsResourceSet) -> bool {
|
|
as_choice_subset(child.asnum.as_ref(), parent.asnum.as_ref())
|
|
&& as_choice_subset(child.rdi.as_ref(), parent.rdi.as_ref())
|
|
}
|
|
|
|
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_subset(
|
|
child: Option<&AsIdentifierChoice>,
|
|
parent: Option<&AsIdentifierChoice>,
|
|
) -> bool {
|
|
let Some(child) = child else {
|
|
return true;
|
|
};
|
|
let Some(parent) = parent else {
|
|
return false;
|
|
};
|
|
|
|
match (child, parent) {
|
|
(AsIdentifierChoice::Inherit, _) => return false,
|
|
(_, AsIdentifierChoice::Inherit) => return false,
|
|
_ => {}
|
|
}
|
|
|
|
let child_intervals = as_choice_to_merged_intervals(child);
|
|
let parent_intervals = as_choice_to_merged_intervals(parent);
|
|
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::data_model::rc::AsIdOrRange::Id(id) => v.push((*id, *id)),
|
|
crate::data_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(
|
|
child: &crate::data_model::rc::IpResourceSet,
|
|
parent: &crate::data_model::rc::IpResourceSet,
|
|
) -> bool {
|
|
let parent_by_afi = ip_resources_to_merged_intervals(parent);
|
|
let child_by_afi = match ip_resources_to_merged_intervals_strict(child) {
|
|
Ok(v) => v,
|
|
Err(()) => return false,
|
|
};
|
|
|
|
for (afi, child_intervals) in child_by_afi {
|
|
let Some(parent_intervals) = parent_by_afi.get(&afi) else {
|
|
return false;
|
|
};
|
|
for (cmin, cmax) in &child_intervals {
|
|
if !interval_is_covered(parent_intervals, cmin, cmax) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
fn ip_resources_is_subset_indexed(
|
|
child: &crate::data_model::rc::IpResourceSet,
|
|
parent: &crate::data_model::rc::IpResourceSet,
|
|
idx: &IssuerResourcesIndex,
|
|
) -> bool {
|
|
let _ = parent;
|
|
|
|
for fam in &child.families {
|
|
let parent_intervals = match fam.afi {
|
|
crate::data_model::rc::Afi::Ipv4 => idx.ip_v4.as_deref(),
|
|
crate::data_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
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
enum AfiKey {
|
|
V4,
|
|
V6,
|
|
}
|
|
|
|
fn ip_resources_to_merged_intervals(
|
|
set: &crate::data_model::rc::IpResourceSet,
|
|
) -> std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> {
|
|
let mut m: std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> =
|
|
std::collections::HashMap::new();
|
|
|
|
for fam in &set.families {
|
|
let afi = match fam.afi {
|
|
crate::data_model::rc::Afi::Ipv4 => AfiKey::V4,
|
|
crate::data_model::rc::Afi::Ipv6 => AfiKey::V6,
|
|
};
|
|
match &fam.choice {
|
|
IpAddressChoice::Inherit => {
|
|
// Effective resource sets should not contain inherit, but if they do we treat it
|
|
// as "unknown" by leaving it empty here (subset checks will fail).
|
|
}
|
|
IpAddressChoice::AddressesOrRanges(items) => {
|
|
let ent = m.entry(afi).or_default();
|
|
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())),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (_afi, v) in m.iter_mut() {
|
|
v.sort_by(|(a, _), (b, _)| a.cmp(b));
|
|
merge_ip_intervals_in_place(v);
|
|
}
|
|
|
|
m
|
|
}
|
|
|
|
fn ip_resources_to_merged_intervals_strict(
|
|
set: &crate::data_model::rc::IpResourceSet,
|
|
) -> Result<std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>>, ()> {
|
|
let mut m: std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> =
|
|
std::collections::HashMap::new();
|
|
|
|
for fam in &set.families {
|
|
let afi = match fam.afi {
|
|
crate::data_model::rc::Afi::Ipv4 => AfiKey::V4,
|
|
crate::data_model::rc::Afi::Ipv6 => AfiKey::V6,
|
|
};
|
|
match &fam.choice {
|
|
IpAddressChoice::Inherit => return Err(()),
|
|
IpAddressChoice::AddressesOrRanges(items) => {
|
|
let ent = m.entry(afi).or_default();
|
|
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())),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (_afi, v) in m.iter_mut() {
|
|
v.sort_by(|(a, _), (b, _)| a.cmp(b));
|
|
merge_ip_intervals_in_place(v);
|
|
}
|
|
|
|
Ok(m)
|
|
}
|
|
|
|
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 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 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 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;
|
|
for j in i + 1..out.len() {
|
|
out[j] = 0;
|
|
}
|
|
return out;
|
|
}
|
|
}
|
|
vec![0u8; out.len()]
|
|
}
|
|
|
|
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::data_model::rc::IpResourceSet>,
|
|
issuer_effective_as: Option<&crate::data_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::data_model::rc::Afi::Ipv4 => &mut v4,
|
|
crate::data_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() {
|
|
if !matches!(choice, AsIdentifierChoice::Inherit) {
|
|
idx.asnum = Some(as_choice_to_merged_intervals(choice));
|
|
}
|
|
}
|
|
if let Some(choice) = asr.rdi.as_ref() {
|
|
if !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",
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::analysis::timing::{TimingHandle, TimingMeta};
|
|
use crate::data_model::rc::{
|
|
Afi, AsIdOrRange, AsIdentifierChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange,
|
|
IpPrefix, IpResourceSet,
|
|
};
|
|
use crate::policy::Policy;
|
|
use crate::storage::{
|
|
PackTime, RoaCacheProjection, ValidatedManifestMeta, VcirAuditSummary,
|
|
VcirCcrManifestProjection, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
|
};
|
|
use crate::validation::publication_point::PublicationPointSnapshot;
|
|
use std::collections::HashMap;
|
|
use time::OffsetDateTime;
|
|
use time::format_description::well_known::Rfc3339;
|
|
|
|
fn fixture_bytes(path: &str) -> Vec<u8> {
|
|
std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path))
|
|
.unwrap_or_else(|e| panic!("read fixture {path}: {e}"))
|
|
}
|
|
|
|
fn fixed_time(value: &str) -> OffsetDateTime {
|
|
OffsetDateTime::parse(value, &Rfc3339).expect("parse fixed test time")
|
|
}
|
|
|
|
fn sample_roa_cache_vcir(
|
|
issuer_der: &[u8],
|
|
crl_hash: [u8; 32],
|
|
roa_hash: [u8; 32],
|
|
item_effective_until: OffsetDateTime,
|
|
instance_effective_until: OffsetDateTime,
|
|
) -> ValidatedCaInstanceResult {
|
|
let manifest_time = PackTime::from_utc_offset_datetime(fixed_time("2026-06-04T00:00:00Z"));
|
|
let effective_until = PackTime::from_utc_offset_datetime(instance_effective_until);
|
|
ValidatedCaInstanceResult {
|
|
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
|
parent_manifest_rsync_uri: Some("rsync://example.test/repo/parent.mft".to_string()),
|
|
tal_id: "test-tal".to_string(),
|
|
ca_subject_name: "CN=example".to_string(),
|
|
ca_ski: "001122".to_string(),
|
|
issuer_ski: "334455".to_string(),
|
|
last_successful_validation_time: manifest_time.clone(),
|
|
current_manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
|
current_crl_rsync_uri: "rsync://example.test/repo/current.crl".to_string(),
|
|
validated_manifest_meta: ValidatedManifestMeta {
|
|
validated_manifest_number: vec![1],
|
|
validated_manifest_this_update: manifest_time.clone(),
|
|
validated_manifest_next_update: effective_until.clone(),
|
|
},
|
|
ccr_manifest_projection: VcirCcrManifestProjection {
|
|
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
|
manifest_sha256: vec![0xaa; 32],
|
|
manifest_size: 2048,
|
|
manifest_ee_aki: vec![0xbb; 20],
|
|
manifest_number_be: vec![1],
|
|
manifest_this_update: manifest_time.clone(),
|
|
manifest_sia_locations_der: vec![vec![0x30, 0x00]],
|
|
subordinate_skis: Vec::new(),
|
|
},
|
|
instance_gate: VcirInstanceGate {
|
|
manifest_next_update: effective_until.clone(),
|
|
current_crl_next_update: effective_until.clone(),
|
|
self_ca_not_after: effective_until.clone(),
|
|
instance_effective_until: effective_until,
|
|
},
|
|
child_entries: Vec::new(),
|
|
local_outputs: vec![VcirLocalOutput {
|
|
output_type: VcirOutputType::Vrp,
|
|
item_effective_until: PackTime::from_utc_offset_datetime(item_effective_until),
|
|
source_object_uri: "rsync://example.test/repo/a.roa".to_string(),
|
|
source_object_type: VcirSourceObjectType::Roa,
|
|
source_object_hash: roa_hash,
|
|
source_ee_cert_hash: [0xcc; 32],
|
|
payload: VcirLocalOutputPayload::Vrp {
|
|
asn: 64500,
|
|
afi: RoaAfi::Ipv4,
|
|
prefix_len: 24,
|
|
addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
max_length: 24,
|
|
},
|
|
rule_hash: [0xdd; 32],
|
|
}],
|
|
related_artifacts: vec![
|
|
VcirRelatedArtifact {
|
|
artifact_role: VcirArtifactRole::IssuerCert,
|
|
artifact_kind: VcirArtifactKind::Cer,
|
|
uri: Some("rsync://example.test/repo/ca.cer".to_string()),
|
|
sha256: sha256_hex(issuer_der),
|
|
object_type: Some("cer".to_string()),
|
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
},
|
|
VcirRelatedArtifact {
|
|
artifact_role: VcirArtifactRole::CurrentCrl,
|
|
artifact_kind: VcirArtifactKind::Crl,
|
|
uri: Some("rsync://example.test/repo/current.crl".to_string()),
|
|
sha256: sha256_hex_from_32(&crl_hash),
|
|
object_type: Some("crl".to_string()),
|
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
},
|
|
],
|
|
summary: VcirSummary {
|
|
local_vrp_count: 1,
|
|
local_aspa_count: 0,
|
|
local_router_key_count: 0,
|
|
child_count: 0,
|
|
accepted_object_count: 2,
|
|
rejected_object_count: 0,
|
|
},
|
|
audit_summary: VcirAuditSummary {
|
|
failed_fetch_eligible: true,
|
|
last_failed_fetch_reason: None,
|
|
warning_count: 0,
|
|
audit_flags: Vec::new(),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_view_hits_when_context_and_hash_match() {
|
|
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
|
let issuer_der = b"issuer-ca";
|
|
let crl_hash = [0x22; 32];
|
|
let roa_hash = [0x11; 32];
|
|
let vcir = sample_roa_cache_vcir(
|
|
issuer_der,
|
|
crl_hash,
|
|
roa_hash,
|
|
fixed_time("2026-06-07T00:00:00Z"),
|
|
fixed_time("2026-06-08T00:00:00Z"),
|
|
);
|
|
let view = RoaValidationCacheView::from_vcir(&vcir, validation_time);
|
|
let files = vec![
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![0x01],
|
|
roa_hash,
|
|
),
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/current.crl",
|
|
vec![0x02],
|
|
crl_hash,
|
|
),
|
|
];
|
|
|
|
assert!(view.matches_current_context(issuer_der, &files));
|
|
let hit = view.lookup(&files[0], validation_time);
|
|
let RoaCacheLookupResult::Hit(ok) = hit else {
|
|
panic!("expected cache hit, got {hit:?}");
|
|
};
|
|
assert!(ok.reused_from_cache);
|
|
assert_eq!(ok.vrps.len(), 1);
|
|
assert_eq!(ok.vrps[0].asn, 64500);
|
|
assert_eq!(ok.local_outputs.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_view_from_projection_matches_vcir_view() {
|
|
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
|
let issuer_der = b"issuer-ca";
|
|
let crl_hash = [0x22; 32];
|
|
let roa_hash = [0x11; 32];
|
|
let vcir = sample_roa_cache_vcir(
|
|
issuer_der,
|
|
crl_hash,
|
|
roa_hash,
|
|
fixed_time("2026-06-07T00:00:00Z"),
|
|
fixed_time("2026-06-08T00:00:00Z"),
|
|
);
|
|
let projection = RoaCacheProjection::from_vcir(&vcir)
|
|
.expect("build projection")
|
|
.expect("projection exists");
|
|
let view = RoaValidationCacheView::from_projection(&projection, validation_time);
|
|
let files = vec![
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![0x01],
|
|
roa_hash,
|
|
),
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/current.crl",
|
|
vec![0x02],
|
|
crl_hash,
|
|
),
|
|
];
|
|
|
|
assert!(view.matches_current_context(issuer_der, &files));
|
|
let hit = view.lookup(&files[0], validation_time);
|
|
let RoaCacheLookupResult::Hit(ok) = hit else {
|
|
panic!("expected projection cache hit, got {hit:?}");
|
|
};
|
|
assert!(ok.reused_from_cache);
|
|
assert_eq!(ok.vrps.len(), 1);
|
|
assert_eq!(ok.vrps[0].asn, 64500);
|
|
assert_eq!(ok.local_outputs.len(), 1);
|
|
assert_eq!(
|
|
ok.local_outputs[0].source_object_uri,
|
|
"rsync://example.test/repo/a.roa"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_view_from_projection_blocks_on_gates() {
|
|
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
|
let issuer_der = b"issuer-ca";
|
|
let crl_hash = [0x22; 32];
|
|
let roa_hash = [0x11; 32];
|
|
let vcir = sample_roa_cache_vcir(
|
|
issuer_der,
|
|
crl_hash,
|
|
roa_hash,
|
|
fixed_time("2026-06-07T00:00:00Z"),
|
|
fixed_time("2026-06-08T00:00:00Z"),
|
|
);
|
|
let mut projection = RoaCacheProjection::from_vcir(&vcir)
|
|
.expect("build projection")
|
|
.expect("projection exists");
|
|
let files = vec![
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![0x01],
|
|
roa_hash,
|
|
),
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/current.crl",
|
|
vec![0x02],
|
|
crl_hash,
|
|
),
|
|
];
|
|
|
|
projection.issuer_ca_sha256_hex = Some("00".repeat(32));
|
|
let issuer_changed = RoaValidationCacheView::from_projection(&projection, validation_time);
|
|
assert!(!issuer_changed.matches_current_context(issuer_der, &files));
|
|
|
|
let mut projection = RoaCacheProjection::from_vcir(&vcir)
|
|
.expect("build projection")
|
|
.expect("projection exists");
|
|
projection.crl_sha256_by_uri[0].sha256 = "11".repeat(32);
|
|
let crl_changed = RoaValidationCacheView::from_projection(&projection, validation_time);
|
|
assert!(!crl_changed.matches_current_context(issuer_der, &files));
|
|
|
|
let mut projection = RoaCacheProjection::from_vcir(&vcir)
|
|
.expect("build projection")
|
|
.expect("projection exists");
|
|
projection.entries[0].source_object_hash = [0xff; 32];
|
|
let roa_changed = RoaValidationCacheView::from_projection(&projection, validation_time);
|
|
assert!(matches!(
|
|
roa_changed.lookup(&files[0], validation_time),
|
|
RoaCacheLookupResult::Blocked
|
|
));
|
|
|
|
let expired_vcir = sample_roa_cache_vcir(
|
|
issuer_der,
|
|
crl_hash,
|
|
roa_hash,
|
|
fixed_time("2026-06-07T00:00:00Z"),
|
|
fixed_time("2026-06-04T00:00:00Z"),
|
|
);
|
|
let expired_projection = RoaCacheProjection::from_vcir(&expired_vcir)
|
|
.expect("build projection")
|
|
.expect("projection exists");
|
|
let expired_view =
|
|
RoaValidationCacheView::from_projection(&expired_projection, validation_time);
|
|
assert!(!expired_view.matches_current_context(issuer_der, &files));
|
|
assert!(matches!(
|
|
expired_view.lookup(&files[0], validation_time),
|
|
RoaCacheLookupResult::Blocked
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_view_blocks_when_context_changes() {
|
|
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
|
let issuer_der = b"issuer-ca";
|
|
let crl_hash = [0x22; 32];
|
|
let roa_hash = [0x11; 32];
|
|
let vcir = sample_roa_cache_vcir(
|
|
issuer_der,
|
|
crl_hash,
|
|
roa_hash,
|
|
fixed_time("2026-06-07T00:00:00Z"),
|
|
fixed_time("2026-06-08T00:00:00Z"),
|
|
);
|
|
let view = RoaValidationCacheView::from_vcir(&vcir, validation_time);
|
|
let files = vec![
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![0x01],
|
|
roa_hash,
|
|
),
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/current.crl",
|
|
vec![0x02],
|
|
[0x33; 32],
|
|
),
|
|
];
|
|
let mut stats =
|
|
RoaValidationCacheStats::for_input(RoaValidationCacheInput::enabled(Some(&view)), 1);
|
|
|
|
assert!(!view.matches_current_context(issuer_der, &files));
|
|
assert!(
|
|
active_roa_cache_view(
|
|
RoaValidationCacheInput::enabled(Some(&view)),
|
|
issuer_der,
|
|
&files,
|
|
&mut stats,
|
|
1,
|
|
)
|
|
.is_none()
|
|
);
|
|
assert_eq!(stats.blocked_roas, 1);
|
|
assert_eq!(stats.fresh_roas, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_view_blocks_expired_output() {
|
|
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
|
let issuer_der = b"issuer-ca";
|
|
let crl_hash = [0x22; 32];
|
|
let roa_hash = [0x11; 32];
|
|
let vcir = sample_roa_cache_vcir(
|
|
issuer_der,
|
|
crl_hash,
|
|
roa_hash,
|
|
fixed_time("2026-06-04T00:00:00Z"),
|
|
fixed_time("2026-06-08T00:00:00Z"),
|
|
);
|
|
let view = RoaValidationCacheView::from_vcir(&vcir, validation_time);
|
|
let file = PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![0x01],
|
|
roa_hash,
|
|
);
|
|
|
|
assert!(matches!(
|
|
view.lookup(&file, validation_time),
|
|
RoaCacheLookupResult::Blocked
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_stats_records_vcir_miss_to_timing() {
|
|
let stats = RoaValidationCacheStats::for_input(RoaValidationCacheInput::enabled(None), 3);
|
|
assert_eq!(stats.enabled_publication_points, 1);
|
|
assert_eq!(stats.vcir_miss_publication_points, 1);
|
|
assert_eq!(stats.miss_roas, 3);
|
|
assert_eq!(stats.fresh_roas, 3);
|
|
|
|
let timing = TimingHandle::new(TimingMeta {
|
|
recorded_at_utc_rfc3339: "2026-06-05T00:00:00Z".to_string(),
|
|
validation_time_utc_rfc3339: "2026-06-05T00:00:00Z".to_string(),
|
|
tal_url: None,
|
|
db_path: None,
|
|
});
|
|
stats.record_to_timing(Some(&timing));
|
|
let dir = tempfile::tempdir().expect("timing dir");
|
|
let path = dir.path().join("timing.json");
|
|
timing.write_json(&path, 10).expect("write timing");
|
|
let report: serde_json::Value =
|
|
serde_json::from_slice(&std::fs::read(path).expect("read timing"))
|
|
.expect("parse timing");
|
|
|
|
assert_eq!(
|
|
report["counts"]["roa_validation_cache_enabled_publication_points"],
|
|
1
|
|
);
|
|
assert_eq!(
|
|
report["counts"]["roa_validation_cache_vcir_miss_publication_points"],
|
|
1
|
|
);
|
|
assert_eq!(report["counts"]["roa_validation_cache_miss_roas"], 3);
|
|
assert_eq!(report["counts"]["roa_validation_cache_fresh_roas"], 3);
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_view_blocks_expired_instance_gate() {
|
|
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
|
let issuer_der = b"issuer-ca";
|
|
let crl_hash = [0x22; 32];
|
|
let roa_hash = [0x11; 32];
|
|
let vcir = sample_roa_cache_vcir(
|
|
issuer_der,
|
|
crl_hash,
|
|
roa_hash,
|
|
fixed_time("2026-06-07T00:00:00Z"),
|
|
fixed_time("2026-06-04T00:00:00Z"),
|
|
);
|
|
let view = RoaValidationCacheView::from_vcir(&vcir, validation_time);
|
|
let file = PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![0x01],
|
|
roa_hash,
|
|
);
|
|
|
|
assert!(!view.matches_current_context(issuer_der, &[]));
|
|
assert!(matches!(
|
|
view.lookup(&file, validation_time),
|
|
RoaCacheLookupResult::Blocked
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_view_ignores_rejected_artifacts_and_non_roa_outputs() {
|
|
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
|
let issuer_der = b"issuer-ca";
|
|
let crl_hash = [0x22; 32];
|
|
let roa_hash = [0x11; 32];
|
|
let mut vcir = sample_roa_cache_vcir(
|
|
issuer_der,
|
|
crl_hash,
|
|
roa_hash,
|
|
fixed_time("2026-06-07T00:00:00Z"),
|
|
fixed_time("2026-06-08T00:00:00Z"),
|
|
);
|
|
vcir.related_artifacts.insert(
|
|
0,
|
|
VcirRelatedArtifact {
|
|
artifact_role: VcirArtifactRole::IssuerCert,
|
|
artifact_kind: VcirArtifactKind::Cer,
|
|
uri: Some("rsync://example.test/repo/rejected.cer".to_string()),
|
|
sha256: "00".repeat(32),
|
|
object_type: Some("cer".to_string()),
|
|
validation_status: VcirArtifactValidationStatus::Rejected,
|
|
},
|
|
);
|
|
vcir.local_outputs.push(VcirLocalOutput {
|
|
output_type: VcirOutputType::Aspa,
|
|
item_effective_until: PackTime::from_utc_offset_datetime(fixed_time(
|
|
"2026-06-07T00:00:00Z",
|
|
)),
|
|
source_object_uri: "rsync://example.test/repo/a.asa".to_string(),
|
|
source_object_type: VcirSourceObjectType::Aspa,
|
|
source_object_hash: [0x44; 32],
|
|
source_ee_cert_hash: [0x55; 32],
|
|
payload: VcirLocalOutputPayload::Aspa {
|
|
customer_as_id: 64500,
|
|
provider_as_ids: vec![64501],
|
|
},
|
|
rule_hash: [0x66; 32],
|
|
});
|
|
let view = RoaValidationCacheView::from_vcir(&vcir, validation_time);
|
|
let files = vec![
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![0x01],
|
|
roa_hash,
|
|
),
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/current.crl",
|
|
vec![0x02],
|
|
crl_hash,
|
|
),
|
|
];
|
|
|
|
assert!(view.matches_current_context(issuer_der, &files));
|
|
assert!(matches!(
|
|
view.lookup(&files[0], validation_time),
|
|
RoaCacheLookupResult::Hit(_)
|
|
));
|
|
assert!(matches!(
|
|
view.lookup(
|
|
&PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.asa",
|
|
vec![0x03],
|
|
[0x44; 32],
|
|
),
|
|
validation_time
|
|
),
|
|
RoaCacheLookupResult::Miss
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_context_rejects_missing_and_changed_issuer() {
|
|
let mut view = RoaValidationCacheView {
|
|
entries_by_uri: HashMap::new(),
|
|
issuer_ca_sha256_hex: None,
|
|
crl_sha256_by_uri: HashMap::new(),
|
|
blocked: false,
|
|
};
|
|
assert!(!view.matches_current_context(b"issuer-ca", &[]));
|
|
|
|
view.issuer_ca_sha256_hex = Some("00".repeat(32));
|
|
assert!(!view.matches_current_context(b"issuer-ca", &[]));
|
|
|
|
view.issuer_ca_sha256_hex = Some(sha256_hex(b"issuer-ca"));
|
|
assert!(view.matches_current_context(b"issuer-ca", &[]));
|
|
}
|
|
|
|
#[test]
|
|
fn roa_validation_cache_lookup_classifies_hash_empty_payload_and_missing_uri() {
|
|
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
|
let roa_hash = [0x11; 32];
|
|
let file = PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![0x01],
|
|
roa_hash,
|
|
);
|
|
let mut output = sample_roa_cache_vcir(
|
|
b"issuer-ca",
|
|
[0x22; 32],
|
|
roa_hash,
|
|
fixed_time("2026-06-07T00:00:00Z"),
|
|
fixed_time("2026-06-08T00:00:00Z"),
|
|
)
|
|
.local_outputs
|
|
.remove(0);
|
|
let mut view = RoaValidationCacheView {
|
|
entries_by_uri: HashMap::new(),
|
|
issuer_ca_sha256_hex: Some(sha256_hex(b"issuer-ca")),
|
|
crl_sha256_by_uri: HashMap::new(),
|
|
blocked: false,
|
|
};
|
|
|
|
assert!(matches!(
|
|
view.lookup(&file, validation_time),
|
|
RoaCacheLookupResult::Miss
|
|
));
|
|
|
|
view.entries_by_uri.insert(
|
|
file.rsync_uri.clone(),
|
|
CachedRoaValidationResult {
|
|
source_object_hash: [0xff; 32],
|
|
outputs: vec![output.clone()],
|
|
},
|
|
);
|
|
assert!(matches!(
|
|
view.lookup(&file, validation_time),
|
|
RoaCacheLookupResult::Blocked
|
|
));
|
|
|
|
view.entries_by_uri.insert(
|
|
file.rsync_uri.clone(),
|
|
CachedRoaValidationResult {
|
|
source_object_hash: roa_hash,
|
|
outputs: Vec::new(),
|
|
},
|
|
);
|
|
assert!(matches!(
|
|
view.lookup(&file, validation_time),
|
|
RoaCacheLookupResult::Miss
|
|
));
|
|
|
|
output.payload = VcirLocalOutputPayload::Aspa {
|
|
customer_as_id: 64500,
|
|
provider_as_ids: vec![64501],
|
|
};
|
|
view.entries_by_uri.insert(
|
|
file.rsync_uri.clone(),
|
|
CachedRoaValidationResult {
|
|
source_object_hash: roa_hash,
|
|
outputs: vec![output],
|
|
},
|
|
);
|
|
assert!(matches!(
|
|
view.lookup(&file, validation_time),
|
|
RoaCacheLookupResult::Blocked
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parallel_roa_cache_blocked_falls_back_to_single_fresh_task() {
|
|
let manifest_bytes = fixture_bytes(
|
|
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
|
);
|
|
let issuer_ca_der = fixture_bytes(
|
|
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
|
);
|
|
let crl_hash = [0x22; 32];
|
|
let actual_roa_hash = [0x11; 32];
|
|
let cached_roa_hash = [0x33; 32];
|
|
let validation_time = fixed_time("2026-06-05T00:00:00Z");
|
|
let publication_point = PublicationPointSnapshot {
|
|
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
|
|
manifest_rsync_uri:
|
|
"rsync://rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"
|
|
.to_string(),
|
|
publication_point_rsync_uri: "rsync://rpki.cernet.net/repo/cernet/0/".to_string(),
|
|
manifest_number_be: vec![1],
|
|
this_update: PackTime::from_utc_offset_datetime(validation_time),
|
|
next_update: PackTime::from_utc_offset_datetime(
|
|
validation_time + time::Duration::days(1),
|
|
),
|
|
verified_at: PackTime::from_utc_offset_datetime(validation_time),
|
|
manifest_bytes,
|
|
files: vec![
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/current.crl",
|
|
vec![0x02],
|
|
crl_hash,
|
|
),
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![0x01],
|
|
actual_roa_hash,
|
|
),
|
|
],
|
|
};
|
|
let vcir = sample_roa_cache_vcir(
|
|
&issuer_ca_der,
|
|
crl_hash,
|
|
cached_roa_hash,
|
|
fixed_time("2026-06-07T00:00:00Z"),
|
|
fixed_time("2026-06-08T00:00:00Z"),
|
|
);
|
|
let view = RoaValidationCacheView::from_vcir(&vcir, validation_time);
|
|
|
|
let stage = match prepare_publication_point_for_parallel_roa_with_cache(
|
|
7,
|
|
&publication_point,
|
|
&Policy::default(),
|
|
&issuer_ca_der,
|
|
None,
|
|
None,
|
|
None,
|
|
validation_time,
|
|
false,
|
|
RoaValidationCacheInput::enabled(Some(&view)),
|
|
) {
|
|
ParallelObjectsPrepare::Staged(stage) => stage,
|
|
ParallelObjectsPrepare::Complete(_) => panic!("expected staged ROA fallback"),
|
|
};
|
|
|
|
assert_eq!(stage.roa_cache_stats.blocked_roas, 1);
|
|
assert_eq!(stage.roa_cache_stats.fresh_roas, 1);
|
|
assert_eq!(stage.roa_task_indices, vec![1]);
|
|
assert_eq!(stage.roa_task_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn merge_as_intervals_merges_overlapping_and_adjacent() {
|
|
let v = vec![(1, 2), (3, 5), (10, 10), (11, 12)];
|
|
let merged = merge_as_intervals(&v);
|
|
assert_eq!(merged, vec![(1, 5), (10, 12)]);
|
|
}
|
|
|
|
#[test]
|
|
fn as_choice_subset_rejects_inherit() {
|
|
let child = Some(&AsIdentifierChoice::Inherit);
|
|
let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range { min: 1, max: 10 },
|
|
]));
|
|
assert!(!as_choice_subset(child, parent));
|
|
}
|
|
|
|
#[test]
|
|
fn as_choice_subset_checks_ranges() {
|
|
let child = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Id(5),
|
|
AsIdOrRange::Range { min: 7, max: 9 },
|
|
]));
|
|
let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range { min: 1, max: 10 },
|
|
]));
|
|
assert!(as_choice_subset(child, parent));
|
|
}
|
|
|
|
#[test]
|
|
fn ip_resources_is_subset_accepts_prefixes_and_ranges() {
|
|
let parent = 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],
|
|
}),
|
|
IpAddressOrRange::Range(IpAddressRange {
|
|
min: vec![192, 0, 2, 0],
|
|
max: vec![192, 0, 2, 255],
|
|
}),
|
|
]),
|
|
}],
|
|
};
|
|
let child = 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],
|
|
}),
|
|
IpAddressOrRange::Range(IpAddressRange {
|
|
min: vec![192, 0, 2, 10],
|
|
max: vec![192, 0, 2, 20],
|
|
}),
|
|
]),
|
|
}],
|
|
};
|
|
|
|
assert!(ip_resources_is_subset(&child, &parent));
|
|
}
|
|
|
|
#[test]
|
|
fn ip_resources_is_subset_rejects_inherit_in_child() {
|
|
let parent = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv6,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv6,
|
|
prefix_len: 32,
|
|
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let child = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv6,
|
|
choice: IpAddressChoice::Inherit,
|
|
}],
|
|
};
|
|
assert!(!ip_resources_is_subset(&child, &parent));
|
|
}
|
|
|
|
#[test]
|
|
fn increment_bytes_wraps_all_ff_to_zero() {
|
|
assert_eq!(increment_bytes(&[0xFF, 0xFF]), vec![0x00, 0x00]);
|
|
}
|
|
|
|
#[test]
|
|
fn merge_ip_intervals_merges_contiguous() {
|
|
let mut v = vec![
|
|
(vec![0, 0, 0, 0], vec![0, 0, 0, 10]),
|
|
(vec![0, 0, 0, 11], vec![0, 0, 0, 20]),
|
|
];
|
|
merge_ip_intervals_in_place(&mut v);
|
|
assert_eq!(v, vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 20])]);
|
|
}
|
|
|
|
#[test]
|
|
fn choose_crl_for_certificate_reports_missing_crl_in_snapshot() {
|
|
let roa_der =
|
|
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
|
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
|
|
let ee_crldp_uris = roa.signed_object.signed_data.certificates[0]
|
|
.resource_cert
|
|
.tbs
|
|
.extensions
|
|
.crl_distribution_points_uris
|
|
.as_ref();
|
|
let crl_cache: HashMap<String, CachedIssuerCrl> = HashMap::new();
|
|
let err = choose_crl_uri_for_certificate(ee_crldp_uris, &crl_cache).unwrap_err();
|
|
assert!(matches!(err, ObjectValidateError::MissingCrlInPack));
|
|
}
|
|
|
|
#[test]
|
|
fn choose_crl_for_certificate_reports_missing_crldp_uris() {
|
|
let mut crl_cache: HashMap<String, CachedIssuerCrl> = HashMap::new();
|
|
crl_cache.insert(
|
|
"rsync://example.test/a.crl".to_string(),
|
|
CachedIssuerCrl::Pending(vec![0x01]),
|
|
);
|
|
let err = choose_crl_uri_for_certificate(None, &crl_cache).unwrap_err();
|
|
assert!(matches!(err, ObjectValidateError::MissingCrlDpUris));
|
|
}
|
|
|
|
#[test]
|
|
fn choose_crl_for_certificate_prefers_matching_crldp_uri_in_order() {
|
|
let roa_der =
|
|
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
|
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
|
|
let ee_crldp_uris = roa.signed_object.signed_data.certificates[0]
|
|
.resource_cert
|
|
.tbs
|
|
.extensions
|
|
.crl_distribution_points_uris
|
|
.as_ref()
|
|
.expect("fixture ee has crldp");
|
|
|
|
let matching_uri = ee_crldp_uris[0].as_str().to_string();
|
|
let mut crl_cache: HashMap<String, CachedIssuerCrl> = HashMap::new();
|
|
crl_cache.insert(
|
|
"rsync://example.test/other.crl".to_string(),
|
|
CachedIssuerCrl::Pending(vec![0x00]),
|
|
);
|
|
crl_cache.insert(matching_uri.clone(), CachedIssuerCrl::Pending(vec![0x01]));
|
|
|
|
let uri = choose_crl_uri_for_certificate(Some(ee_crldp_uris), &crl_cache).unwrap();
|
|
assert_eq!(uri, matching_uri);
|
|
}
|
|
|
|
#[test]
|
|
fn choose_crl_for_certificate_reports_not_found_when_crldp_does_not_match_snapshot() {
|
|
let roa_der =
|
|
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
|
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
|
|
let ee_crldp_uris = roa.signed_object.signed_data.certificates[0]
|
|
.resource_cert
|
|
.tbs
|
|
.extensions
|
|
.crl_distribution_points_uris
|
|
.as_ref();
|
|
|
|
let mut crl_cache: HashMap<String, CachedIssuerCrl> = HashMap::new();
|
|
crl_cache.insert(
|
|
"rsync://example.test/other.crl".to_string(),
|
|
CachedIssuerCrl::Pending(vec![0x01]),
|
|
);
|
|
let err = choose_crl_uri_for_certificate(ee_crldp_uris, &crl_cache).unwrap_err();
|
|
assert!(matches!(err, ObjectValidateError::CrlNotFound(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_ee_resources_subset_reports_missing_issuer_effective_ip() {
|
|
let roa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
|
let roa_der = std::fs::read(roa_path).expect("read roa");
|
|
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
|
|
let ee = &roa.signed_object.signed_data.certificates[0].resource_cert;
|
|
|
|
let idx = IssuerResourcesIndex::default();
|
|
let err = validate_ee_resources_subset(ee, None, None, &idx).unwrap_err();
|
|
assert!(matches!(err, ObjectValidateError::MissingIssuerEffectiveIp));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_ee_resources_subset_reports_missing_issuer_effective_as() {
|
|
let aspa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
|
|
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
|
);
|
|
let aspa_der = std::fs::read(aspa_path).expect("read aspa");
|
|
let aspa = AspaObject::decode_der(&aspa_der).expect("decode aspa");
|
|
let ee = &aspa.signed_object.signed_data.certificates[0].resource_cert;
|
|
|
|
let issuer_ip = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv6,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv6,
|
|
prefix_len: 32,
|
|
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
|
|
let idx = build_issuer_resources_index(Some(&issuer_ip), None);
|
|
let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None, &idx).unwrap_err();
|
|
assert!(matches!(err, ObjectValidateError::MissingIssuerEffectiveAs));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_ee_resources_subset_reports_not_subset() {
|
|
let roa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
|
let roa_der = std::fs::read(roa_path).expect("read roa");
|
|
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
|
|
let ee = &roa.signed_object.signed_data.certificates[0].resource_cert;
|
|
|
|
// Unrelated parent resources.
|
|
let issuer_ip = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv6,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv6,
|
|
prefix_len: 32,
|
|
addr: vec![0x26, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
|
|
let idx = build_issuer_resources_index(Some(&issuer_ip), None);
|
|
let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None, &idx).unwrap_err();
|
|
assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset));
|
|
}
|
|
|
|
#[test]
|
|
fn extra_rfc_refs_for_crl_selection_distinguishes_crl_errors() {
|
|
assert_eq!(
|
|
extra_rfc_refs_for_crl_selection(&ObjectValidateError::MissingCrlDpUris),
|
|
RFC_CRLDP
|
|
);
|
|
assert_eq!(
|
|
extra_rfc_refs_for_crl_selection(&ObjectValidateError::CrlNotFound(
|
|
"rsync://example.test/x.crl".to_string(),
|
|
)),
|
|
RFC_CRLDP_AND_LOCKED_PACK
|
|
);
|
|
assert!(
|
|
extra_rfc_refs_for_crl_selection(&ObjectValidateError::MissingCrlInPack).is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn as_subset_helpers_cover_success_and_failure_paths() {
|
|
let child = AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Id(5),
|
|
AsIdOrRange::Range { min: 7, max: 9 },
|
|
]);
|
|
let parent =
|
|
AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 10 }]);
|
|
let parent_intervals = [(1, 10)];
|
|
|
|
assert!(as_choice_subset(None, Some(&parent)));
|
|
assert!(!as_choice_subset(Some(&child), None));
|
|
assert!(!as_choice_subset(
|
|
Some(&AsIdentifierChoice::Inherit),
|
|
Some(&parent)
|
|
));
|
|
assert!(!as_choice_subset(
|
|
Some(&child),
|
|
Some(&AsIdentifierChoice::Inherit)
|
|
));
|
|
assert!(as_choice_subset(Some(&child), Some(&parent)));
|
|
|
|
assert!(as_choice_subset_indexed(None, Some(&parent_intervals)));
|
|
assert!(!as_choice_subset_indexed(Some(&child), None));
|
|
assert!(!as_choice_subset_indexed(
|
|
Some(&AsIdentifierChoice::Inherit),
|
|
Some(&parent_intervals),
|
|
));
|
|
assert!(as_choice_subset_indexed(
|
|
Some(&child),
|
|
Some(&parent_intervals)
|
|
));
|
|
assert!(!as_choice_subset_indexed(
|
|
Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range { min: 11, max: 12 }
|
|
])),
|
|
Some(&parent_intervals),
|
|
));
|
|
|
|
let child_set = AsResourceSet {
|
|
asnum: Some(child.clone()),
|
|
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(42)])),
|
|
};
|
|
let parent_set = AsResourceSet {
|
|
asnum: Some(parent.clone()),
|
|
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range { min: 40, max: 50 },
|
|
])),
|
|
};
|
|
assert!(as_resources_is_subset(&child_set, &parent_set));
|
|
assert!(as_resources_is_subset_indexed(
|
|
&child_set,
|
|
&parent_set,
|
|
&IssuerResourcesIndex {
|
|
asnum: Some(vec![(1, 10)]),
|
|
rdi: Some(vec![(40, 50)]),
|
|
..IssuerResourcesIndex::default()
|
|
},
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn ip_subset_helpers_cover_strict_and_indexed_paths() {
|
|
let parent = 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],
|
|
}),
|
|
IpAddressOrRange::Range(IpAddressRange {
|
|
min: vec![192, 0, 2, 0],
|
|
max: vec![192, 0, 2, 255],
|
|
}),
|
|
]),
|
|
},
|
|
IpAddressFamily {
|
|
afi: Afi::Ipv6,
|
|
choice: IpAddressChoice::Inherit,
|
|
},
|
|
],
|
|
};
|
|
let child = 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],
|
|
}),
|
|
IpAddressOrRange::Range(IpAddressRange {
|
|
min: vec![192, 0, 2, 10],
|
|
max: vec![192, 0, 2, 20],
|
|
}),
|
|
]),
|
|
}],
|
|
};
|
|
let strict_bad = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::Inherit,
|
|
}],
|
|
};
|
|
|
|
assert!(ip_resources_is_subset(&child, &parent));
|
|
assert!(ip_resources_to_merged_intervals(&parent).contains_key(&AfiKey::V4));
|
|
assert!(ip_resources_to_merged_intervals_strict(&child).is_ok());
|
|
assert!(ip_resources_to_merged_intervals_strict(&strict_bad).is_err());
|
|
|
|
let idx = build_issuer_resources_index(
|
|
Some(&parent),
|
|
Some(&AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range {
|
|
min: 64500,
|
|
max: 64510,
|
|
},
|
|
])),
|
|
rdi: Some(AsIdentifierChoice::Inherit),
|
|
}),
|
|
);
|
|
assert!(idx.ip_v4.is_some());
|
|
assert!(idx.ip_v6.is_none());
|
|
assert!(idx.asnum.is_some());
|
|
assert!(idx.rdi.is_none());
|
|
assert!(ip_resources_is_subset_indexed(&child, &parent, &idx));
|
|
assert!(!ip_resources_is_subset_indexed(&strict_bad, &parent, &idx));
|
|
}
|
|
|
|
#[test]
|
|
fn interval_and_byte_helpers_cover_edge_cases() {
|
|
let parent = vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 10])];
|
|
assert!(interval_is_covered(&parent, &[0, 0, 0, 1], &[0, 0, 0, 2]));
|
|
assert!(!interval_is_covered(
|
|
&parent,
|
|
&[0, 0, 0, 11],
|
|
&[0, 0, 0, 12]
|
|
));
|
|
assert!(intervals_are_covered(
|
|
&parent,
|
|
&[(vec![0, 0, 0, 1], vec![0, 0, 0, 2])]
|
|
));
|
|
assert!(!intervals_are_covered(
|
|
&parent,
|
|
&[(vec![0, 0, 0, 9], vec![0, 0, 0, 11])],
|
|
));
|
|
|
|
let prefix = RcIpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 24,
|
|
addr: vec![203, 0, 113, 7],
|
|
};
|
|
assert_eq!(
|
|
prefix_to_range(&prefix),
|
|
(vec![203, 0, 113, 0], vec![203, 0, 113, 255])
|
|
);
|
|
assert_eq!(increment_bytes(&[0, 0, 0, 255]), vec![0, 0, 1, 0]);
|
|
assert!(bytes_is_next(&[0, 0, 1, 0], &[0, 0, 0, 255]));
|
|
assert!(!bytes_is_next(&[1, 2], &[1]));
|
|
}
|
|
|
|
#[test]
|
|
fn merged_interval_helpers_cover_empty_and_break_paths() {
|
|
let mut empty: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
|
merge_ip_intervals_in_place(&mut empty);
|
|
assert!(empty.is_empty());
|
|
|
|
let mut v = vec![
|
|
(vec![0, 0, 0, 20], vec![0, 0, 0, 30]),
|
|
(vec![0, 0, 0, 0], vec![0, 0, 0, 10]),
|
|
(vec![0, 0, 0, 11], vec![0, 0, 0, 19]),
|
|
];
|
|
v.sort_by(|(a, _), (b, _)| a.cmp(b));
|
|
merge_ip_intervals_in_place(&mut v);
|
|
assert_eq!(v, vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 30])]);
|
|
|
|
assert_eq!(
|
|
as_choice_to_merged_intervals(&AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Id(1),
|
|
AsIdOrRange::Range { min: 2, max: 3 },
|
|
AsIdOrRange::Range { min: 7, max: 9 },
|
|
])),
|
|
vec![(1, 3), (7, 9)]
|
|
);
|
|
assert!(as_interval_is_covered(&[(1, 3), (7, 9)], 2, 3));
|
|
assert!(!as_interval_is_covered(&[(7, 9)], 2, 3));
|
|
}
|
|
|
|
#[test]
|
|
fn roa_output_helpers_cover_vrps_and_afi_strings() {
|
|
let roa_der =
|
|
fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
|
let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
|
|
let vrps = roa_to_vrps(&roa);
|
|
assert!(!vrps.is_empty());
|
|
assert!(vrps.iter().all(|vrp| vrp.asn == roa.roa.as_id));
|
|
assert_eq!(roa_afi_to_string(RoaAfi::Ipv4), "ipv4");
|
|
assert_eq!(roa_afi_to_string(RoaAfi::Ipv6), "ipv6");
|
|
}
|
|
|
|
#[test]
|
|
fn parallel_stage_roa_tasks_share_stage_owned_payloads() {
|
|
let stage = ParallelObjectsStage {
|
|
publication_point_id: 7,
|
|
shared: Arc::new(RoaTaskShared {
|
|
locked_files: Arc::<[PackFile]>::from(vec![
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/a.roa",
|
|
vec![1, 2, 3],
|
|
[1u8; 32],
|
|
),
|
|
PackFile::from_bytes_with_sha256(
|
|
"rsync://example.test/repo/b.roa",
|
|
vec![4, 5, 6],
|
|
[2u8; 32],
|
|
),
|
|
]),
|
|
manifest_rsync_uri: Arc::<str>::from("rsync://example.test/repo/manifest.mft"),
|
|
issuer_ca_der: Arc::from([0x01u8].as_slice()),
|
|
issuer_ca: Arc::new(
|
|
ResourceCertificate::decode_der(&fixture_bytes(
|
|
"tests/fixtures/ta/apnic-ta.cer",
|
|
))
|
|
.expect("decode fixture CA certificate"),
|
|
),
|
|
issuer_spki_der: Arc::from([0x02u8].as_slice()),
|
|
issuer_ca_rsync_uri: Some(Arc::<str>::from("rsync://example.test/repo/ca.cer")),
|
|
crl_cache: Arc::new(Mutex::new(HashMap::new())),
|
|
issuer_resources_index: Arc::new(IssuerResourcesIndex::default()),
|
|
issuer_effective_ip: None,
|
|
issuer_effective_as: None,
|
|
}),
|
|
validation_time: OffsetDateTime::now_utc(),
|
|
collect_vcir_local_outputs: false,
|
|
strict_cms_der: false,
|
|
strict_name: false,
|
|
roa_task_indices: vec![0, 1],
|
|
cached_roa_results: Vec::new(),
|
|
roa_cache_stats: RoaValidationCacheStats::default(),
|
|
warnings: Vec::new(),
|
|
stats: ObjectsStats {
|
|
roa_total: 2,
|
|
..ObjectsStats::default()
|
|
},
|
|
audit: Vec::new(),
|
|
};
|
|
let tasks = stage.build_roa_tasks();
|
|
assert_eq!(tasks.len(), 2);
|
|
assert!(Arc::ptr_eq(&tasks[0].shared, &tasks[1].shared));
|
|
}
|
|
|
|
#[test]
|
|
fn strict_name_manifest_decode_failure_drops_publication_point() {
|
|
let publication_point = PublicationPointSnapshot {
|
|
format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
|
|
manifest_rsync_uri: "rsync://example.test/repo/manifest.mft".to_string(),
|
|
publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
|
|
manifest_number_be: vec![0x01],
|
|
this_update: PackTime::from_utc_offset_datetime(OffsetDateTime::now_utc()),
|
|
next_update: PackTime::from_utc_offset_datetime(OffsetDateTime::now_utc()),
|
|
verified_at: PackTime::from_utc_offset_datetime(OffsetDateTime::now_utc()),
|
|
manifest_bytes: vec![0x01, 0x02, 0x03],
|
|
files: vec![],
|
|
};
|
|
let policy = Policy::default();
|
|
let output = process_publication_point_for_issuer_with_options(
|
|
&publication_point,
|
|
&policy,
|
|
&[],
|
|
None,
|
|
None,
|
|
None,
|
|
OffsetDateTime::now_utc(),
|
|
None,
|
|
false,
|
|
);
|
|
assert!(output.stats.publication_point_dropped);
|
|
assert!(output.vrps.is_empty());
|
|
assert!(
|
|
output
|
|
.warnings
|
|
.iter()
|
|
.any(|warning| warning.message.contains("manifest decode failed")),
|
|
"{:?}",
|
|
output.warnings
|
|
);
|
|
}
|
|
}
|