diff --git a/src/name.rs b/src/name.rs index 0635eeab..1e51567f 100644 --- a/src/name.rs +++ b/src/name.rs @@ -25,6 +25,26 @@ use std::string::String; #[cfg(feature = "std")] use std::vec::Vec; +/// A DNS Name suitable for use in the TLS Server Name Indication (SNI) +/// extension and/or for use as the reference hostname for which to verify a +/// certificate. +pub enum GeneralDNSNameRef<'name> { + /// a valid DNS name + DNSName(DNSNameRef<'name>), + /// a DNS name containing a wildcard + Wildcard(WildcardDNSNameRef<'name>), +} + +impl<'a> From> for &'a str { + fn from(d: GeneralDNSNameRef<'a>) -> Self { + match d { + GeneralDNSNameRef::DNSName(name) => name.into(), + GeneralDNSNameRef::Wildcard(name) => name.into(), + } + } +} + + /// A DNS Name suitable for use in the TLS Server Name Indication (SNI) /// extension and/or for use as the reference hostname for which to verify a /// certificate. @@ -137,6 +157,109 @@ impl<'a> From> for untrusted::Input<'a> { fn from(DNSNameRef(dns_name): DNSNameRef<'a>) -> Self { dns_name } } +/// A reference to a DNS Name suitable for use in the TLS Server Name Indication +/// (SNI) extension and/or for use as the reference hostname for which to verify +/// a certificate. Compared to `DNSName`, this one will store domain names containing +/// a wildcard. +/// +/// A `WildcardDNSName` is guaranteed to be syntactically valid. The validity rules are +/// specified in [RFC 5280 Section 7.2], except that underscores are also +/// allowed, and following [RFC 6125]. +/// +/// `WildcardDNSName` stores a copy of the input it was constructed from in a `String` +/// and so it is only available when the `std` default feature is enabled. +/// +/// `Eq`, `PartialEq`, etc. are not implemented because name comparison +/// frequently should be done case-insensitively and/or with other caveats that +/// depend on the specific circumstances in which the comparison is done. +/// +/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2 +/// [RFC 6125]: https://tools.ietf.org/html/rfc6125 +#[cfg(feature = "std")] +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct WildcardDNSName(String); + +#[cfg(feature = "std")] +impl WildcardDNSName { + /// Returns a `WildcardDNSNameRef` that refers to this `WildcardDNSName`. + pub fn as_ref(&self) -> WildcardDNSNameRef { WildcardDNSNameRef(untrusted::Input::from(self.0.as_bytes())) } +} + +#[cfg(feature = "std")] +impl AsRef for WildcardDNSName { + fn as_ref(&self) -> &str { self.0.as_ref() } +} + +// Deprecated +#[cfg(feature = "std")] +impl From> for WildcardDNSName { + fn from(dns_name: WildcardDNSNameRef) -> Self { dns_name.to_owned() } +} + +/// A reference to a DNS Name suitable for use in the TLS Server Name Indication +/// (SNI) extension and/or for use as the reference hostname for which to verify +/// a certificate. +/// +/// A `WildcardDNSNameRef` is guaranteed to be syntactically valid. The validity rules +/// are specified in [RFC 5280 Section 7.2], except that underscores are also +/// allowed. +/// +/// `Eq`, `PartialEq`, etc. are not implemented because name comparison +/// frequently should be done case-insensitively and/or with other caveats that +/// depend on the specific circumstances in which the comparison is done. +/// +/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2 +#[derive(Clone, Copy)] +pub struct WildcardDNSNameRef<'a>(untrusted::Input<'a>); + +impl<'a> WildcardDNSNameRef<'a> { + /// Constructs a `WildcardDNSNameRef` from the given input if the input is a + /// syntactically-valid DNS name. + pub fn try_from_ascii(dns_name: untrusted::Input<'a>) -> Result { + if !is_valid_wildcard_dns_id(dns_name) { + return Err(InvalidDNSNameError); + } + + Ok(Self(dns_name)) + } + + /// Constructs a `WildcardDNSNameRef` from the given input if the input is a + /// syntactically-valid DNS name. + pub fn try_from_ascii_str(dns_name: &'a str) -> Result { + Self::try_from_ascii(untrusted::Input::from(dns_name.as_bytes())) + } + + /// Constructs a `WildcardDNSName` from this `WildcardDNSNameRef` + #[cfg(feature = "std")] + pub fn to_owned(&self) -> WildcardDNSName { + // WildcardDNSNameRef is already guaranteed to be valid ASCII, which is a + // subset of UTF-8. + let s: &str = self.clone().into(); + WildcardDNSName(s.to_ascii_lowercase()) + } +} + +#[cfg(feature = "std")] +impl core::fmt::Debug for WildcardDNSNameRef<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + let lowercase = self.clone().to_owned(); + f.debug_tuple("WildcardDNSNameRef").field(&lowercase.0).finish() + } +} + +impl<'a> From> for &'a str { + fn from(WildcardDNSNameRef(d): WildcardDNSNameRef<'a>) -> Self { + // The unwrap won't fail because DNSNameRefs are guaranteed to be ASCII + // and ASCII is a subset of UTF-8. + core::str::from_utf8(d.as_slice_less_safe()).unwrap() + } +} + +impl<'a> From> for untrusted::Input<'a> { + fn from(WildcardDNSNameRef(dns_name): WildcardDNSNameRef<'a>) -> Self { dns_name } +} + + pub fn verify_cert_dns_name( cert: &super::EndEntityCert, DNSNameRef(dns_name): DNSNameRef, ) -> Result<(), Error> { @@ -168,14 +291,15 @@ pub fn verify_cert_dns_name( #[cfg(feature = "std")] pub fn list_cert_dns_names<'names>(cert: &super::EndEntityCert<'names>) - -> Result>, Error> { + -> Result>, Error> { let cert = &cert.inner; let names = std::cell::RefCell::new(Vec::new()); iterate_names(cert.subject, cert.subject_alt_name, Ok(()), &|name| { match name { GeneralName::DNSName(presented_id) => { - match DNSNameRef::try_from_ascii(presented_id) { + match DNSNameRef::try_from_ascii(presented_id).map(GeneralDNSNameRef::DNSName) + .or_else(|_| WildcardDNSNameRef::try_from_ascii(presented_id).map(GeneralDNSNameRef::Wildcard)) { Ok(name) => names.borrow_mut().push(name), Err(_) => { /* keep going */ }, }; @@ -788,6 +912,10 @@ fn is_valid_reference_dns_id(hostname: untrusted::Input) -> bool { is_valid_dns_id(hostname, IDRole::ReferenceID, AllowWildcards::No) } +fn is_valid_wildcard_dns_id(hostname: untrusted::Input) -> bool { + is_valid_dns_id(hostname, IDRole::ReferenceID, AllowWildcards::Yes) +} + // https://tools.ietf.org/html/rfc5280#section-4.2.1.6: // // When the subjectAltName extension contains a domain name system diff --git a/src/webpki.rs b/src/webpki.rs index 28bf0a71..0fafd617 100644 --- a/src/webpki.rs +++ b/src/webpki.rs @@ -60,7 +60,7 @@ pub mod trust_anchor_util; mod verify_cert; pub use error::Error; -pub use name::{DNSNameRef, InvalidDNSNameError}; +pub use name::{GeneralDNSNameRef, DNSNameRef, WildcardDNSNameRef, InvalidDNSNameError}; #[cfg(feature = "std")] pub use name::DNSName; @@ -244,7 +244,7 @@ impl<'a> EndEntityCert<'a> { /// Requires the `std` default feature; i.e. this isn't available in /// `#![no_std]` configurations. #[cfg(feature = "std")] - pub fn dns_names(&self) -> Result>, Error> { + pub fn dns_names(&self) -> Result>, Error> { name::list_cert_dns_names(&self) } } diff --git a/tests/integration.rs b/tests/integration.rs index 7062e9a0..41b0c1e2 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -175,7 +175,7 @@ pub fn wildcard_subject_alternative_names() expect_cert_dns_names(data, &[ "account.netflix.com", - // NOT "c*.netflix.com", + "*.netflix.com", "netflix.ca", "netflix.com", "signup.netflix.com",