手动执行全量同步
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user