Skip to content

Commit

Permalink
add a method to collect the DNS names from a cert.
Browse files Browse the repository at this point in the history
This commit adds an `EndEntityCert::dns_names` method, which returns
a list of the DNS names provided in the subject alternative names
extension of the certificate.

Authored-by: Geoffroy Couprie geo.couprie@gmail.com
Co-authored-by: Sean McArthur sean@seanmonstar.com
Co-authored-by: Eliza Weisman eliza@buoyant.io
Co-authored-by: Daniel McCarney daniel@binaryparadox.net
Signed-off-by: Daniel McCarney daniel@binaryparadox.net
  • Loading branch information
Geal authored and djc committed Apr 20, 2023
1 parent 6dd4a44 commit bdb7874
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 34 deletions.
15 changes: 15 additions & 0 deletions src/end_entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

#[cfg(feature = "alloc")]
use crate::subject_name::GeneralDnsNameRef;
use crate::{
cert, signed_data, subject_name, verify_cert, Error, SignatureAlgorithm, SubjectNameRef, Time,
TlsClientTrustAnchors, TlsServerTrustAnchors,
Expand Down Expand Up @@ -174,4 +176,17 @@ impl<'a> EndEntityCert<'a> {
untrusted::Input::from(signature),
)
}

/// Returns a list of the DNS names provided in the subject alternative names extension
///
/// This function must not be used to implement custom DNS name verification.
/// Verification functions are already provided as `verify_is_valid_for_dns_name`
/// and `verify_is_valid_for_at_least_one_dns_name`.
///
/// Requires the `alloc` default feature; i.e. this isn't available in
/// `#![no_std]` configurations.
#[cfg(feature = "alloc")]
pub fn dns_names(&'a self) -> Result<impl Iterator<Item = GeneralDnsNameRef<'a>>, Error> {
subject_name::list_cert_dns_names(self)
}
}
134 changes: 107 additions & 27 deletions src/subject_name/dns_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,18 @@

#[cfg(feature = "alloc")]
use alloc::string::String;
use core::fmt::Write;

/// 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
/// allowed.
/// allowed. `DnsName`s do not include wildcard labels.
///
/// `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.
///
/// `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.
/// and so it is only available when the `alloc` default feature is enabled.
///
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
///
Expand Down Expand Up @@ -69,14 +66,10 @@ impl From<DnsNameRef<'_>> for DnsName {
///
/// 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.
/// allowed. `DnsNameRef`s do not include wildcard labels.
///
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub struct DnsNameRef<'a>(pub(crate) &'a [u8]);

impl AsRef<[u8]> for DnsNameRef<'_> {
Expand Down Expand Up @@ -105,7 +98,11 @@ impl<'a> DnsNameRef<'a> {
/// Constructs a `DnsNameRef` 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)) {
if !is_valid_dns_id(
untrusted::Input::from(dns_name),
IdRole::Reference,
AllowWildcards::No,
) {
return Err(InvalidDnsNameError);
}

Expand All @@ -130,19 +127,18 @@ impl<'a> DnsNameRef<'a> {
}
}

/// Requires the `alloc` feature.
#[cfg(feature = "alloc")]
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()
}
}
f.write_str("DnsNameRef(\"")?;

#[cfg(not(feature = "alloc"))]
impl core::fmt::Debug for DnsNameRef<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
f.debug_tuple("DnsNameRef").field(&self.0).finish()
// Convert each byte of the underlying ASCII string to a `char` and
// downcase it prior to formatting it. We avoid self.clone().to_owned()
// since it requires allocation.
for &ch in self.0 {
f.write_char(char::from(ch).to_ascii_lowercase())?;
}

f.write_str("\")")
}
}

Expand All @@ -154,6 +150,94 @@ impl<'a> From<DnsNameRef<'a>> for &'a str {
}
}

/// A DNS name that may be either a DNS name identifier presented by a server (which may include
/// wildcards), or a DNS name identifier referenced by a client for matching purposes (wildcards
/// not permitted).
pub enum GeneralDnsNameRef<'name> {
/// a reference to a DNS name that may be used for matching purposes.
DnsName(DnsNameRef<'name>),
/// a reference to a presented DNS name that may include a wildcard.
Wildcard(WildcardDnsNameRef<'name>),
}

impl<'a> From<GeneralDnsNameRef<'a>> for &'a str {
fn from(d: GeneralDnsNameRef<'a>) -> Self {
match d {
GeneralDnsNameRef::DnsName(name) => name.into(),
GeneralDnsNameRef::Wildcard(name) => name.into(),
}
}
}

/// A reference to a DNS Name presented by a server that may include a wildcard.
///
/// 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.
///
/// Additionally, while [RFC6125 Section 4.1] says that a wildcard label may be of the form
/// `<x>*<y>.<DNSID>`, where `<x>` and/or `<y>` may be empty, we follow a stricter policy common
/// to most validation libraries (e.g. NSS) and only accept wildcard labels that are exactly `*`.
///
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
/// [RFC 6125 Section 4.1]: https://www.rfc-editor.org/rfc/rfc6125#section-4.1
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub struct WildcardDnsNameRef<'a>(&'a [u8]);

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: &'a [u8]) -> Result<Self, InvalidDnsNameError> {
if !is_valid_dns_id(
untrusted::Input::from(dns_name),
IdRole::Reference,
AllowWildcards::Yes,
) {
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, InvalidDnsNameError> {
Self::try_from_ascii(dns_name.as_bytes())
}
}

impl core::fmt::Debug for WildcardDnsNameRef<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
f.write_str("WildcardDnsNameRef(\"")?;

// Convert each byte of the underlying ASCII string to a `char` and
// downcase it prior to formatting it. We avoid self.to_owned() since
// it requires allocation.
for &ch in self.0 {
f.write_char(char::from(ch).to_ascii_lowercase())?;
}

f.write_str("\")")
}
}

impl<'a> From<WildcardDnsNameRef<'a>> for &'a str {
fn from(WildcardDnsNameRef(d): WildcardDnsNameRef<'a>) -> Self {
// The unwrap won't fail because WildcardDnsNameRef are guaranteed to be ASCII
// and ASCII is a subset of UTF-8.
core::str::from_utf8(d).unwrap()
}
}

impl AsRef<str> for WildcardDnsNameRef<'_> {
#[inline]
fn as_ref(&self) -> &str {
// The unwrap won't fail because WildcardDnsNameRef are guaranteed to be ASCII
// and ASCII is a subset of UTF-8.
core::str::from_utf8(self.0).unwrap()
}
}

pub(super) fn presented_id_matches_reference_id(
presented_dns_id: untrusted::Input,
reference_dns_id: untrusted::Input,
Expand Down Expand Up @@ -445,10 +529,6 @@ enum IdRole {
NameConstraint,
}

fn is_valid_reference_dns_id(hostname: untrusted::Input) -> bool {
is_valid_dns_id(hostname, IdRole::Reference, AllowWildcards::No)
}

// https://tools.ietf.org/html/rfc5280#section-4.2.1.6:
//
// When the subjectAltName extension contains a domain name system
Expand Down
4 changes: 4 additions & 0 deletions src/subject_name/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

mod dns_name;
#[cfg(feature = "alloc")]
pub(crate) use dns_name::GeneralDnsNameRef;
pub use dns_name::{DnsNameRef, InvalidDnsNameError};

/// Requires the `alloc` feature.
Expand All @@ -29,6 +31,8 @@ pub use ip_address::{AddrParseError, IpAddrRef};
pub use ip_address::IpAddr;

mod verify;
#[cfg(feature = "alloc")]
pub(super) use verify::list_cert_dns_names;
pub(super) use verify::{
check_name_constraints, verify_cert_subject_name, SubjectCommonNameContents,
};
52 changes: 45 additions & 7 deletions src/subject_name/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ use crate::{
cert::{Cert, EndEntityOrCa},
der, Error,
};
#[cfg(feature = "alloc")]
use {
alloc::vec::Vec,
dns_name::{GeneralDnsNameRef, WildcardDnsNameRef},
};

pub(crate) fn verify_cert_dns_name(
cert: &crate::EndEntityCert,
Expand All @@ -33,7 +38,7 @@ pub(crate) fn verify_cert_dns_name(
cert.subject_alt_name,
SubjectCommonNameContents::Ignore,
Err(Error::CertNotValidForName),
&|name| {
&mut |name| {
if let GeneralName::DnsName(presented_id) = name {
match dns_name::presented_id_matches_reference_id(presented_id, dns_name) {
Some(true) => return NameIteration::Stop(Ok(())),
Expand Down Expand Up @@ -67,7 +72,7 @@ pub(crate) fn verify_cert_subject_name(
cert.inner().subject_alt_name,
SubjectCommonNameContents::Ignore,
Err(Error::CertNotValidForName),
&|name| {
&mut |name| {
if let GeneralName::IpAddress(presented_id) = name {
match ip_address::presented_id_matches_reference_id(presented_id, ip_address) {
Ok(true) => return NameIteration::Stop(Ok(())),
Expand Down Expand Up @@ -115,7 +120,7 @@ pub(crate) fn check_name_constraints(
child.subject_alt_name,
subject_common_name_contents,
Ok(()),
&|name| {
&mut |name| {
check_presented_id_conforms_to_constraints(
name,
permitted_subtrees,
Expand Down Expand Up @@ -302,12 +307,12 @@ pub(crate) enum SubjectCommonNameContents {
Ignore,
}

fn iterate_names(
subject: Option<untrusted::Input>,
subject_alt_name: Option<untrusted::Input>,
fn iterate_names<'names>(
subject: Option<untrusted::Input<'names>>,
subject_alt_name: Option<untrusted::Input<'names>>,
subject_common_name_contents: SubjectCommonNameContents,
result_if_never_stopped_early: Result<(), Error>,
f: &dyn Fn(GeneralName) -> NameIteration,
f: &mut impl FnMut(GeneralName<'names>) -> NameIteration,
) -> Result<(), Error> {
if let Some(subject_alt_name) = subject_alt_name {
let mut subject_alt_name = untrusted::Reader::new(subject_alt_name);
Expand Down Expand Up @@ -351,6 +356,39 @@ fn iterate_names(
}
}

#[cfg(feature = "alloc")]
pub(crate) fn list_cert_dns_names<'names>(
cert: &'names crate::EndEntityCert<'names>,
) -> Result<impl Iterator<Item = GeneralDnsNameRef<'names>>, Error> {
let cert = &cert.inner();
let mut names = Vec::new();

iterate_names(
Some(cert.subject),
cert.subject_alt_name,
SubjectCommonNameContents::DnsName,
Ok(()),
&mut |name| {
if let GeneralName::DnsName(presented_id) = name {
let dns_name = DnsNameRef::try_from_ascii(presented_id.as_slice_less_safe())
.map(GeneralDnsNameRef::DnsName)
.or_else(|_| {
WildcardDnsNameRef::try_from_ascii(presented_id.as_slice_less_safe())
.map(GeneralDnsNameRef::Wildcard)
});

// if the name could be converted to a DNS name, add it; otherwise,
// keep going.
if let Ok(name) = dns_name {
names.push(name)
}
}
NameIteration::KeepGoing
},
)
.map(|_| names.into_iter())
}

// It is *not* valid to derive `Eq`, `PartialEq, etc. for this type. In
// particular, for the types of `GeneralName`s that we don't understand, we
// don't even store the value. Also, the meaning of a `GeneralName` in a name
Expand Down
Loading

0 comments on commit bdb7874

Please sign in to comment.