diff --git a/src/name.rs b/src/name.rs index 60703978..d88926d4 100644 --- a/src/name.rs +++ b/src/name.rs @@ -16,126 +16,216 @@ use crate::{ cert::{Cert, EndEntityOrCA}, der, Error, }; +use core::fmt::Write as _; -/// 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 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 `DnsName` is guaranteed to be syntactically valid. The validity rules are -/// specified in [RFC 5280 Section 7.2], except that underscores are also +/// A `DnsName` is guaranteed to be syntactically valid. The validity rules +/// are specified in [RFC 5280 Section 7.2], except that underscores are also /// allowed. /// -/// `DnsName` 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. +/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2 +pub struct DnsName<B>(B) +where + B: AsRef<[u8]>; + +/// A borrowed `DnsName`. /// -/// `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. +/// This is an alias for `DnsName<&'a [u8]>`. +pub type DnsNameRef<'a> = DnsName<&'a [u8]>; + +/// An owned `DnsName` /// -/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2 +/// This is an alias for `DnsName<Box<[u8]>>`. #[cfg(feature = "std")] -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct DnsName(String); +pub type DnsNameBox = DnsName<Box<[u8]>>; -#[cfg(feature = "std")] -impl DnsName { - /// Returns a `DnsNameRef` that refers to this `DnsName`. - pub fn as_ref(&self) -> DnsNameRef { - DnsNameRef(self.0.as_bytes()) +/// An error indicating that a `DnsName` could not built because the input +/// is not a syntactically-valid DNS Name. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct InvalidDnsNameError; + +impl core::fmt::Display for InvalidDnsNameError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{:?}", self) } } #[cfg(feature = "std")] -impl AsRef<str> for DnsName { - fn as_ref(&self) -> &str { - self.0.as_ref() - } +impl ::std::error::Error for InvalidDnsNameError {} + +pub trait DnsNameInput: AsRef<[u8]> + Sized { + type Storage: AsRef<[u8]>; + fn into_storage(self) -> Self::Storage; } -// Deprecated #[cfg(feature = "std")] -impl From<DnsNameRef<'_>> for DnsName { - fn from(dns_name: DnsNameRef) -> Self { - dns_name.to_owned() +impl DnsNameInput for String { + type Storage = Box<[u8]>; + fn into_storage(self) -> Self::Storage { + self.into_boxed_str().into() } } -/// 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 `DnsNameRef` 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 DnsNameRef<'a>(&'a [u8]); +#[cfg(feature = "std")] +impl DnsNameInput for Box<[u8]> { + type Storage = Box<[u8]>; + fn into_storage(self) -> Self::Storage { + self + } +} -/// An error indicating that a `DnsNameRef` could not built because the input -/// is not a syntactically-valid DNS Name. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct InvalidDnsNameError; +#[cfg(feature = "std")] +impl DnsNameInput for Vec<u8> { + type Storage = Box<[u8]>; + fn into_storage(self) -> Self::Storage { + self.into() + } +} -impl core::fmt::Display for InvalidDnsNameError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{:?}", self) +impl<'a> DnsNameInput for &'a str { + type Storage = &'a [u8]; + fn into_storage(self) -> Self::Storage { + self.as_ref() } } -#[cfg(feature = "std")] -impl ::std::error::Error for InvalidDnsNameError {} +impl<'a> DnsNameInput for &'a [u8] { + type Storage = &'a [u8]; + fn into_storage(self) -> Self::Storage { + self.as_ref() + } +} -impl<'a> DnsNameRef<'a> { - /// Constructs a `DnsNameRef` from the given input if the input is a +impl<B> DnsName<B> +where + B: AsRef<[u8]>, +{ + /// Constructs a `DnsName` from the given input if the input is a /// syntactically-valid DNS name. - pub fn try_from_ascii(dns_name: &'a [u8]) -> Result<Self, InvalidDnsNameError> { - if !is_valid_reference_dns_id(untrusted::Input::from(dns_name)) { + pub fn try_from_punycode( + input: impl DnsNameInput<Storage = B>, + ) -> Result<Self, InvalidDnsNameError> { + if !is_valid_reference_dns_id(untrusted::Input::from(input.as_ref())) { return Err(InvalidDnsNameError); } - Ok(Self(dns_name)) + Ok(Self(input.into_storage())) } +} - /// Constructs a `DnsNameRef` 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, InvalidDnsNameError> { - Self::try_from_ascii(dns_name.as_bytes()) +impl<B> DnsName<B> +where + B: AsRef<[u8]>, +{ + /// Borrows any `DnsName` as a `DnsName<&[u8]>`. + /// + /// Use `DnsName<&[u8]>` when you don't *need* to be generic over the + /// underlying representation to reduce monomorphization overhead. + #[inline] + pub fn borrow(&self) -> DnsName<&[u8]> { + DnsName(self.0.as_ref()) } - /// Constructs a `DnsName` from this `DnsNameRef` + /// TODO: #[cfg(feature = "std")] - pub fn to_owned(&self) -> DnsName { - // DnsNameRef is already guaranteed to be valid ASCII, which is a - // subset of UTF-8. - let s: &str = self.clone().into(); - DnsName(s.to_ascii_lowercase()) + pub fn into_owned(self) -> DnsName<Box<[u8]>> { + DnsName(Box::from(self.0.as_ref())) + } + + /// Returns an iterator of the punycode characters of the DNS name, as + /// bytes. + /// + /// The iterator implements many specialized iterator traits, such as + /// `ExactSizeIterator` and `DoubleEndedIterator`. + /// + /// ``` + /// # #[cfg(feature = "std")] + /// use std::{iter::FromIterator, string::String}; + /// + /// # #[cfg(feature = "std")] + /// fn string_from_dns_name<B>(dns_name: webpki::DnsName<B>) -> String where B: AsRef<[u8]> { + /// String::from_iter(dns_name.punycode_lowercase_bytes().map(char::from)) + /// } + /// ``` + #[inline] + pub fn punycode_lowercase_bytes<'b>( + &self, + ) -> core::iter::Map<core::slice::Iter<u8>, fn(&u8) -> u8> + where + B: 'b, + { + // The unwrap won't fail because DnsNames are guaranteed to be ASCII + // and ASCII is a subset of UTF-8. + self.0.as_ref().iter().map(u8::to_ascii_lowercase) } } -#[cfg(feature = "std")] -impl core::fmt::Debug for DnsNameRef<'_> { - fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { - let lowercase = self.clone().to_owned(); - f.debug_tuple("DnsNameRef").field(&lowercase.0).finish() +impl<B> Clone for DnsName<B> +where + B: AsRef<[u8]>, + B: Clone, +{ + fn clone(&self) -> Self { + Self(self.0.clone()) } } -impl<'a> From<DnsNameRef<'a>> for &'a str { - fn from(DnsNameRef(d): DnsNameRef<'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).unwrap() +impl<B> Copy for DnsName<B> where B: AsRef<[u8]> + Copy {} + +impl<B> core::fmt::Debug for DnsName<B> +where + B: AsRef<[u8]>, +{ + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.write_str("DnsName(\"")?; + // None of the characters in a `DnsName` need to be escaped. + self.punycode_lowercase_bytes() + .try_for_each(|b| f.write_char(b.into()))?; + f.write_str("\")") + } +} + +impl<B> core::fmt::Display for DnsName<B> +where + B: AsRef<[u8]>, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.punycode_lowercase_bytes() + .try_for_each(|b| f.write_char(b.into())) + } +} + +impl<B> core::hash::Hash for DnsName<B> +where + B: AsRef<[u8]>, +{ + fn hash<H: core::hash::Hasher>(&self, state: &mut H) { + // This is modeled after the implementation of `Hash` for `[T]`. + let lowercase = self.punycode_lowercase_bytes(); + state.write_usize(lowercase.len()); + lowercase.for_each(|b| state.write_u8(b)); } } -pub fn verify_cert_dns_name( +impl<B1, B2> PartialEq<DnsName<B1>> for DnsName<B2> +where + B1: AsRef<[u8]>, + B2: AsRef<[u8]>, +{ + #[inline] + fn eq(&self, other: &DnsName<B1>) -> bool { + self.0.as_ref().eq_ignore_ascii_case(other.0.as_ref()) + } +} + +impl<B> Eq for DnsName<B> where B: AsRef<[u8]> {} + +pub(crate) fn verify_cert_dns_name( cert: &super::EndEntityCert, - DnsNameRef(dns_name): DnsNameRef, + DnsName(dns_name): DnsName<&[u8]>, ) -> Result<(), Error> { let cert = &cert.inner; let dns_name = untrusted::Input::from(dns_name); diff --git a/src/webpki.rs b/src/webpki.rs index 7076de8b..2675058d 100644 --- a/src/webpki.rs +++ b/src/webpki.rs @@ -41,10 +41,10 @@ pub mod trust_anchor_util; mod verify_cert; pub use error::Error; -pub use name::{DnsNameRef, InvalidDnsNameError}; +pub use name::{DnsName, DnsNameRef, InvalidDnsNameError}; #[cfg(feature = "std")] -pub use name::DnsName; +pub use name::DnsNameBox; pub use signed_data::{ SignatureAlgorithm, ECDSA_P256_SHA256, ECDSA_P256_SHA384, ECDSA_P384_SHA256, ECDSA_P384_SHA384, @@ -167,7 +167,7 @@ impl<'a> EndEntityCert<'a> { } /// Verifies that the certificate is valid for the given DNS host name. - pub fn verify_is_valid_for_dns_name(&self, dns_name: DnsNameRef) -> Result<(), Error> { + pub fn verify_is_valid_for_dns_name(&self, dns_name: DnsName<&[u8]>) -> Result<(), Error> { name::verify_cert_dns_name(&self, dns_name) } @@ -181,15 +181,16 @@ impl<'a> EndEntityCert<'a> { /// Requires the `std` default feature; i.e. this isn't available in /// `#![no_std]` configurations. #[cfg(feature = "std")] - pub fn verify_is_valid_for_at_least_one_dns_name<'names, Names>( + pub fn verify_is_valid_for_at_least_one_dns_name<'names, B, Names>( &self, dns_names: Names, - ) -> Result<Vec<DnsNameRef<'names>>, Error> + ) -> Result<Vec<DnsName<B>>, Error> where - Names: Iterator<Item = DnsNameRef<'names>>, + B: AsRef<[u8]>, + Names: Iterator<Item = DnsName<B>>, { - let result: Vec<DnsNameRef<'names>> = dns_names - .filter(|n| self.verify_is_valid_for_dns_name(*n).is_ok()) + let result: Vec<_> = dns_names + .filter(|n| self.verify_is_valid_for_dns_name(n.borrow()).is_ok()) .collect(); if result.is_empty() { return Err(Error::CertNotValidForName); diff --git a/tests/dns_name_tests.rs b/tests/dns_name_tests.rs index b3a3adc4..636bfb00 100644 --- a/tests/dns_name_tests.rs +++ b/tests/dns_name_tests.rs @@ -1,5 +1,7 @@ // Copyright 2014-2017 Brian Smith. +use webpki::{DnsName, DnsNameRef}; + // (name, is_valid) static DNS_NAME_VALIDITY: &[(&[u8], bool)] = &[ (b"a", true), @@ -399,10 +401,102 @@ fn dns_name_ref_try_from_ascii_test() { .chain(IP_ADDRESS_DNS_VALIDITY.iter()) { assert_eq!( - webpki::DnsNameRef::try_from_ascii(s).is_ok(), + DnsName::<&[u8]>::try_from_punycode(s).is_ok(), is_valid, - "DnsNameRef::try_from_ascii_str failed for \"{:?}\"", + "DnsName::try_from_punycode failed for \"{:?}\"", s ); } } + +const DNS_NAME_LOWERCASE_TEST_CASES: &[(&str, &str)] = &[ + // (expected_lowercase, input) + ("abc", "abc"), + ("abc", "Abc"), + ("abc", "aBc"), + ("abc", "abC"), + ("abc1", "abC1"), + ("abc.def", "abC.Def"), +]; + +#[test] +fn test_dns_name_ascii_lowercase_chars() { + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let dns_name: DnsNameRef = DnsName::try_from_punycode(*input).unwrap(); + let actual_lowercase = dns_name.punycode_lowercase_bytes(); + + assert_eq!(expected_lowercase.len(), actual_lowercase.len()); + assert!(expected_lowercase + .chars() + .zip(actual_lowercase) + .all(|(a, b)| a == b.into())); + } +} + +// XXX: This shouldn't require the "std" feature. +#[cfg(feature = "std")] +#[test] +fn test_dns_name_fmt() { + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let dns_name: DnsNameRef = DnsName::try_from_punycode(*input).unwrap(); + + // Test `Display` implementation. + assert_eq!(*expected_lowercase, format!("{}", dns_name)); + + // Test `Debug` implementation. + let debug_formatted = format!("{:?}", &dns_name); + assert_eq!( + String::from("DnsName(\"") + *expected_lowercase + "\")", + debug_formatted + ); + } +} + +// XXX: We need more test cases. +#[test] +fn test_dns_name_eq_different_len() { + #[rustfmt::skip] + const NOT_EQUAL: &[(&str, &str)] = &[ + ("a", "aa"), + ("aa", "a"), + ("aaa", "a"), + ("a", "aaa") + ]; + + for (a, b) in NOT_EQUAL { + let a: DnsNameRef = DnsName::try_from_punycode(*a).unwrap(); + let b: DnsNameRef = DnsName::try_from_punycode(*b).unwrap(); + assert_ne!(a, b) + } +} + +/// XXX: We need more test cases. +#[test] +fn test_dns_name_eq_case() { + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let a: DnsNameRef = DnsName::try_from_punycode(*expected_lowercase).unwrap(); + let b: DnsNameRef = DnsName::try_from_punycode(*input).unwrap(); + assert_eq!(a, b); + } +} + +/// XXX: We need more test cases. +#[cfg(feature = "std")] +#[test] +fn test_dns_name_eq_various_types() { + use webpki::DnsNameBox; + + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let a: DnsNameRef = DnsName::try_from_punycode(*expected_lowercase).unwrap(); + let b: DnsNameBox = DnsName::try_from_punycode(*input).unwrap().into_owned(); + assert_eq!(a, b); + } + + for (expected_lowercase, input) in DNS_NAME_LOWERCASE_TEST_CASES { + let a: DnsNameBox = DnsName::try_from_punycode(*expected_lowercase) + .unwrap() + .into_owned(); + let b: DnsNameRef = DnsName::try_from_punycode(*input).unwrap(); + assert_eq!(a, b); + } +}