Skip to content

Commit dfbb4ef

Browse files
committed
add a method to collect the DNS names from a certificate
1 parent 0573c1e commit dfbb4ef

File tree

6 files changed

+156
-4
lines changed

6 files changed

+156
-4
lines changed

src/name.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ use crate::{
1818
};
1919
use core;
2020

21+
#[cfg(feature = "std")]
22+
use std::string::String;
23+
24+
#[cfg(feature = "std")]
25+
use std::vec::Vec;
26+
2127
/// A DNS Name suitable for use in the TLS Server Name Indication (SNI)
2228
/// extension and/or for use as the reference hostname for which to verify a
2329
/// certificate.
@@ -154,6 +160,27 @@ pub fn verify_cert_dns_name(
154160
)
155161
}
156162

163+
164+
#[cfg(feature = "std")]
165+
pub fn list_cert_dns_names<'names>(cert: &super::EndEntityCert<'names>)
166+
-> Result<Vec<DNSNameRef<'names>>, Error> {
167+
let cert = &cert.inner;
168+
let names = std::cell::RefCell::new(Vec::new());
169+
170+
iterate_names(cert.subject, cert.subject_alt_name, Ok(()), &|name| {
171+
match name {
172+
GeneralName::DNSName(presented_id) => {
173+
match DNSNameRef::try_from_ascii(presented_id) {
174+
Ok(name) => names.borrow_mut().push(name),
175+
Err(_) => { /* keep going */ },
176+
};
177+
},
178+
_ => ()
179+
}
180+
NameIteration::KeepGoing
181+
}).map(|_| names.into_inner())
182+
}
183+
157184
// https://tools.ietf.org/html/rfc5280#section-4.2.1.10
158185
pub fn check_name_constraints(
159186
input: Option<&mut untrusted::Reader>, subordinate_certs: &Cert,
@@ -384,10 +411,11 @@ enum NameIteration {
384411
Stop(Result<(), Error>),
385412
}
386413

387-
fn iterate_names(
388-
subject: untrusted::Input, subject_alt_name: Option<untrusted::Input>,
389-
result_if_never_stopped_early: Result<(), Error>, f: &dyn Fn(GeneralName) -> NameIteration,
390-
) -> Result<(), Error> {
414+
fn iterate_names<'names, F>(
415+
subject: untrusted::Input<'names>, subject_alt_name: Option<untrusted::Input<'names>>,
416+
result_if_never_stopped_early: Result<(), Error>, f: &F,
417+
) -> Result<(), Error>
418+
where F: Fn(GeneralName<'names>) -> NameIteration, {
391419
match subject_alt_name {
392420
Some(subject_alt_name) => {
393421
let mut subject_alt_name = untrusted::Reader::new(subject_alt_name);

src/webpki.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,19 @@ impl<'a> EndEntityCert<'a> {
241241
untrusted::Input::from(signature),
242242
)
243243
}
244+
245+
/// Returns a list of the DNS names provided in the subject alternative names extension
246+
///
247+
/// This function must not be used to implement custom DNS name verification.
248+
/// Verification functions are already provided as `verify_is_valid_for_dns_name`
249+
/// and `verify_is_valid_for_at_least_one_dns_name`.
250+
///
251+
/// Requires the `std` default feature; i.e. this isn't available in
252+
/// `#![no_std]` configurations.
253+
#[cfg(feature = "std")]
254+
pub fn dns_names(&self) -> Result<Vec<DNSNameRef<'a>>, Error> {
255+
name::list_cert_dns_names(&self)
256+
}
244257
}
245258

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

tests/integration.rs

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

0 commit comments

Comments
 (0)