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" )] |
16 | use alloc::format; |
17 | use core::fmt::Write; |
18 | |
19 | #[cfg (feature = "alloc" )] |
20 | use pki_types::ServerName; |
21 | use pki_types::{DnsName, InvalidDnsNameError}; |
22 | |
23 | use super::{GeneralName, NameIterator}; |
24 | use crate::cert::Cert; |
25 | use crate::error::{Error, InvalidNameContext}; |
26 | |
27 | pub(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)] |
81 | pub(crate) struct WildcardDnsNameRef<'a>(&'a [u8]); |
82 | |
83 | impl<'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 | hostname:untrusted::Input::from(dns_name), |
89 | IdRole::Reference, |
90 | allow_wildcards: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 | |
106 | impl core::fmt::Debug for WildcardDnsNameRef<'_> { |
107 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { |
108 | f.write_str(data:"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: u8 in self.0 { |
114 | f.write_char(char::from(ch).to_ascii_lowercase())?; |
115 | } |
116 | |
117 | f.write_str(data:" \")" ) |
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 |
237 | pub(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 ] |
369 | fn 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)] |
377 | enum Wildcards { |
378 | Deny, |
379 | Allow, |
380 | } |
381 | |
382 | #[derive (Clone, Copy, PartialEq)] |
383 | pub(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. |
399 | fn 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)] |
525 | mod 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 | } |
982 | |