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 | use core::fmt::Write; |
16 | |
17 | use pki_types::{DnsName, InvalidDnsNameError}; |
18 | |
19 | use super::verify::{GeneralName, NameIterator}; |
20 | use crate::Error; |
21 | |
22 | pub(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)] |
61 | pub(crate) struct WildcardDnsNameRef<'a>(&'a [u8]); |
62 | |
63 | impl<'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 | |
86 | impl 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 |
217 | pub(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 ] |
349 | fn 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)] |
357 | enum Wildcards { |
358 | Deny, |
359 | Allow, |
360 | } |
361 | |
362 | #[derive (Clone, Copy, PartialEq)] |
363 | pub(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. |
379 | fn 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)] |
505 | mod 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 | |