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}