1436 lines
49 KiB
Rust
1436 lines
49 KiB
Rust
use der_parser::ber::{BerObjectContent, Class};
|
|
use der_parser::der::{DerObject, Tag, parse_der};
|
|
use der_parser::num_bigint::BigUint;
|
|
use url::Url;
|
|
use x509_parser::asn1_rs::{Class as Asn1Class, Tag as Asn1Tag};
|
|
use x509_parser::extensions::ParsedExtension;
|
|
use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version};
|
|
|
|
use crate::data_model::common::{
|
|
Asn1TimeUtc, InvalidTimeEncodingError, UtcTime, asn1_time_to_model,
|
|
};
|
|
use crate::data_model::oid::{
|
|
OID_AD_CA_ISSUERS, OID_AD_SIGNED_OBJECT, OID_AUTHORITY_INFO_ACCESS,
|
|
OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTONOMOUS_SYS_IDS, OID_CRL_DISTRIBUTION_POINTS,
|
|
OID_CP_IPADDR_ASNUMBER, OID_IP_ADDR_BLOCKS, OID_SHA256_WITH_RSA_ENCRYPTION,
|
|
OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER,
|
|
};
|
|
|
|
/// Resource Certificate kind (semantic classification).
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum ResourceCertKind {
|
|
Ca,
|
|
Ee,
|
|
}
|
|
|
|
/// A parsed RPKI Resource Certificate (RFC 6487) data model.
|
|
///
|
|
/// This module intentionally focuses on the semantics needed by Signed Object validation and
|
|
/// object-specific EE certificate checks (MFT/ROA/ASPA), as described in
|
|
/// `rpki/specs/03_resource_certificate_rc.md`.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct ResourceCertificate {
|
|
pub raw_der: Vec<u8>,
|
|
pub tbs: RpkixTbsCertificate,
|
|
pub kind: ResourceCertKind,
|
|
}
|
|
|
|
pub type ResourceCaCertificate = ResourceCertificate;
|
|
pub type ResourceEeCertificate = ResourceCertificate;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RpkixTbsCertificate {
|
|
pub version: u32,
|
|
pub serial_number: BigUint,
|
|
pub signature_algorithm: String,
|
|
pub issuer_dn: String,
|
|
pub subject_dn: String,
|
|
pub validity_not_before: UtcTime,
|
|
pub validity_not_after: UtcTime,
|
|
/// DER encoding of SubjectPublicKeyInfo.
|
|
pub subject_public_key_info: Vec<u8>,
|
|
pub extensions: RcExtensions,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RcExtensions {
|
|
pub basic_constraints_ca: bool,
|
|
pub subject_key_identifier: Option<Vec<u8>>,
|
|
/// Authority Key Identifier (AKI) keyIdentifier value.
|
|
pub authority_key_identifier: Option<Vec<u8>>,
|
|
/// CRL Distribution Points URIs (fullName).
|
|
pub crl_distribution_points_uris: Option<Vec<Url>>,
|
|
/// Authority Information Access (AIA) caIssuers URIs.
|
|
pub ca_issuers_uris: Option<Vec<Url>>,
|
|
pub subject_info_access: Option<SubjectInfoAccess>,
|
|
pub certificate_policies_oid: Option<String>,
|
|
|
|
pub ip_resources: Option<IpResourceSet>,
|
|
pub as_resources: Option<AsResourceSet>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct ResourceCertificateParsed {
|
|
pub raw_der: Vec<u8>,
|
|
pub version: X509Version,
|
|
pub serial_number: BigUint,
|
|
pub signature_algorithm: AlgorithmIdentifierValue,
|
|
pub tbs_signature_algorithm: AlgorithmIdentifierValue,
|
|
pub issuer_dn: String,
|
|
pub subject_dn: String,
|
|
pub validity_not_before: Asn1TimeUtc,
|
|
pub validity_not_after: Asn1TimeUtc,
|
|
/// DER encoding of SubjectPublicKeyInfo.
|
|
pub subject_public_key_info: Vec<u8>,
|
|
pub extensions: RcExtensionsParsed,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct AlgorithmIdentifierValue {
|
|
pub oid: String,
|
|
pub parameters: Option<AlgorithmParametersValue>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct AlgorithmParametersValue {
|
|
pub class: Asn1Class,
|
|
pub tag: Asn1Tag,
|
|
pub data: Vec<u8>,
|
|
}
|
|
|
|
impl AlgorithmIdentifierValue {
|
|
pub fn params_absent_or_null(&self) -> bool {
|
|
match &self.parameters {
|
|
None => true,
|
|
Some(p) if p.class == Asn1Class::Universal && p.tag == Asn1Tag::Null => true,
|
|
Some(_p) => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RcExtensionsParsed {
|
|
pub basic_constraints_ca: Vec<bool>,
|
|
pub subject_key_identifier: Vec<(Vec<u8>, bool)>,
|
|
pub authority_key_identifier: Vec<(AuthorityKeyIdentifierParsed, bool)>,
|
|
pub crl_distribution_points: Vec<(CrlDistributionPointsParsed, bool)>,
|
|
pub authority_info_access: Vec<(AuthorityInfoAccessParsed, bool)>,
|
|
pub subject_info_access: Vec<(SubjectInfoAccessParsed, bool)>,
|
|
pub certificate_policies: Vec<(Vec<String>, bool)>,
|
|
pub ip_resources: Vec<(IpResourceSet, bool)>,
|
|
pub as_resources: Vec<(AsResourceSet, bool)>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct AuthorityKeyIdentifierParsed {
|
|
pub key_identifier: Option<Vec<u8>>,
|
|
pub has_authority_cert_issuer: bool,
|
|
pub has_authority_cert_serial: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct AuthorityInfoAccessParsed {
|
|
pub ca_issuers_uris: Vec<Url>,
|
|
pub ca_issuers_access_location_not_uri: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct CrlDistributionPointsParsed {
|
|
pub distribution_points: Vec<CrlDistributionPointParsed>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct CrlDistributionPointParsed {
|
|
pub distribution_point_present: bool,
|
|
pub reasons_present: bool,
|
|
pub crl_issuer_present: bool,
|
|
pub name_relative_to_crl_issuer_present: bool,
|
|
pub full_name_uris: Vec<Url>,
|
|
pub full_name_not_uri: bool,
|
|
pub full_name_present: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SubjectInfoAccessParsed {
|
|
pub access_descriptions: Vec<AccessDescription>,
|
|
pub signed_object_uris: Vec<Url>,
|
|
pub signed_object_access_location_not_uri: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum SubjectInfoAccess {
|
|
Ca(SubjectInfoAccessCa),
|
|
Ee(SubjectInfoAccessEe),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SubjectInfoAccessCa {
|
|
pub access_descriptions: Vec<AccessDescription>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SubjectInfoAccessEe {
|
|
pub signed_object_uris: Vec<Url>,
|
|
/// The full list of access descriptions as carried in the SIA extension.
|
|
pub access_descriptions: Vec<AccessDescription>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct AccessDescription {
|
|
pub access_method_oid: String,
|
|
pub access_location: Url,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
pub enum Afi {
|
|
Ipv4,
|
|
Ipv6,
|
|
}
|
|
|
|
impl Afi {
|
|
pub fn ub(self) -> u16 {
|
|
match self {
|
|
Afi::Ipv4 => 32,
|
|
Afi::Ipv6 => 128,
|
|
}
|
|
}
|
|
|
|
pub fn octets_len(self) -> usize {
|
|
match self {
|
|
Afi::Ipv4 => 4,
|
|
Afi::Ipv6 => 16,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct IpResourceSet {
|
|
pub families: Vec<IpAddressFamily>,
|
|
}
|
|
|
|
impl IpResourceSet {
|
|
/// Decode the DER bytes carried inside the X.509 `extnValue` OCTET STRING for
|
|
/// `id-pe-ipAddrBlocks` (RFC 3779 / RFC 6487).
|
|
pub fn decode_extn_value(extn_value: &[u8]) -> Result<Self, IpResourceSetDecodeError> {
|
|
parse_ip_addr_blocks(extn_value).map_err(|_| IpResourceSetDecodeError::InvalidEncoding)
|
|
}
|
|
|
|
pub fn is_all_inherit(&self) -> bool {
|
|
self.families
|
|
.iter()
|
|
.all(|f| matches!(f.choice, IpAddressChoice::Inherit))
|
|
}
|
|
|
|
pub fn has_any_inherit(&self) -> bool {
|
|
self.families
|
|
.iter()
|
|
.any(|f| matches!(f.choice, IpAddressChoice::Inherit))
|
|
}
|
|
|
|
pub fn contains_prefix(&self, prefix: &IpPrefix) -> bool {
|
|
self.families.iter().any(|fam| fam.contains_prefix(prefix))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum IpResourceSetDecodeError {
|
|
#[error("invalid ipAddrBlocks encoding (RFC 3779 §2.2.3; RFC 6487 §4.8.10)")]
|
|
InvalidEncoding,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct IpAddressFamily {
|
|
pub afi: Afi,
|
|
pub choice: IpAddressChoice,
|
|
}
|
|
|
|
impl IpAddressFamily {
|
|
pub fn contains_prefix(&self, prefix: &IpPrefix) -> bool {
|
|
if self.afi != prefix.afi {
|
|
return false;
|
|
}
|
|
match &self.choice {
|
|
IpAddressChoice::Inherit => true,
|
|
IpAddressChoice::AddressesOrRanges(items) => items.iter().any(|item| match item {
|
|
IpAddressOrRange::Prefix(p) => prefix_covers(p, prefix),
|
|
IpAddressOrRange::Range(r) => range_covers_prefix(self.afi, r, prefix),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum IpAddressChoice {
|
|
Inherit,
|
|
AddressesOrRanges(Vec<IpAddressOrRange>),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum IpAddressOrRange {
|
|
Prefix(IpPrefix),
|
|
Range(IpAddressRange),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct IpAddressRange {
|
|
pub min: Vec<u8>,
|
|
pub max: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct IpPrefix {
|
|
pub afi: Afi,
|
|
pub prefix_len: u16,
|
|
pub addr: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct AsResourceSet {
|
|
pub asnum: Option<AsIdentifierChoice>,
|
|
pub rdi: Option<AsIdentifierChoice>,
|
|
}
|
|
|
|
impl AsResourceSet {
|
|
/// Decode the DER bytes carried inside the X.509 `extnValue` OCTET STRING for
|
|
/// `id-pe-autonomousSysIds` (RFC 3779 / RFC 6487).
|
|
pub fn decode_extn_value(extn_value: &[u8]) -> Result<Self, AsResourceSetDecodeError> {
|
|
parse_as_identifiers(extn_value).map_err(|_| AsResourceSetDecodeError::InvalidEncoding)
|
|
}
|
|
|
|
pub fn is_asnum_inherit(&self) -> bool {
|
|
matches!(self.asnum, Some(AsIdentifierChoice::Inherit))
|
|
}
|
|
|
|
pub fn has_any_range(&self) -> bool {
|
|
self.asnum.as_ref().map(|c| c.has_range()).unwrap_or(false)
|
|
|| self.rdi.as_ref().map(|c| c.has_range()).unwrap_or(false)
|
|
}
|
|
|
|
pub fn asnum_single_id(&self) -> Option<u32> {
|
|
match self.asnum.as_ref()? {
|
|
AsIdentifierChoice::Inherit => None,
|
|
AsIdentifierChoice::AsIdsOrRanges(items) => {
|
|
if items.len() != 1 {
|
|
return None;
|
|
}
|
|
match &items[0] {
|
|
AsIdOrRange::Id(v) => Some(*v),
|
|
AsIdOrRange::Range { .. } => None,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum AsResourceSetDecodeError {
|
|
#[error("invalid autonomousSysIds encoding (RFC 3779 §3.2.3; RFC 6487 §4.8.11)")]
|
|
InvalidEncoding,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum AsIdentifierChoice {
|
|
Inherit,
|
|
AsIdsOrRanges(Vec<AsIdOrRange>),
|
|
}
|
|
|
|
impl AsIdentifierChoice {
|
|
pub fn has_range(&self) -> bool {
|
|
match self {
|
|
AsIdentifierChoice::Inherit => false,
|
|
AsIdentifierChoice::AsIdsOrRanges(items) => {
|
|
items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. }))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum AsIdOrRange {
|
|
Id(u32),
|
|
Range { min: u32, max: u32 },
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ResourceCertificateParseError {
|
|
#[error("X.509 parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
|
|
Parse(String),
|
|
|
|
#[error("trailing bytes after certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
|
|
TrailingBytes(usize),
|
|
|
|
#[error("invalid RFC 3779 IP resources extension encoding (RFC 6487 §4.8.10; RFC 3779 §2.2)")]
|
|
InvalidIpResourcesEncoding,
|
|
|
|
#[error("invalid RFC 3779 AS resources extension encoding (RFC 6487 §4.8.11; RFC 3779 §3.2)")]
|
|
InvalidAsResourcesEncoding,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ResourceCertificateProfileError {
|
|
#[error("{0}")]
|
|
InvalidTimeEncoding(#[from] InvalidTimeEncodingError),
|
|
|
|
#[error("certificate version must be v3 (RFC 5280 §4.1; RFC 6487 §4)")]
|
|
InvalidVersion,
|
|
|
|
#[error("signatureAlgorithm does not match tbsCertificate.signature (RFC 5280 §4.1)")]
|
|
SignatureAlgorithmMismatch,
|
|
|
|
#[error(
|
|
"unsupported signature algorithm (expected sha256WithRSAEncryption {OID_SHA256_WITH_RSA_ENCRYPTION}) (RFC 7935 §2; RFC 6487 §4)"
|
|
)]
|
|
UnsupportedSignatureAlgorithm,
|
|
|
|
#[error("invalid signature algorithm parameters (RFC 5280 §4.1.1.2)")]
|
|
InvalidSignatureAlgorithmParameters,
|
|
|
|
#[error("duplicate extension: {0} (RFC 5280 §4.2; RFC 6487 §4.8)")]
|
|
DuplicateExtension(&'static str),
|
|
|
|
#[error("SubjectKeyIdentifier criticality must be non-critical (RFC 6487 §4.8.2)")]
|
|
SkiCriticality,
|
|
|
|
#[error("SubjectInfoAccess criticality must be non-critical (RFC 6487 §4.8.8)")]
|
|
SiaCriticality,
|
|
|
|
#[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")]
|
|
CertificatePoliciesCriticality,
|
|
|
|
#[error(
|
|
"certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)"
|
|
)]
|
|
InvalidCertificatePolicy(String),
|
|
|
|
#[error(
|
|
"SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)"
|
|
)]
|
|
SignedObjectSiaNotUri,
|
|
|
|
#[error("SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")]
|
|
SignedObjectSiaNoRsync,
|
|
|
|
#[error("ipAddrBlocks criticality must be critical when present (RFC 6487 §4.8.10)")]
|
|
IpResourcesCriticality,
|
|
|
|
#[error("autonomousSysIds criticality must be critical when present (RFC 6487 §4.8.11)")]
|
|
AsResourcesCriticality,
|
|
|
|
#[error(
|
|
"authorityKeyIdentifier must be present in non-self-signed certificates (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)"
|
|
)]
|
|
AkiMissing,
|
|
|
|
#[error(
|
|
"authorityKeyIdentifier criticality must be non-critical (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)"
|
|
)]
|
|
AkiCriticality,
|
|
|
|
#[error(
|
|
"authorityKeyIdentifier authorityCertIssuer MUST NOT be present (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)"
|
|
)]
|
|
AkiAuthorityCertIssuerPresent,
|
|
|
|
#[error(
|
|
"authorityKeyIdentifier authorityCertSerialNumber MUST NOT be present (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)"
|
|
)]
|
|
AkiAuthorityCertSerialPresent,
|
|
|
|
#[error(
|
|
"self-signed certificate authorityKeyIdentifier must equal subjectKeyIdentifier when present (RFC 6487 §4.8.3)"
|
|
)]
|
|
AkiSelfSignedNotEqualSki,
|
|
|
|
#[error(
|
|
"CRLDistributionPoints must be present in non-self-signed certificates (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)"
|
|
)]
|
|
CrlDistributionPointsMissing,
|
|
|
|
#[error(
|
|
"CRLDistributionPoints criticality must be non-critical (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)"
|
|
)]
|
|
CrlDistributionPointsCriticality,
|
|
|
|
#[error(
|
|
"CRLDistributionPoints MUST be omitted in self-signed certificates (RFC 6487 §4.8.6)"
|
|
)]
|
|
CrlDistributionPointsSelfSignedMustOmit,
|
|
|
|
#[error(
|
|
"CRLDistributionPoints must contain exactly one DistributionPoint (RFC 6487 §4.8.6)"
|
|
)]
|
|
CrlDistributionPointsNotSingle,
|
|
|
|
#[error(
|
|
"CRLDistributionPoints distributionPoint field MUST be present (RFC 6487 §4.8.6)"
|
|
)]
|
|
CrlDistributionPointsNoDistributionPoint,
|
|
|
|
#[error("CRLDistributionPoints reasons field MUST be omitted (RFC 6487 §4.8.6)")]
|
|
CrlDistributionPointsHasReasons,
|
|
|
|
#[error("CRLDistributionPoints cRLIssuer field MUST be omitted (RFC 6487 §4.8.6)")]
|
|
CrlDistributionPointsHasCrlIssuer,
|
|
|
|
#[error(
|
|
"CRLDistributionPoints distributionPoint MUST contain fullName and MUST NOT contain nameRelativeToCRLIssuer (RFC 6487 §4.8.6)"
|
|
)]
|
|
CrlDistributionPointsInvalidName,
|
|
|
|
#[error(
|
|
"CRLDistributionPoints fullName must contain only URI GeneralNames (RFC 6487 §4.8.6; RFC 5280 §4.2.1.6)"
|
|
)]
|
|
CrlDistributionPointsFullNameNotUri,
|
|
|
|
#[error("CRLDistributionPoints must include at least one rsync:// URI (RFC 6487 §4.8.6)")]
|
|
CrlDistributionPointsNoRsync,
|
|
|
|
#[error(
|
|
"authorityInfoAccess must be present in non-self-signed certificates (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)"
|
|
)]
|
|
AuthorityInfoAccessMissing,
|
|
|
|
#[error(
|
|
"authorityInfoAccess criticality must be non-critical (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)"
|
|
)]
|
|
AuthorityInfoAccessCriticality,
|
|
|
|
#[error(
|
|
"authorityInfoAccess MUST be omitted in self-signed certificates (RFC 6487 §4.8.7)"
|
|
)]
|
|
AuthorityInfoAccessSelfSignedMustOmit,
|
|
|
|
#[error(
|
|
"authorityInfoAccess id-ad-caIssuers accessLocation must be URI (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)"
|
|
)]
|
|
AuthorityInfoAccessCaIssuersNotUri,
|
|
|
|
#[error(
|
|
"authorityInfoAccess must include at least one id-ad-caIssuers URI (RFC 6487 §4.8.7)"
|
|
)]
|
|
AuthorityInfoAccessMissingCaIssuers,
|
|
|
|
#[error("authorityInfoAccess must include at least one rsync:// URI (RFC 6487 §4.8.7)")]
|
|
AuthorityInfoAccessNoRsync,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ResourceCertificateDecodeError {
|
|
#[error("{0}")]
|
|
Parse(#[from] ResourceCertificateParseError),
|
|
|
|
#[error("{0}")]
|
|
Validate(#[from] ResourceCertificateProfileError),
|
|
}
|
|
|
|
pub type ResourceCertificateError = ResourceCertificateDecodeError;
|
|
|
|
impl ResourceCertificate {
|
|
/// Parse step of scheme A (`parse → validate → verify`).
|
|
pub fn parse_der(
|
|
der: &[u8],
|
|
) -> Result<ResourceCertificateParsed, ResourceCertificateParseError> {
|
|
let (rem, cert) = X509Certificate::from_der(der)
|
|
.map_err(|e| ResourceCertificateParseError::Parse(e.to_string()))?;
|
|
if !rem.is_empty() {
|
|
return Err(ResourceCertificateParseError::TrailingBytes(rem.len()));
|
|
}
|
|
|
|
let validity_not_before = asn1_time_to_model(cert.validity().not_before);
|
|
let validity_not_after = asn1_time_to_model(cert.validity().not_after);
|
|
|
|
let subject_public_key_info = cert.tbs_certificate.subject_pki.raw.to_vec();
|
|
|
|
let signature_algorithm = algorithm_identifier_value(&cert.signature_algorithm);
|
|
let tbs_signature_algorithm = algorithm_identifier_value(&cert.tbs_certificate.signature);
|
|
let extensions = parse_extensions_parse(cert.extensions())?;
|
|
|
|
Ok(ResourceCertificateParsed {
|
|
raw_der: der.to_vec(),
|
|
version: cert.version(),
|
|
serial_number: cert.tbs_certificate.serial.clone(),
|
|
signature_algorithm,
|
|
tbs_signature_algorithm,
|
|
issuer_dn: cert.issuer().to_string(),
|
|
subject_dn: cert.subject().to_string(),
|
|
validity_not_before,
|
|
validity_not_after,
|
|
subject_public_key_info,
|
|
extensions,
|
|
})
|
|
}
|
|
|
|
/// Profile validate step of scheme A (`parse → validate → verify`).
|
|
///
|
|
/// `ResourceCertificate` is already profile-validated when constructed via `decode_der()` /
|
|
/// `ResourceCertificateParsed::validate_profile()`.
|
|
pub fn validate_profile(&self) -> Result<(), ResourceCertificateProfileError> {
|
|
Ok(())
|
|
}
|
|
|
|
/// Decode a resource certificate (`parse + validate`).
|
|
pub fn decode_der(der: &[u8]) -> Result<Self, ResourceCertificateDecodeError> {
|
|
Ok(Self::parse_der(der)?.validate_profile()?)
|
|
}
|
|
|
|
/// Backwards-compatible helper (historical name).
|
|
pub fn from_der(der: &[u8]) -> Result<Self, ResourceCertificateError> {
|
|
Self::decode_der(der)
|
|
}
|
|
}
|
|
|
|
impl ResourceCertificateParsed {
|
|
pub fn validate_profile(self) -> Result<ResourceCertificate, ResourceCertificateProfileError> {
|
|
let version = match self.version {
|
|
X509Version::V3 => 2u32,
|
|
_ => return Err(ResourceCertificateProfileError::InvalidVersion),
|
|
};
|
|
|
|
self.validity_not_before
|
|
.validate_encoding_rfc5280("notBefore")?;
|
|
self.validity_not_after
|
|
.validate_encoding_rfc5280("notAfter")?;
|
|
|
|
if self.signature_algorithm != self.tbs_signature_algorithm {
|
|
return Err(ResourceCertificateProfileError::SignatureAlgorithmMismatch);
|
|
}
|
|
if self.signature_algorithm.oid != OID_SHA256_WITH_RSA_ENCRYPTION {
|
|
return Err(ResourceCertificateProfileError::UnsupportedSignatureAlgorithm);
|
|
}
|
|
if !self.signature_algorithm.params_absent_or_null() {
|
|
return Err(ResourceCertificateProfileError::InvalidSignatureAlgorithmParameters);
|
|
}
|
|
|
|
let is_self_signed = self.issuer_dn == self.subject_dn;
|
|
let extensions = self.extensions.validate_profile(is_self_signed)?;
|
|
let kind = if extensions.basic_constraints_ca {
|
|
ResourceCertKind::Ca
|
|
} else {
|
|
ResourceCertKind::Ee
|
|
};
|
|
|
|
Ok(ResourceCertificate {
|
|
raw_der: self.raw_der,
|
|
tbs: RpkixTbsCertificate {
|
|
version,
|
|
serial_number: self.serial_number,
|
|
signature_algorithm: self.signature_algorithm.oid,
|
|
issuer_dn: self.issuer_dn,
|
|
subject_dn: self.subject_dn,
|
|
validity_not_before: self.validity_not_before.utc,
|
|
validity_not_after: self.validity_not_after.utc,
|
|
subject_public_key_info: self.subject_public_key_info,
|
|
extensions,
|
|
},
|
|
kind,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl RcExtensionsParsed {
|
|
pub fn validate_profile(
|
|
self,
|
|
is_self_signed: bool,
|
|
) -> Result<RcExtensions, ResourceCertificateProfileError> {
|
|
if self.basic_constraints_ca.len() > 1 {
|
|
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
|
"basicConstraints",
|
|
));
|
|
}
|
|
let basic_constraints_ca = self.basic_constraints_ca.first().copied().unwrap_or(false);
|
|
|
|
let subject_key_identifier = match self.subject_key_identifier.as_slice() {
|
|
[] => None,
|
|
[(ski, critical)] => {
|
|
if *critical {
|
|
return Err(ResourceCertificateProfileError::SkiCriticality);
|
|
}
|
|
Some(ski.clone())
|
|
}
|
|
_ => {
|
|
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
|
"subjectKeyIdentifier",
|
|
));
|
|
}
|
|
};
|
|
|
|
let authority_key_identifier = match self.authority_key_identifier.as_slice() {
|
|
[] => {
|
|
if is_self_signed {
|
|
None
|
|
} else {
|
|
return Err(ResourceCertificateProfileError::AkiMissing);
|
|
}
|
|
}
|
|
[(aki, critical)] => {
|
|
if *critical {
|
|
return Err(ResourceCertificateProfileError::AkiCriticality);
|
|
}
|
|
if aki.has_authority_cert_issuer {
|
|
return Err(ResourceCertificateProfileError::AkiAuthorityCertIssuerPresent);
|
|
}
|
|
if aki.has_authority_cert_serial {
|
|
return Err(ResourceCertificateProfileError::AkiAuthorityCertSerialPresent);
|
|
}
|
|
let keyid = aki.key_identifier.clone();
|
|
if is_self_signed {
|
|
if let (Some(keyid), Some(ski)) = (keyid.as_ref(), subject_key_identifier.as_ref())
|
|
{
|
|
if keyid != ski {
|
|
return Err(ResourceCertificateProfileError::AkiSelfSignedNotEqualSki);
|
|
}
|
|
}
|
|
} else if keyid.is_none() {
|
|
return Err(ResourceCertificateProfileError::AkiMissing);
|
|
}
|
|
keyid
|
|
}
|
|
_ => {
|
|
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
|
"authorityKeyIdentifier",
|
|
));
|
|
}
|
|
};
|
|
|
|
let crl_distribution_points_uris = match self.crl_distribution_points.as_slice() {
|
|
[] => {
|
|
if is_self_signed {
|
|
None
|
|
} else {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsMissing);
|
|
}
|
|
}
|
|
[(crldp, critical)] => {
|
|
if *critical {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsCriticality);
|
|
}
|
|
if is_self_signed {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsSelfSignedMustOmit);
|
|
}
|
|
if crldp.distribution_points.len() != 1 {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsNotSingle);
|
|
}
|
|
let dp = &crldp.distribution_points[0];
|
|
if dp.reasons_present {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsHasReasons);
|
|
}
|
|
if dp.crl_issuer_present {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsHasCrlIssuer);
|
|
}
|
|
if !dp.distribution_point_present {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsNoDistributionPoint);
|
|
}
|
|
if dp.name_relative_to_crl_issuer_present || !dp.full_name_present {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsInvalidName);
|
|
}
|
|
if dp.full_name_not_uri {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsFullNameNotUri);
|
|
}
|
|
if !dp.full_name_uris.iter().any(|u| u.scheme() == "rsync") {
|
|
return Err(ResourceCertificateProfileError::CrlDistributionPointsNoRsync);
|
|
}
|
|
Some(dp.full_name_uris.clone())
|
|
}
|
|
_ => {
|
|
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
|
"cRLDistributionPoints",
|
|
));
|
|
}
|
|
};
|
|
|
|
let ca_issuers_uris = match self.authority_info_access.as_slice() {
|
|
[] => {
|
|
if is_self_signed {
|
|
None
|
|
} else {
|
|
return Err(ResourceCertificateProfileError::AuthorityInfoAccessMissing);
|
|
}
|
|
}
|
|
[(aia, critical)] => {
|
|
if *critical {
|
|
return Err(ResourceCertificateProfileError::AuthorityInfoAccessCriticality);
|
|
}
|
|
if is_self_signed {
|
|
return Err(ResourceCertificateProfileError::AuthorityInfoAccessSelfSignedMustOmit);
|
|
}
|
|
if aia.ca_issuers_access_location_not_uri {
|
|
return Err(ResourceCertificateProfileError::AuthorityInfoAccessCaIssuersNotUri);
|
|
}
|
|
if aia.ca_issuers_uris.is_empty() {
|
|
return Err(ResourceCertificateProfileError::AuthorityInfoAccessMissingCaIssuers);
|
|
}
|
|
if !aia.ca_issuers_uris.iter().any(|u| u.scheme() == "rsync") {
|
|
return Err(ResourceCertificateProfileError::AuthorityInfoAccessNoRsync);
|
|
}
|
|
Some(aia.ca_issuers_uris.clone())
|
|
}
|
|
_ => {
|
|
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
|
"authorityInfoAccess",
|
|
));
|
|
}
|
|
};
|
|
|
|
let subject_info_access = match self.subject_info_access.as_slice() {
|
|
[] => None,
|
|
[(sia, critical)] => {
|
|
if *critical {
|
|
return Err(ResourceCertificateProfileError::SiaCriticality);
|
|
}
|
|
if sia.signed_object_access_location_not_uri {
|
|
return Err(ResourceCertificateProfileError::SignedObjectSiaNotUri);
|
|
}
|
|
if !sia.signed_object_uris.is_empty()
|
|
&& !sia.signed_object_uris.iter().any(|u| u.scheme() == "rsync")
|
|
{
|
|
return Err(ResourceCertificateProfileError::SignedObjectSiaNoRsync);
|
|
}
|
|
if sia.signed_object_uris.is_empty() {
|
|
Some(SubjectInfoAccess::Ca(SubjectInfoAccessCa {
|
|
access_descriptions: sia.access_descriptions.clone(),
|
|
}))
|
|
} else {
|
|
Some(SubjectInfoAccess::Ee(SubjectInfoAccessEe {
|
|
signed_object_uris: sia.signed_object_uris.clone(),
|
|
access_descriptions: sia.access_descriptions.clone(),
|
|
}))
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
|
"subjectInfoAccess",
|
|
));
|
|
}
|
|
};
|
|
|
|
let certificate_policies_oid = match self.certificate_policies.as_slice() {
|
|
[] => None,
|
|
[(oids, critical)] => {
|
|
if !*critical {
|
|
return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality);
|
|
}
|
|
if oids.len() != 1 {
|
|
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
|
|
"expected exactly one policy".into(),
|
|
));
|
|
}
|
|
let policy_oid = oids[0].clone();
|
|
if policy_oid != OID_CP_IPADDR_ASNUMBER {
|
|
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
|
|
policy_oid,
|
|
));
|
|
}
|
|
Some(OID_CP_IPADDR_ASNUMBER.to_string())
|
|
}
|
|
_ => {
|
|
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
|
"certificatePolicies",
|
|
));
|
|
}
|
|
};
|
|
|
|
let ip_resources = match self.ip_resources.as_slice() {
|
|
[] => None,
|
|
[(ip, critical)] => {
|
|
if !*critical {
|
|
return Err(ResourceCertificateProfileError::IpResourcesCriticality);
|
|
}
|
|
Some(ip.clone())
|
|
}
|
|
_ => {
|
|
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
|
"ipAddrBlocks",
|
|
));
|
|
}
|
|
};
|
|
|
|
let as_resources = match self.as_resources.as_slice() {
|
|
[] => None,
|
|
[(asn, critical)] => {
|
|
if !*critical {
|
|
return Err(ResourceCertificateProfileError::AsResourcesCriticality);
|
|
}
|
|
Some(asn.clone())
|
|
}
|
|
_ => {
|
|
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
|
"autonomousSysIds",
|
|
));
|
|
}
|
|
};
|
|
|
|
Ok(RcExtensions {
|
|
basic_constraints_ca,
|
|
subject_key_identifier,
|
|
authority_key_identifier,
|
|
crl_distribution_points_uris,
|
|
ca_issuers_uris,
|
|
subject_info_access,
|
|
certificate_policies_oid,
|
|
ip_resources,
|
|
as_resources,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn algorithm_identifier_value(
|
|
ai: &x509_parser::x509::AlgorithmIdentifier<'_>,
|
|
) -> AlgorithmIdentifierValue {
|
|
let parameters = ai.parameters.as_ref().map(|p| AlgorithmParametersValue {
|
|
class: p.class(),
|
|
tag: p.tag(),
|
|
data: p.as_bytes().to_vec(),
|
|
});
|
|
AlgorithmIdentifierValue {
|
|
oid: ai.algorithm.to_id_string(),
|
|
parameters,
|
|
}
|
|
}
|
|
|
|
fn parse_extensions_parse(
|
|
exts: &[X509Extension<'_>],
|
|
) -> Result<RcExtensionsParsed, ResourceCertificateParseError> {
|
|
let mut basic_constraints_ca: Vec<bool> = Vec::new();
|
|
let mut ski: Vec<(Vec<u8>, bool)> = Vec::new();
|
|
let mut aki: Vec<(AuthorityKeyIdentifierParsed, bool)> = Vec::new();
|
|
let mut crldp: Vec<(CrlDistributionPointsParsed, bool)> = Vec::new();
|
|
let mut aia: Vec<(AuthorityInfoAccessParsed, bool)> = Vec::new();
|
|
let mut sia: Vec<(SubjectInfoAccessParsed, bool)> = Vec::new();
|
|
let mut cert_policies: Vec<(Vec<String>, bool)> = Vec::new();
|
|
|
|
let mut ip_resources: Vec<(IpResourceSet, bool)> = Vec::new();
|
|
let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new();
|
|
|
|
for ext in exts {
|
|
let oid = ext.oid.to_id_string();
|
|
match oid.as_str() {
|
|
crate::data_model::oid::OID_BASIC_CONSTRAINTS => {
|
|
let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else {
|
|
return Err(ResourceCertificateParseError::Parse(
|
|
"basicConstraints parse failed".into(),
|
|
));
|
|
};
|
|
basic_constraints_ca.push(bc.ca);
|
|
}
|
|
OID_SUBJECT_KEY_IDENTIFIER => {
|
|
let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else {
|
|
return Err(ResourceCertificateParseError::Parse(
|
|
"subjectKeyIdentifier parse failed".into(),
|
|
));
|
|
};
|
|
ski.push((s.0.to_vec(), ext.critical));
|
|
}
|
|
OID_AUTHORITY_KEY_IDENTIFIER => {
|
|
let ParsedExtension::AuthorityKeyIdentifier(a) = ext.parsed_extension() else {
|
|
return Err(ResourceCertificateParseError::Parse(
|
|
"authorityKeyIdentifier parse failed".into(),
|
|
));
|
|
};
|
|
aki.push((
|
|
AuthorityKeyIdentifierParsed {
|
|
key_identifier: a.key_identifier.as_ref().map(|k| k.0.to_vec()),
|
|
has_authority_cert_issuer: a.authority_cert_issuer.is_some(),
|
|
has_authority_cert_serial: a.authority_cert_serial.is_some(),
|
|
},
|
|
ext.critical,
|
|
));
|
|
}
|
|
OID_CRL_DISTRIBUTION_POINTS => {
|
|
let ParsedExtension::CRLDistributionPoints(p) = ext.parsed_extension() else {
|
|
return Err(ResourceCertificateParseError::Parse(
|
|
"cRLDistributionPoints parse failed".into(),
|
|
));
|
|
};
|
|
crldp.push((parse_crldp_parse(p)?, ext.critical));
|
|
}
|
|
OID_AUTHORITY_INFO_ACCESS => {
|
|
let ParsedExtension::AuthorityInfoAccess(p) = ext.parsed_extension() else {
|
|
return Err(ResourceCertificateParseError::Parse(
|
|
"authorityInfoAccess parse failed".into(),
|
|
));
|
|
};
|
|
aia.push((parse_aia_parse(p.accessdescs.as_slice())?, ext.critical));
|
|
}
|
|
OID_SUBJECT_INFO_ACCESS => {
|
|
let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else {
|
|
return Err(ResourceCertificateParseError::Parse(
|
|
"subjectInfoAccess parse failed".into(),
|
|
));
|
|
};
|
|
sia.push((parse_sia_parse(s.accessdescs.as_slice())?, ext.critical));
|
|
}
|
|
crate::data_model::oid::OID_CERTIFICATE_POLICIES => {
|
|
let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else {
|
|
return Err(ResourceCertificateParseError::Parse(
|
|
"certificatePolicies parse failed".into(),
|
|
));
|
|
};
|
|
let oids: Vec<String> = cp.iter().map(|p| p.policy_id.to_id_string()).collect();
|
|
cert_policies.push((oids, ext.critical));
|
|
}
|
|
OID_IP_ADDR_BLOCKS => {
|
|
let parsed = IpResourceSet::decode_extn_value(ext.value)
|
|
.map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?;
|
|
ip_resources.push((parsed, ext.critical));
|
|
}
|
|
OID_AUTONOMOUS_SYS_IDS => {
|
|
let parsed = AsResourceSet::decode_extn_value(ext.value)
|
|
.map_err(|_e| ResourceCertificateParseError::InvalidAsResourcesEncoding)?;
|
|
as_resources.push((parsed, ext.critical));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok(RcExtensionsParsed {
|
|
basic_constraints_ca,
|
|
subject_key_identifier: ski,
|
|
authority_key_identifier: aki,
|
|
crl_distribution_points: crldp,
|
|
authority_info_access: aia,
|
|
subject_info_access: sia,
|
|
certificate_policies: cert_policies,
|
|
ip_resources,
|
|
as_resources,
|
|
})
|
|
}
|
|
|
|
fn parse_aia_parse(
|
|
access: &[x509_parser::extensions::AccessDescription<'_>],
|
|
) -> Result<AuthorityInfoAccessParsed, ResourceCertificateParseError> {
|
|
let mut ca_issuers_uris: Vec<Url> = Vec::new();
|
|
let mut ca_issuers_access_location_not_uri = false;
|
|
|
|
for ad in access {
|
|
let access_method_oid = ad.access_method.to_id_string();
|
|
if access_method_oid != OID_AD_CA_ISSUERS {
|
|
continue;
|
|
}
|
|
let uri = match &ad.access_location {
|
|
x509_parser::extensions::GeneralName::URI(u) => u,
|
|
_ => {
|
|
ca_issuers_access_location_not_uri = true;
|
|
continue;
|
|
}
|
|
};
|
|
let url = Url::parse(uri)
|
|
.map_err(|_| ResourceCertificateParseError::Parse(format!("invalid URI: {uri}")))?;
|
|
ca_issuers_uris.push(url);
|
|
}
|
|
|
|
Ok(AuthorityInfoAccessParsed {
|
|
ca_issuers_uris,
|
|
ca_issuers_access_location_not_uri,
|
|
})
|
|
}
|
|
|
|
fn parse_crldp_parse(
|
|
crldp: &x509_parser::extensions::CRLDistributionPoints<'_>,
|
|
) -> Result<CrlDistributionPointsParsed, ResourceCertificateParseError> {
|
|
let mut out: Vec<CrlDistributionPointParsed> = Vec::new();
|
|
for p in crldp.iter() {
|
|
let mut full_name_uris: Vec<Url> = Vec::new();
|
|
let mut full_name_not_uri = false;
|
|
let mut full_name_present = false;
|
|
let mut name_relative_to_crl_issuer_present = false;
|
|
let mut distribution_point_present = false;
|
|
|
|
if let Some(dp) = &p.distribution_point {
|
|
distribution_point_present = true;
|
|
match dp {
|
|
x509_parser::extensions::DistributionPointName::FullName(names) => {
|
|
full_name_present = true;
|
|
for n in names {
|
|
match n {
|
|
x509_parser::extensions::GeneralName::URI(u) => {
|
|
let url = Url::parse(u).map_err(|_| {
|
|
ResourceCertificateParseError::Parse(format!(
|
|
"invalid URI: {u}"
|
|
))
|
|
})?;
|
|
full_name_uris.push(url);
|
|
}
|
|
_ => {
|
|
full_name_not_uri = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
x509_parser::extensions::DistributionPointName::NameRelativeToCRLIssuer(_) => {
|
|
name_relative_to_crl_issuer_present = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
out.push(CrlDistributionPointParsed {
|
|
distribution_point_present,
|
|
reasons_present: p.reasons.is_some(),
|
|
crl_issuer_present: p.crl_issuer.is_some(),
|
|
name_relative_to_crl_issuer_present,
|
|
full_name_uris,
|
|
full_name_not_uri,
|
|
full_name_present,
|
|
});
|
|
}
|
|
Ok(CrlDistributionPointsParsed {
|
|
distribution_points: out,
|
|
})
|
|
}
|
|
|
|
fn parse_sia_parse(
|
|
access: &[x509_parser::extensions::AccessDescription<'_>],
|
|
) -> Result<SubjectInfoAccessParsed, ResourceCertificateParseError> {
|
|
let mut all = Vec::with_capacity(access.len());
|
|
let mut signed_object_uris: Vec<Url> = Vec::new();
|
|
let mut signed_object_access_location_not_uri = false;
|
|
|
|
for ad in access {
|
|
let access_method_oid = ad.access_method.to_id_string();
|
|
let uri = match &ad.access_location {
|
|
x509_parser::extensions::GeneralName::URI(u) => u,
|
|
_ => {
|
|
if access_method_oid == OID_AD_SIGNED_OBJECT {
|
|
signed_object_access_location_not_uri = true;
|
|
}
|
|
continue;
|
|
}
|
|
};
|
|
let url = Url::parse(uri)
|
|
.map_err(|_| ResourceCertificateParseError::Parse(format!("invalid URI: {uri}")))?;
|
|
if access_method_oid == OID_AD_SIGNED_OBJECT {
|
|
signed_object_uris.push(url.clone());
|
|
}
|
|
all.push(AccessDescription {
|
|
access_method_oid,
|
|
access_location: url,
|
|
});
|
|
}
|
|
|
|
Ok(SubjectInfoAccessParsed {
|
|
access_descriptions: all,
|
|
signed_object_uris,
|
|
signed_object_access_location_not_uri,
|
|
})
|
|
}
|
|
|
|
fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result<IpResourceSet, ()> {
|
|
let (rem, obj) = parse_der(ext_value).map_err(|_| ())?;
|
|
if !rem.is_empty() {
|
|
return Err(());
|
|
}
|
|
let seq = obj.as_sequence().map_err(|_| ())?;
|
|
let mut families = Vec::with_capacity(seq.len());
|
|
for fam in seq {
|
|
let fam_seq = fam.as_sequence().map_err(|_| ())?;
|
|
if fam_seq.len() != 2 {
|
|
return Err(());
|
|
}
|
|
let af_bytes = fam_seq[0].as_slice().map_err(|_| ())?;
|
|
if af_bytes.len() != 2 {
|
|
return Err(());
|
|
}
|
|
let afi = match af_bytes {
|
|
[0x00, 0x01] => Afi::Ipv4,
|
|
[0x00, 0x02] => Afi::Ipv6,
|
|
_ => return Err(()),
|
|
};
|
|
|
|
let choice = match &fam_seq[1].content {
|
|
BerObjectContent::Null => IpAddressChoice::Inherit,
|
|
BerObjectContent::Sequence(_) => {
|
|
let items_seq = fam_seq[1].as_sequence().map_err(|_| ())?;
|
|
let mut items = Vec::with_capacity(items_seq.len());
|
|
for item in items_seq {
|
|
items.push(parse_ip_address_or_range(afi, item)?);
|
|
}
|
|
IpAddressChoice::AddressesOrRanges(items)
|
|
}
|
|
_ => return Err(()),
|
|
};
|
|
families.push(IpAddressFamily { afi, choice });
|
|
}
|
|
Ok(IpResourceSet { families })
|
|
}
|
|
|
|
fn parse_ip_address_or_range(afi: Afi, obj: &DerObject<'_>) -> Result<IpAddressOrRange, ()> {
|
|
match &obj.content {
|
|
BerObjectContent::BitString(_, _) => {
|
|
Ok(IpAddressOrRange::Prefix(parse_ip_prefix(afi, obj)?))
|
|
}
|
|
BerObjectContent::Sequence(_) => {
|
|
let seq = obj.as_sequence().map_err(|_| ())?;
|
|
if seq.len() != 2 {
|
|
return Err(());
|
|
}
|
|
let min = parse_ip_address_bound(afi, &seq[0], false)?;
|
|
let max = parse_ip_address_bound(afi, &seq[1], true)?;
|
|
Ok(IpAddressOrRange::Range(IpAddressRange { min, max }))
|
|
}
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
|
|
fn parse_ip_prefix(afi: Afi, obj: &DerObject<'_>) -> Result<IpPrefix, ()> {
|
|
let (unused_bits, bytes) = match &obj.content {
|
|
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
|
_ => return Err(()),
|
|
};
|
|
if unused_bits > 7 {
|
|
return Err(());
|
|
}
|
|
if !bytes.is_empty() && unused_bits != 0 {
|
|
let mask = (1u8 << unused_bits) - 1;
|
|
if (bytes[bytes.len() - 1] & mask) != 0 {
|
|
return Err(());
|
|
}
|
|
} else if bytes.is_empty() && unused_bits != 0 {
|
|
return Err(());
|
|
}
|
|
let prefix_len = (bytes.len() * 8)
|
|
.checked_sub(unused_bits as usize)
|
|
.ok_or(())? as u16;
|
|
if prefix_len > afi.ub() {
|
|
return Err(());
|
|
}
|
|
let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes);
|
|
Ok(IpPrefix {
|
|
afi,
|
|
prefix_len,
|
|
addr,
|
|
})
|
|
}
|
|
|
|
/// Parse an RFC 3779 `IPAddress` BIT STRING into an address-like byte array.
|
|
///
|
|
/// When used as an `IPAddressRange` endpoint, RFC 3779 allows endpoints to be encoded with
|
|
/// fewer than `ub` bits. In that case, the missing bits are interpreted as 0s for the lower
|
|
/// bound and 1s for the upper bound. This is essential to correctly interpret ranges that
|
|
/// are expressed on non-octet boundaries.
|
|
fn parse_ip_address_bound(
|
|
afi: Afi,
|
|
obj: &DerObject<'_>,
|
|
fill_remaining_ones: bool,
|
|
) -> Result<Vec<u8>, ()> {
|
|
let (unused_bits, bytes) = match &obj.content {
|
|
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
|
_ => return Err(()),
|
|
};
|
|
if unused_bits > 7 {
|
|
return Err(());
|
|
}
|
|
if !bytes.is_empty() && unused_bits != 0 {
|
|
let mask = (1u8 << unused_bits) - 1;
|
|
if (bytes[bytes.len() - 1] & mask) != 0 {
|
|
return Err(());
|
|
}
|
|
} else if bytes.is_empty() && unused_bits != 0 {
|
|
return Err(());
|
|
}
|
|
|
|
let bit_len: u16 = (bytes.len() * 8)
|
|
.checked_sub(unused_bits as usize)
|
|
.ok_or(())?
|
|
.try_into()
|
|
.map_err(|_| ())?;
|
|
if bit_len > afi.ub() {
|
|
return Err(());
|
|
}
|
|
|
|
let mut out = vec![0u8; afi.octets_len()];
|
|
let copy_len = bytes.len().min(out.len());
|
|
out[..copy_len].copy_from_slice(&bytes[..copy_len]);
|
|
|
|
if fill_remaining_ones {
|
|
if bit_len == 0 {
|
|
for b in &mut out {
|
|
*b = 0xFF;
|
|
}
|
|
return Ok(out);
|
|
}
|
|
|
|
let last_bit = (bit_len - 1) as usize;
|
|
let last_byte = last_bit / 8;
|
|
let rem = (bit_len % 8) as u8;
|
|
|
|
if rem != 0 && last_byte < out.len() {
|
|
// Set the (8-rem) trailing bits in the last byte to 1.
|
|
let mask: u8 = (1u8 << (8 - rem)) - 1;
|
|
out[last_byte] |= mask;
|
|
}
|
|
for b in out.iter_mut().skip(last_byte + 1) {
|
|
*b = 0xFF;
|
|
}
|
|
}
|
|
|
|
Ok(out)
|
|
}
|
|
|
|
fn parse_as_identifiers(ext_value: &[u8]) -> Result<AsResourceSet, ()> {
|
|
let (rem, obj) = parse_der(ext_value).map_err(|_| ())?;
|
|
if !rem.is_empty() {
|
|
return Err(());
|
|
}
|
|
let seq = obj.as_sequence().map_err(|_| ())?;
|
|
let mut asnum: Option<AsIdentifierChoice> = None;
|
|
let mut rdi: Option<AsIdentifierChoice> = None;
|
|
for item in seq {
|
|
if item.class() != Class::ContextSpecific {
|
|
return Err(());
|
|
}
|
|
match item.tag() {
|
|
Tag(0) => {
|
|
if asnum.is_some() {
|
|
return Err(());
|
|
}
|
|
let inner = parse_explicit_inner(item)?;
|
|
asnum = Some(parse_as_identifier_choice(&inner)?);
|
|
}
|
|
Tag(1) => {
|
|
if rdi.is_some() {
|
|
return Err(());
|
|
}
|
|
let inner = parse_explicit_inner(item)?;
|
|
rdi = Some(parse_as_identifier_choice(&inner)?);
|
|
}
|
|
_ => return Err(()),
|
|
}
|
|
}
|
|
Ok(AsResourceSet { asnum, rdi })
|
|
}
|
|
|
|
fn parse_explicit_inner<'a>(obj: &'a DerObject<'a>) -> Result<DerObject<'a>, ()> {
|
|
let inner_der = obj.as_slice().map_err(|_| ())?;
|
|
let (rem, inner) = parse_der(inner_der).map_err(|_| ())?;
|
|
if !rem.is_empty() {
|
|
return Err(());
|
|
}
|
|
Ok(inner)
|
|
}
|
|
|
|
fn parse_as_identifier_choice(obj: &DerObject<'_>) -> Result<AsIdentifierChoice, ()> {
|
|
match &obj.content {
|
|
BerObjectContent::Null => Ok(AsIdentifierChoice::Inherit),
|
|
BerObjectContent::Sequence(_) => {
|
|
let seq = obj.as_sequence().map_err(|_| ())?;
|
|
let mut items = Vec::with_capacity(seq.len());
|
|
for item in seq {
|
|
items.push(parse_as_id_or_range(item)?);
|
|
}
|
|
Ok(AsIdentifierChoice::AsIdsOrRanges(items))
|
|
}
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
|
|
fn parse_as_id_or_range(obj: &DerObject<'_>) -> Result<AsIdOrRange, ()> {
|
|
match &obj.content {
|
|
BerObjectContent::Integer(_) => {
|
|
let v = obj.as_u64().map_err(|_| ())?;
|
|
if v > u32::MAX as u64 {
|
|
return Err(());
|
|
}
|
|
Ok(AsIdOrRange::Id(v as u32))
|
|
}
|
|
BerObjectContent::Sequence(_) => {
|
|
let seq = obj.as_sequence().map_err(|_| ())?;
|
|
if seq.len() != 2 {
|
|
return Err(());
|
|
}
|
|
let min = seq[0].as_u64().map_err(|_| ())?;
|
|
let max = seq[1].as_u64().map_err(|_| ())?;
|
|
if min > u32::MAX as u64 || max > u32::MAX as u64 || min > max {
|
|
return Err(());
|
|
}
|
|
Ok(AsIdOrRange::Range {
|
|
min: min as u32,
|
|
max: max as u32,
|
|
})
|
|
}
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
|
|
fn canonicalize_prefix_addr(afi: Afi, prefix_len: u16, bytes: &[u8]) -> Vec<u8> {
|
|
let full_len = afi.octets_len();
|
|
let mut addr = vec![0u8; full_len];
|
|
let copy_len = bytes.len().min(full_len);
|
|
addr[..copy_len].copy_from_slice(&bytes[..copy_len]);
|
|
|
|
if prefix_len == 0 {
|
|
return addr;
|
|
}
|
|
|
|
let last_prefix_bit = (prefix_len - 1) as usize;
|
|
let last_prefix_byte = last_prefix_bit / 8;
|
|
let rem = (prefix_len % 8) as u8;
|
|
if rem != 0 && last_prefix_byte < addr.len() {
|
|
let mask: u8 = 0xFF << (8 - rem);
|
|
addr[last_prefix_byte] &= mask;
|
|
}
|
|
addr
|
|
}
|
|
|
|
fn prefix_covers(resource: &IpPrefix, subject: &IpPrefix) -> bool {
|
|
if resource.afi != subject.afi {
|
|
return false;
|
|
}
|
|
if resource.prefix_len > subject.prefix_len {
|
|
return false;
|
|
}
|
|
let n = resource.prefix_len as usize;
|
|
let whole = n / 8;
|
|
let rem = (n % 8) as u8;
|
|
if resource.addr.len() != subject.addr.len() {
|
|
return false;
|
|
}
|
|
if resource.addr[..whole] != subject.addr[..whole] {
|
|
return false;
|
|
}
|
|
if rem == 0 {
|
|
return true;
|
|
}
|
|
let mask = 0xFFu8 << (8 - rem);
|
|
(resource.addr[whole] & mask) == (subject.addr[whole] & mask)
|
|
}
|
|
|
|
fn prefix_range(afi: Afi, p: &IpPrefix) -> (u128, u128) {
|
|
let mut base_bytes = [0u8; 16];
|
|
match afi {
|
|
Afi::Ipv4 => {
|
|
base_bytes[12..].copy_from_slice(&p.addr[..4]);
|
|
}
|
|
Afi::Ipv6 => {
|
|
base_bytes.copy_from_slice(&p.addr[..16]);
|
|
}
|
|
}
|
|
let base = u128::from_be_bytes(base_bytes);
|
|
let host_bits = (afi.ub() - p.prefix_len) as u32;
|
|
if host_bits == 0 {
|
|
return (base, base);
|
|
}
|
|
let mask = (1u128 << host_bits) - 1;
|
|
(base, base | mask)
|
|
}
|
|
|
|
fn range_covers_prefix(afi: Afi, r: &IpAddressRange, p: &IpPrefix) -> bool {
|
|
let (p_min, p_max) = prefix_range(afi, p);
|
|
let r_min = bytes_to_u128(afi, &r.min);
|
|
let r_max = bytes_to_u128(afi, &r.max);
|
|
r_min <= p_min && p_max <= r_max
|
|
}
|
|
|
|
fn bytes_to_u128(afi: Afi, bytes: &[u8]) -> u128 {
|
|
let mut out = [0u8; 16];
|
|
match afi {
|
|
Afi::Ipv4 => {
|
|
let copy_len = bytes.len().min(4);
|
|
out[12..12 + copy_len].copy_from_slice(&bytes[..copy_len]);
|
|
}
|
|
Afi::Ipv6 => {
|
|
let copy_len = bytes.len().min(16);
|
|
out[..copy_len].copy_from_slice(&bytes[..copy_len]);
|
|
}
|
|
}
|
|
u128::from_be_bytes(out)
|
|
}
|