rpki/src/validation/objects.rs
2026-02-11 10:07:24 +08:00

1035 lines
38 KiB
Rust

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::policy::{Policy, SignedObjectFailurePolicy};
use crate::report::{RfcRef, Warning};
use crate::storage::{FetchCachePpPack, PackFile};
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path};
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 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 ObjectsOutput {
pub vrps: Vec<Vrp>,
pub aspas: Vec<AspaAttestation>,
pub warnings: Vec<Warning>,
pub stats: ObjectsStats,
pub audit: Vec<ObjectAuditEntry>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ObjectsStats {
pub roa_total: usize,
pub roa_ok: usize,
pub aspa_total: usize,
pub aspa_ok: usize,
/// 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,
}
/// Process objects from a fetch_cache_pp publication point pack using a known issuer CA certificate
/// and its effective resources (resolved via the resource-path, RFC 6487 §7.2).
pub fn process_fetch_cache_pp_pack_for_issuer(
pack: &FetchCachePpPack,
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,
) -> ObjectsOutput {
let mut warnings: Vec<Warning> = Vec::new();
let mut stats = ObjectsStats::default();
stats.roa_total = pack
.files
.iter()
.filter(|f| f.rsync_uri.ends_with(".roa"))
.count();
stats.aspa_total = pack
.files
.iter()
.filter(|f| f.rsync_uri.ends_with(".asa"))
.count();
let mut audit: Vec<ObjectAuditEntry> = Vec::new();
// Enforce that `manifest_bytes` is actually a manifest object.
let _manifest =
ManifestObject::decode_der(&pack.manifest_bytes).expect("fetch_cache_pp manifest decodes");
let crl_files = pack
.files
.iter()
.filter(|f| f.rsync_uri.ends_with(".crl"))
.map(|f| (f.rsync_uri.clone(), f.bytes.clone()))
.collect::<Vec<_>>();
// If the pack 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 pack).
if crl_files.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 fetch_cache_pp")
.with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")])
.with_context(&pack.manifest_rsync_uri),
);
for f in &pack.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 fetch_cache_pp".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 fetch_cache_pp".to_string(),
),
});
}
}
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
warnings,
stats,
audit,
};
}
let mut vrps: Vec<Vrp> = Vec::new();
let mut aspas: Vec<AspaAttestation> = Vec::new();
for (idx, file) in pack.files.iter().enumerate() {
if file.rsync_uri.ends_with(".roa") {
match process_roa_with_issuer(
file,
issuer_ca_der,
issuer_ca_rsync_uri,
&crl_files,
issuer_effective_ip,
issuer_effective_as,
validation_time,
) {
Ok(mut out) => {
stats.roa_ok += 1;
vrps.append(&mut out);
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 pack.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(&pack.manifest_rsync_uri),
);
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
warnings,
stats,
audit,
};
}
},
}
} else if file.rsync_uri.ends_with(".asa") {
match process_aspa_with_issuer(
file,
issuer_ca_der,
issuer_ca_rsync_uri,
&crl_files,
issuer_effective_ip,
issuer_effective_as,
validation_time,
) {
Ok(att) => {
stats.aspa_ok += 1;
aspas.push(att);
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 pack.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(&pack.manifest_rsync_uri),
);
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
warnings,
stats,
audit,
};
}
},
}
}
}
ObjectsOutput {
vrps,
aspas,
warnings,
stats,
audit,
}
}
#[derive(Debug, thiserror::Error)]
enum ObjectValidateError {
#[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 fetch_cache_pp (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
)]
MissingCrlInPack,
#[error(
"CRL referenced by CRLDistributionPoints not found in fetch_cache_pp: {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,
}
fn process_roa_with_issuer(
file: &PackFile,
issuer_ca_der: &[u8],
issuer_ca_rsync_uri: Option<&str>,
crl_files: &[(String, Vec<u8>)],
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
validation_time: time::OffsetDateTime,
) -> Result<Vec<Vrp>, ObjectValidateError> {
let roa = RoaObject::decode_der(&file.bytes)?;
roa.validate_embedded_ee_cert()?;
roa.signed_object.verify()?;
let ee_der = &roa.signed_object.signed_data.certificates[0].raw_der;
let ee_crldp_uris = roa.signed_object.signed_data.certificates[0]
.resource_cert
.tbs
.extensions
.crl_distribution_points_uris
.as_ref();
let (issuer_crl_rsync_uri, issuer_crl_der) =
choose_crl_for_certificate(ee_crldp_uris, crl_files)?;
let validated = validate_ee_cert_path(
ee_der,
issuer_ca_der,
&issuer_crl_der,
issuer_ca_rsync_uri,
Some(issuer_crl_rsync_uri.as_str()),
validation_time,
)?;
validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?;
Ok(roa_to_vrps(&roa))
}
fn process_aspa_with_issuer(
file: &PackFile,
issuer_ca_der: &[u8],
issuer_ca_rsync_uri: Option<&str>,
crl_files: &[(String, Vec<u8>)],
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
validation_time: time::OffsetDateTime,
) -> Result<AspaAttestation, ObjectValidateError> {
let aspa = AspaObject::decode_der(&file.bytes)?;
aspa.validate_embedded_ee_cert()?;
aspa.signed_object.verify()?;
let ee_der = &aspa.signed_object.signed_data.certificates[0].raw_der;
let ee_crldp_uris = aspa.signed_object.signed_data.certificates[0]
.resource_cert
.tbs
.extensions
.crl_distribution_points_uris
.as_ref();
let (issuer_crl_rsync_uri, issuer_crl_der) =
choose_crl_for_certificate(ee_crldp_uris, crl_files)?;
let validated = validate_ee_cert_path(
ee_der,
issuer_ca_der,
&issuer_crl_der,
issuer_ca_rsync_uri,
Some(issuer_crl_rsync_uri.as_str()),
validation_time,
)?;
validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?;
Ok(AspaAttestation {
customer_as_id: aspa.aspa.customer_as_id,
provider_as_ids: aspa.aspa.provider_as_ids.clone(),
})
}
fn choose_crl_for_certificate(
crldp_uris: Option<&Vec<url::Url>>,
crl_files: &[(String, Vec<u8>)],
) -> Result<(String, Vec<u8>), ObjectValidateError> {
if crl_files.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 let Some((uri, bytes)) = crl_files.iter().find(|(uri, _)| uri.as_str() == s) {
return Ok((uri.clone(), bytes.clone()));
}
}
Err(ObjectValidateError::CrlNotFound(
crldp_uris
.iter()
.map(|u| u.as_str())
.collect::<Vec<_>>()
.join(", "),
))
}
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>,
) -> 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(child_ip, parent_ip) {
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(child_as, parent_as) {
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_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
}
#[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));
*v = merge_ip_intervals(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));
*v = merge_ip_intervals(v);
}
Ok(m)
}
fn merge_ip_intervals(v: &[(Vec<u8>, Vec<u8>)]) -> Vec<(Vec<u8>, Vec<u8>)> {
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
for (min, max) in v {
let Some(last) = out.last_mut() else {
out.push((min.clone(), max.clone()));
continue;
};
if bytes_leq(min, &increment_bytes(&last.1)) {
if bytes_leq(&last.1, max) {
last.1 = max.clone();
}
continue;
}
out.push((min.clone(), max.clone()));
}
out
}
fn interval_is_covered(parent: &[(Vec<u8>, Vec<u8>)], min: &[u8], max: &[u8]) -> bool {
for (pmin, pmax) in parent {
if bytes_leq(pmin, min) && bytes_leq(max, pmax) {
return true;
}
if pmin.as_slice() > min {
break;
}
}
false
}
fn prefix_to_range(prefix: &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 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::data_model::rc::{
Afi, AsIdOrRange, AsIdentifierChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange,
IpPrefix, IpResourceSet,
};
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}"))
}
#[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 v = vec![
(vec![0, 0, 0, 0], vec![0, 0, 0, 10]),
(vec![0, 0, 0, 11], vec![0, 0, 0, 20]),
];
let merged = merge_ip_intervals(&v);
assert_eq!(merged, vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 20])]);
}
#[test]
fn choose_crl_for_certificate_reports_missing_crl_in_pack() {
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 err = choose_crl_for_certificate(ee_crldp_uris, &[]).unwrap_err();
assert!(matches!(err, ObjectValidateError::MissingCrlInPack));
}
#[test]
fn choose_crl_for_certificate_reports_missing_crldp_uris() {
let crl_a = ("rsync://example.test/a.crl".to_string(), vec![0x01]);
let err = choose_crl_for_certificate(None, &[crl_a]).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");
// Use two CRLs, only one matches the first CRLDP URI.
let other_crl_der = fixture_bytes(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
);
let matching_uri = ee_crldp_uris[0].as_str().to_string();
let matching_crl_der = vec![0x01, 0x02, 0x03];
let (uri, bytes) = choose_crl_for_certificate(
Some(ee_crldp_uris),
&[
("rsync://example.test/other.crl".to_string(), other_crl_der),
(matching_uri.clone(), matching_crl_der.clone()),
],
)
.unwrap();
assert_eq!(uri, matching_uri);
assert_eq!(bytes, matching_crl_der);
}
#[test]
fn choose_crl_for_certificate_reports_not_found_when_crldp_does_not_match_pack() {
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 err = choose_crl_for_certificate(
ee_crldp_uris,
&[("rsync://example.test/other.crl".to_string(), vec![0x01])],
)
.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 err = validate_ee_resources_subset(ee, None, None).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 err = validate_ee_resources_subset(ee, Some(&issuer_ip), None).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 err = validate_ee_resources_subset(ee, Some(&issuer_ip), None).unwrap_err();
assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset));
}
}