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