use cert::{Cert, EndEntityOrCA};
use core;
use {der, Error};
use untrusted;
#[cfg(feature = "std")]
use std;
#[cfg(feature = "std")]
use std::string::String;
#[cfg(feature = "std")]
#[derive(Clone, Debug)]
pub struct DNSName(String);
#[cfg(feature = "std")]
impl DNSName {
pub fn as_ref(&self) -> DNSNameRef {
DNSNameRef(untrusted::Input::from(self.0.as_bytes()))
}
}
#[cfg(feature = "std")]
impl<'a> From<DNSNameRef<'a>> for DNSName {
fn from(DNSNameRef(dns_name): DNSNameRef<'a>) -> Self {
let s = std::str::from_utf8(dns_name.as_slice_less_safe()).unwrap();
DNSName(String::from(s))
}
}
#[derive(Clone, Copy)]
pub struct DNSNameRef<'a>(untrusted::Input<'a>);
impl<'a> DNSNameRef<'a> {
pub fn try_from_ascii(dns_name: untrusted::Input<'a>) -> Result<Self, ()> {
if !is_valid_reference_dns_id(dns_name) {
return Err(());
}
Ok(DNSNameRef(dns_name))
}
pub fn try_from_ascii_str(dns_name: &str) -> Result<DNSNameRef, ()> {
DNSNameRef::try_from_ascii(untrusted::Input::from(dns_name.as_bytes()))
}
}
#[cfg(feature = "std")]
impl<'a> core::fmt::Debug for DNSNameRef<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
DNSName::from(*self).fmt(f)
}
}
impl<'a> Into<&'a str> for DNSNameRef<'a> {
fn into(self) -> &'a str {
core::str::from_utf8(self.0.as_slice_less_safe()).unwrap()
}
}
impl<'a> From<DNSNameRef<'a>> for untrusted::Input<'a> {
fn from(DNSNameRef(dns_name): DNSNameRef<'a>) -> Self { dns_name }
}
pub fn verify_cert_dns_name(cert: &super::EndEntityCert,
DNSNameRef(dns_name): DNSNameRef)
-> Result<(), Error> {
let cert = &cert.inner;
iterate_names(cert.subject, cert.subject_alt_name,
Err(Error::CertNotValidForName), &|name| {
match name {
GeneralName::DNSName(presented_id) => {
match presented_dns_id_matches_reference_dns_id(
presented_id, IDRole::ReferenceID, dns_name) {
Some(true) => { return NameIteration::Stop(Ok(())); },
Some(false) => (),
None => { return NameIteration::Stop(Err(Error::BadDER)); },
}
},
_ => ()
}
NameIteration::KeepGoing
})
}
pub fn check_name_constraints<'a>(input: Option<&mut untrusted::Reader<'a>>,
subordinate_certs: &Cert)
-> Result<(), Error> {
let input = match input {
Some(input) => input,
None => { return Ok(()); }
};
fn parse_subtrees<'b>(inner: &mut untrusted::Reader<'b>,
subtrees_tag: der::Tag)
-> Result<Option<untrusted::Input<'b>>, Error> {
if !inner.peek(subtrees_tag as u8) {
return Ok(None);
}
let subtrees = der::nested(inner, subtrees_tag, Error::BadDER, |tagged| {
der::expect_tag_and_get_value(tagged, der::Tag::Sequence)
})?;
Ok(Some(subtrees))
}
let permitted_subtrees =
parse_subtrees(input, der::Tag::ContextSpecificConstructed0)?;
let excluded_subtrees =
parse_subtrees(input, der::Tag::ContextSpecificConstructed1)?;
let mut child = subordinate_certs;
loop {
iterate_names(child.subject, child.subject_alt_name, Ok(()),
&|name| check_presented_id_conforms_to_constraints(
name, permitted_subtrees, excluded_subtrees))?;
child = match child.ee_or_ca {
EndEntityOrCA::CA(child_cert) => child_cert,
EndEntityOrCA::EndEntity => { break; }
};
}
Ok(())
}
fn check_presented_id_conforms_to_constraints(
name: GeneralName, permitted_subtrees: Option<untrusted::Input>,
excluded_subtrees: Option<untrusted::Input>) -> NameIteration {
match check_presented_id_conforms_to_constraints_in_subtree(
name, Subtrees::PermittedSubtrees, permitted_subtrees) {
stop @ NameIteration::Stop(..) => { return stop; },
NameIteration::KeepGoing => ()
};
check_presented_id_conforms_to_constraints_in_subtree(
name, Subtrees::ExcludedSubtrees, excluded_subtrees)
}
#[derive(Clone, Copy)]
enum Subtrees {
PermittedSubtrees,
ExcludedSubtrees
}
fn check_presented_id_conforms_to_constraints_in_subtree(
name: GeneralName, subtrees: Subtrees,
constraints: Option<untrusted::Input>) -> NameIteration {
let mut constraints = match constraints {
Some(constraints) => untrusted::Reader::new(constraints),
None => { return NameIteration::KeepGoing; }
};
let mut has_permitted_subtrees_match = false;
let mut has_permitted_subtrees_mismatch = false;
loop {
fn general_subtree<'b>(input: &mut untrusted::Reader<'b>)
-> Result<GeneralName<'b>, Error> {
let general_subtree =
der::expect_tag_and_get_value(input, der::Tag::Sequence)?;
general_subtree.read_all(Error::BadDER,
|subtree| general_name(subtree))
}
let base = match general_subtree(&mut constraints) {
Ok(base) => base,
Err(err) => { return NameIteration::Stop(Err(err)); }
};
let matches = match (name, base) {
(GeneralName::DNSName(name),
GeneralName::DNSName(base)) =>
presented_dns_id_matches_reference_dns_id(
name, IDRole::NameConstraint, base)
.ok_or(Error::BadDER),
(GeneralName::DirectoryName(name),
GeneralName::DirectoryName(base)) =>
presented_directory_name_matches_constraint(name, base,
subtrees),
(GeneralName::IPAddress(name),
GeneralName::IPAddress(base)) =>
presented_ip_address_matches_constraint(name, base),
(GeneralName::Unsupported(name_tag),
GeneralName::Unsupported(base_tag)) if name_tag == base_tag =>
Err(Error::NameConstraintViolation),
_ => Ok(false)
};
match (subtrees, matches) {
(Subtrees::PermittedSubtrees, Ok(true)) => {
has_permitted_subtrees_match = true;
},
(Subtrees::PermittedSubtrees, Ok(false)) => {
has_permitted_subtrees_mismatch = true;
},
(Subtrees::ExcludedSubtrees, Ok(true)) => {
return NameIteration::Stop(Err(Error::NameConstraintViolation));
},
(Subtrees::ExcludedSubtrees, Ok(false)) => (),
(_, Err(err)) => {
return NameIteration::Stop(Err(err));
}
}
if constraints.at_end() {
break;
}
}
if has_permitted_subtrees_mismatch && !has_permitted_subtrees_match {
NameIteration::Stop(Err(Error::NameConstraintViolation))
} else {
NameIteration::KeepGoing
}
}
fn presented_directory_name_matches_constraint<'a>(
name: untrusted::Input<'a>, constraint: untrusted::Input<'a>,
subtrees: Subtrees) -> Result<bool, Error> {
match subtrees {
Subtrees::PermittedSubtrees => Ok(name == constraint),
Subtrees::ExcludedSubtrees => Ok(true),
}
}
fn presented_ip_address_matches_constraint(name: untrusted::Input,
constraint: untrusted::Input)
-> Result<bool, Error> {
if name.len() != 4 && name.len() != 16 {
return Err(Error::BadDER);
}
if constraint.len() != 8 && constraint.len() != 32 {
return Err(Error::BadDER);
}
if name.len() * 2 != constraint.len() {
return Ok(false);
}
let (constraint_address, constraint_mask) =
constraint.read_all(Error::BadDER, |value| {
let address = value.skip_and_get_input(constraint.len() / 2).unwrap();
let mask = value.skip_and_get_input(constraint.len() / 2).unwrap();
Ok((address, mask))
})?;
let mut name = untrusted::Reader::new(name);
let mut constraint_address = untrusted::Reader::new(constraint_address);
let mut constraint_mask = untrusted::Reader::new(constraint_mask);
loop {
let name_byte = name.read_byte().unwrap();
let constraint_address_byte = constraint_address.read_byte().unwrap();
let constraint_mask_byte = constraint_mask.read_byte().unwrap();
if ((name_byte ^ constraint_address_byte) & constraint_mask_byte) != 0 {
return Ok(false);
}
if name.at_end() {
break;
}
}
return Ok(true);
}
#[derive(Clone, Copy)]
enum NameIteration {
KeepGoing,
Stop(Result<(), Error>)
}
fn iterate_names(subject: untrusted::Input,
subject_alt_name: Option<untrusted::Input>,
result_if_never_stopped_early: Result<(), Error>,
f: &Fn(GeneralName) -> NameIteration) -> Result<(), Error> {
match subject_alt_name {
Some(subject_alt_name) => {
let mut subject_alt_name = untrusted::Reader::new(subject_alt_name);
while !subject_alt_name.at_end() {
let name = general_name(&mut subject_alt_name)?;
match f(name) {
NameIteration::Stop(result) => { return result; },
NameIteration::KeepGoing => ()
}
}
},
None => ()
}
match f(GeneralName::DirectoryName(subject)) {
NameIteration::Stop(result) => result,
NameIteration::KeepGoing => result_if_never_stopped_early
}
}
#[derive(Clone, Copy)]
enum GeneralName<'a> {
DNSName(untrusted::Input<'a>),
DirectoryName(untrusted::Input<'a>),
IPAddress(untrusted::Input<'a>),
Unsupported(u8)
}
fn general_name<'a>(input: &mut untrusted::Reader<'a>)
-> Result<GeneralName<'a>, Error> {
use ring::der::{CONSTRUCTED, CONTEXT_SPECIFIC};
const OTHER_NAME_TAG: u8 = CONTEXT_SPECIFIC | CONSTRUCTED | 0;
const RFC822_NAME_TAG: u8 = CONTEXT_SPECIFIC | 1;
const DNS_NAME_TAG: u8 = CONTEXT_SPECIFIC | 2;
const X400_ADDRESS_TAG : u8 = CONTEXT_SPECIFIC | CONSTRUCTED | 3;
const DIRECTORY_NAME_TAG: u8 = CONTEXT_SPECIFIC | CONSTRUCTED | 4;
const EDI_PARTY_NAME_TAG: u8 = CONTEXT_SPECIFIC | CONSTRUCTED | 5;
const UNIFORM_RESOURCE_IDENTIFIER_TAG: u8 = CONTEXT_SPECIFIC | 6;
const IP_ADDRESS_TAG: u8 = CONTEXT_SPECIFIC | 7;
const REGISTERED_ID_TAG: u8 = CONTEXT_SPECIFIC | 8;
let (tag, value) = der::read_tag_and_get_value(input)?;
let name = match tag {
DNS_NAME_TAG => GeneralName::DNSName(value),
DIRECTORY_NAME_TAG => GeneralName::DirectoryName(value),
IP_ADDRESS_TAG => GeneralName::IPAddress(value),
OTHER_NAME_TAG |
RFC822_NAME_TAG |
X400_ADDRESS_TAG |
EDI_PARTY_NAME_TAG |
UNIFORM_RESOURCE_IDENTIFIER_TAG |
REGISTERED_ID_TAG =>
GeneralName::Unsupported(tag & !(CONTEXT_SPECIFIC | CONSTRUCTED)),
_ => return Err(Error::BadDER)
};
Ok(name)
}
fn presented_dns_id_matches_reference_dns_id(
presented_dns_id: untrusted::Input, reference_dns_id_role: IDRole,
reference_dns_id: untrusted::Input) -> Option<bool> {
if !is_valid_dns_id(presented_dns_id, IDRole::PresentedID,
AllowWildcards::Yes) {
return None;
}
if !is_valid_dns_id(reference_dns_id, reference_dns_id_role,
AllowWildcards::No) {
return None;
}
let mut presented = untrusted::Reader::new(presented_dns_id);
let mut reference = untrusted::Reader::new(reference_dns_id);
match reference_dns_id_role {
IDRole::ReferenceID => (),
IDRole::NameConstraint
if presented_dns_id.len() > reference_dns_id.len() => {
if reference_dns_id.len() == 0 {
return Some(true);
}
if reference.peek(b'.') {
if presented.skip(presented_dns_id.len() -
reference_dns_id.len()).is_err() {
unreachable!();
}
} else {
if presented.skip(presented_dns_id.len() -
reference_dns_id.len() - 1).is_err() {
unreachable!();
}
if presented.read_byte() != Ok(b'.') {
return Some(false);
}
}
},
IDRole::NameConstraint => (),
IDRole::PresentedID => unreachable!()
}
if presented.peek(b'*') {
if presented.skip(1).is_err() {
unreachable!();
}
loop {
if reference.read_byte().is_err() {
return Some(false);
}
if reference.peek(b'.') {
break;
}
}
}
loop {
let presented_byte =
match (presented.read_byte(), reference.read_byte()) {
(Ok(p), Ok(r)) if ascii_lower(p) == ascii_lower(r) => p,
_ => { return Some(false); }
};
if presented.at_end() {
if presented_byte == b'.' {
return None;
}
break;
}
}
if !reference.at_end() {
if reference_dns_id_role != IDRole::NameConstraint {
match reference.read_byte() {
Ok(b'.') => (),
_ => { return Some(false); }
};
}
if !reference.at_end() {
return Some(false);
}
}
assert!(presented.at_end());
assert!(reference.at_end());
return Some(true);
}
#[inline]
fn ascii_lower(b: u8) -> u8 {
match b {
b'A'...b'Z' => b + b'a' - b'A',
_ => b,
}
}
#[derive(PartialEq)]
enum AllowWildcards {
No,
Yes
}
#[derive(Clone, Copy, PartialEq)]
enum IDRole {
ReferenceID,
PresentedID,
NameConstraint,
}
fn is_valid_reference_dns_id(hostname: untrusted::Input) -> bool {
is_valid_dns_id(hostname, IDRole::ReferenceID, AllowWildcards::No)
}
fn is_valid_dns_id(hostname: untrusted::Input, id_role: IDRole,
allow_wildcards: AllowWildcards) -> bool {
if hostname.len() > 253 {
return false;
}
let mut input = untrusted::Reader::new(hostname);
if id_role == IDRole::NameConstraint && input.at_end() {
return true;
}
let mut dot_count = 0;
let mut label_length = 0;
let mut label_is_all_numeric = false;
let mut label_ends_with_hyphen = false;
let is_wildcard = allow_wildcards == AllowWildcards::Yes &&
input.peek(b'*');
let mut is_first_byte = !is_wildcard;
if is_wildcard {
if input.read_byte() != Ok(b'*') ||
input.read_byte() != Ok(b'.') {
return false;
}
dot_count += 1;
}
loop {
const MAX_LABEL_LENGTH: usize = 63;
match input.read_byte() {
Ok(b'-') => {
if label_length == 0 {
return false; }
label_is_all_numeric = false;
label_ends_with_hyphen = true;
label_length += 1;
if label_length > MAX_LABEL_LENGTH {
return false;
}
},
Ok(b'0'...b'9') => {
if label_length == 0 {
label_is_all_numeric = true;
}
label_ends_with_hyphen = false;
label_length += 1;
if label_length > MAX_LABEL_LENGTH {
return false;
}
},
Ok(b'a'...b'z') | Ok(b'A'...b'Z') | Ok(b'_') => {
label_is_all_numeric = false;
label_ends_with_hyphen = false;
label_length += 1;
if label_length > MAX_LABEL_LENGTH {
return false;
}
},
Ok(b'.') => {
dot_count += 1;
if label_length == 0 &&
(id_role != IDRole::NameConstraint || !is_first_byte) {
return false;
}
if label_ends_with_hyphen {
return false; }
label_length = 0;
},
_ => { return false; }
}
is_first_byte = false;
if input.at_end() {
break;
}
}
if label_length == 0 && id_role != IDRole::ReferenceID {
return false;
}
if label_ends_with_hyphen {
return false; }
if label_is_all_numeric {
return false; }
if is_wildcard {
let label_count = if label_length == 0 { dot_count }
else { dot_count + 1 };
if label_count < 3 {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use std::string::String;
use super::*;
use untrusted;
const PRESENTED_MATCHES_REFERENCE:
&[(&'static [u8], &'static [u8], Option<bool>)] = &[
(b"", b"a", None),
(b"a", b"a", Some(true)),
(b"b", b"a", Some(false)),
(b"*.b.a", b"c.b.a", Some(true)),
(b"*.b.a", b"b.a", Some(false)),
(b"*.b.a", b"b.a.", Some(false)),
(b"d.c.b.a", b"d.c.b.a", Some(true)),
(b"d.*.b.a", b"d.c.b.a", None),
(b"d.c*.b.a", b"d.c.b.a", None),
(b"d.c*.b.a", b"d.cc.b.a", None),
(b"abcdefghijklmnopqrstuvwxyz", b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", Some(true)),
(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", b"abcdefghijklmnopqrstuvwxyz", Some(true)),
(b"aBc", b"Abc", Some(true)),
(b"a1", b"a1", Some(true)),
(b"example", b"example", Some(true)),
(b"example.", b"example.", None),
(b"example", b"example.", Some(true)),
(b"example.", b"example", None),
(b"example.com", b"example.com", Some(true)),
(b"example.com.", b"example.com.", None),
(b"example.com", b"example.com.", Some(true)),
(b"example.com.", b"example.com", None),
(b"example.com..", b"example.com.", None),
(b"example.com..", b"example.com", None),
(b"example.com...", b"example.com.", None),
(b"x*.b.a", b"xa.b.a", None),
(b"x*.b.a", b"xna.b.a", None),
(b"x*.b.a", b"xn-a.b.a", None),
(b"x*.b.a", b"xn--a.b.a", None),
(b"xn*.b.a", b"xn--a.b.a", None),
(b"xn-*.b.a", b"xn--a.b.a", None),
(b"xn--*.b.a", b"xn--a.b.a", None),
(b"xn*.b.a", b"xn--a.b.a", None),
(b"xn-*.b.a", b"xn--a.b.a", None),
(b"xn--*.b.a", b"xn--a.b.a", None),
(b"xn---*.b.a", b"xn--a.b.a", None),
(b"c*.b.a", b"c.b.a", None),
(b"foo.com", b"foo.com", Some(true)),
(b"f", b"f", Some(true)),
(b"i", b"h", Some(false)),
(b"*.foo.com", b"bar.foo.com", Some(true)),
(b"*.test.fr", b"www.test.fr", Some(true)),
(b"*.test.FR", b"wwW.tESt.fr", Some(true)),
(b".uk", b"f.uk", None),
(b"?.bar.foo.com", b"w.bar.foo.com", None),
(b"(www|ftp).foo.com", b"www.foo.com", None), (b"www.foo.com\0", b"www.foo.com", None),
(b"www.foo.com\0*.foo.com", b"www.foo.com", None),
(b"ww.house.example", b"www.house.example", Some(false)),
(b"www.test.org", b"test.org", Some(false)),
(b"*.test.org", b"test.org", Some(false)),
(b"*.org", b"test.org", None),
(b"w*.bar.foo.com", b"w.bar.foo.com", None),
(b"ww*ww.bar.foo.com", b"www.bar.foo.com", None),
(b"ww*ww.bar.foo.com", b"wwww.bar.foo.com", None),
(b"w*w.bar.foo.com", b"wwww.bar.foo.com", None),
(b"w*w.bar.foo.c0m", b"wwww.bar.foo.com", None),
(b"wa*.bar.foo.com", b"WALLY.bar.foo.com", None),
(b"*Ly.bar.foo.com", b"wally.bar.foo.com", None),
(b"*.test.de", b"www.test.co.jp", Some(false)),
(b"*.jp", b"www.test.co.jp", None),
(b"www.test.co.uk", b"www.test.co.jp", Some(false)),
(b"www.*.co.jp", b"www.test.co.jp", None),
(b"www.bar.foo.com", b"www.bar.foo.com", Some(true)),
(b"*.foo.com", b"www.bar.foo.com", Some(false)),
(b"*.*.foo.com", b"www.bar.foo.com", None),
(b"www.bath.org", b"www.bath.org", Some(true)),
(b"xn--poema-9qae5a.com.br", b"xn--poema-9qae5a.com.br", Some(true)),
(b"*.xn--poema-9qae5a.com.br", b"www.xn--poema-9qae5a.com.br", Some(true)),
(b"*.xn--poema-9qae5a.com.br", b"xn--poema-9qae5a.com.br", Some(false)),
(b"xn--poema-*.com.br", b"xn--poema-9qae5a.com.br", None),
(b"xn--*-9qae5a.com.br", b"xn--poema-9qae5a.com.br", None),
(b"*--poema-9qae5a.com.br", b"xn--poema-9qae5a.com.br", None),
(b"*.example.com", b"foo.example.com", Some(true)),
(b"*.example.com", b"bar.foo.example.com", Some(false)),
(b"*.example.com", b"example.com", Some(false)),
(b"baz*.example.net", b"baz1.example.net", None),
(b"*baz.example.net", b"foobaz.example.net", None),
(b"b*z.example.net", b"buzz.example.net", None),
(b"*.test.example", b"www.test.example", Some(true)),
(b"*.example.co.uk", b"test.example.co.uk", Some(true)),
(b"*.example", b"test.example", None),
(b"*.co.uk", b"example.co.uk", Some(true)),
(b"*.com", b"foo.com", None),
(b"*.us", b"foo.us", None),
(b"*", b"foo", None),
(b"*.xn--poema-9qae5a.com.br", b"www.xn--poema-9qae5a.com.br", Some(true)),
(b"*.example.xn--mgbaam7a8h", b"test.example.xn--mgbaam7a8h", Some(true)),
(b"*.com.br", b"xn--poema-9qae5a.com.br", Some(true)),
(b"*.xn--mgbaam7a8h", b"example.xn--mgbaam7a8h", None),
(b"*.appspot.com", b"www.appspot.com", Some(true)),
(b"*.s3.amazonaws.com", b"foo.s3.amazonaws.com", Some(true)),
(b"*.*.com", b"foo.example.com", None),
(b"*.bar.*.com", b"foo.bar.example.com", None),
(b"foo.com.", b"foo.com", None),
(b"foo.com", b"foo.com.", Some(true)),
(b"foo.com.", b"foo.com.", None),
(b"f.", b"f", None),
(b"f", b"f.", Some(true)),
(b"f.", b"f.", None),
(b"*.bar.foo.com.", b"www-3.bar.foo.com", None),
(b"*.bar.foo.com", b"www-3.bar.foo.com.", Some(true)),
(b"*.bar.foo.com.", b"www-3.bar.foo.com.", None),
(b"*.com.", b"example.com", None),
(b"*.com", b"example.com.", None),
(b"*.com.", b"example.com.", None),
(b"*.", b"foo.", None),
(b"*.", b"foo", None),
(b"*.co.uk.", b"foo.co.uk", None),
(b"*.co.uk.", b"foo.co.uk.", None),
];
#[test]
fn presented_matches_reference_test() {
for &(presented, reference, expected_result) in PRESENTED_MATCHES_REFERENCE {
let actual_result = presented_dns_id_matches_reference_dns_id(
untrusted::Input::from(presented), IDRole::ReferenceID,
untrusted::Input::from(reference));
assert_eq!(actual_result, expected_result,
"presented_dns_id_matches_reference_dns_id(\"{}\", IDRole::ReferenceID, \"{}\")",
String::from_utf8_lossy(presented),
String::from_utf8_lossy(reference));
}
}
}