diff --git a/src/name.rs b/src/name.rs index 82a85373..40afe88f 100644 --- a/src/name.rs +++ b/src/name.rs @@ -20,6 +20,9 @@ use untrusted; #[cfg(feature = "std")] use std::string::String; +#[cfg(feature = "std")] +use std::vec::Vec; + /// 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. @@ -148,6 +151,27 @@ pub fn verify_cert_dns_name(cert: &super::EndEntityCert, }) } + +#[cfg(feature = "std")] +pub fn list_cert_dns_names<'names>(cert: &super::EndEntityCert<'names>) + -> Result>, Error> { + let cert = &cert.inner; + let names = std::cell::RefCell::new(Vec::new()); + + iterate_names(cert.subject, cert.subject_alt_name, Ok(()), &|name| { + match name { + GeneralName::DNSName(presented_id) => { + match DNSNameRef::try_from_ascii(presented_id) { + Ok(name) => names.borrow_mut().push(name), + Err(()) => { /* keep going */ }, + }; + }, + _ => () + } + NameIteration::KeepGoing + }).map(|_| names.into_inner()) +} + // https://tools.ietf.org/html/rfc5280#section-4.2.1.10 pub fn check_name_constraints<'a>(input: Option<&mut untrusted::Reader<'a>>, subordinate_certs: &Cert) @@ -370,10 +394,10 @@ enum NameIteration { Stop(Result<(), Error>) } -fn iterate_names(subject: untrusted::Input, - subject_alt_name: Option, +fn iterate_names<'input>(subject: untrusted::Input<'input>, + subject_alt_name: Option>, result_if_never_stopped_early: Result<(), Error>, - f: &Fn(GeneralName) -> NameIteration) -> Result<(), Error> { + f: &Fn(GeneralName<'input>) -> NameIteration) -> Result<(), Error> { match subject_alt_name { Some(subject_alt_name) => { let mut subject_alt_name = untrusted::Reader::new(subject_alt_name); diff --git a/src/webpki.rs b/src/webpki.rs index f9f1dcc9..3012e878 100644 --- a/src/webpki.rs +++ b/src/webpki.rs @@ -248,6 +248,19 @@ impl <'a> EndEntityCert<'a> { signed_data::verify_signature(signature_alg, self.inner.spki, msg, 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 `std` default feature; i.e. this isn't available in + /// `#![no_std]` configurations. + #[cfg(feature = "std")] + pub fn dns_names(&self) -> Result>, Error> { + name::list_cert_dns_names(&self) + } } /// A trust anchor (a.k.a. root CA). diff --git a/tests/integration.rs b/tests/integration.rs index 8dc919b3..99c21cfa 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -130,3 +130,114 @@ fn time_constructor() { let _ = webpki::Time::try_from(std::time::SystemTime::now()).unwrap(); } + +#[cfg(feature = "std")] +#[test] +pub fn list_netflix_names() +{ + let ee = include_bytes!("netflix/ee.der"); + + expect_cert_dns_names(ee, &[ + "account.netflix.com", + "ca.netflix.com", + "netflix.ca", + "netflix.com", + "signup.netflix.com", + "www.netflix.ca", + "www1.netflix.com", + "www2.netflix.com", + "www3.netflix.com", + "develop-stage.netflix.com", + "release-stage.netflix.com", + "www.netflix.com", + ]); +} + +#[cfg(feature = "std")] +#[test] +pub fn invalid_subject_alt_names() +{ + // same as netflix ee certificate, but with the last name in the list + // changed to 'www.netflix:com' + let data = include_bytes!("misc/invalid_subject_alternative_name.der"); + + expect_cert_dns_names(data, &[ + "account.netflix.com", + "ca.netflix.com", + "netflix.ca", + "netflix.com", + "signup.netflix.com", + "www.netflix.ca", + "www1.netflix.com", + "www2.netflix.com", + "www3.netflix.com", + "develop-stage.netflix.com", + "release-stage.netflix.com", + // NOT 'www.netflix:com' + ]); +} + +#[cfg(feature = "std")] +#[test] +pub fn wildcard_subject_alternative_names() +{ + // same as netflix ee certificate, but with the last name in the list + // changed to 'ww*.netflix:com' + let data = include_bytes!("misc/dns_names_and_wildcards.der"); + + expect_cert_dns_names(data, &[ + "account.netflix.com", + // NOT "c*.netflix.com", + "netflix.ca", + "netflix.com", + "signup.netflix.com", + "www.netflix.ca", + "www1.netflix.com", + "www2.netflix.com", + "www3.netflix.com", + "develop-stage.netflix.com", + "release-stage.netflix.com", + "www.netflix.com" + ]); +} + +#[cfg(feature = "std")] +fn expect_cert_dns_names(data: &[u8], expected_names: &[&str]) +{ + use std::iter::FromIterator; + + let input = untrusted::Input::from(data); + let cert = webpki::EndEntityCert::from(input) + .expect("should parse end entity certificate correctly"); + + let expected_names = + std::collections::HashSet::from_iter(expected_names.iter().cloned()); + + let mut actual_names = cert.dns_names() + .expect("should get all DNS names correctly for end entity cert"); + + // Ensure that converting the list to a set doesn't throw away + // any duplicates that aren't supposed to be there + assert_eq!(actual_names.len(), expected_names.len()); + + let actual_names: std::collections::HashSet<&str> = actual_names.drain(..).map(|name| { + name.into() + }).collect(); + + assert_eq!(actual_names, expected_names); +} + +#[cfg(feature = "std")] +#[test] +pub fn no_subject_alt_names() +{ + let data = include_bytes!("misc/no_subject_alternative_name.der"); + + let input = untrusted::Input::from(data); + let cert = webpki::EndEntityCert::from(input) + .expect("should parse end entity certificate correctly"); + + let names = cert.dns_names().expect("we should get a result even without subjectAltNames"); + + assert!(names.is_empty()); +} diff --git a/tests/misc/dns_names_and_wildcards.der b/tests/misc/dns_names_and_wildcards.der new file mode 100644 index 00000000..4c9713dd Binary files /dev/null and b/tests/misc/dns_names_and_wildcards.der differ diff --git a/tests/misc/invalid_subject_alternative_name.der b/tests/misc/invalid_subject_alternative_name.der new file mode 100644 index 00000000..f81ad7b7 Binary files /dev/null and b/tests/misc/invalid_subject_alternative_name.der differ diff --git a/tests/misc/no_subject_alternative_name.der b/tests/misc/no_subject_alternative_name.der new file mode 100644 index 00000000..8292e358 Binary files /dev/null and b/tests/misc/no_subject_alternative_name.der differ