| 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 | |