chore: checkpoint before Python removal

This commit is contained in:
2026-03-26 22:33:59 +00:00
parent 683cec9307
commit e568ddf82a
29972 changed files with 11269302 additions and 2 deletions

View File

@@ -0,0 +1,979 @@
// Copyright 2015-2020 Brian Smith.
//
// 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.
#[cfg(feature = "alloc")]
use alloc::format;
use core::fmt::Write;
#[cfg(feature = "alloc")]
use pki_types::ServerName;
use pki_types::{DnsName, InvalidDnsNameError};
use super::{GeneralName, NameIterator};
use crate::cert::Cert;
use crate::error::{Error, InvalidNameContext};
pub(crate) fn verify_dns_names(reference: &DnsName<'_>, cert: &Cert<'_>) -> Result<(), Error> {
let dns_name = untrusted::Input::from(reference.as_ref().as_bytes());
let result = NameIterator::new(cert.subject_alt_name).find_map(|result| {
let name = match result {
Ok(name) => name,
Err(err) => return Some(Err(err)),
};
let presented_id = match name {
GeneralName::DnsName(presented) => presented,
_ => return None,
};
match presented_id_matches_reference_id(presented_id, IdRole::Reference, dns_name) {
Ok(true) => Some(Ok(())),
Ok(false) | Err(Error::MalformedDnsIdentifier) => None,
Err(e) => Some(Err(e)),
}
});
match result {
Some(result) => return result,
#[cfg(feature = "alloc")]
None => {}
#[cfg(not(feature = "alloc"))]
None => Err(Error::CertNotValidForName(InvalidNameContext {})),
}
// Try to yield a more useful error. To avoid allocating on the happy path,
// we reconstruct the same `NameIterator` and replay it.
#[cfg(feature = "alloc")]
{
Err(Error::CertNotValidForName(InvalidNameContext {
expected: ServerName::DnsName(reference.to_owned()),
presented: NameIterator::new(cert.subject_alt_name)
.filter_map(|result| Some(format!("{:?}", result.ok()?)))
.collect(),
}))
}
}
/// A reference to a DNS Name presented by a server that may include a wildcard.
///
/// A `WildcardDnsNameRef` is guaranteed to be syntactically valid. The validity rules
/// are specified in [RFC 5280 Section 7.2], except that underscores are also
/// allowed.
///
/// Additionally, while [RFC6125 Section 4.1] says that a wildcard label may be of the form
/// `<x>*<y>.<DNSID>`, where `<x>` and/or `<y>` may be empty, we follow a stricter policy common
/// to most validation libraries (e.g. NSS) and only accept wildcard labels that are exactly `*`.
///
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
/// [RFC 6125 Section 4.1]: https://www.rfc-editor.org/rfc/rfc6125#section-4.1
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub(crate) struct WildcardDnsNameRef<'a>(&'a [u8]);
impl<'a> WildcardDnsNameRef<'a> {
/// Constructs a `WildcardDnsNameRef` from the given input if the input is a
/// syntactically-valid DNS name.
pub(crate) fn try_from_ascii(dns_name: &'a [u8]) -> Result<Self, InvalidDnsNameError> {
if !is_valid_dns_id(
untrusted::Input::from(dns_name),
IdRole::Reference,
Wildcards::Allow,
) {
return Err(InvalidDnsNameError);
}
Ok(Self(dns_name))
}
/// Yields a reference to the DNS name as a `&str`.
pub(crate) fn as_str(&self) -> &'a str {
// The unwrap won't fail because a `WildcardDnsNameRef` is guaranteed to be ASCII and
// ASCII is a subset of UTF-8.
core::str::from_utf8(self.0).unwrap()
}
}
impl core::fmt::Debug for WildcardDnsNameRef<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
f.write_str("WildcardDnsNameRef(\"")?;
// Convert each byte of the underlying ASCII string to a `char` and
// downcase it prior to formatting it. We avoid self.to_owned() since
// it requires allocation.
for &ch in self.0 {
f.write_char(char::from(ch).to_ascii_lowercase())?;
}
f.write_str("\")")
}
}
// We assume that both presented_dns_id and reference_dns_id are encoded in
// such a way that US-ASCII (7-bit) characters are encoded in one byte and no
// encoding of a non-US-ASCII character contains a code point in the range
// 0-127. For example, UTF-8 is OK but UTF-16 is not.
//
// RFC6125 says that a wildcard label may be of the form <x>*<y>.<DNSID>, where
// <x> and/or <y> may be empty. However, NSS requires <y> to be empty, and we
// follow NSS's stricter policy by accepting wildcards only of the form
// <x>*.<DNSID>, where <x> may be empty.
//
// An relative presented DNS ID matches both an absolute reference ID and a
// relative reference ID. Absolute presented DNS IDs are not supported:
//
// Presented ID Reference ID Result
// -------------------------------------
// example.com example.com Match
// example.com. example.com Mismatch
// example.com example.com. Match
// example.com. example.com. Mismatch
//
// There are more subtleties documented inline in the code.
//
// Name constraints ///////////////////////////////////////////////////////////
//
// This is all RFC 5280 has to say about dNSName constraints:
//
// DNS name restrictions are expressed as host.example.com. Any DNS
// name that can be constructed by simply adding zero or more labels to
// the left-hand side of the name satisfies the name constraint. For
// example, www.host.example.com would satisfy the constraint but
// host1.example.com would not.
//
// This lack of specificity has lead to a lot of uncertainty regarding
// subdomain matching. In particular, the following questions have been
// raised and answered:
//
// Q: Does a presented identifier equal (case insensitive) to the name
// constraint match the constraint? For example, does the presented
// ID "host.example.com" match a "host.example.com" constraint?
// A: Yes. RFC5280 says "by simply adding zero or more labels" and this
// is the case of adding zero labels.
//
// Q: When the name constraint does not start with ".", do subdomain
// presented identifiers match it? For example, does the presented
// ID "www.host.example.com" match a "host.example.com" constraint?
// A: Yes. RFC5280 says "by simply adding zero or more labels" and this
// is the case of adding more than zero labels. The example is the
// one from RFC 5280.
//
// Q: When the name constraint does not start with ".", does a
// non-subdomain prefix match it? For example, does "bigfoo.bar.com"
// match "foo.bar.com"? [4]
// A: No. We interpret RFC 5280's language of "adding zero or more labels"
// to mean that whole labels must be prefixed.
//
// (Note that the above three scenarios are the same as the RFC 6265
// domain matching rules [0].)
//
// Q: Is a name constraint that starts with "." valid, and if so, what
// semantics does it have? For example, does a presented ID of
// "www.example.com" match a constraint of ".example.com"? Does a
// presented ID of "example.com" match a constraint of ".example.com"?
// A: This implementation, NSS[1], and SChannel[2] all support a
// leading ".", but OpenSSL[3] does not yet. Amongst the
// implementations that support it, a leading "." is legal and means
// the same thing as when the "." is omitted, EXCEPT that a
// presented identifier equal (case insensitive) to the name
// constraint is not matched; i.e. presented dNSName identifiers
// must be subdomains. Some CAs in Mozilla's CA program (e.g. HARICA)
// have name constraints with the leading "." in their root
// certificates. The name constraints imposed on DCISS by Mozilla also
// have the it, so supporting this is a requirement for backward
// compatibility, even if it is not yet standardized. So, for example, a
// presented ID of "www.example.com" matches a constraint of
// ".example.com" but a presented ID of "example.com" does not.
//
// Q: Is there a way to prevent subdomain matches?
// A: Yes.
//
// Some people have proposed that dNSName constraints that do not
// start with a "." should be restricted to exact (case insensitive)
// matches. However, such a change of semantics from what RFC5280
// specifies would be a non-backward-compatible change in the case of
// permittedSubtrees constraints, and it would be a security issue for
// excludedSubtrees constraints.
//
// However, it can be done with a combination of permittedSubtrees and
// excludedSubtrees, e.g. "example.com" in permittedSubtrees and
// ".example.com" in excludedSubtrees.
//
// Q: Are name constraints allowed to be specified as absolute names?
// For example, does a presented ID of "example.com" match a name
// constraint of "example.com." and vice versa.
// A: Absolute names are not supported as presented IDs or name
// constraints. Only reference IDs may be absolute.
//
// Q: Is "" a valid dNSName constraint? If so, what does it mean?
// A: Yes. Any valid presented dNSName can be formed "by simply adding zero
// or more labels to the left-hand side" of "". In particular, an
// excludedSubtrees dNSName constraint of "" forbids all dNSNames.
//
// Q: Is "." a valid dNSName constraint? If so, what does it mean?
// A: No, because absolute names are not allowed (see above).
//
// [0] RFC 6265 (Cookies) Domain Matching rules:
// http://tools.ietf.org/html/rfc6265#section-5.1.3
// [1] NSS source code:
// https://mxr.mozilla.org/nss/source/lib/certdb/genname.c?rev=2a7348f013cb#1209
// [2] Description of SChannel's behavior from Microsoft:
// http://www.imc.org/ietf-pkix/mail-archive/msg04668.html
// [3] Proposal to add such support to OpenSSL:
// http://www.mail-archive.com/openssl-dev%40openssl.org/msg36204.html
// https://rt.openssl.org/Ticket/Display.html?id=3562
// [4] Feedback on the lack of clarify in the definition that never got
// incorporated into the spec:
// https://www.ietf.org/mail-archive/web/pkix/current/msg21192.html
pub(super) fn presented_id_matches_reference_id(
presented_dns_id: untrusted::Input<'_>,
reference_dns_id_role: IdRole,
reference_dns_id: untrusted::Input<'_>,
) -> Result<bool, Error> {
if !is_valid_dns_id(presented_dns_id, IdRole::Presented, Wildcards::Allow) {
return Err(Error::MalformedDnsIdentifier);
}
if !is_valid_dns_id(reference_dns_id, reference_dns_id_role, Wildcards::Deny) {
return Err(match reference_dns_id_role {
IdRole::NameConstraint => Error::MalformedNameConstraint,
_ => Error::MalformedDnsIdentifier,
});
}
let mut presented = untrusted::Reader::new(presented_dns_id);
let mut reference = untrusted::Reader::new(reference_dns_id);
match reference_dns_id_role {
IdRole::Reference => (),
IdRole::NameConstraint if presented_dns_id.len() > reference_dns_id.len() => {
if reference_dns_id.is_empty() {
// An empty constraint matches everything.
return Ok(true);
}
// If the reference ID starts with a dot then skip the prefix of
// the presented ID and start the comparison at the position of
// that dot. Examples:
//
// Matches Doesn't Match
// -----------------------------------------------------------
// original presented ID: www.example.com badexample.com
// skipped: www ba
// presented ID w/o prefix: .example.com dexample.com
// reference ID: .example.com .example.com
//
// If the reference ID does not start with a dot then we skip
// the prefix of the presented ID but also verify that the
// prefix ends with a dot. Examples:
//
// Matches Doesn't Match
// -----------------------------------------------------------
// original presented ID: www.example.com badexample.com
// skipped: www ba
// must be '.': . d
// presented ID w/o prefix: example.com example.com
// reference ID: example.com example.com
//
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 Ok(false);
}
}
}
IdRole::NameConstraint => (),
IdRole::Presented => unreachable!(),
}
// Only allow wildcard labels that consist only of '*'.
if presented.peek(b'*') {
if presented.skip(1).is_err() {
unreachable!();
}
loop {
if reference.read_byte().is_err() {
return Ok(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 Ok(false);
}
};
if presented.at_end() {
// Don't allow presented IDs to be absolute.
if presented_byte == b'.' {
return Err(Error::MalformedDnsIdentifier);
}
break;
}
}
// Allow a relative presented DNS ID to match an absolute reference DNS ID,
// unless we're matching a name constraint.
if !reference.at_end() {
if reference_dns_id_role != IdRole::NameConstraint {
match reference.read_byte() {
Ok(b'.') => (),
_ => {
return Ok(false);
}
};
}
if !reference.at_end() {
return Ok(false);
}
}
assert!(presented.at_end());
assert!(reference.at_end());
Ok(true)
}
#[inline]
fn ascii_lower(b: u8) -> u8 {
match b {
b'A'..=b'Z' => b + b'a' - b'A',
_ => b,
}
}
#[derive(Clone, Copy, PartialEq)]
enum Wildcards {
Deny,
Allow,
}
#[derive(Clone, Copy, PartialEq)]
pub(super) enum IdRole {
Reference,
Presented,
NameConstraint,
}
// https://tools.ietf.org/html/rfc5280#section-4.2.1.6:
//
// When the subjectAltName extension contains a domain name system
// label, the domain name MUST be stored in the dNSName (an IA5String).
// The name MUST be in the "preferred name syntax", as specified by
// Section 3.5 of [RFC1034] and as modified by Section 2.1 of
// [RFC1123].
//
// https://bugzilla.mozilla.org/show_bug.cgi?id=1136616: As an exception to the
// requirement above, underscores are also allowed in names for compatibility.
fn is_valid_dns_id(
hostname: untrusted::Input<'_>,
id_role: IdRole,
allow_wildcards: Wildcards,
) -> bool {
// https://blogs.msdn.microsoft.com/oldnewthing/20120412-00/?p=7873/
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;
// Only presented IDs are allowed to have wildcard labels. And, like
// Chromium, be stricter than RFC 6125 requires by insisting that a
// wildcard label consist only of '*'.
let is_wildcard = allow_wildcards == Wildcards::Allow && 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; // Labels must not start with a hyphen.
}
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; // Labels must not end with a hyphen.
}
label_length = 0;
}
_ => {
return false;
}
}
is_first_byte = false;
if input.at_end() {
break;
}
}
// Only reference IDs, not presented IDs or name constraints, may be
// absolute.
if label_length == 0 && id_role != IdRole::Reference {
return false;
}
if label_ends_with_hyphen {
return false; // Labels must not end with a hyphen.
}
if label_is_all_numeric {
return false; // Last label must not be all numeric.
}
if is_wildcard {
// If the DNS ID ends with a dot, the last dot signifies an absolute ID.
let label_count = if label_length == 0 {
dot_count
} else {
dot_count + 1
};
// Like NSS, require at least two labels to follow the wildcard label.
// TODO: Allow the TrustDomain to control this on a per-eTLD+1 basis,
// similar to Chromium. Even then, it might be better to still enforce
// that there are at least two labels after the wildcard.
if label_count < 3 {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::type_complexity)]
const PRESENTED_MATCHES_REFERENCE: &[(&[u8], &[u8], Result<bool, Error>)] = &[
(b"", b"a", Err(Error::MalformedDnsIdentifier)),
(b"a", b"a", Ok(true)),
(b"b", b"a", Ok(false)),
(b"*.b.a", b"c.b.a", Ok(true)),
(b"*.b.a", b"b.a", Ok(false)),
(b"*.b.a", b"b.a.", Ok(false)),
// Wildcard not in leftmost label
(b"d.c.b.a", b"d.c.b.a", Ok(true)),
(b"d.*.b.a", b"d.c.b.a", Err(Error::MalformedDnsIdentifier)),
(b"d.c*.b.a", b"d.c.b.a", Err(Error::MalformedDnsIdentifier)),
(b"d.c*.b.a", b"d.cc.b.a", Err(Error::MalformedDnsIdentifier)),
// case sensitivity
(
b"abcdefghijklmnopqrstuvwxyz",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
Ok(true),
),
(
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
b"abcdefghijklmnopqrstuvwxyz",
Ok(true),
),
(b"aBc", b"Abc", Ok(true)),
// digits
(b"a1", b"a1", Ok(true)),
// A trailing dot indicates an absolute name, and absolute names can match
// relative names, and vice-versa.
(b"example", b"example", Ok(true)),
(b"example.", b"example.", Err(Error::MalformedDnsIdentifier)),
(b"example", b"example.", Ok(true)),
(b"example.", b"example", Err(Error::MalformedDnsIdentifier)),
(b"example.com", b"example.com", Ok(true)),
(
b"example.com.",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(b"example.com", b"example.com.", Ok(true)),
(
b"example.com.",
b"example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"example.com..",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"example.com..",
b"example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"example.com...",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
// xn-- IDN prefix
(b"x*.b.a", b"xa.b.a", Err(Error::MalformedDnsIdentifier)),
(b"x*.b.a", b"xna.b.a", Err(Error::MalformedDnsIdentifier)),
(b"x*.b.a", b"xn-a.b.a", Err(Error::MalformedDnsIdentifier)),
(b"x*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
(b"xn*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
(
b"xn-*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn--*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(b"xn*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
(
b"xn-*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn--*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn---*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
// "*" cannot expand to nothing.
(b"c*.b.a", b"c.b.a", Err(Error::MalformedDnsIdentifier)),
// --------------------------------------------------------------------------
// The rest of these are test cases adapted from Chromium's
// x509_certificate_unittest.cc. The parameter order is the opposite in
// Chromium's tests. Also, they Ok tests were modified to fit into this
// framework or due to intentional differences between mozilla::pkix and
// Chromium.
(b"foo.com", b"foo.com", Ok(true)),
(b"f", b"f", Ok(true)),
(b"i", b"h", Ok(false)),
(b"*.foo.com", b"bar.foo.com", Ok(true)),
(b"*.test.fr", b"www.test.fr", Ok(true)),
(b"*.test.FR", b"wwW.tESt.fr", Ok(true)),
(b".uk", b"f.uk", Err(Error::MalformedDnsIdentifier)),
(
b"?.bar.foo.com",
b"w.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"(www|ftp).foo.com",
b"www.foo.com",
Err(Error::MalformedDnsIdentifier),
), // regex!
(
b"www.foo.com\0",
b"www.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"www.foo.com\0*.foo.com",
b"www.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"ww.house.example", b"www.house.example", Ok(false)),
(b"www.test.org", b"test.org", Ok(false)),
(b"*.test.org", b"test.org", Ok(false)),
(b"*.org", b"test.org", Err(Error::MalformedDnsIdentifier)),
// '*' must be the only character in the wildcard label
(
b"w*.bar.foo.com",
b"w.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"ww*ww.bar.foo.com",
b"www.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"ww*ww.bar.foo.com",
b"wwww.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"w*w.bar.foo.com",
b"wwww.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"w*w.bar.foo.c0m",
b"wwww.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"wa*.bar.foo.com",
b"WALLY.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"*Ly.bar.foo.com",
b"wally.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
// Chromium does URL decoding of the reference ID, but we don't, and we also
// require that the reference ID is valid, so we can't test these two.
// (b"www.foo.com", b"ww%57.foo.com", Ok(true)),
// (b"www&.foo.com", b"www%26.foo.com", Ok(true)),
(b"*.test.de", b"www.test.co.jp", Ok(false)),
(
b"*.jp",
b"www.test.co.jp",
Err(Error::MalformedDnsIdentifier),
),
(b"www.test.co.uk", b"www.test.co.jp", Ok(false)),
(
b"www.*.co.jp",
b"www.test.co.jp",
Err(Error::MalformedDnsIdentifier),
),
(b"www.bar.foo.com", b"www.bar.foo.com", Ok(true)),
(b"*.foo.com", b"www.bar.foo.com", Ok(false)),
(
b"*.*.foo.com",
b"www.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
// Our matcher requires the reference ID to be a valid DNS name, so we cannot
// test this case.
// (b"*.*.bar.foo.com", b"*..bar.foo.com", Ok(false)),
(b"www.bath.org", b"www.bath.org", Ok(true)),
// Our matcher requires the reference ID to be a valid DNS name, so we cannot
// test these cases.
// DNS_ID_MISMATCH("www.bath.org", ""),
// (b"www.bath.org", b"20.30.40.50", Ok(false)),
// (b"www.bath.org", b"66.77.88.99", Ok(false)),
// IDN tests
(
b"xn--poema-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Ok(true),
),
(
b"*.xn--poema-9qae5a.com.br",
b"www.xn--poema-9qae5a.com.br",
Ok(true),
),
(
b"*.xn--poema-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Ok(false),
),
(
b"xn--poema-*.com.br",
b"xn--poema-9qae5a.com.br",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn--*-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Err(Error::MalformedDnsIdentifier),
),
(
b"*--poema-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Err(Error::MalformedDnsIdentifier),
),
// The following are adapted from the examples quoted from
// http://tools.ietf.org/html/rfc6125#section-6.4.3
// (e.g., *.example.com would match foo.example.com but
// not bar.foo.example.com or example.com).
(b"*.example.com", b"foo.example.com", Ok(true)),
(b"*.example.com", b"bar.foo.example.com", Ok(false)),
(b"*.example.com", b"example.com", Ok(false)),
(
b"baz*.example.net",
b"baz1.example.net",
Err(Error::MalformedDnsIdentifier),
),
(
b"*baz.example.net",
b"foobaz.example.net",
Err(Error::MalformedDnsIdentifier),
),
(
b"b*z.example.net",
b"buzz.example.net",
Err(Error::MalformedDnsIdentifier),
),
// Wildcards should not be valid for public registry controlled domains,
// and unknown/unrecognized domains, at least three domain components must
// be present. For mozilla::pkix and NSS, there must always be at least two
// labels after the wildcard label.
(b"*.test.example", b"www.test.example", Ok(true)),
(b"*.example.co.uk", b"test.example.co.uk", Ok(true)),
(
b"*.example",
b"test.example",
Err(Error::MalformedDnsIdentifier),
),
// The result is different than Chromium, because Chromium takes into account
// the additional knowledge it has that "co.uk" is a TLD. mozilla::pkix does
// not know that.
(b"*.co.uk", b"example.co.uk", Ok(true)),
(b"*.com", b"foo.com", Err(Error::MalformedDnsIdentifier)),
(b"*.us", b"foo.us", Err(Error::MalformedDnsIdentifier)),
(b"*", b"foo", Err(Error::MalformedDnsIdentifier)),
// IDN variants of wildcards and registry controlled domains.
(
b"*.xn--poema-9qae5a.com.br",
b"www.xn--poema-9qae5a.com.br",
Ok(true),
),
(
b"*.example.xn--mgbaam7a8h",
b"test.example.xn--mgbaam7a8h",
Ok(true),
),
// RFC6126 allows this, and NSS accepts it, but Chromium disallows it.
// TODO: File bug against Chromium.
(b"*.com.br", b"xn--poema-9qae5a.com.br", Ok(true)),
(
b"*.xn--mgbaam7a8h",
b"example.xn--mgbaam7a8h",
Err(Error::MalformedDnsIdentifier),
),
// Wildcards should be permissible for 'private' registry-controlled
// domains. (In mozilla::pkix, we do not know if it is a private registry-
// controlled domain or not.)
(b"*.appspot.com", b"www.appspot.com", Ok(true)),
(b"*.s3.amazonaws.com", b"foo.s3.amazonaws.com", Ok(true)),
// Multiple wildcards are not valid.
(
b"*.*.com",
b"foo.example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.bar.*.com",
b"foo.bar.example.com",
Err(Error::MalformedDnsIdentifier),
),
// Absolute vs relative DNS name tests. Although not explicitly specified
// in RFC 6125, absolute reference names (those ending in a .) should
// match either absolute or relative presented names.
// TODO: File errata against RFC 6125 about this.
(b"foo.com.", b"foo.com", Err(Error::MalformedDnsIdentifier)),
(b"foo.com", b"foo.com.", Ok(true)),
(b"foo.com.", b"foo.com.", Err(Error::MalformedDnsIdentifier)),
(b"f.", b"f", Err(Error::MalformedDnsIdentifier)),
(b"f", b"f.", Ok(true)),
(b"f.", b"f.", Err(Error::MalformedDnsIdentifier)),
(
b"*.bar.foo.com.",
b"www-3.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"*.bar.foo.com", b"www-3.bar.foo.com.", Ok(true)),
(
b"*.bar.foo.com.",
b"www-3.bar.foo.com.",
Err(Error::MalformedDnsIdentifier),
),
// We require the reference ID to be a valid DNS name, so we cannot test this
// case.
// (b".", b".", Ok(false)),
(
b"*.com.",
b"example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.com",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.com.",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(b"*.", b"foo.", Err(Error::MalformedDnsIdentifier)),
(b"*.", b"foo", Err(Error::MalformedDnsIdentifier)),
// The result is different than Chromium because we don't know that co.uk is
// a TLD.
(
b"*.co.uk.",
b"foo.co.uk",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.co.uk.",
b"foo.co.uk.",
Err(Error::MalformedDnsIdentifier),
),
];
#[test]
fn presented_matches_reference_test() {
for (presented, reference, expected_result) in PRESENTED_MATCHES_REFERENCE {
let actual_result = presented_id_matches_reference_id(
untrusted::Input::from(presented),
IdRole::Reference,
untrusted::Input::from(reference),
);
assert_eq!(
&actual_result, expected_result,
"presented_id_matches_reference_id(\"{presented:?}\", \"{reference:?}\")"
);
}
}
// (presented_name, constraint, expected_matches)
#[allow(clippy::type_complexity)]
const PRESENTED_MATCHES_CONSTRAINT: &[(&[u8], &[u8], Result<bool, Error>)] = &[
// No absolute presented IDs allowed
(b".", b"", Err(Error::MalformedDnsIdentifier)),
(b"www.example.com.", b"", Err(Error::MalformedDnsIdentifier)),
(
b"www.example.com.",
b"www.example.com.",
Err(Error::MalformedDnsIdentifier),
),
// No absolute constraints allowed
(
b"www.example.com",
b".",
Err(Error::MalformedNameConstraint),
),
(
b"www.example.com",
b"www.example.com.",
Err(Error::MalformedNameConstraint),
),
// No wildcard in constraints allowed
(
b"www.example.com",
b"*.example.com",
Err(Error::MalformedNameConstraint),
),
// No empty presented IDs allowed
(b"", b"", Err(Error::MalformedDnsIdentifier)),
// Empty constraints match everything allowed
(b"example.com", b"", Ok(true)),
(b"*.example.com", b"", Ok(true)),
// Constraints that start with a dot
(b"www.example.com", b".example.com", Ok(true)),
(b"www.example.com", b".EXAMPLE.COM", Ok(true)),
(b"www.example.com", b".axample.com", Ok(false)),
(b"www.example.com", b".xample.com", Ok(false)),
(b"www.example.com", b".exampl.com", Ok(false)),
(b"badexample.com", b".example.com", Ok(false)),
// Constraints that do not start with a dot
(b"www.example.com", b"example.com", Ok(true)),
(b"www.example.com", b"EXAMPLE.COM", Ok(true)),
(b"www.example.com", b"axample.com", Ok(false)),
(b"www.example.com", b"xample.com", Ok(false)),
(b"www.example.com", b"exampl.com", Ok(false)),
(b"badexample.com", b"example.com", Ok(false)),
// Presented IDs with wildcard
(b"*.example.com", b".example.com", Ok(true)),
(b"*.example.com", b"example.com", Ok(true)),
(b"*.example.com", b"www.example.com", Ok(true)),
(b"*.example.com", b"www.EXAMPLE.COM", Ok(true)),
(b"*.example.com", b"www.axample.com", Ok(false)),
(b"*.example.com", b".xample.com", Ok(false)),
(b"*.example.com", b"xample.com", Ok(false)),
(b"*.example.com", b".exampl.com", Ok(false)),
(b"*.example.com", b"exampl.com", Ok(false)),
// Matching IDs
(b"www.example.com", b"www.example.com", Ok(true)),
];
#[test]
fn presented_matches_constraint_test() {
for (presented, constraint, expected_result) in PRESENTED_MATCHES_CONSTRAINT {
let actual_result = presented_id_matches_reference_id(
untrusted::Input::from(presented),
IdRole::NameConstraint,
untrusted::Input::from(constraint),
);
assert_eq!(
&actual_result, expected_result,
"presented_id_matches_constraint(\"{presented:?}\", \"{constraint:?}\")",
);
}
}
}

View File

@@ -0,0 +1,706 @@
// Copyright 2015-2020 Brian Smith.
//
// 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.
#[cfg(feature = "alloc")]
use alloc::format;
use pki_types::IpAddr;
#[cfg(feature = "alloc")]
use pki_types::ServerName;
use super::{GeneralName, NameIterator};
use crate::cert::Cert;
use crate::error::{Error, InvalidNameContext};
pub(crate) fn verify_ip_address_names(reference: &IpAddr, cert: &Cert<'_>) -> Result<(), Error> {
let ip_address = match reference {
IpAddr::V4(ip) => untrusted::Input::from(ip.as_ref()),
IpAddr::V6(ip) => untrusted::Input::from(ip.as_ref()),
};
let result = NameIterator::new(cert.subject_alt_name).find_map(|result| {
let name = match result {
Ok(name) => name,
Err(err) => return Some(Err(err)),
};
let presented_id = match name {
GeneralName::IpAddress(presented) => presented,
_ => return None,
};
match presented_id_matches_reference_id(presented_id, ip_address) {
true => Some(Ok(())),
false => None,
}
});
match result {
Some(result) => return result,
#[cfg(feature = "alloc")]
None => {}
#[cfg(not(feature = "alloc"))]
None => Err(Error::CertNotValidForName(InvalidNameContext {})),
}
#[cfg(feature = "alloc")]
{
Err(Error::CertNotValidForName(InvalidNameContext {
expected: ServerName::from(*reference),
presented: NameIterator::new(cert.subject_alt_name)
.filter_map(|result| Some(format!("{:?}", result.ok()?)))
.collect(),
}))
}
}
// https://tools.ietf.org/html/rfc5280#section-4.2.1.6 says:
// When the subjectAltName extension contains an iPAddress, the address
// MUST be stored in the octet string in "network byte order", as
// specified in [RFC791]. The least significant bit (LSB) of each octet
// is the LSB of the corresponding byte in the network address. For IP
// version 4, as specified in [RFC791], the octet string MUST contain
// exactly four octets. For IP version 6, as specified in
// [RFC2460], the octet string MUST contain exactly sixteen octets.
fn presented_id_matches_reference_id(
presented_id: untrusted::Input<'_>,
reference_id: untrusted::Input<'_>,
) -> bool {
match (presented_id.len(), reference_id.len()) {
(4, 4) => (),
(16, 16) => (),
_ => {
return false;
}
};
let mut presented_ip_address = untrusted::Reader::new(presented_id);
let mut reference_ip_address = untrusted::Reader::new(reference_id);
while !presented_ip_address.at_end() {
let presented_ip_address_byte = presented_ip_address.read_byte().unwrap();
let reference_ip_address_byte = reference_ip_address.read_byte().unwrap();
if presented_ip_address_byte != reference_ip_address_byte {
return false;
}
}
true
}
// https://tools.ietf.org/html/rfc5280#section-4.2.1.10 says:
//
// For IPv4 addresses, the iPAddress field of GeneralName MUST contain
// eight (8) octets, encoded in the style of RFC 4632 (CIDR) to represent
// an address range [RFC4632]. For IPv6 addresses, the iPAddress field
// MUST contain 32 octets similarly encoded. For example, a name
// constraint for "class C" subnet 192.0.2.0 is represented as the
// octets C0 00 02 00 FF FF FF 00, representing the CIDR notation
// 192.0.2.0/24 (mask 255.255.255.0).
pub(super) fn presented_id_matches_constraint(
name: untrusted::Input<'_>,
constraint: untrusted::Input<'_>,
) -> Result<bool, Error> {
match (name.len(), constraint.len()) {
(4, 8) => (),
(16, 32) => (),
// 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();
let mask = value.read_bytes(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);
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);
}
if name.at_end() {
break;
}
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn presented_id_matches_constraint_ipv4_test() {
let names_and_constraints = vec![
(
// 192.0.2.0 matches constraint 192.0.2.0/24
[0xC0, 0x00, 0x02, 0x00],
[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00],
Ok(true),
),
(
// 192.0.2.1 matches constraint 192.0.2.0/24
[0xC0, 0x00, 0x02, 0x01],
[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00],
Ok(true),
),
(
// 192.0.2.255 matches constraint 192.0.2.0/24
[0xC0, 0x00, 0x02, 0xFF],
[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00],
Ok(true),
),
(
// 192.0.1.255 does not match constraint 192.0.2.0/24
[0xC0, 0x00, 0x01, 0xFF],
[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00],
Ok(false),
),
(
// 192.0.3.0 does not match constraint 192.0.2.0/24
[0xC0, 0x00, 0x03, 0x00],
[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00],
Ok(false),
),
];
for (name, constraint, match_result) in names_and_constraints {
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&name),
untrusted::Input::from(&constraint),
),
match_result
)
}
// Invalid name length (shorter)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[0xC0, 0x00, 0x02]),
untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00]),
),
Err(Error::BadDer),
);
// Invalid name length (longer)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0x00]),
untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00]),
),
Err(Error::BadDer),
);
// Unmatching constraint size (shorter)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00]),
untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF]),
),
Err(Error::InvalidNetworkMaskConstraint),
);
// Unmatching constraint size (longer)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00]),
untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00]),
),
Err(Error::InvalidNetworkMaskConstraint),
);
// Unmatching constraint size (IPv6 constraint for IPv4 address)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00]),
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]),
),
Ok(false),
);
}
#[test]
fn presented_id_matches_constraint_ipv6_test() {
let names_and_constraints = vec![
(
// 2001:0DB8:ABCD:0012:0000:0000:0000:0000 matches constraint
// 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
],
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
],
Ok(true),
),
(
// 2001:0DB8:ABCD:0012:0000:0000:0000:0001 matches constraint
// 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01,
],
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
],
Ok(true),
),
(
// 2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF matches constraint
// 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF,
],
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
],
Ok(true),
),
(
// 2001:0DB8:ABCD:0011:0000:0000:0000:0000 does not match constraint
// 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
],
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
],
Ok(false),
),
(
// 2001:0DB8:ABCD:0013:0000:0000:0000:0000 does not match constraint
// 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
],
[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
],
Ok(false),
),
];
for (name, constraint, match_result) in names_and_constraints {
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&name),
untrusted::Input::from(&constraint),
),
match_result
)
}
// Invalid name length (shorter)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
]),
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]),
),
Err(Error::BadDer),
);
// Invalid name length (longer)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
]),
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]),
),
Err(Error::BadDer),
);
// Unmatching constraint size (shorter)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
]),
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00
]),
),
Err(Error::InvalidNetworkMaskConstraint),
);
// Unmatching constraint size (longer)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
]),
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]),
),
Err(Error::InvalidNetworkMaskConstraint),
);
// Unmatching constraint size (IPv4 constraint for IPv6 address)
assert_eq!(
presented_id_matches_constraint(
untrusted::Input::from(&[
0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
]),
untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00]),
),
Ok(false),
);
}
#[test]
fn test_presented_id_matches_reference_id() {
assert!(!presented_id_matches_reference_id(
untrusted::Input::from(&[]),
untrusted::Input::from(&[]),
));
assert!(!presented_id_matches_reference_id(
untrusted::Input::from(&[0x01]),
untrusted::Input::from(&[])
));
assert!(!presented_id_matches_reference_id(
untrusted::Input::from(&[]),
untrusted::Input::from(&[0x01])
));
assert!(presented_id_matches_reference_id(
untrusted::Input::from(&[1, 2, 3, 4]),
untrusted::Input::from(&[1, 2, 3, 4])
));
assert!(!presented_id_matches_reference_id(
untrusted::Input::from(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
untrusted::Input::from(&[1, 2, 3, 4])
));
assert!(!presented_id_matches_reference_id(
untrusted::Input::from(&[1, 2, 3, 4]),
untrusted::Input::from(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
));
assert!(presented_id_matches_reference_id(
untrusted::Input::from(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
untrusted::Input::from(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
));
}
#[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 = "std"))]
mod alloc_tests {
use super::*;
// (presented_address, constraint_address, constraint_mask, expected_result)
const PRESENTED_MATCHES_CONSTRAINT: &[(&str, &str, &str, Result<bool, Error>)] = &[
// 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)),
];
#[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::<IpAddr>().unwrap() {
IpAddr::V4(p) => Box::new(p.octets()),
IpAddr::V6(p) => Box::new(p.octets()),
};
let ca_bytes: Box<[u8]> = match constraint_address.parse::<IpAddr>().unwrap() {
IpAddr::V4(ca) => Box::new(ca.octets()),
IpAddr::V6(ca) => Box::new(ca.octets()),
};
let cm_bytes: Box<[u8]> = match constraint_mask.parse::<IpAddr>().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:?}\")"
);
}
}
}

View File

@@ -0,0 +1,456 @@
// Copyright 2022 Rafael Fernández López.
//
// 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.
#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "alloc")]
use core::fmt;
use crate::der::{self, FromDer};
use crate::error::{DerTypeId, Error};
use crate::verify_cert::{Budget, PathNode};
mod dns_name;
use dns_name::IdRole;
pub(crate) use dns_name::{WildcardDnsNameRef, verify_dns_names};
mod ip_address;
pub(crate) use ip_address::verify_ip_address_names;
// https://tools.ietf.org/html/rfc5280#section-4.2.1.10
pub(crate) fn check_name_constraints(
constraints: Option<&mut untrusted::Reader<'_>>,
path: &PathNode<'_>,
budget: &mut Budget,
) -> Result<(), Error> {
let constraints = match constraints {
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.into()) {
return Ok(None);
}
der::expect_tag(inner, subtrees_tag).map(Some)
}
let permitted_subtrees = parse_subtrees(constraints, der::Tag::ContextSpecificConstructed0)?;
let excluded_subtrees = parse_subtrees(constraints, der::Tag::ContextSpecificConstructed1)?;
for path in path.iter() {
let result = NameIterator::new(path.cert.subject_alt_name).find_map(|result| {
let name = match result {
Ok(name) => name,
Err(err) => return Some(Err(err)),
};
check_presented_id_conforms_to_constraints(
name,
permitted_subtrees,
excluded_subtrees,
budget,
)
});
if let Some(Err(err)) = result {
return Err(err);
}
let result = check_presented_id_conforms_to_constraints(
GeneralName::DirectoryName,
permitted_subtrees,
excluded_subtrees,
budget,
);
if let Some(Err(err)) = result {
return Err(err);
}
}
Ok(())
}
fn check_presented_id_conforms_to_constraints(
name: GeneralName<'_>,
permitted_subtrees: Option<untrusted::Input<'_>>,
excluded_subtrees: Option<untrusted::Input<'_>>,
budget: &mut Budget,
) -> Option<Result<(), Error>> {
let subtrees = [
(Subtrees::PermittedSubtrees, permitted_subtrees),
(Subtrees::ExcludedSubtrees, excluded_subtrees),
];
fn general_subtree<'b>(input: &mut untrusted::Reader<'b>) -> Result<GeneralName<'b>, Error> {
der::read_all(der::expect_tag(input, der::Tag::Sequence)?)
}
for (subtrees, constraints) in subtrees {
let mut constraints = match constraints {
Some(constraints) => untrusted::Reader::new(constraints),
None => continue,
};
let mut has_permitted_subtrees_match = false;
let mut has_permitted_subtrees_mismatch = false;
while !constraints.at_end() {
if let Err(e) = budget.consume_name_constraint_comparison() {
return Some(Err(e));
}
// 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."
//
// Since the default value isn't allowed to be encoded according to the
// DER encoding rules for DEFAULT, this is equivalent to saying that
// neither minimum or maximum must be encoded.
let base = match general_subtree(&mut constraints) {
Ok(base) => base,
Err(err) => return Some(Err(err)),
};
let matches = match (name, base) {
(GeneralName::DnsName(name), GeneralName::DnsName(base)) => {
dns_name::presented_id_matches_reference_id(name, IdRole::NameConstraint, base)
}
(GeneralName::DirectoryName, GeneralName::DirectoryName) => Ok(
// 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 => false,
Subtrees::ExcludedSubtrees => true,
},
),
(GeneralName::IpAddress(name), GeneralName::IpAddress(base)) => {
ip_address::presented_id_matches_constraint(name, base)
}
// RFC 4280 says "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." Later, the CABForum agreed to support non-critical
// constraints, so it is important to reject the cert without
// considering whether the name constraint it critical.
(GeneralName::Unsupported(name_tag), GeneralName::Unsupported(base_tag))
if name_tag == base_tag =>
{
Err(Error::NameConstraintViolation)
}
_ => {
// mismatch between constraint and name types; continue with current
// name and next constraint
continue;
}
};
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 Some(Err(Error::NameConstraintViolation));
}
(Subtrees::ExcludedSubtrees, Ok(false)) => (),
(_, Err(err)) => return Some(Err(err)),
}
}
if has_permitted_subtrees_mismatch && !has_permitted_subtrees_match {
// If there was any entry of the given type in permittedSubtrees, then
// it required that at least one of them must match. Since none of them
// did, we have a failure.
return Some(Err(Error::NameConstraintViolation));
}
}
None
}
#[derive(Clone, Copy)]
enum Subtrees {
PermittedSubtrees,
ExcludedSubtrees,
}
pub(crate) struct NameIterator<'a> {
subject_alt_name: Option<untrusted::Reader<'a>>,
}
impl<'a> NameIterator<'a> {
pub(crate) fn new(subject_alt_name: Option<untrusted::Input<'a>>) -> Self {
Self {
subject_alt_name: subject_alt_name.map(untrusted::Reader::new),
}
}
}
impl<'a> Iterator for NameIterator<'a> {
type Item = Result<GeneralName<'a>, Error>;
fn next(&mut self) -> Option<Self::Item> {
let subject_alt_name = self.subject_alt_name.as_mut()?;
// https://bugzilla.mozilla.org/show_bug.cgi?id=1143085: An empty
// subjectAltName is not legal, but some certificates have an empty
// subjectAltName. Since we don't support CN-IDs, the certificate
// will be rejected either way, but checking `at_end` before
// attempting to parse the first entry allows us to return a better
// error code.
if subject_alt_name.at_end() {
self.subject_alt_name = None;
return None;
}
let err = match GeneralName::from_der(subject_alt_name) {
Ok(name) => return Some(Ok(name)),
Err(err) => err,
};
// Make sure we don't yield any items after this error.
self.subject_alt_name = None;
Some(Err(err))
}
}
// It is *not* valid to derive `Eq`, `PartialEq, etc. for this type. In
// particular, for the types of `GeneralName`s that we don't understand, we
// don't even store the value. Also, the meaning of a `GeneralName` in a name
// constraint is different than the meaning of the identically-represented
// `GeneralName` in other contexts.
#[derive(Clone, Copy)]
pub(crate) enum GeneralName<'a> {
DnsName(untrusted::Input<'a>),
DirectoryName,
IpAddress(untrusted::Input<'a>),
UniformResourceIdentifier(untrusted::Input<'a>),
// The value is the `tag & ~(der::CONTEXT_SPECIFIC | der::CONSTRUCTED)` so
// that the name constraint checking matches tags regardless of whether
// those bits are set.
Unsupported(u8),
}
impl<'a> FromDer<'a> for GeneralName<'a> {
fn from_der(reader: &mut untrusted::Reader<'a>) -> Result<Self, Error> {
use GeneralName::*;
use der::{CONSTRUCTED, CONTEXT_SPECIFIC};
#[allow(clippy::identity_op)]
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(reader)?;
Ok(match tag {
DNS_NAME_TAG => DnsName(value),
DIRECTORY_NAME_TAG => DirectoryName,
IP_ADDRESS_TAG => IpAddress(value),
UNIFORM_RESOURCE_IDENTIFIER_TAG => UniformResourceIdentifier(value),
OTHER_NAME_TAG | RFC822_NAME_TAG | X400_ADDRESS_TAG | EDI_PARTY_NAME_TAG
| REGISTERED_ID_TAG => Unsupported(tag & !(CONTEXT_SPECIFIC | CONSTRUCTED)),
_ => return Err(Error::BadDer),
})
}
const TYPE_ID: DerTypeId = DerTypeId::GeneralName;
}
#[cfg(feature = "alloc")]
impl fmt::Debug for GeneralName<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GeneralName::DnsName(name) => write!(
f,
"DnsName(\"{}\")",
String::from_utf8_lossy(name.as_slice_less_safe())
),
GeneralName::DirectoryName => write!(f, "DirectoryName"),
GeneralName::IpAddress(ip) => {
write!(f, "IpAddress({:?})", IpAddrSlice(ip.as_slice_less_safe()))
}
GeneralName::UniformResourceIdentifier(uri) => write!(
f,
"UniformResourceIdentifier(\"{}\")",
String::from_utf8_lossy(uri.as_slice_less_safe())
),
GeneralName::Unsupported(tag) => write!(f, "Unsupported(0x{tag:02x})"),
}
}
}
#[cfg(feature = "alloc")]
struct IpAddrSlice<'a>(&'a [u8]);
#[cfg(feature = "alloc")]
impl fmt::Debug for IpAddrSlice<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0.len() {
4 => {
let mut first = true;
for byte in self.0 {
match first {
true => first = false,
false => f.write_str(".")?,
}
write!(f, "{byte}")?;
}
Ok(())
}
16 => {
let (mut first, mut skipping) = (true, false);
for group in self.0.chunks_exact(2) {
match (first, group == [0, 0], skipping) {
(true, _, _) => first = false,
(false, false, false) => f.write_str(":")?,
(false, true, _) => {
skipping = true;
continue;
}
(false, false, true) => {
skipping = false;
f.write_str("::")?;
}
}
if group[0] != 0 {
write!(f, "{:x}", group[0])?;
}
match group[0] {
0 => write!(f, "{:x}", group[1])?,
_ => write!(f, "{:02x}", group[1])?,
}
}
Ok(())
}
_ => {
f.write_str("[invalid: ")?;
let mut first = true;
for byte in self.0 {
match first {
true => first = false,
false => f.write_str(", ")?,
}
write!(f, "{byte:02x}")?;
}
f.write_str("]")
}
}
}
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
#[test]
fn debug_names() {
assert_eq!(
format!(
"{:?}",
GeneralName::DnsName(untrusted::Input::from(b"example.com"))
),
"DnsName(\"example.com\")"
);
assert_eq!(format!("{:?}", GeneralName::DirectoryName), "DirectoryName");
assert_eq!(
format!(
"{:?}",
GeneralName::IpAddress(untrusted::Input::from(&[192, 0, 2, 1][..]))
),
"IpAddress(192.0.2.1)"
);
assert_eq!(
format!(
"{:?}",
GeneralName::IpAddress(untrusted::Input::from(
&[0x20, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0d, 0xb8][..]
))
),
"IpAddress(2001::db8)"
);
assert_eq!(
format!(
"{:?}",
GeneralName::IpAddress(untrusted::Input::from(&[1, 2, 3, 4, 5, 6][..]))
),
"IpAddress([invalid: 01, 02, 03, 04, 05, 06])"
);
assert_eq!(
format!(
"{:?}",
GeneralName::UniformResourceIdentifier(untrusted::Input::from(
b"https://example.com"
))
),
"UniformResourceIdentifier(\"https://example.com\")"
);
assert_eq!(
format!("{:?}", GeneralName::Unsupported(0x66)),
"Unsupported(0x66)"
);
}
#[test]
fn name_iter_end_after_error() {
let input = untrusted::Input::from(&[0x30]);
let mut iter = NameIterator::new(Some(input));
assert_eq!(iter.next().unwrap().unwrap_err(), Error::BadDer);
assert!(iter.next().is_none());
}
}