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