webpki/
name.rs

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