手动执行全量同步
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
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;
|
||||
@@ -10,13 +11,35 @@ 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};
|
||||
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_prevalidated_issuer};
|
||||
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")];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct VerifiedIssuerCrl {
|
||||
crl: crate::data_model::crl::RpkixCrl,
|
||||
revoked_serials: std::collections::HashSet<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum CachedIssuerCrl {
|
||||
Pending(Vec<u8>),
|
||||
Ok(VerifiedIssuerCrl),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
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,
|
||||
@@ -68,6 +91,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
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 {
|
||||
let mut warnings: Vec<Warning> = Vec::new();
|
||||
let mut stats = ObjectsStats::default();
|
||||
@@ -87,16 +111,105 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
let _manifest =
|
||||
ManifestObject::decode_der(&pack.manifest_bytes).expect("fetch_cache_pp manifest decodes");
|
||||
|
||||
let crl_files = pack
|
||||
// Decode issuer CA once; if it fails we cannot validate ROA/ASPA EE certificates.
|
||||
let issuer_ca = match ResourceCertificate::decode_der(issuer_ca_der) {
|
||||
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(&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: 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(),
|
||||
warnings,
|
||||
stats,
|
||||
audit,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 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(&pack.manifest_rsync_uri),
|
||||
);
|
||||
return ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings,
|
||||
stats,
|
||||
audit,
|
||||
};
|
||||
}
|
||||
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(&pack.manifest_rsync_uri),
|
||||
);
|
||||
return ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings,
|
||||
stats,
|
||||
audit,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let mut crl_cache: std::collections::HashMap<String, CachedIssuerCrl> = pack
|
||||
.files
|
||||
.iter()
|
||||
.filter(|f| f.rsync_uri.ends_with(".crl"))
|
||||
.map(|f| (f.rsync_uri.clone(), f.bytes.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
.map(|f| {
|
||||
(
|
||||
f.rsync_uri.clone(),
|
||||
CachedIssuerCrl::Pending(f.bytes.clone()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let issuer_resources_index =
|
||||
build_issuer_resources_index(issuer_effective_ip, issuer_effective_as);
|
||||
|
||||
// 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) {
|
||||
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 fetch_cache_pp")
|
||||
@@ -110,9 +223,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
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(),
|
||||
),
|
||||
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 {
|
||||
@@ -120,9 +231,7 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
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(),
|
||||
),
|
||||
detail: Some("skipped due to missing CRL files in fetch_cache_pp".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -140,14 +249,19 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
|
||||
for (idx, file) in pack.files.iter().enumerate() {
|
||||
if file.rsync_uri.ends_with(".roa") {
|
||||
let _t = timing.as_ref().map(|t| t.span_phase("objects_roa_total"));
|
||||
match process_roa_with_issuer(
|
||||
file,
|
||||
issuer_ca_der,
|
||||
&issuer_ca,
|
||||
&issuer_spki,
|
||||
issuer_ca_rsync_uri,
|
||||
&crl_files,
|
||||
&mut crl_cache,
|
||||
&issuer_resources_index,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
) {
|
||||
Ok(mut out) => {
|
||||
stats.roa_ok += 1;
|
||||
@@ -232,14 +346,19 @@ pub fn process_fetch_cache_pp_pack_for_issuer(
|
||||
},
|
||||
}
|
||||
} 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,
|
||||
issuer_ca_der,
|
||||
&issuer_ca,
|
||||
&issuer_spki,
|
||||
issuer_ca_rsync_uri,
|
||||
&crl_files,
|
||||
&mut crl_cache,
|
||||
&issuer_resources_index,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
validation_time,
|
||||
timing,
|
||||
) {
|
||||
Ok(att) => {
|
||||
stats.aspa_ok += 1;
|
||||
@@ -389,15 +508,33 @@ enum ObjectValidateError {
|
||||
fn process_roa_with_issuer(
|
||||
file: &PackFile,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca: &ResourceCertificate,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
crl_files: &[(String, Vec<u8>)],
|
||||
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>,
|
||||
) -> Result<Vec<Vrp>, ObjectValidateError> {
|
||||
let _decode = timing
|
||||
.as_ref()
|
||||
.map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
|
||||
let roa = RoaObject::decode_der(&file.bytes)?;
|
||||
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_der = &roa.signed_object.signed_data.certificates[0].raw_der;
|
||||
let ee_crldp_uris = roa.signed_object.signed_data.certificates[0]
|
||||
@@ -406,18 +543,34 @@ fn process_roa_with_issuer(
|
||||
.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(
|
||||
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"));
|
||||
let ee = validate_ee_cert_path_with_prevalidated_issuer(
|
||||
ee_der,
|
||||
issuer_ca_der,
|
||||
&issuer_crl_der,
|
||||
issuer_ca,
|
||||
issuer_spki,
|
||||
&verified_crl.crl,
|
||||
&verified_crl.revoked_serials,
|
||||
issuer_ca_rsync_uri,
|
||||
Some(issuer_crl_rsync_uri.as_str()),
|
||||
Some(issuer_crl_rsync_uri),
|
||||
validation_time,
|
||||
)?;
|
||||
drop(_cert_path);
|
||||
|
||||
validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?;
|
||||
let _subset = timing
|
||||
.as_ref()
|
||||
.map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total"));
|
||||
validate_ee_resources_subset(
|
||||
&ee,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
issuer_resources_index,
|
||||
)?;
|
||||
drop(_subset);
|
||||
|
||||
Ok(roa_to_vrps(&roa))
|
||||
}
|
||||
@@ -425,15 +578,33 @@ fn process_roa_with_issuer(
|
||||
fn process_aspa_with_issuer(
|
||||
file: &PackFile,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca: &ResourceCertificate,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
crl_files: &[(String, Vec<u8>)],
|
||||
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>,
|
||||
) -> Result<AspaAttestation, ObjectValidateError> {
|
||||
let _decode = timing
|
||||
.as_ref()
|
||||
.map(|t| t.span_phase("objects_aspa_decode_and_validate_total"));
|
||||
let aspa = AspaObject::decode_der(&file.bytes)?;
|
||||
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_der = &aspa.signed_object.signed_data.certificates[0].raw_der;
|
||||
let ee_crldp_uris = aspa.signed_object.signed_data.certificates[0]
|
||||
@@ -442,18 +613,34 @@ fn process_aspa_with_issuer(
|
||||
.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(
|
||||
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"));
|
||||
let ee = validate_ee_cert_path_with_prevalidated_issuer(
|
||||
ee_der,
|
||||
issuer_ca_der,
|
||||
&issuer_crl_der,
|
||||
issuer_ca,
|
||||
issuer_spki,
|
||||
&verified_crl.crl,
|
||||
&verified_crl.revoked_serials,
|
||||
issuer_ca_rsync_uri,
|
||||
Some(issuer_crl_rsync_uri.as_str()),
|
||||
Some(issuer_crl_rsync_uri),
|
||||
validation_time,
|
||||
)?;
|
||||
drop(_cert_path);
|
||||
|
||||
validate_ee_resources_subset(&validated.ee, issuer_effective_ip, issuer_effective_as)?;
|
||||
let _subset = timing
|
||||
.as_ref()
|
||||
.map(|t| t.span_phase("objects_aspa_validate_ee_resources_subset_total"));
|
||||
validate_ee_resources_subset(
|
||||
&ee,
|
||||
issuer_effective_ip,
|
||||
issuer_effective_as,
|
||||
issuer_resources_index,
|
||||
)?;
|
||||
drop(_subset);
|
||||
|
||||
Ok(AspaAttestation {
|
||||
customer_as_id: aspa.aspa.customer_as_id,
|
||||
@@ -461,11 +648,11 @@ fn process_aspa_with_issuer(
|
||||
})
|
||||
}
|
||||
|
||||
fn choose_crl_for_certificate(
|
||||
crldp_uris: Option<&Vec<String>>,
|
||||
crl_files: &[(String, Vec<u8>)],
|
||||
) -> Result<(String, Vec<u8>), ObjectValidateError> {
|
||||
if crl_files.is_empty() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -475,8 +662,8 @@ fn choose_crl_for_certificate(
|
||||
|
||||
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()));
|
||||
if crl_cache.contains_key(s) {
|
||||
return Ok(s);
|
||||
}
|
||||
}
|
||||
Err(ObjectValidateError::CrlNotFound(
|
||||
@@ -488,16 +675,52 @@ fn choose_crl_for_certificate(
|
||||
))
|
||||
}
|
||||
|
||||
fn ensure_issuer_crl_verified<'a>(
|
||||
crl_rsync_uri: &str,
|
||||
crl_cache: &'a mut std::collections::HashMap<String, CachedIssuerCrl>,
|
||||
issuer_ca_der: &[u8],
|
||||
) -> Result<&'a VerifiedIssuerCrl, CertPathError> {
|
||||
let entry = crl_cache
|
||||
.get_mut(crl_rsync_uri)
|
||||
.expect("CRL must exist in cache");
|
||||
match entry {
|
||||
CachedIssuerCrl::Ok(v) => Ok(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(VerifiedIssuerCrl {
|
||||
crl,
|
||||
revoked_serials,
|
||||
});
|
||||
match entry {
|
||||
CachedIssuerCrl::Ok(v) => Ok(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(child_ip, parent_ip) {
|
||||
if !ip_resources_is_subset_indexed(child_ip, parent_ip, issuer_resources_index) {
|
||||
return Err(ObjectValidateError::EeResourcesNotSubset);
|
||||
}
|
||||
}
|
||||
@@ -506,7 +729,7 @@ fn validate_ee_resources_subset(
|
||||
let Some(parent_as) = issuer_effective_as else {
|
||||
return Err(ObjectValidateError::MissingIssuerEffectiveAs);
|
||||
};
|
||||
if !as_resources_is_subset(child_as, parent_as) {
|
||||
if !as_resources_is_subset_indexed(child_as, parent_as, issuer_resources_index) {
|
||||
return Err(ObjectValidateError::EeResourcesNotSubset);
|
||||
}
|
||||
}
|
||||
@@ -519,6 +742,40 @@ fn as_resources_is_subset(child: &AsResourceSet, parent: &AsResourceSet) -> bool
|
||||
&& 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>,
|
||||
@@ -614,6 +871,45 @@ fn ip_resources_is_subset(
|
||||
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,
|
||||
@@ -650,7 +946,7 @@ fn ip_resources_to_merged_intervals(
|
||||
|
||||
for (_afi, v) in m.iter_mut() {
|
||||
v.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
*v = merge_ip_intervals(v);
|
||||
merge_ip_intervals_in_place(v);
|
||||
}
|
||||
|
||||
m
|
||||
@@ -683,28 +979,31 @@ fn ip_resources_to_merged_intervals_strict(
|
||||
|
||||
for (_afi, v) in m.iter_mut() {
|
||||
v.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
*v = merge_ip_intervals(v);
|
||||
merge_ip_intervals_in_place(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 {
|
||||
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.clone(), max.clone()));
|
||||
out.push((min, max));
|
||||
continue;
|
||||
};
|
||||
if bytes_leq(min, &increment_bytes(&last.1)) {
|
||||
if bytes_leq(&last.1, max) {
|
||||
last.1 = max.clone();
|
||||
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.clone(), max.clone()));
|
||||
out.push((min, max));
|
||||
}
|
||||
out
|
||||
*v = out;
|
||||
}
|
||||
|
||||
fn interval_is_covered(parent: &[(Vec<u8>, Vec<u8>)], min: &[u8], max: &[u8]) -> bool {
|
||||
@@ -719,6 +1018,23 @@ fn interval_is_covered(parent: &[(Vec<u8>, Vec<u8>)], min: &[u8], max: &[u8]) ->
|
||||
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();
|
||||
@@ -752,6 +1068,78 @@ fn increment_bytes(v: &[u8]) -> Vec<u8> {
|
||||
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();
|
||||
@@ -783,6 +1171,7 @@ mod tests {
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange,
|
||||
IpPrefix, IpResourceSet,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn fixture_bytes(path: &str) -> Vec<u8> {
|
||||
std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path))
|
||||
@@ -885,12 +1274,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn merge_ip_intervals_merges_contiguous() {
|
||||
let v = vec![
|
||||
let mut 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])]);
|
||||
merge_ip_intervals_in_place(&mut v);
|
||||
assert_eq!(v, vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 20])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -904,14 +1293,19 @@ mod tests {
|
||||
.extensions
|
||||
.crl_distribution_points_uris
|
||||
.as_ref();
|
||||
let err = choose_crl_for_certificate(ee_crldp_uris, &[]).unwrap_err();
|
||||
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 crl_a = ("rsync://example.test/a.crl".to_string(), vec![0x01]);
|
||||
let err = choose_crl_for_certificate(None, &[crl_a]).unwrap_err();
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -928,24 +1322,16 @@ mod tests {
|
||||
.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();
|
||||
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);
|
||||
assert_eq!(bytes, matching_crl_der);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -960,11 +1346,12 @@ mod tests {
|
||||
.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();
|
||||
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(_)));
|
||||
}
|
||||
|
||||
@@ -976,7 +1363,8 @@ mod tests {
|
||||
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();
|
||||
let idx = IssuerResourcesIndex::default();
|
||||
let err = validate_ee_resources_subset(ee, None, None, &idx).unwrap_err();
|
||||
assert!(matches!(err, ObjectValidateError::MissingIssuerEffectiveIp));
|
||||
}
|
||||
|
||||
@@ -1002,7 +1390,8 @@ mod tests {
|
||||
}],
|
||||
};
|
||||
|
||||
let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None).unwrap_err();
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -1028,7 +1417,8 @@ mod tests {
|
||||
}],
|
||||
};
|
||||
|
||||
let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None).unwrap_err();
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user