webpki/subject_name/
dns_name.rs

1// Copyright 2015-2020 Brian Smith.
2//
3// Permission to use, copy, modify, and/or distribute this software for any
4// purpose with or without fee is hereby granted, provided that the above
5// copyright notice and this permission notice appear in all copies.
6//
7// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
8// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
10// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
15#[cfg(feature = "alloc")]
16use alloc::format;
17use core::fmt::Write;
18
19#[cfg(feature = "alloc")]
20use pki_types::ServerName;
21use pki_types::{DnsName, InvalidDnsNameError};
22
23use super::{GeneralName, NameIterator};
24use crate::cert::Cert;
25use crate::error::{Error, InvalidNameContext};
26
27pub(crate) fn verify_dns_names(reference: &DnsName<'_>, cert: &Cert<'_>) -> Result<(), Error> {
28    let dns_name = untrusted::Input::from(reference.as_ref().as_bytes());
29    let result = NameIterator::new(cert.subject_alt_name).find_map(|result| {
30        let name = match result {
31            Ok(name) => name,
32            Err(err) => return Some(Err(err)),
33        };
34
35        let presented_id = match name {
36            GeneralName::DnsName(presented) => presented,
37            _ => return None,
38        };
39
40        match presented_id_matches_reference_id(presented_id, IdRole::Reference, dns_name) {
41            Ok(true) => Some(Ok(())),
42            Ok(false) | Err(Error::MalformedDnsIdentifier) => None,
43            Err(e) => Some(Err(e)),
44        }
45    });
46
47    match result {
48        Some(result) => return result,
49        #[cfg(feature = "alloc")]
50        None => {}
51        #[cfg(not(feature = "alloc"))]
52        None => Err(Error::CertNotValidForName(InvalidNameContext {})),
53    }
54
55    // Try to yield a more useful error. To avoid allocating on the happy path,
56    // we reconstruct the same `NameIterator` and replay it.
57    #[cfg(feature = "alloc")]
58    {
59        Err(Error::CertNotValidForName(InvalidNameContext {
60            expected: ServerName::DnsName(reference.to_owned()),
61            presented: NameIterator::new(cert.subject_alt_name)
62                .filter_map(|result| Some(format!("{:?}", result.ok()?)))
63                .collect(),
64        }))
65    }
66}
67
68/// A reference to a DNS Name presented by a server that may include a wildcard.
69///
70/// A `WildcardDnsNameRef` is guaranteed to be syntactically valid. The validity rules
71/// are specified in [RFC 5280 Section 7.2], except that underscores are also
72/// allowed.
73///
74/// Additionally, while [RFC6125 Section 4.1] says that a wildcard label may be of the form
75/// `<x>*<y>.<DNSID>`, where `<x>` and/or `<y>` may be empty, we follow a stricter policy common
76/// to most validation libraries (e.g. NSS) and only accept wildcard labels that are exactly `*`.
77///
78/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
79/// [RFC 6125 Section 4.1]: https://www.rfc-editor.org/rfc/rfc6125#section-4.1
80#[derive(Clone, Copy, Eq, PartialEq, Hash)]
81pub(crate) struct WildcardDnsNameRef<'a>(&'a [u8]);
82
83impl<'a> WildcardDnsNameRef<'a> {
84    /// Constructs a `WildcardDnsNameRef` from the given input if the input is a
85    /// syntactically-valid DNS name.
86    pub(crate) fn try_from_ascii(dns_name: &'a [u8]) -> Result<Self, InvalidDnsNameError> {
87        if !is_valid_dns_id(
88            untrusted::Input::from(dns_name),
89            IdRole::Reference,
90            Wildcards::Allow,
91        ) {
92            return Err(InvalidDnsNameError);
93        }
94
95        Ok(Self(dns_name))
96    }
97
98    /// Yields a reference to the DNS name as a `&str`.
99    pub(crate) fn as_str(&self) -> &'a str {
100        // The unwrap won't fail because a `WildcardDnsNameRef` is guaranteed to be ASCII and
101        // ASCII is a subset of UTF-8.
102        core::str::from_utf8(self.0).unwrap()
103    }
104}
105
106impl core::fmt::Debug for WildcardDnsNameRef<'_> {
107    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
108        f.write_str("WildcardDnsNameRef(\"")?;
109
110        // Convert each byte of the underlying ASCII string to a `char` and
111        // downcase it prior to formatting it. We avoid self.to_owned() since
112        // it requires allocation.
113        for &ch in self.0 {
114            f.write_char(char::from(ch).to_ascii_lowercase())?;
115        }
116
117        f.write_str("\")")
118    }
119}
120
121// We assume that both presented_dns_id and reference_dns_id are encoded in
122// such a way that US-ASCII (7-bit) characters are encoded in one byte and no
123// encoding of a non-US-ASCII character contains a code point in the range
124// 0-127. For example, UTF-8 is OK but UTF-16 is not.
125//
126// RFC6125 says that a wildcard label may be of the form <x>*<y>.<DNSID>, where
127// <x> and/or <y> may be empty. However, NSS requires <y> to be empty, and we
128// follow NSS's stricter policy by accepting wildcards only of the form
129// <x>*.<DNSID>, where <x> may be empty.
130//
131// An relative presented DNS ID matches both an absolute reference ID and a
132// relative reference ID. Absolute presented DNS IDs are not supported:
133//
134//      Presented ID   Reference ID  Result
135//      -------------------------------------
136//      example.com    example.com   Match
137//      example.com.   example.com   Mismatch
138//      example.com    example.com.  Match
139//      example.com.   example.com.  Mismatch
140//
141// There are more subtleties documented inline in the code.
142//
143// Name constraints ///////////////////////////////////////////////////////////
144//
145// This is all RFC 5280 has to say about dNSName constraints:
146//
147//     DNS name restrictions are expressed as host.example.com.  Any DNS
148//     name that can be constructed by simply adding zero or more labels to
149//     the left-hand side of the name satisfies the name constraint.  For
150//     example, www.host.example.com would satisfy the constraint but
151//     host1.example.com would not.
152//
153// This lack of specificity has lead to a lot of uncertainty regarding
154// subdomain matching. In particular, the following questions have been
155// raised and answered:
156//
157//     Q: Does a presented identifier equal (case insensitive) to the name
158//        constraint match the constraint? For example, does the presented
159//        ID "host.example.com" match a "host.example.com" constraint?
160//     A: Yes. RFC5280 says "by simply adding zero or more labels" and this
161//        is the case of adding zero labels.
162//
163//     Q: When the name constraint does not start with ".", do subdomain
164//        presented identifiers match it? For example, does the presented
165//        ID "www.host.example.com" match a "host.example.com" constraint?
166//     A: Yes. RFC5280 says "by simply adding zero or more labels" and this
167//        is the case of adding more than zero labels. The example is the
168//        one from RFC 5280.
169//
170//     Q: When the name constraint does not start with ".", does a
171//        non-subdomain prefix match it? For example, does "bigfoo.bar.com"
172//        match "foo.bar.com"? [4]
173//     A: No. We interpret RFC 5280's language of "adding zero or more labels"
174//        to mean that whole labels must be prefixed.
175//
176//     (Note that the above three scenarios are the same as the RFC 6265
177//     domain matching rules [0].)
178//
179//     Q: Is a name constraint that starts with "." valid, and if so, what
180//        semantics does it have? For example, does a presented ID of
181//        "www.example.com" match a constraint of ".example.com"? Does a
182//        presented ID of "example.com" match a constraint of ".example.com"?
183//     A: This implementation, NSS[1], and SChannel[2] all support a
184//        leading ".", but OpenSSL[3] does not yet. Amongst the
185//        implementations that support it, a leading "." is legal and means
186//        the same thing as when the "." is omitted, EXCEPT that a
187//        presented identifier equal (case insensitive) to the name
188//        constraint is not matched; i.e. presented dNSName identifiers
189//        must be subdomains. Some CAs in Mozilla's CA program (e.g. HARICA)
190//        have name constraints with the leading "." in their root
191//        certificates. The name constraints imposed on DCISS by Mozilla also
192//        have the it, so supporting this is a requirement for backward
193//        compatibility, even if it is not yet standardized. So, for example, a
194//        presented ID of "www.example.com" matches a constraint of
195//        ".example.com" but a presented ID of "example.com" does not.
196//
197//     Q: Is there a way to prevent subdomain matches?
198//     A: Yes.
199//
200//        Some people have proposed that dNSName constraints that do not
201//        start with a "." should be restricted to exact (case insensitive)
202//        matches. However, such a change of semantics from what RFC5280
203//        specifies would be a non-backward-compatible change in the case of
204//        permittedSubtrees constraints, and it would be a security issue for
205//        excludedSubtrees constraints.
206//
207//        However, it can be done with a combination of permittedSubtrees and
208//        excludedSubtrees, e.g. "example.com" in permittedSubtrees and
209//        ".example.com" in excludedSubtrees.
210//
211//     Q: Are name constraints allowed to be specified as absolute names?
212//        For example, does a presented ID of "example.com" match a name
213//        constraint of "example.com." and vice versa.
214//     A: Absolute names are not supported as presented IDs or name
215//        constraints. Only reference IDs may be absolute.
216//
217//     Q: Is "" a valid dNSName constraint? If so, what does it mean?
218//     A: Yes. Any valid presented dNSName can be formed "by simply adding zero
219//        or more labels to the left-hand side" of "". In particular, an
220//        excludedSubtrees dNSName constraint of "" forbids all dNSNames.
221//
222//     Q: Is "." a valid dNSName constraint? If so, what does it mean?
223//     A: No, because absolute names are not allowed (see above).
224//
225// [0] RFC 6265 (Cookies) Domain Matching rules:
226//     http://tools.ietf.org/html/rfc6265#section-5.1.3
227// [1] NSS source code:
228//     https://mxr.mozilla.org/nss/source/lib/certdb/genname.c?rev=2a7348f013cb#1209
229// [2] Description of SChannel's behavior from Microsoft:
230//     http://www.imc.org/ietf-pkix/mail-archive/msg04668.html
231// [3] Proposal to add such support to OpenSSL:
232//     http://www.mail-archive.com/openssl-dev%40openssl.org/msg36204.html
233//     https://rt.openssl.org/Ticket/Display.html?id=3562
234// [4] Feedback on the lack of clarify in the definition that never got
235//     incorporated into the spec:
236//     https://www.ietf.org/mail-archive/web/pkix/current/msg21192.html
237pub(super) fn presented_id_matches_reference_id(
238    presented_dns_id: untrusted::Input<'_>,
239    reference_dns_id_role: IdRole,
240    reference_dns_id: untrusted::Input<'_>,
241) -> Result<bool, Error> {
242    if !is_valid_dns_id(presented_dns_id, IdRole::Presented, Wildcards::Allow) {
243        return Err(Error::MalformedDnsIdentifier);
244    }
245
246    if !is_valid_dns_id(reference_dns_id, reference_dns_id_role, Wildcards::Deny) {
247        return Err(match reference_dns_id_role {
248            IdRole::NameConstraint => Error::MalformedNameConstraint,
249            _ => Error::MalformedDnsIdentifier,
250        });
251    }
252
253    let mut presented = untrusted::Reader::new(presented_dns_id);
254    let mut reference = untrusted::Reader::new(reference_dns_id);
255
256    match reference_dns_id_role {
257        IdRole::Reference => (),
258
259        IdRole::NameConstraint if presented_dns_id.len() > reference_dns_id.len() => {
260            if reference_dns_id.is_empty() {
261                // An empty constraint matches everything.
262                return Ok(true);
263            }
264
265            // If the reference ID starts with a dot then skip the prefix of
266            // the presented ID and start the comparison at the position of
267            // that dot. Examples:
268            //
269            //                                       Matches     Doesn't Match
270            //     -----------------------------------------------------------
271            //       original presented ID:  www.example.com    badexample.com
272            //                     skipped:  www                ba
273            //     presented ID w/o prefix:     .example.com      dexample.com
274            //                reference ID:     .example.com      .example.com
275            //
276            // If the reference ID does not start with a dot then we skip
277            // the prefix of the presented ID but also verify that the
278            // prefix ends with a dot. Examples:
279            //
280            //                                       Matches     Doesn't Match
281            //     -----------------------------------------------------------
282            //       original presented ID:  www.example.com    badexample.com
283            //                     skipped:  www                ba
284            //                 must be '.':     .                 d
285            //     presented ID w/o prefix:      example.com       example.com
286            //                reference ID:      example.com       example.com
287            //
288            if reference.peek(b'.') {
289                if presented
290                    .skip(presented_dns_id.len() - reference_dns_id.len())
291                    .is_err()
292                {
293                    unreachable!();
294                }
295            } else {
296                if presented
297                    .skip(presented_dns_id.len() - reference_dns_id.len() - 1)
298                    .is_err()
299                {
300                    unreachable!();
301                }
302                if presented.read_byte() != Ok(b'.') {
303                    return Ok(false);
304                }
305            }
306        }
307
308        IdRole::NameConstraint => (),
309
310        IdRole::Presented => unreachable!(),
311    }
312
313    // Only allow wildcard labels that consist only of '*'.
314    if presented.peek(b'*') {
315        if presented.skip(1).is_err() {
316            unreachable!();
317        }
318
319        loop {
320            if reference.read_byte().is_err() {
321                return Ok(false);
322            }
323            if reference.peek(b'.') {
324                break;
325            }
326        }
327    }
328
329    loop {
330        let presented_byte = match (presented.read_byte(), reference.read_byte()) {
331            (Ok(p), Ok(r)) if ascii_lower(p) == ascii_lower(r) => p,
332            _ => {
333                return Ok(false);
334            }
335        };
336
337        if presented.at_end() {
338            // Don't allow presented IDs to be absolute.
339            if presented_byte == b'.' {
340                return Err(Error::MalformedDnsIdentifier);
341            }
342            break;
343        }
344    }
345
346    // Allow a relative presented DNS ID to match an absolute reference DNS ID,
347    // unless we're matching a name constraint.
348    if !reference.at_end() {
349        if reference_dns_id_role != IdRole::NameConstraint {
350            match reference.read_byte() {
351                Ok(b'.') => (),
352                _ => {
353                    return Ok(false);
354                }
355            };
356        }
357        if !reference.at_end() {
358            return Ok(false);
359        }
360    }
361
362    assert!(presented.at_end());
363    assert!(reference.at_end());
364
365    Ok(true)
366}
367
368#[inline]
369fn ascii_lower(b: u8) -> u8 {
370    match b {
371        b'A'..=b'Z' => b + b'a' - b'A',
372        _ => b,
373    }
374}
375
376#[derive(Clone, Copy, PartialEq)]
377enum Wildcards {
378    Deny,
379    Allow,
380}
381
382#[derive(Clone, Copy, PartialEq)]
383pub(super) enum IdRole {
384    Reference,
385    Presented,
386    NameConstraint,
387}
388
389// https://tools.ietf.org/html/rfc5280#section-4.2.1.6:
390//
391//   When the subjectAltName extension contains a domain name system
392//   label, the domain name MUST be stored in the dNSName (an IA5String).
393//   The name MUST be in the "preferred name syntax", as specified by
394//   Section 3.5 of [RFC1034] and as modified by Section 2.1 of
395//   [RFC1123].
396//
397// https://bugzilla.mozilla.org/show_bug.cgi?id=1136616: As an exception to the
398// requirement above, underscores are also allowed in names for compatibility.
399fn is_valid_dns_id(
400    hostname: untrusted::Input<'_>,
401    id_role: IdRole,
402    allow_wildcards: Wildcards,
403) -> bool {
404    // https://blogs.msdn.microsoft.com/oldnewthing/20120412-00/?p=7873/
405    if hostname.len() > 253 {
406        return false;
407    }
408
409    let mut input = untrusted::Reader::new(hostname);
410
411    if id_role == IdRole::NameConstraint && input.at_end() {
412        return true;
413    }
414
415    let mut dot_count = 0;
416    let mut label_length = 0;
417    let mut label_is_all_numeric = false;
418    let mut label_ends_with_hyphen = false;
419
420    // Only presented IDs are allowed to have wildcard labels. And, like
421    // Chromium, be stricter than RFC 6125 requires by insisting that a
422    // wildcard label consist only of '*'.
423    let is_wildcard = allow_wildcards == Wildcards::Allow && input.peek(b'*');
424    let mut is_first_byte = !is_wildcard;
425    if is_wildcard {
426        if input.read_byte() != Ok(b'*') || input.read_byte() != Ok(b'.') {
427            return false;
428        }
429        dot_count += 1;
430    }
431
432    loop {
433        const MAX_LABEL_LENGTH: usize = 63;
434
435        match input.read_byte() {
436            Ok(b'-') => {
437                if label_length == 0 {
438                    return false; // Labels must not start with a hyphen.
439                }
440                label_is_all_numeric = false;
441                label_ends_with_hyphen = true;
442                label_length += 1;
443                if label_length > MAX_LABEL_LENGTH {
444                    return false;
445                }
446            }
447
448            Ok(b'0'..=b'9') => {
449                if label_length == 0 {
450                    label_is_all_numeric = true;
451                }
452                label_ends_with_hyphen = false;
453                label_length += 1;
454                if label_length > MAX_LABEL_LENGTH {
455                    return false;
456                }
457            }
458
459            Ok(b'a'..=b'z') | Ok(b'A'..=b'Z') | Ok(b'_') => {
460                label_is_all_numeric = false;
461                label_ends_with_hyphen = false;
462                label_length += 1;
463                if label_length > MAX_LABEL_LENGTH {
464                    return false;
465                }
466            }
467
468            Ok(b'.') => {
469                dot_count += 1;
470                if label_length == 0 && (id_role != IdRole::NameConstraint || !is_first_byte) {
471                    return false;
472                }
473                if label_ends_with_hyphen {
474                    return false; // Labels must not end with a hyphen.
475                }
476                label_length = 0;
477            }
478
479            _ => {
480                return false;
481            }
482        }
483        is_first_byte = false;
484
485        if input.at_end() {
486            break;
487        }
488    }
489
490    // Only reference IDs, not presented IDs or name constraints, may be
491    // absolute.
492    if label_length == 0 && id_role != IdRole::Reference {
493        return false;
494    }
495
496    if label_ends_with_hyphen {
497        return false; // Labels must not end with a hyphen.
498    }
499
500    if label_is_all_numeric {
501        return false; // Last label must not be all numeric.
502    }
503
504    if is_wildcard {
505        // If the DNS ID ends with a dot, the last dot signifies an absolute ID.
506        let label_count = if label_length == 0 {
507            dot_count
508        } else {
509            dot_count + 1
510        };
511
512        // Like NSS, require at least two labels to follow the wildcard label.
513        // TODO: Allow the TrustDomain to control this on a per-eTLD+1 basis,
514        // similar to Chromium. Even then, it might be better to still enforce
515        // that there are at least two labels after the wildcard.
516        if label_count < 3 {
517            return false;
518        }
519    }
520
521    true
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    #[allow(clippy::type_complexity)]
529    const PRESENTED_MATCHES_REFERENCE: &[(&[u8], &[u8], Result<bool, Error>)] = &[
530        (b"", b"a", Err(Error::MalformedDnsIdentifier)),
531        (b"a", b"a", Ok(true)),
532        (b"b", b"a", Ok(false)),
533        (b"*.b.a", b"c.b.a", Ok(true)),
534        (b"*.b.a", b"b.a", Ok(false)),
535        (b"*.b.a", b"b.a.", Ok(false)),
536        // Wildcard not in leftmost label
537        (b"d.c.b.a", b"d.c.b.a", Ok(true)),
538        (b"d.*.b.a", b"d.c.b.a", Err(Error::MalformedDnsIdentifier)),
539        (b"d.c*.b.a", b"d.c.b.a", Err(Error::MalformedDnsIdentifier)),
540        (b"d.c*.b.a", b"d.cc.b.a", Err(Error::MalformedDnsIdentifier)),
541        // case sensitivity
542        (
543            b"abcdefghijklmnopqrstuvwxyz",
544            b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
545            Ok(true),
546        ),
547        (
548            b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
549            b"abcdefghijklmnopqrstuvwxyz",
550            Ok(true),
551        ),
552        (b"aBc", b"Abc", Ok(true)),
553        // digits
554        (b"a1", b"a1", Ok(true)),
555        // A trailing dot indicates an absolute name, and absolute names can match
556        // relative names, and vice-versa.
557        (b"example", b"example", Ok(true)),
558        (b"example.", b"example.", Err(Error::MalformedDnsIdentifier)),
559        (b"example", b"example.", Ok(true)),
560        (b"example.", b"example", Err(Error::MalformedDnsIdentifier)),
561        (b"example.com", b"example.com", Ok(true)),
562        (
563            b"example.com.",
564            b"example.com.",
565            Err(Error::MalformedDnsIdentifier),
566        ),
567        (b"example.com", b"example.com.", Ok(true)),
568        (
569            b"example.com.",
570            b"example.com",
571            Err(Error::MalformedDnsIdentifier),
572        ),
573        (
574            b"example.com..",
575            b"example.com.",
576            Err(Error::MalformedDnsIdentifier),
577        ),
578        (
579            b"example.com..",
580            b"example.com",
581            Err(Error::MalformedDnsIdentifier),
582        ),
583        (
584            b"example.com...",
585            b"example.com.",
586            Err(Error::MalformedDnsIdentifier),
587        ),
588        // xn-- IDN prefix
589        (b"x*.b.a", b"xa.b.a", Err(Error::MalformedDnsIdentifier)),
590        (b"x*.b.a", b"xna.b.a", Err(Error::MalformedDnsIdentifier)),
591        (b"x*.b.a", b"xn-a.b.a", Err(Error::MalformedDnsIdentifier)),
592        (b"x*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
593        (b"xn*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
594        (
595            b"xn-*.b.a",
596            b"xn--a.b.a",
597            Err(Error::MalformedDnsIdentifier),
598        ),
599        (
600            b"xn--*.b.a",
601            b"xn--a.b.a",
602            Err(Error::MalformedDnsIdentifier),
603        ),
604        (b"xn*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
605        (
606            b"xn-*.b.a",
607            b"xn--a.b.a",
608            Err(Error::MalformedDnsIdentifier),
609        ),
610        (
611            b"xn--*.b.a",
612            b"xn--a.b.a",
613            Err(Error::MalformedDnsIdentifier),
614        ),
615        (
616            b"xn---*.b.a",
617            b"xn--a.b.a",
618            Err(Error::MalformedDnsIdentifier),
619        ),
620        // "*" cannot expand to nothing.
621        (b"c*.b.a", b"c.b.a", Err(Error::MalformedDnsIdentifier)),
622        // --------------------------------------------------------------------------
623        // The rest of these are test cases adapted from Chromium's
624        // x509_certificate_unittest.cc. The parameter order is the opposite in
625        // Chromium's tests. Also, they Ok tests were modified to fit into this
626        // framework or due to intentional differences between mozilla::pkix and
627        // Chromium.
628        (b"foo.com", b"foo.com", Ok(true)),
629        (b"f", b"f", Ok(true)),
630        (b"i", b"h", Ok(false)),
631        (b"*.foo.com", b"bar.foo.com", Ok(true)),
632        (b"*.test.fr", b"www.test.fr", Ok(true)),
633        (b"*.test.FR", b"wwW.tESt.fr", Ok(true)),
634        (b".uk", b"f.uk", Err(Error::MalformedDnsIdentifier)),
635        (
636            b"?.bar.foo.com",
637            b"w.bar.foo.com",
638            Err(Error::MalformedDnsIdentifier),
639        ),
640        (
641            b"(www|ftp).foo.com",
642            b"www.foo.com",
643            Err(Error::MalformedDnsIdentifier),
644        ), // regex!
645        (
646            b"www.foo.com\0",
647            b"www.foo.com",
648            Err(Error::MalformedDnsIdentifier),
649        ),
650        (
651            b"www.foo.com\0*.foo.com",
652            b"www.foo.com",
653            Err(Error::MalformedDnsIdentifier),
654        ),
655        (b"ww.house.example", b"www.house.example", Ok(false)),
656        (b"www.test.org", b"test.org", Ok(false)),
657        (b"*.test.org", b"test.org", Ok(false)),
658        (b"*.org", b"test.org", Err(Error::MalformedDnsIdentifier)),
659        // '*' must be the only character in the wildcard label
660        (
661            b"w*.bar.foo.com",
662            b"w.bar.foo.com",
663            Err(Error::MalformedDnsIdentifier),
664        ),
665        (
666            b"ww*ww.bar.foo.com",
667            b"www.bar.foo.com",
668            Err(Error::MalformedDnsIdentifier),
669        ),
670        (
671            b"ww*ww.bar.foo.com",
672            b"wwww.bar.foo.com",
673            Err(Error::MalformedDnsIdentifier),
674        ),
675        (
676            b"w*w.bar.foo.com",
677            b"wwww.bar.foo.com",
678            Err(Error::MalformedDnsIdentifier),
679        ),
680        (
681            b"w*w.bar.foo.c0m",
682            b"wwww.bar.foo.com",
683            Err(Error::MalformedDnsIdentifier),
684        ),
685        (
686            b"wa*.bar.foo.com",
687            b"WALLY.bar.foo.com",
688            Err(Error::MalformedDnsIdentifier),
689        ),
690        (
691            b"*Ly.bar.foo.com",
692            b"wally.bar.foo.com",
693            Err(Error::MalformedDnsIdentifier),
694        ),
695        // Chromium does URL decoding of the reference ID, but we don't, and we also
696        // require that the reference ID is valid, so we can't test these two.
697        //     (b"www.foo.com", b"ww%57.foo.com", Ok(true)),
698        //     (b"www&.foo.com", b"www%26.foo.com", Ok(true)),
699        (b"*.test.de", b"www.test.co.jp", Ok(false)),
700        (
701            b"*.jp",
702            b"www.test.co.jp",
703            Err(Error::MalformedDnsIdentifier),
704        ),
705        (b"www.test.co.uk", b"www.test.co.jp", Ok(false)),
706        (
707            b"www.*.co.jp",
708            b"www.test.co.jp",
709            Err(Error::MalformedDnsIdentifier),
710        ),
711        (b"www.bar.foo.com", b"www.bar.foo.com", Ok(true)),
712        (b"*.foo.com", b"www.bar.foo.com", Ok(false)),
713        (
714            b"*.*.foo.com",
715            b"www.bar.foo.com",
716            Err(Error::MalformedDnsIdentifier),
717        ),
718        // Our matcher requires the reference ID to be a valid DNS name, so we cannot
719        // test this case.
720        //     (b"*.*.bar.foo.com", b"*..bar.foo.com", Ok(false)),
721        (b"www.bath.org", b"www.bath.org", Ok(true)),
722        // Our matcher requires the reference ID to be a valid DNS name, so we cannot
723        // test these cases.
724        // DNS_ID_MISMATCH("www.bath.org", ""),
725        //     (b"www.bath.org", b"20.30.40.50", Ok(false)),
726        //     (b"www.bath.org", b"66.77.88.99", Ok(false)),
727
728        // IDN tests
729        (
730            b"xn--poema-9qae5a.com.br",
731            b"xn--poema-9qae5a.com.br",
732            Ok(true),
733        ),
734        (
735            b"*.xn--poema-9qae5a.com.br",
736            b"www.xn--poema-9qae5a.com.br",
737            Ok(true),
738        ),
739        (
740            b"*.xn--poema-9qae5a.com.br",
741            b"xn--poema-9qae5a.com.br",
742            Ok(false),
743        ),
744        (
745            b"xn--poema-*.com.br",
746            b"xn--poema-9qae5a.com.br",
747            Err(Error::MalformedDnsIdentifier),
748        ),
749        (
750            b"xn--*-9qae5a.com.br",
751            b"xn--poema-9qae5a.com.br",
752            Err(Error::MalformedDnsIdentifier),
753        ),
754        (
755            b"*--poema-9qae5a.com.br",
756            b"xn--poema-9qae5a.com.br",
757            Err(Error::MalformedDnsIdentifier),
758        ),
759        // The following are adapted from the examples quoted from
760        //   http://tools.ietf.org/html/rfc6125#section-6.4.3
761        // (e.g., *.example.com would match foo.example.com but
762        // not bar.foo.example.com or example.com).
763        (b"*.example.com", b"foo.example.com", Ok(true)),
764        (b"*.example.com", b"bar.foo.example.com", Ok(false)),
765        (b"*.example.com", b"example.com", Ok(false)),
766        (
767            b"baz*.example.net",
768            b"baz1.example.net",
769            Err(Error::MalformedDnsIdentifier),
770        ),
771        (
772            b"*baz.example.net",
773            b"foobaz.example.net",
774            Err(Error::MalformedDnsIdentifier),
775        ),
776        (
777            b"b*z.example.net",
778            b"buzz.example.net",
779            Err(Error::MalformedDnsIdentifier),
780        ),
781        // Wildcards should not be valid for public registry controlled domains,
782        // and unknown/unrecognized domains, at least three domain components must
783        // be present. For mozilla::pkix and NSS, there must always be at least two
784        // labels after the wildcard label.
785        (b"*.test.example", b"www.test.example", Ok(true)),
786        (b"*.example.co.uk", b"test.example.co.uk", Ok(true)),
787        (
788            b"*.example",
789            b"test.example",
790            Err(Error::MalformedDnsIdentifier),
791        ),
792        // The result is different than Chromium, because Chromium takes into account
793        // the additional knowledge it has that "co.uk" is a TLD. mozilla::pkix does
794        // not know that.
795        (b"*.co.uk", b"example.co.uk", Ok(true)),
796        (b"*.com", b"foo.com", Err(Error::MalformedDnsIdentifier)),
797        (b"*.us", b"foo.us", Err(Error::MalformedDnsIdentifier)),
798        (b"*", b"foo", Err(Error::MalformedDnsIdentifier)),
799        // IDN variants of wildcards and registry controlled domains.
800        (
801            b"*.xn--poema-9qae5a.com.br",
802            b"www.xn--poema-9qae5a.com.br",
803            Ok(true),
804        ),
805        (
806            b"*.example.xn--mgbaam7a8h",
807            b"test.example.xn--mgbaam7a8h",
808            Ok(true),
809        ),
810        // RFC6126 allows this, and NSS accepts it, but Chromium disallows it.
811        // TODO: File bug against Chromium.
812        (b"*.com.br", b"xn--poema-9qae5a.com.br", Ok(true)),
813        (
814            b"*.xn--mgbaam7a8h",
815            b"example.xn--mgbaam7a8h",
816            Err(Error::MalformedDnsIdentifier),
817        ),
818        // Wildcards should be permissible for 'private' registry-controlled
819        // domains. (In mozilla::pkix, we do not know if it is a private registry-
820        // controlled domain or not.)
821        (b"*.appspot.com", b"www.appspot.com", Ok(true)),
822        (b"*.s3.amazonaws.com", b"foo.s3.amazonaws.com", Ok(true)),
823        // Multiple wildcards are not valid.
824        (
825            b"*.*.com",
826            b"foo.example.com",
827            Err(Error::MalformedDnsIdentifier),
828        ),
829        (
830            b"*.bar.*.com",
831            b"foo.bar.example.com",
832            Err(Error::MalformedDnsIdentifier),
833        ),
834        // Absolute vs relative DNS name tests. Although not explicitly specified
835        // in RFC 6125, absolute reference names (those ending in a .) should
836        // match either absolute or relative presented names.
837        // TODO: File errata against RFC 6125 about this.
838        (b"foo.com.", b"foo.com", Err(Error::MalformedDnsIdentifier)),
839        (b"foo.com", b"foo.com.", Ok(true)),
840        (b"foo.com.", b"foo.com.", Err(Error::MalformedDnsIdentifier)),
841        (b"f.", b"f", Err(Error::MalformedDnsIdentifier)),
842        (b"f", b"f.", Ok(true)),
843        (b"f.", b"f.", Err(Error::MalformedDnsIdentifier)),
844        (
845            b"*.bar.foo.com.",
846            b"www-3.bar.foo.com",
847            Err(Error::MalformedDnsIdentifier),
848        ),
849        (b"*.bar.foo.com", b"www-3.bar.foo.com.", Ok(true)),
850        (
851            b"*.bar.foo.com.",
852            b"www-3.bar.foo.com.",
853            Err(Error::MalformedDnsIdentifier),
854        ),
855        // We require the reference ID to be a valid DNS name, so we cannot test this
856        // case.
857        //     (b".", b".", Ok(false)),
858        (
859            b"*.com.",
860            b"example.com",
861            Err(Error::MalformedDnsIdentifier),
862        ),
863        (
864            b"*.com",
865            b"example.com.",
866            Err(Error::MalformedDnsIdentifier),
867        ),
868        (
869            b"*.com.",
870            b"example.com.",
871            Err(Error::MalformedDnsIdentifier),
872        ),
873        (b"*.", b"foo.", Err(Error::MalformedDnsIdentifier)),
874        (b"*.", b"foo", Err(Error::MalformedDnsIdentifier)),
875        // The result is different than Chromium because we don't know that co.uk is
876        // a TLD.
877        (
878            b"*.co.uk.",
879            b"foo.co.uk",
880            Err(Error::MalformedDnsIdentifier),
881        ),
882        (
883            b"*.co.uk.",
884            b"foo.co.uk.",
885            Err(Error::MalformedDnsIdentifier),
886        ),
887    ];
888
889    #[test]
890    fn presented_matches_reference_test() {
891        for (presented, reference, expected_result) in PRESENTED_MATCHES_REFERENCE {
892            let actual_result = presented_id_matches_reference_id(
893                untrusted::Input::from(presented),
894                IdRole::Reference,
895                untrusted::Input::from(reference),
896            );
897            assert_eq!(
898                &actual_result, expected_result,
899                "presented_id_matches_reference_id(\"{:?}\", \"{:?}\")",
900                presented, reference
901            );
902        }
903    }
904
905    // (presented_name, constraint, expected_matches)
906    #[allow(clippy::type_complexity)]
907    const PRESENTED_MATCHES_CONSTRAINT: &[(&[u8], &[u8], Result<bool, Error>)] = &[
908        // No absolute presented IDs allowed
909        (b".", b"", Err(Error::MalformedDnsIdentifier)),
910        (b"www.example.com.", b"", Err(Error::MalformedDnsIdentifier)),
911        (
912            b"www.example.com.",
913            b"www.example.com.",
914            Err(Error::MalformedDnsIdentifier),
915        ),
916        // No absolute constraints allowed
917        (
918            b"www.example.com",
919            b".",
920            Err(Error::MalformedNameConstraint),
921        ),
922        (
923            b"www.example.com",
924            b"www.example.com.",
925            Err(Error::MalformedNameConstraint),
926        ),
927        // No wildcard in constraints allowed
928        (
929            b"www.example.com",
930            b"*.example.com",
931            Err(Error::MalformedNameConstraint),
932        ),
933        // No empty presented IDs allowed
934        (b"", b"", Err(Error::MalformedDnsIdentifier)),
935        // Empty constraints match everything allowed
936        (b"example.com", b"", Ok(true)),
937        (b"*.example.com", b"", Ok(true)),
938        // Constraints that start with a dot
939        (b"www.example.com", b".example.com", Ok(true)),
940        (b"www.example.com", b".EXAMPLE.COM", Ok(true)),
941        (b"www.example.com", b".axample.com", Ok(false)),
942        (b"www.example.com", b".xample.com", Ok(false)),
943        (b"www.example.com", b".exampl.com", Ok(false)),
944        (b"badexample.com", b".example.com", Ok(false)),
945        // Constraints that do not start with a dot
946        (b"www.example.com", b"example.com", Ok(true)),
947        (b"www.example.com", b"EXAMPLE.COM", Ok(true)),
948        (b"www.example.com", b"axample.com", Ok(false)),
949        (b"www.example.com", b"xample.com", Ok(false)),
950        (b"www.example.com", b"exampl.com", Ok(false)),
951        (b"badexample.com", b"example.com", Ok(false)),
952        // Presented IDs with wildcard
953        (b"*.example.com", b".example.com", Ok(true)),
954        (b"*.example.com", b"example.com", Ok(true)),
955        (b"*.example.com", b"www.example.com", Ok(true)),
956        (b"*.example.com", b"www.EXAMPLE.COM", Ok(true)),
957        (b"*.example.com", b"www.axample.com", Ok(false)),
958        (b"*.example.com", b".xample.com", Ok(false)),
959        (b"*.example.com", b"xample.com", Ok(false)),
960        (b"*.example.com", b".exampl.com", Ok(false)),
961        (b"*.example.com", b"exampl.com", Ok(false)),
962        // Matching IDs
963        (b"www.example.com", b"www.example.com", Ok(true)),
964    ];
965
966    #[test]
967    fn presented_matches_constraint_test() {
968        for (presented, constraint, expected_result) in PRESENTED_MATCHES_CONSTRAINT {
969            let actual_result = presented_id_matches_reference_id(
970                untrusted::Input::from(presented),
971                IdRole::NameConstraint,
972                untrusted::Input::from(constraint),
973            );
974            assert_eq!(
975                &actual_result, expected_result,
976                "presented_id_matches_constraint(\"{:?}\", \"{:?}\")",
977                presented, constraint,
978            );
979        }
980    }
981}