串行验证通过
This commit is contained in:
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()]
|
||||
}
|
||||
Reference in New Issue
Block a user