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