Skip to content

Commit c4d77fd

Browse files
committed
recognize wildcard names
1 parent dfbb4ef commit c4d77fd

File tree

4 files changed

+130
-9
lines changed

4 files changed

+130
-9
lines changed

src/name.rs

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ use std::string::String;
2424
#[cfg(feature = "std")]
2525
use std::vec::Vec;
2626

27+
/// A DNS Name suitable for use in the TLS Server Name Indication (SNI)
28+
/// extension and/or for use as the reference hostname for which to verify a
29+
/// certificate.
30+
pub enum GeneralDNSNameRef<'name> {
31+
/// a valid DNS name
32+
DNSName(DNSNameRef<'name>),
33+
/// a DNS name containing a wildcard
34+
Wildcard(WildcardDNSNameRef<'name>),
35+
}
36+
37+
impl<'a> From<GeneralDNSNameRef<'a>> for &'a str {
38+
fn from(d: GeneralDNSNameRef<'a>) -> Self {
39+
match d {
40+
GeneralDNSNameRef::DNSName(name) => name.into(),
41+
GeneralDNSNameRef::Wildcard(name) => name.into(),
42+
}
43+
}
44+
}
45+
46+
2747
/// A DNS Name suitable for use in the TLS Server Name Indication (SNI)
2848
/// extension and/or for use as the reference hostname for which to verify a
2949
/// certificate.
@@ -132,6 +152,104 @@ impl<'a> From<DNSNameRef<'a>> for &'a str {
132152
}
133153
}
134154

155+
/// A reference to a DNS Name suitable for use in the TLS Server Name Indication
156+
/// (SNI) extension and/or for use as the reference hostname for which to verify
157+
/// a certificate. Compared to `DNSName`, this one will store domain names containing
158+
/// a wildcard.
159+
///
160+
/// A `WildcardDNSName` is guaranteed to be syntactically valid. The validity rules are
161+
/// specified in [RFC 5280 Section 7.2], except that underscores are also
162+
/// allowed, and following [RFC 6125].
163+
///
164+
/// `WildcardDNSName` stores a copy of the input it was constructed from in a `String`
165+
/// and so it is only available when the `std` default feature is enabled.
166+
///
167+
/// `Eq`, `PartialEq`, etc. are not implemented because name comparison
168+
/// frequently should be done case-insensitively and/or with other caveats that
169+
/// depend on the specific circumstances in which the comparison is done.
170+
///
171+
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
172+
/// [RFC 6125]: https://tools.ietf.org/html/rfc6125
173+
#[cfg(feature = "std")]
174+
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
175+
pub struct WildcardDNSName(String);
176+
177+
#[cfg(feature = "std")]
178+
impl WildcardDNSName {
179+
/// Returns a `WildcardDNSNameRef` that refers to this `WildcardDNSName`.
180+
pub fn as_ref(&self) -> WildcardDNSNameRef { WildcardDNSNameRef(self.0.as_bytes()) }
181+
}
182+
183+
#[cfg(feature = "std")]
184+
impl AsRef<str> for WildcardDNSName {
185+
fn as_ref(&self) -> &str { self.0.as_ref() }
186+
}
187+
188+
// Deprecated
189+
#[cfg(feature = "std")]
190+
impl From<WildcardDNSNameRef<'_>> for WildcardDNSName {
191+
fn from(dns_name: WildcardDNSNameRef) -> Self { dns_name.to_owned() }
192+
}
193+
194+
/// A reference to a DNS Name suitable for use in the TLS Server Name Indication
195+
/// (SNI) extension and/or for use as the reference hostname for which to verify
196+
/// a certificate.
197+
///
198+
/// A `WildcardDNSNameRef` is guaranteed to be syntactically valid. The validity rules
199+
/// are specified in [RFC 5280 Section 7.2], except that underscores are also
200+
/// allowed.
201+
///
202+
/// `Eq`, `PartialEq`, etc. are not implemented because name comparison
203+
/// frequently should be done case-insensitively and/or with other caveats that
204+
/// depend on the specific circumstances in which the comparison is done.
205+
///
206+
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
207+
#[derive(Clone, Copy)]
208+
pub struct WildcardDNSNameRef<'a>(&'a[u8]);
209+
210+
impl<'a> WildcardDNSNameRef<'a> {
211+
/// Constructs a `WildcardDNSNameRef` from the given input if the input is a
212+
/// syntactically-valid DNS name.
213+
pub fn try_from_ascii(dns_name: &'a[u8]) -> Result<Self, InvalidDNSNameError> {
214+
if !is_valid_wildcard_dns_id(untrusted::Input::from(dns_name)) {
215+
return Err(InvalidDNSNameError);
216+
}
217+
218+
Ok(Self(dns_name))
219+
}
220+
221+
/// Constructs a `WildcardDNSNameRef` from the given input if the input is a
222+
/// syntactically-valid DNS name.
223+
pub fn try_from_ascii_str(dns_name: &'a str) -> Result<Self, InvalidDNSNameError> {
224+
Self::try_from_ascii(dns_name.as_bytes())
225+
}
226+
227+
/// Constructs a `WildcardDNSName` from this `WildcardDNSNameRef`
228+
#[cfg(feature = "std")]
229+
pub fn to_owned(&self) -> WildcardDNSName {
230+
// WildcardDNSNameRef is already guaranteed to be valid ASCII, which is a
231+
// subset of UTF-8.
232+
let s: &str = self.clone().into();
233+
WildcardDNSName(s.to_ascii_lowercase())
234+
}
235+
}
236+
237+
#[cfg(feature = "std")]
238+
impl core::fmt::Debug for WildcardDNSNameRef<'_> {
239+
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
240+
let lowercase = self.clone().to_owned();
241+
f.debug_tuple("WildcardDNSNameRef").field(&lowercase.0).finish()
242+
}
243+
}
244+
245+
impl<'a> From<WildcardDNSNameRef<'a>> for &'a str {
246+
fn from(WildcardDNSNameRef(d): WildcardDNSNameRef<'a>) -> Self {
247+
// The unwrap won't fail because DNSNameRefs are guaranteed to be ASCII
248+
// and ASCII is a subset of UTF-8.
249+
core::str::from_utf8(d).unwrap()
250+
}
251+
}
252+
135253
pub fn verify_cert_dns_name(
136254
cert: &super::EndEntityCert, DNSNameRef(dns_name): DNSNameRef,
137255
) -> Result<(), Error> {
@@ -163,14 +281,15 @@ pub fn verify_cert_dns_name(
163281

164282
#[cfg(feature = "std")]
165283
pub fn list_cert_dns_names<'names>(cert: &super::EndEntityCert<'names>)
166-
-> Result<Vec<DNSNameRef<'names>>, Error> {
284+
-> Result<Vec<GeneralDNSNameRef<'names>>, Error> {
167285
let cert = &cert.inner;
168286
let names = std::cell::RefCell::new(Vec::new());
169287

170288
iterate_names(cert.subject, cert.subject_alt_name, Ok(()), &|name| {
171289
match name {
172290
GeneralName::DNSName(presented_id) => {
173-
match DNSNameRef::try_from_ascii(presented_id) {
291+
match DNSNameRef::try_from_ascii(presented_id.as_slice_less_safe()).map(GeneralDNSNameRef::DNSName)
292+
.or_else(|_| WildcardDNSNameRef::try_from_ascii(presented_id.as_slice_less_safe()).map(GeneralDNSNameRef::Wildcard)) {
174293
Ok(name) => names.borrow_mut().push(name),
175294
Err(_) => { /* keep going */ },
176295
};
@@ -783,6 +902,10 @@ fn is_valid_reference_dns_id(hostname: untrusted::Input) -> bool {
783902
is_valid_dns_id(hostname, IDRole::ReferenceID, AllowWildcards::No)
784903
}
785904

905+
fn is_valid_wildcard_dns_id(hostname: untrusted::Input) -> bool {
906+
is_valid_dns_id(hostname, IDRole::ReferenceID, AllowWildcards::Yes)
907+
}
908+
786909
// https://tools.ietf.org/html/rfc5280#section-4.2.1.6:
787910
//
788911
// When the subjectAltName extension contains a domain name system

src/webpki.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ pub mod trust_anchor_util;
6060
mod verify_cert;
6161

6262
pub use error::Error;
63-
pub use name::{DNSNameRef, InvalidDNSNameError};
63+
pub use name::{GeneralDNSNameRef, DNSNameRef, WildcardDNSNameRef, InvalidDNSNameError};
6464

6565
#[cfg(feature = "std")]
6666
pub use name::DNSName;
@@ -251,7 +251,7 @@ impl<'a> EndEntityCert<'a> {
251251
/// Requires the `std` default feature; i.e. this isn't available in
252252
/// `#![no_std]` configurations.
253253
#[cfg(feature = "std")]
254-
pub fn dns_names(&self) -> Result<Vec<DNSNameRef<'a>>, Error> {
254+
pub fn dns_names(&self) -> Result<Vec<GeneralDNSNameRef<'a>>, Error> {
255255
name::list_cert_dns_names(&self)
256256
}
257257
}

tests/integration.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ pub fn wildcard_subject_alternative_names()
161161

162162
expect_cert_dns_names(data, &[
163163
"account.netflix.com",
164-
// NOT "c*.netflix.com",
164+
"*.netflix.com",
165165
"netflix.ca",
166166
"netflix.com",
167167
"signup.netflix.com",
@@ -180,8 +180,7 @@ fn expect_cert_dns_names(data: &[u8], expected_names: &[&str])
180180
{
181181
use std::iter::FromIterator;
182182

183-
let input = untrusted::Input::from(data);
184-
let cert = webpki::EndEntityCert::from(input)
183+
let cert = webpki::EndEntityCert::from(data)
185184
.expect("should parse end entity certificate correctly");
186185

187186
let expected_names =
@@ -207,8 +206,7 @@ pub fn no_subject_alt_names()
207206
{
208207
let data = include_bytes!("misc/no_subject_alternative_name.der");
209208

210-
let input = untrusted::Input::from(data);
211-
let cert = webpki::EndEntityCert::from(input)
209+
let cert = webpki::EndEntityCert::from(data)
212210
.expect("should parse end entity certificate correctly");
213211

214212
let names = cert.dns_names().expect("we should get a result even without subjectAltNames");
-568 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)