1035 lines
38 KiB
Rust
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));
|
|
}
|
|
}
|