Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename and overhall DNS name types #125

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 167 additions & 77 deletions src/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]>>;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming back to this after a long time, I'm not sure why I thought it was important to make it especially convenient to support Box<[u8]>. I think I'll just remove this.


#[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 {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will try to remove DnsNameInput. It seems like a very heavyweight mechanism for accomplishing something simple.

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);
Expand Down
17 changes: 9 additions & 8 deletions src/webpki.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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> {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make this generic over the storage type of dns_name.

name::verify_cert_dns_name(&self, dns_name)
}

Expand All @@ -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);
Expand Down
Loading