Skip to content

Commit 2534131

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

8 files changed

+294
-9
lines changed

src/name.rs

+2-2
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, InvalidDnsNameError, WildcardDnsNameRef};
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, list_cert_dns_names, verify_cert_dns_name};

src/name/dns_name.rs

+130-1
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,131 @@ 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 {
195+
WildcardDnsNameRef(self.0.as_bytes())
196+
}
197+
}
198+
199+
#[cfg(feature = "alloc")]
200+
impl AsRef<str> for WildcardDnsName {
201+
fn as_ref(&self) -> &str {
202+
self.0.as_ref()
203+
}
204+
}
205+
206+
// Deprecated
207+
#[cfg(feature = "alloc")]
208+
impl From<WildcardDnsNameRef<'_>> for WildcardDnsName {
209+
fn from(dns_name: WildcardDnsNameRef) -> Self {
210+
dns_name.to_owned()
211+
}
212+
}
213+
214+
/// A reference to a DNS Name suitable for use in the TLS Server Name Indication
215+
/// (SNI) extension and/or for use as the reference hostname for which to verify
216+
/// a certificate.
217+
///
218+
/// A `WildcardDnsNameRef` is guaranteed to be syntactically valid. The validity rules
219+
/// are specified in [RFC 5280 Section 7.2], except that underscores are also
220+
/// allowed.
221+
///
222+
/// `Eq`, `PartialEq`, etc. are not implemented because name comparison
223+
/// frequently should be done case-insensitively and/or with other caveats that
224+
/// depend on the specific circumstances in which the comparison is done.
225+
///
226+
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
227+
#[derive(Clone, Copy)]
228+
pub struct WildcardDnsNameRef<'a>(&'a [u8]);
229+
230+
impl<'a> WildcardDnsNameRef<'a> {
231+
/// Constructs a `WildcardDnsNameRef` from the given input if the input is a
232+
/// syntactically-valid DNS name.
233+
pub fn try_from_ascii(dns_name: &'a [u8]) -> Result<Self, InvalidDnsNameError> {
234+
if !is_valid_wildcard_dns_id(untrusted::Input::from(dns_name)) {
235+
return Err(InvalidDnsNameError);
236+
}
237+
238+
Ok(Self(dns_name))
239+
}
240+
241+
/// Constructs a `WildcardDnsNameRef` from the given input if the input is a
242+
/// syntactically-valid DNS name.
243+
pub fn try_from_ascii_str(dns_name: &'a str) -> Result<Self, InvalidDnsNameError> {
244+
Self::try_from_ascii(dns_name.as_bytes())
245+
}
246+
247+
/// Constructs a `WildcardDnsName` from this `WildcardDnsNameRef`
248+
#[cfg(feature = "alloc")]
249+
pub fn to_owned(&self) -> WildcardDnsName {
250+
// WildcardDnsNameRef is already guaranteed to be valid ASCII, which is a
251+
// subset of UTF-8.
252+
let s: &str = self.clone().into();
253+
WildcardDnsName(s.to_ascii_lowercase())
254+
}
255+
}
256+
257+
#[cfg(feature = "alloc")]
258+
impl core::fmt::Debug for WildcardDnsNameRef<'_> {
259+
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
260+
let lowercase = self.clone().to_owned();
261+
f.debug_tuple("WildcardDnsNameRef")
262+
.field(&lowercase.0)
263+
.finish()
264+
}
265+
}
266+
267+
impl<'a> From<WildcardDnsNameRef<'a>> for &'a str {
268+
fn from(WildcardDnsNameRef(d): WildcardDnsNameRef<'a>) -> Self {
269+
// The unwrap won't fail because DnsNameRefs are guaranteed to be ASCII
270+
// and ASCII is a subset of UTF-8.
271+
core::str::from_utf8(d).unwrap()
272+
}
273+
}
274+
150275
pub(super) fn presented_id_matches_reference_id(
151276
presented_dns_id: untrusted::Input,
152277
reference_dns_id: untrusted::Input,
@@ -577,6 +702,10 @@ fn is_valid_dns_id(
577702
true
578703
}
579704

705+
fn is_valid_wildcard_dns_id(hostname: untrusted::Input) -> bool {
706+
is_valid_dns_id(hostname, IDRole::ReferenceID, AllowWildcards::Yes)
707+
}
708+
580709
#[cfg(test)]
581710
mod tests {
582711
use super::*;

src/name/verify.rs

+34-5
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,33 @@ fn iterate_names(
279281
}
280282
}
281283

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

src/webpki.rs

+14-1
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::{DnsNameRef, GeneralDnsNameRef, InvalidDnsNameError, WildcardDnsNameRef};
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

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