Skip to content

Commit 5cb21dd

Browse files
committed
add a method to collect the DNS names from a certificate
recognize wildcard names
1 parent 9cf9f45 commit 5cb21dd

File tree

8 files changed

+276
-9
lines changed

8 files changed

+276
-9
lines changed

src/name.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1414

1515
mod dns_name;
16-
pub use dns_name::{DnsNameRef, InvalidDnsNameError};
16+
pub use dns_name::{DnsNameRef, GeneralDnsNameRef, WildcardDnsNameRef, InvalidDnsNameError};
1717

1818
/// Requires the `alloc` feature.
1919
#[cfg(feature = "alloc")]
@@ -22,4 +22,4 @@ pub use dns_name::DnsName;
2222
mod ip_address;
2323

2424
mod verify;
25-
pub(super) use verify::{check_name_constraints, verify_cert_dns_name};
25+
pub(super) use verify::{check_name_constraints, verify_cert_dns_name, list_cert_dns_names};

src/name/dns_name.rs

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use alloc::string::String;
2424
/// allowed.
2525
///
2626
/// `DnsName` stores a copy of the input it was constructed from in a `String`
27-
/// and so it is only available when the `std` default feature is enabled.
27+
/// and so it is only available when the `alloc` default feature is enabled.
2828
///
2929
/// `Eq`, `PartialEq`, etc. are not implemented because name comparison
3030
/// frequently should be done case-insensitively and/or with other caveats that
@@ -147,6 +147,123 @@ impl<'a> From<DnsNameRef<'a>> for &'a str {
147147
}
148148
}
149149

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

697+
fn is_valid_wildcard_dns_id(hostname: untrusted::Input) -> bool {
698+
is_valid_dns_id(hostname, IDRole::ReferenceID, AllowWildcards::Yes)
699+
}
700+
580701
#[cfg(test)]
581702
mod tests {
582703
use super::*;

src/name/verify.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1414

1515
use super::{
16-
dns_name::{self, DnsNameRef},
16+
dns_name::{self, DnsNameRef, GeneralDnsNameRef, WildcardDnsNameRef},
1717
ip_address,
1818
};
1919
use crate::{
2020
cert::{Cert, EndEntityOrCA},
2121
der, Error,
2222
};
23+
#[cfg(feature = "alloc")]
24+
use alloc::vec::Vec;
2325

2426
pub fn verify_cert_dns_name(
2527
cert: &crate::EndEntityCert,
@@ -245,11 +247,11 @@ enum NameIteration {
245247
Stop(Result<(), Error>),
246248
}
247249

248-
fn iterate_names(
249-
subject: untrusted::Input,
250-
subject_alt_name: Option<untrusted::Input>,
250+
fn iterate_names<'names>(
251+
subject: untrusted::Input<'names>,
252+
subject_alt_name: Option<untrusted::Input<'names>>,
251253
result_if_never_stopped_early: Result<(), Error>,
252-
f: &dyn Fn(GeneralName) -> NameIteration,
254+
f: &dyn Fn(GeneralName<'names>) -> NameIteration,
253255
) -> Result<(), Error> {
254256
match subject_alt_name {
255257
Some(subject_alt_name) => {
@@ -279,6 +281,28 @@ fn iterate_names(
279281
}
280282
}
281283

284+
#[cfg(feature = "alloc")]
285+
pub fn list_cert_dns_names<'names>(cert: &crate::EndEntityCert<'names>)
286+
-> Result<Vec<GeneralDnsNameRef<'names>>, Error> {
287+
let cert = &cert.inner;
288+
let names = core::cell::RefCell::new(Vec::new());
289+
290+
iterate_names(cert.subject, cert.subject_alt_name, Ok(()), &|name| {
291+
match name {
292+
GeneralName::DnsName(presented_id) => {
293+
match DnsNameRef::try_from_ascii(presented_id.as_slice_less_safe()).map(GeneralDnsNameRef::DnsName)
294+
.or_else(|_| WildcardDnsNameRef::try_from_ascii(presented_id.as_slice_less_safe()).map(GeneralDnsNameRef::Wildcard)) {
295+
Ok(name) => names.borrow_mut().push(name),
296+
Err(_) => { /* keep going */ },
297+
};
298+
},
299+
_ => ()
300+
}
301+
NameIteration::KeepGoing
302+
}).map(|_| names.into_inner())
303+
}
304+
305+
282306
// It is *not* valid to derive `Eq`, `PartialEq, etc. for this type. In
283307
// particular, for the types of `GeneralName`s that we don't understand, we
284308
// don't even store the value. Also, the meaning of a `GeneralName` in a name

src/webpki.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub mod trust_anchor_util;
5050
mod verify_cert;
5151

5252
pub use error::Error;
53-
pub use name::{DnsNameRef, InvalidDnsNameError};
53+
pub use name::{GeneralDnsNameRef, DnsNameRef, WildcardDnsNameRef, InvalidDnsNameError};
5454

5555
#[cfg(feature = "alloc")]
5656
pub use name::DnsName;
@@ -248,6 +248,19 @@ impl<'a> EndEntityCert<'a> {
248248
untrusted::Input::from(signature),
249249
)
250250
}
251+
252+
/// Returns a list of the DNS names provided in the subject alternative names extension
253+
///
254+
/// This function must not be used to implement custom DNS name verification.
255+
/// Verification functions are already provided as `verify_is_valid_for_dns_name`
256+
/// and `verify_is_valid_for_at_least_one_dns_name`.
257+
///
258+
/// Requires the `alloc` default feature; i.e. this isn't available in
259+
/// `#![no_std]` configurations.
260+
#[cfg(feature = "alloc")]
261+
pub fn dns_names(&self) -> Result<Vec<GeneralDnsNameRef<'a>>, Error> {
262+
name::list_cert_dns_names(&self)
263+
}
251264
}
252265

253266
/// A trust anchor (a.k.a. root CA).

tests/integration.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,112 @@ fn read_root_with_neg_serial() {
9090
fn time_constructor() {
9191
let _ = webpki::Time::try_from(std::time::SystemTime::now()).unwrap();
9292
}
93+
94+
#[cfg(feature = "std")]
95+
#[test]
96+
pub fn list_netflix_names()
97+
{
98+
let ee = include_bytes!("netflix/ee.der");
99+
100+
expect_cert_dns_names(ee, &[
101+
"account.netflix.com",
102+
"ca.netflix.com",
103+
"netflix.ca",
104+
"netflix.com",
105+
"signup.netflix.com",
106+
"www.netflix.ca",
107+
"www1.netflix.com",
108+
"www2.netflix.com",
109+
"www3.netflix.com",
110+
"develop-stage.netflix.com",
111+
"release-stage.netflix.com",
112+
"www.netflix.com",
113+
]);
114+
}
115+
116+
#[cfg(feature = "std")]
117+
#[test]
118+
pub fn invalid_subject_alt_names()
119+
{
120+
// same as netflix ee certificate, but with the last name in the list
121+
// changed to 'www.netflix:com'
122+
let data = include_bytes!("misc/invalid_subject_alternative_name.der");
123+
124+
expect_cert_dns_names(data, &[
125+
"account.netflix.com",
126+
"ca.netflix.com",
127+
"netflix.ca",
128+
"netflix.com",
129+
"signup.netflix.com",
130+
"www.netflix.ca",
131+
"www1.netflix.com",
132+
"www2.netflix.com",
133+
"www3.netflix.com",
134+
"develop-stage.netflix.com",
135+
"release-stage.netflix.com",
136+
// NOT 'www.netflix:com'
137+
]);
138+
}
139+
140+
#[cfg(feature = "std")]
141+
#[test]
142+
pub fn wildcard_subject_alternative_names()
143+
{
144+
// same as netflix ee certificate, but with the last name in the list
145+
// changed to 'ww*.netflix:com'
146+
let data = include_bytes!("misc/dns_names_and_wildcards.der");
147+
148+
expect_cert_dns_names(data, &[
149+
"account.netflix.com",
150+
"*.netflix.com",
151+
"netflix.ca",
152+
"netflix.com",
153+
"signup.netflix.com",
154+
"www.netflix.ca",
155+
"www1.netflix.com",
156+
"www2.netflix.com",
157+
"www3.netflix.com",
158+
"develop-stage.netflix.com",
159+
"release-stage.netflix.com",
160+
"www.netflix.com"
161+
]);
162+
}
163+
164+
#[cfg(feature = "std")]
165+
fn expect_cert_dns_names(data: &[u8], expected_names: &[&str])
166+
{
167+
use std::iter::FromIterator;
168+
169+
let cert = webpki::EndEntityCert::from(data)
170+
.expect("should parse end entity certificate correctly");
171+
172+
let expected_names =
173+
std::collections::HashSet::from_iter(expected_names.iter().cloned());
174+
175+
let mut actual_names = cert.dns_names()
176+
.expect("should get all DNS names correctly for end entity cert");
177+
178+
// Ensure that converting the list to a set doesn't throw away
179+
// any duplicates that aren't supposed to be there
180+
assert_eq!(actual_names.len(), expected_names.len());
181+
182+
let actual_names: std::collections::HashSet<&str> = actual_names.drain(..).map(|name| {
183+
name.into()
184+
}).collect();
185+
186+
assert_eq!(actual_names, expected_names);
187+
}
188+
189+
#[cfg(feature = "std")]
190+
#[test]
191+
pub fn no_subject_alt_names()
192+
{
193+
let data = include_bytes!("misc/no_subject_alternative_name.der");
194+
195+
let cert = webpki::EndEntityCert::from(data)
196+
.expect("should parse end entity certificate correctly");
197+
198+
let names = cert.dns_names().expect("we should get a result even without subjectAltNames");
199+
200+
assert!(names.is_empty());
201+
}
1.18 KB
Binary file not shown.
1.73 KB
Binary file not shown.
797 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)