手动执行全量同步

This commit is contained in:
2026-03-04 11:12:53 +08:00
parent 0f3d65254e
commit 6276d13814
54 changed files with 3879 additions and 670 deletions

View File

@@ -8,6 +8,8 @@ use crate::data_model::rc::{
use x509_parser::prelude::{FromDer, X509Certificate};
use crate::validation::x509_name::x509_names_equivalent;
use std::collections::HashSet;
use x509_parser::x509::SubjectPublicKeyInfo;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ValidatedSubordinateCa {
@@ -18,6 +20,13 @@ pub struct ValidatedSubordinateCa {
pub effective_as_resources: Option<AsResourceSet>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ValidatedSubordinateCaLite {
pub child_ca: ResourceCertificate,
pub effective_ip_resources: Option<IpResourceSet>,
pub effective_as_resources: Option<AsResourceSet>,
}
#[derive(Debug, thiserror::Error)]
pub enum CaPathError {
#[error("child CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
@@ -50,6 +59,14 @@ pub enum CaPathError {
#[error("child CA certificate signature verification failed: {0} (RFC 5280 §6.1)")]
ChildSignatureInvalid(String),
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
IssuerSpkiParse(String),
#[error(
"trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)"
)]
IssuerSpkiTrailingBytes(usize),
#[error("certificate not valid at validation_time (RFC 5280 §4.1.2.5; RFC 5280 §6.1)")]
CertificateNotValidAtTime,
@@ -136,6 +153,7 @@ pub fn validate_subordinate_ca_cert(
if issuer_ca.kind != ResourceCertKind::Ca {
return Err(CaPathError::IssuerNotCa);
}
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
return Err(CaPathError::IssuerSubjectMismatch {
@@ -162,8 +180,9 @@ pub fn validate_subordinate_ca_cert(
return Err(CaPathError::CertificateNotValidAtTime);
}
verify_cert_signature_with_issuer(child_ca_der, issuer_ca_der)?;
validate_child_ca_key_usage(child_ca_der)?;
let child_x509 = parse_x509_cert(child_ca_der)?;
verify_child_signature(&child_x509, &issuer_spki)?;
validate_child_ca_key_usage(&child_x509)?;
let issuer_crl = RpkixCrl::decode_der(issuer_crl_der)?;
issuer_crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
@@ -196,6 +215,116 @@ pub fn validate_subordinate_ca_cert(
})
}
/// Validate a subordinate child CA using *pre-decoded issuer CA* and *pre-decoded+verified issuer CRL*.
///
/// This avoids repeating issuer CA decode and issuer CRL decode+signature verification for every
/// child CA certificate discovered in a publication point.
pub fn validate_subordinate_ca_cert_with_prevalidated_issuer(
child_ca_der: &[u8],
child_ca: ResourceCertificate,
issuer_ca: &ResourceCertificate,
issuer_spki: &SubjectPublicKeyInfo<'_>,
issuer_crl: &RpkixCrl,
issuer_crl_revoked_serials: &HashSet<Vec<u8>>,
issuer_ca_rsync_uri: Option<&str>,
issuer_crl_rsync_uri: &str,
issuer_effective_ip: Option<&IpResourceSet>,
issuer_effective_as: Option<&AsResourceSet>,
validation_time: time::OffsetDateTime,
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
if child_ca.kind != ResourceCertKind::Ca {
return Err(CaPathError::ChildNotCa);
}
if issuer_ca.kind != ResourceCertKind::Ca {
return Err(CaPathError::IssuerNotCa);
}
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
return Err(CaPathError::IssuerSubjectMismatch {
child_issuer_dn: child_ca.tbs.issuer_name.to_string(),
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
});
}
validate_child_aki_matches_issuer_ski(&child_ca, issuer_ca)?;
if let Some(expected_issuer_uri) = issuer_ca_rsync_uri {
validate_child_aia_points_to_issuer_uri(&child_ca, expected_issuer_uri)?;
}
validate_child_crldp_contains_issuer_crl_uri(&child_ca, issuer_crl_rsync_uri)?;
if !time_within_validity(
validation_time,
child_ca.tbs.validity_not_before,
child_ca.tbs.validity_not_after,
) || !time_within_validity(
validation_time,
issuer_ca.tbs.validity_not_before,
issuer_ca.tbs.validity_not_after,
) {
return Err(CaPathError::CertificateNotValidAtTime);
}
let child_x509 = parse_x509_cert(child_ca_der)?;
verify_child_signature(&child_x509, issuer_spki)?;
validate_child_ca_key_usage(&child_x509)?;
if !crl_valid_at_time(issuer_crl, validation_time) {
return Err(CaPathError::CrlNotValidAtTime);
}
let serial = BigUnsigned::from_biguint(&child_ca.tbs.serial_number);
if issuer_crl_revoked_serials.contains(&serial.bytes_be) {
return Err(CaPathError::ChildRevoked);
}
let effective_ip_resources = resolve_child_ip_resources(
child_ca.tbs.extensions.ip_resources.as_ref(),
issuer_effective_ip,
)?;
let effective_as_resources = resolve_child_as_resources(
child_ca.tbs.extensions.as_resources.as_ref(),
issuer_effective_as,
)?;
if effective_ip_resources.is_none() && effective_as_resources.is_none() {
return Err(CaPathError::ResourcesMissing);
}
Ok(ValidatedSubordinateCaLite {
child_ca,
effective_ip_resources,
effective_as_resources,
})
}
fn parse_subject_pki_from_der(der: &[u8]) -> Result<SubjectPublicKeyInfo<'_>, CaPathError> {
let (rem, spki) = SubjectPublicKeyInfo::from_der(der)
.map_err(|e| CaPathError::IssuerSpkiParse(e.to_string()))?;
if !rem.is_empty() {
return Err(CaPathError::IssuerSpkiTrailingBytes(rem.len()));
}
Ok(spki)
}
fn parse_x509_cert(der: &[u8]) -> Result<X509Certificate<'_>, CaPathError> {
let (rem, cert) = X509Certificate::from_der(der)
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CaPathError::ChildSignatureInvalid(
"trailing bytes after child certificate".to_string(),
));
}
Ok(cert)
}
fn verify_child_signature(
child: &X509Certificate<'_>,
issuer_spki: &SubjectPublicKeyInfo<'_>,
) -> Result<(), CaPathError> {
child
.verify_signature(Some(issuer_spki))
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
}
fn validate_child_aki_matches_issuer_ski(
child: &ResourceCertificate,
issuer: &ResourceCertificate,
@@ -238,15 +367,7 @@ fn validate_child_crldp_contains_issuer_crl_uri(
Ok(())
}
fn validate_child_ca_key_usage(child_ca_der: &[u8]) -> Result<(), CaPathError> {
let (rem, cert) = X509Certificate::from_der(child_ca_der)
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CaPathError::ChildSignatureInvalid(
"trailing bytes after child CA certificate".to_string(),
));
}
fn validate_child_ca_key_usage(cert: &X509Certificate<'_>) -> Result<(), CaPathError> {
let mut ku_critical: Option<bool> = None;
for ext in cert.extensions() {
if ext.oid.as_bytes() == OID_KEY_USAGE_RAW {
@@ -286,30 +407,6 @@ fn validate_child_ca_key_usage(child_ca_der: &[u8]) -> Result<(), CaPathError> {
Ok(())
}
fn verify_cert_signature_with_issuer(
child_der: &[u8],
issuer_ca_der: &[u8],
) -> Result<(), CaPathError> {
let (rem, child) = X509Certificate::from_der(child_der)
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CaPathError::ChildSignatureInvalid(
"trailing bytes after child certificate".to_string(),
));
}
let (rem, issuer) = X509Certificate::from_der(issuer_ca_der)
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CaPathError::ChildSignatureInvalid(
"trailing bytes after issuer certificate".to_string(),
));
}
child
.verify_signature(Some(&issuer.tbs_certificate.subject_pki))
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
}
fn time_within_validity(
t: time::OffsetDateTime,
not_before: time::OffsetDateTime,
@@ -691,13 +788,13 @@ fn bytes_leq(a: &[u8], b: &[u8]) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::data_model::common::X509NameDer;
use crate::data_model::rc::{
Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpResourceSet,
};
use crate::data_model::rc::{
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
};
use crate::data_model::common::X509NameDer;
use der_parser::num_bigint::BigUint;
use std::process::Command;
fn dummy_cert(
@@ -823,7 +920,10 @@ mod tests {
};
// Issuer self-signed.
run(Command::new("openssl").args(["genrsa", "-out"]).arg(&issuer_key).arg("2048"));
run(Command::new("openssl")
.args(["genrsa", "-out"])
.arg(&issuer_key)
.arg("2048"));
run(Command::new("openssl")
.args(["req", "-new", "-key"])
.arg(&issuer_key)
@@ -843,7 +943,10 @@ mod tests {
.arg(&issuer_der));
// Child signed by issuer.
run(Command::new("openssl").args(["genrsa", "-out"]).arg(&child_key).arg("2048"));
run(Command::new("openssl")
.args(["genrsa", "-out"])
.arg(&child_key)
.arg("2048"));
run(Command::new("openssl")
.args(["req", "-new", "-key"])
.arg(&child_key)
@@ -865,7 +968,10 @@ mod tests {
.arg(&child_der));
// Other self-signed issuer.
run(Command::new("openssl").args(["genrsa", "-out"]).arg(&other_key).arg("2048"));
run(Command::new("openssl")
.args(["genrsa", "-out"])
.arg(&other_key)
.arg("2048"));
run(Command::new("openssl")
.args(["req", "-new", "-key"])
.arg(&other_key)
@@ -1116,20 +1222,23 @@ mod tests {
td.path(),
Some("keyUsage = critical, keyCertSign, cRLSign"),
);
validate_child_ca_key_usage(&der).expect("key usage ok");
let cert = parse_x509_cert(&der).expect("x509 parse ok");
validate_child_ca_key_usage(&cert).expect("key usage ok");
}
#[test]
fn validate_child_ca_key_usage_rejects_missing_noncritical_and_invalid_bits() {
let td = tempfile::tempdir().expect("tempdir");
let missing = write_cert_der_with_addext(td.path(), None);
let err = validate_child_ca_key_usage(&missing).unwrap_err();
let cert = parse_x509_cert(&missing).expect("x509 parse ok");
let err = validate_child_ca_key_usage(&cert).unwrap_err();
assert!(matches!(err, CaPathError::KeyUsageMissing), "{err}");
let td = tempfile::tempdir().expect("tempdir");
let noncritical =
write_cert_der_with_addext(td.path(), Some("keyUsage = keyCertSign, cRLSign"));
let err = validate_child_ca_key_usage(&noncritical).unwrap_err();
let cert = parse_x509_cert(&noncritical).expect("x509 parse ok");
let err = validate_child_ca_key_usage(&cert).unwrap_err();
assert!(matches!(err, CaPathError::KeyUsageNotCritical), "{err}");
let td = tempfile::tempdir().expect("tempdir");
@@ -1137,7 +1246,8 @@ mod tests {
td.path(),
Some("keyUsage = critical, keyCertSign, cRLSign, digitalSignature"),
);
let err = validate_child_ca_key_usage(&invalid).unwrap_err();
let cert = parse_x509_cert(&invalid).expect("x509 parse ok");
let err = validate_child_ca_key_usage(&cert).unwrap_err();
assert!(matches!(err, CaPathError::KeyUsageInvalidBits), "{err}");
}
@@ -1145,9 +1255,17 @@ mod tests {
fn verify_cert_signature_with_issuer_accepts_valid_chain_and_rejects_wrong_issuer() {
let td = tempfile::tempdir().expect("tempdir");
let (issuer, child, other) = gen_issuer_and_child_der(td.path());
verify_cert_signature_with_issuer(&child, &issuer).expect("signature ok");
let err = verify_cert_signature_with_issuer(&child, &other).unwrap_err();
assert!(matches!(err, CaPathError::ChildSignatureInvalid(_)), "{err}");
let issuer_cert = parse_x509_cert(&issuer).expect("x509 parse issuer");
let child_cert = parse_x509_cert(&child).expect("x509 parse child");
verify_child_signature(&child_cert, &issuer_cert.tbs_certificate.subject_pki)
.expect("signature ok");
let other_cert = parse_x509_cert(&other).expect("x509 parse other");
let err = verify_child_signature(&child_cert, &other_cert.tbs_certificate.subject_pki)
.unwrap_err();
assert!(
matches!(err, CaPathError::ChildSignatureInvalid(_)),
"{err}"
);
}
#[test]
@@ -1214,10 +1332,9 @@ mod tests {
assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}");
let parent_as = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
min: 1,
max: 100,
}])),
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Range { min: 1, max: 100 },
])),
rdi: None,
};
let child_as_inherit = AsResourceSet {
@@ -1238,7 +1355,9 @@ mod tests {
.expect("some");
let child_as_bad = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(200)])),
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
200,
)])),
rdi: None,
};
let err = resolve_child_as_resources(Some(&child_as_bad), Some(&parent_as)).unwrap_err();

View File

@@ -6,6 +6,8 @@ use crate::data_model::rc::{
use x509_parser::prelude::{FromDer, X509Certificate};
use crate::validation::x509_name::x509_names_equivalent;
use std::collections::HashSet;
use x509_parser::x509::SubjectPublicKeyInfo;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ValidatedEeCertPath {
@@ -44,6 +46,14 @@ pub enum CertPathError {
#[error("EE certificate signature verification failed: {0} (RFC 5280 §6.1)")]
EeSignatureInvalid(String),
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
IssuerSpkiParse(String),
#[error(
"trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)"
)]
IssuerSpkiTrailingBytes(usize),
#[error("EE KeyUsage extension missing (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")]
KeyUsageMissing,
@@ -113,6 +123,7 @@ pub fn validate_ee_cert_path(
if issuer_ca.kind != ResourceCertKind::Ca {
return Err(CertPathError::IssuerNotCa);
}
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
return Err(CertPathError::IssuerSubjectMismatch {
@@ -141,8 +152,9 @@ pub fn validate_ee_cert_path(
return Err(CertPathError::CertificateNotValidAtTime);
}
verify_cert_signature_with_issuer(ee_cert_der, issuer_ca_der)?;
validate_ee_key_usage(ee_cert_der)?;
let ee_x509 = parse_x509_cert(ee_cert_der)?;
verify_ee_cert_signature(&ee_x509, &issuer_spki)?;
validate_ee_key_usage(&ee_x509)?;
let issuer_crl = RpkixCrl::decode_der(issuer_crl_der)?;
issuer_crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
@@ -161,6 +173,104 @@ pub fn validate_ee_cert_path(
})
}
/// Validate the EE certificate path using a *pre-decoded issuer CA* and a *pre-decoded and
/// pre-verified issuer CRL*.
///
/// This is a performance-oriented helper for stage2 serial runs: it avoids repeating issuer CA
/// decode and issuer CRL decode+signature verification for every signed object in a publication point.
///
/// The caller must ensure:
/// - `issuer_ca` corresponds to `issuer_ca_der`
/// - `issuer_crl` cryptographic signature/binding has already been verified against `issuer_ca_der`
pub fn validate_ee_cert_path_with_prevalidated_issuer(
ee_cert_der: &[u8],
issuer_ca: &ResourceCertificate,
issuer_spki: &SubjectPublicKeyInfo<'_>,
issuer_crl: &RpkixCrl,
issuer_crl_revoked_serials: &HashSet<Vec<u8>>,
issuer_ca_rsync_uri: Option<&str>,
issuer_crl_rsync_uri: Option<&str>,
validation_time: time::OffsetDateTime,
) -> Result<ResourceCertificate, CertPathError> {
let ee = ResourceCertificate::decode_der(ee_cert_der)?;
if ee.kind != ResourceCertKind::Ee {
return Err(CertPathError::EeNotEe);
}
if issuer_ca.kind != ResourceCertKind::Ca {
return Err(CertPathError::IssuerNotCa);
}
if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
return Err(CertPathError::IssuerSubjectMismatch {
ee_issuer_dn: ee.tbs.issuer_name.to_string(),
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
});
}
validate_ee_aki_matches_issuer_ski(&ee, issuer_ca)?;
if let Some(expected_issuer_uri) = issuer_ca_rsync_uri {
validate_ee_aia_points_to_issuer_uri(&ee, expected_issuer_uri)?;
}
if let Some(expected_crl_uri) = issuer_crl_rsync_uri {
validate_ee_crldp_contains_issuer_crl_uri(&ee, expected_crl_uri)?;
}
if !time_within_validity(
validation_time,
ee.tbs.validity_not_before,
ee.tbs.validity_not_after,
) || !time_within_validity(
validation_time,
issuer_ca.tbs.validity_not_before,
issuer_ca.tbs.validity_not_after,
) {
return Err(CertPathError::CertificateNotValidAtTime);
}
let ee_x509 = parse_x509_cert(ee_cert_der)?;
verify_ee_cert_signature(&ee_x509, issuer_spki)?;
validate_ee_key_usage(&ee_x509)?;
if !crl_valid_at_time(issuer_crl, validation_time) {
return Err(CertPathError::CrlNotValidAtTime);
}
let serial = BigUnsigned::from_biguint(&ee.tbs.serial_number);
if issuer_crl_revoked_serials.contains(&serial.bytes_be) {
return Err(CertPathError::EeRevoked);
}
Ok(ee)
}
fn parse_subject_pki_from_der(der: &[u8]) -> Result<SubjectPublicKeyInfo<'_>, CertPathError> {
let (rem, spki) = SubjectPublicKeyInfo::from_der(der)
.map_err(|e| CertPathError::IssuerSpkiParse(e.to_string()))?;
if !rem.is_empty() {
return Err(CertPathError::IssuerSpkiTrailingBytes(rem.len()));
}
Ok(spki)
}
fn parse_x509_cert(der: &[u8]) -> Result<X509Certificate<'_>, CertPathError> {
let (rem, cert) = X509Certificate::from_der(der)
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CertPathError::EeSignatureInvalid(
"trailing bytes after EE certificate".to_string(),
));
}
Ok(cert)
}
fn verify_ee_cert_signature(
ee: &X509Certificate<'_>,
issuer_spki: &SubjectPublicKeyInfo<'_>,
) -> Result<(), CertPathError> {
ee.verify_signature(Some(issuer_spki))
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
}
fn validate_ee_aki_matches_issuer_ski(
ee: &ResourceCertificate,
issuer_ca: &ResourceCertificate,
@@ -203,15 +313,7 @@ fn validate_ee_crldp_contains_issuer_crl_uri(
Ok(())
}
fn validate_ee_key_usage(ee_cert_der: &[u8]) -> Result<(), CertPathError> {
let (rem, cert) = X509Certificate::from_der(ee_cert_der)
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CertPathError::EeSignatureInvalid(
"trailing bytes after EE certificate".to_string(),
));
}
fn validate_ee_key_usage(cert: &X509Certificate<'_>) -> Result<(), CertPathError> {
let mut ku_critical: Option<bool> = None;
for ext in cert.extensions() {
if ext.oid.as_bytes() == crate::data_model::oid::OID_KEY_USAGE_RAW {
@@ -250,29 +352,6 @@ fn validate_ee_key_usage(ee_cert_der: &[u8]) -> Result<(), CertPathError> {
Ok(())
}
fn verify_cert_signature_with_issuer(
ee_cert_der: &[u8],
issuer_ca_der: &[u8],
) -> Result<(), CertPathError> {
let (rem, ee) = X509Certificate::from_der(ee_cert_der)
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CertPathError::EeSignatureInvalid(
"trailing bytes after EE certificate".to_string(),
));
}
let (rem, issuer) = X509Certificate::from_der(issuer_ca_der)
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CertPathError::EeSignatureInvalid(
"trailing bytes after issuer certificate".to_string(),
));
}
ee.verify_signature(Some(&issuer.tbs_certificate.subject_pki))
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
}
fn time_within_validity(
t: time::OffsetDateTime,
not_before: time::OffsetDateTime,
@@ -301,10 +380,10 @@ fn is_serial_revoked_by_crl(ee: &ResourceCertificate, crl: &RpkixCrl) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::data_model::common::X509NameDer;
use crate::data_model::rc::{
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
};
use crate::data_model::common::X509NameDer;
use der_parser::num_bigint::BigUint;
fn dummy_cert(

View File

@@ -119,9 +119,7 @@ pub enum ManifestCachedError {
)]
CachedMissingFile { rsync_uri: String },
#[error(
"cached fetch_cache_pp file hash mismatch: {rsync_uri} (RFC 9286 §6.5; RFC 9286 §6.6)"
)]
#[error("cached fetch_cache_pp file hash mismatch: {rsync_uri} (RFC 9286 §6.5; RFC 9286 §6.6)")]
CachedHashMismatch { rsync_uri: String },
}
@@ -242,7 +240,7 @@ pub fn process_manifest_publication_point_after_repo_sync(
}
}
fn load_and_revalidate_cached_pack(
pub fn load_and_revalidate_cached_pack(
store: &RocksStore,
manifest_rsync_uri: &str,
publication_point_rsync_uri: &str,
@@ -283,8 +281,9 @@ fn revalidate_cached_pack_with_current_time(
validation_time: time::OffsetDateTime,
) -> Result<(), ManifestCachedError> {
// First, re-validate the cached manifest itself with the current time.
let manifest = decode_and_validate_manifest_with_current_time(&pack.manifest_bytes, validation_time)
.map_err(ManifestCachedError::from)?;
let manifest =
decode_and_validate_manifest_with_current_time(&pack.manifest_bytes, validation_time)
.map_err(ManifestCachedError::from)?;
// Then, re-bind the manifest fileList to the cached pack contents, as per RFC 9286 §6.4-§6.5.
let by_uri: HashMap<&str, &crate::storage::PackFile> = pack
@@ -379,10 +378,17 @@ fn try_build_fresh_pack(
manifest_rsync_uri: manifest_rsync_uri.to_string(),
})?;
let manifest = decode_and_validate_manifest_with_current_time(&manifest_bytes, validation_time)?;
let manifest =
decode_and_validate_manifest_with_current_time(&manifest_bytes, validation_time)?;
let this_update = manifest.manifest.this_update.to_offset(time::UtcOffset::UTC);
let next_update = manifest.manifest.next_update.to_offset(time::UtcOffset::UTC);
let this_update = manifest
.manifest
.this_update
.to_offset(time::UtcOffset::UTC);
let next_update = manifest
.manifest
.next_update
.to_offset(time::UtcOffset::UTC);
let now = validation_time.to_offset(time::UtcOffset::UTC);
// RFC 9286 §4.2.1: replay/rollback detection for manifestNumber and thisUpdate.
@@ -443,7 +449,10 @@ fn try_build_fresh_pack(
}
}
let entries = manifest.manifest.parse_files().map_err(ManifestDecodeError::Validate)?;
let entries = manifest
.manifest
.parse_files()
.map_err(ManifestDecodeError::Validate)?;
let mut files = Vec::with_capacity(manifest.manifest.file_count());
for entry in &entries {
let rsync_uri =
@@ -493,9 +502,7 @@ fn try_build_fresh_pack(
fn cmp_minimal_be_unsigned(a: &[u8], b: &[u8]) -> Ordering {
// Compare two minimal big-endian byte strings as unsigned integers.
// (Leading zeros are not expected; callers store minimal big-endian.)
a.len()
.cmp(&b.len())
.then_with(|| a.cmp(b))
a.len().cmp(&b.len()).then_with(|| a.cmp(b))
}
fn join_rsync_dir_and_file(base: &str, file_name: &str) -> String {

View File

@@ -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));
}
}

View File

@@ -5,9 +5,7 @@ use crate::storage::{FetchCachePpKey, RocksStore};
use crate::sync::repo::{RepoSyncResult, sync_publication_point};
use crate::sync::rrdp::Fetcher as HttpFetcher;
use crate::validation::manifest::{PublicationPointResult, process_manifest_publication_point};
use crate::validation::objects::{
ObjectsOutput, process_fetch_cache_pp_pack_for_issuer,
};
use crate::validation::objects::{ObjectsOutput, process_fetch_cache_pp_pack_for_issuer};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunOutput {
@@ -53,6 +51,7 @@ pub fn run_publication_point_once(
rsync_base_uri,
http_fetcher,
rsync_fetcher,
None,
)?;
let publication_point = process_manifest_publication_point(
@@ -73,6 +72,7 @@ pub fn run_publication_point_once(
issuer_effective_ip,
issuer_effective_as,
validation_time,
None,
);
Ok(RunOutput {

View File

@@ -1,5 +1,6 @@
use url::Url;
use crate::analysis::timing::TimingHandle;
use crate::audit::PublicationPointAudit;
use crate::data_model::ta::TrustAnchor;
use crate::sync::rrdp::Fetcher;
@@ -12,6 +13,8 @@ use crate::validation::tree::{
run_tree_serial, run_tree_serial_audit,
};
use crate::validation::tree_runner::Rpkiv1PublicationPointRunner;
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunTreeFromTalOutput {
@@ -71,6 +74,12 @@ pub fn run_tree_from_tal_url_serial(
http_fetcher,
rsync_fetcher,
validation_time,
timing: None,
revalidate_only: config.revalidate_only,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
};
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
@@ -96,6 +105,12 @@ pub fn run_tree_from_tal_url_serial_audit(
http_fetcher,
rsync_fetcher,
validation_time,
timing: None,
revalidate_only: config.revalidate_only,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
};
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
@@ -111,6 +126,48 @@ pub fn run_tree_from_tal_url_serial_audit(
})
}
pub fn run_tree_from_tal_url_serial_audit_with_timing(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_url: &str,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let _tal = timing.span_phase("tal_bootstrap");
let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?;
drop(_tal);
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing: Some(timing.clone()),
revalidate_only: config.revalidate_only,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
};
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
let _tree = timing.span_phase("tree_run_total");
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
Ok(RunTreeFromTalAuditOutput {
discovery,
tree,
publication_points,
})
}
pub fn run_tree_from_tal_and_ta_der_serial(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
@@ -131,6 +188,12 @@ pub fn run_tree_from_tal_and_ta_der_serial(
http_fetcher,
rsync_fetcher,
validation_time,
timing: None,
revalidate_only: config.revalidate_only,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
};
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
@@ -159,6 +222,12 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit(
http_fetcher,
rsync_fetcher,
validation_time,
timing: None,
revalidate_only: config.revalidate_only,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
};
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
@@ -173,3 +242,48 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit(
publication_points,
})
}
pub fn run_tree_from_tal_and_ta_der_serial_audit_with_timing(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let _tal = timing.span_phase("tal_bootstrap");
let discovery =
discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?;
drop(_tal);
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing: Some(timing.clone()),
revalidate_only: config.revalidate_only,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
};
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
let _tree = timing.span_phase("tree_run_total");
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
Ok(RunTreeFromTalAuditOutput {
discovery,
tree,
publication_points,
})
}

View File

@@ -12,6 +12,10 @@ pub struct TreeRunConfig {
pub max_depth: Option<usize>,
/// Max number of CA instances to process.
pub max_instances: Option<usize>,
/// Skip RRDP/rsync fetch and re-validate from existing `fetch_cache_pp` packs in the DB.
///
/// This is primarily intended for profiling/analysis runs to remove network noise.
pub revalidate_only: bool,
}
impl Default for TreeRunConfig {
@@ -19,6 +23,7 @@ impl Default for TreeRunConfig {
Self {
max_depth: None,
max_instances: None,
revalidate_only: false,
}
}
}
@@ -182,7 +187,8 @@ pub fn run_tree_serial_audit(
audit.discovered_from = node.discovered_from.clone();
publication_points.push(audit);
let enqueue_children = res.source == PublicationPointSource::Fresh;
let enqueue_children =
res.source == PublicationPointSource::Fresh || config.revalidate_only;
if !enqueue_children && !res.discovered_children.is_empty() {
warnings.push(
Warning::new("skipping child CA discovery due to failed fetch cache use")

File diff suppressed because it is too large Load Diff