串行验证通过
This commit is contained in:
123
src/validation/ca_instance.rs
Normal file
123
src/validation/ca_instance.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use crate::data_model::oid::{OID_AD_CA_REPOSITORY, OID_AD_RPKI_MANIFEST, OID_AD_RPKI_NOTIFY};
|
||||
use crate::data_model::rc::{ResourceCertKind, ResourceCertificate, SubjectInfoAccess};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CaInstanceUris {
|
||||
/// CA repository base rsync URI (must end with `/`).
|
||||
pub rsync_base_uri: String,
|
||||
/// rsync URI for the manifest object (`.mft`).
|
||||
pub manifest_rsync_uri: String,
|
||||
/// Publication point rsync URI (RFC 9286 terminology). In v1 this equals `rsync_base_uri`.
|
||||
pub publication_point_rsync_uri: String,
|
||||
/// Optional RRDP notification URI (https).
|
||||
pub rrdp_notification_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CaInstanceUrisError {
|
||||
#[error("certificate must be a CA certificate (RFC 6487 §4.8.1)")]
|
||||
NotCa,
|
||||
|
||||
#[error(
|
||||
"CA certificate must contain Subject Information Access extension (RFC 6487 §4.8.8; RFC 5280 §4.2.2.2)"
|
||||
)]
|
||||
MissingSia,
|
||||
|
||||
#[error("CA certificate SIA must contain id-ad-caRepository rsync URI (RFC 6487 §4.8.8.1)")]
|
||||
MissingCaRepository,
|
||||
|
||||
#[error("CA certificate SIA must contain id-ad-rpkiManifest rsync URI (RFC 6487 §4.8.8.2)")]
|
||||
MissingRpkiManifest,
|
||||
|
||||
#[error(
|
||||
"SIA id-ad-caRepository accessLocation must be rsync:// URI, got {0} (RFC 6487 §4.8.8.1)"
|
||||
)]
|
||||
CaRepositoryNotRsync(String),
|
||||
|
||||
#[error(
|
||||
"SIA id-ad-rpkiManifest accessLocation must be rsync:// URI, got {0} (RFC 6487 §4.8.8.2)"
|
||||
)]
|
||||
RpkiManifestNotRsync(String),
|
||||
|
||||
#[error(
|
||||
"SIA id-ad-rpkiNotify accessLocation must be https:// URI, got {0} (RFC 8182 §3.4.1; RFC 6487 §4.8.8.3)"
|
||||
)]
|
||||
RpkiNotifyNotHttps(String),
|
||||
|
||||
#[error(
|
||||
"manifest rsync URI must be under CA publication point: manifest={manifest_rsync_uri} publication_point={publication_point_rsync_uri} (RFC 9286 §6.1)"
|
||||
)]
|
||||
ManifestNotUnderPublicationPoint {
|
||||
manifest_rsync_uri: String,
|
||||
publication_point_rsync_uri: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn ca_instance_uris_from_ca_certificate(
|
||||
cert: &ResourceCertificate,
|
||||
) -> Result<CaInstanceUris, CaInstanceUrisError> {
|
||||
if cert.kind != ResourceCertKind::Ca {
|
||||
return Err(CaInstanceUrisError::NotCa);
|
||||
}
|
||||
|
||||
let sia = cert
|
||||
.tbs
|
||||
.extensions
|
||||
.subject_info_access
|
||||
.as_ref()
|
||||
.ok_or(CaInstanceUrisError::MissingSia)?;
|
||||
|
||||
let access_descriptions = match sia {
|
||||
SubjectInfoAccess::Ca(ca) => &ca.access_descriptions,
|
||||
SubjectInfoAccess::Ee(_ee) => return Err(CaInstanceUrisError::MissingSia),
|
||||
};
|
||||
|
||||
let mut ca_repo: Option<String> = None;
|
||||
let mut manifest: Option<String> = None;
|
||||
let mut notify: Option<String> = None;
|
||||
|
||||
for ad in access_descriptions {
|
||||
if ad.access_method_oid == OID_AD_CA_REPOSITORY {
|
||||
let u = ad.access_location.to_string();
|
||||
if ad.access_location.scheme() != "rsync" {
|
||||
return Err(CaInstanceUrisError::CaRepositoryNotRsync(u));
|
||||
}
|
||||
ca_repo.get_or_insert(u);
|
||||
} else if ad.access_method_oid == OID_AD_RPKI_MANIFEST {
|
||||
let u = ad.access_location.to_string();
|
||||
if ad.access_location.scheme() != "rsync" {
|
||||
return Err(CaInstanceUrisError::RpkiManifestNotRsync(u));
|
||||
}
|
||||
manifest.get_or_insert(u);
|
||||
} else if ad.access_method_oid == OID_AD_RPKI_NOTIFY {
|
||||
let u = ad.access_location.to_string();
|
||||
if ad.access_location.scheme() != "https" {
|
||||
return Err(CaInstanceUrisError::RpkiNotifyNotHttps(u));
|
||||
}
|
||||
notify.get_or_insert(u);
|
||||
}
|
||||
}
|
||||
|
||||
let mut publication_point_rsync_uri = ca_repo.ok_or(CaInstanceUrisError::MissingCaRepository)?;
|
||||
if !publication_point_rsync_uri.ends_with('/') {
|
||||
publication_point_rsync_uri.push('/');
|
||||
}
|
||||
|
||||
let rsync_base_uri = publication_point_rsync_uri.clone();
|
||||
|
||||
let manifest_rsync_uri = manifest.ok_or(CaInstanceUrisError::MissingRpkiManifest)?;
|
||||
if !manifest_rsync_uri.starts_with(&publication_point_rsync_uri) {
|
||||
return Err(CaInstanceUrisError::ManifestNotUnderPublicationPoint {
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(CaInstanceUris {
|
||||
rsync_base_uri,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
rrdp_notification_uri: notify,
|
||||
})
|
||||
}
|
||||
|
||||
965
src/validation/ca_path.rs
Normal file
965
src/validation/ca_path.rs
Normal file
@@ -0,0 +1,965 @@
|
||||
use crate::data_model::common::BigUnsigned;
|
||||
use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
|
||||
use crate::data_model::oid::OID_KEY_USAGE;
|
||||
use crate::data_model::rc::{
|
||||
AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind,
|
||||
ResourceCertificate, ResourceCertificateDecodeError,
|
||||
};
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ValidatedSubordinateCa {
|
||||
pub child_ca: ResourceCertificate,
|
||||
pub issuer_ca: ResourceCertificate,
|
||||
pub issuer_crl: RpkixCrl,
|
||||
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)")]
|
||||
ChildDecode(#[from] ResourceCertificateDecodeError),
|
||||
|
||||
#[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
|
||||
IssuerDecode(ResourceCertificateDecodeError),
|
||||
|
||||
#[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")]
|
||||
CrlDecode(#[from] CrlDecodeError),
|
||||
|
||||
#[error(
|
||||
"child certificate must be a CA resource certificate (RFC 6487 §4.8.1; RFC 5280 §4.2.1.9)"
|
||||
)]
|
||||
ChildNotCa,
|
||||
|
||||
#[error(
|
||||
"issuer certificate must be a CA resource certificate (RFC 6487 §4.8.1; RFC 5280 §4.2.1.9)"
|
||||
)]
|
||||
IssuerNotCa,
|
||||
|
||||
#[error(
|
||||
"child issuer DN does not match issuer CA subject DN: child.issuer={child_issuer_dn} issuer.subject={issuer_subject_dn} (RFC 5280 §6.1)"
|
||||
)]
|
||||
IssuerSubjectMismatch {
|
||||
child_issuer_dn: String,
|
||||
issuer_subject_dn: String,
|
||||
},
|
||||
|
||||
#[error("child CA certificate signature verification failed: {0} (RFC 5280 §6.1)")]
|
||||
ChildSignatureInvalid(String),
|
||||
|
||||
#[error("certificate not valid at validation_time (RFC 5280 §4.1.2.5; RFC 5280 §6.1)")]
|
||||
CertificateNotValidAtTime,
|
||||
|
||||
#[error(
|
||||
"child CA KeyUsage extension missing (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)"
|
||||
)]
|
||||
KeyUsageMissing,
|
||||
|
||||
#[error("child CA KeyUsage criticality must be critical (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")]
|
||||
KeyUsageNotCritical,
|
||||
|
||||
#[error(
|
||||
"child CA KeyUsage must have only keyCertSign and cRLSign set (RFC 6487 §4.8.4)"
|
||||
)]
|
||||
KeyUsageInvalidBits,
|
||||
|
||||
#[error(
|
||||
"CRL signature/binding verification failed: {0} (RFC 5280 §6.3.3; RFC 6487 §5; RFC 9829 §3.1)"
|
||||
)]
|
||||
CrlVerify(#[from] CrlVerifyError),
|
||||
|
||||
#[error(
|
||||
"CRL not valid at validation_time (RFC 5280 §6.3.3(g); RFC 5280 §5.1.2.4-§5.1.2.5; RFC 6487 §5)"
|
||||
)]
|
||||
CrlNotValidAtTime,
|
||||
|
||||
#[error("child CA certificate is revoked by issuer CRL (RFC 5280 §6.3.3; RFC 6487 §5)")]
|
||||
ChildRevoked,
|
||||
|
||||
#[error(
|
||||
"child CA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11)"
|
||||
)]
|
||||
ResourcesMissing,
|
||||
|
||||
#[error(
|
||||
"resource extension inheritance cannot be resolved (parent missing resources) (RFC 6487 §7.2)"
|
||||
)]
|
||||
InheritWithoutParentResources,
|
||||
|
||||
#[error("child CA resources are not a subset of issuer resources (RFC 6487 §7.2)")]
|
||||
ResourcesNotSubset,
|
||||
|
||||
#[error("issuer CA subjectKeyIdentifier missing (RFC 6487 §4.8.2)")]
|
||||
IssuerSkiMissing,
|
||||
|
||||
#[error("child CA authorityKeyIdentifier missing (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)")]
|
||||
ChildAkiMissing,
|
||||
|
||||
#[error(
|
||||
"child CA authorityKeyIdentifier does not match issuer subjectKeyIdentifier (RFC 6487 §4.8.3)"
|
||||
)]
|
||||
ChildAkiMismatch,
|
||||
|
||||
#[error("child CA authorityInfoAccess missing (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)")]
|
||||
ChildAiaMissing,
|
||||
|
||||
#[error(
|
||||
"child CA authorityInfoAccess does not reference issuer certificate rsync URI (RFC 6487 §4.8.7)"
|
||||
)]
|
||||
ChildAiaIssuerUriMismatch,
|
||||
|
||||
#[error("child CA CRLDistributionPoints missing (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)")]
|
||||
ChildCrlDpMissing,
|
||||
|
||||
#[error(
|
||||
"child CA CRLDistributionPoints does not reference issuer CRL rsync URI (RFC 6487 §4.8.6)"
|
||||
)]
|
||||
ChildCrlDpUriMismatch,
|
||||
}
|
||||
|
||||
pub fn validate_subordinate_ca_cert(
|
||||
child_ca_der: &[u8],
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_crl_der: &[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<ValidatedSubordinateCa, CaPathError> {
|
||||
let child_ca = ResourceCertificate::decode_der(child_ca_der)?;
|
||||
if child_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::ChildNotCa);
|
||||
}
|
||||
|
||||
let issuer_ca =
|
||||
ResourceCertificate::decode_der(issuer_ca_der).map_err(CaPathError::IssuerDecode)?;
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::IssuerNotCa);
|
||||
}
|
||||
|
||||
if child_ca.tbs.issuer_dn != issuer_ca.tbs.subject_dn {
|
||||
return Err(CaPathError::IssuerSubjectMismatch {
|
||||
child_issuer_dn: child_ca.tbs.issuer_dn.clone(),
|
||||
issuer_subject_dn: issuer_ca.tbs.subject_dn.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
verify_cert_signature_with_issuer(child_ca_der, issuer_ca_der)?;
|
||||
validate_child_ca_key_usage(child_ca_der)?;
|
||||
|
||||
let issuer_crl = RpkixCrl::decode_der(issuer_crl_der)?;
|
||||
issuer_crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
|
||||
if !crl_valid_at_time(&issuer_crl, validation_time) {
|
||||
return Err(CaPathError::CrlNotValidAtTime);
|
||||
}
|
||||
|
||||
if is_serial_revoked_by_crl(&child_ca, &issuer_crl) {
|
||||
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(ValidatedSubordinateCa {
|
||||
child_ca,
|
||||
issuer_ca,
|
||||
issuer_crl,
|
||||
effective_ip_resources,
|
||||
effective_as_resources,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_child_aki_matches_issuer_ski(
|
||||
child: &ResourceCertificate,
|
||||
issuer: &ResourceCertificate,
|
||||
) -> Result<(), CaPathError> {
|
||||
let Some(issuer_ski) = issuer.tbs.extensions.subject_key_identifier.as_deref() else {
|
||||
return Err(CaPathError::IssuerSkiMissing);
|
||||
};
|
||||
let Some(child_aki) = child.tbs.extensions.authority_key_identifier.as_deref() else {
|
||||
return Err(CaPathError::ChildAkiMissing);
|
||||
};
|
||||
if child_aki != issuer_ski {
|
||||
return Err(CaPathError::ChildAkiMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_child_aia_points_to_issuer_uri(
|
||||
child: &ResourceCertificate,
|
||||
issuer_ca_rsync_uri: &str,
|
||||
) -> Result<(), CaPathError> {
|
||||
let Some(uris) = child.tbs.extensions.ca_issuers_uris.as_ref() else {
|
||||
return Err(CaPathError::ChildAiaMissing);
|
||||
};
|
||||
if !uris.iter().any(|u| u.as_str() == issuer_ca_rsync_uri) {
|
||||
return Err(CaPathError::ChildAiaIssuerUriMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_child_crldp_contains_issuer_crl_uri(
|
||||
child: &ResourceCertificate,
|
||||
issuer_crl_rsync_uri: &str,
|
||||
) -> Result<(), CaPathError> {
|
||||
let Some(uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else {
|
||||
return Err(CaPathError::ChildCrlDpMissing);
|
||||
};
|
||||
if !uris.iter().any(|u| u.as_str() == issuer_crl_rsync_uri) {
|
||||
return Err(CaPathError::ChildCrlDpUriMismatch);
|
||||
}
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut ku_critical: Option<bool> = None;
|
||||
for ext in cert.extensions() {
|
||||
if ext.oid.to_id_string() == OID_KEY_USAGE {
|
||||
ku_critical = Some(ext.critical);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(critical) = ku_critical else {
|
||||
return Err(CaPathError::KeyUsageMissing);
|
||||
};
|
||||
if !critical {
|
||||
return Err(CaPathError::KeyUsageNotCritical);
|
||||
}
|
||||
|
||||
let Some(ku) = cert
|
||||
.key_usage()
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?
|
||||
else {
|
||||
return Err(CaPathError::KeyUsageMissing);
|
||||
};
|
||||
|
||||
let v = &ku.value;
|
||||
let ok = v.key_cert_sign()
|
||||
&& v.crl_sign()
|
||||
&& !v.digital_signature()
|
||||
&& !v.non_repudiation()
|
||||
&& !v.key_encipherment()
|
||||
&& !v.data_encipherment()
|
||||
&& !v.key_agreement()
|
||||
&& !v.encipher_only()
|
||||
&& !v.decipher_only();
|
||||
if !ok {
|
||||
return Err(CaPathError::KeyUsageInvalidBits);
|
||||
}
|
||||
|
||||
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,
|
||||
not_after: time::OffsetDateTime,
|
||||
) -> bool {
|
||||
let t = t.to_offset(time::UtcOffset::UTC);
|
||||
let not_before = not_before.to_offset(time::UtcOffset::UTC);
|
||||
let not_after = not_after.to_offset(time::UtcOffset::UTC);
|
||||
t >= not_before && t <= not_after
|
||||
}
|
||||
|
||||
fn crl_valid_at_time(crl: &RpkixCrl, t: time::OffsetDateTime) -> bool {
|
||||
let t = t.to_offset(time::UtcOffset::UTC);
|
||||
let this_update = crl.this_update.utc.to_offset(time::UtcOffset::UTC);
|
||||
let next_update = crl.next_update.utc.to_offset(time::UtcOffset::UTC);
|
||||
t >= this_update && t < next_update
|
||||
}
|
||||
|
||||
fn is_serial_revoked_by_crl(cert: &ResourceCertificate, crl: &RpkixCrl) -> bool {
|
||||
let serial = BigUnsigned::from_biguint(&cert.tbs.serial_number);
|
||||
crl.revoked_certs
|
||||
.iter()
|
||||
.any(|rc| rc.serial_number == serial)
|
||||
}
|
||||
|
||||
fn resolve_child_ip_resources(
|
||||
child_ip: Option<&IpResourceSet>,
|
||||
issuer_effective: Option<&IpResourceSet>,
|
||||
) -> Result<Option<IpResourceSet>, CaPathError> {
|
||||
let Some(child_ip) = child_ip else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(parent) = issuer_effective else {
|
||||
if child_ip.has_any_inherit() {
|
||||
return Err(CaPathError::InheritWithoutParentResources);
|
||||
}
|
||||
// With no parent effective resources, we cannot validate subset.
|
||||
return Err(CaPathError::ResourcesNotSubset);
|
||||
};
|
||||
|
||||
// Resolve per-AFI inherit, producing an effective set with no inherit.
|
||||
let parent_by_afi = ip_resources_by_afi_items(parent)?;
|
||||
let mut out_families: Vec<crate::data_model::rc::IpAddressFamily> = Vec::new();
|
||||
|
||||
for fam in &child_ip.families {
|
||||
match &fam.choice {
|
||||
IpAddressChoice::Inherit => {
|
||||
let items = parent_by_afi
|
||||
.get(&fam.afi)
|
||||
.ok_or(CaPathError::InheritWithoutParentResources)?;
|
||||
out_families.push(crate::data_model::rc::IpAddressFamily {
|
||||
afi: fam.afi,
|
||||
choice: IpAddressChoice::AddressesOrRanges(items.clone()),
|
||||
});
|
||||
}
|
||||
IpAddressChoice::AddressesOrRanges(items) => {
|
||||
// Subset check against parent union for that AFI.
|
||||
let parent_set = ip_resources_single_afi(parent, fam.afi, parent_by_afi.get(&fam.afi));
|
||||
if !ip_family_items_subset(items, &parent_set) {
|
||||
return Err(CaPathError::ResourcesNotSubset);
|
||||
}
|
||||
out_families.push(crate::data_model::rc::IpAddressFamily {
|
||||
afi: fam.afi,
|
||||
choice: IpAddressChoice::AddressesOrRanges(items.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(IpResourceSet { families: out_families }))
|
||||
}
|
||||
|
||||
fn resolve_child_as_resources(
|
||||
child_as: Option<&AsResourceSet>,
|
||||
issuer_effective: Option<&AsResourceSet>,
|
||||
) -> Result<Option<AsResourceSet>, CaPathError> {
|
||||
let Some(child_as) = child_as else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(parent) = issuer_effective else {
|
||||
if matches!(child_as.asnum, Some(AsIdentifierChoice::Inherit))
|
||||
|| matches!(child_as.rdi, Some(AsIdentifierChoice::Inherit))
|
||||
{
|
||||
return Err(CaPathError::InheritWithoutParentResources);
|
||||
}
|
||||
return Err(CaPathError::ResourcesNotSubset);
|
||||
};
|
||||
|
||||
let asnum = match child_as.asnum.as_ref() {
|
||||
None => None,
|
||||
Some(AsIdentifierChoice::Inherit) => parent.asnum.clone().ok_or(CaPathError::InheritWithoutParentResources).map(Some)?,
|
||||
Some(_) => {
|
||||
if !as_choice_subset(child_as.asnum.as_ref(), parent.asnum.as_ref()) {
|
||||
return Err(CaPathError::ResourcesNotSubset);
|
||||
}
|
||||
child_as.asnum.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let rdi = match child_as.rdi.as_ref() {
|
||||
None => None,
|
||||
Some(AsIdentifierChoice::Inherit) => parent.rdi.clone().ok_or(CaPathError::InheritWithoutParentResources).map(Some)?,
|
||||
Some(_) => {
|
||||
if !as_choice_subset(child_as.rdi.as_ref(), parent.rdi.as_ref()) {
|
||||
return Err(CaPathError::ResourcesNotSubset);
|
||||
}
|
||||
child_as.rdi.clone()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(AsResourceSet { asnum, rdi }))
|
||||
}
|
||||
|
||||
fn as_choice_subset(
|
||||
child: Option<&AsIdentifierChoice>,
|
||||
parent: Option<&AsIdentifierChoice>,
|
||||
) -> bool {
|
||||
let Some(child) = child else {
|
||||
return true;
|
||||
};
|
||||
let Some(parent) = parent else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Treat inherit as "all of parent" here; actual resolution is handled elsewhere.
|
||||
if matches!(child, AsIdentifierChoice::Inherit) {
|
||||
return true;
|
||||
}
|
||||
if matches!(parent, AsIdentifierChoice::Inherit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let child_intervals = as_choice_to_merged_intervals(child);
|
||||
let parent_intervals = as_choice_to_merged_intervals(parent);
|
||||
for (cmin, cmax) in &child_intervals {
|
||||
if !as_interval_is_covered(&parent_intervals, *cmin, *cmax) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn as_choice_to_merged_intervals(choice: &AsIdentifierChoice) -> Vec<(u32, u32)> {
|
||||
let mut v = Vec::new();
|
||||
match choice {
|
||||
AsIdentifierChoice::Inherit => {}
|
||||
AsIdentifierChoice::AsIdsOrRanges(items) => {
|
||||
for item in items {
|
||||
match item {
|
||||
crate::data_model::rc::AsIdOrRange::Id(id) => v.push((*id, *id)),
|
||||
crate::data_model::rc::AsIdOrRange::Range { min, max } => v.push((*min, *max)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v.sort_by_key(|(a, _b)| *a);
|
||||
merge_as_intervals(&v)
|
||||
}
|
||||
|
||||
fn merge_as_intervals(v: &[(u32, u32)]) -> Vec<(u32, u32)> {
|
||||
let mut out: Vec<(u32, u32)> = Vec::new();
|
||||
for (min, max) in v {
|
||||
let Some(last) = out.last_mut() else {
|
||||
out.push((*min, *max));
|
||||
continue;
|
||||
};
|
||||
if *min <= last.1.saturating_add(1) {
|
||||
last.1 = last.1.max(*max);
|
||||
continue;
|
||||
}
|
||||
out.push((*min, *max));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn as_interval_is_covered(parent: &[(u32, u32)], min: u32, max: u32) -> bool {
|
||||
for (pmin, pmax) in parent {
|
||||
if *pmin <= min && max <= *pmax {
|
||||
return true;
|
||||
}
|
||||
if *pmin > min {
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
enum AfiKey {
|
||||
V4,
|
||||
V6,
|
||||
}
|
||||
|
||||
fn ip_resources_by_afi_items(
|
||||
set: &IpResourceSet,
|
||||
) -> Result<std::collections::BTreeMap<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>>, CaPathError> {
|
||||
let mut m: std::collections::BTreeMap<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>> =
|
||||
std::collections::BTreeMap::new();
|
||||
for fam in &set.families {
|
||||
match &fam.choice {
|
||||
IpAddressChoice::Inherit => return Err(CaPathError::InheritWithoutParentResources),
|
||||
IpAddressChoice::AddressesOrRanges(items) => {
|
||||
m.insert(fam.afi, items.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
fn ip_resources_single_afi(
|
||||
parent: &IpResourceSet,
|
||||
afi: crate::data_model::rc::Afi,
|
||||
items: Option<&Vec<crate::data_model::rc::IpAddressOrRange>>,
|
||||
) -> IpResourceSet {
|
||||
let mut families = Vec::new();
|
||||
if let Some(items) = items {
|
||||
families.push(crate::data_model::rc::IpAddressFamily {
|
||||
afi,
|
||||
choice: IpAddressChoice::AddressesOrRanges(items.clone()),
|
||||
});
|
||||
} else {
|
||||
// If parent doesn't mention this AFI explicitly, treat as empty.
|
||||
// The subset check will fail.
|
||||
let _ = parent;
|
||||
}
|
||||
IpResourceSet { families }
|
||||
}
|
||||
|
||||
fn ip_family_items_subset(
|
||||
child_items: &[crate::data_model::rc::IpAddressOrRange],
|
||||
parent_set: &IpResourceSet,
|
||||
) -> bool {
|
||||
let parent_by_afi = ip_resources_to_merged_intervals(parent_set);
|
||||
// parent_set should contain exactly one AFI.
|
||||
let (afi_key, parent_intervals) = match parent_by_afi.into_iter().next() {
|
||||
None => return false,
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
let mut child_intervals: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
||||
for item in child_items {
|
||||
match item {
|
||||
crate::data_model::rc::IpAddressOrRange::Prefix(p) => child_intervals.push(prefix_to_range(p)),
|
||||
crate::data_model::rc::IpAddressOrRange::Range(r) => child_intervals.push((r.min.clone(), r.max.clone())),
|
||||
}
|
||||
}
|
||||
child_intervals.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
let child_intervals = merge_ip_intervals(&child_intervals);
|
||||
|
||||
let _ = afi_key;
|
||||
for (cmin, cmax) in &child_intervals {
|
||||
if !interval_is_covered(&parent_intervals, cmin, cmax) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn ip_resources_to_merged_intervals(
|
||||
set: &IpResourceSet,
|
||||
) -> std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> {
|
||||
let mut m: std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for fam in &set.families {
|
||||
let afi = match fam.afi {
|
||||
crate::data_model::rc::Afi::Ipv4 => AfiKey::V4,
|
||||
crate::data_model::rc::Afi::Ipv6 => AfiKey::V6,
|
||||
};
|
||||
match &fam.choice {
|
||||
IpAddressChoice::Inherit => {
|
||||
// When used in subset checks, treat inherit as "all" by leaving it absent.
|
||||
// Resolution should have happened earlier.
|
||||
}
|
||||
IpAddressChoice::AddressesOrRanges(items) => {
|
||||
let ent = m.entry(afi).or_default();
|
||||
for item in items {
|
||||
match item {
|
||||
crate::data_model::rc::IpAddressOrRange::Prefix(p) => {
|
||||
let (min, max) = prefix_to_range(p);
|
||||
ent.push((min, max));
|
||||
}
|
||||
crate::data_model::rc::IpAddressOrRange::Range(r) => {
|
||||
ent.push((r.min.clone(), r.max.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (_afi, v) in m.iter_mut() {
|
||||
v.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
*v = merge_ip_intervals(v);
|
||||
}
|
||||
|
||||
m
|
||||
}
|
||||
|
||||
fn merge_ip_intervals(v: &[(Vec<u8>, Vec<u8>)]) -> Vec<(Vec<u8>, Vec<u8>)> {
|
||||
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
||||
for (min, max) in v {
|
||||
let Some(last) = out.last_mut() else {
|
||||
out.push((min.clone(), max.clone()));
|
||||
continue;
|
||||
};
|
||||
|
||||
if bytes_leq(min, &increment_bytes(&last.1)) {
|
||||
if bytes_leq(&last.1, max) {
|
||||
last.1 = max.clone();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push((min.clone(), max.clone()));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn interval_is_covered(parent: &[(Vec<u8>, Vec<u8>)], min: &[u8], max: &[u8]) -> bool {
|
||||
for (pmin, pmax) in parent {
|
||||
if bytes_leq(pmin, min) && bytes_leq(max, pmax) {
|
||||
return true;
|
||||
}
|
||||
if pmin.as_slice() > min {
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn prefix_to_range(prefix: &crate::data_model::rc::IpPrefix) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut min = prefix.addr.clone();
|
||||
let mut max = prefix.addr.clone();
|
||||
|
||||
let bitlen = match prefix.afi {
|
||||
crate::data_model::rc::Afi::Ipv4 => 32u16,
|
||||
crate::data_model::rc::Afi::Ipv6 => 128u16,
|
||||
};
|
||||
let plen = prefix.prefix_len.min(bitlen);
|
||||
for bit in plen..bitlen {
|
||||
let byte = (bit / 8) as usize;
|
||||
let offset = 7 - (bit % 8);
|
||||
let mask = 1u8 << offset;
|
||||
min[byte] &= !mask;
|
||||
max[byte] |= mask;
|
||||
}
|
||||
(min, max)
|
||||
}
|
||||
|
||||
fn bytes_leq(a: &[u8], b: &[u8]) -> bool {
|
||||
a <= b
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data_model::rc::{
|
||||
Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpResourceSet,
|
||||
};
|
||||
use crate::data_model::rc::{RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use url::Url;
|
||||
|
||||
fn dummy_cert(
|
||||
kind: ResourceCertKind,
|
||||
subject_dn: &str,
|
||||
issuer_dn: &str,
|
||||
ski: Option<Vec<u8>>,
|
||||
aki: Option<Vec<u8>>,
|
||||
aia: Option<Vec<&str>>,
|
||||
crldp: Option<Vec<&str>>,
|
||||
) -> ResourceCertificate {
|
||||
let aia = aia.map(|v| {
|
||||
v.into_iter()
|
||||
.map(|s| Url::parse(s).expect("url"))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let crldp = crldp.map(|v| {
|
||||
v.into_iter()
|
||||
.map(|s| Url::parse(s).expect("url"))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
ResourceCertificate {
|
||||
raw_der: Vec::new(),
|
||||
kind,
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_dn: issuer_dn.to_string(),
|
||||
subject_dn: subject_dn.to_string(),
|
||||
validity_not_before: time::OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: time::OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: kind == ResourceCertKind::Ca,
|
||||
subject_key_identifier: ski,
|
||||
authority_key_identifier: aki,
|
||||
crl_distribution_points_uris: crldp,
|
||||
ca_issuers_uris: aia,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: None,
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_child_ip_resources_rejects_inherit_without_parent_effective_resources() {
|
||||
let child = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
};
|
||||
let err = resolve_child_ip_resources(Some(&child), None).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::InheritWithoutParentResources));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_child_ip_resources_rejects_non_inherit_without_parent_effective_resources() {
|
||||
let child = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![]),
|
||||
}],
|
||||
};
|
||||
let err = resolve_child_ip_resources(Some(&child), None).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ResourcesNotSubset));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_resources_by_afi_items_rejects_inherit_families() {
|
||||
let parent = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
};
|
||||
let err = ip_resources_by_afi_items(&parent).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::InheritWithoutParentResources));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_child_as_resources_rejects_inherit_without_parent_effective_resources() {
|
||||
let child = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
};
|
||||
let err = resolve_child_as_resources(Some(&child), None).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::InheritWithoutParentResources));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_aki_mismatch_is_rejected() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![9]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_child_aki_matches_issuer_ski(&child, &issuer).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildAkiMismatch), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_aia_missing_is_rejected() {
|
||||
let _issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
None,
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err =
|
||||
validate_child_aia_points_to_issuer_uri(&child, "rsync://example.test/issuer.cer")
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildAiaMissing), "{err}");
|
||||
|
||||
// Also cover issuer ski missing.
|
||||
let issuer_missing_ski = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let err = validate_child_aki_matches_issuer_ski(&child, &issuer_missing_ski).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::IssuerSkiMissing), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_aia_issuer_uri_mismatch_is_rejected() {
|
||||
let _issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/other.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err =
|
||||
validate_child_aia_points_to_issuer_uri(&child, "rsync://example.test/issuer.cer")
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CaPathError::ChildAiaIssuerUriMismatch),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_crldp_mismatch_is_rejected() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
None,
|
||||
);
|
||||
let err = validate_child_crldp_contains_issuer_crl_uri(
|
||||
&child,
|
||||
"rsync://example.test/issuer.crl",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildCrlDpMissing), "{err}");
|
||||
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/other.crl"]),
|
||||
);
|
||||
let err = validate_child_crldp_contains_issuer_crl_uri(
|
||||
&child,
|
||||
"rsync://example.test/issuer.crl",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CaPathError::ChildCrlDpUriMismatch),
|
||||
"{err}"
|
||||
);
|
||||
|
||||
// Cover child AKI missing.
|
||||
let child_missing_aki = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
None,
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_child_aki_matches_issuer_ski(&child_missing_aki, &issuer).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildAkiMissing), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_binding_checks_accept_when_matching() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let child = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=child",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
validate_child_aki_matches_issuer_ski(&child, &issuer).expect("aki ok");
|
||||
validate_child_aia_points_to_issuer_uri(&child, "rsync://example.test/issuer.cer")
|
||||
.expect("aia ok");
|
||||
validate_child_crldp_contains_issuer_crl_uri(&child, "rsync://example.test/issuer.crl")
|
||||
.expect("crldp ok");
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_bytes(v: &[u8]) -> Vec<u8> {
|
||||
let mut out = v.to_vec();
|
||||
for i in (0..out.len()).rev() {
|
||||
if out[i] != 0xFF {
|
||||
out[i] += 1;
|
||||
for j in i + 1..out.len() {
|
||||
out[j] = 0;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
vec![0u8; out.len()]
|
||||
}
|
||||
497
src/validation/cert_path.rs
Normal file
497
src/validation/cert_path.rs
Normal file
@@ -0,0 +1,497 @@
|
||||
use crate::data_model::common::BigUnsigned;
|
||||
use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
|
||||
use crate::data_model::rc::{
|
||||
ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError,
|
||||
};
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ValidatedEeCertPath {
|
||||
pub ee: ResourceCertificate,
|
||||
pub issuer_ca: ResourceCertificate,
|
||||
pub issuer_crl: RpkixCrl,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CertPathError {
|
||||
#[error("EE certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
|
||||
EeDecode(#[from] ResourceCertificateDecodeError),
|
||||
|
||||
#[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
|
||||
IssuerDecode(ResourceCertificateDecodeError),
|
||||
|
||||
#[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")]
|
||||
CrlDecode(#[from] CrlDecodeError),
|
||||
|
||||
#[error(
|
||||
"issuer certificate must be a CA resource certificate (RFC 6487 §4.8.1; RFC 5280 §4.2.1.9)"
|
||||
)]
|
||||
IssuerNotCa,
|
||||
|
||||
#[error("EE certificate must be an EE resource certificate (RFC 6487 §4)")]
|
||||
EeNotEe,
|
||||
|
||||
#[error(
|
||||
"EE issuer DN does not match issuer CA subject DN: ee.issuer={ee_issuer_dn} issuer.subject={issuer_subject_dn} (RFC 5280 §6.1)"
|
||||
)]
|
||||
IssuerSubjectMismatch {
|
||||
ee_issuer_dn: String,
|
||||
issuer_subject_dn: String,
|
||||
},
|
||||
|
||||
#[error("EE certificate signature verification failed: {0} (RFC 5280 §6.1)")]
|
||||
EeSignatureInvalid(String),
|
||||
|
||||
#[error(
|
||||
"EE KeyUsage extension missing (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)"
|
||||
)]
|
||||
KeyUsageMissing,
|
||||
|
||||
#[error("EE KeyUsage criticality must be critical (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")]
|
||||
KeyUsageNotCritical,
|
||||
|
||||
#[error(
|
||||
"EE KeyUsage must have only digitalSignature set (RFC 6487 §4.8.4)"
|
||||
)]
|
||||
KeyUsageInvalidBits,
|
||||
|
||||
#[error("issuer CA subjectKeyIdentifier missing (RFC 6487 §4.8.2)")]
|
||||
IssuerSkiMissing,
|
||||
|
||||
#[error("EE authorityKeyIdentifier missing (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)")]
|
||||
EeAkiMissing,
|
||||
|
||||
#[error(
|
||||
"EE authorityKeyIdentifier does not match issuer subjectKeyIdentifier (RFC 6487 §4.8.3)"
|
||||
)]
|
||||
EeAkiMismatch,
|
||||
|
||||
#[error("EE authorityInfoAccess missing (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)")]
|
||||
EeAiaMissing,
|
||||
|
||||
#[error(
|
||||
"EE authorityInfoAccess does not reference issuer certificate rsync URI (RFC 6487 §4.8.7)"
|
||||
)]
|
||||
EeAiaIssuerUriMismatch,
|
||||
|
||||
#[error("EE CRLDistributionPoints missing (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)")]
|
||||
EeCrlDpMissing,
|
||||
|
||||
#[error("EE CRLDistributionPoints does not reference issuer CRL rsync URI (RFC 6487 §4.8.6)")]
|
||||
EeCrlDpUriMismatch,
|
||||
|
||||
#[error("certificate not valid at validation_time (RFC 5280 §4.1.2.5; RFC 5280 §6.1)")]
|
||||
CertificateNotValidAtTime,
|
||||
|
||||
#[error(
|
||||
"CRL signature/binding verification failed: {0} (RFC 5280 §6.3.3; RFC 6487 §5; RFC 9829 §3.1)"
|
||||
)]
|
||||
CrlVerify(#[from] CrlVerifyError),
|
||||
|
||||
#[error(
|
||||
"CRL not valid at validation_time (RFC 5280 §6.3.3(g); RFC 5280 §5.1.2.4-§5.1.2.5; RFC 6487 §5)"
|
||||
)]
|
||||
CrlNotValidAtTime,
|
||||
|
||||
#[error("EE certificate is revoked by issuer CRL (RFC 5280 §6.3.3; RFC 6487 §5)")]
|
||||
EeRevoked,
|
||||
}
|
||||
|
||||
pub fn validate_ee_cert_path(
|
||||
ee_cert_der: &[u8],
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_crl_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_crl_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<ValidatedEeCertPath, CertPathError> {
|
||||
let ee = ResourceCertificate::decode_der(ee_cert_der)?;
|
||||
if ee.kind != ResourceCertKind::Ee {
|
||||
return Err(CertPathError::EeNotEe);
|
||||
}
|
||||
|
||||
let issuer_ca =
|
||||
ResourceCertificate::decode_der(issuer_ca_der).map_err(CertPathError::IssuerDecode)?;
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CertPathError::IssuerNotCa);
|
||||
}
|
||||
|
||||
if ee.tbs.issuer_dn != issuer_ca.tbs.subject_dn {
|
||||
return Err(CertPathError::IssuerSubjectMismatch {
|
||||
ee_issuer_dn: ee.tbs.issuer_dn.clone(),
|
||||
issuer_subject_dn: issuer_ca.tbs.subject_dn.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
verify_cert_signature_with_issuer(ee_cert_der, issuer_ca_der)?;
|
||||
validate_ee_key_usage(ee_cert_der)?;
|
||||
|
||||
let issuer_crl = RpkixCrl::decode_der(issuer_crl_der)?;
|
||||
issuer_crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
|
||||
if !crl_valid_at_time(&issuer_crl, validation_time) {
|
||||
return Err(CertPathError::CrlNotValidAtTime);
|
||||
}
|
||||
|
||||
if is_serial_revoked_by_crl(&ee, &issuer_crl) {
|
||||
return Err(CertPathError::EeRevoked);
|
||||
}
|
||||
|
||||
Ok(ValidatedEeCertPath {
|
||||
ee,
|
||||
issuer_ca,
|
||||
issuer_crl,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_ee_aki_matches_issuer_ski(
|
||||
ee: &ResourceCertificate,
|
||||
issuer_ca: &ResourceCertificate,
|
||||
) -> Result<(), CertPathError> {
|
||||
let Some(issuer_ski) = issuer_ca.tbs.extensions.subject_key_identifier.as_deref() else {
|
||||
return Err(CertPathError::IssuerSkiMissing);
|
||||
};
|
||||
let Some(ee_aki) = ee.tbs.extensions.authority_key_identifier.as_deref() else {
|
||||
return Err(CertPathError::EeAkiMissing);
|
||||
};
|
||||
if ee_aki != issuer_ski {
|
||||
return Err(CertPathError::EeAkiMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ee_aia_points_to_issuer_uri(
|
||||
ee: &ResourceCertificate,
|
||||
issuer_ca_rsync_uri: &str,
|
||||
) -> Result<(), CertPathError> {
|
||||
let Some(uris) = ee.tbs.extensions.ca_issuers_uris.as_ref() else {
|
||||
return Err(CertPathError::EeAiaMissing);
|
||||
};
|
||||
if !uris.iter().any(|u| u.as_str() == issuer_ca_rsync_uri) {
|
||||
return Err(CertPathError::EeAiaIssuerUriMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ee_crldp_contains_issuer_crl_uri(
|
||||
ee: &ResourceCertificate,
|
||||
issuer_crl_rsync_uri: &str,
|
||||
) -> Result<(), CertPathError> {
|
||||
let Some(uris) = ee.tbs.extensions.crl_distribution_points_uris.as_ref() else {
|
||||
return Err(CertPathError::EeCrlDpMissing);
|
||||
};
|
||||
if !uris.iter().any(|u| u.as_str() == issuer_crl_rsync_uri) {
|
||||
return Err(CertPathError::EeCrlDpUriMismatch);
|
||||
}
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut ku_critical: Option<bool> = None;
|
||||
for ext in cert.extensions() {
|
||||
if ext.oid.to_id_string() == crate::data_model::oid::OID_KEY_USAGE {
|
||||
ku_critical = Some(ext.critical);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(critical) = ku_critical else {
|
||||
return Err(CertPathError::KeyUsageMissing);
|
||||
};
|
||||
if !critical {
|
||||
return Err(CertPathError::KeyUsageNotCritical);
|
||||
}
|
||||
|
||||
let Some(ku) = cert
|
||||
.key_usage()
|
||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?
|
||||
else {
|
||||
return Err(CertPathError::KeyUsageMissing);
|
||||
};
|
||||
|
||||
let v = &ku.value;
|
||||
let ok = v.digital_signature()
|
||||
&& !v.key_cert_sign()
|
||||
&& !v.crl_sign()
|
||||
&& !v.non_repudiation()
|
||||
&& !v.key_encipherment()
|
||||
&& !v.data_encipherment()
|
||||
&& !v.key_agreement()
|
||||
&& !v.encipher_only()
|
||||
&& !v.decipher_only();
|
||||
if !ok {
|
||||
return Err(CertPathError::KeyUsageInvalidBits);
|
||||
}
|
||||
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,
|
||||
not_after: time::OffsetDateTime,
|
||||
) -> bool {
|
||||
let t = t.to_offset(time::UtcOffset::UTC);
|
||||
let not_before = not_before.to_offset(time::UtcOffset::UTC);
|
||||
let not_after = not_after.to_offset(time::UtcOffset::UTC);
|
||||
t >= not_before && t <= not_after
|
||||
}
|
||||
|
||||
fn crl_valid_at_time(crl: &RpkixCrl, t: time::OffsetDateTime) -> bool {
|
||||
let t = t.to_offset(time::UtcOffset::UTC);
|
||||
let this_update = crl.this_update.utc.to_offset(time::UtcOffset::UTC);
|
||||
let next_update = crl.next_update.utc.to_offset(time::UtcOffset::UTC);
|
||||
t >= this_update && t < next_update
|
||||
}
|
||||
|
||||
fn is_serial_revoked_by_crl(ee: &ResourceCertificate, crl: &RpkixCrl) -> bool {
|
||||
let serial = BigUnsigned::from_biguint(&ee.tbs.serial_number);
|
||||
crl.revoked_certs
|
||||
.iter()
|
||||
.any(|rc| rc.serial_number == serial)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data_model::rc::{RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use url::Url;
|
||||
|
||||
fn dummy_cert(
|
||||
kind: ResourceCertKind,
|
||||
subject_dn: &str,
|
||||
issuer_dn: &str,
|
||||
ski: Option<Vec<u8>>,
|
||||
aki: Option<Vec<u8>>,
|
||||
aia: Option<Vec<&str>>,
|
||||
crldp: Option<Vec<&str>>,
|
||||
) -> ResourceCertificate {
|
||||
let aia = aia.map(|v| {
|
||||
v.into_iter()
|
||||
.map(|s| Url::parse(s).expect("url"))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let crldp = crldp.map(|v| {
|
||||
v.into_iter()
|
||||
.map(|s| Url::parse(s).expect("url"))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
ResourceCertificate {
|
||||
raw_der: Vec::new(),
|
||||
kind,
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_dn: issuer_dn.to_string(),
|
||||
subject_dn: subject_dn.to_string(),
|
||||
validity_not_before: time::OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: time::OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: kind == ResourceCertKind::Ca,
|
||||
subject_key_identifier: ski,
|
||||
authority_key_identifier: aki,
|
||||
crl_distribution_points_uris: crldp,
|
||||
ca_issuers_uris: aia,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: None,
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_aki_and_aia_and_crldp_binding_checks_report_errors() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let ee = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![9]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_ee_aki_matches_issuer_ski(&ee, &issuer).unwrap_err();
|
||||
assert!(matches!(err, CertPathError::EeAkiMismatch), "{err}");
|
||||
|
||||
let issuer_missing_ski = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let err = validate_ee_aki_matches_issuer_ski(&ee, &issuer_missing_ski).unwrap_err();
|
||||
assert!(matches!(err, CertPathError::IssuerSkiMissing), "{err}");
|
||||
|
||||
let ee_missing_aki = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
None,
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err = validate_ee_aki_matches_issuer_ski(&ee_missing_aki, &issuer).unwrap_err();
|
||||
assert!(matches!(err, CertPathError::EeAkiMissing), "{err}");
|
||||
|
||||
let ee_missing_aia = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
None,
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err =
|
||||
validate_ee_aia_points_to_issuer_uri(&ee_missing_aia, "rsync://example.test/issuer.cer")
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CertPathError::EeAiaMissing), "{err}");
|
||||
|
||||
let ee_wrong_aia = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/other.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
let err =
|
||||
validate_ee_aia_points_to_issuer_uri(&ee_wrong_aia, "rsync://example.test/issuer.cer")
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CertPathError::EeAiaIssuerUriMismatch),
|
||||
"{err}"
|
||||
);
|
||||
|
||||
let ee_missing_crldp = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
None,
|
||||
);
|
||||
let err = validate_ee_crldp_contains_issuer_crl_uri(
|
||||
&ee_missing_crldp,
|
||||
"rsync://example.test/issuer.crl",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CertPathError::EeCrlDpMissing), "{err}");
|
||||
|
||||
let ee_wrong_crldp = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/other.crl"]),
|
||||
);
|
||||
let err = validate_ee_crldp_contains_issuer_crl_uri(
|
||||
&ee_wrong_crldp,
|
||||
"rsync://example.test/issuer.crl",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CertPathError::EeCrlDpUriMismatch),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ee_binding_checks_accept_when_matching() {
|
||||
let issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let ee = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
validate_ee_aki_matches_issuer_ski(&ee, &issuer).expect("aki ok");
|
||||
validate_ee_aia_points_to_issuer_uri(&ee, "rsync://example.test/issuer.cer")
|
||||
.expect("aia ok");
|
||||
validate_ee_crldp_contains_issuer_crl_uri(&ee, "rsync://example.test/issuer.crl")
|
||||
.expect("crldp ok");
|
||||
}
|
||||
}
|
||||
146
src/validation/from_tal.rs
Normal file
146
src/validation/from_tal.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use url::Url;
|
||||
|
||||
use crate::data_model::ta::{TrustAnchor, TrustAnchorError};
|
||||
use crate::data_model::tal::{Tal, TalDecodeError};
|
||||
use crate::sync::rrdp::Fetcher;
|
||||
use crate::validation::ca_instance::{CaInstanceUris, CaInstanceUrisError, ca_instance_uris_from_ca_certificate};
|
||||
use crate::validation::objects::IssuerCaCertificateResolver;
|
||||
use crate::validation::run::{RunError, RunOutput, run_publication_point_once};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DiscoveredRootCaInstance {
|
||||
pub tal_url: Option<String>,
|
||||
pub trust_anchor: TrustAnchor,
|
||||
pub ca_instance: CaInstanceUris,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunFromTalOutput {
|
||||
pub discovery: DiscoveredRootCaInstance,
|
||||
pub run: RunOutput,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FromTalError {
|
||||
#[error("TAL fetch failed: {0} (RFC 8630 §2.2)")]
|
||||
TalFetch(String),
|
||||
|
||||
#[error("TAL decode failed: {0} (RFC 8630 §2.2)")]
|
||||
TalDecode(#[from] TalDecodeError),
|
||||
|
||||
#[error("failed to fetch TA certificate from TAL: {0} (RFC 8630 §2.3)")]
|
||||
TaFetch(String),
|
||||
|
||||
#[error("failed to bind TAL and TA certificate: {0} (RFC 8630 §2.3)")]
|
||||
Bind(#[from] TrustAnchorError),
|
||||
|
||||
#[error("failed to discover CA instance URIs from TA certificate: {0}")]
|
||||
CaInstanceUris(#[from] CaInstanceUrisError),
|
||||
|
||||
#[error("run failed: {0}")]
|
||||
Run(#[from] RunError),
|
||||
|
||||
#[error("TAL contains no TA URIs (RFC 8630 §2.2)")]
|
||||
NoTaUris,
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_url(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
tal_url: &str,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
let tal_bytes = http_fetcher
|
||||
.fetch(tal_url)
|
||||
.map_err(FromTalError::TalFetch)?;
|
||||
let tal = Tal::decode_bytes(&tal_bytes)?;
|
||||
discover_root_ca_instance_from_tal(http_fetcher, tal, Some(tal_url.to_string()))
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal(
|
||||
http_fetcher: &dyn Fetcher,
|
||||
tal: Tal,
|
||||
tal_url: Option<String>,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
if tal.ta_uris.is_empty() {
|
||||
return Err(FromTalError::NoTaUris);
|
||||
}
|
||||
|
||||
let mut last_err: Option<String> = None;
|
||||
for ta_uri in tal.ta_uris.iter() {
|
||||
let ta_der = match http_fetcher.fetch(ta_uri.as_str()) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
last_err = Some(format!("fetch {ta_uri} failed: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let trust_anchor = match TrustAnchor::bind_der(tal.clone(), &ta_der, Some(ta_uri)) {
|
||||
Ok(ta) => ta,
|
||||
Err(e) => {
|
||||
last_err = Some(format!("bind {ta_uri} failed: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let ca_instance = match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca)
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
last_err = Some(format!("CA instance discovery failed: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(DiscoveredRootCaInstance {
|
||||
tal_url,
|
||||
trust_anchor,
|
||||
ca_instance,
|
||||
});
|
||||
}
|
||||
|
||||
Err(FromTalError::TaFetch(
|
||||
last_err.unwrap_or_else(|| "unknown TA candidate error".to_string()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn discover_root_ca_instance_from_tal_and_ta_der(
|
||||
tal_bytes: &[u8],
|
||||
ta_der: &[u8],
|
||||
resolved_ta_uri: Option<&Url>,
|
||||
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
||||
let tal = Tal::decode_bytes(tal_bytes)?;
|
||||
let trust_anchor = TrustAnchor::bind_der(tal, ta_der, resolved_ta_uri)?;
|
||||
let ca_instance = ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca)?;
|
||||
Ok(DiscoveredRootCaInstance {
|
||||
tal_url: None,
|
||||
trust_anchor,
|
||||
ca_instance,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_root_from_tal_url_once(
|
||||
store: &crate::storage::RocksStore,
|
||||
policy: &crate::policy::Policy,
|
||||
tal_url: &str,
|
||||
http_fetcher: &dyn Fetcher,
|
||||
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
|
||||
issuer_resolver: &dyn IssuerCaCertificateResolver,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<RunFromTalOutput, FromTalError> {
|
||||
let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?;
|
||||
|
||||
let run = run_publication_point_once(
|
||||
store,
|
||||
policy,
|
||||
discovery.ca_instance.rrdp_notification_uri.as_deref(),
|
||||
&discovery.ca_instance.rsync_base_uri,
|
||||
&discovery.ca_instance.manifest_rsync_uri,
|
||||
&discovery.ca_instance.publication_point_rsync_uri,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
issuer_resolver,
|
||||
validation_time,
|
||||
)?;
|
||||
|
||||
Ok(RunFromTalOutput { discovery, run })
|
||||
}
|
||||
295
src/validation/manifest.rs
Normal file
295
src/validation/manifest.rs
Normal file
@@ -0,0 +1,295 @@
|
||||
use crate::data_model::manifest::{ManifestDecodeError, ManifestObject, ManifestValidateError};
|
||||
use crate::data_model::signed_object::SignedObjectVerifyError;
|
||||
use crate::policy::{CaFailedFetchPolicy, Policy};
|
||||
use crate::report::{RfcRef, Warning};
|
||||
use crate::storage::{RocksStore, StorageError, VerifiedKey, VerifiedPublicationPointPack};
|
||||
use sha2::Digest;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PublicationPointSource {
|
||||
Fresh,
|
||||
VerifiedCache,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublicationPointResult {
|
||||
pub source: PublicationPointSource,
|
||||
pub pack: VerifiedPublicationPointPack,
|
||||
pub warnings: Vec<Warning>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestFreshError {
|
||||
#[error(
|
||||
"manifest not found in raw_objects: {manifest_rsync_uri} (RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
MissingManifest { manifest_rsync_uri: String },
|
||||
|
||||
#[error("manifest decode failed: {0} (RFC 9286 §4; RFC 9286 §6.2; RFC 9286 §6.6)")]
|
||||
Decode(#[from] ManifestDecodeError),
|
||||
|
||||
#[error(
|
||||
"manifest embedded EE certificate resources invalid: {0} (RFC 9286 §5.1; RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
EeResources(#[from] ManifestValidateError),
|
||||
|
||||
#[error(
|
||||
"manifest CMS signature verification failed: {0} (RFC 6488 §3; RFC 9589 §4; RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
Signature(#[from] SignedObjectVerifyError),
|
||||
|
||||
#[error(
|
||||
"manifest is not valid at validation_time: this_update={this_update_rfc3339_utc} next_update={next_update_rfc3339_utc} validation_time={validation_time_rfc3339_utc} (RFC 9286 §6.3; RFC 9286 §6.6)"
|
||||
)]
|
||||
StaleOrEarly {
|
||||
this_update_rfc3339_utc: String,
|
||||
next_update_rfc3339_utc: String,
|
||||
validation_time_rfc3339_utc: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"manifest referenced file missing in raw_objects: {rsync_uri} (RFC 9286 §6.4; RFC 9286 §6.6)"
|
||||
)]
|
||||
MissingFile { rsync_uri: String },
|
||||
|
||||
#[error("manifest file hash mismatch: {rsync_uri} (RFC 9286 §6.5; RFC 9286 §6.6)")]
|
||||
HashMismatch { rsync_uri: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestCachedError {
|
||||
#[error("verified cache entry missing: {0} (RFC 9286 §6.6)")]
|
||||
MissingVerifiedCache(String),
|
||||
|
||||
#[error("verified cache pack invalid: {0}")]
|
||||
InvalidPack(#[from] StorageError),
|
||||
|
||||
#[error("cached manifest revalidation failed: {0}")]
|
||||
CachedManifestFresh(#[from] ManifestFreshError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestProcessError {
|
||||
#[error("manifest processing failed and cache use is disabled: {0}")]
|
||||
StopAllOutput(#[from] ManifestFreshError),
|
||||
|
||||
#[error(
|
||||
"manifest processing failed and no usable verified cache is available: fresh={fresh}; cached={cached}"
|
||||
)]
|
||||
NoUsableCache {
|
||||
fresh: ManifestFreshError,
|
||||
cached: ManifestCachedError,
|
||||
},
|
||||
|
||||
#[error("storage error during manifest processing: {0}")]
|
||||
Storage(#[from] StorageError),
|
||||
}
|
||||
|
||||
pub fn process_manifest_publication_point(
|
||||
store: &RocksStore,
|
||||
policy: &Policy,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<PublicationPointResult, ManifestProcessError> {
|
||||
let fresh = try_build_fresh_pack(
|
||||
store,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
validation_time,
|
||||
);
|
||||
|
||||
match fresh {
|
||||
Ok(pack) => {
|
||||
let key = VerifiedKey::from_manifest_rsync_uri(manifest_rsync_uri);
|
||||
let bytes = pack.encode()?;
|
||||
store.put_verified(&key, &bytes)?;
|
||||
Ok(PublicationPointResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
pack,
|
||||
warnings: Vec::new(),
|
||||
})
|
||||
}
|
||||
Err(fresh_err) => match policy.ca_failed_fetch_policy {
|
||||
CaFailedFetchPolicy::StopAllOutput => {
|
||||
Err(ManifestProcessError::StopAllOutput(fresh_err))
|
||||
}
|
||||
CaFailedFetchPolicy::UseVerifiedCache => {
|
||||
let mut warnings = vec![
|
||||
Warning::new(format!("manifest failed fetch: {fresh_err}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(manifest_rsync_uri),
|
||||
];
|
||||
|
||||
match load_and_revalidate_cached_pack(
|
||||
store,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
validation_time,
|
||||
) {
|
||||
Ok(pack) => {
|
||||
warnings.push(
|
||||
Warning::new("using verified cache for publication point")
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(manifest_rsync_uri),
|
||||
);
|
||||
Ok(PublicationPointResult {
|
||||
source: PublicationPointSource::VerifiedCache,
|
||||
pack,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
Err(cached_err) => Err(ManifestProcessError::NoUsableCache {
|
||||
fresh: fresh_err,
|
||||
cached: cached_err,
|
||||
}),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn load_and_revalidate_cached_pack(
|
||||
store: &RocksStore,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<VerifiedPublicationPointPack, ManifestCachedError> {
|
||||
let key = VerifiedKey::from_manifest_rsync_uri(manifest_rsync_uri);
|
||||
let bytes = store
|
||||
.get_verified(&key)?
|
||||
.ok_or_else(|| ManifestCachedError::MissingVerifiedCache(key.as_str().to_string()))?;
|
||||
let pack = VerifiedPublicationPointPack::decode(&bytes)?;
|
||||
|
||||
if pack.manifest_rsync_uri != manifest_rsync_uri {
|
||||
return Err(ManifestCachedError::InvalidPack(StorageError::RocksDb(
|
||||
"cached pack manifest_rsync_uri does not match key".to_string(),
|
||||
)));
|
||||
}
|
||||
if pack.publication_point_rsync_uri != publication_point_rsync_uri {
|
||||
return Err(ManifestCachedError::InvalidPack(StorageError::RocksDb(
|
||||
"cached pack publication_point_rsync_uri does not match expected".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
revalidate_pack_with_current_time(&pack, validation_time).map_err(ManifestCachedError::from)?;
|
||||
Ok(pack)
|
||||
}
|
||||
|
||||
fn revalidate_pack_with_current_time(
|
||||
pack: &VerifiedPublicationPointPack,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<(), ManifestFreshError> {
|
||||
let manifest = ManifestObject::decode_der(&pack.manifest_bytes)?;
|
||||
manifest.validate_embedded_ee_cert()?;
|
||||
manifest.signed_object.verify()?;
|
||||
|
||||
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);
|
||||
if now < this_update || now > next_update {
|
||||
return Err(ManifestFreshError::StaleOrEarly {
|
||||
this_update_rfc3339_utc: this_update
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("format thisUpdate"),
|
||||
next_update_rfc3339_utc: next_update
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("format nextUpdate"),
|
||||
validation_time_rfc3339_utc: now
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("format validation_time"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_build_fresh_pack(
|
||||
store: &RocksStore,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<VerifiedPublicationPointPack, ManifestFreshError> {
|
||||
let manifest_bytes = store
|
||||
.get_raw(manifest_rsync_uri)
|
||||
.map_err(|e| ManifestFreshError::MissingManifest {
|
||||
manifest_rsync_uri: format!("{manifest_rsync_uri} ({e})"),
|
||||
})?
|
||||
.ok_or_else(|| ManifestFreshError::MissingManifest {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
})?;
|
||||
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes)?;
|
||||
manifest.validate_embedded_ee_cert()?;
|
||||
manifest.signed_object.verify()?;
|
||||
|
||||
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);
|
||||
if now < this_update || now > next_update {
|
||||
return Err(ManifestFreshError::StaleOrEarly {
|
||||
this_update_rfc3339_utc: this_update
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("format thisUpdate"),
|
||||
next_update_rfc3339_utc: next_update
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("format nextUpdate"),
|
||||
validation_time_rfc3339_utc: now
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.expect("format validation_time"),
|
||||
});
|
||||
}
|
||||
|
||||
let mut files = Vec::with_capacity(manifest.manifest.files.len());
|
||||
for entry in &manifest.manifest.files {
|
||||
let rsync_uri = join_rsync_dir_and_file(publication_point_rsync_uri, &entry.file_name);
|
||||
let bytes = store
|
||||
.get_raw(&rsync_uri)
|
||||
.map_err(|_e| ManifestFreshError::MissingFile {
|
||||
rsync_uri: rsync_uri.clone(),
|
||||
})?
|
||||
.ok_or_else(|| ManifestFreshError::MissingFile {
|
||||
rsync_uri: rsync_uri.clone(),
|
||||
})?;
|
||||
|
||||
let computed = sha2::Sha256::digest(&bytes);
|
||||
if computed.as_slice() != entry.hash_bytes.as_slice() {
|
||||
return Err(ManifestFreshError::HashMismatch { rsync_uri });
|
||||
}
|
||||
|
||||
files.push(crate::storage::PackFile::from_bytes_compute_sha256(
|
||||
rsync_uri, bytes,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(VerifiedPublicationPointPack {
|
||||
format_version: VerifiedPublicationPointPack::FORMAT_VERSION_V1,
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
publication_point_rsync_uri: publication_point_rsync_uri.to_string(),
|
||||
this_update: crate::storage::PackTime::from_utc_offset_datetime(this_update),
|
||||
next_update: crate::storage::PackTime::from_utc_offset_datetime(next_update),
|
||||
verified_at: crate::storage::PackTime::from_utc_offset_datetime(now),
|
||||
manifest_bytes,
|
||||
files,
|
||||
})
|
||||
}
|
||||
|
||||
fn join_rsync_dir_and_file(base: &str, file_name: &str) -> String {
|
||||
if base.ends_with('/') {
|
||||
format!("{base}{file_name}")
|
||||
} else {
|
||||
format!("{base}/{file_name}")
|
||||
}
|
||||
}
|
||||
10
src/validation/mod.rs
Normal file
10
src/validation/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
pub mod cert_path;
|
||||
pub mod ca_instance;
|
||||
pub mod ca_path;
|
||||
pub mod from_tal;
|
||||
pub mod manifest;
|
||||
pub mod objects;
|
||||
pub mod run;
|
||||
pub mod tree;
|
||||
pub mod tree_runner;
|
||||
pub mod run_tree_from_tal;
|
||||
1293
src/validation/objects.rs
Normal file
1293
src/validation/objects.rs
Normal file
File diff suppressed because it is too large
Load Diff
86
src/validation/run.rs
Normal file
86
src/validation/run.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use crate::fetch::rsync::RsyncFetcher;
|
||||
use crate::policy::Policy;
|
||||
use crate::storage::{RocksStore, VerifiedKey};
|
||||
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::{
|
||||
IssuerCaCertificateResolver, ObjectsOutput, ObjectsProcessError,
|
||||
process_verified_publication_point_pack,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunOutput {
|
||||
pub repo_sync: RepoSyncResult,
|
||||
pub publication_point: PublicationPointResult,
|
||||
pub objects: ObjectsOutput,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RunError {
|
||||
#[error("repo sync failed: {0}")]
|
||||
RepoSync(#[from] crate::sync::repo::RepoSyncError),
|
||||
|
||||
#[error("manifest processing failed: {0}")]
|
||||
Manifest(#[from] crate::validation::manifest::ManifestProcessError),
|
||||
|
||||
#[error("objects processing failed: {0}")]
|
||||
Objects(#[from] ObjectsProcessError),
|
||||
}
|
||||
|
||||
/// v1 serial offline-friendly end-to-end execution for a single publication point.
|
||||
///
|
||||
/// This orchestrates:
|
||||
/// 1) repo sync (RRDP or rsync fallback) into `raw_objects`
|
||||
/// 2) manifest RP processing into a verified pack (`verified:<manifest-rsync-uri>`)
|
||||
/// 3) signed object processing (ROA/ASPA) from the verified pack
|
||||
pub fn run_publication_point_once(
|
||||
store: &RocksStore,
|
||||
policy: &Policy,
|
||||
rrdp_notification_uri: Option<&str>,
|
||||
rsync_base_uri: &str,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
http_fetcher: &dyn HttpFetcher,
|
||||
rsync_fetcher: &dyn RsyncFetcher,
|
||||
issuer_resolver: &dyn IssuerCaCertificateResolver,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<RunOutput, RunError> {
|
||||
let repo_sync = sync_publication_point(
|
||||
store,
|
||||
policy,
|
||||
rrdp_notification_uri,
|
||||
rsync_base_uri,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
)?;
|
||||
|
||||
let publication_point = process_manifest_publication_point(
|
||||
store,
|
||||
policy,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
validation_time,
|
||||
)?;
|
||||
|
||||
let objects = process_verified_publication_point_pack(
|
||||
&publication_point.pack,
|
||||
policy,
|
||||
issuer_resolver,
|
||||
validation_time,
|
||||
)?;
|
||||
|
||||
Ok(RunOutput {
|
||||
repo_sync,
|
||||
publication_point,
|
||||
objects,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn verified_pack_exists(store: &RocksStore, manifest_rsync_uri: &str) -> Result<bool, String> {
|
||||
let key = VerifiedKey::from_manifest_rsync_uri(manifest_rsync_uri);
|
||||
store
|
||||
.get_verified(&key)
|
||||
.map(|v| v.is_some())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
202
src/validation/run_tree_from_tal.rs
Normal file
202
src/validation/run_tree_from_tal.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use url::Url;
|
||||
|
||||
use crate::data_model::ta::TrustAnchor;
|
||||
use crate::sync::rrdp::Fetcher;
|
||||
use crate::audit::PublicationPointAudit;
|
||||
use crate::validation::from_tal::{
|
||||
DiscoveredRootCaInstance, FromTalError, discover_root_ca_instance_from_tal_and_ta_der,
|
||||
discover_root_ca_instance_from_tal_url,
|
||||
};
|
||||
use crate::validation::tree::{CaInstanceHandle, TreeRunConfig, TreeRunError, TreeRunOutput, run_tree_serial};
|
||||
use crate::validation::tree_runner::Rpkiv1PublicationPointRunner;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunTreeFromTalOutput {
|
||||
pub discovery: DiscoveredRootCaInstance,
|
||||
pub tree: TreeRunOutput,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunTreeFromTalAuditOutput {
|
||||
pub discovery: DiscoveredRootCaInstance,
|
||||
pub tree: TreeRunOutput,
|
||||
pub publication_points: Vec<PublicationPointAudit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RunTreeFromTalError {
|
||||
#[error("{0}")]
|
||||
FromTal(#[from] FromTalError),
|
||||
|
||||
#[error("{0}")]
|
||||
Tree(#[from] TreeRunError),
|
||||
}
|
||||
|
||||
pub fn root_handle_from_trust_anchor(
|
||||
trust_anchor: &TrustAnchor,
|
||||
ca_certificate_rsync_uri: Option<String>,
|
||||
ca_instance: &crate::validation::ca_instance::CaInstanceUris,
|
||||
) -> CaInstanceHandle {
|
||||
let ta_rc = trust_anchor.ta_certificate.rc_ca.clone();
|
||||
CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: trust_anchor.ta_certificate.raw_der.clone(),
|
||||
ca_certificate_rsync_uri,
|
||||
effective_ip_resources: ta_rc.tbs.extensions.ip_resources.clone(),
|
||||
effective_as_resources: ta_rc.tbs.extensions.as_resources.clone(),
|
||||
rsync_base_uri: ca_instance.rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: ca_instance.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: ca_instance.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: ca_instance.rrdp_notification_uri.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_url_serial(
|
||||
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,
|
||||
) -> Result<RunTreeFromTalOutput, RunTreeFromTalError> {
|
||||
let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?;
|
||||
|
||||
let runner = Rpkiv1PublicationPointRunner {
|
||||
store,
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
|
||||
let tree = run_tree_serial(root, &runner, config)?;
|
||||
|
||||
Ok(RunTreeFromTalOutput { discovery, tree })
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_url_serial_audit(
|
||||
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,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
|
||||
let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?;
|
||||
|
||||
let runner = Rpkiv1PublicationPointRunner {
|
||||
store,
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
};
|
||||
|
||||
let audits: std::cell::RefCell<Vec<PublicationPointAudit>> = std::cell::RefCell::new(Vec::new());
|
||||
struct AuditingRunner<'a> {
|
||||
inner: &'a Rpkiv1PublicationPointRunner<'a>,
|
||||
audits: &'a std::cell::RefCell<Vec<PublicationPointAudit>>,
|
||||
}
|
||||
impl<'a> crate::validation::tree::PublicationPointRunner for AuditingRunner<'a> {
|
||||
fn run_publication_point(
|
||||
&self,
|
||||
ca: &crate::validation::tree::CaInstanceHandle,
|
||||
) -> Result<crate::validation::tree::PublicationPointRunResult, String> {
|
||||
let res = self.inner.run_publication_point(ca)?;
|
||||
self.audits.borrow_mut().push(res.audit.clone());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
|
||||
let auditing_runner = AuditingRunner { inner: &runner, audits: &audits };
|
||||
let tree = run_tree_serial(root, &auditing_runner, config)?;
|
||||
let publication_points = audits.into_inner();
|
||||
|
||||
Ok(RunTreeFromTalAuditOutput {
|
||||
discovery,
|
||||
tree,
|
||||
publication_points,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_and_ta_der_serial(
|
||||
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,
|
||||
) -> Result<RunTreeFromTalOutput, RunTreeFromTalError> {
|
||||
let discovery =
|
||||
discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?;
|
||||
|
||||
let runner = Rpkiv1PublicationPointRunner {
|
||||
store,
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
|
||||
let tree = run_tree_serial(root, &runner, config)?;
|
||||
|
||||
Ok(RunTreeFromTalOutput { discovery, tree })
|
||||
}
|
||||
|
||||
pub fn run_tree_from_tal_and_ta_der_serial_audit(
|
||||
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,
|
||||
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
|
||||
let discovery =
|
||||
discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?;
|
||||
|
||||
let runner = Rpkiv1PublicationPointRunner {
|
||||
store,
|
||||
policy,
|
||||
http_fetcher,
|
||||
rsync_fetcher,
|
||||
validation_time,
|
||||
};
|
||||
|
||||
let audits: std::cell::RefCell<Vec<PublicationPointAudit>> = std::cell::RefCell::new(Vec::new());
|
||||
struct AuditingRunner<'a> {
|
||||
inner: &'a Rpkiv1PublicationPointRunner<'a>,
|
||||
audits: &'a std::cell::RefCell<Vec<PublicationPointAudit>>,
|
||||
}
|
||||
impl<'a> crate::validation::tree::PublicationPointRunner for AuditingRunner<'a> {
|
||||
fn run_publication_point(
|
||||
&self,
|
||||
ca: &crate::validation::tree::CaInstanceHandle,
|
||||
) -> Result<crate::validation::tree::PublicationPointRunResult, String> {
|
||||
let res = self.inner.run_publication_point(ca)?;
|
||||
self.audits.borrow_mut().push(res.audit.clone());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
let root = root_handle_from_trust_anchor(&discovery.trust_anchor, None, &discovery.ca_instance);
|
||||
let auditing_runner = AuditingRunner { inner: &runner, audits: &audits };
|
||||
let tree = run_tree_serial(root, &auditing_runner, config)?;
|
||||
let publication_points = audits.into_inner();
|
||||
|
||||
Ok(RunTreeFromTalAuditOutput {
|
||||
discovery,
|
||||
tree,
|
||||
publication_points,
|
||||
})
|
||||
}
|
||||
163
src/validation/tree.rs
Normal file
163
src/validation/tree.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use crate::report::{RfcRef, Warning};
|
||||
use crate::storage::VerifiedPublicationPointPack;
|
||||
use crate::validation::manifest::PublicationPointSource;
|
||||
use crate::audit::PublicationPointAudit;
|
||||
use crate::data_model::rc::{AsResourceSet, IpResourceSet};
|
||||
use crate::validation::objects::{AspaAttestation, ObjectsOutput, Vrp};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TreeRunConfig {
|
||||
/// Max CA instance depth to process (0 = root only).
|
||||
pub max_depth: Option<usize>,
|
||||
/// Max number of CA instances to process.
|
||||
pub max_instances: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for TreeRunConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_depth: None,
|
||||
max_instances: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CaInstanceHandle {
|
||||
pub depth: usize,
|
||||
/// DER bytes of the CA certificate for this CA instance.
|
||||
pub ca_certificate_der: Vec<u8>,
|
||||
/// rsync URI of this CA certificate object (where it is published).
|
||||
///
|
||||
/// This is used for strict AIA binding checks (RFC 6487 §4.8.7) when validating
|
||||
/// subordinate CA and EE certificates. Trust anchor certificates downloaded from
|
||||
/// TAL URIs may not have an rsync publication URI, so this can be `None`.
|
||||
pub ca_certificate_rsync_uri: Option<String>,
|
||||
/// Effective (fully resolved) resources for this CA instance, used for resource-path
|
||||
/// validation of subordinate CA certificates.
|
||||
pub effective_ip_resources: Option<IpResourceSet>,
|
||||
pub effective_as_resources: Option<AsResourceSet>,
|
||||
pub rsync_base_uri: String,
|
||||
pub manifest_rsync_uri: String,
|
||||
pub publication_point_rsync_uri: String,
|
||||
pub rrdp_notification_uri: Option<String>,
|
||||
}
|
||||
|
||||
impl CaInstanceHandle {
|
||||
pub fn with_depth(mut self, depth: usize) -> Self {
|
||||
self.depth = depth;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublicationPointRunResult {
|
||||
pub source: PublicationPointSource,
|
||||
pub pack: VerifiedPublicationPointPack,
|
||||
pub warnings: Vec<Warning>,
|
||||
pub objects: ObjectsOutput,
|
||||
pub audit: PublicationPointAudit,
|
||||
/// Candidate child CA instances discovered from this publication point.
|
||||
///
|
||||
/// RFC 9286 §6.6 restriction is enforced by the tree engine: if this
|
||||
/// publication point used verified cache due to failed fetch, children MUST NOT
|
||||
/// be enqueued/processed in this run.
|
||||
pub discovered_children: Vec<CaInstanceHandle>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TreeRunOutput {
|
||||
pub instances_processed: usize,
|
||||
pub instances_failed: usize,
|
||||
pub warnings: Vec<Warning>,
|
||||
pub vrps: Vec<Vrp>,
|
||||
pub aspas: Vec<AspaAttestation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TreeRunError {
|
||||
#[error("publication point runner failed: {0}")]
|
||||
Runner(String),
|
||||
}
|
||||
|
||||
pub trait PublicationPointRunner {
|
||||
fn run_publication_point(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
) -> Result<PublicationPointRunResult, String>;
|
||||
}
|
||||
|
||||
pub fn run_tree_serial(
|
||||
root: CaInstanceHandle,
|
||||
runner: &dyn PublicationPointRunner,
|
||||
config: &TreeRunConfig,
|
||||
) -> Result<TreeRunOutput, TreeRunError> {
|
||||
let mut queue: std::collections::VecDeque<CaInstanceHandle> = std::collections::VecDeque::new();
|
||||
queue.push_back(root);
|
||||
|
||||
let mut visited_manifest_uris: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut instances_processed = 0usize;
|
||||
let mut instances_failed = 0usize;
|
||||
let mut warnings: Vec<Warning> = Vec::new();
|
||||
let mut vrps: Vec<Vrp> = Vec::new();
|
||||
let mut aspas: Vec<AspaAttestation> = Vec::new();
|
||||
|
||||
while let Some(ca) = queue.pop_front() {
|
||||
if !visited_manifest_uris.insert(ca.manifest_rsync_uri.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(max_instances) = config.max_instances {
|
||||
if instances_processed >= max_instances {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(max_depth) = config.max_depth {
|
||||
if ca.depth > max_depth {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let res = match runner.run_publication_point(&ca) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
instances_failed += 1;
|
||||
warnings.push(
|
||||
Warning::new(format!("publication point failed: {e}"))
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
instances_processed += 1;
|
||||
warnings.extend(res.warnings.clone());
|
||||
warnings.extend(res.objects.warnings.clone());
|
||||
vrps.extend(res.objects.vrps.clone());
|
||||
aspas.extend(res.objects.aspas.clone());
|
||||
|
||||
let enqueue_children = res.source == PublicationPointSource::Fresh;
|
||||
if !enqueue_children && !res.discovered_children.is_empty() {
|
||||
warnings.push(
|
||||
Warning::new("skipping child CA discovery due to failed fetch cache use")
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
}
|
||||
|
||||
if enqueue_children {
|
||||
for child in res.discovered_children {
|
||||
queue.push_back(child.with_depth(ca.depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TreeRunOutput {
|
||||
instances_processed,
|
||||
instances_failed,
|
||||
warnings,
|
||||
vrps,
|
||||
aspas,
|
||||
})
|
||||
}
|
||||
836
src/validation/tree_runner.rs
Normal file
836
src/validation/tree_runner.rs
Normal file
@@ -0,0 +1,836 @@
|
||||
use crate::fetch::rsync::RsyncFetcher;
|
||||
use crate::policy::Policy;
|
||||
use crate::report::{RfcRef, Warning};
|
||||
use crate::storage::RocksStore;
|
||||
use crate::sync::repo::sync_publication_point;
|
||||
use crate::sync::rrdp::Fetcher;
|
||||
use crate::audit::{
|
||||
AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit,
|
||||
sha256_hex, sha256_hex_from_32,
|
||||
};
|
||||
use crate::validation::ca_instance::ca_instance_uris_from_ca_certificate;
|
||||
use crate::validation::ca_path::{CaPathError, validate_subordinate_ca_cert};
|
||||
use crate::validation::manifest::{PublicationPointSource, process_manifest_publication_point};
|
||||
use crate::validation::objects::process_verified_publication_point_pack_for_issuer;
|
||||
use crate::validation::tree::{CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner};
|
||||
|
||||
pub struct Rpkiv1PublicationPointRunner<'a> {
|
||||
pub store: &'a RocksStore,
|
||||
pub policy: &'a Policy,
|
||||
pub http_fetcher: &'a dyn Fetcher,
|
||||
pub rsync_fetcher: &'a dyn RsyncFetcher,
|
||||
pub validation_time: time::OffsetDateTime,
|
||||
}
|
||||
|
||||
impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
fn run_publication_point(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
) -> Result<PublicationPointRunResult, String> {
|
||||
let mut warnings: Vec<Warning> = Vec::new();
|
||||
|
||||
if let Err(e) = sync_publication_point(
|
||||
self.store,
|
||||
self.policy,
|
||||
ca.rrdp_notification_uri.as_deref(),
|
||||
&ca.rsync_base_uri,
|
||||
self.http_fetcher,
|
||||
self.rsync_fetcher,
|
||||
) {
|
||||
warnings.push(
|
||||
Warning::new(format!("repo sync failed (continuing with cached/raw data): {e}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 8182 §3.4.5"), RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.rsync_base_uri),
|
||||
);
|
||||
}
|
||||
|
||||
let pp = match process_manifest_publication_point(
|
||||
self.store,
|
||||
self.policy,
|
||||
&ca.manifest_rsync_uri,
|
||||
&ca.publication_point_rsync_uri,
|
||||
self.validation_time,
|
||||
) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Err(format!("{e}")),
|
||||
};
|
||||
|
||||
warnings.extend(pp.warnings.clone());
|
||||
|
||||
let objects = process_verified_publication_point_pack_for_issuer(
|
||||
&pp.pack,
|
||||
self.policy,
|
||||
&ca.ca_certificate_der,
|
||||
ca.ca_certificate_rsync_uri.as_deref(),
|
||||
ca.effective_ip_resources.as_ref(),
|
||||
ca.effective_as_resources.as_ref(),
|
||||
self.validation_time,
|
||||
);
|
||||
|
||||
let (discovered_children, child_audits) = if pp.source == PublicationPointSource::Fresh {
|
||||
match discover_children_from_fresh_pack_with_audit(ca, &pp.pack, self.validation_time) {
|
||||
Ok(out) => (out.children, out.audits),
|
||||
Err(e) => {
|
||||
warnings.push(
|
||||
Warning::new(format!("child CA discovery failed: {e}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2")])
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
(Vec::new(), Vec::new())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(Vec::new(), Vec::new())
|
||||
};
|
||||
|
||||
let audit = build_publication_point_audit(ca, &pp, &warnings, &objects, &child_audits);
|
||||
|
||||
Ok(PublicationPointRunResult {
|
||||
source: pp.source,
|
||||
pack: pp.pack,
|
||||
warnings,
|
||||
objects,
|
||||
audit,
|
||||
discovered_children,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ChildDiscoveryOutput {
|
||||
children: Vec<CaInstanceHandle>,
|
||||
audits: Vec<ObjectAuditEntry>,
|
||||
}
|
||||
|
||||
fn discover_children_from_fresh_pack_with_audit(
|
||||
issuer: &CaInstanceHandle,
|
||||
pack: &crate::storage::VerifiedPublicationPointPack,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<ChildDiscoveryOutput, String> {
|
||||
let issuer_ca_der = issuer.ca_certificate_der.as_slice();
|
||||
let (issuer_crl_uri, issuer_crl_der) = select_issuer_crl_from_pack(issuer, pack)?;
|
||||
|
||||
let mut out: Vec<CaInstanceHandle> = Vec::new();
|
||||
let mut audits: Vec<ObjectAuditEntry> = Vec::new();
|
||||
for f in &pack.files {
|
||||
if !f.rsync_uri.ends_with(".cer") {
|
||||
continue;
|
||||
}
|
||||
let child_der = f.bytes.as_slice();
|
||||
|
||||
let validated = match validate_subordinate_ca_cert(
|
||||
child_der,
|
||||
issuer_ca_der,
|
||||
issuer_crl_der,
|
||||
issuer.ca_certificate_rsync_uri.as_deref(),
|
||||
issuer_crl_uri,
|
||||
issuer.effective_ip_resources.as_ref(),
|
||||
issuer.effective_as_resources.as_ref(),
|
||||
validation_time,
|
||||
) {
|
||||
Ok(v) => v,
|
||||
Err(CaPathError::ChildNotCa) => {
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&f.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Skipped,
|
||||
detail: Some("skipped: not a CA resource certificate".to_string()),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&f.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(e.to_string()),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let uris = match ca_instance_uris_from_ca_certificate(&validated.child_ca) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&f.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Error,
|
||||
detail: Some(format!("CA instance URI discovery failed: {e}")),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
out.push(CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: child_der.to_vec(),
|
||||
ca_certificate_rsync_uri: Some(f.rsync_uri.clone()),
|
||||
effective_ip_resources: validated.effective_ip_resources.clone(),
|
||||
effective_as_resources: validated.effective_as_resources.clone(),
|
||||
rsync_base_uri: uris.rsync_base_uri,
|
||||
manifest_rsync_uri: uris.manifest_rsync_uri,
|
||||
publication_point_rsync_uri: uris.publication_point_rsync_uri,
|
||||
rrdp_notification_uri: uris.rrdp_notification_uri,
|
||||
});
|
||||
|
||||
audits.push(ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&f.sha256),
|
||||
kind: AuditObjectKind::Certificate,
|
||||
result: AuditObjectResult::Ok,
|
||||
detail: Some("validated subordinate CA certificate; enqueued CA instance".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ChildDiscoveryOutput { children: out, audits })
|
||||
}
|
||||
|
||||
fn select_issuer_crl_from_pack<'a>(
|
||||
issuer: &CaInstanceHandle,
|
||||
pack: &'a crate::storage::VerifiedPublicationPointPack,
|
||||
) -> Result<(&'a str, &'a [u8]), String> {
|
||||
let issuer_ca = crate::data_model::rc::ResourceCertificate::decode_der(&issuer.ca_certificate_der)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let subject_dn = issuer_ca.tbs.subject_dn;
|
||||
|
||||
if let Some(uris) = issuer_ca.tbs.extensions.crl_distribution_points_uris.as_ref() {
|
||||
for u in uris {
|
||||
let s = u.as_str();
|
||||
if let Some(f) = pack.files.iter().find(|f| f.rsync_uri == s) {
|
||||
return Ok((f.rsync_uri.as_str(), f.bytes.as_slice()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for f in &pack.files {
|
||||
if !f.rsync_uri.ends_with(".crl") {
|
||||
continue;
|
||||
}
|
||||
let Ok(crl) = crate::data_model::crl::RpkixCrl::decode_der(&f.bytes) else {
|
||||
continue;
|
||||
};
|
||||
if crl.issuer_dn == subject_dn {
|
||||
return Ok((f.rsync_uri.as_str(), f.bytes.as_slice()));
|
||||
}
|
||||
}
|
||||
Err("issuer CRL not found in verified pack (RFC 9286 §7)".to_string())
|
||||
}
|
||||
|
||||
fn kind_from_rsync_uri(uri: &str) -> AuditObjectKind {
|
||||
if uri.ends_with(".crl") {
|
||||
AuditObjectKind::Crl
|
||||
} else if uri.ends_with(".cer") {
|
||||
AuditObjectKind::Certificate
|
||||
} else if uri.ends_with(".roa") {
|
||||
AuditObjectKind::Roa
|
||||
} else if uri.ends_with(".asa") {
|
||||
AuditObjectKind::Aspa
|
||||
} else {
|
||||
AuditObjectKind::Other
|
||||
}
|
||||
}
|
||||
|
||||
fn build_publication_point_audit(
|
||||
ca: &CaInstanceHandle,
|
||||
pp: &crate::validation::manifest::PublicationPointResult,
|
||||
runner_warnings: &[Warning],
|
||||
objects: &crate::validation::objects::ObjectsOutput,
|
||||
child_audits: &[ObjectAuditEntry],
|
||||
) -> PublicationPointAudit {
|
||||
use crate::data_model::crl::RpkixCrl;
|
||||
use crate::validation::manifest::PublicationPointSource;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
|
||||
for f in &pp.pack.files {
|
||||
audit_by_uri.insert(
|
||||
f.rsync_uri.clone(),
|
||||
ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&f.sha256),
|
||||
kind: kind_from_rsync_uri(&f.rsync_uri),
|
||||
result: AuditObjectResult::Skipped,
|
||||
detail: Some("skipped: not processed in stage2".to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// CRL decode status (best-effort).
|
||||
for f in &pp.pack.files {
|
||||
if !f.rsync_uri.ends_with(".crl") {
|
||||
continue;
|
||||
}
|
||||
let ok = RpkixCrl::decode_der(&f.bytes).is_ok();
|
||||
audit_by_uri.insert(
|
||||
f.rsync_uri.clone(),
|
||||
ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&f.sha256),
|
||||
kind: AuditObjectKind::Crl,
|
||||
result: if ok {
|
||||
AuditObjectResult::Ok
|
||||
} else {
|
||||
AuditObjectResult::Error
|
||||
},
|
||||
detail: if ok {
|
||||
None
|
||||
} else {
|
||||
Some("CRL decode failed".to_string())
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Child discovery audits (.cer).
|
||||
for e in child_audits {
|
||||
audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
|
||||
}
|
||||
|
||||
// Signed object audits (.roa/.asa).
|
||||
for e in &objects.audit {
|
||||
audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
|
||||
}
|
||||
|
||||
// Emit as a stable order: manifest first, then pack files as listed in the pack.
|
||||
let mut objects_out: Vec<ObjectAuditEntry> = Vec::with_capacity(pp.pack.files.len() + 1);
|
||||
objects_out.push(ObjectAuditEntry {
|
||||
rsync_uri: pp.pack.manifest_rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex(&pp.pack.manifest_bytes),
|
||||
kind: AuditObjectKind::Manifest,
|
||||
result: AuditObjectResult::Ok,
|
||||
detail: None,
|
||||
});
|
||||
for f in &pp.pack.files {
|
||||
if let Some(e) = audit_by_uri.remove(&f.rsync_uri) {
|
||||
objects_out.push(e);
|
||||
} else {
|
||||
objects_out.push(ObjectAuditEntry {
|
||||
rsync_uri: f.rsync_uri.clone(),
|
||||
sha256_hex: sha256_hex_from_32(&f.sha256),
|
||||
kind: kind_from_rsync_uri(&f.rsync_uri),
|
||||
result: AuditObjectResult::Skipped,
|
||||
detail: Some("skipped: no audit entry".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
// `runner_warnings` already includes `pp.warnings` (mirrors the runtime propagation behavior).
|
||||
warnings.extend(runner_warnings.iter().map(AuditWarning::from));
|
||||
warnings.extend(objects.warnings.iter().map(AuditWarning::from));
|
||||
|
||||
PublicationPointAudit {
|
||||
rsync_base_uri: ca.rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
|
||||
source: match pp.source {
|
||||
PublicationPointSource::Fresh => "fresh".to_string(),
|
||||
PublicationPointSource::VerifiedCache => "verified_cache".to_string(),
|
||||
},
|
||||
this_update_rfc3339_utc: pp.pack.this_update.rfc3339_utc.clone(),
|
||||
next_update_rfc3339_utc: pp.pack.next_update.rfc3339_utc.clone(),
|
||||
verified_at_rfc3339_utc: pp.pack.verified_at.rfc3339_utc.clone(),
|
||||
warnings,
|
||||
objects: objects_out,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data_model::rc::ResourceCertificate;
|
||||
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
||||
use crate::storage::{PackFile, PackTime, VerifiedPublicationPointPack};
|
||||
use crate::sync::rrdp::Fetcher;
|
||||
use crate::validation::tree::PublicationPointRunner;
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
struct NeverHttpFetcher;
|
||||
impl Fetcher for NeverHttpFetcher {
|
||||
fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
|
||||
Err("http fetch disabled in test".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn openssl_available() -> bool {
|
||||
Command::new("openssl")
|
||||
.arg("version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
struct Generated {
|
||||
issuer_ca_der: Vec<u8>,
|
||||
child_ca_der: Vec<u8>,
|
||||
issuer_crl_der: Vec<u8>,
|
||||
}
|
||||
|
||||
fn run(cmd: &mut Command) {
|
||||
let out = cmd.output().expect("run command");
|
||||
if !out.status.success() {
|
||||
panic!(
|
||||
"command failed: {:?}\nstdout={}\nstderr={}",
|
||||
cmd,
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_chain_and_crl() -> Generated {
|
||||
assert!(openssl_available(), "openssl is required for this test");
|
||||
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let dir = td.path();
|
||||
|
||||
std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts");
|
||||
std::fs::write(dir.join("index.txt"), b"").expect("index");
|
||||
std::fs::write(dir.join("serial"), b"1000\n").expect("serial");
|
||||
std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber");
|
||||
|
||||
let cnf = format!(
|
||||
r#"
|
||||
[ ca ]
|
||||
default_ca = CA_default
|
||||
|
||||
[ CA_default ]
|
||||
dir = {dir}
|
||||
database = $dir/index.txt
|
||||
new_certs_dir = $dir/newcerts
|
||||
certificate = $dir/issuer.pem
|
||||
private_key = $dir/issuer.key
|
||||
serial = $dir/serial
|
||||
crlnumber = $dir/crlnumber
|
||||
default_md = sha256
|
||||
default_days = 365
|
||||
default_crl_days = 1
|
||||
policy = policy_any
|
||||
x509_extensions = v3_issuer_ca
|
||||
crl_extensions = crl_ext
|
||||
unique_subject = no
|
||||
copy_extensions = none
|
||||
|
||||
[ policy_any ]
|
||||
commonName = supplied
|
||||
|
||||
[ req ]
|
||||
prompt = no
|
||||
distinguished_name = dn
|
||||
|
||||
[ dn ]
|
||||
CN = Test Issuer CA
|
||||
|
||||
[ v3_issuer_ca ]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical, keyCertSign, cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
|
||||
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml
|
||||
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
|
||||
sbgp-autonomousSysNum = critical, AS:64496-64511
|
||||
|
||||
[ v3_child_ca ]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical, keyCertSign, cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl
|
||||
authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer
|
||||
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
|
||||
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/child/, rpkiManifest;URI:rsync://example.test/repo/child/child.mft, rpkiNotify;URI:https://example.test/notification.xml
|
||||
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16
|
||||
sbgp-autonomousSysNum = critical, AS:64496
|
||||
|
||||
[ crl_ext ]
|
||||
authorityKeyIdentifier = keyid:always
|
||||
"#,
|
||||
dir = dir.display()
|
||||
);
|
||||
std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf");
|
||||
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("genrsa")
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.key"))
|
||||
.arg("2048"),
|
||||
);
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("req")
|
||||
.arg("-new")
|
||||
.arg("-x509")
|
||||
.arg("-sha256")
|
||||
.arg("-days")
|
||||
.arg("365")
|
||||
.arg("-key")
|
||||
.arg(dir.join("issuer.key"))
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-extensions")
|
||||
.arg("v3_issuer_ca")
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.pem")),
|
||||
);
|
||||
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("genrsa")
|
||||
.arg("-out")
|
||||
.arg(dir.join("child.key"))
|
||||
.arg("2048"),
|
||||
);
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("req")
|
||||
.arg("-new")
|
||||
.arg("-key")
|
||||
.arg(dir.join("child.key"))
|
||||
.arg("-subj")
|
||||
.arg("/CN=Test Child CA")
|
||||
.arg("-out")
|
||||
.arg(dir.join("child.csr")),
|
||||
);
|
||||
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-batch")
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-in")
|
||||
.arg(dir.join("child.csr"))
|
||||
.arg("-extensions")
|
||||
.arg("v3_child_ca")
|
||||
.arg("-out")
|
||||
.arg(dir.join("child.pem")),
|
||||
);
|
||||
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("x509")
|
||||
.arg("-in")
|
||||
.arg(dir.join("issuer.pem"))
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.cer")),
|
||||
);
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("x509")
|
||||
.arg("-in")
|
||||
.arg(dir.join("child.pem"))
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(dir.join("child.cer")),
|
||||
);
|
||||
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-gencrl")
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.crl.pem")),
|
||||
);
|
||||
run(
|
||||
Command::new("openssl")
|
||||
.arg("crl")
|
||||
.arg("-in")
|
||||
.arg(dir.join("issuer.crl.pem"))
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.crl")),
|
||||
);
|
||||
|
||||
Generated {
|
||||
issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"),
|
||||
child_ca_der: std::fs::read(dir.join("child.cer")).expect("read child der"),
|
||||
issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"),
|
||||
}
|
||||
}
|
||||
|
||||
fn dummy_pack_with_files(files: Vec<PackFile>) -> VerifiedPublicationPointPack {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
VerifiedPublicationPointPack {
|
||||
format_version: VerifiedPublicationPointPack::FORMAT_VERSION_V1,
|
||||
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
this_update: PackTime::from_utc_offset_datetime(now),
|
||||
next_update: PackTime::from_utc_offset_datetime(now + time::Duration::hours(1)),
|
||||
verified_at: PackTime::from_utc_offset_datetime(now),
|
||||
manifest_bytes: vec![0x01],
|
||||
files,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_issuer_crl_from_pack_finds_matching_crl() {
|
||||
let g = generate_chain_and_crl();
|
||||
|
||||
let pack = dummy_pack_with_files(vec![PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
g.issuer_crl_der.clone(),
|
||||
)]);
|
||||
|
||||
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
|
||||
let issuer = CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: g.issuer_ca_der.clone(),
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
|
||||
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
|
||||
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
|
||||
let (uri, found) = select_issuer_crl_from_pack(&issuer, &pack).expect("find crl");
|
||||
assert_eq!(uri, "rsync://example.test/repo/issuer/issuer.crl");
|
||||
assert_eq!(found, g.issuer_crl_der.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_children_from_fresh_pack_discovers_child_ca() {
|
||||
let g = generate_chain_and_crl();
|
||||
|
||||
let pack = dummy_pack_with_files(vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
g.issuer_crl_der.clone(),
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/issuer/child.cer",
|
||||
g.child_ca_der.clone(),
|
||||
),
|
||||
]);
|
||||
|
||||
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
|
||||
let issuer = CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: g.issuer_ca_der.clone(),
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
|
||||
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
|
||||
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let children = discover_children_from_fresh_pack_with_audit(&issuer, &pack, now)
|
||||
.expect("discover children")
|
||||
.children;
|
||||
assert_eq!(children.len(), 1);
|
||||
assert_eq!(children[0].rsync_base_uri, "rsync://example.test/repo/child/".to_string());
|
||||
assert_eq!(
|
||||
children[0].manifest_rsync_uri,
|
||||
"rsync://example.test/repo/child/child.mft".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
children[0].publication_point_rsync_uri,
|
||||
"rsync://example.test/repo/child/".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
children[0].rrdp_notification_uri.as_deref(),
|
||||
Some("https://example.test/notification.xml")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runner_offline_rsync_fixture_produces_pack_and_warnings() {
|
||||
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0",
|
||||
);
|
||||
assert!(fixture_dir.is_dir(), "fixture directory must exist");
|
||||
|
||||
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
|
||||
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
|
||||
// Pick a validation_time inside the fixture manifest's validity window to keep this
|
||||
// test stable across wall-clock time.
|
||||
let fixture_manifest_bytes =
|
||||
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
|
||||
let fixture_manifest =
|
||||
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
|
||||
.expect("decode manifest fixture");
|
||||
let validation_time = {
|
||||
let this_update = fixture_manifest.manifest.this_update;
|
||||
let next_update = fixture_manifest.manifest.next_update;
|
||||
let candidate = this_update + time::Duration::seconds(60);
|
||||
if candidate < next_update {
|
||||
candidate
|
||||
} else {
|
||||
this_update
|
||||
}
|
||||
};
|
||||
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
let policy = Policy {
|
||||
sync_preference: crate::policy::SyncPreference::RsyncOnly,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
let runner = Rpkiv1PublicationPointRunner {
|
||||
store: &store,
|
||||
policy: &policy,
|
||||
http_fetcher: &NeverHttpFetcher,
|
||||
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
|
||||
validation_time,
|
||||
};
|
||||
|
||||
// For this fixture-driven smoke, we provide the correct issuer CA certificate (the CA for
|
||||
// this publication point) so ROA EE certificate paths can validate.
|
||||
let issuer_ca_der = std::fs::read(
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
),
|
||||
)
|
||||
.expect("read issuer ca fixture");
|
||||
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
|
||||
|
||||
let handle = CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: issuer_ca_der,
|
||||
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
|
||||
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
|
||||
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
|
||||
rsync_base_uri: rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: rsync_base_uri.clone(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
|
||||
let out = runner
|
||||
.run_publication_point(&handle)
|
||||
.expect("run publication point");
|
||||
assert_eq!(out.source, PublicationPointSource::Fresh);
|
||||
assert_eq!(out.pack.manifest_rsync_uri, manifest_rsync_uri);
|
||||
assert!(out.pack.files.len() > 1);
|
||||
assert!(
|
||||
out.objects.vrps.len() > 1,
|
||||
"expected to extract VRPs from ROAs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_publication_point_audit_marks_invalid_crl_as_error_and_overlays_roa_audit() {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let pack = dummy_pack_with_files(vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/issuer/bad.crl",
|
||||
vec![0u8],
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/issuer/x.roa",
|
||||
vec![1u8],
|
||||
),
|
||||
]);
|
||||
|
||||
let pp = crate::validation::manifest::PublicationPointResult {
|
||||
source: crate::validation::manifest::PublicationPointSource::Fresh,
|
||||
pack: pack.clone(),
|
||||
warnings: Vec::new(),
|
||||
};
|
||||
|
||||
let issuer = CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: vec![1],
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: None,
|
||||
effective_as_resources: None,
|
||||
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
|
||||
let objects = crate::validation::objects::ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: crate::validation::objects::ObjectsStats::default(),
|
||||
audit: vec![ObjectAuditEntry {
|
||||
rsync_uri: "rsync://example.test/repo/issuer/x.roa".to_string(),
|
||||
sha256_hex: sha256_hex_from_32(&pack.files[1].sha256),
|
||||
kind: AuditObjectKind::Roa,
|
||||
result: AuditObjectResult::Ok,
|
||||
detail: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let audit = build_publication_point_audit(&issuer, &pp, &[], &objects, &[]);
|
||||
assert_eq!(audit.objects[0].kind, AuditObjectKind::Manifest);
|
||||
|
||||
let crl = audit
|
||||
.objects
|
||||
.iter()
|
||||
.find(|e| e.rsync_uri.ends_with("bad.crl"))
|
||||
.expect("crl entry");
|
||||
assert!(matches!(crl.result, AuditObjectResult::Error));
|
||||
|
||||
let roa = audit
|
||||
.objects
|
||||
.iter()
|
||||
.find(|e| e.rsync_uri.ends_with("x.roa"))
|
||||
.expect("roa entry");
|
||||
assert!(matches!(roa.result, AuditObjectResult::Ok));
|
||||
|
||||
// Smoke that time fields are populated from pack.
|
||||
assert!(audit.verified_at_rfc3339_utc.contains('T'));
|
||||
assert!(audit.this_update_rfc3339_utc.contains('T'));
|
||||
assert!(audit.next_update_rfc3339_utc.contains('T'));
|
||||
let _ = now;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_children_with_audit_records_decode_error_for_corrupt_cer() {
|
||||
let g = generate_chain_and_crl();
|
||||
|
||||
let pack = dummy_pack_with_files(vec![
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/issuer/issuer.crl",
|
||||
g.issuer_crl_der.clone(),
|
||||
),
|
||||
PackFile::from_bytes_compute_sha256(
|
||||
"rsync://example.test/repo/issuer/corrupt.cer",
|
||||
vec![0u8],
|
||||
),
|
||||
]);
|
||||
|
||||
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
|
||||
let issuer = CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: g.issuer_ca_der.clone(),
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
|
||||
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
|
||||
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let out = discover_children_from_fresh_pack_with_audit(&issuer, &pack, now)
|
||||
.expect("discover children");
|
||||
assert!(out.children.is_empty());
|
||||
assert_eq!(out.audits.len(), 1);
|
||||
assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user