diff --git a/Cargo.toml b/Cargo.toml index df5077f9..ae8ffb25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,8 @@ include = [ "src/calendar.rs", "src/cert.rs", - "src/crl.rs", + "src/crl/mod.rs", + "src/crl/types.rs", "src/der.rs", "src/end_entity.rs", "src/error.rs", diff --git a/src/cert.rs b/src/cert.rs index b47b0049..23924f93 100644 --- a/src/cert.rs +++ b/src/cert.rs @@ -17,20 +17,8 @@ use crate::error::{DerTypeId, Error}; use crate::signed_data::SignedData; use crate::x509::{remember_extension, set_extension_once, DistributionPointName, Extension}; -/// An enumeration indicating whether a [`Cert`] is a leaf end-entity cert, or a linked -/// list node from the CA `Cert` to a child `Cert` it issued. -pub enum EndEntityOrCa<'a> { - /// The [`Cert`] is a leaf end-entity certificate. - EndEntity, - - /// The [`Cert`] is an issuer certificate, and issued the referenced child `Cert`. - Ca(&'a Cert<'a>), -} - /// A parsed X509 certificate. pub struct Cert<'a> { - pub(crate) ee_or_ca: EndEntityOrCa<'a>, - pub(crate) serial: untrusted::Input<'a>, pub(crate) signed_data: SignedData<'a>, pub(crate) issuer: untrusted::Input<'a>, @@ -51,10 +39,7 @@ pub struct Cert<'a> { } impl<'a> Cert<'a> { - pub(crate) fn from_der( - cert_der: untrusted::Input<'a>, - ee_or_ca: EndEntityOrCa<'a>, - ) -> Result { + pub(crate) fn from_der(cert_der: untrusted::Input<'a>) -> Result { let (tbs, signed_data) = cert_der.read_all(Error::TrailingData(DerTypeId::Certificate), |cert_der| { der::nested( @@ -94,8 +79,6 @@ impl<'a> Cert<'a> { // contain them. let mut cert = Cert { - ee_or_ca, - signed_data, serial, issuer, @@ -153,12 +136,6 @@ impl<'a> Cert<'a> { self.subject.as_slice_less_safe() } - /// Returns an indication of whether the certificate is an end-entity (leaf) certificate, - /// or a certificate authority. - pub fn end_entity_or_ca(&self) -> &EndEntityOrCa { - &self.ee_or_ca - } - /// Returns an iterator over the certificate's cRLDistributionPoints extension values, if any. pub(crate) fn crl_distribution_points( &self, @@ -321,7 +298,7 @@ impl<'a> FromDer<'a> for CrlDistributionPoint<'a> { #[cfg(test)] mod tests { - use crate::cert::{Cert, EndEntityOrCa}; + use crate::cert::Cert; #[cfg(feature = "alloc")] use crate::{ cert::{CrlDistributionPoint, DistributionPointName}, @@ -335,13 +312,11 @@ mod tests { // is read correctly here instead of in tests/integration.rs. fn test_serial_read() { let ee = include_bytes!("../tests/misc/serial_neg_ee.der"); - let cert = Cert::from_der(untrusted::Input::from(ee), EndEntityOrCa::EndEntity) - .expect("failed to parse certificate"); + let cert = Cert::from_der(untrusted::Input::from(ee)).expect("failed to parse certificate"); assert_eq!(cert.serial.as_slice_less_safe(), &[255, 33, 82, 65, 17]); let ee = include_bytes!("../tests/misc/serial_large_positive.der"); - let cert = Cert::from_der(untrusted::Input::from(ee), EndEntityOrCa::EndEntity) - .expect("failed to parse certificate"); + let cert = Cert::from_der(untrusted::Input::from(ee)).expect("failed to parse certificate"); assert_eq!( cert.serial.as_slice_less_safe(), &[ @@ -356,10 +331,9 @@ mod tests { fn test_crl_distribution_point_netflix() { let ee = include_bytes!("../tests/netflix/ee.der"); let inter = include_bytes!("../tests/netflix/inter.der"); - let ee_cert = Cert::from_der(untrusted::Input::from(ee), EndEntityOrCa::EndEntity) - .expect("failed to parse EE cert"); - let cert = Cert::from_der(untrusted::Input::from(inter), EndEntityOrCa::Ca(&ee_cert)) - .expect("failed to parse certificate"); + let ee_cert = Cert::from_der(untrusted::Input::from(ee)).expect("failed to parse EE cert"); + let cert = + Cert::from_der(untrusted::Input::from(inter)).expect("failed to parse certificate"); // The end entity certificate shouldn't have a distribution point. assert!(ee_cert.crl_distribution_points.is_none()); @@ -423,8 +397,8 @@ mod tests { #[cfg(feature = "alloc")] fn test_crl_distribution_point_with_reasons() { let der = include_bytes!("../tests/crl_distrib_point/with_reasons.der"); - let cert = Cert::from_der(untrusted::Input::from(der), EndEntityOrCa::EndEntity) - .expect("failed to parse certificate"); + let cert = + Cert::from_der(untrusted::Input::from(der)).expect("failed to parse certificate"); // We expect to be able to parse the intermediate certificate's CRL distribution points. let crl_distribution_points = cert @@ -462,8 +436,8 @@ mod tests { #[cfg(feature = "alloc")] fn test_crl_distribution_point_with_crl_issuer() { let der = include_bytes!("../tests/crl_distrib_point/with_crl_issuer.der"); - let cert = Cert::from_der(untrusted::Input::from(der), EndEntityOrCa::EndEntity) - .expect("failed to parse certificate"); + let cert = + Cert::from_der(untrusted::Input::from(der)).expect("failed to parse certificate"); // We expect to be able to parse the intermediate certificate's CRL distribution points. let crl_distribution_points = cert @@ -490,8 +464,8 @@ mod tests { // Created w/ // ascii2der -i tests/crl_distrib_point/unknown_tag.der.txt -o tests/crl_distrib_point/unknown_tag.der let der = include_bytes!("../tests/crl_distrib_point/unknown_tag.der"); - let cert = Cert::from_der(untrusted::Input::from(der), EndEntityOrCa::EndEntity) - .expect("failed to parse certificate"); + let cert = + Cert::from_der(untrusted::Input::from(der)).expect("failed to parse certificate"); // We expect there to be a distribution point extension, but parsing it should fail // due to the unknown tag in the SEQUENCE. @@ -508,8 +482,8 @@ mod tests { // Created w/ // ascii2der -i tests/crl_distrib_point/only_reasons.der.txt -o tests/crl_distrib_point/only_reasons.der let der = include_bytes!("../tests/crl_distrib_point/only_reasons.der"); - let cert = Cert::from_der(untrusted::Input::from(der), EndEntityOrCa::EndEntity) - .expect("failed to parse certificate"); + let cert = + Cert::from_der(untrusted::Input::from(der)).expect("failed to parse certificate"); // We expect there to be a distribution point extension, but parsing it should fail // because no distribution points or cRLIssuer are set in the SEQUENCE, just reason codes. @@ -524,8 +498,8 @@ mod tests { #[cfg(feature = "alloc")] fn test_crl_distribution_point_name_relative_to_issuer() { let der = include_bytes!("../tests/crl_distrib_point/dp_name_relative_to_issuer.der"); - let cert = Cert::from_der(untrusted::Input::from(der), EndEntityOrCa::EndEntity) - .expect("failed to parse certificate"); + let cert = + Cert::from_der(untrusted::Input::from(der)).expect("failed to parse certificate"); // We expect to be able to parse the intermediate certificate's CRL distribution points. let crl_distribution_points = cert @@ -564,8 +538,8 @@ mod tests { // Created w/ // ascii2der -i tests/crl_distrib_point/unknown_dp_name_tag.der.txt > tests/crl_distrib_point/unknown_dp_name_tag.der let der = include_bytes!("../tests/crl_distrib_point/unknown_dp_name_tag.der"); - let cert = Cert::from_der(untrusted::Input::from(der), EndEntityOrCa::EndEntity) - .expect("failed to parse certificate"); + let cert = + Cert::from_der(untrusted::Input::from(der)).expect("failed to parse certificate"); // We expect to be able to parse the intermediate certificate's CRL distribution points. let crl_distribution_points = cert @@ -589,8 +563,8 @@ mod tests { #[cfg(feature = "alloc")] fn test_crl_distribution_point_multiple() { let der = include_bytes!("../tests/crl_distrib_point/multiple_distribution_points.der"); - let cert = Cert::from_der(untrusted::Input::from(der), EndEntityOrCa::EndEntity) - .expect("failed to parse certificate"); + let cert = + Cert::from_der(untrusted::Input::from(der)).expect("failed to parse certificate"); // We expect to be able to parse the intermediate certificate's CRL distribution points. let crl_distribution_points = cert diff --git a/src/crl/mod.rs b/src/crl/mod.rs new file mode 100644 index 00000000..005fa8bd --- /dev/null +++ b/src/crl/mod.rs @@ -0,0 +1,412 @@ +// Copyright 2023 Daniel McCarney. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use pki_types::SignatureVerificationAlgorithm; + +use crate::der; +use crate::error::Error; +use crate::verify_cert::{Budget, PathNode}; + +use core::fmt::Debug; + +mod types; +use types::IssuingDistributionPoint; +pub use types::{ + BorrowedCertRevocationList, BorrowedRevokedCert, CertRevocationList, RevocationReason, +}; +#[cfg(feature = "alloc")] +pub use types::{OwnedCertRevocationList, OwnedRevokedCert}; + +/// Builds a RevocationOptions instance to control how revocation checking is performed. +#[derive(Debug, Copy, Clone)] +pub struct RevocationOptionsBuilder<'a> { + crls: &'a [&'a dyn CertRevocationList], + + depth: RevocationCheckDepth, + + status_requirement: UnknownStatusPolicy, +} + +impl<'a> RevocationOptionsBuilder<'a> { + /// Create a builder that will perform revocation checking using the provided certificate + /// revocation lists (CRLs). At least one CRL must be provided. + /// + /// Use [RevocationOptionsBuilder::build] to create a [RevocationOptions] instance. + /// + /// By default revocation checking will be performed on both the end-entity (leaf) certificate + /// and intermediate certificates. This can be customized using the + /// [RevocationOptionsBuilder::with_depth] method. + /// + /// By default revocation checking will fail if the revocation status of a certificate cannot + /// be determined. This can be customized using the + /// [RevocationOptionsBuilder::allow_unknown_status] method. + pub fn new(crls: &'a [&'a dyn CertRevocationList]) -> Result { + if crls.is_empty() { + return Err(CrlsRequired(())); + } + + Ok(Self { + crls, + depth: RevocationCheckDepth::Chain, + status_requirement: UnknownStatusPolicy::Deny, + }) + } + + /// Customize the depth at which revocation checking will be performed, controlling + /// whether only the end-entity (leaf) certificate in the chain to a trust anchor will + /// have its revocation status checked, or whether the intermediate certificates will as well. + pub fn with_depth(mut self, depth: RevocationCheckDepth) -> Self { + self.depth = depth; + self + } + + /// Treat unknown revocation status permissively, acting as if the certificate were not + /// revoked. + pub fn allow_unknown_status(mut self) -> Self { + self.status_requirement = UnknownStatusPolicy::Allow; + self + } + + /// Treat unknown revocation status strictly, considering it an error condition. + pub fn forbid_unknown_status(mut self) -> Self { + self.status_requirement = UnknownStatusPolicy::Deny; + self + } + + /// Construct a [RevocationOptions] instance based on the builder's configuration. + pub fn build(self) -> RevocationOptions<'a> { + RevocationOptions { + crls: self.crls, + depth: self.depth, + status_requirement: self.status_requirement, + } + } +} + +/// Describes how revocation checking is performed, if at all. Can be constructed with a +/// [RevocationOptionsBuilder] instance. +#[derive(Debug, Copy, Clone)] +pub struct RevocationOptions<'a> { + pub(crate) crls: &'a [&'a dyn CertRevocationList], + pub(crate) depth: RevocationCheckDepth, + pub(crate) status_requirement: UnknownStatusPolicy, +} + +impl<'a> RevocationOptions<'a> { + pub(crate) fn check( + &self, + path: &PathNode<'_>, + issuer_subject: untrusted::Input, + issuer_spki: untrusted::Input, + issuer_ku: Option, + supported_sig_algs: &[&dyn SignatureVerificationAlgorithm], + budget: &mut Budget, + ) -> Result, Error> { + assert_eq!(path.cert.issuer, issuer_subject); + + // If the policy only specifies checking EndEntity revocation state and we're looking at an + // issuer certificate, return early without considering the certificate's revocation state. + if let (RevocationCheckDepth::EndEntity, Some(_)) = (self.depth, &path.issued) { + return Ok(None); + } + + let crl = self + .crls + .iter() + .find(|candidate_crl| crl_authoritative(**candidate_crl, path)); + + use UnknownStatusPolicy::*; + let crl = match (crl, self.status_requirement) { + (Some(crl), _) => crl, + // If the policy allows unknown, return Ok(None) to indicate that the certificate + // was not confirmed as CertNotRevoked, but that this isn't an error condition. + (None, Allow) => return Ok(None), + // Otherwise, this is an error condition based on the provided policy. + (None, _) => return Err(Error::UnknownRevocationStatus), + }; + + // Verify the CRL signature with the issuer SPKI. + // TODO(XXX): consider whether we can refactor so this happens once up-front, instead + // of per-lookup. + // https://github.com/rustls/webpki/issues/81 + crl.verify_signature(supported_sig_algs, issuer_spki.as_slice_less_safe(), budget) + .map_err(crl_signature_err)?; + + // Verify that if the issuer has a KeyUsage bitstring it asserts cRLSign. + KeyUsageMode::CrlSign.check(issuer_ku)?; + + // Try to find the cert serial in the verified CRL contents. + let cert_serial = path.cert.serial.as_slice_less_safe(); + match crl.find_serial(cert_serial)? { + None => Ok(Some(CertNotRevoked::assertion())), + Some(_) => Err(Error::CertRevoked), + } + } +} + +// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3 +#[repr(u8)] +#[derive(Clone, Copy)] +enum KeyUsageMode { + // DigitalSignature = 0, + // ContentCommitment = 1, + // KeyEncipherment = 2, + // DataEncipherment = 3, + // KeyAgreement = 4, + // CertSign = 5, + CrlSign = 6, + // EncipherOnly = 7, + // DecipherOnly = 8, +} + +impl KeyUsageMode { + // https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3 + fn check(self, input: Option) -> Result<(), Error> { + let bit_string = match input { + Some(input) => { + der::expect_tag(&mut untrusted::Reader::new(input), der::Tag::BitString)? + } + // While RFC 5280 requires KeyUsage be present, historically the absence of a KeyUsage + // has been treated as "Any Usage". We follow that convention here and assume the absence + // of KeyUsage implies the required_ku_bit_if_present we're checking for. + None => return Ok(()), + }; + + let flags = der::bit_string_flags(bit_string)?; + #[allow(clippy::as_conversions)] // u8 always fits in usize. + match flags.bit_set(self as usize) { + true => Ok(()), + false => Err(Error::IssuerNotCrlSigner), + } + } +} + +/// Returns true if the CRL can be considered authoritative for the given certificate. +/// +/// A CRL is considered authoritative for a certificate when: +/// * The certificate issuer matches the CRL issuer and, +/// * The certificate has no CRL distribution points, and the CRL has no issuing distribution +/// point extension. +/// * Or, the certificate has no CRL distribution points, but the the CRL has an issuing +/// distribution point extension with a scope that includes the certificate. +/// * Or, the certificate has CRL distribution points, and the CRL has an issuing +/// distribution point extension with a scope that includes the certificate, and at least +/// one distribution point full name is a URI type general name that can also be found in +/// the CRL issuing distribution point full name general name sequence. +/// +/// In all other circumstances the CRL is not considered authoritative. +fn crl_authoritative(crl: &dyn CertRevocationList, path: &PathNode<'_>) -> bool { + // In all cases we require that the authoritative CRL have the same issuer + // as the certificate. Recall we do not support indirect CRLs. + if crl.issuer() != path.cert.issuer() { + return false; + } + + let crl_idp = match ( + path.cert.crl_distribution_points(), + crl.issuing_distribution_point(), + ) { + // If the certificate has no CRL distribution points, and the CRL has no issuing distribution point, + // then we can consider this CRL authoritative based on the issuer matching. + (cert_dps, None) => return cert_dps.is_none(), + + // If the CRL has an issuing distribution point, parse it so we can consider its scope + // and compare against the cert CRL distribution points, if present. + (_, Some(crl_idp)) => { + match IssuingDistributionPoint::from_der(untrusted::Input::from(crl_idp)) { + Ok(crl_idp) => crl_idp, + Err(_) => return false, // Note: shouldn't happen - we verify IDP at CRL-load. + } + } + }; + + crl_idp.authoritative_for(path) +} + +// When verifying CRL signed data we want to disambiguate the context of possible errors by mapping +// them to CRL specific variants that a consumer can use to tell the issue was with the CRL's +// signature, not a certificate. +fn crl_signature_err(err: Error) -> Error { + match err { + Error::UnsupportedSignatureAlgorithm => Error::UnsupportedCrlSignatureAlgorithm, + Error::UnsupportedSignatureAlgorithmForPublicKey => { + Error::UnsupportedCrlSignatureAlgorithmForPublicKey + } + Error::InvalidSignatureForPublicKey => Error::InvalidCrlSignatureForPublicKey, + _ => err, + } +} + +/// Describes how much of a certificate chain is checked for revocation status. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum RevocationCheckDepth { + /// Only check the end entity (leaf) certificate's revocation status. + EndEntity, + /// Check the revocation status of the end entity (leaf) and all intermediates. + Chain, +} + +/// Describes how to handle the case where a certificate's revocation status is unknown. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum UnknownStatusPolicy { + /// Treat unknown revocation status permissively, acting as if the certificate were + /// not revoked. + Allow, + /// Treat unknown revocation status as an error condition, yielding + /// [Error::UnknownRevocationStatus]. + Deny, +} + +// Zero-sized marker type representing positive assertion that revocation status was checked +// for a certificate and the result was that the certificate is not revoked. +pub(crate) struct CertNotRevoked(()); + +impl CertNotRevoked { + // Construct a CertNotRevoked marker. + fn assertion() -> Self { + Self(()) + } +} + +#[derive(Debug, Copy, Clone)] +/// An opaque error indicating the caller must provide at least one CRL when building a +/// [RevocationOptions] instance. +pub struct CrlsRequired(pub(crate) ()); + +mod private { + pub trait Sealed {} +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{verify_cert::PathNode, Cert}; + + #[test] + // safe to convert BorrowedCertRevocationList to CertRevocationList. + // redundant clone, clone_on_copy allowed to verify derived traits. + #[allow(clippy::as_conversions, clippy::redundant_clone, clippy::clone_on_copy)] + fn test_revocation_opts_builder() { + // Trying to build a RevocationOptionsBuilder w/o CRLs should err. + let result = RevocationOptionsBuilder::new(&[]); + assert!(matches!(result, Err(CrlsRequired(_)))); + + // The CrlsRequired error should be debug and clone when alloc is enabled. + #[cfg(feature = "alloc")] + { + let err = result.unwrap_err(); + println!("{:?}", err.clone()); + } + + // It should be possible to build a revocation options builder with defaults. + let crl = include_bytes!("../../tests/crls/crl.valid.der"); + let crl = + &BorrowedCertRevocationList::from_der(&crl[..]).unwrap() as &dyn CertRevocationList; + let crls = [crl]; + let builder = RevocationOptionsBuilder::new(&crls[..]).unwrap(); + #[cfg(feature = "alloc")] + { + // The builder should be debug, and clone when alloc is enabled + println!("{:?}", builder); + _ = builder.clone(); + } + let opts = builder.build(); + assert_eq!(opts.depth, RevocationCheckDepth::Chain); + assert_eq!(opts.status_requirement, UnknownStatusPolicy::Deny); + assert_eq!(opts.crls.len(), 1); + + // It should be possible to build a revocation options builder with custom depth. + let opts = RevocationOptionsBuilder::new(&crls[..]) + .unwrap() + .with_depth(RevocationCheckDepth::EndEntity) + .build(); + assert_eq!(opts.depth, RevocationCheckDepth::EndEntity); + assert_eq!(opts.status_requirement, UnknownStatusPolicy::Deny); + assert_eq!(opts.crls.len(), 1); + + // It should be possible to build a revocation options builder that allows unknown + // revocation status. + let opts = RevocationOptionsBuilder::new(&crls[..]) + .unwrap() + .allow_unknown_status() + .build(); + assert_eq!(opts.depth, RevocationCheckDepth::Chain); + assert_eq!(opts.status_requirement, UnknownStatusPolicy::Allow); + assert_eq!(opts.crls.len(), 1); + + // It should be possible to specify both depth and unknown status requirements together. + let opts = RevocationOptionsBuilder::new(&crls[..]) + .unwrap() + .allow_unknown_status() + .with_depth(RevocationCheckDepth::EndEntity) + .build(); + assert_eq!(opts.depth, RevocationCheckDepth::EndEntity); + assert_eq!(opts.status_requirement, UnknownStatusPolicy::Allow); + assert_eq!(opts.crls.len(), 1); + + // The same should be true for explicitly forbidding unknown status. + let opts = RevocationOptionsBuilder::new(&crls[..]) + .unwrap() + .forbid_unknown_status() + .with_depth(RevocationCheckDepth::EndEntity) + .build(); + assert_eq!(opts.depth, RevocationCheckDepth::EndEntity); + assert_eq!(opts.status_requirement, UnknownStatusPolicy::Deny); + assert_eq!(opts.crls.len(), 1); + + // Built revocation options should be debug and clone when alloc is enabled. + #[cfg(feature = "alloc")] + { + println!("{:?}", opts.clone()); + } + } + + #[test] + fn test_crl_authoritative_issuer_mismatch() { + let crl = include_bytes!("../../tests/crls/crl.valid.der"); + let crl = BorrowedCertRevocationList::from_der(&crl[..]).unwrap(); + + let ee = include_bytes!("../../tests/client_auth_revocation/no_ku_chain.ee.der"); + let ee = Cert::from_der(untrusted::Input::from(&ee[..])).unwrap(); + + // The CRL should not be authoritative for an EE issued by a different issuer. + assert!(!crl_authoritative( + &crl, + &PathNode { + cert: &ee, + issued: None + } + )); + } + + #[test] + fn test_crl_authoritative_no_idp_no_cert_dp() { + let crl = + include_bytes!("../../tests/client_auth_revocation/ee_revoked_crl_ku_ee_depth.crl.der"); + let crl = BorrowedCertRevocationList::from_der(&crl[..]).unwrap(); + + let ee = include_bytes!("../../tests/client_auth_revocation/ku_chain.ee.der"); + let ee = Cert::from_der(untrusted::Input::from(&ee[..])).unwrap(); + + // The CRL should be considered authoritative, the issuers match, the CRL has no IDP and the + // cert has no CRL DPs. + assert!(crl_authoritative( + &crl, + &PathNode { + cert: &ee, + issued: None + } + )); + } +} diff --git a/src/crl.rs b/src/crl/types.rs similarity index 93% rename from src/crl.rs rename to src/crl/types.rs index a709ab5e..694e876e 100644 --- a/src/crl.rs +++ b/src/crl/types.rs @@ -1,35 +1,21 @@ -// Copyright 2023 Daniel McCarney. -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR -// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - use pki_types::SignatureVerificationAlgorithm; -use crate::cert::{lenient_certificate_serial_number, Cert, EndEntityOrCa}; +use crate::cert::lenient_certificate_serial_number; use crate::der::{self, DerIterator, FromDer, Tag, CONSTRUCTED, CONTEXT_SPECIFIC}; use crate::error::{DerTypeId, Error}; use crate::signed_data::{self, SignedData}; use crate::subject_name::GeneralName; -use crate::verify_cert::Budget; +use crate::verify_cert::{Budget, PathNode}; use crate::x509::{remember_extension, set_extension_once, DistributionPointName, Extension}; use crate::Time; -use core::fmt::Debug; #[cfg(feature = "alloc")] use alloc::collections::BTreeMap; #[cfg(feature = "alloc")] use alloc::vec::Vec; +use core::fmt::Debug; -use private::Sealed; +use super::private::Sealed; /// Operations over a RFC 5280[^1] profile Certificate Revocation List (CRL) required /// for revocation checking. Implemented by [`OwnedCertRevocationList`] and @@ -391,6 +377,208 @@ impl<'a> IntoIterator for &'a BorrowedCertRevocationList<'a> { } } +pub(crate) struct IssuingDistributionPoint<'a> { + distribution_point: Option>, + pub(crate) only_contains_user_certs: bool, + pub(crate) only_contains_ca_certs: bool, + pub(crate) only_some_reasons: Option>, + pub(crate) indirect_crl: bool, + pub(crate) only_contains_attribute_certs: bool, +} + +impl<'a> IssuingDistributionPoint<'a> { + pub(crate) fn from_der(der: untrusted::Input<'a>) -> Result { + const DISTRIBUTION_POINT_TAG: u8 = CONTEXT_SPECIFIC | CONSTRUCTED; + const ONLY_CONTAINS_USER_CERTS_TAG: u8 = CONTEXT_SPECIFIC | 1; + const ONLY_CONTAINS_CA_CERTS_TAG: u8 = CONTEXT_SPECIFIC | 2; + const ONLY_CONTAINS_SOME_REASONS_TAG: u8 = CONTEXT_SPECIFIC | 3; + const INDIRECT_CRL_TAG: u8 = CONTEXT_SPECIFIC | 4; + const ONLY_CONTAINS_ATTRIBUTE_CERTS_TAG: u8 = CONTEXT_SPECIFIC | 5; + + let mut result = IssuingDistributionPoint { + distribution_point: None, + only_contains_user_certs: false, + only_contains_ca_certs: false, + only_some_reasons: None, + indirect_crl: false, + only_contains_attribute_certs: false, + }; + + // Note: we can't use der::optional_boolean here because the distribution point + // booleans are context specific primitives and der::optional_boolean expects + // to unwrap a Tag::Boolean constructed value. + fn decode_bool(value: untrusted::Input) -> Result { + let mut reader = untrusted::Reader::new(value); + let value = reader.read_byte()?; + if !reader.at_end() { + return Err(Error::BadDer); + } + match value { + 0xFF => Ok(true), + 0x00 => Ok(false), // non-conformant explicit encoding allowed for compat. + _ => Err(Error::BadDer), + } + } + + // RFC 5280 section §4.2.1.13: + der::nested( + &mut untrusted::Reader::new(der), + Tag::Sequence, + Error::TrailingData(DerTypeId::IssuingDistributionPoint), + |der| { + while !der.at_end() { + let (tag, value) = der::read_tag_and_get_value(der)?; + match tag { + DISTRIBUTION_POINT_TAG => { + set_extension_once(&mut result.distribution_point, || Ok(value))? + } + ONLY_CONTAINS_USER_CERTS_TAG => { + result.only_contains_user_certs = decode_bool(value)? + } + ONLY_CONTAINS_CA_CERTS_TAG => { + result.only_contains_ca_certs = decode_bool(value)? + } + ONLY_CONTAINS_SOME_REASONS_TAG => { + set_extension_once(&mut result.only_some_reasons, || { + der::bit_string_flags(value) + })? + } + INDIRECT_CRL_TAG => result.indirect_crl = decode_bool(value)?, + ONLY_CONTAINS_ATTRIBUTE_CERTS_TAG => { + result.only_contains_attribute_certs = decode_bool(value)? + } + _ => return Err(Error::BadDer), + } + } + + Ok(()) + }, + )?; + + // RFC 5280 4.2.1.10: + // Conforming CRLs issuers MUST set the onlyContainsAttributeCerts boolean to FALSE. + if result.only_contains_attribute_certs { + return Err(Error::MalformedExtensions); + } + + // We don't support indirect CRLs. + if result.indirect_crl { + return Err(Error::UnsupportedIndirectCrl); + } + + // We don't support CRLs partitioned by revocation reason. + if result.only_some_reasons.is_some() { + return Err(Error::UnsupportedRevocationReasonsPartitioning); + } + + // We require a distribution point, and it must be a full name. + use DistributionPointName::*; + match result.names() { + Ok(Some(FullName(_))) => Ok(result), + Ok(Some(NameRelativeToCrlIssuer(_))) | Ok(None) => { + Err(Error::UnsupportedCrlIssuingDistributionPoint) + } + Err(_) => Err(Error::MalformedExtensions), + } + } + + /// Return the distribution point names (if any). + pub(crate) fn names(&self) -> Result>, Error> { + self.distribution_point + .map(|input| DistributionPointName::from_der(&mut untrusted::Reader::new(input))) + .transpose() + } + + /// Returns true if the CRL can be considered authoritative for the given certificate. We make + /// this determination using the certificate and CRL issuers, and the distribution point names + /// that may be present in extensions found on both. + /// + /// We consider the CRL authoritative for the certificate if the CRL issuing distribution point + /// has a scope that could include the cert and if the cert has CRL distribution points, that + /// at least one CRL DP has a valid distribution point full name where one of the general names + /// is a Uniform Resource Identifier (URI) general name that can also be found in the CRL + /// issuing distribution point. + /// + /// We do not consider: + /// * Distribution point names relative to an issuer. + /// * General names of a type other than URI. + /// * Malformed names or invalid IDP or CRL DP extensions. + pub(crate) fn authoritative_for(&self, node: &PathNode<'a>) -> bool { + assert!(!self.only_contains_attribute_certs); // We check this at time of parse. + + // Check that the scope of the CRL issuing distribution point could include the cert. + if self.only_contains_ca_certs && node.issued.is_none() + || self.only_contains_user_certs && node.issued.is_some() + { + return false; + } + + let cert_dps = match node.cert.crl_distribution_points() { + // If the certificate has no distribution points, then the CRL can be authoritative + // based on the issuer matching and the scope including the cert. + None => return true, + Some(cert_dps) => cert_dps, + }; + + let mut idp_general_names = match self.names() { + Ok(Some(DistributionPointName::FullName(general_names))) => general_names, + _ => return false, // Note: Either no full names, or malformed. Shouldn't occur, we check at CRL parse time. + }; + + for cert_dp in cert_dps { + let cert_dp = match cert_dp { + Ok(cert_dp) => cert_dp, + // certificate CRL DP was invalid, can't match. + Err(_) => return false, + }; + + // If the certificate CRL DP was for an indirect CRL, or a CRL + // sharded by revocation reason, it can't match. + if cert_dp.crl_issuer.is_some() || cert_dp.reasons.is_some() { + return false; + } + + let mut dp_general_names = match cert_dp.names() { + Ok(Some(DistributionPointName::FullName(general_names))) => general_names, + _ => return false, // Either no full names, or malformed. + }; + + // At least one URI type name in the IDP full names must match a URI type name in the + // DP full names. + if Self::uri_name_in_common(&mut idp_general_names, &mut dp_general_names) { + return true; + } + } + + false + } + + fn uri_name_in_common( + idp_general_names: &mut DerIterator<'a, GeneralName<'a>>, + dp_general_names: &mut DerIterator<'a, GeneralName<'a>>, + ) -> bool { + use GeneralName::UniformResourceIdentifier; + for name in idp_general_names.flatten() { + let uri = match name { + UniformResourceIdentifier(uri) => uri, + _ => continue, + }; + + for other_name in (&mut *dp_general_names).flatten() { + match other_name { + UniformResourceIdentifier(other_uri) + if uri.as_slice_less_safe() == other_uri.as_slice_less_safe() => + { + return true + } + _ => continue, + } + } + } + false + } +} + /// Owned representation of a RFC 5280[^1] profile Certificate Revocation List (CRL) revoked /// certificate entry. /// @@ -645,306 +833,28 @@ impl TryFrom for RevocationReason { } } -pub(crate) struct IssuingDistributionPoint<'a> { - distribution_point: Option>, - pub(crate) only_contains_user_certs: bool, - pub(crate) only_contains_ca_certs: bool, - pub(crate) only_some_reasons: Option>, - pub(crate) indirect_crl: bool, - pub(crate) only_contains_attribute_certs: bool, -} +#[cfg(feature = "alloc")] +#[cfg(test)] +mod tests { + use super::*; + use crate::cert::Cert; -impl<'a> IssuingDistributionPoint<'a> { - pub(crate) fn from_der(der: untrusted::Input<'a>) -> Result { - const DISTRIBUTION_POINT_TAG: u8 = CONTEXT_SPECIFIC | CONSTRUCTED; - const ONLY_CONTAINS_USER_CERTS_TAG: u8 = CONTEXT_SPECIFIC | 1; - const ONLY_CONTAINS_CA_CERTS_TAG: u8 = CONTEXT_SPECIFIC | 2; - const ONLY_CONTAINS_SOME_REASONS_TAG: u8 = CONTEXT_SPECIFIC | 3; - const INDIRECT_CRL_TAG: u8 = CONTEXT_SPECIFIC | 4; - const ONLY_CONTAINS_ATTRIBUTE_CERTS_TAG: u8 = CONTEXT_SPECIFIC | 5; + #[test] + fn parse_issuing_distribution_point_ext() { + let crl = include_bytes!("../../tests/crls/crl.idp.valid.der"); + let crl = BorrowedCertRevocationList::from_der(&crl[..]).unwrap(); - let mut result = IssuingDistributionPoint { - distribution_point: None, - only_contains_user_certs: false, - only_contains_ca_certs: false, - only_some_reasons: None, - indirect_crl: false, - only_contains_attribute_certs: false, - }; + // We should be able to parse the issuing distribution point extension. + let crl_issuing_dp = crl + .issuing_distribution_point() + .expect("missing crl distribution point DER"); - // Note: we can't use der::optional_boolean here because the distribution point - // booleans are context specific primitives and der::optional_boolean expects - // to unwrap a Tag::Boolean constructed value. - fn decode_bool(value: untrusted::Input) -> Result { - let mut reader = untrusted::Reader::new(value); - let value = reader.read_byte()?; - if !reader.at_end() { - return Err(Error::BadDer); - } - match value { - 0xFF => Ok(true), - 0x00 => Ok(false), // non-conformant explicit encoding allowed for compat. - _ => Err(Error::BadDer), - } - } - - // RFC 5280 section §4.2.1.13: - der::nested( - &mut untrusted::Reader::new(der), - Tag::Sequence, - Error::TrailingData(DerTypeId::IssuingDistributionPoint), - |der| { - while !der.at_end() { - let (tag, value) = der::read_tag_and_get_value(der)?; - match tag { - DISTRIBUTION_POINT_TAG => { - set_extension_once(&mut result.distribution_point, || Ok(value))? - } - ONLY_CONTAINS_USER_CERTS_TAG => { - result.only_contains_user_certs = decode_bool(value)? - } - ONLY_CONTAINS_CA_CERTS_TAG => { - result.only_contains_ca_certs = decode_bool(value)? - } - ONLY_CONTAINS_SOME_REASONS_TAG => { - set_extension_once(&mut result.only_some_reasons, || { - der::bit_string_flags(value) - })? - } - INDIRECT_CRL_TAG => result.indirect_crl = decode_bool(value)?, - ONLY_CONTAINS_ATTRIBUTE_CERTS_TAG => { - result.only_contains_attribute_certs = decode_bool(value)? - } - _ => return Err(Error::BadDer), - } - } - - Ok(()) - }, - )?; - - // RFC 5280 4.2.1.10: - // Conforming CRLs issuers MUST set the onlyContainsAttributeCerts boolean to FALSE. - if result.only_contains_attribute_certs { - return Err(Error::MalformedExtensions); - } - - // We don't support indirect CRLs. - if result.indirect_crl { - return Err(Error::UnsupportedIndirectCrl); - } - - // We don't support CRLs partitioned by revocation reason. - if result.only_some_reasons.is_some() { - return Err(Error::UnsupportedRevocationReasonsPartitioning); - } - - // We require a distribution point, and it must be a full name. - use DistributionPointName::*; - match result.names() { - Ok(Some(FullName(_))) => Ok(result), - Ok(Some(NameRelativeToCrlIssuer(_))) | Ok(None) => { - Err(Error::UnsupportedCrlIssuingDistributionPoint) - } - Err(_) => Err(Error::MalformedExtensions), - } - } - - /// Return the distribution point names (if any). - pub(crate) fn names(&self) -> Result>, Error> { - self.distribution_point - .map(|input| DistributionPointName::from_der(&mut untrusted::Reader::new(input))) - .transpose() - } - - /// Returns true if the CRL can be considered authoritative for the given certificate. We make - /// this determination using the certificate and CRL issuers, and the distribution point names - /// that may be present in extensions found on both. - /// - /// We consider the CRL authoritative for the certificate if the CRL issuing distribution point - /// has a scope that could include the cert and if the cert has CRL distribution points, that - /// at least one CRL DP has a valid distribution point full name where one of the general names - /// is a Uniform Resource Identifier (URI) general name that can also be found in the CRL - /// issuing distribution point. - /// - /// We do not consider: - /// * Distribution point names relative to an issuer. - /// * General names of a type other than URI. - /// * Malformed names or invalid IDP or CRL DP extensions. - pub(crate) fn authoritative_for(&self, cert: &Cert<'a>) -> bool { - assert!(!self.only_contains_attribute_certs); // We check this at time of parse. - - // Check that the scope of the CRL issuing distribution point could include the cert. - if self.only_contains_ca_certs && matches!(cert.ee_or_ca, EndEntityOrCa::EndEntity) - || self.only_contains_user_certs && matches!(cert.ee_or_ca, EndEntityOrCa::Ca(_)) - { - return false; - } - - let cert_dps = match cert.crl_distribution_points() { - // If the certificate has no distribution points, then the CRL can be authoritative - // based on the issuer matching and the scope including the cert. - None => return true, - Some(cert_dps) => cert_dps, - }; - - let mut idp_general_names = match self.names() { - Ok(Some(DistributionPointName::FullName(general_names))) => general_names, - _ => return false, // Note: Either no full names, or malformed. Shouldn't occur, we check at CRL parse time. - }; - - for cert_dp in cert_dps { - let cert_dp = match cert_dp { - Ok(cert_dp) => cert_dp, - // certificate CRL DP was invalid, can't match. - Err(_) => return false, - }; - - // If the certificate CRL DP was for an indirect CRL, or a CRL - // sharded by revocation reason, it can't match. - if cert_dp.crl_issuer.is_some() || cert_dp.reasons.is_some() { - return false; - } - - let mut dp_general_names = match cert_dp.names() { - Ok(Some(DistributionPointName::FullName(general_names))) => general_names, - _ => return false, // Either no full names, or malformed. - }; - - // At least one URI type name in the IDP full names must match a URI type name in the - // DP full names. - if Self::uri_name_in_common(&mut idp_general_names, &mut dp_general_names) { - return true; - } - } - - false - } - - fn uri_name_in_common( - idp_general_names: &mut DerIterator<'a, GeneralName<'a>>, - dp_general_names: &mut DerIterator<'a, GeneralName<'a>>, - ) -> bool { - use GeneralName::UniformResourceIdentifier; - for name in idp_general_names.flatten() { - let uri = match name { - UniformResourceIdentifier(uri) => uri, - _ => continue, - }; - - for other_name in (&mut *dp_general_names).flatten() { - match other_name { - UniformResourceIdentifier(other_uri) - if uri.as_slice_less_safe() == other_uri.as_slice_less_safe() => - { - return true - } - _ => continue, - } - } - } - false - } -} - -mod private { - pub trait Sealed {} -} - -#[cfg(test)] -mod tests { - use alloc::vec::Vec; - - use crate::{ - crl::IssuingDistributionPoint, subject_name::GeneralName, x509::DistributionPointName, - BorrowedCertRevocationList, Cert, CertRevocationList, EndEntityOrCa, Error, - RevocationReason, - }; - - #[test] - fn revocation_reasons() { - // Test that we can convert the allowed u8 revocation reason code values into the expected - // revocation reason variant. - let testcases: Vec<(u8, RevocationReason)> = vec![ - (0, RevocationReason::Unspecified), - (1, RevocationReason::KeyCompromise), - (2, RevocationReason::CaCompromise), - (3, RevocationReason::AffiliationChanged), - (4, RevocationReason::Superseded), - (5, RevocationReason::CessationOfOperation), - (6, RevocationReason::CertificateHold), - // Note: 7 is unused. - (8, RevocationReason::RemoveFromCrl), - (9, RevocationReason::PrivilegeWithdrawn), - (10, RevocationReason::AaCompromise), - ]; - for tc in testcases.iter() { - let (id, expected) = tc; - let actual = >::try_into(*id) - .expect("unexpected reason code conversion error"); - assert_eq!(actual, *expected); - #[cfg(feature = "alloc")] - { - // revocation reasons should be Debug. - println!("{:?}", actual); - } - } - - // Unsupported/unknown revocation reason codes should produce an error. - let res = >::try_into(7); - assert!(matches!(res, Err(Error::UnsupportedRevocationReason))); - - // The iterator should produce all possible revocation reason variants. - let expected = testcases - .iter() - .map(|(_, reason)| *reason) - .collect::>(); - let actual = RevocationReason::iter().collect::>(); - assert_eq!(actual, expected); - } - - #[test] - #[cfg(feature = "alloc")] - // redundant clone, clone_on_copy allowed to verify derived traits. - #[allow(clippy::redundant_clone, clippy::clone_on_copy)] - fn test_derived_traits() { - let crl = crate::crl::BorrowedCertRevocationList::from_der(include_bytes!( - "../tests/crls/crl.valid.der" - )) - .unwrap(); - println!("{:?}", crl); // BorrowedCertRevocationList should be debug. - - let owned_crl = crl.to_owned().unwrap(); - println!("{:?}", owned_crl); // OwnedCertRevocationList should be debug. - let _ = owned_crl.clone(); // OwnedCertRevocationList should be clone. - - let mut revoked_certs = crl.into_iter(); - println!("{:?}", revoked_certs); // RevokedCert should be debug. - - let revoked_cert = revoked_certs.next().unwrap().unwrap(); - println!("{:?}", revoked_cert); // BorrowedRevokedCert should be debug. - - let owned_revoked_cert = revoked_cert.to_owned(); - println!("{:?}", owned_revoked_cert); // OwnedRevokedCert should be debug. - let _ = owned_revoked_cert.clone(); // OwnedRevokedCert should be clone. - } - - #[test] - fn parse_issuing_distribution_point_ext() { - let crl = include_bytes!("../tests/crls/crl.idp.valid.der"); - let crl = BorrowedCertRevocationList::from_der(&crl[..]).unwrap(); - - // We should be able to parse the issuing distribution point extension. - let crl_issuing_dp = crl - .issuing_distribution_point() - .expect("missing crl distribution point DER"); - - #[cfg(feature = "alloc")] - { - // We should also be able to find the distribution point extensions bytes from - // an owned representation of the CRL. - let owned_crl = crl.to_owned().unwrap(); - assert!(owned_crl.issuing_distribution_point().is_some()); + #[cfg(feature = "alloc")] + { + // We should also be able to find the distribution point extensions bytes from + // an owned representation of the CRL. + let owned_crl = crl.to_owned().unwrap(); + assert!(owned_crl.issuing_distribution_point().is_some()); } let crl_issuing_dp = @@ -983,7 +893,7 @@ mod tests { #[test] fn test_issuing_distribution_point_only_user_certs() { - let crl = include_bytes!("../tests/crls/crl.idp.only_user_certs.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.only_user_certs.der"); let crl = BorrowedCertRevocationList::from_der(&crl[..]).unwrap(); // We should be able to parse the issuing distribution point extension. @@ -998,16 +908,22 @@ mod tests { assert!(crl_issuing_dp.only_contains_user_certs); // The IDP shouldn't be considered authoritative for a CA Cert. - let ee = include_bytes!("../tests/client_auth_revocation/no_crl_ku_chain.ee.der"); - let ee = Cert::from_der(untrusted::Input::from(&ee[..]), EndEntityOrCa::EndEntity).unwrap(); - let ca = include_bytes!("../tests/client_auth_revocation/no_crl_ku_chain.int.a.ca.der"); - let ca = Cert::from_der(untrusted::Input::from(&ca[..]), EndEntityOrCa::Ca(&ee)).unwrap(); - assert!(!crl_issuing_dp.authoritative_for(&ca)) + let ee = include_bytes!("../../tests/client_auth_revocation/no_crl_ku_chain.ee.der"); + let ee = Cert::from_der(untrusted::Input::from(&ee[..])).unwrap(); + let ca = include_bytes!("../../tests/client_auth_revocation/no_crl_ku_chain.int.a.ca.der"); + let ca = Cert::from_der(untrusted::Input::from(&ca[..])).unwrap(); + assert!(!crl_issuing_dp.authoritative_for(&PathNode { + cert: &ca, + issued: Some(&PathNode { + cert: &ee, + issued: None + }), + })) } #[test] fn test_issuing_distribution_point_only_ca_certs() { - let crl = include_bytes!("../tests/crls/crl.idp.only_ca_certs.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.only_ca_certs.der"); let crl = BorrowedCertRevocationList::from_der(&crl[..]).unwrap(); // We should be able to parse the issuing distribution point extension. @@ -1022,14 +938,17 @@ mod tests { assert!(crl_issuing_dp.only_contains_ca_certs); // The IDP shouldn't be considered authoritative for an EE Cert. - let ee = include_bytes!("../tests/client_auth_revocation/no_crl_ku_chain.ee.der"); - let ee = Cert::from_der(untrusted::Input::from(&ee[..]), EndEntityOrCa::EndEntity).unwrap(); - assert!(!crl_issuing_dp.authoritative_for(&ee)) + let ee = include_bytes!("../../tests/client_auth_revocation/no_crl_ku_chain.ee.der"); + let ee = Cert::from_der(untrusted::Input::from(&ee[..])).unwrap(); + assert!(!crl_issuing_dp.authoritative_for(&PathNode { + cert: &ee, + issued: None + })) } #[test] fn test_issuing_distribution_point_indirect() { - let crl = include_bytes!("../tests/crls/crl.idp.indirect_crl.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.indirect_crl.der"); // We should encounter an error parsing a CRL with an IDP extension that indicates it's an // indirect CRL. let result = BorrowedCertRevocationList::from_der(&crl[..]); @@ -1038,7 +957,7 @@ mod tests { #[test] fn test_issuing_distribution_only_attribute_certs() { - let crl = include_bytes!("../tests/crls/crl.idp.only_attribute_certs.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.only_attribute_certs.der"); // We should find an error when we parse a CRL with an IDP extension that indicates it only // contains attribute certs. let result = BorrowedCertRevocationList::from_der(&crl[..]); @@ -1047,7 +966,7 @@ mod tests { #[test] fn test_issuing_distribution_only_some_reasons() { - let crl = include_bytes!("../tests/crls/crl.idp.only_some_reasons.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.only_some_reasons.der"); // We should encounter an error parsing a CRL with an IDP extension that indicates it's // partitioned by revocation reason. let result = BorrowedCertRevocationList::from_der(&crl[..]); @@ -1061,7 +980,7 @@ mod tests { fn test_issuing_distribution_invalid_bool() { // Created w/ // ascii2der -i tests/crls/crl.idp.invalid.bool.der.txt -o tests/crls/crl.idp.invalid.bool.der - let crl = include_bytes!("../tests/crls/crl.idp.invalid.bool.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.invalid.bool.der"); // We should encounter an error parsing a CRL with an IDP extension with an invalid encoded boolean. let result = BorrowedCertRevocationList::from_der(&crl[..]); assert!(matches!(result, Err(Error::BadDer))) @@ -1071,7 +990,7 @@ mod tests { fn test_issuing_distribution_explicit_false_bool() { // Created w/ // ascii2der -i tests/crls/crl.idp.explicit.false.bool.der.txt -o tests/crls/crl.idp.explicit.false.bool.der - let crl = include_bytes!("../tests/crls/crl.idp.explicit.false.bool.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.explicit.false.bool.der"); let crl = BorrowedCertRevocationList::from_der(&crl[..]).unwrap(); // We should be able to parse the issuing distribution point extension. @@ -1085,7 +1004,7 @@ mod tests { fn test_issuing_distribution_unknown_tag() { // Created w/ // ascii2der -i tests/crls/crl.idp.unknown.tag.der.txt -o tests/crls/crl.idp.unknown.tag.der - let crl = include_bytes!("../tests/crls/crl.idp.unknown.tag.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.unknown.tag.der"); // We should encounter an error parsing a CRL with an invalid IDP extension. let result = BorrowedCertRevocationList::from_der(&crl[..]); assert!(matches!(result, Err(Error::BadDer))); @@ -1095,7 +1014,7 @@ mod tests { fn test_issuing_distribution_invalid_name() { // Created w/ // ascii2der -i tests/crls/crl.idp.invalid.name.der.txt -o tests/crls/crl.idp.invalid.name.der - let crl = include_bytes!("../tests/crls/crl.idp.invalid.name.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.invalid.name.der"); // We should encounter an error parsing a CRL with an invalid issuing distribution point name. let result = BorrowedCertRevocationList::from_der(&crl[..]); @@ -1104,7 +1023,7 @@ mod tests { #[test] fn test_issuing_distribution_relative_name() { - let crl = include_bytes!("../tests/crls/crl.idp.name_relative_to_issuer.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.name_relative_to_issuer.der"); // We should encounter an error parsing a CRL with an issuing distribution point extension // that has a distribution point name relative to an issuer. let result = BorrowedCertRevocationList::from_der(&crl[..]); @@ -1116,7 +1035,7 @@ mod tests { #[test] fn test_issuing_distribution_no_name() { - let crl = include_bytes!("../tests/crls/crl.idp.no_distribution_point_name.der"); + let crl = include_bytes!("../../tests/crls/crl.idp.no_distribution_point_name.der"); // We should encounter an error parsing a CRL with an issuing distribution point extension // that has no distribution point name. let result = BorrowedCertRevocationList::from_der(&crl[..]); @@ -1125,4 +1044,71 @@ mod tests { Err(Error::UnsupportedCrlIssuingDistributionPoint) )) } + + #[test] + fn revocation_reasons() { + // Test that we can convert the allowed u8 revocation reason code values into the expected + // revocation reason variant. + let testcases: Vec<(u8, RevocationReason)> = vec![ + (0, RevocationReason::Unspecified), + (1, RevocationReason::KeyCompromise), + (2, RevocationReason::CaCompromise), + (3, RevocationReason::AffiliationChanged), + (4, RevocationReason::Superseded), + (5, RevocationReason::CessationOfOperation), + (6, RevocationReason::CertificateHold), + // Note: 7 is unused. + (8, RevocationReason::RemoveFromCrl), + (9, RevocationReason::PrivilegeWithdrawn), + (10, RevocationReason::AaCompromise), + ]; + for tc in testcases.iter() { + let (id, expected) = tc; + let actual = >::try_into(*id) + .expect("unexpected reason code conversion error"); + assert_eq!(actual, *expected); + #[cfg(feature = "alloc")] + { + // revocation reasons should be Debug. + println!("{:?}", actual); + } + } + + // Unsupported/unknown revocation reason codes should produce an error. + let res = >::try_into(7); + assert!(matches!(res, Err(Error::UnsupportedRevocationReason))); + + // The iterator should produce all possible revocation reason variants. + let expected = testcases + .iter() + .map(|(_, reason)| *reason) + .collect::>(); + let actual = RevocationReason::iter().collect::>(); + assert_eq!(actual, expected); + } + + #[test] + // redundant clone, clone_on_copy allowed to verify derived traits. + #[allow(clippy::redundant_clone, clippy::clone_on_copy)] + fn test_derived_traits() { + let crl = crate::crl::BorrowedCertRevocationList::from_der(include_bytes!( + "../../tests/crls/crl.valid.der" + )) + .unwrap(); + println!("{:?}", crl); // BorrowedCertRevocationList should be debug. + + let owned_crl = crl.to_owned().unwrap(); + println!("{:?}", owned_crl); // OwnedCertRevocationList should be debug. + let _ = owned_crl.clone(); // OwnedCertRevocationList should be clone. + + let mut revoked_certs = crl.into_iter(); + println!("{:?}", revoked_certs); // RevokedCert should be debug. + + let revoked_cert = revoked_certs.next().unwrap().unwrap(); + println!("{:?}", revoked_cert); // BorrowedRevokedCert should be debug. + + let owned_revoked_cert = revoked_cert.to_owned(); + println!("{:?}", owned_revoked_cert); // OwnedRevokedCert should be debug. + let _ = owned_revoked_cert.clone(); // OwnedRevokedCert should be clone. + } } diff --git a/src/end_entity.rs b/src/end_entity.rs index ab454bf2..a33d762b 100644 --- a/src/end_entity.rs +++ b/src/end_entity.rs @@ -14,12 +14,10 @@ use pki_types::{CertificateDer, SignatureVerificationAlgorithm, TrustAnchor}; +use crate::crl::RevocationOptions; #[cfg(feature = "alloc")] use crate::subject_name::GeneralDnsNameRef; -use crate::{ - cert, signed_data, subject_name, verify_cert, Error, KeyUsage, RevocationOptions, - SubjectNameRef, Time, -}; +use crate::{cert, signed_data, subject_name, verify_cert, Error, KeyUsage, SubjectNameRef, Time}; /// An end-entity certificate. /// @@ -63,10 +61,7 @@ impl<'a> TryFrom<&'a CertificateDer<'a>> for EndEntityCert<'a> { /// `cert_der`. fn try_from(cert: &'a CertificateDer<'a>) -> Result { Ok(Self { - inner: cert::Cert::from_der( - untrusted::Input::from(cert.as_ref()), - cert::EndEntityOrCa::EndEntity, - )?, + inner: cert::Cert::from_der(untrusted::Input::from(cert.as_ref()))?, }) } } @@ -100,17 +95,14 @@ impl<'a> EndEntityCert<'a> { usage: KeyUsage, revocation: Option, ) -> Result<(), Error> { - verify_cert::build_chain( - &verify_cert::ChainOptions { - eku: usage, - supported_sig_algs, - trust_anchors, - intermediate_certs, - revocation, - }, - &self.inner, - time, - ) + verify_cert::ChainOptions { + eku: usage, + supported_sig_algs, + trust_anchors, + intermediate_certs, + revocation, + } + .build_chain(&self.inner, time) } /// Verifies that the certificate is valid for the given Subject Name. diff --git a/src/lib.rs b/src/lib.rs index 507b547d..7d2101e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,8 +64,11 @@ mod x509; pub(crate) mod test_utils; pub use { - cert::{Cert, EndEntityOrCa}, - crl::{BorrowedCertRevocationList, BorrowedRevokedCert, CertRevocationList, RevocationReason}, + cert::Cert, + crl::{ + BorrowedCertRevocationList, BorrowedRevokedCert, CertRevocationList, RevocationCheckDepth, + RevocationOptions, RevocationOptionsBuilder, RevocationReason, UnknownStatusPolicy, + }, end_entity::EndEntityCert, error::{DerTypeId, Error}, signed_data::alg_id, @@ -75,10 +78,7 @@ pub use { }, time::Time, trust_anchor::extract_trust_anchor, - verify_cert::{ - KeyUsage, RevocationCheckDepth, RevocationOptions, RevocationOptionsBuilder, - UnknownStatusPolicy, - }, + verify_cert::KeyUsage, }; pub use pki_types as types; diff --git a/src/subject_name/verify.rs b/src/subject_name/verify.rs index a0616a00..660ebadc 100644 --- a/src/subject_name/verify.rs +++ b/src/subject_name/verify.rs @@ -20,10 +20,9 @@ use super::dns_name::{self, DnsNameRef}; use super::dns_name::{GeneralDnsNameRef, WildcardDnsNameRef}; use super::ip_address::{self, IpAddrRef}; use super::name::SubjectNameRef; -use crate::cert::{Cert, EndEntityOrCa}; use crate::der::{self, FromDer}; use crate::error::{DerTypeId, Error}; -use crate::verify_cert::Budget; +use crate::verify_cert::{Budget, PathNode}; pub(crate) fn verify_cert_dns_name( cert: &crate::EndEntityCert, @@ -94,7 +93,7 @@ pub(crate) fn verify_cert_subject_name( // https://tools.ietf.org/html/rfc5280#section-4.2.1.10 pub(crate) fn check_name_constraints( constraints: Option<&mut untrusted::Reader>, - subordinate_certs: &Cert, + path: &PathNode<'_>, budget: &mut Budget, ) -> Result<(), Error> { let constraints = match constraints { @@ -115,10 +114,9 @@ pub(crate) fn check_name_constraints( let permitted_subtrees = parse_subtrees(constraints, der::Tag::ContextSpecificConstructed0)?; let excluded_subtrees = parse_subtrees(constraints, der::Tag::ContextSpecificConstructed1)?; - let mut child = subordinate_certs; - loop { - let result = - NameIterator::new(Some(child.subject), child.subject_alt_name).find_map(|result| { + for path in path.iter() { + let result = NameIterator::new(Some(path.cert.subject), path.cert.subject_alt_name) + .find_map(|result| { let name = match result { Ok(name) => name, Err(err) => return Some(Err(err)), @@ -135,13 +133,6 @@ pub(crate) fn check_name_constraints( if let Some(Err(err)) = result { return Err(err); } - - child = match child.ee_or_ca { - EndEntityOrCa::Ca(child_cert) => child_cert, - EndEntityOrCa::EndEntity => { - break; - } - }; } Ok(()) diff --git a/src/trust_anchor.rs b/src/trust_anchor.rs index 888b1c00..6a46e75f 100644 --- a/src/trust_anchor.rs +++ b/src/trust_anchor.rs @@ -1,6 +1,6 @@ use pki_types::{CertificateDer, TrustAnchor}; -use crate::cert::{lenient_certificate_serial_number, Cert, EndEntityOrCa}; +use crate::cert::{lenient_certificate_serial_number, Cert}; use crate::der; use crate::error::{DerTypeId, Error}; @@ -20,7 +20,7 @@ pub fn extract_trust_anchor<'a>(cert: &'a CertificateDer<'a>) -> Result Ok(TrustAnchor::from(cert)), Err(Error::UnsupportedCertVersion) => { extract_trust_anchor_from_v1_cert_der(cert_der).or(Err(Error::BadDer)) diff --git a/src/verify_cert.rs b/src/verify_cert.rs index afa037aa..9a327ed1 100644 --- a/src/verify_cert.rs +++ b/src/verify_cert.rs @@ -17,110 +17,10 @@ use core::ops::ControlFlow; use pki_types::{CertificateDer, SignatureVerificationAlgorithm, TrustAnchor}; -use crate::cert::{Cert, EndEntityOrCa}; -use crate::crl::IssuingDistributionPoint; +use crate::cert::Cert; +use crate::crl::RevocationOptions; use crate::der::{self, FromDer}; -use crate::{signed_data, subject_name, time, CertRevocationList, Error}; - -/// Builds a RevocationOptions instance to control how revocation checking is performed. -#[derive(Debug, Copy, Clone)] -pub struct RevocationOptionsBuilder<'a> { - crls: &'a [&'a dyn CertRevocationList], - - depth: RevocationCheckDepth, - - status_requirement: UnknownStatusPolicy, -} - -impl<'a> RevocationOptionsBuilder<'a> { - /// Create a builder that will perform revocation checking using the provided certificate - /// revocation lists (CRLs). At least one CRL must be provided. - /// - /// Use [RevocationOptionsBuilder::build] to create a [RevocationOptions] instance. - /// - /// By default revocation checking will be performed on both the end-entity (leaf) certificate - /// and intermediate certificates. This can be customized using the - /// [RevocationOptionsBuilder::with_depth] method. - /// - /// By default revocation checking will fail if the revocation status of a certificate cannot - /// be determined. This can be customized using the - /// [RevocationOptionsBuilder::allow_unknown_status] method. - pub fn new(crls: &'a [&'a dyn CertRevocationList]) -> Result { - if crls.is_empty() { - return Err(CrlsRequired(())); - } - - Ok(Self { - crls, - depth: RevocationCheckDepth::Chain, - status_requirement: UnknownStatusPolicy::Deny, - }) - } - - /// Customize the depth at which revocation checking will be performed, controlling - /// whether only the end-entity (leaf) certificate in the chain to a trust anchor will - /// have its revocation status checked, or whether the intermediate certificates will as well. - pub fn with_depth(mut self, depth: RevocationCheckDepth) -> Self { - self.depth = depth; - self - } - - /// Treat unknown revocation status permissively, acting as if the certificate were not - /// revoked. - pub fn allow_unknown_status(mut self) -> Self { - self.status_requirement = UnknownStatusPolicy::Allow; - self - } - - /// Treat unknown revocation status strictly, considering it an error condition. - pub fn forbid_unknown_status(mut self) -> Self { - self.status_requirement = UnknownStatusPolicy::Deny; - self - } - - /// Construct a [RevocationOptions] instance based on the builder's configuration. - pub fn build(self) -> RevocationOptions<'a> { - RevocationOptions { - crls: self.crls, - depth: self.depth, - status_requirement: self.status_requirement, - } - } -} - -/// Describes how revocation checking is performed, if at all. Can be constructed with a -/// [RevocationOptionsBuilder] instance. -#[derive(Debug, Copy, Clone)] -pub struct RevocationOptions<'a> { - crls: &'a [&'a dyn CertRevocationList], - depth: RevocationCheckDepth, - status_requirement: UnknownStatusPolicy, -} - -/// Describes how much of a certificate chain is checked for revocation status. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum RevocationCheckDepth { - /// Only check the end entity (leaf) certificate's revocation status. - EndEntity, - /// Check the revocation status of the end entity (leaf) and all intermediates. - Chain, -} - -/// Describes how to handle the case where a certificate's revocation status is unknown. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum UnknownStatusPolicy { - /// Treat unknown revocation status permissively, acting as if the certificate were - /// not revoked. - Allow, - /// Treat unknown revocation status as an error condition, yielding - /// [Error::UnknownRevocationStatus]. - Deny, -} - -#[derive(Debug, Copy, Clone)] -/// An opaque error indicating the caller must provide at least one CRL when building a -/// [RevocationOptions] instance. -pub struct CrlsRequired(()); +use crate::{signed_data, subject_name, time, Error}; pub(crate) struct ChainOptions<'a> { pub(crate) eku: KeyUsage, @@ -130,175 +30,152 @@ pub(crate) struct ChainOptions<'a> { pub(crate) revocation: Option>, } -pub(crate) fn build_chain(opts: &ChainOptions, cert: &Cert, time: time::Time) -> Result<(), Error> { - build_chain_inner(opts, cert, time, 0, &mut Budget::default()).map_err(|e| match e { - ControlFlow::Break(err) => err, - ControlFlow::Continue(err) => err, - }) -} +impl<'a> ChainOptions<'a> { + pub(crate) fn build_chain(&self, cert: &Cert<'_>, time: time::Time) -> Result<(), Error> { + let path = PathNode { cert, issued: None }; + self.build_chain_inner(&path, time, 0, &mut Budget::default()) + .map_err(|e| match e { + ControlFlow::Break(err) => err, + ControlFlow::Continue(err) => err, + }) + } -fn build_chain_inner( - opts: &ChainOptions, - cert: &Cert, - time: time::Time, - sub_ca_count: usize, - budget: &mut Budget, -) -> Result<(), ControlFlow> { - let used_as_ca = used_as_ca(&cert.ee_or_ca); + fn build_chain_inner( + &self, + path: &PathNode<'_>, + time: time::Time, + sub_ca_count: usize, + budget: &mut Budget, + ) -> Result<(), ControlFlow> { + let role = path.role(); - check_issuer_independent_properties(cert, time, used_as_ca, sub_ca_count, opts.eku.inner)?; + check_issuer_independent_properties(path.cert, time, role, sub_ca_count, self.eku.inner)?; - // TODO: HPKP checks. + // TODO: HPKP checks. - match used_as_ca { - UsedAsCa::Yes => { - const MAX_SUB_CA_COUNT: usize = 6; + match role { + Role::Issuer => { + const MAX_SUB_CA_COUNT: usize = 6; - if sub_ca_count >= MAX_SUB_CA_COUNT { - return Err(Error::MaximumPathDepthExceeded.into()); + if sub_ca_count >= MAX_SUB_CA_COUNT { + return Err(Error::MaximumPathDepthExceeded.into()); + } } - } - UsedAsCa::No => { - assert_eq!(0, sub_ca_count); - } - } - - let result = loop_while_non_fatal_error( - Error::UnknownIssuer, - opts.trust_anchors, - |trust_anchor: &TrustAnchor| { - let trust_anchor_subject = untrusted::Input::from(trust_anchor.subject.as_ref()); - if cert.issuer != trust_anchor_subject { - return Err(Error::UnknownIssuer.into()); + Role::EndEntity => { + assert_eq!(0, sub_ca_count); } + } - // TODO: check_distrust(trust_anchor_subject, trust_anchor_spki)?; - - check_signed_chain( - opts.supported_sig_algs, - cert, - trust_anchor, - opts.revocation, - budget, - )?; + let result = loop_while_non_fatal_error( + Error::UnknownIssuer, + self.trust_anchors, + |trust_anchor: &TrustAnchor| { + let trust_anchor_subject = untrusted::Input::from(trust_anchor.subject.as_ref()); + if path.cert.issuer != trust_anchor_subject { + return Err(Error::UnknownIssuer.into()); + } - check_signed_chain_name_constraints(cert, trust_anchor, budget)?; + // TODO: check_distrust(trust_anchor_subject, trust_anchor_spki)?; - Ok(()) - }, - ); + self.check_signed_chain(path, trust_anchor, budget)?; - let err = match result { - Ok(()) => return Ok(()), - // Fatal errors should halt further path building. - res @ Err(ControlFlow::Break(_)) => return res, - // Non-fatal errors should be carried forward as the default_error for subsequent - // loop_while_non_fatal_error processing and only returned once all other path-building - // options have been exhausted. - Err(ControlFlow::Continue(err)) => err, - }; + check_signed_chain_name_constraints(path, trust_anchor, budget)?; - loop_while_non_fatal_error(err, opts.intermediate_certs, |cert_der| { - let potential_issuer = - Cert::from_der(untrusted::Input::from(cert_der), EndEntityOrCa::Ca(cert))?; + Ok(()) + }, + ); - if potential_issuer.subject != cert.issuer { - return Err(Error::UnknownIssuer.into()); - } + let err = match result { + Ok(()) => return Ok(()), + // Fatal errors should halt further path building. + res @ Err(ControlFlow::Break(_)) => return res, + // Non-fatal errors should be carried forward as the default_error for subsequent + // loop_while_non_fatal_error processing and only returned once all other path-building + // options have been exhausted. + Err(ControlFlow::Continue(err)) => err, + }; - // Prevent loops; see RFC 4158 section 5.2. - let mut prev = cert; - loop { - if potential_issuer.spki == prev.spki && potential_issuer.subject == prev.subject { + loop_while_non_fatal_error(err, self.intermediate_certs, |cert_der| { + let potential_issuer = Cert::from_der(untrusted::Input::from(cert_der))?; + if potential_issuer.subject != path.cert.issuer { return Err(Error::UnknownIssuer.into()); } - match &prev.ee_or_ca { - EndEntityOrCa::EndEntity => { - break; - } - EndEntityOrCa::Ca(child_cert) => { - prev = child_cert; - } + + // Prevent loops; see RFC 4158 section 5.2. + if path.iter().any(|prev| { + potential_issuer.spki == prev.cert.spki + && potential_issuer.subject == prev.cert.subject + }) { + return Err(Error::UnknownIssuer.into()); } - } - let next_sub_ca_count = match used_as_ca { - UsedAsCa::No => sub_ca_count, - UsedAsCa::Yes => sub_ca_count + 1, - }; + let next_sub_ca_count = match role { + Role::EndEntity => sub_ca_count, + Role::Issuer => sub_ca_count + 1, + }; - budget.consume_build_chain_call()?; - build_chain_inner(opts, &potential_issuer, time, next_sub_ca_count, budget) - }) -} + budget.consume_build_chain_call()?; + let potential_path = PathNode { + cert: &potential_issuer, + issued: Some(path), + }; + self.build_chain_inner(&potential_path, time, next_sub_ca_count, budget) + }) + } -fn check_signed_chain( - supported_sig_algs: &[&dyn SignatureVerificationAlgorithm], - cert_chain: &Cert, - trust_anchor: &TrustAnchor, - revocation: Option, - budget: &mut Budget, -) -> Result<(), ControlFlow> { - let mut spki_value = untrusted::Input::from(trust_anchor.subject_public_key_info.as_ref()); - let mut issuer_subject = untrusted::Input::from(trust_anchor.subject.as_ref()); - let mut issuer_key_usage = None; // TODO(XXX): Consider whether to track TrustAnchor KU. - let mut cert = cert_chain; - loop { - signed_data::verify_signed_data(supported_sig_algs, spki_value, &cert.signed_data, budget)?; - - if let Some(revocation_opts) = &revocation { - check_crls( - supported_sig_algs, - cert, - issuer_subject, + fn check_signed_chain( + &self, + path: &PathNode<'_>, + trust_anchor: &TrustAnchor, + budget: &mut Budget, + ) -> Result<(), ControlFlow> { + let mut spki_value = untrusted::Input::from(trust_anchor.subject_public_key_info.as_ref()); + let mut issuer_subject = untrusted::Input::from(trust_anchor.subject.as_ref()); + let mut issuer_key_usage = None; // TODO(XXX): Consider whether to track TrustAnchor KU. + for path in path.iter() { + signed_data::verify_signed_data( + self.supported_sig_algs, spki_value, - issuer_key_usage, - revocation_opts, + &path.cert.signed_data, budget, )?; - } - match &cert.ee_or_ca { - EndEntityOrCa::Ca(child_cert) => { - spki_value = cert.spki; - issuer_subject = cert.subject; - issuer_key_usage = cert.key_usage; - cert = child_cert; - } - EndEntityOrCa::EndEntity => { - break; + if let Some(revocation_opts) = &self.revocation { + revocation_opts.check( + path, + issuer_subject, + spki_value, + issuer_key_usage, + self.supported_sig_algs, + budget, + )?; } + + spki_value = path.cert.spki; + issuer_subject = path.cert.subject; + issuer_key_usage = path.cert.key_usage; } - } - Ok(()) + Ok(()) + } } fn check_signed_chain_name_constraints( - cert_chain: &Cert, + path: &PathNode<'_>, trust_anchor: &TrustAnchor, budget: &mut Budget, ) -> Result<(), ControlFlow> { - let mut cert = cert_chain; let mut name_constraints = trust_anchor .name_constraints .as_ref() .map(|der| untrusted::Input::from(der.as_ref())); - loop { + for path in path.iter() { untrusted::read_all_optional(name_constraints, Error::BadDer, |value| { - subject_name::check_name_constraints(value, cert, budget) + subject_name::check_name_constraints(value, path, budget) })?; - match &cert.ee_or_ca { - EndEntityOrCa::Ca(child_cert) => { - name_constraints = cert.name_constraints; - cert = child_cert; - } - EndEntityOrCa::EndEntity => { - break; - } - } + name_constraints = path.cert.name_constraints; } Ok(()) @@ -359,129 +236,10 @@ impl Default for Budget { } } -// Zero-sized marker type representing positive assertion that revocation status was checked -// for a certificate and the result was that the certificate is not revoked. -struct CertNotRevoked(()); - -impl CertNotRevoked { - // Construct a CertNotRevoked marker. - fn assertion() -> Self { - Self(()) - } -} - -fn check_crls( - supported_sig_algs: &[&dyn SignatureVerificationAlgorithm], - cert: &Cert, - issuer_subject: untrusted::Input, - issuer_spki: untrusted::Input, - issuer_ku: Option, - revocation: &RevocationOptions, - budget: &mut Budget, -) -> Result, Error> { - assert_eq!(cert.issuer, issuer_subject); - - // If the policy only specifies checking EndEntity revocation state and we're looking at an - // issuer certificate, return early without considering the certificate's revocation state. - if let (RevocationCheckDepth::EndEntity, EndEntityOrCa::Ca(_)) = - (revocation.depth, &cert.ee_or_ca) - { - return Ok(None); - } - - let crl = revocation - .crls - .iter() - .find(|candidate_crl| crl_authoritative(**candidate_crl, cert)); - - use UnknownStatusPolicy::*; - let crl = match (crl, revocation.status_requirement) { - (Some(crl), _) => crl, - // If the policy allows unknown, return Ok(None) to indicate that the certificate - // was not confirmed as CertNotRevoked, but that this isn't an error condition. - (None, Allow) => return Ok(None), - // Otherwise, this is an error condition based on the provided policy. - (None, _) => return Err(Error::UnknownRevocationStatus), - }; - - // Verify the CRL signature with the issuer SPKI. - // TODO(XXX): consider whether we can refactor so this happens once up-front, instead - // of per-lookup. - // https://github.com/rustls/webpki/issues/81 - crl.verify_signature(supported_sig_algs, issuer_spki.as_slice_less_safe(), budget) - .map_err(crl_signature_err)?; - - // Verify that if the issuer has a KeyUsage bitstring it asserts cRLSign. - KeyUsageMode::CrlSign.check(issuer_ku)?; - - // Try to find the cert serial in the verified CRL contents. - let cert_serial = cert.serial.as_slice_less_safe(); - match crl.find_serial(cert_serial)? { - None => Ok(Some(CertNotRevoked::assertion())), - Some(_) => Err(Error::CertRevoked), - } -} - -/// Returns true if the CRL can be considered authoritative for the given certificate. -/// -/// A CRL is considered authoritative for a certificate when: -/// * The certificate issuer matches the CRL issuer and, -/// * The certificate has no CRL distribution points, and the CRL has no issuing distribution -/// point extension. -/// * Or, the certificate has no CRL distribution points, but the the CRL has an issuing -/// distribution point extension with a scope that includes the certificate. -/// * Or, the certificate has CRL distribution points, and the CRL has an issuing -/// distribution point extension with a scope that includes the certificate, and at least -/// one distribution point full name is a URI type general name that can also be found in -/// the CRL issuing distribution point full name general name sequence. -/// -/// In all other circumstances the CRL is not considered authoritative. -fn crl_authoritative(crl: &dyn CertRevocationList, cert: &Cert<'_>) -> bool { - // In all cases we require that the authoritative CRL have the same issuer - // as the certificate. Recall we do not support indirect CRLs. - if crl.issuer() != cert.issuer() { - return false; - } - - let crl_idp = match ( - cert.crl_distribution_points(), - crl.issuing_distribution_point(), - ) { - // If the certificate has no CRL distribution points, and the CRL has no issuing distribution point, - // then we can consider this CRL authoritative based on the issuer matching. - (cert_dps, None) => return cert_dps.is_none(), - - // If the CRL has an issuing distribution point, parse it so we can consider its scope - // and compare against the cert CRL distribution points, if present. - (_, Some(crl_idp)) => { - match IssuingDistributionPoint::from_der(untrusted::Input::from(crl_idp)) { - Ok(crl_idp) => crl_idp, - Err(_) => return false, // Note: shouldn't happen - we verify IDP at CRL-load. - } - } - }; - - crl_idp.authoritative_for(cert) -} - -// When verifying CRL signed data we want to disambiguate the context of possible errors by mapping -// them to CRL specific variants that a consumer can use to tell the issue was with the CRL's -// signature, not a certificate. -fn crl_signature_err(err: Error) -> Error { - match err { - Error::UnsupportedSignatureAlgorithm => Error::UnsupportedCrlSignatureAlgorithm, - Error::UnsupportedSignatureAlgorithmForPublicKey => { - Error::UnsupportedCrlSignatureAlgorithmForPublicKey - } - Error::InvalidSignatureForPublicKey => Error::InvalidCrlSignatureForPublicKey, - _ => err, - } -} - fn check_issuer_independent_properties( cert: &Cert, time: time::Time, - used_as_ca: UsedAsCa, + role: Role, sub_ca_count: usize, eku: ExtendedKeyUsage, ) -> Result<(), Error> { @@ -499,7 +257,7 @@ fn check_issuer_independent_properties( cert.validity .read_all(Error::BadDer, |value| check_validity(value, time))?; untrusted::read_all_optional(cert.basic_constraints, Error::BadDer, |value| { - check_basic_constraints(value, used_as_ca, sub_ca_count) + check_basic_constraints(value, role, sub_ca_count) })?; untrusted::read_all_optional(cert.eku, Error::BadDer, |value| eku.check(value))?; @@ -528,23 +286,10 @@ fn check_validity(input: &mut untrusted::Reader, time: time::Time) -> Result<(), Ok(()) } -#[derive(Clone, Copy, PartialEq)] -enum UsedAsCa { - Yes, - No, -} - -fn used_as_ca(ee_or_ca: &EndEntityOrCa) -> UsedAsCa { - match ee_or_ca { - EndEntityOrCa::EndEntity => UsedAsCa::No, - EndEntityOrCa::Ca(..) => UsedAsCa::Yes, - } -} - // https://tools.ietf.org/html/rfc5280#section-4.2.1.9 fn check_basic_constraints( input: Option<&mut untrusted::Reader>, - used_as_ca: UsedAsCa, + role: Role, sub_ca_count: usize, ) -> Result<(), Error> { let (is_ca, path_len_constraint) = match input { @@ -566,10 +311,10 @@ fn check_basic_constraints( None => (false, None), }; - match (used_as_ca, is_ca, path_len_constraint) { - (UsedAsCa::No, true, _) => Err(Error::CaUsedAsEndEntity), - (UsedAsCa::Yes, false, _) => Err(Error::EndEntityUsedAsCa), - (UsedAsCa::Yes, true, Some(len)) if sub_ca_count > len => { + match (role, is_ca, path_len_constraint) { + (Role::EndEntity, true, _) => Err(Error::CaUsedAsEndEntity), + (Role::Issuer, false, _) => Err(Error::EndEntityUsedAsCa), + (Role::Issuer, true, Some(len)) if sub_ca_count > len => { Err(Error::PathLenConstraintViolated) } _ => Ok(()), @@ -686,43 +431,6 @@ const EKU_SERVER_AUTH: KeyPurposeId = KeyPurposeId::new(&oid!(1, 3, 6, 1, 5, 5, // id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 } const EKU_CLIENT_AUTH: KeyPurposeId = KeyPurposeId::new(&oid!(1, 3, 6, 1, 5, 5, 7, 3, 2)); -// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3 -#[repr(u8)] -#[derive(Clone, Copy)] -enum KeyUsageMode { - // DigitalSignature = 0, - // ContentCommitment = 1, - // KeyEncipherment = 2, - // DataEncipherment = 3, - // KeyAgreement = 4, - // CertSign = 5, - CrlSign = 6, - // EncipherOnly = 7, - // DecipherOnly = 8, -} - -impl KeyUsageMode { - // https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3 - fn check(self, input: Option) -> Result<(), Error> { - let bit_string = match input { - Some(input) => { - der::expect_tag(&mut untrusted::Reader::new(input), der::Tag::BitString)? - } - // While RFC 5280 requires KeyUsage be present, historically the absence of a KeyUsage - // has been treated as "Any Usage". We follow that convention here and assume the absence - // of KeyUsage implies the required_ku_bit_if_present we're checking for. - None => return Ok(()), - }; - - let flags = der::bit_string_flags(bit_string)?; - #[allow(clippy::as_conversions)] // u8 always fits in usize. - match flags.bit_set(self as usize) { - true => Ok(()), - false => Err(Error::IssuerNotCrlSigner), - } - } -} - fn loop_while_non_fatal_error( default_error: Error, values: V, @@ -745,122 +453,58 @@ where Err(error.into()) } -#[cfg(test)] -mod tests { - use super::*; - #[cfg(feature = "alloc")] - use crate::test_utils::{make_end_entity, make_issuer}; - use crate::BorrowedCertRevocationList; +/// A node in a [`Cert`] path, represented as a linked list from trust anchor to end-entity. +pub(crate) struct PathNode<'a> { + pub(crate) cert: &'a Cert<'a>, + /// Links to the next node in the path; this list is in trust anchor to end-entity order. + /// As such, the next node, `issued`, was issued by this node; and `issued` is `None` for the + /// last node, which thus represents the end-entity certificate. + pub(crate) issued: Option<&'a PathNode<'a>>, +} - #[test] - fn eku_key_purpose_id() { - assert!(ExtendedKeyUsage::RequiredIfPresent(EKU_SERVER_AUTH) - .key_purpose_id_equals(EKU_SERVER_AUTH.oid_value)) +impl<'a> PathNode<'a> { + pub(crate) fn iter(&'a self) -> PathNodeIter<'a> { + PathNodeIter { next: Some(self) } } - #[test] - // safe to convert BorrowedCertRevocationList to CertRevocationList. - // redundant clone, clone_on_copy allowed to verify derived traits. - #[allow(clippy::as_conversions, clippy::redundant_clone, clippy::clone_on_copy)] - fn test_revocation_opts_builder() { - // Trying to build a RevocationOptionsBuilder w/o CRLs should err. - let result = RevocationOptionsBuilder::new(&[]); - assert!(matches!(result, Err(CrlsRequired(_)))); - - // The CrlsRequired error should be debug and clone when alloc is enabled. - #[cfg(feature = "alloc")] - { - let err = result.unwrap_err(); - println!("{:?}", err.clone()); - } - - // It should be possible to build a revocation options builder with defaults. - let crl = include_bytes!("../tests/crls/crl.valid.der"); - let crl = - &BorrowedCertRevocationList::from_der(&crl[..]).unwrap() as &dyn CertRevocationList; - let crls = [crl]; - let builder = RevocationOptionsBuilder::new(&crls[..]).unwrap(); - #[cfg(feature = "alloc")] - { - // The builder should be debug, and clone when alloc is enabled - println!("{:?}", builder); - _ = builder.clone(); - } - let opts = builder.build(); - assert_eq!(opts.depth, RevocationCheckDepth::Chain); - assert_eq!(opts.status_requirement, UnknownStatusPolicy::Deny); - assert_eq!(opts.crls.len(), 1); - - // It should be possible to build a revocation options builder with custom depth. - let opts = RevocationOptionsBuilder::new(&crls[..]) - .unwrap() - .with_depth(RevocationCheckDepth::EndEntity) - .build(); - assert_eq!(opts.depth, RevocationCheckDepth::EndEntity); - assert_eq!(opts.status_requirement, UnknownStatusPolicy::Deny); - assert_eq!(opts.crls.len(), 1); - - // It should be possible to build a revocation options builder that allows unknown - // revocation status. - let opts = RevocationOptionsBuilder::new(&crls[..]) - .unwrap() - .allow_unknown_status() - .build(); - assert_eq!(opts.depth, RevocationCheckDepth::Chain); - assert_eq!(opts.status_requirement, UnknownStatusPolicy::Allow); - assert_eq!(opts.crls.len(), 1); - - // It should be possible to specify both depth and unknown status requirements together. - let opts = RevocationOptionsBuilder::new(&crls[..]) - .unwrap() - .allow_unknown_status() - .with_depth(RevocationCheckDepth::EndEntity) - .build(); - assert_eq!(opts.depth, RevocationCheckDepth::EndEntity); - assert_eq!(opts.status_requirement, UnknownStatusPolicy::Allow); - assert_eq!(opts.crls.len(), 1); - - // The same should be true for explicitly forbidding unknown status. - let opts = RevocationOptionsBuilder::new(&crls[..]) - .unwrap() - .forbid_unknown_status() - .with_depth(RevocationCheckDepth::EndEntity) - .build(); - assert_eq!(opts.depth, RevocationCheckDepth::EndEntity); - assert_eq!(opts.status_requirement, UnknownStatusPolicy::Deny); - assert_eq!(opts.crls.len(), 1); - - // Built revocation options should be debug and clone when alloc is enabled. - #[cfg(feature = "alloc")] - { - println!("{:?}", opts.clone()); + fn role(&self) -> Role { + match self.issued { + Some(_) => Role::Issuer, + None => Role::EndEntity, } } +} - #[test] - fn test_crl_authoritative_issuer_mismatch() { - let crl = include_bytes!("../tests/crls/crl.valid.der"); - let crl = BorrowedCertRevocationList::from_der(&crl[..]).unwrap(); +pub(crate) struct PathNodeIter<'a> { + next: Option<&'a PathNode<'a>>, +} - let ee = include_bytes!("../tests/client_auth_revocation/no_ku_chain.ee.der"); - let ee = Cert::from_der(untrusted::Input::from(&ee[..]), EndEntityOrCa::EndEntity).unwrap(); +impl<'a> Iterator for PathNodeIter<'a> { + type Item = &'a PathNode<'a>; - // The CRL should not be authoritative for an EE issued by a different issuer. - assert!(!crl_authoritative(&crl, &ee)); + fn next(&mut self) -> Option { + let next = self.next?; + self.next = next.issued; + Some(next) } +} - #[test] - fn test_crl_authoritative_no_idp_no_cert_dp() { - let crl = - include_bytes!("../tests/client_auth_revocation/ee_revoked_crl_ku_ee_depth.crl.der"); - let crl = BorrowedCertRevocationList::from_der(&crl[..]).unwrap(); +#[derive(Clone, Copy, PartialEq)] +enum Role { + Issuer, + EndEntity, +} - let ee = include_bytes!("../tests/client_auth_revocation/ku_chain.ee.der"); - let ee = Cert::from_der(untrusted::Input::from(&ee[..]), EndEntityOrCa::EndEntity).unwrap(); +#[cfg(test)] +mod tests { + use super::*; + #[cfg(feature = "alloc")] + use crate::test_utils::{make_end_entity, make_issuer}; - // The CRL should be considered authoritative, the issuers match, the CRL has no IDP and the - // cert has no CRL DPs. - assert!(crl_authoritative(&crl, &ee)); + #[test] + fn eku_key_purpose_id() { + assert!(ExtendedKeyUsage::RequiredIfPresent(EKU_SERVER_AUTH) + .key_purpose_id_equals(EKU_SERVER_AUTH.oid_value)) } #[cfg(feature = "alloc")] @@ -1066,15 +710,18 @@ mod tests { .map(|x| CertificateDer::from(x.as_ref())) .collect::>(); - build_chain_inner( - &ChainOptions { - eku: KeyUsage::server_auth(), - supported_sig_algs: &[ECDSA_P256_SHA256], - trust_anchors: anchors, - intermediate_certs: &intermediates_der, - revocation: None, + ChainOptions { + eku: KeyUsage::server_auth(), + supported_sig_algs: &[ECDSA_P256_SHA256], + trust_anchors: anchors, + intermediate_certs: &intermediates_der, + revocation: None, + } + .build_chain_inner( + &PathNode { + cert: cert.inner(), + issued: None, }, - cert.inner(), time, 0, &mut budget.unwrap_or_default(),