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