run tree from tal pass

This commit is contained in:
2026-02-10 12:09:59 +08:00
parent afc31c02ab
commit 6e135b9d7a
38 changed files with 1667 additions and 1313 deletions

View File

@@ -1,18 +1,29 @@
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,
ResourceCertKind, ResourceCertificate,
ResourceCertificate,
};
use crate::data_model::roa::{IpPrefix, RoaAfi, RoaDecodeError, RoaObject, RoaValidateError};
use crate::data_model::signed_object::SignedObjectVerifyError;
use crate::audit::{AuditObjectKind, AuditObjectResult, ObjectAuditEntry, sha256_hex_from_32};
use crate::policy::{Policy, SignedObjectFailurePolicy};
use crate::report::{RfcRef, Warning};
use crate::storage::{PackFile, VerifiedPublicationPointPack};
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path};
use std::collections::HashMap;
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 {
@@ -60,8 +71,16 @@ pub fn process_verified_publication_point_pack_for_issuer(
) -> 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();
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.
@@ -75,43 +94,42 @@ pub fn process_verified_publication_point_pack_for_issuer(
.map(|f| (f.rsync_uri.clone(), f.bytes.clone()))
.collect::<Vec<_>>();
let (issuer_crl_uri, issuer_crl_der) = match choose_crl_for_issuer(issuer_ca_der, &crl_files) {
Ok((uri, der)) => (uri, der),
Err(e) => {
stats.publication_point_dropped = true;
warnings.push(
Warning::new(format!("dropping publication point: {e}"))
.with_rfc_refs(&[RfcRef("RFC 6487 §5")])
.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 issuer CRL".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 issuer CRL".to_string()),
});
}
// 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 verified pack")
.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 verified pack".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 verified pack".to_string()),
});
}
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
warnings,
stats,
audit,
};
}
};
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();
@@ -121,9 +139,8 @@ pub fn process_verified_publication_point_pack_for_issuer(
match process_roa_with_issuer(
file,
issuer_ca_der,
&issuer_crl_der,
issuer_ca_rsync_uri,
Some(issuer_crl_uri.as_str()),
&crl_files,
issuer_effective_ip,
issuer_effective_as,
validation_time,
@@ -148,9 +165,11 @@ pub fn process_verified_publication_point_pack_for_issuer(
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(&[RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")])
.with_rfc_refs(&refs)
.with_context(&file.rsync_uri),
)
}
@@ -188,12 +207,14 @@ pub fn process_verified_publication_point_pack_for_issuer(
});
}
}
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(&[RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")])
.with_rfc_refs(&refs)
.with_context(&pack.manifest_rsync_uri),
);
return ObjectsOutput {
@@ -210,9 +231,8 @@ pub fn process_verified_publication_point_pack_for_issuer(
match process_aspa_with_issuer(
file,
issuer_ca_der,
&issuer_crl_der,
issuer_ca_rsync_uri,
Some(issuer_crl_uri.as_str()),
&crl_files,
issuer_effective_ip,
issuer_effective_as,
validation_time,
@@ -237,9 +257,11 @@ pub fn process_verified_publication_point_pack_for_issuer(
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(&[RfcRef("RFC 6488 §3")])
.with_rfc_refs(&refs)
.with_context(&file.rsync_uri),
)
}
@@ -277,12 +299,14 @@ pub fn process_verified_publication_point_pack_for_issuer(
});
}
}
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(&[RfcRef("RFC 6488 §3")])
.with_rfc_refs(&refs)
.with_context(&pack.manifest_rsync_uri),
);
return ObjectsOutput {
@@ -307,221 +331,6 @@ pub fn process_verified_publication_point_pack_for_issuer(
}
}
#[derive(Debug, thiserror::Error)]
pub enum ObjectsProcessError {
#[error(
"publication point dropped due to invalid signed object: {rsync_uri}: {detail} (policy=signed_object_failure_policy=drop_publication_point)"
)]
PublicationPointDropped { rsync_uri: String, detail: String },
}
pub trait IssuerCaCertificateResolver {
fn resolve_by_subject_dn(&self, subject_dn: &str) -> Option<Vec<u8>>;
}
pub fn process_verified_publication_point_pack(
pack: &VerifiedPublicationPointPack,
policy: &Policy,
issuer_resolver: &dyn IssuerCaCertificateResolver,
validation_time: time::OffsetDateTime,
) -> Result<ObjectsOutput, ObjectsProcessError> {
let mut warnings = 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();
// Parse manifest once (primarily to enforce that `manifest_bytes` really is a manifest object).
let _manifest =
ManifestObject::decode_der(&pack.manifest_bytes).expect("verified pack manifest decodes");
// Index CA certs found in the pack by subject DN (best-effort; packs may be incomplete).
let mut ca_certs_by_subject: HashMap<String, Vec<u8>> = HashMap::new();
for f in &pack.files {
if !f.rsync_uri.ends_with(".cer") {
continue;
}
let Ok(cert) = ResourceCertificate::decode_der(&f.bytes) else {
continue;
};
if cert.kind != ResourceCertKind::Ca {
continue;
}
ca_certs_by_subject.insert(cert.tbs.subject_dn.clone(), f.bytes.clone());
}
// Decode CRLs present in the pack (may be none in synthetic tests).
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<_>>();
let mut vrps = Vec::new();
let mut aspas = Vec::new();
for (idx, file) in pack.files.iter().enumerate() {
if file.rsync_uri.ends_with(".roa") {
match process_roa(
file,
&ca_certs_by_subject,
issuer_resolver,
&crl_files,
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()),
});
warnings.push(
Warning::new(format!("dropping invalid ROA: {}: {e}", file.rsync_uri))
.with_rfc_refs(&[RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")])
.with_context(&file.rsync_uri),
)
}
SignedObjectFailurePolicy::DropPublicationPoint => {
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(),
),
});
}
}
return Err(ObjectsProcessError::PublicationPointDropped {
rsync_uri: file.rsync_uri.clone(),
detail: e.to_string(),
});
}
},
}
} else if file.rsync_uri.ends_with(".asa") {
match process_aspa(
file,
&ca_certs_by_subject,
issuer_resolver,
&crl_files,
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()),
});
warnings.push(
Warning::new(format!("dropping invalid ASPA: {}: {e}", file.rsync_uri))
.with_rfc_refs(&[RfcRef("RFC 6488 §3")])
.with_context(&file.rsync_uri),
)
}
SignedObjectFailurePolicy::DropPublicationPoint => {
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(),
),
});
}
}
return Err(ObjectsProcessError::PublicationPointDropped {
rsync_uri: file.rsync_uri.clone(),
detail: e.to_string(),
});
}
},
}
}
}
Ok(ObjectsOutput {
vrps,
aspas,
warnings,
stats,
audit,
})
}
#[derive(Debug, thiserror::Error)]
enum ObjectValidateError {
#[error("ROA decode failed: {0}")]
@@ -542,11 +351,20 @@ enum ObjectValidateError {
#[error("EE certificate path validation failed: {0}")]
CertPath(#[from] CertPathError),
#[error("missing issuer CA certificate for subject DN: {0}")]
MissingIssuerCaCert(String),
#[error(
"certificate CRLDistributionPoints URIs missing (cannot select issuer CRL) (RFC 6487 §4.8.6)"
)]
MissingCrlDpUris,
#[error("no CRL available for issuer CA")]
MissingCrl,
#[error(
"no CRL available in verified pack (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
)]
MissingCrlInPack,
#[error(
"CRL referenced by CRLDistributionPoints not found in verified pack: {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)"
@@ -564,49 +382,11 @@ enum ObjectValidateError {
EeResourcesNotSubset,
}
fn process_roa(
file: &PackFile,
ca_certs_by_subject: &HashMap<String, Vec<u8>>,
issuer_resolver: &dyn IssuerCaCertificateResolver,
crl_files: &[(String, Vec<u8>)],
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_issuer_dn = roa.signed_object.signed_data.certificates[0]
.resource_cert
.tbs
.issuer_dn
.clone();
let issuer_ca_der = ca_certs_by_subject
.get(&ee_issuer_dn)
.cloned()
.or_else(|| issuer_resolver.resolve_by_subject_dn(&ee_issuer_dn))
.ok_or_else(|| ObjectValidateError::MissingIssuerCaCert(ee_issuer_dn.clone()))?;
let (crl_uri, crl_der) = choose_crl_for_issuer(&issuer_ca_der, crl_files)?;
validate_ee_cert_path(
ee_der,
&issuer_ca_der,
&crl_der,
None,
Some(crl_uri.as_str()),
validation_time,
)?;
Ok(roa_to_vrps(&roa))
}
fn process_roa_with_issuer(
file: &PackFile,
issuer_ca_der: &[u8],
issuer_crl_der: &[u8],
issuer_ca_rsync_uri: Option<&str>,
issuer_crl_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,
@@ -616,70 +396,33 @@ fn process_roa_with_issuer(
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_crl_der,
issuer_ca_rsync_uri,
issuer_crl_rsync_uri,
Some(issuer_crl_rsync_uri.as_str()),
validation_time,
)?;
validate_ee_resources_subset(
&validated.ee,
issuer_effective_ip,
issuer_effective_as,
)?;
validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?;
Ok(roa_to_vrps(&roa))
}
fn process_aspa(
file: &PackFile,
ca_certs_by_subject: &HashMap<String, Vec<u8>>,
issuer_resolver: &dyn IssuerCaCertificateResolver,
crl_files: &[(String, Vec<u8>)],
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_issuer_dn = aspa.signed_object.signed_data.certificates[0]
.resource_cert
.tbs
.issuer_dn
.clone();
let issuer_ca_der = ca_certs_by_subject
.get(&ee_issuer_dn)
.cloned()
.or_else(|| issuer_resolver.resolve_by_subject_dn(&ee_issuer_dn))
.ok_or_else(|| ObjectValidateError::MissingIssuerCaCert(ee_issuer_dn.clone()))?;
let (crl_uri, crl_der) = choose_crl_for_issuer(&issuer_ca_der, crl_files)?;
validate_ee_cert_path(
ee_der,
&issuer_ca_der,
&crl_der,
None,
Some(crl_uri.as_str()),
validation_time,
)?;
Ok(AspaAttestation {
customer_as_id: aspa.aspa.customer_as_id,
provider_as_ids: aspa.aspa.provider_as_ids.clone(),
})
}
fn process_aspa_with_issuer(
file: &PackFile,
issuer_ca_der: &[u8],
issuer_crl_der: &[u8],
issuer_ca_rsync_uri: Option<&str>,
issuer_crl_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,
@@ -689,20 +432,24 @@ fn process_aspa_with_issuer(
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_crl_der,
issuer_ca_rsync_uri,
issuer_crl_rsync_uri,
Some(issuer_crl_rsync_uri.as_str()),
validation_time,
)?;
validate_ee_resources_subset(
&validated.ee,
issuer_effective_ip,
issuer_effective_as,
)?;
validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?;
Ok(AspaAttestation {
customer_as_id: aspa.aspa.customer_as_id,
@@ -710,42 +457,31 @@ fn process_aspa_with_issuer(
})
}
fn choose_crl_for_issuer(
issuer_ca_der: &[u8],
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::MissingCrl);
return Err(ObjectValidateError::MissingCrlInPack);
}
let issuer_tbs = ResourceCertificate::decode_der(issuer_ca_der)
.ok()
.map(|c| c.tbs);
let Some(issuer_tbs) = issuer_tbs else {
return Ok(crl_files[0].clone());
let Some(crldp_uris) = crldp_uris else {
return Err(ObjectValidateError::MissingCrlDpUris);
};
if let Some(uris) = issuer_tbs.extensions.crl_distribution_points_uris.as_ref() {
for u in 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()));
}
}
}
for (uri, bytes) in crl_files {
let Ok(crl) = crate::data_model::crl::RpkixCrl::decode_der(bytes) else {
continue;
};
if crl.issuer_dn == issuer_tbs.subject_dn {
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()));
}
}
// Fall back to the first CRL when the pack is incomplete or uses a different DN string
// representation. Signature binding is still validated in `validate_ee_cert_path`.
Ok(crl_files[0].clone())
Err(ObjectValidateError::CrlNotFound(
crldp_uris
.iter()
.map(|u| u.as_str())
.collect::<Vec<_>>()
.join(", "),
))
}
fn validate_ee_resources_subset(
@@ -1039,7 +775,10 @@ fn roa_afi_to_string(afi: RoaAfi) -> &'static str {
#[cfg(test)]
mod tests {
use super::*;
use crate::data_model::rc::{Afi, AsIdOrRange, AsIdentifierChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, IpResourceSet};
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))
@@ -1056,10 +795,9 @@ mod tests {
#[test]
fn as_choice_subset_rejects_inherit() {
let child = Some(&AsIdentifierChoice::Inherit);
let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
min: 1,
max: 10,
}]));
let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Range { min: 1, max: 10 },
]));
assert!(!as_choice_subset(child, parent));
}
@@ -1069,10 +807,9 @@ mod tests {
AsIdOrRange::Id(5),
AsIdOrRange::Range { min: 7, max: 9 },
]));
let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
min: 1,
max: 10,
}]));
let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Range { min: 1, max: 10 },
]));
assert!(as_choice_subset(child, parent));
}
@@ -1153,82 +890,84 @@ mod tests {
}
#[test]
fn choose_crl_for_issuer_reports_missing_crl() {
let issuer_ca_der = fixture_bytes(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
);
let err = choose_crl_for_issuer(&issuer_ca_der, &[]).unwrap_err();
assert!(matches!(err, ObjectValidateError::MissingCrl));
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_issuer_falls_back_to_first_when_issuer_ca_is_not_decodable() {
let invalid_issuer_ca_der = vec![0x01, 0x02, 0x03];
fn choose_crl_for_certificate_reports_missing_crldp_uris() {
let crl_a = ("rsync://example.test/a.crl".to_string(), vec![0x01]);
let crl_b = ("rsync://example.test/b.crl".to_string(), vec![0x02]);
let (uri, bytes) =
choose_crl_for_issuer(&invalid_issuer_ca_der, &[crl_a.clone(), crl_b]).unwrap();
assert_eq!(uri, crl_a.0);
assert_eq!(bytes, crl_a.1);
let err = choose_crl_for_certificate(None, &[crl_a]).unwrap_err();
assert!(matches!(err, ObjectValidateError::MissingCrlDpUris));
}
#[test]
fn choose_crl_for_issuer_prefers_matching_crldp_uri() {
let issuer_ca_der = fixture_bytes(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
);
let matching_crl_der = fixture_bytes(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl",
);
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_issuer(
&issuer_ca_der,
let (uri, bytes) = choose_crl_for_certificate(
Some(ee_crldp_uris),
&[
(
"rsync://example.test/other.crl".to_string(),
other_crl_der,
),
(
"rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl".to_string(),
matching_crl_der.clone(),
),
("rsync://example.test/other.crl".to_string(), other_crl_der),
(matching_uri.clone(), matching_crl_der.clone()),
],
)
.unwrap();
assert_eq!(
uri,
"rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl"
);
assert_eq!(uri, matching_uri);
assert_eq!(bytes, matching_crl_der);
}
#[test]
fn choose_crl_for_issuer_falls_back_to_first_when_no_dn_match() {
let issuer_ca_der = fixture_bytes(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
);
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 (uri, bytes) = choose_crl_for_issuer(
&issuer_ca_der,
&[
("rsync://example.test/a.crl".to_string(), vec![0x01]),
("rsync://example.test/b.crl".to_string(), vec![0x02]),
],
let err = choose_crl_for_certificate(
ee_crldp_uris,
&[("rsync://example.test/other.crl".to_string(), vec![0x01])],
)
.unwrap();
assert_eq!(uri, "rsync://example.test/a.crl");
assert_eq!(bytes, 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_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;
@@ -1265,9 +1004,8 @@ mod tests {
#[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_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;
@@ -1286,8 +1024,7 @@ mod tests {
}],
};
let err =
validate_ee_resources_subset(ee, Some(&issuer_ip), None).unwrap_err();
let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None).unwrap_err();
assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset));
}
}