diff --git a/README.md b/README.md index 1605cbbf..536bb6d1 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,11 @@ Changelog - Allow verification of certificates with IP address subjectAltNames. `EndEntityCert::verify_is_valid_for_subject_name` was added, and `EndEntityCert::verify_is_valid_for_dns_name` was removed. + - Make `Error` type non-exhaustive. + - Reject non-contiguous netmasks in IP address name constraints. + - Name constraints of type dNSName and iPAddress now work and are tested. + directoryName name constraints are not implemented and will prevent + path building where they appear. * 0.22.0 (2021-04-10) - last upstream release of `webpki` crate. diff --git a/src/der.rs b/src/der.rs index adcb371d..bb8213c7 100644 --- a/src/der.rs +++ b/src/der.rs @@ -14,16 +14,71 @@ use crate::{calendar, time, Error}; pub(crate) use ring::io::{ - der::{nested, Tag}, + der::{CONSTRUCTED, CONTEXT_SPECIFIC}, Positive, }; +// Copied (and extended) from ring's src/der.rs +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Copy, Eq, PartialEq)] +#[repr(u8)] +pub(crate) enum Tag { + Boolean = 0x01, + Integer = 0x02, + BitString = 0x03, + OctetString = 0x04, + OID = 0x06, + UTF8String = 0x0C, + Sequence = CONSTRUCTED | 0x10, // 0x30 + Set = CONSTRUCTED | 0x11, // 0x31 + UTCTime = 0x17, + GeneralizedTime = 0x18, + + #[allow(clippy::identity_op)] + ContextSpecificConstructed0 = CONTEXT_SPECIFIC | CONSTRUCTED | 0, + ContextSpecificConstructed1 = CONTEXT_SPECIFIC | CONSTRUCTED | 1, + ContextSpecificConstructed3 = CONTEXT_SPECIFIC | CONSTRUCTED | 3, +} + +impl From for usize { + #[allow(clippy::as_conversions)] + fn from(tag: Tag) -> Self { + tag as Self + } +} + +impl From for u8 { + #[allow(clippy::as_conversions)] + fn from(tag: Tag) -> Self { + tag as Self + } // XXX: narrowing conversion. +} + #[inline(always)] pub(crate) fn expect_tag_and_get_value<'a>( input: &mut untrusted::Reader<'a>, tag: Tag, ) -> Result, Error> { - ring::io::der::expect_tag_and_get_value(input, tag).map_err(|_| Error::BadDER) + let (actual_tag, inner) = read_tag_and_get_value(input)?; + if usize::from(tag) != usize::from(actual_tag) { + return Err(Error::BadDER); + } + Ok(inner) +} + +// TODO: investigate taking decoder as a reference to reduce generated code +// size. +pub(crate) fn nested<'a, F, R, E: Copy>( + input: &mut untrusted::Reader<'a>, + tag: Tag, + error: E, + decoder: F, +) -> Result +where + F: FnOnce(&mut untrusted::Reader<'a>) -> Result, +{ + let inner = expect_tag_and_get_value(input, tag).map_err(|_| error)?; + inner.read_all(error, decoder) } pub struct Value<'a> { diff --git a/src/error.rs b/src/error.rs index 6324bfc9..4c0b9d4d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,7 @@ use core::fmt; /// An error that occurs during certificate validation or name validation. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum Error { /// The encoding of some ASN.1 DER-encoded item is invalid. // TODO: Rename to `BadDer` in the next release. @@ -97,6 +98,11 @@ pub enum Error { /// The signature algorithm for a signature is not in the set of supported /// signature algorithms given. UnsupportedSignatureAlgorithm, + + /// A iPAddress name constraint was invalid: + /// - it had a sparse network mask (ie, cannot be written in CIDR form). + /// - it was too long or short + InvalidNetworkMaskConstraint, } impl fmt::Display for Error { diff --git a/src/subject_name/dns_name.rs b/src/subject_name/dns_name.rs index 348d9a0f..54e3c979 100644 --- a/src/subject_name/dns_name.rs +++ b/src/subject_name/dns_name.rs @@ -792,11 +792,68 @@ mod tests { untrusted::Input::from(reference), ); assert_eq!( - actual_result, - expected_result, - "presented_dns_id_matches_reference_dns_id(\"{:?}\", IDRole::ReferenceID, \"{:?}\")", - presented, - reference + actual_result, expected_result, + "presented_id_matches_reference_id(\"{:?}\", \"{:?}\")", + presented, reference + ); + } + } + + // (presented_name, constraint, expected_matches) + const PRESENTED_MATCHES_CONSTRAINT: &[(&[u8], &[u8], Option)] = &[ + // No absolute presented IDs allowed + (b".", b"", None), + (b"www.example.com.", b"", None), + (b"www.example.com.", b"www.example.com.", None), + // No absolute constraints allowed + (b"www.example.com", b".", None), + (b"www.example.com", b"www.example.com.", None), + // No wildcard in constraints allowed + (b"www.example.com", b"*.example.com", None), + // No empty presented IDs allowed + (b"", b"", None), + // Empty constraints match everything allowed + (b"example.com", b"", Some(true)), + (b"*.example.com", b"", Some(true)), + // Constraints that start with a dot + (b"www.example.com", b".example.com", Some(true)), + (b"www.example.com", b".EXAMPLE.COM", Some(true)), + (b"www.example.com", b".axample.com", Some(false)), + (b"www.example.com", b".xample.com", Some(false)), + (b"www.example.com", b".exampl.com", Some(false)), + (b"badexample.com", b".example.com", Some(false)), + // Constraints that do not start with a dot + (b"www.example.com", b"example.com", Some(true)), + (b"www.example.com", b"EXAMPLE.COM", Some(true)), + (b"www.example.com", b"axample.com", Some(false)), + (b"www.example.com", b"xample.com", Some(false)), + (b"www.example.com", b"exampl.com", Some(false)), + (b"badexample.com", b"example.com", Some(false)), + // Presented IDs with wildcard + (b"*.example.com", b".example.com", Some(true)), + (b"*.example.com", b"example.com", Some(true)), + (b"*.example.com", b"www.example.com", Some(true)), + (b"*.example.com", b"www.EXAMPLE.COM", Some(true)), + (b"*.example.com", b"www.axample.com", Some(false)), + (b"*.example.com", b".xample.com", Some(false)), + (b"*.example.com", b"xample.com", Some(false)), + (b"*.example.com", b".exampl.com", Some(false)), + (b"*.example.com", b"exampl.com", Some(false)), + // Matching IDs + (b"www.example.com", b"www.example.com", Some(true)), + ]; + + #[test] + fn presented_matches_constraint_test() { + for &(presented, constraint, expected_result) in PRESENTED_MATCHES_CONSTRAINT { + let actual_result = presented_id_matches_constraint( + untrusted::Input::from(presented), + untrusted::Input::from(constraint), + ); + assert_eq!( + actual_result, expected_result, + "presented_id_matches_constraint(\"{:?}\", \"{:?}\")", + presented, constraint ); } } diff --git a/src/subject_name/ip_address.rs b/src/subject_name/ip_address.rs index f5ac5ced..9c324949 100644 --- a/src/subject_name/ip_address.rs +++ b/src/subject_name/ip_address.rs @@ -236,17 +236,25 @@ pub(super) fn presented_id_matches_constraint( name: untrusted::Input, constraint: untrusted::Input, ) -> Result { - if name.len() != 4 && name.len() != 16 { - return Err(Error::BadDER); - } - if constraint.len() != 8 && constraint.len() != 32 { - return Err(Error::BadDER); - } + match (name.len(), constraint.len()) { + (4, 8) => (), + (16, 32) => (), - // an IPv4 address never matches an IPv6 constraint, and vice versa. - if name.len() * 2 != constraint.len() { - return Ok(false); - } + // an IPv4 address never matches an IPv6 constraint, and vice versa. + (4, 32) | (16, 8) => { + return Ok(false); + } + + // invalid constraint length + (4, _) | (16, _) => { + return Err(Error::InvalidNetworkMaskConstraint); + } + + // invalid name length, or anything else + _ => { + return Err(Error::BadDER); + } + }; let (constraint_address, constraint_mask) = constraint.read_all(Error::BadDER, |value| { let address = value.read_bytes(constraint.len() / 2).unwrap(); @@ -257,10 +265,38 @@ pub(super) fn presented_id_matches_constraint( 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); + let mut seen_zero_bit = false; + loop { + // Iterate through the name, constraint address, and constraint mask + // a byte at a time. 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(); + + // A valid mask consists of a sequence of 1 bits, followed by a + // sequence of 0 bits. Either sequence could be empty. + + let leading = constraint_mask_byte.leading_ones(); + let trailing = constraint_mask_byte.trailing_zeros(); + + // At the resolution of a single octet, a valid mask is one where + // leading_ones() and trailing_zeros() sums to 8. + // This includes all-ones and all-zeroes. + if leading + trailing != 8 { + return Err(Error::InvalidNetworkMaskConstraint); + } + + // There should be no bits set after the first octet with a zero bit is seen. + if seen_zero_bit && constraint_mask_byte != 0x00 { + return Err(Error::InvalidNetworkMaskConstraint); + } + + // Note when a zero bit is seen for later octets. + if constraint_mask_byte != 0xff { + seen_zero_bit = true; + } + if ((name_byte ^ constraint_address_byte) & constraint_mask_byte) != 0 { return Ok(false); } @@ -911,7 +947,7 @@ mod tests { untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00]), untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF]), ), - Err(Error::BadDER), + Err(Error::InvalidNetworkMaskConstraint), ); // Unmatching constraint size (longer) @@ -920,7 +956,7 @@ mod tests { untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00]), untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00]), ), - Err(Error::BadDER), + Err(Error::InvalidNetworkMaskConstraint), ); // Unmatching constraint size (IPv6 constraint for IPv4 address) @@ -1066,7 +1102,7 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00 ]), ), - Err(Error::BadDER), + Err(Error::InvalidNetworkMaskConstraint), ); // Unmatching constraint size (longer) @@ -1082,7 +1118,7 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]), ), - Err(Error::BadDER), + Err(Error::InvalidNetworkMaskConstraint), ); // Unmatching constraint size (IPv4 constraint for IPv6 address) @@ -1156,6 +1192,89 @@ mod tests { Ok(true), ); } + + #[test] + fn presented_id_matches_constraint_rejects_incorrect_length_arguments() { + // wrong length names + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(b"\x00\x00\x00"), + untrusted::Input::from(b"") + ), + Err(Error::BadDER) + ); + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(b"\x00\x00\x00\x00\x00"), + untrusted::Input::from(b"") + ), + Err(Error::BadDER) + ); + + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from( + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + ), + untrusted::Input::from(b"") + ), + Err(Error::BadDER) + ); + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from( + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + ), + untrusted::Input::from(b"") + ), + Err(Error::BadDER) + ); + + // wrong length constraints + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(b"\x00\x00\x00\x00"), + untrusted::Input::from(b"\x00\x00\x00\x00\xff\xff\xff") + ), + Err(Error::InvalidNetworkMaskConstraint) + ); + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(b"\x00\x00\x00\x00"), + untrusted::Input::from(b"\x00\x00\x00\x00\xff\xff\xff\xff\x00") + ), + Err(Error::InvalidNetworkMaskConstraint) + ); + assert_eq!( + presented_id_matches_constraint(untrusted::Input::from(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), + untrusted::Input::from(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ + \xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff")), + Err(Error::InvalidNetworkMaskConstraint) + ); + assert_eq!( + presented_id_matches_constraint(untrusted::Input::from(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), + untrusted::Input::from(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ + \xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff")), + Err(Error::InvalidNetworkMaskConstraint) + ); + + // ipv4-length not considered for ipv6-length name, and vv + assert_eq!( + presented_id_matches_constraint(untrusted::Input::from(b"\x00\x00\x00\x00"), + untrusted::Input::from(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ + \xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff")), + Ok(false) + ); + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from( + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + ), + untrusted::Input::from(b"\x00\x00\x00\x00\xff\xff\xff\xff") + ), + Ok(false) + ); + } } #[cfg(all(test, feature = "alloc"))] @@ -1293,4 +1412,142 @@ mod alloc_tests { ) } } + + // (presented_address, constraint_address, constraint_mask, expected_result) + const PRESENTED_MATCHES_CONSTRAINT: &[(&str, &str, &str, Result)] = &[ + // Cannot mix IpV4 with IpV6 and viceversa + ("2001:db8::", "8.8.8.8", "255.255.255.255", Ok(false)), + ("8.8.8.8", "2001:db8::", "ffff::", Ok(false)), + // IpV4 non-contiguous masks + ( + "8.8.8.8", + "8.8.8.8", + "255.255.255.1", + Err(Error::InvalidNetworkMaskConstraint), + ), + ( + "8.8.8.8", + "8.8.8.8", + "255.255.0.255", + Err(Error::InvalidNetworkMaskConstraint), + ), + ( + "8.8.8.8", + "8.8.8.8", + "255.0.255.255", + Err(Error::InvalidNetworkMaskConstraint), + ), + ( + "8.8.8.8", + "8.8.8.8", + "0.255.255.255", + Err(Error::InvalidNetworkMaskConstraint), + ), + ( + "8.8.8.8", + "8.8.8.8", + "1.255.255.255", + Err(Error::InvalidNetworkMaskConstraint), + ), + ( + "8.8.8.8", + "8.8.8.8", + "128.128.128.128", + Err(Error::InvalidNetworkMaskConstraint), + ), + // IpV4 + ("8.8.8.8", "8.8.8.8", "255.255.255.255", Ok(true)), + ("8.8.8.9", "8.8.8.8", "255.255.255.255", Ok(false)), + ("8.8.8.9", "8.8.8.8", "255.255.255.254", Ok(true)), + ("8.8.8.10", "8.8.8.8", "255.255.255.254", Ok(false)), + ("8.8.8.10", "8.8.8.8", "255.255.255.0", Ok(true)), + ("8.8.15.10", "8.8.8.8", "255.255.248.0", Ok(true)), + ("8.8.16.10", "8.8.8.8", "255.255.248.0", Ok(false)), + ("8.8.16.10", "8.8.8.8", "255.255.0.0", Ok(true)), + ("8.31.16.10", "8.8.8.8", "255.224.0.0", Ok(true)), + ("8.32.16.10", "8.8.8.8", "255.224.0.0", Ok(false)), + ("8.32.16.10", "8.8.8.8", "255.0.0.0", Ok(true)), + ("63.32.16.10", "8.8.8.8", "192.0.0.0", Ok(true)), + ("64.32.16.10", "8.8.8.8", "192.0.0.0", Ok(false)), + ("64.32.16.10", "8.8.8.8", "0.0.0.0", Ok(true)), + // IpV6 non-contiguous masks + ( + "2001:db8::", + "2001:db8::", + "fffe:ffff::", + Err(Error::InvalidNetworkMaskConstraint), + ), + ( + "2001:db8::", + "2001:db8::", + "ffff:fdff::", + Err(Error::InvalidNetworkMaskConstraint), + ), + ( + "2001:db8::", + "2001:db8::", + "ffff:feff::", + Err(Error::InvalidNetworkMaskConstraint), + ), + ( + "2001:db8::", + "2001:db8::", + "ffff:fcff::", + Err(Error::InvalidNetworkMaskConstraint), + ), + ( + "2001:db8::", + "2001:db8::", + "7fff:ffff::", + Err(Error::InvalidNetworkMaskConstraint), + ), + // IpV6 + ("2001:db8::", "2001:db8::", "ffff:ffff::", Ok(true)), + ("2001:db9::", "2001:db8::", "ffff:ffff::", Ok(false)), + ("2001:db9::", "2001:db8::", "ffff:fffe::", Ok(true)), + ("2001:dba::", "2001:db8::", "ffff:fffe::", Ok(false)), + ("2001:dba::", "2001:db8::", "ffff:ff00::", Ok(true)), + ("2001:dca::", "2001:db8::", "ffff:fe00::", Ok(true)), + ("2001:fca::", "2001:db8::", "ffff:fe00::", Ok(false)), + ("2001:fca::", "2001:db8::", "ffff:0000::", Ok(true)), + ("2000:fca::", "2001:db8::", "fffe:0000::", Ok(true)), + ("2003:fca::", "2001:db8::", "fffe:0000::", Ok(false)), + ("2003:fca::", "2001:db8::", "ff00:0000::", Ok(true)), + ("1003:fca::", "2001:db8::", "e000:0000::", Ok(false)), + ("1003:fca::", "2001:db8::", "0000:0000::", Ok(true)), + ]; + + #[cfg(feature = "std")] + #[test] + fn presented_matches_constraint_test() { + use std::boxed::Box; + use std::net::IpAddr; + + for &(presented, constraint_address, constraint_mask, expected_result) in + PRESENTED_MATCHES_CONSTRAINT + { + let presented_bytes: Box<[u8]> = match presented.parse::().unwrap() { + IpAddr::V4(p) => Box::new(p.octets()), + IpAddr::V6(p) => Box::new(p.octets()), + }; + let ca_bytes: Box<[u8]> = match constraint_address.parse::().unwrap() { + IpAddr::V4(ca) => Box::new(ca.octets()), + IpAddr::V6(ca) => Box::new(ca.octets()), + }; + let cm_bytes: Box<[u8]> = match constraint_mask.parse::().unwrap() { + IpAddr::V4(cm) => Box::new(cm.octets()), + IpAddr::V6(cm) => Box::new(cm.octets()), + }; + let constraint_bytes = [ca_bytes, cm_bytes].concat(); + let actual_result = presented_id_matches_constraint( + untrusted::Input::from(&presented_bytes), + untrusted::Input::from(&constraint_bytes), + ); + assert_eq!( + actual_result, expected_result, + "presented_id_matches_constraint(\"{:?}\", \"{:?}\")", + presented_bytes, constraint_bytes + ); + } + } } diff --git a/src/subject_name/mod.rs b/src/subject_name/mod.rs index a3588e39..a27d5920 100644 --- a/src/subject_name/mod.rs +++ b/src/subject_name/mod.rs @@ -29,4 +29,6 @@ pub use ip_address::{AddrParseError, IpAddrRef}; pub use ip_address::IpAddr; mod verify; -pub(super) use verify::{check_name_constraints, verify_cert_subject_name}; +pub(super) use verify::{ + check_name_constraints, verify_cert_subject_name, SubjectCommonNameContents, +}; diff --git a/src/subject_name/verify.rs b/src/subject_name/verify.rs index fabebeed..8b958e04 100644 --- a/src/subject_name/verify.rs +++ b/src/subject_name/verify.rs @@ -31,6 +31,7 @@ pub(crate) fn verify_cert_dns_name( iterate_names( Some(cert.subject), cert.subject_alt_name, + SubjectCommonNameContents::Ignore, Err(Error::CertNotValidForName), &|name| { if let GeneralName::DnsName(presented_id) = name { @@ -64,6 +65,7 @@ pub(crate) fn verify_cert_subject_name( // only against Subject Alternative Names. None, cert.inner().subject_alt_name, + SubjectCommonNameContents::Ignore, Err(Error::CertNotValidForName), &|name| { if let GeneralName::IpAddress(presented_id) = name { @@ -84,6 +86,7 @@ pub(crate) fn verify_cert_subject_name( pub(crate) fn check_name_constraints( input: Option<&mut untrusted::Reader>, subordinate_certs: &Cert, + subject_common_name_contents: SubjectCommonNameContents, ) -> Result<(), Error> { let input = match input { Some(input) => input, @@ -99,10 +102,7 @@ pub(crate) fn check_name_constraints( if !inner.peek(subtrees_tag.into()) { 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)) + der::expect_tag_and_get_value(inner, subtrees_tag).map(Some) } let permitted_subtrees = parse_subtrees(input, der::Tag::ContextSpecificConstructed0)?; @@ -113,6 +113,7 @@ pub(crate) fn check_name_constraints( iterate_names( Some(child.subject), child.subject_alt_name, + subject_common_name_contents, Ok(()), &|name| { check_presented_id_conforms_to_constraints( @@ -178,7 +179,7 @@ fn check_presented_id_conforms_to_constraints_in_subtree( let mut has_permitted_subtrees_match = false; let mut has_permitted_subtrees_mismatch = false; - loop { + while !constraints.at_end() { // http://tools.ietf.org/html/rfc5280#section-4.2.1.10: "Within this // profile, the minimum and maximum fields are not used with any name // forms, thus, the minimum MUST be zero, and maximum MUST be absent." @@ -227,7 +228,11 @@ fn check_presented_id_conforms_to_constraints_in_subtree( Err(Error::NameConstraintViolation) } - _ => Ok(false), + _ => { + // mismatch between constraint and name types; continue with current + // name and next constraint + continue; + } }; match (subtrees, matches) { @@ -249,10 +254,6 @@ fn check_presented_id_conforms_to_constraints_in_subtree( return NameIteration::Stop(Err(err)); } } - - if constraints.at_end() { - break; - } } if has_permitted_subtrees_mismatch && !has_permitted_subtrees_match { @@ -265,14 +266,26 @@ fn check_presented_id_conforms_to_constraints_in_subtree( } } -// TODO: document this. fn presented_directory_name_matches_constraint( - name: untrusted::Input, - constraint: untrusted::Input, + _name: untrusted::Input, + _constraint: untrusted::Input, subtrees: Subtrees, ) -> bool { + // Reject any uses of directory name constraints; we don't implement this. + // + // Rejecting everything technically confirms to RFC5280: + // + // "If a name constraints extension that is marked as critical imposes constraints + // on a particular name form, and an instance of that name form appears in the + // subject field or subjectAltName extension of a subsequent certificate, then + // the application MUST either process the constraint or _reject the certificate_." + // + // TODO: rustls/webpki#19 + // + // Rejection is achieved by not matching any PermittedSubtrees, and matching all + // ExcludedSubtrees. match subtrees { - Subtrees::PermittedSubtrees => name == constraint, + Subtrees::PermittedSubtrees => false, Subtrees::ExcludedSubtrees => true, } } @@ -283,9 +296,16 @@ enum NameIteration { Stop(Result<(), Error>), } +#[derive(Clone, Copy)] +pub(crate) enum SubjectCommonNameContents { + DnsName, + Ignore, +} + fn iterate_names( subject: Option, subject_alt_name: Option, + subject_common_name_contents: SubjectCommonNameContents, result_if_never_stopped_early: Result<(), Error>, f: &dyn Fn(GeneralName) -> NameIteration, ) -> Result<(), Error> { @@ -310,8 +330,21 @@ fn iterate_names( if let Some(subject) = subject { match f(GeneralName::DirectoryName(subject)) { - NameIteration::Stop(result) => result, - NameIteration::KeepGoing => result_if_never_stopped_early, + NameIteration::Stop(result) => return result, + NameIteration::KeepGoing => (), + }; + } + + if let (SubjectCommonNameContents::DnsName, Some(subject)) = + (subject_common_name_contents, subject) + { + match common_name(subject) { + Ok(Some(cn)) => match f(GeneralName::DnsName(cn)) { + NameIteration::Stop(result) => result, + NameIteration::KeepGoing => result_if_never_stopped_early, + }, + Ok(None) => result_if_never_stopped_early, + Err(err) => Err(err), } } else { result_if_never_stopped_early @@ -365,3 +398,23 @@ fn general_name<'a>(input: &mut untrusted::Reader<'a>) -> Result }; Ok(name) } + +static COMMON_NAME: untrusted::Input = untrusted::Input::from(&[85, 4, 3]); + +fn common_name(input: untrusted::Input) -> Result, Error> { + let inner = &mut untrusted::Reader::new(input); + der::nested(inner, der::Tag::Set, Error::BadDER, |tagged| { + der::nested(tagged, der::Tag::Sequence, Error::BadDER, |tagged| { + while !tagged.at_end() { + let name_oid = der::expect_tag_and_get_value(tagged, der::Tag::OID)?; + if name_oid == COMMON_NAME { + return der::expect_tag_and_get_value(tagged, der::Tag::UTF8String).map(Some); + } else { + // discard unused name value + der::read_tag_and_get_value(tagged)?; + } + } + Ok(None) + }) + }) +} diff --git a/src/verify_cert.rs b/src/verify_cert.rs index c21c22ef..773c7665 100644 --- a/src/verify_cert.rs +++ b/src/verify_cert.rs @@ -51,6 +51,16 @@ pub(crate) fn build_chain( } } + // for the purpose of name constraints checking, only end-entity server certificates + // could plausibly have a DNS name as a subject commonName that could contribute to + // path validity + let subject_common_name_contents = + if required_eku_if_present == EKU_SERVER_AUTH && used_as_ca == UsedAsCa::No { + subject_name::SubjectCommonNameContents::DnsName + } else { + subject_name::SubjectCommonNameContents::Ignore + }; + // TODO: revocation. let result = loop_while_non_fatal_error(trust_anchors, |trust_anchor: &TrustAnchor| { @@ -62,7 +72,7 @@ pub(crate) fn build_chain( let name_constraints = trust_anchor.name_constraints.map(untrusted::Input::from); untrusted::read_all_optional(name_constraints, Error::BadDER, |value| { - subject_name::check_name_constraints(value, cert) + subject_name::check_name_constraints(value, cert, subject_common_name_contents) })?; let trust_anchor_spki = untrusted::Input::from(trust_anchor.spki); @@ -106,7 +116,7 @@ pub(crate) fn build_chain( } untrusted::read_all_optional(potential_issuer.name_constraints, Error::BadDER, |value| { - subject_name::check_name_constraints(value, cert) + subject_name::check_name_constraints(value, cert, subject_common_name_contents) })?; let next_sub_ca_count = match used_as_ca { @@ -201,7 +211,7 @@ fn check_validity(input: &mut untrusted::Reader, time: time::Time) -> Result<(), Ok(()) } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] enum UsedAsCa { Yes, No, @@ -250,7 +260,7 @@ fn check_basic_constraints( } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub(crate) struct KeyPurposeId { oid_value: untrusted::Input<'static>, } diff --git a/tests/integration.rs b/tests/integration.rs index 36490c7e..b29e9e59 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -104,6 +104,25 @@ pub fn cloudflare_dns() { check_addr("2606:4700:4700:0000:0000:0000:0000:6400"); } +#[cfg(feature = "alloc")] +#[test] +pub fn wpt() { + let ee: &[u8] = include_bytes!("wpt/ee.der"); + let ca = include_bytes!("wpt/ca.der"); + + let anchors = vec![webpki::TrustAnchor::try_from_cert_der(ca).unwrap()]; + let anchors = webpki::TLSServerTrustAnchors(&anchors); + + #[allow(clippy::unreadable_literal)] // TODO: Make this clear. + let time = webpki::Time::from_seconds_since_unix_epoch(1619256684); + + let cert = webpki::EndEntityCert::try_from(ee).unwrap(); + assert_eq!( + Ok(()), + cert.verify_is_valid_tls_server_cert(ALL_SIGALGS, &anchors, &[], time) + ); +} + #[test] pub fn ed25519() { let ee: &[u8] = include_bytes!("ed25519/ee.der"); diff --git a/tests/name_constraints.rs b/tests/name_constraints.rs new file mode 100644 index 00000000..ba76f362 --- /dev/null +++ b/tests/name_constraints.rs @@ -0,0 +1,381 @@ +// Copyright 2022 Joseph Birr-Pixton. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use core::convert::TryFrom; +extern crate webpki; + +static ALL_SIGALGS: &[&webpki::SignatureAlgorithm] = &[ + &webpki::ECDSA_P256_SHA256, + &webpki::ECDSA_P256_SHA384, + &webpki::ECDSA_P384_SHA256, + &webpki::ECDSA_P384_SHA384, + &webpki::ED25519, + #[cfg(feature = "alloc")] + &webpki::RSA_PKCS1_2048_8192_SHA256, + #[cfg(feature = "alloc")] + &webpki::RSA_PKCS1_2048_8192_SHA384, + #[cfg(feature = "alloc")] + &webpki::RSA_PKCS1_2048_8192_SHA512, + #[cfg(feature = "alloc")] + &webpki::RSA_PKCS1_3072_8192_SHA384, +]; + +fn check_cert( + ee: &[u8], + ca: &[u8], + valid_names: &[&str], + invalid_names: &[&str], +) -> Result<(), webpki::Error> { + let anchors = vec![webpki::TrustAnchor::try_from_cert_der(ca).unwrap()]; + let anchors = webpki::TLSServerTrustAnchors(&anchors); + + let time = webpki::Time::from_seconds_since_unix_epoch(0x1fed_f00d); + let cert = webpki::EndEntityCert::try_from(ee).unwrap(); + cert.verify_is_valid_tls_server_cert(ALL_SIGALGS, &anchors, &[], time)?; + + for valid in valid_names { + let name = webpki::SubjectNameRef::try_from_ascii_str(valid).unwrap(); + assert_eq!(cert.verify_is_valid_for_subject_name(name), Ok(())); + } + + for invalid in invalid_names { + let name = webpki::SubjectNameRef::try_from_ascii_str(invalid).unwrap(); + assert_eq!( + cert.verify_is_valid_for_subject_name(name), + Err(webpki::Error::CertNotValidForName) + ); + } + + Ok(()) +} + +// DO NOT EDIT BELOW: generated by name_constraints/generate.py + +#[test] +#[cfg(feature = "alloc")] +fn no_name_constraints() { + let ee = include_bytes!("name_constraints/no_name_constraints.ee.der"); + let ca = include_bytes!("name_constraints/no_name_constraints.ca.der"); + assert_eq!( + check_cert(ee, ca, &["dns.example.com"], &["subject.example.com"]), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn additional_dns_labels() { + let ee = include_bytes!("name_constraints/additional_dns_labels.ee.der"); + let ca = include_bytes!("name_constraints/additional_dns_labels.ca.der"); + assert_eq!( + check_cert( + ee, + ca, + &["host1.example.com", "host2.example.com"], + &["subject.example.com"] + ), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn disallow_subject_common_name() { + let ee = include_bytes!("name_constraints/disallow_subject_common_name.ee.der"); + let ca = include_bytes!("name_constraints/disallow_subject_common_name.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn disallow_dns_san() { + let ee = include_bytes!("name_constraints/disallow_dns_san.ee.der"); + let ca = include_bytes!("name_constraints/disallow_dns_san.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn allow_subject_common_name() { + let ee = include_bytes!("name_constraints/allow_subject_common_name.ee.der"); + let ca = include_bytes!("name_constraints/allow_subject_common_name.ca.der"); + assert_eq!(check_cert(ee, ca, &[], &["allowed.example.com"]), Ok(())); +} + +#[test] +#[cfg(feature = "alloc")] +fn allow_dns_san() { + let ee = include_bytes!("name_constraints/allow_dns_san.ee.der"); + let ca = include_bytes!("name_constraints/allow_dns_san.ca.der"); + assert_eq!(check_cert(ee, ca, &["allowed.example.com"], &[]), Ok(())); +} + +#[test] +#[cfg(feature = "alloc")] +fn allow_dns_san_and_subject_common_name() { + let ee = include_bytes!("name_constraints/allow_dns_san_and_subject_common_name.ee.der"); + let ca = include_bytes!("name_constraints/allow_dns_san_and_subject_common_name.ca.der"); + assert_eq!( + check_cert( + ee, + ca, + &["allowed-san.example.com"], + &["allowed-cn.example.com"] + ), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn allow_dns_san_and_disallow_subject_common_name() { + let ee = + include_bytes!("name_constraints/allow_dns_san_and_disallow_subject_common_name.ee.der"); + let ca = + include_bytes!("name_constraints/allow_dns_san_and_disallow_subject_common_name.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn disallow_dns_san_and_allow_subject_common_name() { + let ee = + include_bytes!("name_constraints/disallow_dns_san_and_allow_subject_common_name.ee.der"); + let ca = + include_bytes!("name_constraints/disallow_dns_san_and_allow_subject_common_name.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn we_incorrectly_ignore_name_constraints_on_name_in_subject() { + let ee = include_bytes!( + "name_constraints/we_incorrectly_ignore_name_constraints_on_name_in_subject.ee.der" + ); + let ca = include_bytes!( + "name_constraints/we_incorrectly_ignore_name_constraints_on_name_in_subject.ca.der" + ); + assert_eq!(check_cert(ee, ca, &[], &[]), Ok(())); +} + +#[test] +#[cfg(feature = "alloc")] +fn reject_constraints_on_unimplemented_names() { + let ee = include_bytes!("name_constraints/reject_constraints_on_unimplemented_names.ee.der"); + let ca = include_bytes!("name_constraints/reject_constraints_on_unimplemented_names.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn we_ignore_constraints_on_names_that_do_not_appear_in_cert() { + let ee = include_bytes!( + "name_constraints/we_ignore_constraints_on_names_that_do_not_appear_in_cert.ee.der" + ); + let ca = include_bytes!( + "name_constraints/we_ignore_constraints_on_names_that_do_not_appear_in_cert.ca.der" + ); + assert_eq!( + check_cert(ee, ca, &["notexample.com"], &["example.com"]), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn wildcard_san_accepted_if_in_subtree() { + let ee = include_bytes!("name_constraints/wildcard_san_accepted_if_in_subtree.ee.der"); + let ca = include_bytes!("name_constraints/wildcard_san_accepted_if_in_subtree.ca.der"); + assert_eq!( + check_cert( + ee, + ca, + &["bob.example.com", "jane.example.com"], + &["example.com", "uh.oh.example.com"] + ), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn wildcard_san_rejected_if_in_excluded_subtree() { + let ee = include_bytes!("name_constraints/wildcard_san_rejected_if_in_excluded_subtree.ee.der"); + let ca = include_bytes!("name_constraints/wildcard_san_rejected_if_in_excluded_subtree.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn ip4_address_san_rejected_if_in_excluded_subtree() { + let ee = + include_bytes!("name_constraints/ip4_address_san_rejected_if_in_excluded_subtree.ee.der"); + let ca = + include_bytes!("name_constraints/ip4_address_san_rejected_if_in_excluded_subtree.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn ip4_address_san_allowed_if_outside_excluded_subtree() { + let ee = include_bytes!( + "name_constraints/ip4_address_san_allowed_if_outside_excluded_subtree.ee.der" + ); + let ca = include_bytes!( + "name_constraints/ip4_address_san_allowed_if_outside_excluded_subtree.ca.der" + ); + assert_eq!(check_cert(ee, ca, &["12.34.56.78"], &[]), Ok(())); +} + +#[test] +#[cfg(feature = "alloc")] +fn ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask() { + let ee = include_bytes!( + "name_constraints/ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask.ee.der" + ); + let ca = include_bytes!( + "name_constraints/ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask.ca.der" + ); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn ip4_address_san_allowed() { + let ee = include_bytes!("name_constraints/ip4_address_san_allowed.ee.der"); + let ca = include_bytes!("name_constraints/ip4_address_san_allowed.ca.der"); + assert_eq!( + check_cert( + ee, + ca, + &["12.34.56.78"], + &[ + "12.34.56.77", + "12.34.56.79", + "0000:0000:0000:0000:0000:ffff:0c22:384e" + ] + ), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn ip6_address_san_rejected_if_in_excluded_subtree() { + let ee = + include_bytes!("name_constraints/ip6_address_san_rejected_if_in_excluded_subtree.ee.der"); + let ca = + include_bytes!("name_constraints/ip6_address_san_rejected_if_in_excluded_subtree.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn ip6_address_san_allowed_if_outside_excluded_subtree() { + let ee = include_bytes!( + "name_constraints/ip6_address_san_allowed_if_outside_excluded_subtree.ee.der" + ); + let ca = include_bytes!( + "name_constraints/ip6_address_san_allowed_if_outside_excluded_subtree.ca.der" + ); + assert_eq!( + check_cert(ee, ca, &["2001:0db9:0000:0000:0000:0000:0000:0001"], &[]), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn ip6_address_san_allowed() { + let ee = include_bytes!("name_constraints/ip6_address_san_allowed.ee.der"); + let ca = include_bytes!("name_constraints/ip6_address_san_allowed.ca.der"); + assert_eq!( + check_cert( + ee, + ca, + &["2001:0db9:0000:0000:0000:0000:0000:0001"], + &["12.34.56.78"] + ), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn ip46_mixed_address_san_allowed() { + let ee = include_bytes!("name_constraints/ip46_mixed_address_san_allowed.ee.der"); + let ca = include_bytes!("name_constraints/ip46_mixed_address_san_allowed.ca.der"); + assert_eq!( + check_cert( + ee, + ca, + &["12.34.56.78", "2001:0db9:0000:0000:0000:0000:0000:0001"], + &[ + "12.34.56.77", + "12.34.56.79", + "0000:0000:0000:0000:0000:ffff:0c22:384e" + ] + ), + Ok(()) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn permit_directory_name_not_implemented() { + let ee = include_bytes!("name_constraints/permit_directory_name_not_implemented.ee.der"); + let ca = include_bytes!("name_constraints/permit_directory_name_not_implemented.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} + +#[test] +#[cfg(feature = "alloc")] +fn exclude_directory_name_not_implemented() { + let ee = include_bytes!("name_constraints/exclude_directory_name_not_implemented.ee.der"); + let ca = include_bytes!("name_constraints/exclude_directory_name_not_implemented.ca.der"); + assert_eq!( + check_cert(ee, ca, &[], &[]), + Err(webpki::Error::UnknownIssuer) + ); +} diff --git a/tests/name_constraints/additional_dns_labels.ca.der b/tests/name_constraints/additional_dns_labels.ca.der new file mode 100644 index 00000000..a19b5c70 Binary files /dev/null and b/tests/name_constraints/additional_dns_labels.ca.der differ diff --git a/tests/name_constraints/additional_dns_labels.ee.der b/tests/name_constraints/additional_dns_labels.ee.der new file mode 100644 index 00000000..14e2e09e Binary files /dev/null and b/tests/name_constraints/additional_dns_labels.ee.der differ diff --git a/tests/name_constraints/allow_dns_san.ca.der b/tests/name_constraints/allow_dns_san.ca.der new file mode 100644 index 00000000..131a79f9 Binary files /dev/null and b/tests/name_constraints/allow_dns_san.ca.der differ diff --git a/tests/name_constraints/allow_dns_san.ee.der b/tests/name_constraints/allow_dns_san.ee.der new file mode 100644 index 00000000..bee4b311 Binary files /dev/null and b/tests/name_constraints/allow_dns_san.ee.der differ diff --git a/tests/name_constraints/allow_dns_san_and_disallow_subject_common_name.ca.der b/tests/name_constraints/allow_dns_san_and_disallow_subject_common_name.ca.der new file mode 100644 index 00000000..e3b050ad Binary files /dev/null and b/tests/name_constraints/allow_dns_san_and_disallow_subject_common_name.ca.der differ diff --git a/tests/name_constraints/allow_dns_san_and_disallow_subject_common_name.ee.der b/tests/name_constraints/allow_dns_san_and_disallow_subject_common_name.ee.der new file mode 100644 index 00000000..ea3a0210 Binary files /dev/null and b/tests/name_constraints/allow_dns_san_and_disallow_subject_common_name.ee.der differ diff --git a/tests/name_constraints/allow_dns_san_and_subject_common_name.ca.der b/tests/name_constraints/allow_dns_san_and_subject_common_name.ca.der new file mode 100644 index 00000000..f2e64b8f Binary files /dev/null and b/tests/name_constraints/allow_dns_san_and_subject_common_name.ca.der differ diff --git a/tests/name_constraints/allow_dns_san_and_subject_common_name.ee.der b/tests/name_constraints/allow_dns_san_and_subject_common_name.ee.der new file mode 100644 index 00000000..e823eaf2 Binary files /dev/null and b/tests/name_constraints/allow_dns_san_and_subject_common_name.ee.der differ diff --git a/tests/name_constraints/allow_subject_common_name.ca.der b/tests/name_constraints/allow_subject_common_name.ca.der new file mode 100644 index 00000000..099d8d8e Binary files /dev/null and b/tests/name_constraints/allow_subject_common_name.ca.der differ diff --git a/tests/name_constraints/allow_subject_common_name.ee.der b/tests/name_constraints/allow_subject_common_name.ee.der new file mode 100644 index 00000000..b0a452b8 Binary files /dev/null and b/tests/name_constraints/allow_subject_common_name.ee.der differ diff --git a/tests/name_constraints/disallow_dns_san.ca.der b/tests/name_constraints/disallow_dns_san.ca.der new file mode 100644 index 00000000..dc56ce1c Binary files /dev/null and b/tests/name_constraints/disallow_dns_san.ca.der differ diff --git a/tests/name_constraints/disallow_dns_san.ee.der b/tests/name_constraints/disallow_dns_san.ee.der new file mode 100644 index 00000000..9b6b32e7 Binary files /dev/null and b/tests/name_constraints/disallow_dns_san.ee.der differ diff --git a/tests/name_constraints/disallow_dns_san_and_allow_subject_common_name.ca.der b/tests/name_constraints/disallow_dns_san_and_allow_subject_common_name.ca.der new file mode 100644 index 00000000..6213a95e Binary files /dev/null and b/tests/name_constraints/disallow_dns_san_and_allow_subject_common_name.ca.der differ diff --git a/tests/name_constraints/disallow_dns_san_and_allow_subject_common_name.ee.der b/tests/name_constraints/disallow_dns_san_and_allow_subject_common_name.ee.der new file mode 100644 index 00000000..0b525875 Binary files /dev/null and b/tests/name_constraints/disallow_dns_san_and_allow_subject_common_name.ee.der differ diff --git a/tests/name_constraints/disallow_subject_common_name.ca.der b/tests/name_constraints/disallow_subject_common_name.ca.der new file mode 100644 index 00000000..2b010237 Binary files /dev/null and b/tests/name_constraints/disallow_subject_common_name.ca.der differ diff --git a/tests/name_constraints/disallow_subject_common_name.ee.der b/tests/name_constraints/disallow_subject_common_name.ee.der new file mode 100644 index 00000000..65ea3fae Binary files /dev/null and b/tests/name_constraints/disallow_subject_common_name.ee.der differ diff --git a/tests/name_constraints/exclude_directory_name_not_implemented.ca.der b/tests/name_constraints/exclude_directory_name_not_implemented.ca.der new file mode 100644 index 00000000..080c7de3 Binary files /dev/null and b/tests/name_constraints/exclude_directory_name_not_implemented.ca.der differ diff --git a/tests/name_constraints/exclude_directory_name_not_implemented.ee.der b/tests/name_constraints/exclude_directory_name_not_implemented.ee.der new file mode 100644 index 00000000..2df7ad0b Binary files /dev/null and b/tests/name_constraints/exclude_directory_name_not_implemented.ee.der differ diff --git a/tests/name_constraints/generate.py b/tests/name_constraints/generate.py new file mode 100644 index 00000000..60889c21 --- /dev/null +++ b/tests/name_constraints/generate.py @@ -0,0 +1,336 @@ +""" +Generates test cases that aim to validate name constraints and other +name-related parts of webpki. + +Run this script from tests/name_constraints. It edits the bottom part of +tests/name_constraints.rs and drops files into tests/name_constraints. +""" + + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.hazmat.backends import default_backend +from cryptography.x509.oid import NameOID +import ipaddress +import datetime + +ISSUER_PRIVATE_KEY = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend() +) +ISSUER_PUBLIC_KEY = ISSUER_PRIVATE_KEY.public_key() + +NOT_BEFORE = datetime.datetime.fromtimestamp(0x1fedf00d - 30) +NOT_AFTER = datetime.datetime.fromtimestamp(0x1fedf00d + 30) + +def name_constraints_test(test_name, + expected_error=None, + subject_common_name=None, + extra_subject_names=[], + valid_names=[], + invalid_names=[], + sans=None, + permitted_subtrees=None, + excluded_subtrees=None): + """ + Generate a test case, writing a rust '#[test]' function into + name_constraints.rs, and writing supporting files into the current + directory. + + - `test_name`: name of the test, must be a rust identifier. + - `expected_error`: item in `webpki::Error` enum, expected error from + webpki `verify_is_valid_tls_server_cert` function. Leave absent to + expect success. + - `subject_common_name`: optional string to put in end-entity certificate + subject common name. + - `extra_subject_names`: optional sequence of `x509.NameAttributes` to add + to end-entity certificate subject. + - `valid_names`: optional sequence of valid names that the end-entity + certificate is expected to pass `verify_is_valid_for_subject_name` for. + - `invalid_names`: optional sequence of invalid names that the end-entity + certificate is expected to fail `verify_is_valid_for_subject_name` with + `CertNotValidForName`. + - `sans`: optional sequence of `x509.GeneralName`s that are the contents of + the subjectAltNames extension. If empty or not provided the end-entity + certificate does not have a subjectAltName extension. + - `permitted_subtrees`: optional sequence of `x509.GeneralName`s that are + the `permittedSubtrees` contents of the `nameConstraints` extension. + If this and `excluded_subtrees` are empty/absent then the end-entity + certificate does not have a `nameConstraints` extension. + - `excluded_subtrees`: optional sequence of `x509.GeneralName`s that are + the `excludedSubtrees` contents of the `nameConstraints` extension. + If this and `permitted_subtrees` are both empty/absent then the + end-entity certificate does not have a `nameConstraints` extension. + """ + + # keys must be valid but are otherwise unimportant for these tests + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend(), + ) + public_key = private_key.public_key() + + issuer_name = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u'issuer.example.com'), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, test_name), + ]) + + # end-entity + builder = x509.CertificateBuilder() + builder = builder.subject_name(x509.Name( + ([x509.NameAttribute(NameOID.COMMON_NAME, subject_common_name)] if subject_common_name else []) + + [x509.NameAttribute(NameOID.ORGANIZATION_NAME, test_name) ] + + extra_subject_names + )) + builder = builder.issuer_name(issuer_name) + + builder = builder.not_valid_before(NOT_BEFORE) + builder = builder.not_valid_after(NOT_AFTER) + builder = builder.serial_number(x509.random_serial_number()) + builder = builder.public_key(public_key) + if sans: + builder = builder.add_extension( + x509.SubjectAlternativeName(sans), + critical=False + ) + builder = builder.add_extension( + x509.BasicConstraints(ca=False, path_length=None), critical=True, + ) + certificate = builder.sign( + private_key=ISSUER_PRIVATE_KEY, + algorithm=hashes.SHA256(), + backend=default_backend(), + ) + + with open(test_name + '.ee.der', 'wb') as f: + f.write(certificate.public_bytes(Encoding.DER)) + + # issuer + builder = x509.CertificateBuilder() + builder = builder.subject_name(issuer_name) + builder = builder.issuer_name(issuer_name) + builder = builder.not_valid_before(NOT_BEFORE) + builder = builder.not_valid_after(NOT_AFTER) + builder = builder.serial_number(x509.random_serial_number()) + builder = builder.public_key(ISSUER_PUBLIC_KEY) + builder = builder.add_extension( + x509.BasicConstraints(ca=True, path_length=None), critical=True, + ) + if permitted_subtrees or excluded_subtrees: + builder = builder.add_extension( + x509.NameConstraints(permitted_subtrees, excluded_subtrees), + critical=True + ) + + certificate = builder.sign( + private_key=ISSUER_PRIVATE_KEY, + algorithm=hashes.SHA256(), + backend=default_backend() + ) + + with open(test_name + '.ca.der', 'wb') as f: + f.write(certificate.public_bytes(Encoding.DER)) + + if expected_error is None: + expected = 'Ok(())' + else: + expected = 'Err(webpki::Error::' + expected_error + ')' + + valid_names = ', '.join('"' + name + '"' for name in valid_names) + invalid_names = ', '.join('"' + name + '"' for name in invalid_names) + + print(""" +#[test] +#[cfg(feature = "alloc")] +fn %(test_name)s() { + let ee = include_bytes!("name_constraints/%(test_name)s.ee.der"); + let ca = include_bytes!("name_constraints/%(test_name)s.ca.der"); + assert_eq!( + check_cert(ee, ca, &[%(valid_names)s], &[%(invalid_names)s]), + %(expected)s + ); +}""" % locals(), file=output) + +top = open('../name_constraints.rs', 'r').readlines() +top = top[:top.index('// DO NOT EDIT BELOW: generated by name_constraints/generate.py\n')+1] +output = open('../name_constraints.rs', 'w') +for l in top: + output.write(l) + +name_constraints_test( + 'no_name_constraints', + subject_common_name='subject.example.com', + valid_names=['dns.example.com'], + invalid_names=['subject.example.com'], + sans=[x509.DNSName('dns.example.com')]) + +name_constraints_test( + 'additional_dns_labels', + subject_common_name='subject.example.com', + valid_names=['host1.example.com', 'host2.example.com'], + invalid_names=['subject.example.com'], + sans=[x509.DNSName('host1.example.com'), x509.DNSName('host2.example.com')], + permitted_subtrees=[x509.DNSName('.example.com')]) + + +name_constraints_test( + 'disallow_subject_common_name', + expected_error='UnknownIssuer', + subject_common_name='disallowed.example.com', + excluded_subtrees=[x509.DNSName('disallowed.example.com')]) +name_constraints_test( + 'disallow_dns_san', + expected_error='UnknownIssuer', + sans=[x509.DNSName('disallowed.example.com')], + excluded_subtrees=[x509.DNSName('disallowed.example.com')]) + +name_constraints_test( + 'allow_subject_common_name', + subject_common_name='allowed.example.com', + invalid_names=['allowed.example.com'], + permitted_subtrees=[x509.DNSName('allowed.example.com')]) +name_constraints_test( + 'allow_dns_san', + valid_names=['allowed.example.com'], + sans=[x509.DNSName('allowed.example.com')], + permitted_subtrees=[x509.DNSName('allowed.example.com')]) +name_constraints_test( + 'allow_dns_san_and_subject_common_name', + valid_names=['allowed-san.example.com'], + invalid_names=['allowed-cn.example.com'], + sans=[x509.DNSName('allowed-san.example.com')], + subject_common_name='allowed-cn.example.com', + permitted_subtrees=[x509.DNSName('allowed-san.example.com'), x509.DNSName('allowed-cn.example.com')]) +name_constraints_test( + 'allow_dns_san_and_disallow_subject_common_name', + expected_error='UnknownIssuer', + sans=[x509.DNSName('allowed-san.example.com')], + subject_common_name='disallowed-cn.example.com', + permitted_subtrees=[x509.DNSName('allowed-san.example.com')], + excluded_subtrees=[x509.DNSName('disallowed-cn.example.com')]) +name_constraints_test( + 'disallow_dns_san_and_allow_subject_common_name', + expected_error='UnknownIssuer', + sans=[x509.DNSName('allowed-san.example.com'), x509.DNSName('disallowed-san.example.com')], + subject_common_name='allowed-cn.example.com', + permitted_subtrees=[x509.DNSName('allowed-san.example.com'), x509.DNSName('allowed-cn.example.com')], + excluded_subtrees=[x509.DNSName('disallowed-san.example.com')]) + +# XXX: ideally this test case would be a negative one, because the name constraints +# should apply to the subject name. +# however, because we don't look at email addresses in subjects, it is accepted. +name_constraints_test( + 'we_incorrectly_ignore_name_constraints_on_name_in_subject', + extra_subject_names=[x509.NameAttribute(NameOID.EMAIL_ADDRESS, 'joe@notexample.com')], + permitted_subtrees=[x509.RFC822Name('example.com')]) + +# this does work, however, because we process all SANs +name_constraints_test( + 'reject_constraints_on_unimplemented_names', + expected_error='UnknownIssuer', + sans=[x509.RFC822Name('joe@example.com')], + permitted_subtrees=[x509.RFC822Name('example.com')]) + +# RFC5280 4.2.1.10: +# "If no name of the type is in the certificate, +# the certificate is acceptable." +name_constraints_test( + 'we_ignore_constraints_on_names_that_do_not_appear_in_cert', + sans=[x509.DNSName('notexample.com')], + valid_names=['notexample.com'], + invalid_names=['example.com'], + permitted_subtrees=[x509.RFC822Name('example.com')]) + +name_constraints_test( + 'wildcard_san_accepted_if_in_subtree', + sans=[x509.DNSName('*.example.com')], + valid_names=['bob.example.com', 'jane.example.com'], + invalid_names=['example.com', 'uh.oh.example.com'], + permitted_subtrees=[x509.DNSName('example.com')]) + +name_constraints_test( + 'wildcard_san_rejected_if_in_excluded_subtree', + expected_error='UnknownIssuer', + sans=[x509.DNSName('*.example.com')], + excluded_subtrees=[x509.DNSName('example.com')]) + +name_constraints_test( + 'ip4_address_san_rejected_if_in_excluded_subtree', + expected_error='UnknownIssuer', + sans=[x509.IPAddress(ipaddress.ip_address('12.34.56.78'))], + excluded_subtrees=[x509.IPAddress(ipaddress.ip_network('12.34.56.0/24'))]) + +name_constraints_test( + 'ip4_address_san_allowed_if_outside_excluded_subtree', + valid_names=['12.34.56.78'], + sans=[x509.IPAddress(ipaddress.ip_address('12.34.56.78'))], + excluded_subtrees=[x509.IPAddress(ipaddress.ip_network('12.34.56.252/30'))]) + +sparse_net_addr = ipaddress.ip_network('12.34.56.78/24', strict=False) +sparse_net_addr.netmask = ipaddress.ip_address('255.255.255.1') +name_constraints_test( + 'ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask', + expected_error='UnknownIssuer', + sans=[ + # inside excluded network, if netmask is allowed to be sparse + x509.IPAddress(ipaddress.ip_address('12.34.56.79')), + ], + excluded_subtrees=[x509.IPAddress(sparse_net_addr)]) + + +name_constraints_test( + 'ip4_address_san_allowed', + valid_names=['12.34.56.78'], + invalid_names=['12.34.56.77', '12.34.56.79', '0000:0000:0000:0000:0000:ffff:0c22:384e'], + sans=[x509.IPAddress(ipaddress.ip_address('12.34.56.78'))], + permitted_subtrees=[x509.IPAddress(ipaddress.ip_network('12.34.56.0/24'))]) + +name_constraints_test( + 'ip6_address_san_rejected_if_in_excluded_subtree', + expected_error='UnknownIssuer', + sans=[x509.IPAddress(ipaddress.ip_address('2001:db8::1'))], + excluded_subtrees=[x509.IPAddress(ipaddress.ip_network('2001:db8::/48'))]) + +name_constraints_test( + 'ip6_address_san_allowed_if_outside_excluded_subtree', + valid_names=['2001:0db9:0000:0000:0000:0000:0000:0001'], + sans=[x509.IPAddress(ipaddress.ip_address('2001:db9::1'))], + excluded_subtrees=[x509.IPAddress(ipaddress.ip_network('2001:db8::/48'))]) + +name_constraints_test( + 'ip6_address_san_allowed', + valid_names=['2001:0db9:0000:0000:0000:0000:0000:0001'], + invalid_names=['12.34.56.78'], + sans=[x509.IPAddress(ipaddress.ip_address('2001:db9::1'))], + permitted_subtrees=[x509.IPAddress(ipaddress.ip_network('2001:db9::/48'))]) + +name_constraints_test( + 'ip46_mixed_address_san_allowed', + valid_names=['12.34.56.78', '2001:0db9:0000:0000:0000:0000:0000:0001'], + invalid_names=['12.34.56.77', '12.34.56.79', '0000:0000:0000:0000:0000:ffff:0c22:384e'], + sans=[ + x509.IPAddress(ipaddress.ip_address('12.34.56.78')), + x509.IPAddress(ipaddress.ip_address('2001:db9::1')), + ], + permitted_subtrees=[ + x509.IPAddress(ipaddress.ip_network('12.34.56.0/24')), + x509.IPAddress(ipaddress.ip_network('2001:db9::/48')) + ]) + +name_constraints_test( + 'permit_directory_name_not_implemented', + expected_error='UnknownIssuer', + permitted_subtrees=[ + x509.DirectoryName(x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'CN')])) + ]) + +name_constraints_test( + 'exclude_directory_name_not_implemented', + expected_error='UnknownIssuer', + excluded_subtrees=[ + x509.DirectoryName(x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'CN')])) + ]) diff --git a/tests/name_constraints/ignore_name_constraints_on_unimplemented_names.ca.der b/tests/name_constraints/ignore_name_constraints_on_unimplemented_names.ca.der new file mode 100644 index 00000000..f2dcbbf1 Binary files /dev/null and b/tests/name_constraints/ignore_name_constraints_on_unimplemented_names.ca.der differ diff --git a/tests/name_constraints/ignore_name_constraints_on_unimplemented_names.ee.der b/tests/name_constraints/ignore_name_constraints_on_unimplemented_names.ee.der new file mode 100644 index 00000000..12872da8 Binary files /dev/null and b/tests/name_constraints/ignore_name_constraints_on_unimplemented_names.ee.der differ diff --git a/tests/name_constraints/ip46_mixed_address_san_allowed.ca.der b/tests/name_constraints/ip46_mixed_address_san_allowed.ca.der new file mode 100644 index 00000000..8389fa95 Binary files /dev/null and b/tests/name_constraints/ip46_mixed_address_san_allowed.ca.der differ diff --git a/tests/name_constraints/ip46_mixed_address_san_allowed.ee.der b/tests/name_constraints/ip46_mixed_address_san_allowed.ee.der new file mode 100644 index 00000000..85d4f414 Binary files /dev/null and b/tests/name_constraints/ip46_mixed_address_san_allowed.ee.der differ diff --git a/tests/name_constraints/ip4_address_san_allowed.ca.der b/tests/name_constraints/ip4_address_san_allowed.ca.der new file mode 100644 index 00000000..4af1a8c4 Binary files /dev/null and b/tests/name_constraints/ip4_address_san_allowed.ca.der differ diff --git a/tests/name_constraints/ip4_address_san_allowed.ee.der b/tests/name_constraints/ip4_address_san_allowed.ee.der new file mode 100644 index 00000000..15978f95 Binary files /dev/null and b/tests/name_constraints/ip4_address_san_allowed.ee.der differ diff --git a/tests/name_constraints/ip4_address_san_allowed_if_outside_excluded_subtree.ca.der b/tests/name_constraints/ip4_address_san_allowed_if_outside_excluded_subtree.ca.der new file mode 100644 index 00000000..825007e1 Binary files /dev/null and b/tests/name_constraints/ip4_address_san_allowed_if_outside_excluded_subtree.ca.der differ diff --git a/tests/name_constraints/ip4_address_san_allowed_if_outside_excluded_subtree.ee.der b/tests/name_constraints/ip4_address_san_allowed_if_outside_excluded_subtree.ee.der new file mode 100644 index 00000000..990921b5 Binary files /dev/null and b/tests/name_constraints/ip4_address_san_allowed_if_outside_excluded_subtree.ee.der differ diff --git a/tests/name_constraints/ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask.ca.der b/tests/name_constraints/ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask.ca.der new file mode 100644 index 00000000..e9a1cf07 Binary files /dev/null and b/tests/name_constraints/ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask.ca.der differ diff --git a/tests/name_constraints/ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask.ee.der b/tests/name_constraints/ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask.ee.der new file mode 100644 index 00000000..9cfeb213 Binary files /dev/null and b/tests/name_constraints/ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask.ee.der differ diff --git a/tests/name_constraints/ip4_address_san_rejected_if_in_excluded_subtree.ca.der b/tests/name_constraints/ip4_address_san_rejected_if_in_excluded_subtree.ca.der new file mode 100644 index 00000000..2844afca Binary files /dev/null and b/tests/name_constraints/ip4_address_san_rejected_if_in_excluded_subtree.ca.der differ diff --git a/tests/name_constraints/ip4_address_san_rejected_if_in_excluded_subtree.ee.der b/tests/name_constraints/ip4_address_san_rejected_if_in_excluded_subtree.ee.der new file mode 100644 index 00000000..8516e53f Binary files /dev/null and b/tests/name_constraints/ip4_address_san_rejected_if_in_excluded_subtree.ee.der differ diff --git a/tests/name_constraints/ip6_address_san_allowed.ca.der b/tests/name_constraints/ip6_address_san_allowed.ca.der new file mode 100644 index 00000000..c7dbb2f1 Binary files /dev/null and b/tests/name_constraints/ip6_address_san_allowed.ca.der differ diff --git a/tests/name_constraints/ip6_address_san_allowed.ee.der b/tests/name_constraints/ip6_address_san_allowed.ee.der new file mode 100644 index 00000000..d7f51d04 Binary files /dev/null and b/tests/name_constraints/ip6_address_san_allowed.ee.der differ diff --git a/tests/name_constraints/ip6_address_san_allowed_if_outside_excluded_subtree.ca.der b/tests/name_constraints/ip6_address_san_allowed_if_outside_excluded_subtree.ca.der new file mode 100644 index 00000000..7ea0a4f9 Binary files /dev/null and b/tests/name_constraints/ip6_address_san_allowed_if_outside_excluded_subtree.ca.der differ diff --git a/tests/name_constraints/ip6_address_san_allowed_if_outside_excluded_subtree.ee.der b/tests/name_constraints/ip6_address_san_allowed_if_outside_excluded_subtree.ee.der new file mode 100644 index 00000000..65657f55 Binary files /dev/null and b/tests/name_constraints/ip6_address_san_allowed_if_outside_excluded_subtree.ee.der differ diff --git a/tests/name_constraints/ip6_address_san_rejected_if_in_excluded_subtree.ca.der b/tests/name_constraints/ip6_address_san_rejected_if_in_excluded_subtree.ca.der new file mode 100644 index 00000000..86b9d349 Binary files /dev/null and b/tests/name_constraints/ip6_address_san_rejected_if_in_excluded_subtree.ca.der differ diff --git a/tests/name_constraints/ip6_address_san_rejected_if_in_excluded_subtree.ee.der b/tests/name_constraints/ip6_address_san_rejected_if_in_excluded_subtree.ee.der new file mode 100644 index 00000000..6ae95dc6 Binary files /dev/null and b/tests/name_constraints/ip6_address_san_rejected_if_in_excluded_subtree.ee.der differ diff --git a/tests/name_constraints/no_name_constraints.ca.der b/tests/name_constraints/no_name_constraints.ca.der new file mode 100644 index 00000000..575f0f75 Binary files /dev/null and b/tests/name_constraints/no_name_constraints.ca.der differ diff --git a/tests/name_constraints/no_name_constraints.ee.der b/tests/name_constraints/no_name_constraints.ee.der new file mode 100644 index 00000000..8011d2c7 Binary files /dev/null and b/tests/name_constraints/no_name_constraints.ee.der differ diff --git a/tests/name_constraints/permit_directory_name_not_implemented.ca.der b/tests/name_constraints/permit_directory_name_not_implemented.ca.der new file mode 100644 index 00000000..84eeea87 Binary files /dev/null and b/tests/name_constraints/permit_directory_name_not_implemented.ca.der differ diff --git a/tests/name_constraints/permit_directory_name_not_implemented.ee.der b/tests/name_constraints/permit_directory_name_not_implemented.ee.der new file mode 100644 index 00000000..22ca7651 Binary files /dev/null and b/tests/name_constraints/permit_directory_name_not_implemented.ee.der differ diff --git a/tests/name_constraints/reject_constraints_on_unimplemented_names.ca.der b/tests/name_constraints/reject_constraints_on_unimplemented_names.ca.der new file mode 100644 index 00000000..7c9e2572 Binary files /dev/null and b/tests/name_constraints/reject_constraints_on_unimplemented_names.ca.der differ diff --git a/tests/name_constraints/reject_constraints_on_unimplemented_names.ee.der b/tests/name_constraints/reject_constraints_on_unimplemented_names.ee.der new file mode 100644 index 00000000..f171441e Binary files /dev/null and b/tests/name_constraints/reject_constraints_on_unimplemented_names.ee.der differ diff --git a/tests/name_constraints/reject_unimplemented_name_constraints.ca.der b/tests/name_constraints/reject_unimplemented_name_constraints.ca.der new file mode 100644 index 00000000..851f0068 Binary files /dev/null and b/tests/name_constraints/reject_unimplemented_name_constraints.ca.der differ diff --git a/tests/name_constraints/reject_unimplemented_name_constraints.ee.der b/tests/name_constraints/reject_unimplemented_name_constraints.ee.der new file mode 100644 index 00000000..13b6a89a Binary files /dev/null and b/tests/name_constraints/reject_unimplemented_name_constraints.ee.der differ diff --git a/tests/name_constraints/we_ignore_constraints_on_names_that_do_not_appear_in_cert.ca.der b/tests/name_constraints/we_ignore_constraints_on_names_that_do_not_appear_in_cert.ca.der new file mode 100644 index 00000000..16dcb26d Binary files /dev/null and b/tests/name_constraints/we_ignore_constraints_on_names_that_do_not_appear_in_cert.ca.der differ diff --git a/tests/name_constraints/we_ignore_constraints_on_names_that_do_not_appear_in_cert.ee.der b/tests/name_constraints/we_ignore_constraints_on_names_that_do_not_appear_in_cert.ee.der new file mode 100644 index 00000000..6671d1af Binary files /dev/null and b/tests/name_constraints/we_ignore_constraints_on_names_that_do_not_appear_in_cert.ee.der differ diff --git a/tests/name_constraints/we_incorrectly_ignore_name_constraints_on_name_in_subject.ca.der b/tests/name_constraints/we_incorrectly_ignore_name_constraints_on_name_in_subject.ca.der new file mode 100644 index 00000000..7e416141 Binary files /dev/null and b/tests/name_constraints/we_incorrectly_ignore_name_constraints_on_name_in_subject.ca.der differ diff --git a/tests/name_constraints/we_incorrectly_ignore_name_constraints_on_name_in_subject.ee.der b/tests/name_constraints/we_incorrectly_ignore_name_constraints_on_name_in_subject.ee.der new file mode 100644 index 00000000..7fde11b8 Binary files /dev/null and b/tests/name_constraints/we_incorrectly_ignore_name_constraints_on_name_in_subject.ee.der differ diff --git a/tests/name_constraints/wildcard_san_accepted_if_in_subtree.ca.der b/tests/name_constraints/wildcard_san_accepted_if_in_subtree.ca.der new file mode 100644 index 00000000..d842b389 Binary files /dev/null and b/tests/name_constraints/wildcard_san_accepted_if_in_subtree.ca.der differ diff --git a/tests/name_constraints/wildcard_san_accepted_if_in_subtree.ee.der b/tests/name_constraints/wildcard_san_accepted_if_in_subtree.ee.der new file mode 100644 index 00000000..e7626304 Binary files /dev/null and b/tests/name_constraints/wildcard_san_accepted_if_in_subtree.ee.der differ diff --git a/tests/name_constraints/wildcard_san_rejected_if_in_excluded_subtree.ca.der b/tests/name_constraints/wildcard_san_rejected_if_in_excluded_subtree.ca.der new file mode 100644 index 00000000..0ad73087 Binary files /dev/null and b/tests/name_constraints/wildcard_san_rejected_if_in_excluded_subtree.ca.der differ diff --git a/tests/name_constraints/wildcard_san_rejected_if_in_excluded_subtree.ee.der b/tests/name_constraints/wildcard_san_rejected_if_in_excluded_subtree.ee.der new file mode 100644 index 00000000..571441d4 Binary files /dev/null and b/tests/name_constraints/wildcard_san_rejected_if_in_excluded_subtree.ee.der differ diff --git a/tests/wpt/ca.der b/tests/wpt/ca.der new file mode 100644 index 00000000..f7d00160 Binary files /dev/null and b/tests/wpt/ca.der differ diff --git a/tests/wpt/ee.der b/tests/wpt/ee.der new file mode 100644 index 00000000..7160db5a Binary files /dev/null and b/tests/wpt/ee.der differ