// Copyright 2015 Brian Smith. // // 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 core::default::Default; use crate::{ budget::Budget, cert::{self, Cert, EndEntityOrCa}, der, equal, error::ErrorExt, name, signed_data, time, Error, SignatureAlgorithm, TrustAnchor, }; pub fn build_chain( required_eku_if_present: KeyPurposeId, supported_sig_algs: &[&SignatureAlgorithm], trust_anchors: &[TrustAnchor], intermediate_certs: &[&[u8]], cert: &Cert, time: time::Time, ) -> Result<(), ErrorExt> { build_chain_inner( required_eku_if_present, supported_sig_algs, trust_anchors, intermediate_certs, cert, time, 0, &mut Budget::default(), ) } #[allow(clippy::too_many_arguments)] fn build_chain_inner( required_eku_if_present: KeyPurposeId, supported_sig_algs: &[&SignatureAlgorithm], trust_anchors: &[TrustAnchor], intermediate_certs: &[&[u8]], cert: &Cert, time: time::Time, sub_ca_count: usize, budget: &mut Budget, ) -> Result<(), ErrorExt> { let used_as_ca = used_as_ca(&cert.ee_or_ca); check_issuer_independent_properties( cert, time, used_as_ca, sub_ca_count, required_eku_if_present, )?; // TODO: HPKP checks. match used_as_ca { UsedAsCa::Yes => { const MAX_SUB_CA_COUNT: usize = 6; if sub_ca_count >= MAX_SUB_CA_COUNT { return Err(Error::UnknownIssuer.into()); } } UsedAsCa::No => { assert_eq!(0, sub_ca_count); } } // TODO: revocation. match loop_while_non_fatal_error(trust_anchors, |trust_anchor: &TrustAnchor| { let trust_anchor_subject = untrusted::Input::from(trust_anchor.subject); if !equal(cert.issuer, trust_anchor_subject) { return Err(Error::UnknownIssuer.into()); } let trust_anchor_spki = untrusted::Input::from(trust_anchor.spki); // TODO: check_distrust(trust_anchor_subject, trust_anchor_spki)?; check_signatures(supported_sig_algs, cert, trust_anchor_spki, budget)?; check_signed_chain_name_constraints(cert, trust_anchor)?; Ok(()) }) { Ok(()) => { return Ok(()); } Err(e) => { if e.is_fatal() { return Err(e); } // If the error is not fatal, then keep going. } } loop_while_non_fatal_error(intermediate_certs, |cert_der| { let potential_issuer = cert::parse_cert(untrusted::Input::from(cert_der), EndEntityOrCa::Ca(cert))?; if !equal(potential_issuer.subject, cert.issuer) { return Err(Error::UnknownIssuer.into()); } // Prevent loops; see RFC 4158 section 5.2. let mut prev = cert; loop { if equal(potential_issuer.spki.value(), prev.spki.value()) && equal(potential_issuer.subject, prev.subject) { return Err(Error::UnknownIssuer.into()); } match &prev.ee_or_ca { EndEntityOrCa::EndEntity => { break; } EndEntityOrCa::Ca(child_cert) => { prev = child_cert; } } } let next_sub_ca_count = match used_as_ca { UsedAsCa::No => sub_ca_count, UsedAsCa::Yes => sub_ca_count + 1, }; budget.consume_build_chain_call()?; build_chain_inner( required_eku_if_present, supported_sig_algs, trust_anchors, intermediate_certs, &potential_issuer, time, next_sub_ca_count, budget, ) }) } fn check_signatures( supported_sig_algs: &[&SignatureAlgorithm], cert_chain: &Cert, trust_anchor_key: untrusted::Input, budget: &mut Budget, ) -> Result<(), ErrorExt> { let mut spki_value = trust_anchor_key; let mut cert = cert_chain; loop { budget.consume_signature()?; signed_data::verify_signed_data(supported_sig_algs, spki_value, &cert.signed_data)?; // TODO: check revocation match &cert.ee_or_ca { EndEntityOrCa::Ca(child_cert) => { spki_value = cert.spki.value(); cert = child_cert; } EndEntityOrCa::EndEntity => { break; } } } Ok(()) } fn check_signed_chain_name_constraints( cert_chain: &Cert, trust_anchor: &TrustAnchor, ) -> Result<(), Error> { let mut cert = cert_chain; let mut name_constraints = trust_anchor.name_constraints.map(untrusted::Input::from); loop { untrusted::read_all_optional(name_constraints, Error::BadDer, |value| { name::check_name_constraints(value, cert) })?; match &cert.ee_or_ca { EndEntityOrCa::Ca(child_cert) => { name_constraints = cert.name_constraints; cert = child_cert; } EndEntityOrCa::EndEntity => { break; } } } Ok(()) } fn check_issuer_independent_properties( cert: &Cert, time: time::Time, used_as_ca: UsedAsCa, sub_ca_count: usize, required_eku_if_present: KeyPurposeId, ) -> Result<(), Error> { // TODO: check_distrust(trust_anchor_subject, trust_anchor_spki)?; // TODO: Check signature algorithm like mozilla::pkix. // TODO: Check SPKI like mozilla::pkix. // TODO: check for active distrust like mozilla::pkix. // See the comment in `remember_extension` for why we don't check the // KeyUsage extension. 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) })?; untrusted::read_all_optional(cert.eku, Error::BadDer, |value| { check_eku(value, required_eku_if_present) })?; Ok(()) } // https://tools.ietf.org/html/rfc5280#section-4.1.2.5 fn check_validity(input: &mut untrusted::Reader, time: time::Time) -> Result<(), Error> { let not_before = der::time_choice(input)?; let not_after = der::time_choice(input)?; if not_before > not_after { return Err(Error::InvalidCertValidity); } if time < not_before { return Err(Error::CertNotValidYet); } if time > not_after { return Err(Error::CertExpired); } // TODO: mozilla::pkix allows the TrustDomain to check not_before and // not_after, to enforce things like a maximum validity period. We should // do something similar. Ok(()) } #[derive(Clone, Copy)] 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, sub_ca_count: usize, ) -> Result<(), Error> { let (is_ca, path_len_constraint) = match input { Some(input) => { let is_ca = der::optional_boolean(input)?; // https://bugzilla.mozilla.org/show_bug.cgi?id=985025: RFC 5280 // says that a certificate must not have pathLenConstraint unless // it is a CA certificate, but some real-world end-entity // certificates have pathLenConstraint. let path_len_constraint = if !input.at_end() { let value = der::small_nonnegative_integer(input)?; Some(usize::from(value)) } else { None }; (is_ca, path_len_constraint) } 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 => { Err(Error::PathLenConstraintViolated) } _ => Ok(()), } } #[derive(Clone, Copy)] pub struct KeyPurposeId { oid_value: untrusted::Input<'static>, } // id-pkix OBJECT IDENTIFIER ::= { 1 3 6 1 5 5 7 } // id-kp OBJECT IDENTIFIER ::= { id-pkix 3 } // id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 } #[allow(clippy::identity_op)] // TODO: Make this clearer pub static EKU_SERVER_AUTH: KeyPurposeId = KeyPurposeId { oid_value: untrusted::Input::from(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 1]), }; // id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 } #[allow(clippy::identity_op)] // TODO: Make this clearer pub static EKU_CLIENT_AUTH: KeyPurposeId = KeyPurposeId { oid_value: untrusted::Input::from(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 2]), }; // id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 } #[allow(clippy::identity_op)] // TODO: Make this clearer pub static EKU_OCSP_SIGNING: KeyPurposeId = KeyPurposeId { oid_value: untrusted::Input::from(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 9]), }; // https://tools.ietf.org/html/rfc5280#section-4.2.1.12 // // Notable Differences from RFC 5280: // // * We follow the convention established by Microsoft's implementation and // mozilla::pkix of treating the EKU extension in a CA certificate as a // restriction on the allowable EKUs for certificates issued by that CA. RFC // 5280 doesn't prescribe any meaning to the EKU extension when a certificate // is being used as a CA certificate. // // * We do not recognize anyExtendedKeyUsage. NSS and mozilla::pkix do not // recognize it either. // // * We treat id-Netscape-stepUp as being equivalent to id-kp-serverAuth in CA // certificates (only). Comodo has issued certificates that require this // behavior that don't expire until June 2020. See https://bugzilla.mozilla.org/show_bug.cgi?id=982292. fn check_eku( input: Option<&mut untrusted::Reader>, required_eku_if_present: KeyPurposeId, ) -> Result<(), Error> { match input { Some(input) => { loop { let value = der::expect_tag_and_get_value(input, der::Tag::OID)?; if equal(value, required_eku_if_present.oid_value) { input.skip_to_end(); break; } if input.at_end() { return Err(Error::RequiredEkuNotFound); } } Ok(()) } None => { // http://tools.ietf.org/html/rfc6960#section-4.2.2.2: // "OCSP signing delegation SHALL be designated by the inclusion of // id-kp-OCSPSigning in an extended key usage certificate extension // included in the OCSP response signer's certificate." // // A missing EKU extension generally means "any EKU", but it is // important that id-kp-OCSPSigning is explicit so that a normal // end-entity certificate isn't able to sign trusted OCSP responses // for itself or for other certificates issued by its issuing CA. if equal( required_eku_if_present.oid_value, EKU_OCSP_SIGNING.oid_value, ) { return Err(Error::RequiredEkuNotFound); } Ok(()) } } } fn loop_while_non_fatal_error( values: V, mut f: impl FnMut(V::Item) -> Result<(), ErrorExt>, ) -> Result<(), ErrorExt> where V: IntoIterator, { for v in values { match f(v) { Ok(()) => { return Ok(()); } Err(e) => { if e.is_fatal() { return Err(e); } // If the error is not fatal, then keep going. } } } Err(Error::UnknownIssuer.into()) }