diff --git a/src/end_entity.rs b/src/end_entity.rs index 6c1247c4..aad4e7e6 100644 --- a/src/end_entity.rs +++ b/src/end_entity.rs @@ -15,8 +15,9 @@ #[cfg(feature = "alloc")] use crate::subject_name::GeneralDnsNameRef; use crate::{ - cert, signed_data, subject_name, verify_cert, CertRevocationList, Error, SignatureAlgorithm, - SubjectNameRef, Time, TlsClientTrustAnchors, TlsServerTrustAnchors, + cert, signed_data, subject_name, verify_cert, CertRevocationList, Error, ExtendedKeyUsage, + NonTlsTrustAnchors, SignatureAlgorithm, SubjectNameRef, Time, TlsClientTrustAnchors, + TlsServerTrustAnchors, TrustAnchor, }; /// An end-entity certificate. @@ -74,6 +75,57 @@ impl<'a> EndEntityCert<'a> { &self.inner } + fn verify_is_valid_cert( + &self, + supported_sig_algs: &[&SignatureAlgorithm], + trust_anchors: &[TrustAnchor], + intermediate_certs: &[&[u8]], + time: Time, + eku: ExtendedKeyUsage, + crls: &[&dyn CertRevocationList], + ) -> Result<(), Error> { + verify_cert::build_chain( + &verify_cert::ChainOptions { + eku, + supported_sig_algs, + trust_anchors, + intermediate_certs, + crls, + }, + &self.inner, + time, + ) + } + + /// Verifies that the end-entity certificate is valid for use against the + /// specified Extended Key Usage (EKU). + /// + /// `supported_sig_algs` is the list of signature algorithms that are + /// trusted for use in certificate signatures; the end-entity certificate's + /// public key is not validated against this list. `trust_anchors` is the + /// list of root CAs to trust. `intermediate_certs` is the sequence of + /// intermediate certificates that the server sent in the TLS handshake. + /// `time` is the time for which the validation is effective (usually the + /// current time). + pub fn verify_is_valid_cert_with_eku( + &self, + supported_sig_algs: &[&SignatureAlgorithm], + &NonTlsTrustAnchors(trust_anchors): &NonTlsTrustAnchors, + intermediate_certs: &[&[u8]], + time: Time, + eku: ExtendedKeyUsage, + crls: &[&dyn CertRevocationList], + ) -> Result<(), Error> { + self.verify_is_valid_cert( + supported_sig_algs, + trust_anchors, + intermediate_certs, + time, + eku, + crls, + ) + } + /// Verifies that the end-entity certificate is valid for use by a TLS /// server. /// @@ -91,16 +143,13 @@ impl<'a> EndEntityCert<'a> { intermediate_certs: &[&[u8]], time: Time, ) -> Result<(), Error> { - verify_cert::build_chain( - &verify_cert::ChainOptions { - required_eku_if_present: verify_cert::EKU_SERVER_AUTH, - supported_sig_algs, - trust_anchors, - intermediate_certs, - crls: &[], - }, - &self.inner, + self.verify_is_valid_cert( + supported_sig_algs, + trust_anchors, + intermediate_certs, time, + ExtendedKeyUsage::RequiredIfPresent(verify_cert::EKU_SERVER_AUTH), + &[], ) } @@ -123,16 +172,13 @@ impl<'a> EndEntityCert<'a> { time: Time, crls: &[&dyn CertRevocationList], ) -> Result<(), Error> { - verify_cert::build_chain( - &verify_cert::ChainOptions { - required_eku_if_present: verify_cert::EKU_CLIENT_AUTH, - supported_sig_algs, - trust_anchors, - intermediate_certs, - crls, - }, - &self.inner, + self.verify_is_valid_cert( + supported_sig_algs, + trust_anchors, + intermediate_certs, time, + ExtendedKeyUsage::RequiredIfPresent(verify_cert::EKU_CLIENT_AUTH), + crls, ) } diff --git a/src/lib.rs b/src/lib.rs index 982a93f7..57ca13e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,7 +72,8 @@ pub use { SubjectNameRef, }, time::Time, - trust_anchor::{TlsClientTrustAnchors, TlsServerTrustAnchors, TrustAnchor}, + trust_anchor::{NonTlsTrustAnchors, TlsClientTrustAnchors, TlsServerTrustAnchors, TrustAnchor}, + verify_cert::{ExtendedKeyUsage, KeyPurposeId}, }; #[cfg(feature = "alloc")] diff --git a/src/trust_anchor.rs b/src/trust_anchor.rs index 87506b47..129b4587 100644 --- a/src/trust_anchor.rs +++ b/src/trust_anchor.rs @@ -26,6 +26,10 @@ pub struct TrustAnchor<'a> { pub name_constraints: Option<&'a [u8]>, } +/// Trust anchors which may be used for authenticating certificates of any kind. +#[derive(Debug)] +pub struct NonTlsTrustAnchors<'a>(pub &'a [TrustAnchor<'a>]); + /// Trust anchors which may be used for authenticating servers. #[derive(Debug)] pub struct TlsServerTrustAnchors<'a>(pub &'a [TrustAnchor<'a>]); diff --git a/src/verify_cert.rs b/src/verify_cert.rs index 894f4259..5374d9a4 100644 --- a/src/verify_cert.rs +++ b/src/verify_cert.rs @@ -19,7 +19,7 @@ use crate::{ }; pub(crate) struct ChainOptions<'a> { - pub(crate) required_eku_if_present: KeyPurposeId, + pub(crate) eku: ExtendedKeyUsage, pub(crate) supported_sig_algs: &'a [&'a SignatureAlgorithm], pub(crate) trust_anchors: &'a [TrustAnchor<'a>], pub(crate) intermediate_certs: &'a [&'a [u8]], @@ -38,13 +38,7 @@ fn build_chain_inner( ) -> Result<(), Error> { let used_as_ca = used_as_ca(&cert.ee_or_ca); - check_issuer_independent_properties( - cert, - time, - used_as_ca, - sub_ca_count, - opts.required_eku_if_present, - )?; + check_issuer_independent_properties(cert, time, used_as_ca, sub_ca_count, opts.eku)?; // TODO: HPKP checks. @@ -65,12 +59,13 @@ fn build_chain_inner( // for the purpose of name constraints checking, only end-entity server certificates // could plausibly have a DNS name as a subject commonName that could contribute to // path validity - let subject_common_name_contents = - if opts.required_eku_if_present == EKU_SERVER_AUTH && used_as_ca == UsedAsCa::No { - subject_name::SubjectCommonNameContents::DnsName - } else { - subject_name::SubjectCommonNameContents::Ignore - }; + let subject_common_name_contents = if opts.eku.key_purpose_id_equals(EKU_SERVER_AUTH.oid_value) + && used_as_ca == UsedAsCa::No + { + subject_name::SubjectCommonNameContents::DnsName + } else { + subject_name::SubjectCommonNameContents::Ignore + }; let result = loop_while_non_fatal_error( Error::UnknownIssuer, @@ -245,7 +240,7 @@ fn check_issuer_independent_properties( time: time::Time, used_as_ca: UsedAsCa, sub_ca_count: usize, - required_eku_if_present: KeyPurposeId, + eku: ExtendedKeyUsage, ) -> Result<(), Error> { // TODO: check_distrust(trust_anchor_subject, trust_anchor_spki)?; // TODO: Check signature algorithm like mozilla::pkix. @@ -263,9 +258,7 @@ fn check_issuer_independent_properties( 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) - })?; + untrusted::read_all_optional(cert.eku, Error::BadDer, |value| check_eku(value, eku))?; Ok(()) } @@ -341,42 +334,69 @@ fn check_basic_constraints( } } +/// Extended Key Usage (EKU) of a certificate. +#[derive(Clone, Copy)] +pub enum ExtendedKeyUsage { + /// The certificate must contain the specified [`KeyPurposeId`] as EKU. + Required(KeyPurposeId), + + /// If the certificate has EKUs, then the specified [`KeyPurposeId`] must be included. + RequiredIfPresent(KeyPurposeId), +} + +impl ExtendedKeyUsage { + fn key_purpose_id_equals(&self, value: untrusted::Input<'_>) -> bool { + match self { + ExtendedKeyUsage::Required(eku) => *eku, + ExtendedKeyUsage::RequiredIfPresent(eku) => *eku, + } + .oid_value + == value + } +} + +/// An OID value indicating an Extended Key Usage (EKU) key purpose. #[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) struct KeyPurposeId { +pub struct KeyPurposeId { oid_value: untrusted::Input<'static>, } +impl KeyPurposeId { + /// Construct a new [`KeyPurposeId`]. + /// + /// `oid` is the OBJECT IDENTIFIER in bytes. + pub const fn new(oid: &'static [u8]) -> Self { + Self { + oid_value: untrusted::Input::from(oid), + } + } +} + // 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(crate) static EKU_SERVER_AUTH: KeyPurposeId = KeyPurposeId { - oid_value: untrusted::Input::from(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 1]), -}; +pub(crate) static EKU_SERVER_AUTH: KeyPurposeId = + KeyPurposeId::new(&[(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(crate) static EKU_CLIENT_AUTH: KeyPurposeId = KeyPurposeId { - oid_value: untrusted::Input::from(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 2]), -}; +pub(crate) static EKU_CLIENT_AUTH: KeyPurposeId = + KeyPurposeId::new(&[(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(crate) static EKU_OCSP_SIGNING: KeyPurposeId = KeyPurposeId { - oid_value: untrusted::Input::from(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 9]), -}; +pub(crate) static EKU_OCSP_SIGNING: KeyPurposeId = + KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 9]); // https://tools.ietf.org/html/rfc5280#section-4.2.1.12 -fn check_eku( - input: Option<&mut untrusted::Reader>, - required_eku_if_present: KeyPurposeId, -) -> Result<(), Error> { +fn check_eku(input: Option<&mut untrusted::Reader>, eku: ExtendedKeyUsage) -> Result<(), Error> { match input { Some(input) => { loop { let value = der::expect_tag_and_get_value(input, der::Tag::OID)?; - if value == required_eku_if_present.oid_value { + if eku.key_purpose_id_equals(value) { input.skip_to_end(); break; } @@ -387,6 +407,9 @@ fn check_eku( Ok(()) } None => { + if matches!(eku, ExtendedKeyUsage::Required(_)) { + return Err(Error::RequiredEkuNotFound); + } // 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 @@ -396,7 +419,7 @@ fn check_eku( // 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 required_eku_if_present.oid_value == EKU_OCSP_SIGNING.oid_value { + if eku.key_purpose_id_equals(EKU_OCSP_SIGNING.oid_value) { return Err(Error::RequiredEkuNotFound); } @@ -457,3 +480,14 @@ where } Err(error) } + +#[cfg(test)] +mod tests { + use crate::{verify_cert::EKU_SERVER_AUTH, ExtendedKeyUsage}; + + #[test] + fn eku_key_purpose_id() { + assert!(ExtendedKeyUsage::RequiredIfPresent(EKU_SERVER_AUTH) + .key_purpose_id_equals(EKU_SERVER_AUTH.oid_value)) + } +} diff --git a/tests/custom_ekus.rs b/tests/custom_ekus.rs new file mode 100644 index 00000000..d3d322ac --- /dev/null +++ b/tests/custom_ekus.rs @@ -0,0 +1,84 @@ +#[cfg(feature = "alloc")] +use webpki::ExtendedKeyUsage::{Required, RequiredIfPresent}; + +#[cfg(feature = "alloc")] +fn check_cert( + ee: &[u8], + ca: &[u8], + eku: webpki::ExtendedKeyUsage, + time: webpki::Time, + result: Result<(), webpki::Error>, +) { + let anchors = vec![webpki::TrustAnchor::try_from_cert_der(ca).unwrap()]; + let anchors = webpki::NonTlsTrustAnchors(&anchors); + let algs = &[ + &webpki::RSA_PKCS1_2048_8192_SHA256, + &webpki::ECDSA_P256_SHA256, + ]; + + let cert = webpki::EndEntityCert::try_from(ee).unwrap(); + + assert_eq!( + cert.verify_is_valid_cert_with_eku(algs, &anchors, &[], time, eku, &[]), + result + ); +} + +#[cfg(feature = "alloc")] +#[allow(clippy::identity_op)] +static EKU_CLIENT_AUTH: webpki::KeyPurposeId = + webpki::KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 2]); + +#[cfg(feature = "alloc")] +#[allow(clippy::identity_op)] +static EKU_SERVER_AUTH: webpki::KeyPurposeId = + webpki::KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 1]); + +#[cfg(feature = "alloc")] +#[allow(clippy::identity_op)] +static EKU_MDOC_ISSUER_AUTH: webpki::KeyPurposeId = + webpki::KeyPurposeId::new(&[(40 * 1) + 0, 129, 140, 93, 5, 1, 2]); + +#[cfg(feature = "alloc")] +#[test] +pub fn verify_custom_eku_mdoc() { + let err = Err(webpki::Error::RequiredEkuNotFound); + let time = webpki::Time::from_seconds_since_unix_epoch(1609459200); // Jan 1 01:00:00 CET 2021 + + let ee = include_bytes!("misc/mdoc_eku.ee.der"); + let ca = include_bytes!("misc/mdoc_eku.ca.der"); + check_cert(ee, ca, Required(EKU_MDOC_ISSUER_AUTH), time, Ok(())); + check_cert(ee, ca, Required(EKU_SERVER_AUTH), time, err); + check_cert( + ee, + ca, + RequiredIfPresent(EKU_MDOC_ISSUER_AUTH), + time, + Ok(()), + ); + check_cert(ee, ca, RequiredIfPresent(EKU_SERVER_AUTH), time, err); +} + +#[cfg(feature = "alloc")] +#[test] +pub fn verify_custom_eku_client() { + let err = Err(webpki::Error::RequiredEkuNotFound); + let time = webpki::Time::from_seconds_since_unix_epoch(0x1fed_f00d); + + let ee = include_bytes!("client_auth/cert_with_no_eku_accepted_for_client_auth.ee.der"); + let ca = include_bytes!("client_auth/cert_with_no_eku_accepted_for_client_auth.ca.der"); + check_cert(ee, ca, Required(EKU_CLIENT_AUTH), time, err); + check_cert(ee, ca, RequiredIfPresent(EKU_CLIENT_AUTH), time, Ok(())); + + let ee = include_bytes!("client_auth/cert_with_both_ekus_accepted_for_client_auth.ee.der"); + let ca = include_bytes!("client_auth/cert_with_both_ekus_accepted_for_client_auth.ca.der"); + check_cert(ee, ca, Required(EKU_CLIENT_AUTH), time, Ok(())); + check_cert(ee, ca, Required(EKU_SERVER_AUTH), time, Ok(())); + check_cert(ee, ca, RequiredIfPresent(EKU_CLIENT_AUTH), time, Ok(())); + check_cert(ee, ca, RequiredIfPresent(EKU_SERVER_AUTH), time, Ok(())); +} + +#[test] +fn key_purpose_id() { + webpki::KeyPurposeId::new(&[1, 2, 3]); +} diff --git a/tests/misc/mdoc_eku.ca.der b/tests/misc/mdoc_eku.ca.der new file mode 100644 index 00000000..7a5c2136 Binary files /dev/null and b/tests/misc/mdoc_eku.ca.der differ diff --git a/tests/misc/mdoc_eku.ee.der b/tests/misc/mdoc_eku.ee.der new file mode 100644 index 00000000..c1d9ab44 Binary files /dev/null and b/tests/misc/mdoc_eku.ee.der differ