2353 lines
79 KiB
Rust
2353 lines
79 KiB
Rust
use crate::data_model::common::BigUnsigned;
|
|
use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
|
|
use crate::data_model::oid::OID_KEY_USAGE_RAW;
|
|
use crate::data_model::rc::{
|
|
AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind,
|
|
ResourceCertificate, ResourceCertificateDecodeError, ResourceCertificateProfileError,
|
|
ResourceCertificateRole,
|
|
};
|
|
use crate::policy::ResourceValidationMode;
|
|
use x509_parser::prelude::{FromDer, X509Certificate};
|
|
|
|
use crate::validation::x509_name::x509_names_equivalent;
|
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
|
|
|
#[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>,
|
|
pub resource_warnings: ResourceValidationWarnings,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct ValidatedSubordinateCaLite {
|
|
pub child_ca: ResourceCertificate,
|
|
pub effective_ip_resources: Option<IpResourceSet>,
|
|
pub effective_as_resources: Option<AsResourceSet>,
|
|
pub resource_warnings: ResourceValidationWarnings,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct ResourceValidationWarnings {
|
|
pub ip_reduced_by_vrs: bool,
|
|
pub as_reduced_by_vrs: bool,
|
|
pub ip_vrs_empty: bool,
|
|
pub as_vrs_empty: bool,
|
|
}
|
|
|
|
impl ResourceValidationWarnings {
|
|
pub fn is_empty(&self) -> bool {
|
|
!self.ip_reduced_by_vrs
|
|
&& !self.as_reduced_by_vrs
|
|
&& !self.ip_vrs_empty
|
|
&& !self.as_vrs_empty
|
|
}
|
|
|
|
pub fn summary(&self) -> String {
|
|
let mut parts = Vec::new();
|
|
if self.ip_reduced_by_vrs {
|
|
parts.push("ip_reduced_by_vrs");
|
|
}
|
|
if self.as_reduced_by_vrs {
|
|
parts.push("as_reduced_by_vrs");
|
|
}
|
|
if self.ip_vrs_empty {
|
|
parts.push("ip_vrs_empty");
|
|
}
|
|
if self.as_vrs_empty {
|
|
parts.push("as_vrs_empty");
|
|
}
|
|
parts.join(",")
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct IssuerEffectiveResourcesIndex {
|
|
parent_ip_by_afi_items:
|
|
Option<BTreeMap<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>>>,
|
|
parent_ip_merged_intervals: HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>>,
|
|
parent_asnum_intervals: Option<Vec<(u32, u32)>>,
|
|
parent_rdi_intervals: Option<Vec<(u32, u32)>>,
|
|
}
|
|
|
|
impl IssuerEffectiveResourcesIndex {
|
|
pub fn from_effective_resources(
|
|
issuer_effective_ip: Option<&IpResourceSet>,
|
|
issuer_effective_as: Option<&AsResourceSet>,
|
|
) -> Result<Self, CaPathError> {
|
|
let parent_ip_by_afi_items = issuer_effective_ip
|
|
.map(ip_resources_by_afi_items)
|
|
.transpose()?;
|
|
|
|
let parent_ip_merged_intervals = issuer_effective_ip
|
|
.map(ip_resources_to_merged_intervals_by_afi)
|
|
.unwrap_or_default();
|
|
|
|
let parent_asnum_intervals = issuer_effective_as
|
|
.and_then(|resources| resources.asnum.as_ref().map(as_choice_to_merged_intervals));
|
|
let parent_rdi_intervals = issuer_effective_as
|
|
.and_then(|resources| resources.rdi.as_ref().map(as_choice_to_merged_intervals));
|
|
|
|
Ok(Self {
|
|
parent_ip_by_afi_items,
|
|
parent_ip_merged_intervals,
|
|
parent_asnum_intervals,
|
|
parent_rdi_intervals,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[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("child CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
|
|
ChildProfile(ResourceCertificateProfileError),
|
|
|
|
#[error("issuer CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
|
|
IssuerProfile(ResourceCertificateProfileError),
|
|
|
|
#[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("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
|
|
IssuerSpkiParse(String),
|
|
|
|
#[error(
|
|
"trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)"
|
|
)]
|
|
IssuerSpkiTrailingBytes(usize),
|
|
|
|
#[error("certificate not valid at validation_time (RFC 5280 §4.1.2.5; RFC 5280 §6.1)")]
|
|
CertificateNotValidAtTime,
|
|
|
|
#[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> {
|
|
validate_subordinate_ca_cert_with_resource_validation_mode(
|
|
child_ca_der,
|
|
issuer_ca_der,
|
|
issuer_crl_der,
|
|
issuer_ca_rsync_uri,
|
|
issuer_crl_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
validation_time,
|
|
ResourceValidationMode::Rfc6487,
|
|
)
|
|
}
|
|
|
|
pub fn validate_subordinate_ca_cert_with_resource_validation_mode(
|
|
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,
|
|
resource_validation_mode: ResourceValidationMode,
|
|
) -> Result<ValidatedSubordinateCa, CaPathError> {
|
|
let child_ca = ResourceCertificate::decode_der(child_ca_der)?;
|
|
if child_ca.kind != ResourceCertKind::Ca {
|
|
return Err(CaPathError::ChildNotCa);
|
|
}
|
|
child_ca
|
|
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
|
.map_err(CaPathError::ChildProfile)?;
|
|
|
|
let issuer_ca =
|
|
ResourceCertificate::decode_der(issuer_ca_der).map_err(CaPathError::IssuerDecode)?;
|
|
if issuer_ca.kind != ResourceCertKind::Ca {
|
|
return Err(CaPathError::IssuerNotCa);
|
|
}
|
|
issuer_ca
|
|
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
|
.map_err(CaPathError::IssuerProfile)?;
|
|
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
|
|
|
|
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
|
return Err(CaPathError::IssuerSubjectMismatch {
|
|
child_issuer_dn: child_ca.tbs.issuer_name.to_string(),
|
|
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
|
|
});
|
|
}
|
|
|
|
validate_child_aki_matches_issuer_ski(&child_ca, &issuer_ca)?;
|
|
if let Some(expected_issuer_uri) = issuer_ca_rsync_uri {
|
|
validate_child_aia_points_to_issuer_uri(&child_ca, expected_issuer_uri)?;
|
|
}
|
|
validate_child_crldp_contains_issuer_crl_uri(&child_ca, issuer_crl_rsync_uri)?;
|
|
|
|
if !time_within_validity(
|
|
validation_time,
|
|
child_ca.tbs.validity_not_before,
|
|
child_ca.tbs.validity_not_after,
|
|
) || !time_within_validity(
|
|
validation_time,
|
|
issuer_ca.tbs.validity_not_before,
|
|
issuer_ca.tbs.validity_not_after,
|
|
) {
|
|
return Err(CaPathError::CertificateNotValidAtTime);
|
|
}
|
|
|
|
let child_x509 = parse_x509_cert(child_ca_der)?;
|
|
verify_child_signature(&child_x509, &issuer_spki)?;
|
|
validate_child_ca_key_usage(&child_x509)?;
|
|
|
|
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 ResourceResolution {
|
|
effective_ip_resources,
|
|
effective_as_resources,
|
|
warnings: resource_warnings,
|
|
} = resolve_child_resources(
|
|
child_ca.tbs.extensions.ip_resources.as_ref(),
|
|
issuer_effective_ip,
|
|
child_ca.tbs.extensions.as_resources.as_ref(),
|
|
issuer_effective_as,
|
|
&IssuerEffectiveResourcesIndex::from_effective_resources(
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
)?,
|
|
resource_validation_mode,
|
|
)?;
|
|
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,
|
|
resource_warnings,
|
|
})
|
|
}
|
|
|
|
/// Validate a subordinate child CA using *pre-decoded issuer CA* and *pre-decoded+verified issuer CRL*.
|
|
///
|
|
/// This avoids repeating issuer CA decode and issuer CRL decode+signature verification for every
|
|
/// child CA certificate discovered in a publication point.
|
|
pub fn validate_subordinate_ca_cert_with_prevalidated_issuer(
|
|
child_ca_der: &[u8],
|
|
child_ca: ResourceCertificate,
|
|
issuer_ca: &ResourceCertificate,
|
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
|
issuer_crl: &RpkixCrl,
|
|
issuer_crl_revoked_serials: &HashSet<Vec<u8>>,
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
issuer_crl_rsync_uri: &str,
|
|
issuer_effective_ip: Option<&IpResourceSet>,
|
|
issuer_effective_as: Option<&AsResourceSet>,
|
|
validation_time: time::OffsetDateTime,
|
|
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
|
|
let issuer_resources_index = IssuerEffectiveResourcesIndex::from_effective_resources(
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
)?;
|
|
validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
|
|
child_ca_der,
|
|
child_ca,
|
|
issuer_ca,
|
|
issuer_spki,
|
|
issuer_crl,
|
|
issuer_crl_revoked_serials,
|
|
issuer_ca_rsync_uri,
|
|
issuer_crl_rsync_uri,
|
|
issuer_effective_ip,
|
|
issuer_effective_as,
|
|
&issuer_resources_index,
|
|
validation_time,
|
|
ResourceValidationMode::Rfc6487,
|
|
)
|
|
}
|
|
|
|
pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
|
|
child_ca_der: &[u8],
|
|
child_ca: ResourceCertificate,
|
|
issuer_ca: &ResourceCertificate,
|
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
|
issuer_crl: &RpkixCrl,
|
|
issuer_crl_revoked_serials: &HashSet<Vec<u8>>,
|
|
issuer_ca_rsync_uri: Option<&str>,
|
|
issuer_crl_rsync_uri: &str,
|
|
issuer_effective_ip: Option<&IpResourceSet>,
|
|
issuer_effective_as: Option<&AsResourceSet>,
|
|
issuer_resources_index: &IssuerEffectiveResourcesIndex,
|
|
validation_time: time::OffsetDateTime,
|
|
resource_validation_mode: ResourceValidationMode,
|
|
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
|
|
if child_ca.kind != ResourceCertKind::Ca {
|
|
return Err(CaPathError::ChildNotCa);
|
|
}
|
|
if issuer_ca.kind != ResourceCertKind::Ca {
|
|
return Err(CaPathError::IssuerNotCa);
|
|
}
|
|
child_ca
|
|
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
|
.map_err(CaPathError::ChildProfile)?;
|
|
issuer_ca
|
|
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
|
|
.map_err(CaPathError::IssuerProfile)?;
|
|
|
|
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
|
return Err(CaPathError::IssuerSubjectMismatch {
|
|
child_issuer_dn: child_ca.tbs.issuer_name.to_string(),
|
|
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
|
|
});
|
|
}
|
|
|
|
validate_child_aki_matches_issuer_ski(&child_ca, issuer_ca)?;
|
|
if let Some(expected_issuer_uri) = issuer_ca_rsync_uri {
|
|
validate_child_aia_points_to_issuer_uri(&child_ca, expected_issuer_uri)?;
|
|
}
|
|
validate_child_crldp_contains_issuer_crl_uri(&child_ca, issuer_crl_rsync_uri)?;
|
|
|
|
if !time_within_validity(
|
|
validation_time,
|
|
child_ca.tbs.validity_not_before,
|
|
child_ca.tbs.validity_not_after,
|
|
) || !time_within_validity(
|
|
validation_time,
|
|
issuer_ca.tbs.validity_not_before,
|
|
issuer_ca.tbs.validity_not_after,
|
|
) {
|
|
return Err(CaPathError::CertificateNotValidAtTime);
|
|
}
|
|
|
|
let child_x509 = parse_x509_cert(child_ca_der)?;
|
|
verify_child_signature(&child_x509, issuer_spki)?;
|
|
validate_child_ca_key_usage(&child_x509)?;
|
|
|
|
if !crl_valid_at_time(issuer_crl, validation_time) {
|
|
return Err(CaPathError::CrlNotValidAtTime);
|
|
}
|
|
|
|
let serial = BigUnsigned::from_biguint(&child_ca.tbs.serial_number);
|
|
if issuer_crl_revoked_serials.contains(&serial.bytes_be) {
|
|
return Err(CaPathError::ChildRevoked);
|
|
}
|
|
|
|
let ResourceResolution {
|
|
effective_ip_resources,
|
|
effective_as_resources,
|
|
warnings: resource_warnings,
|
|
} = resolve_child_resources(
|
|
child_ca.tbs.extensions.ip_resources.as_ref(),
|
|
issuer_effective_ip,
|
|
child_ca.tbs.extensions.as_resources.as_ref(),
|
|
issuer_effective_as,
|
|
issuer_resources_index,
|
|
resource_validation_mode,
|
|
)?;
|
|
if effective_ip_resources.is_none() && effective_as_resources.is_none() {
|
|
return Err(CaPathError::ResourcesMissing);
|
|
}
|
|
|
|
Ok(ValidatedSubordinateCaLite {
|
|
child_ca,
|
|
effective_ip_resources,
|
|
effective_as_resources,
|
|
resource_warnings,
|
|
})
|
|
}
|
|
|
|
fn parse_subject_pki_from_der(der: &[u8]) -> Result<SubjectPublicKeyInfo<'_>, CaPathError> {
|
|
let (rem, spki) = SubjectPublicKeyInfo::from_der(der)
|
|
.map_err(|e| CaPathError::IssuerSpkiParse(e.to_string()))?;
|
|
if !rem.is_empty() {
|
|
return Err(CaPathError::IssuerSpkiTrailingBytes(rem.len()));
|
|
}
|
|
Ok(spki)
|
|
}
|
|
|
|
fn parse_x509_cert(der: &[u8]) -> Result<X509Certificate<'_>, CaPathError> {
|
|
let (rem, cert) = X509Certificate::from_der(der)
|
|
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
|
|
if !rem.is_empty() {
|
|
return Err(CaPathError::ChildSignatureInvalid(
|
|
"trailing bytes after child certificate".to_string(),
|
|
));
|
|
}
|
|
Ok(cert)
|
|
}
|
|
|
|
fn verify_child_signature(
|
|
child: &X509Certificate<'_>,
|
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
|
) -> Result<(), CaPathError> {
|
|
child
|
|
.verify_signature(Some(issuer_spki))
|
|
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
|
}
|
|
|
|
fn validate_child_aki_matches_issuer_ski(
|
|
child: &ResourceCertificate,
|
|
issuer: &ResourceCertificate,
|
|
) -> 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(cert: &X509Certificate<'_>) -> Result<(), CaPathError> {
|
|
let mut ku_critical: Option<bool> = None;
|
|
for ext in cert.extensions() {
|
|
if ext.oid.as_bytes() == OID_KEY_USAGE_RAW {
|
|
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 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)
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct ResourceResolution {
|
|
effective_ip_resources: Option<IpResourceSet>,
|
|
effective_as_resources: Option<AsResourceSet>,
|
|
warnings: ResourceValidationWarnings,
|
|
}
|
|
|
|
fn resolve_child_resources(
|
|
child_ip: Option<&IpResourceSet>,
|
|
issuer_effective_ip: Option<&IpResourceSet>,
|
|
child_as: Option<&AsResourceSet>,
|
|
issuer_effective_as: Option<&AsResourceSet>,
|
|
issuer_resources_index: &IssuerEffectiveResourcesIndex,
|
|
mode: ResourceValidationMode,
|
|
) -> Result<ResourceResolution, CaPathError> {
|
|
match mode {
|
|
ResourceValidationMode::Rfc6487 => Ok(ResourceResolution {
|
|
effective_ip_resources: resolve_child_ip_resources_indexed(
|
|
child_ip,
|
|
issuer_effective_ip,
|
|
issuer_resources_index.parent_ip_by_afi_items.as_ref(),
|
|
&issuer_resources_index.parent_ip_merged_intervals,
|
|
)?,
|
|
effective_as_resources: resolve_child_as_resources_indexed(
|
|
child_as,
|
|
issuer_effective_as,
|
|
issuer_resources_index.parent_asnum_intervals.as_deref(),
|
|
issuer_resources_index.parent_rdi_intervals.as_deref(),
|
|
)?,
|
|
warnings: ResourceValidationWarnings::default(),
|
|
}),
|
|
ResourceValidationMode::ValidationUpdate03 => {
|
|
let (effective_ip_resources, ip_reduced_by_vrs, ip_vrs_empty) =
|
|
resolve_child_ip_resources_vrs(
|
|
child_ip,
|
|
issuer_effective_ip,
|
|
issuer_resources_index.parent_ip_by_afi_items.as_ref(),
|
|
&issuer_resources_index.parent_ip_merged_intervals,
|
|
)?;
|
|
let (effective_as_resources, as_reduced_by_vrs, as_vrs_empty) =
|
|
resolve_child_as_resources_vrs(
|
|
child_as,
|
|
issuer_effective_as,
|
|
issuer_resources_index.parent_asnum_intervals.as_deref(),
|
|
issuer_resources_index.parent_rdi_intervals.as_deref(),
|
|
)?;
|
|
Ok(ResourceResolution {
|
|
effective_ip_resources,
|
|
effective_as_resources,
|
|
warnings: ResourceValidationWarnings {
|
|
ip_reduced_by_vrs,
|
|
as_reduced_by_vrs,
|
|
ip_vrs_empty,
|
|
as_vrs_empty,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn resolve_child_ip_resources(
|
|
child_ip: Option<&IpResourceSet>,
|
|
issuer_effective: Option<&IpResourceSet>,
|
|
) -> Result<Option<IpResourceSet>, CaPathError> {
|
|
let precomputed_parent_by_afi = issuer_effective
|
|
.map(ip_resources_by_afi_items)
|
|
.transpose()?;
|
|
let precomputed_parent_intervals = issuer_effective
|
|
.map(ip_resources_to_merged_intervals_by_afi)
|
|
.unwrap_or_default();
|
|
resolve_child_ip_resources_indexed(
|
|
child_ip,
|
|
issuer_effective,
|
|
precomputed_parent_by_afi.as_ref(),
|
|
&precomputed_parent_intervals,
|
|
)
|
|
}
|
|
|
|
fn resolve_child_ip_resources_indexed(
|
|
child_ip: Option<&IpResourceSet>,
|
|
issuer_effective: Option<&IpResourceSet>,
|
|
parent_by_afi: Option<
|
|
&BTreeMap<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>>,
|
|
>,
|
|
parent_intervals_by_afi: &HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>>,
|
|
) -> 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 = parent_by_afi.ok_or(CaPathError::InheritWithoutParentResources)?;
|
|
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_intervals = parent_intervals_by_afi
|
|
.get(&fam.afi)
|
|
.map(Vec::as_slice)
|
|
.unwrap_or(&[]);
|
|
if !ip_family_items_subset_with_parent_intervals(items, parent_intervals) {
|
|
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_ip_resources_vrs(
|
|
child_ip: Option<&IpResourceSet>,
|
|
issuer_effective: Option<&IpResourceSet>,
|
|
parent_by_afi: Option<
|
|
&BTreeMap<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>>,
|
|
>,
|
|
parent_intervals_by_afi: &HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>>,
|
|
) -> Result<(Option<IpResourceSet>, bool, bool), CaPathError> {
|
|
let Some(child_ip) = child_ip else {
|
|
return Ok((None, false, false));
|
|
};
|
|
if child_ip.has_any_inherit() && issuer_effective.is_none() {
|
|
return Err(CaPathError::InheritWithoutParentResources);
|
|
}
|
|
|
|
let parent_by_afi = parent_by_afi.unwrap_or_else(|| {
|
|
static EMPTY: std::sync::OnceLock<
|
|
BTreeMap<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>>,
|
|
> = std::sync::OnceLock::new();
|
|
EMPTY.get_or_init(BTreeMap::new)
|
|
});
|
|
let mut out_families = Vec::new();
|
|
let mut reduced = false;
|
|
let mut saw_declared = false;
|
|
|
|
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) => {
|
|
saw_declared = saw_declared || !items.is_empty();
|
|
let parent_intervals = parent_intervals_by_afi
|
|
.get(&fam.afi)
|
|
.map(Vec::as_slice)
|
|
.unwrap_or(&[]);
|
|
let intersections =
|
|
intersect_ip_items_with_parent_intervals(items, parent_intervals);
|
|
let child_intervals = ip_items_to_merged_intervals(items);
|
|
if intervals_changed(&child_intervals, &intersections) {
|
|
reduced = true;
|
|
}
|
|
if !intersections.is_empty() {
|
|
out_families.push(crate::data_model::rc::IpAddressFamily {
|
|
afi: fam.afi,
|
|
choice: IpAddressChoice::AddressesOrRanges(ip_intervals_to_ranges(
|
|
fam.afi,
|
|
&intersections,
|
|
)),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let empty = saw_declared && out_families.is_empty();
|
|
Ok((
|
|
Some(IpResourceSet {
|
|
families: out_families,
|
|
}),
|
|
reduced,
|
|
empty,
|
|
))
|
|
}
|
|
|
|
fn resolve_child_as_resources(
|
|
child_as: Option<&AsResourceSet>,
|
|
issuer_effective: Option<&AsResourceSet>,
|
|
) -> Result<Option<AsResourceSet>, CaPathError> {
|
|
let precomputed_asnum = issuer_effective
|
|
.and_then(|resources| resources.asnum.as_ref().map(as_choice_to_merged_intervals));
|
|
let precomputed_rdi = issuer_effective
|
|
.and_then(|resources| resources.rdi.as_ref().map(as_choice_to_merged_intervals));
|
|
resolve_child_as_resources_indexed(
|
|
child_as,
|
|
issuer_effective,
|
|
precomputed_asnum.as_deref(),
|
|
precomputed_rdi.as_deref(),
|
|
)
|
|
}
|
|
|
|
fn resolve_child_as_resources_indexed(
|
|
child_as: Option<&AsResourceSet>,
|
|
issuer_effective: Option<&AsResourceSet>,
|
|
parent_asnum_intervals: Option<&[(u32, u32)]>,
|
|
parent_rdi_intervals: Option<&[(u32, u32)]>,
|
|
) -> 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_with_parent_intervals(
|
|
child_as.asnum.as_ref(),
|
|
parent.asnum.as_ref(),
|
|
parent_asnum_intervals,
|
|
) {
|
|
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_with_parent_intervals(
|
|
child_as.rdi.as_ref(),
|
|
parent.rdi.as_ref(),
|
|
parent_rdi_intervals,
|
|
) {
|
|
return Err(CaPathError::ResourcesNotSubset);
|
|
}
|
|
child_as.rdi.clone()
|
|
}
|
|
};
|
|
|
|
Ok(Some(AsResourceSet { asnum, rdi }))
|
|
}
|
|
|
|
fn resolve_child_as_resources_vrs(
|
|
child_as: Option<&AsResourceSet>,
|
|
issuer_effective: Option<&AsResourceSet>,
|
|
parent_asnum_intervals: Option<&[(u32, u32)]>,
|
|
parent_rdi_intervals: Option<&[(u32, u32)]>,
|
|
) -> Result<(Option<AsResourceSet>, bool, bool), CaPathError> {
|
|
let Some(child_as) = child_as else {
|
|
return Ok((None, false, false));
|
|
};
|
|
if issuer_effective.is_none()
|
|
&& (matches!(child_as.asnum, Some(AsIdentifierChoice::Inherit))
|
|
|| matches!(child_as.rdi, Some(AsIdentifierChoice::Inherit)))
|
|
{
|
|
return Err(CaPathError::InheritWithoutParentResources);
|
|
}
|
|
|
|
let (asnum, asnum_reduced, asnum_empty) = resolve_as_choice_vrs(
|
|
child_as.asnum.as_ref(),
|
|
issuer_effective.and_then(|p| p.asnum.as_ref()),
|
|
parent_asnum_intervals,
|
|
)?;
|
|
let (rdi, rdi_reduced, rdi_empty) = resolve_as_choice_vrs(
|
|
child_as.rdi.as_ref(),
|
|
issuer_effective.and_then(|p| p.rdi.as_ref()),
|
|
parent_rdi_intervals,
|
|
)?;
|
|
Ok((
|
|
Some(AsResourceSet { asnum, rdi }),
|
|
asnum_reduced || rdi_reduced,
|
|
asnum_empty || rdi_empty,
|
|
))
|
|
}
|
|
|
|
fn resolve_as_choice_vrs(
|
|
child: Option<&AsIdentifierChoice>,
|
|
parent: Option<&AsIdentifierChoice>,
|
|
parent_intervals_hint: Option<&[(u32, u32)]>,
|
|
) -> Result<(Option<AsIdentifierChoice>, bool, bool), CaPathError> {
|
|
let Some(child) = child else {
|
|
return Ok((None, false, false));
|
|
};
|
|
match child {
|
|
AsIdentifierChoice::Inherit => {
|
|
let parent = parent
|
|
.cloned()
|
|
.ok_or(CaPathError::InheritWithoutParentResources)?;
|
|
Ok((Some(parent), false, false))
|
|
}
|
|
AsIdentifierChoice::AsIdsOrRanges(_) => {
|
|
let child_intervals = as_choice_to_merged_intervals(child);
|
|
let parent_intervals;
|
|
let parent_intervals = match parent_intervals_hint {
|
|
Some(v) => v,
|
|
None => {
|
|
parent_intervals = parent
|
|
.map(as_choice_to_merged_intervals)
|
|
.unwrap_or_default();
|
|
parent_intervals.as_slice()
|
|
}
|
|
};
|
|
let intersections = intersect_as_intervals(&child_intervals, parent_intervals);
|
|
let reduced = child_intervals != intersections;
|
|
let empty = !child_intervals.is_empty() && intersections.is_empty();
|
|
Ok((
|
|
Some(AsIdentifierChoice::AsIdsOrRanges(as_intervals_to_items(
|
|
&intersections,
|
|
))),
|
|
reduced,
|
|
empty,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn as_choice_subset(
|
|
child: Option<&AsIdentifierChoice>,
|
|
parent: Option<&AsIdentifierChoice>,
|
|
) -> bool {
|
|
as_choice_subset_with_parent_intervals(child, parent, None)
|
|
}
|
|
|
|
fn as_choice_subset_with_parent_intervals(
|
|
child: Option<&AsIdentifierChoice>,
|
|
parent: Option<&AsIdentifierChoice>,
|
|
parent_intervals_hint: Option<&[(u32, u32)]>,
|
|
) -> 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 owned_parent_intervals;
|
|
let parent_intervals = match parent_intervals_hint {
|
|
Some(intervals) => intervals,
|
|
None => {
|
|
owned_parent_intervals = as_choice_to_merged_intervals(parent);
|
|
owned_parent_intervals.as_slice()
|
|
}
|
|
};
|
|
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 m = ip_resources_to_merged_intervals_by_afi(set);
|
|
m.into_iter()
|
|
.map(|(afi, ranges)| {
|
|
(
|
|
match afi {
|
|
crate::data_model::rc::Afi::Ipv4 => AfiKey::V4,
|
|
crate::data_model::rc::Afi::Ipv6 => AfiKey::V6,
|
|
},
|
|
ranges,
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn ip_resources_to_merged_intervals_by_afi(
|
|
set: &IpResourceSet,
|
|
) -> HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>> {
|
|
let mut m: HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>> = HashMap::new();
|
|
|
|
for fam in &set.families {
|
|
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(fam.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 ip_family_items_subset_with_parent_intervals(
|
|
child_items: &[crate::data_model::rc::IpAddressOrRange],
|
|
parent_intervals: &[(Vec<u8>, Vec<u8>)],
|
|
) -> bool {
|
|
if parent_intervals.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
|
|
for (cmin, cmax) in &child_intervals {
|
|
if !interval_is_covered(parent_intervals, cmin, cmax) {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
fn ip_items_to_merged_intervals(
|
|
items: &[crate::data_model::rc::IpAddressOrRange],
|
|
) -> Vec<(Vec<u8>, Vec<u8>)> {
|
|
let mut intervals = Vec::new();
|
|
for item in items {
|
|
match item {
|
|
crate::data_model::rc::IpAddressOrRange::Prefix(p) => {
|
|
intervals.push(prefix_to_range(p))
|
|
}
|
|
crate::data_model::rc::IpAddressOrRange::Range(r) => {
|
|
intervals.push((r.min.clone(), r.max.clone()))
|
|
}
|
|
}
|
|
}
|
|
intervals.sort_by(|(a, _), (b, _)| a.cmp(b));
|
|
merge_ip_intervals(&intervals)
|
|
}
|
|
|
|
fn intersect_ip_items_with_parent_intervals(
|
|
items: &[crate::data_model::rc::IpAddressOrRange],
|
|
parent_intervals: &[(Vec<u8>, Vec<u8>)],
|
|
) -> Vec<(Vec<u8>, Vec<u8>)> {
|
|
let child_intervals = ip_items_to_merged_intervals(items);
|
|
intersect_ip_intervals(&child_intervals, parent_intervals)
|
|
}
|
|
|
|
fn intersect_ip_intervals(
|
|
child_intervals: &[(Vec<u8>, Vec<u8>)],
|
|
parent_intervals: &[(Vec<u8>, Vec<u8>)],
|
|
) -> Vec<(Vec<u8>, Vec<u8>)> {
|
|
let mut out = Vec::new();
|
|
let mut parent_index = 0usize;
|
|
for (child_min, child_max) in child_intervals {
|
|
while parent_index < parent_intervals.len()
|
|
&& parent_intervals[parent_index].1.as_slice() < child_min.as_slice()
|
|
{
|
|
parent_index += 1;
|
|
}
|
|
let mut scan = parent_index;
|
|
while scan < parent_intervals.len()
|
|
&& parent_intervals[scan].0.as_slice() <= child_max.as_slice()
|
|
{
|
|
let (parent_min, parent_max) = &parent_intervals[scan];
|
|
let min = if bytes_leq(child_min, parent_min) {
|
|
parent_min.clone()
|
|
} else {
|
|
child_min.clone()
|
|
};
|
|
let max = if bytes_leq(child_max, parent_max) {
|
|
child_max.clone()
|
|
} else {
|
|
parent_max.clone()
|
|
};
|
|
if bytes_leq(&min, &max) {
|
|
out.push((min, max));
|
|
}
|
|
scan += 1;
|
|
}
|
|
}
|
|
merge_ip_intervals(&out)
|
|
}
|
|
|
|
fn ip_intervals_to_ranges(
|
|
afi: crate::data_model::rc::Afi,
|
|
intervals: &[(Vec<u8>, Vec<u8>)],
|
|
) -> Vec<crate::data_model::rc::IpAddressOrRange> {
|
|
intervals
|
|
.iter()
|
|
.map(|(min, max)| {
|
|
crate::data_model::rc::IpAddressOrRange::Range(crate::data_model::rc::IpAddressRange {
|
|
min: normalize_ip_bytes(afi, min),
|
|
max: normalize_ip_bytes(afi, max),
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn normalize_ip_bytes(afi: crate::data_model::rc::Afi, bytes: &[u8]) -> Vec<u8> {
|
|
let target_len = afi.octets_len();
|
|
if bytes.len() == target_len {
|
|
return bytes.to_vec();
|
|
}
|
|
let mut out = vec![0u8; target_len];
|
|
let copy_len = bytes.len().min(target_len);
|
|
out[..copy_len].copy_from_slice(&bytes[..copy_len]);
|
|
out
|
|
}
|
|
|
|
fn intervals_changed<T: PartialEq>(left: &[T], right: &[T]) -> bool {
|
|
left != right
|
|
}
|
|
|
|
fn intersect_as_intervals(child: &[(u32, u32)], parent: &[(u32, u32)]) -> Vec<(u32, u32)> {
|
|
let mut out = Vec::new();
|
|
let mut parent_index = 0usize;
|
|
for (child_min, child_max) in child {
|
|
while parent_index < parent.len() && parent[parent_index].1 < *child_min {
|
|
parent_index += 1;
|
|
}
|
|
let mut scan = parent_index;
|
|
while scan < parent.len() && parent[scan].0 <= *child_max {
|
|
let min = (*child_min).max(parent[scan].0);
|
|
let max = (*child_max).min(parent[scan].1);
|
|
if min <= max {
|
|
out.push((min, max));
|
|
}
|
|
scan += 1;
|
|
}
|
|
}
|
|
merge_as_intervals(&out)
|
|
}
|
|
|
|
fn as_intervals_to_items(intervals: &[(u32, u32)]) -> Vec<crate::data_model::rc::AsIdOrRange> {
|
|
intervals
|
|
.iter()
|
|
.map(|(min, max)| {
|
|
if min == max {
|
|
crate::data_model::rc::AsIdOrRange::Id(*min)
|
|
} else {
|
|
crate::data_model::rc::AsIdOrRange::Range {
|
|
min: *min,
|
|
max: *max,
|
|
}
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
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::common::X509NameDer;
|
|
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
|
use crate::data_model::rc::{
|
|
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
|
|
IpAddressOrRange, IpPrefix, IpResourceSet,
|
|
};
|
|
use crate::data_model::rc::{
|
|
BasicConstraintsProfile, CertificatePoliciesProfile, RcExtensions, ResourceCertKind,
|
|
ResourceCertificate, RpkixTbsCertificate,
|
|
};
|
|
use der_parser::num_bigint::BigUint;
|
|
use std::process::Command;
|
|
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| s.to_string()).collect::<Vec<_>>());
|
|
let crldp = crldp.map(|v| v.into_iter().map(|s| s.to_string()).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_name: X509NameDer(issuer_dn.as_bytes().to_vec()),
|
|
subject_name: X509NameDer(subject_dn.as_bytes().to_vec()),
|
|
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,
|
|
basic_constraints: (kind == ResourceCertKind::Ca).then_some(
|
|
BasicConstraintsProfile {
|
|
ca: true,
|
|
critical: true,
|
|
path_len_constraint: None,
|
|
},
|
|
),
|
|
subject_key_identifier: ski,
|
|
authority_key_identifier: aki,
|
|
crl_distribution_points_uris: crldp,
|
|
ca_issuers_uris: aia,
|
|
subject_info_access: None,
|
|
certificate_policies_oid: (kind == ResourceCertKind::Ca)
|
|
.then_some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
|
certificate_policies: (kind == ResourceCertKind::Ca).then_some(
|
|
CertificatePoliciesProfile {
|
|
policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(),
|
|
qualifier_oids: Vec::new(),
|
|
},
|
|
),
|
|
extension_oids: Vec::new(),
|
|
ip_resources: None,
|
|
as_resources: None,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
fn openssl_available() -> bool {
|
|
Command::new("openssl")
|
|
.arg("version")
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn write_cert_der_with_addext(dir: &std::path::Path, addext: Option<&str>) -> Vec<u8> {
|
|
assert!(openssl_available(), "openssl is required for this test");
|
|
let key = dir.join("k.pem");
|
|
let cert = dir.join("c.pem");
|
|
let der = dir.join("c.der");
|
|
|
|
let mut cmd = Command::new("openssl");
|
|
cmd.arg("req")
|
|
.arg("-x509")
|
|
.arg("-newkey")
|
|
.arg("rsa:2048")
|
|
.arg("-nodes")
|
|
.arg("-keyout")
|
|
.arg(&key)
|
|
.arg("-subj")
|
|
.arg("/CN=ku")
|
|
.arg("-days")
|
|
.arg("1")
|
|
.arg("-out")
|
|
.arg(&cert);
|
|
if let Some(ext) = addext {
|
|
cmd.arg("-addext").arg(ext);
|
|
}
|
|
let out = cmd.output().expect("openssl req");
|
|
assert!(
|
|
out.status.success(),
|
|
"openssl req failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
let out = Command::new("openssl")
|
|
.arg("x509")
|
|
.arg("-in")
|
|
.arg(&cert)
|
|
.arg("-outform")
|
|
.arg("DER")
|
|
.arg("-out")
|
|
.arg(&der)
|
|
.output()
|
|
.expect("openssl x509");
|
|
assert!(
|
|
out.status.success(),
|
|
"openssl x509 failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
std::fs::read(&der).expect("read der")
|
|
}
|
|
|
|
fn gen_issuer_and_child_der(dir: &std::path::Path) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
|
|
assert!(openssl_available(), "openssl is required for this test");
|
|
let issuer_key = dir.join("issuer.key");
|
|
let issuer_csr = dir.join("issuer.csr");
|
|
let issuer_pem = dir.join("issuer.pem");
|
|
let issuer_der = dir.join("issuer.der");
|
|
|
|
let child_key = dir.join("child.key");
|
|
let child_csr = dir.join("child.csr");
|
|
let child_pem = dir.join("child.pem");
|
|
let child_der = dir.join("child.der");
|
|
|
|
let other_key = dir.join("other.key");
|
|
let other_csr = dir.join("other.csr");
|
|
let other_pem = dir.join("other.pem");
|
|
let other_der = dir.join("other.der");
|
|
|
|
let run = |cmd: &mut Command| {
|
|
let out = cmd.output().expect("run openssl");
|
|
assert!(
|
|
out.status.success(),
|
|
"command failed: {:?}\nstderr={}",
|
|
cmd,
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
};
|
|
|
|
// Issuer self-signed.
|
|
run(Command::new("openssl")
|
|
.args(["genrsa", "-out"])
|
|
.arg(&issuer_key)
|
|
.arg("2048"));
|
|
run(Command::new("openssl")
|
|
.args(["req", "-new", "-key"])
|
|
.arg(&issuer_key)
|
|
.args(["-subj", "/CN=issuer", "-out"])
|
|
.arg(&issuer_csr));
|
|
run(Command::new("openssl")
|
|
.args(["x509", "-req", "-in"])
|
|
.arg(&issuer_csr)
|
|
.args(["-signkey"])
|
|
.arg(&issuer_key)
|
|
.args(["-days", "1", "-out"])
|
|
.arg(&issuer_pem));
|
|
run(Command::new("openssl")
|
|
.args(["x509", "-in"])
|
|
.arg(&issuer_pem)
|
|
.args(["-outform", "DER", "-out"])
|
|
.arg(&issuer_der));
|
|
|
|
// Child signed by issuer.
|
|
run(Command::new("openssl")
|
|
.args(["genrsa", "-out"])
|
|
.arg(&child_key)
|
|
.arg("2048"));
|
|
run(Command::new("openssl")
|
|
.args(["req", "-new", "-key"])
|
|
.arg(&child_key)
|
|
.args(["-subj", "/CN=child", "-out"])
|
|
.arg(&child_csr));
|
|
run(Command::new("openssl")
|
|
.args(["x509", "-req", "-in"])
|
|
.arg(&child_csr)
|
|
.args(["-CA"])
|
|
.arg(&issuer_pem)
|
|
.args(["-CAkey"])
|
|
.arg(&issuer_key)
|
|
.args(["-CAcreateserial", "-days", "1", "-out"])
|
|
.arg(&child_pem));
|
|
run(Command::new("openssl")
|
|
.args(["x509", "-in"])
|
|
.arg(&child_pem)
|
|
.args(["-outform", "DER", "-out"])
|
|
.arg(&child_der));
|
|
|
|
// Other self-signed issuer.
|
|
run(Command::new("openssl")
|
|
.args(["genrsa", "-out"])
|
|
.arg(&other_key)
|
|
.arg("2048"));
|
|
run(Command::new("openssl")
|
|
.args(["req", "-new", "-key"])
|
|
.arg(&other_key)
|
|
.args(["-subj", "/CN=other", "-out"])
|
|
.arg(&other_csr));
|
|
run(Command::new("openssl")
|
|
.args(["x509", "-req", "-in"])
|
|
.arg(&other_csr)
|
|
.args(["-signkey"])
|
|
.arg(&other_key)
|
|
.args(["-days", "1", "-out"])
|
|
.arg(&other_pem));
|
|
run(Command::new("openssl")
|
|
.args(["x509", "-in"])
|
|
.arg(&other_pem)
|
|
.args(["-outform", "DER", "-out"])
|
|
.arg(&other_der));
|
|
|
|
(
|
|
std::fs::read(&issuer_der).expect("read issuer der"),
|
|
std::fs::read(&child_der).expect("read child der"),
|
|
std::fs::read(&other_der).expect("read other der"),
|
|
)
|
|
}
|
|
|
|
#[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 validation_update_03_intersects_overclaiming_child_ip_resources() {
|
|
let issuer = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 24,
|
|
addr: vec![10, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let child = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 23,
|
|
addr: vec![10, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let index = IssuerEffectiveResourcesIndex::from_effective_resources(Some(&issuer), None)
|
|
.expect("index");
|
|
let resolved = resolve_child_resources(
|
|
Some(&child),
|
|
Some(&issuer),
|
|
None,
|
|
None,
|
|
&index,
|
|
ResourceValidationMode::ValidationUpdate03,
|
|
)
|
|
.expect("vrs resolution");
|
|
assert!(resolved.warnings.ip_reduced_by_vrs);
|
|
assert!(!resolved.warnings.ip_vrs_empty);
|
|
let effective = resolved.effective_ip_resources.expect("effective ip");
|
|
assert!(effective.families[0].contains_prefix(&IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 24,
|
|
addr: vec![10, 0, 0, 0],
|
|
}));
|
|
assert!(!effective.families[0].contains_prefix(&IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 24,
|
|
addr: vec![10, 0, 1, 0],
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn validation_update_03_keeps_empty_vrs_instead_of_rejecting_child_ca() {
|
|
let issuer = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 24,
|
|
addr: vec![10, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let child = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 24,
|
|
addr: vec![192, 0, 2, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let index = IssuerEffectiveResourcesIndex::from_effective_resources(Some(&issuer), None)
|
|
.expect("index");
|
|
let strict_err = resolve_child_resources(
|
|
Some(&child),
|
|
Some(&issuer),
|
|
None,
|
|
None,
|
|
&index,
|
|
ResourceValidationMode::Rfc6487,
|
|
)
|
|
.unwrap_err();
|
|
assert!(matches!(strict_err, CaPathError::ResourcesNotSubset));
|
|
|
|
let resolved = resolve_child_resources(
|
|
Some(&child),
|
|
Some(&issuer),
|
|
None,
|
|
None,
|
|
&index,
|
|
ResourceValidationMode::ValidationUpdate03,
|
|
)
|
|
.expect("vrs resolution");
|
|
assert!(resolved.warnings.ip_reduced_by_vrs);
|
|
assert!(resolved.warnings.ip_vrs_empty);
|
|
assert!(
|
|
resolved
|
|
.effective_ip_resources
|
|
.expect("effective ip")
|
|
.families
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn validation_update_03_intersects_overclaiming_child_as_resources() {
|
|
let issuer = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range {
|
|
min: 64500,
|
|
max: 64510,
|
|
},
|
|
])),
|
|
rdi: None,
|
|
};
|
|
let child = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range {
|
|
min: 64505,
|
|
max: 64520,
|
|
},
|
|
])),
|
|
rdi: None,
|
|
};
|
|
let index = IssuerEffectiveResourcesIndex::from_effective_resources(None, Some(&issuer))
|
|
.expect("index");
|
|
let resolved = resolve_child_resources(
|
|
None,
|
|
None,
|
|
Some(&child),
|
|
Some(&issuer),
|
|
&index,
|
|
ResourceValidationMode::ValidationUpdate03,
|
|
)
|
|
.expect("vrs resolution");
|
|
assert!(resolved.warnings.as_reduced_by_vrs);
|
|
assert!(!resolved.warnings.as_vrs_empty);
|
|
let effective = resolved.effective_as_resources.expect("effective as");
|
|
assert_eq!(
|
|
effective.asnum,
|
|
Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range {
|
|
min: 64505,
|
|
max: 64510,
|
|
},
|
|
]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn validation_update_03_records_all_resource_warning_summary_parts() {
|
|
let issuer_ip = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 24,
|
|
addr: vec![10, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let child_ip = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 24,
|
|
addr: vec![192, 0, 2, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let issuer_as = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range {
|
|
min: 64500,
|
|
max: 64510,
|
|
},
|
|
])),
|
|
rdi: None,
|
|
};
|
|
let child_as = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range {
|
|
min: 64520,
|
|
max: 64530,
|
|
},
|
|
])),
|
|
rdi: None,
|
|
};
|
|
let index = IssuerEffectiveResourcesIndex::from_effective_resources(
|
|
Some(&issuer_ip),
|
|
Some(&issuer_as),
|
|
)
|
|
.expect("index");
|
|
|
|
let resolved = resolve_child_resources(
|
|
Some(&child_ip),
|
|
Some(&issuer_ip),
|
|
Some(&child_as),
|
|
Some(&issuer_as),
|
|
&index,
|
|
ResourceValidationMode::ValidationUpdate03,
|
|
)
|
|
.expect("vrs resolution");
|
|
|
|
assert!(resolved.warnings.ip_reduced_by_vrs);
|
|
assert!(resolved.warnings.ip_vrs_empty);
|
|
assert!(resolved.warnings.as_reduced_by_vrs);
|
|
assert!(resolved.warnings.as_vrs_empty);
|
|
assert_eq!(
|
|
resolved.warnings.summary(),
|
|
"ip_reduced_by_vrs,as_reduced_by_vrs,ip_vrs_empty,as_vrs_empty"
|
|
);
|
|
assert!(!resolved.warnings.is_empty());
|
|
}
|
|
|
|
#[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");
|
|
}
|
|
|
|
#[test]
|
|
fn validate_child_ca_key_usage_accepts_only_keycertsign_and_crlsign_critical() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let der = write_cert_der_with_addext(
|
|
td.path(),
|
|
Some("keyUsage = critical, keyCertSign, cRLSign"),
|
|
);
|
|
let cert = parse_x509_cert(&der).expect("x509 parse ok");
|
|
validate_child_ca_key_usage(&cert).expect("key usage ok");
|
|
}
|
|
|
|
#[test]
|
|
fn validate_child_ca_key_usage_rejects_missing_noncritical_and_invalid_bits() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let missing = write_cert_der_with_addext(td.path(), None);
|
|
let cert = parse_x509_cert(&missing).expect("x509 parse ok");
|
|
let err = validate_child_ca_key_usage(&cert).unwrap_err();
|
|
assert!(matches!(err, CaPathError::KeyUsageMissing), "{err}");
|
|
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let noncritical =
|
|
write_cert_der_with_addext(td.path(), Some("keyUsage = keyCertSign, cRLSign"));
|
|
let cert = parse_x509_cert(&noncritical).expect("x509 parse ok");
|
|
let err = validate_child_ca_key_usage(&cert).unwrap_err();
|
|
assert!(matches!(err, CaPathError::KeyUsageNotCritical), "{err}");
|
|
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let invalid = write_cert_der_with_addext(
|
|
td.path(),
|
|
Some("keyUsage = critical, keyCertSign, cRLSign, digitalSignature"),
|
|
);
|
|
let cert = parse_x509_cert(&invalid).expect("x509 parse ok");
|
|
let err = validate_child_ca_key_usage(&cert).unwrap_err();
|
|
assert!(matches!(err, CaPathError::KeyUsageInvalidBits), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn verify_cert_signature_with_issuer_accepts_valid_chain_and_rejects_wrong_issuer() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let (issuer, child, other) = gen_issuer_and_child_der(td.path());
|
|
let issuer_cert = parse_x509_cert(&issuer).expect("x509 parse issuer");
|
|
let child_cert = parse_x509_cert(&child).expect("x509 parse child");
|
|
verify_child_signature(&child_cert, &issuer_cert.tbs_certificate.subject_pki)
|
|
.expect("signature ok");
|
|
let other_cert = parse_x509_cert(&other).expect("x509 parse other");
|
|
let err = verify_child_signature(&child_cert, &other_cert.tbs_certificate.subject_pki)
|
|
.unwrap_err();
|
|
assert!(
|
|
matches!(err, CaPathError::ChildSignatureInvalid(_)),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn issuer_effective_resources_index_and_indexed_resolvers_cover_success_and_failure_paths() {
|
|
use crate::data_model::rc::{AsIdOrRange, IpPrefix};
|
|
|
|
let parent_ip = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 8,
|
|
addr: vec![10, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let parent_as = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range {
|
|
min: 64500,
|
|
max: 64599,
|
|
},
|
|
])),
|
|
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
|
65000,
|
|
)])),
|
|
};
|
|
let idx = IssuerEffectiveResourcesIndex::from_effective_resources(
|
|
Some(&parent_ip),
|
|
Some(&parent_as),
|
|
)
|
|
.expect("index builds");
|
|
assert_eq!(
|
|
idx.parent_ip_by_afi_items.as_ref().map(|v| v.len()),
|
|
Some(1)
|
|
);
|
|
assert_eq!(idx.parent_ip_merged_intervals.len(), 1);
|
|
assert_eq!(
|
|
idx.parent_asnum_intervals.as_ref().map(|v| v.len()),
|
|
Some(1)
|
|
);
|
|
assert_eq!(idx.parent_rdi_intervals.as_ref().map(|v| v.len()), Some(1));
|
|
|
|
let child_ip_subset = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 16,
|
|
addr: vec![10, 1, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
assert!(
|
|
resolve_child_ip_resources_indexed(
|
|
Some(&child_ip_subset),
|
|
Some(&parent_ip),
|
|
idx.parent_ip_by_afi_items.as_ref(),
|
|
&idx.parent_ip_merged_intervals,
|
|
)
|
|
.expect("subset should resolve")
|
|
.is_some()
|
|
);
|
|
|
|
let child_ip_bad = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 16,
|
|
addr: vec![11, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let err = resolve_child_ip_resources_indexed(
|
|
Some(&child_ip_bad),
|
|
Some(&parent_ip),
|
|
idx.parent_ip_by_afi_items.as_ref(),
|
|
&idx.parent_ip_merged_intervals,
|
|
)
|
|
.unwrap_err();
|
|
assert!(matches!(err, CaPathError::ResourcesNotSubset));
|
|
|
|
let child_as_subset = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
|
64542,
|
|
)])),
|
|
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
|
65000,
|
|
)])),
|
|
};
|
|
assert!(
|
|
resolve_child_as_resources_indexed(
|
|
Some(&child_as_subset),
|
|
Some(&parent_as),
|
|
idx.parent_asnum_intervals.as_deref(),
|
|
idx.parent_rdi_intervals.as_deref(),
|
|
)
|
|
.expect("subset as resolves")
|
|
.is_some()
|
|
);
|
|
|
|
let child_as_bad = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
|
65123,
|
|
)])),
|
|
rdi: None,
|
|
};
|
|
let err = resolve_child_as_resources_indexed(
|
|
Some(&child_as_bad),
|
|
Some(&parent_as),
|
|
idx.parent_asnum_intervals.as_deref(),
|
|
idx.parent_rdi_intervals.as_deref(),
|
|
)
|
|
.unwrap_err();
|
|
assert!(matches!(err, CaPathError::ResourcesNotSubset));
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_child_ip_and_as_resources_success_paths() {
|
|
use crate::data_model::rc::{AsIdOrRange, IpAddressOrRange, IpPrefix};
|
|
|
|
let parent_ip = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 8,
|
|
addr: vec![10, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
|
|
let child_ip_inherit = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::Inherit,
|
|
}],
|
|
};
|
|
let eff = resolve_child_ip_resources(Some(&child_ip_inherit), Some(&parent_ip))
|
|
.expect("inherit resolves")
|
|
.expect("some ip");
|
|
assert_eq!(eff.families.len(), 1);
|
|
assert!(matches!(
|
|
eff.families[0].choice,
|
|
IpAddressChoice::AddressesOrRanges(_)
|
|
));
|
|
|
|
let child_ip_subset = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 16,
|
|
addr: vec![10, 1, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
resolve_child_ip_resources(Some(&child_ip_subset), Some(&parent_ip))
|
|
.expect("subset ok")
|
|
.expect("some");
|
|
|
|
let child_ip_bad = IpResourceSet {
|
|
families: vec![IpAddressFamily {
|
|
afi: Afi::Ipv4,
|
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
|
IpPrefix {
|
|
afi: Afi::Ipv4,
|
|
prefix_len: 16,
|
|
addr: vec![11, 0, 0, 0],
|
|
},
|
|
)]),
|
|
}],
|
|
};
|
|
let err = resolve_child_ip_resources(Some(&child_ip_bad), Some(&parent_ip)).unwrap_err();
|
|
assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}");
|
|
|
|
let parent_as = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
|
AsIdOrRange::Range { min: 1, max: 100 },
|
|
])),
|
|
rdi: None,
|
|
};
|
|
let child_as_inherit = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::Inherit),
|
|
rdi: None,
|
|
};
|
|
let eff_as = resolve_child_as_resources(Some(&child_as_inherit), Some(&parent_as))
|
|
.expect("inherit as")
|
|
.expect("some");
|
|
assert_eq!(eff_as.asnum, parent_as.asnum);
|
|
|
|
let child_as_subset = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(50)])),
|
|
rdi: None,
|
|
};
|
|
resolve_child_as_resources(Some(&child_as_subset), Some(&parent_as))
|
|
.expect("subset as")
|
|
.expect("some");
|
|
|
|
let child_as_bad = AsResourceSet {
|
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
|
200,
|
|
)])),
|
|
rdi: None,
|
|
};
|
|
let err = resolve_child_as_resources(Some(&child_as_bad), Some(&parent_as)).unwrap_err();
|
|
assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}");
|
|
}
|
|
}
|
|
|
|
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()]
|
|
}
|