ser_relaxed.rs (41079B)
1 #![expect( 2 clippy::pub_use, 3 clippy::question_mark_used, 4 reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" 5 )] 6 extern crate alloc; 7 use super::{ 8 ser::{ 9 AuthenticationExtensionsPrfValues, AuthenticationExtensionsPrfValuesVisitor, 10 PRF_VALUES_FIELDS, 11 }, 12 ClientDataJsonParser, CollectedClientData, Origin, SentChallenge, 13 }; 14 #[cfg(doc)] 15 use super::{Challenge, LimitedVerificationParser}; 16 use alloc::borrow::Cow; 17 use core::{ 18 fmt::{self, Formatter}, 19 marker::PhantomData, 20 }; 21 #[cfg(doc)] 22 use data_encoding::BASE64URL_NOPAD; 23 use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor}; 24 #[cfg(doc)] 25 use serde_json::de; 26 /// Category returned by [`SerdeJsonErr::classify`]. 27 #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] 28 pub use serde_json::error::Category; 29 /// Error returned by [`CollectedClientData::from_client_data_json_relaxed`] or any of the [`Deserialize`] 30 /// implementations when relying on [`de::Deserializer`] or [`de::StreamDeserializer`]. 31 #[cfg_attr(docsrs, doc(cfg(feature = "serde_relaxed")))] 32 pub use serde_json::error::Error as SerdeJsonErr; 33 /// "Relaxed" [`ClientDataJsonParser`]. 34 /// 35 /// Unlike [`LimitedVerificationParser`] which requires 36 /// [JSON-compatible serialization of client data](https://www.w3.org/TR/webauthn-3/#collectedclientdata-json-compatible-serialization-of-client-data) 37 /// to be parsed _exactly_ as required by 38 /// the [limited verification algorithm](https://www.w3.org/TR/webauthn-3/#clientdatajson-verification), 39 /// this is a "relaxed" parser. 40 /// 41 /// L1 clients predate the JSON-compatible serialization of client data; additionally there are L2 and L3 clients 42 /// that don't adhere to the JSON-compatible serialization of client data despite being required to. These clients 43 /// serialize `CollectedClientData` so that it's valid JSON and conforms to the Web IDL `dictionary` and nothing more. 44 /// Furthermore the spec requires that data be decoded in a way equivalent 45 /// to [UTF-8 decode](https://encoding.spec.whatwg.org/#utf-8-decode) which both interprets a leading zero 46 /// width no-breaking space (i.e., U+FEFF) as a byte-order mark (BOM) as well as replaces any sequences of invalid 47 /// UTF-8 code units with the replacement character (i.e., U+FFFD). That is precisely what this parser does. 48 /// 49 /// In particular the parser errors iff any of the following is true: 50 /// 51 /// * The payload is not valid JSON _after_ ignoring a leading U+FEFF and replacing any sequences of invalid 52 /// UTF-8 code units with U+FFFD. 53 /// * The JSON does not conform to the Web IDL `dictionary`. 54 /// * [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type) is not `"webauthn.create"` 55 /// or `"webauthn.get"` when `REGISTRATION` and `!REGISTRATION` respectively. 56 /// * [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) is not a 57 /// base64url-encoded [`Challenge`]. 58 /// * Existence of duplicate keys in the root object _including_ keys that otherwise would have been ignored. 59 pub(super) struct RelaxedClientDataJsonParser<const REGISTRATION: bool>; 60 impl<const R: bool> ClientDataJsonParser for RelaxedClientDataJsonParser<R> { 61 type Err = SerdeJsonErr; 62 fn parse(json: &[u8]) -> Result<CollectedClientData<'_>, Self::Err> { 63 /// U+FEFF encoded in UTF-8. 64 const BOM: [u8; 3] = [0xef, 0xbb, 0xbf]; 65 // We avoid first calling `String::from_utf8_lossy` since `CDataJsonHelper` relies on 66 // [`Deserializer::deserialize_bytes`] instead of [`Deserializer::deserialize_str`] and 67 // [`Deserializer::deserialize_identifier`]. Additionally [`CollectedClientData::origin`] and 68 // [`CollectedClientData::top_origin`] are the only fields that need to actually replace invalid 69 // UTF-8 code units, and this is achieved via the inner `OriginWrapper` type. 70 serde_json::from_slice::<CDataJsonHelper<'_, R>>(json.split_at_checked(BOM.len()).map_or( 71 json, 72 |(bom, rem)| { 73 if bom == BOM { 74 rem 75 } else { 76 json 77 } 78 }, 79 )) 80 .map(|val| val.0) 81 } 82 fn get_sent_challenge(json: &[u8]) -> Result<SentChallenge, Self::Err> { 83 /// U+FEFF encoded in UTF-8. 84 const BOM: [u8; 3] = [0xef, 0xbb, 0xbf]; 85 // We avoid first calling `String::from_utf8_lossy` since `Chall` relies on 86 // [`Deserializer::deserialize_bytes`] instead of [`Deserializer::deserialize_identifier`]. 87 serde_json::from_slice::<Chall>(json.split_at_checked(BOM.len()).map_or( 88 json, 89 |(bom, rem)| { 90 if bom == BOM { 91 rem 92 } else { 93 json 94 } 95 }, 96 )) 97 .map(|c| c.0) 98 } 99 } 100 /// Used by [`RelaxedClientDataJsonParser::get_sent_challenge`] to minimally deserialize the JSON. 101 struct Chall(SentChallenge); 102 impl<'de> Deserialize<'de> for Chall { 103 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 104 where 105 D: Deserializer<'de>, 106 { 107 /// `Visitor` for `Chall`. 108 struct ChallVisitor; 109 impl<'d> Visitor<'d> for ChallVisitor { 110 type Value = Chall; 111 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 112 formatter.write_str("Chall") 113 } 114 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> 115 where 116 A: MapAccess<'d>, 117 { 118 /// Fields in `clientDataJSON`. 119 enum Field { 120 /// `"challenge"`. 121 Challenge, 122 /// All other fields. 123 Other, 124 } 125 impl<'e> Deserialize<'e> for Field { 126 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 127 where 128 D: Deserializer<'e>, 129 { 130 /// `Visitor` for `Field`. 131 struct FieldVisitor; 132 impl Visitor<'_> for FieldVisitor { 133 type Value = Field; 134 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 135 write!(formatter, "'{CHALLENGE}'") 136 } 137 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> 138 where 139 E: Error, 140 { 141 if v == b"challenge" { 142 Ok(Field::Challenge) 143 } else { 144 Ok(Field::Other) 145 } 146 } 147 } 148 deserializer.deserialize_bytes(FieldVisitor) 149 } 150 } 151 let mut chall = None; 152 while let Some(key) = map.next_key()? { 153 match key { 154 Field::Challenge => { 155 if chall.is_some() { 156 return Err(Error::duplicate_field(CHALLENGE)); 157 } 158 chall = map.next_value().map(Some)?; 159 } 160 Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?, 161 } 162 } 163 chall 164 .ok_or_else(|| Error::missing_field(CHALLENGE)) 165 .map(Chall) 166 } 167 } 168 /// Fields we care about. 169 const FIELDS: &[&str; 1] = &[CHALLENGE]; 170 deserializer.deserialize_struct("Chall", FIELDS, ChallVisitor) 171 } 172 } 173 /// "type". 174 const TYPE: &str = "type"; 175 /// "challenge". 176 const CHALLENGE: &str = "challenge"; 177 /// "origin". 178 const ORIGIN: &str = "origin"; 179 /// "crossOrigin". 180 const CROSS_ORIGIN: &str = "crossOrigin"; 181 /// "topOrigin". 182 const TOP_ORIGIN: &str = "topOrigin"; 183 /// Fields for `CollectedClientData`. 184 const FIELDS: &[&str; 5] = &[TYPE, CHALLENGE, ORIGIN, CROSS_ORIGIN, TOP_ORIGIN]; 185 /// Helper for [`RelaxedClientDataJsonParser`]. 186 struct RelaxedHelper<'a, const REGISTRATION: bool>(PhantomData<fn() -> &'a ()>); 187 impl<'de: 'a, 'a, const R: bool> Visitor<'de> for RelaxedHelper<'a, R> { 188 type Value = CollectedClientData<'a>; 189 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 190 formatter.write_str("CollectedClientData") 191 } 192 #[expect( 193 clippy::too_many_lines, 194 reason = "don't want to move code to an outer scope" 195 )] 196 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> 197 where 198 A: MapAccess<'de>, 199 { 200 /// "webauthn.create". 201 const CREATE: &str = "webauthn.create"; 202 /// "webauthn.get". 203 const GET: &str = "webauthn.get"; 204 /// `CollectedClientData` fields. 205 enum Field { 206 /// "type" field. 207 Type, 208 /// "challenge" field. 209 Challenge, 210 /// "origin" field. 211 Origin, 212 /// "crossOrigin" field. 213 CrossOrigin, 214 /// "topOrigin" field. 215 TopOrigin, 216 /// Unknown field. 217 Other, 218 } 219 impl<'d> Deserialize<'d> for Field { 220 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 221 where 222 D: Deserializer<'d>, 223 { 224 /// `Visitor` for `Field`. 225 struct FieldVisitor; 226 impl Visitor<'_> for FieldVisitor { 227 type Value = Field; 228 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 229 write!(formatter, "'{TYPE}', '{CHALLENGE}', '{ORIGIN}', '{CROSS_ORIGIN}', or '{TOP_ORIGIN}'") 230 } 231 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> 232 where 233 E: Error, 234 { 235 match v { 236 b"type" => Ok(Field::Type), 237 b"challenge" => Ok(Field::Challenge), 238 b"origin" => Ok(Field::Origin), 239 b"crossOrigin" => Ok(Field::CrossOrigin), 240 b"topOrigin" => Ok(Field::TopOrigin), 241 _ => Ok(Field::Other), 242 } 243 } 244 } 245 // MUST NOT call `Deserializer::deserialize_identifier` since that will call 246 // `FieldVisitor::visit_str` which obviously requires decoding the field as UTF-8 first. 247 deserializer.deserialize_bytes(FieldVisitor) 248 } 249 } 250 /// Deserializes the type value. 251 /// Contains `true` iff the type is `"webauthn.create"`; otherwise the 252 /// value is `"webauthn.get"`. 253 struct Type(bool); 254 impl<'d> Deserialize<'d> for Type { 255 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 256 where 257 D: Deserializer<'d>, 258 { 259 /// `Visitor` for `Type`. 260 struct TypeVisitor; 261 impl Visitor<'_> for TypeVisitor { 262 type Value = Type; 263 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 264 write!(formatter, "'{CREATE}' or '{GET}'") 265 } 266 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> 267 where 268 E: Error, 269 { 270 match v { 271 b"webauthn.create" => Ok(Type(true)), 272 b"webauthn.get" => Ok(Type(false)), 273 _ => Err(Error::invalid_value( 274 Unexpected::Bytes(v), 275 &format!("'{CREATE}' or '{GET}'").as_str(), 276 )), 277 } 278 } 279 } 280 deserializer.deserialize_bytes(TypeVisitor) 281 } 282 } 283 /// `newtype` around `Origin` that implements [`Deserialize`] such that invalid UTF-8 code units are first 284 /// replaced with the replacement character. We don't do this for `Origin` since we want its public API 285 /// to forbid invalid UTF-8. 286 struct OriginWrapper<'d>(Origin<'d>); 287 impl<'d: 'e, 'e> Deserialize<'d> for OriginWrapper<'e> { 288 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 289 where 290 D: Deserializer<'d>, 291 { 292 /// `Visitor` for `OriginWrapper`. 293 struct OriginWrapperVisitor<'f>(PhantomData<fn() -> &'f ()>); 294 impl<'f: 'g, 'g> Visitor<'f> for OriginWrapperVisitor<'g> { 295 type Value = OriginWrapper<'g>; 296 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 297 formatter.write_str("OriginWrapper") 298 } 299 fn visit_borrowed_bytes<E>(self, v: &'f [u8]) -> Result<Self::Value, E> 300 where 301 E: Error, 302 { 303 Ok(OriginWrapper(Origin(String::from_utf8_lossy(v)))) 304 } 305 #[expect(unsafe_code, reason = "safety comment justifies its use")] 306 fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E> 307 where 308 E: Error, 309 { 310 Ok(OriginWrapper(Origin( 311 match String::from_utf8_lossy(v.as_slice()) { 312 Cow::Borrowed(_) => { 313 // SAFETY: 314 // `String::from_utf8_lossy` returns `Cow::Borrowed` iff the input was valid 315 // UTF-8. 316 let val = unsafe { String::from_utf8_unchecked(v) }; 317 Cow::Owned(val) 318 } 319 Cow::Owned(val) => Cow::Owned(val), 320 }, 321 ))) 322 } 323 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> 324 where 325 E: Error, 326 { 327 self.visit_byte_buf(v.to_owned()) 328 } 329 } 330 deserializer.deserialize_bytes(OriginWrapperVisitor(PhantomData)) 331 } 332 } 333 let mut typ = false; 334 let mut chall = None; 335 let mut orig = None; 336 let mut cross = None; 337 let mut top_orig = None; 338 while let Some(key) = map.next_key()? { 339 match key { 340 Field::Type => { 341 if typ { 342 return Err(Error::duplicate_field(TYPE)); 343 } 344 typ = map.next_value::<Type>().and_then(|v| { 345 if v.0 { 346 if R { 347 Ok(true) 348 } else { 349 Err(Error::invalid_value(Unexpected::Str(CREATE), &GET)) 350 } 351 } else if R { 352 Err(Error::invalid_value(Unexpected::Str(GET), &CREATE)) 353 } else { 354 Ok(true) 355 } 356 })?; 357 } 358 Field::Challenge => { 359 if chall.is_some() { 360 return Err(Error::duplicate_field(CHALLENGE)); 361 } 362 chall = map.next_value().map(Some)?; 363 } 364 Field::Origin => { 365 if orig.is_some() { 366 return Err(Error::duplicate_field(ORIGIN)); 367 } 368 orig = map.next_value::<OriginWrapper<'_>>().map(|o| Some(o.0))?; 369 } 370 Field::CrossOrigin => { 371 if cross.is_some() { 372 return Err(Error::duplicate_field(CROSS_ORIGIN)); 373 } 374 cross = map.next_value().map(Some)?; 375 } 376 Field::TopOrigin => { 377 if top_orig.is_some() { 378 return Err(Error::duplicate_field(TOP_ORIGIN)); 379 } 380 top_orig = map.next_value::<Option<OriginWrapper<'_>>>().map(Some)?; 381 } 382 // `IgnoredAny` ignores invalid UTF-8 in and only in JSON strings. 383 Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?, 384 } 385 } 386 if typ { 387 chall 388 .ok_or_else(|| Error::missing_field(CHALLENGE)) 389 .and_then(|challenge| { 390 orig.ok_or_else(|| Error::missing_field(ORIGIN)) 391 .map(|origin| CollectedClientData { 392 challenge, 393 origin, 394 cross_origin: cross.flatten().unwrap_or_default(), 395 top_origin: top_orig.flatten().map(|o| o.0), 396 }) 397 }) 398 } else { 399 Err(Error::missing_field(TYPE)) 400 } 401 } 402 } 403 /// `newtype` around [`CollectedClientData`] to avoid implementing [`Deserialize`] for `CollectedClientData`. 404 struct CDataJsonHelper<'a, const REGISTRATION: bool>(CollectedClientData<'a>); 405 impl<'de: 'a, 'a, const R: bool> Deserialize<'de> for CDataJsonHelper<'a, R> { 406 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 407 where 408 D: Deserializer<'de>, 409 { 410 deserializer 411 .deserialize_struct( 412 "CollectedClientData", 413 FIELDS, 414 RelaxedHelper::<R>(PhantomData), 415 ) 416 .map(Self) 417 } 418 } 419 /// `newtype` around `AuthenticationExtensionsPrfValues` with a "relaxed" [`Self::deserialize`] implementation. 420 pub(super) struct AuthenticationExtensionsPrfValuesRelaxed(pub AuthenticationExtensionsPrfValues); 421 impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfValuesRelaxed { 422 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 423 where 424 D: Deserializer<'de>, 425 { 426 deserializer 427 .deserialize_struct( 428 "AuthenticationExtensionsPrfValuesRelaxed", 429 PRF_VALUES_FIELDS, 430 AuthenticationExtensionsPrfValuesVisitor::<true>, 431 ) 432 .map(Self) 433 } 434 } 435 #[cfg(test)] 436 mod tests { 437 use super::{ClientDataJsonParser, Cow, RelaxedClientDataJsonParser}; 438 use serde::de::{Error as _, Unexpected}; 439 use serde_json::Error; 440 #[test] 441 fn relaxed_client_data_json() { 442 // Base case is correct. 443 let input = serde_json::json!({ 444 "challenge": "ABABABABABABABABABABAA", 445 "type": "webauthn.create", 446 "origin": "https://example.com", 447 "crossOrigin": true, 448 "topOrigin": "https://example.org" 449 }) 450 .to_string(); 451 assert!( 452 RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).map_or(false, |c| { 453 c.cross_origin 454 && c.challenge.0 455 == u128::from_le_bytes([ 456 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 457 ]) 458 && matches!(c.origin.0, Cow::Borrowed(o) if o == "https://example.com") 459 && c.top_origin.map_or( 460 false, 461 |t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org"), 462 ) 463 }) 464 ); 465 // Base case is correct. 466 let input = serde_json::json!({ 467 "challenge": "ABABABABABABABABABABAA", 468 "type": "webauthn.get", 469 "origin": "https://example.com", 470 "crossOrigin": true, 471 "topOrigin": "https://example.org" 472 }) 473 .to_string(); 474 assert!( 475 RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).map_or(false, |c| { 476 c.cross_origin 477 && c.challenge.0 478 == u128::from_le_bytes([ 479 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 480 ]) 481 && matches!(c.origin.0, Cow::Borrowed(o) if o == "https://example.com") 482 && c.top_origin.map_or( 483 false, 484 |t| matches!(t.0, Cow::Borrowed(o) if o == "https://example.org"), 485 ) 486 }) 487 ); 488 // Unknown keys are allowed. 489 let input = serde_json::json!({ 490 "challenge": "ABABABABABABABABABABAA", 491 "type": "webauthn.create", 492 "origin": "https://example.com", 493 "crossOrigin": true, 494 "topOrigin": "https://example.org", 495 "foo": true 496 }) 497 .to_string(); 498 assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok()); 499 // Duplicate keys are forbidden. 500 let input = "{ 501 \"challenge\": \"ABABABABABABABABABABAA\", 502 \"type\": \"webauthn.create\", 503 \"origin\": \"https://example.com\", 504 \"crossOrigin\": true, 505 \"topOrigin\": \"https://example.org\", 506 \"crossOrigin\": true 507 }"; 508 let mut err = Error::duplicate_field("crossOrigin") 509 .to_string() 510 .into_bytes(); 511 assert_eq!( 512 RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 513 .unwrap_err() 514 .to_string() 515 .into_bytes()[..err.len()], 516 err 517 ); 518 // `null` `crossOrigin`. 519 let input = serde_json::json!({ 520 "challenge": "ABABABABABABABABABABAA", 521 "type": "webauthn.create", 522 "origin": "https://example.com", 523 "crossOrigin": null, 524 "topOrigin": "https://example.org" 525 }) 526 .to_string(); 527 assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 528 .map_or(false, |c| !c.cross_origin)); 529 // Missing `crossOrigin`. 530 let input = serde_json::json!({ 531 "challenge": "ABABABABABABABABABABAA", 532 "type": "webauthn.create", 533 "origin": "https://example.com", 534 "topOrigin": "https://example.org" 535 }) 536 .to_string(); 537 assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 538 .map_or(false, |c| !c.cross_origin)); 539 // `null` `topOrigin`. 540 let input = serde_json::json!({ 541 "challenge": "ABABABABABABABABABABAA", 542 "type": "webauthn.create", 543 "origin": "https://example.com", 544 "crossOrigin": true, 545 "topOrigin": null 546 }) 547 .to_string(); 548 assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 549 .map_or(false, |c| c.top_origin.is_none())); 550 // Missing `topOrigin`. 551 let input = serde_json::json!({ 552 "challenge": "ABABABABABABABABABABAA", 553 "type": "webauthn.create", 554 "origin": "https://example.com", 555 "crossOrigin": true, 556 }) 557 .to_string(); 558 assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 559 .map_or(false, |c| c.top_origin.is_none())); 560 // `null` `challenge`. 561 err = Error::invalid_type( 562 Unexpected::Other("null"), 563 &"base64 encoding of the 16-byte challenge in a URL safe way without padding", 564 ) 565 .to_string() 566 .into_bytes(); 567 let input = serde_json::json!({ 568 "challenge": null, 569 "type": "webauthn.create", 570 "origin": "https://example.com", 571 "crossOrigin": true, 572 "topOrigin": "https://example.org" 573 }) 574 .to_string(); 575 assert_eq!( 576 RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 577 .unwrap_err() 578 .to_string() 579 .into_bytes()[..err.len()], 580 err 581 ); 582 // Missing `challenge`. 583 err = Error::missing_field("challenge").to_string().into_bytes(); 584 let input = serde_json::json!({ 585 "type": "webauthn.create", 586 "origin": "https://example.com", 587 "crossOrigin": true, 588 "topOrigin": "https://example.org" 589 }) 590 .to_string(); 591 assert_eq!( 592 RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 593 .unwrap_err() 594 .to_string() 595 .into_bytes()[..err.len()], 596 err 597 ); 598 // `null` `type`. 599 err = Error::invalid_type( 600 Unexpected::Other("null"), 601 &"'webauthn.create' or 'webauthn.get'", 602 ) 603 .to_string() 604 .into_bytes(); 605 let input = serde_json::json!({ 606 "challenge": "ABABABABABABABABABABAA", 607 "type": null, 608 "origin": "https://example.com", 609 "crossOrigin": true, 610 "topOrigin": "https://example.org" 611 }) 612 .to_string(); 613 assert_eq!( 614 RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 615 .unwrap_err() 616 .to_string() 617 .into_bytes()[..err.len()], 618 err 619 ); 620 // Missing `type`. 621 err = Error::missing_field("type").to_string().into_bytes(); 622 let input = serde_json::json!({ 623 "challenge": "ABABABABABABABABABABAA", 624 "origin": "https://example.com", 625 "crossOrigin": true, 626 "topOrigin": "https://example.org" 627 }) 628 .to_string(); 629 assert_eq!( 630 RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 631 .unwrap_err() 632 .to_string() 633 .into_bytes()[..err.len()], 634 err 635 ); 636 // `null` `origin`. 637 err = Error::invalid_type(Unexpected::Other("null"), &"OriginWrapper") 638 .to_string() 639 .into_bytes(); 640 let input = serde_json::json!({ 641 "challenge": "ABABABABABABABABABABAA", 642 "type": "webauthn.get", 643 "origin": null, 644 "crossOrigin": true, 645 "topOrigin": "https://example.org" 646 }) 647 .to_string(); 648 assert_eq!( 649 RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) 650 .unwrap_err() 651 .to_string() 652 .into_bytes()[..err.len()], 653 err 654 ); 655 // Missing `origin`. 656 err = Error::missing_field("origin").to_string().into_bytes(); 657 let input = serde_json::json!({ 658 "challenge": "ABABABABABABABABABABAA", 659 "type": "webauthn.get", 660 "crossOrigin": true, 661 "topOrigin": "https://example.org" 662 }) 663 .to_string(); 664 assert_eq!( 665 RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) 666 .unwrap_err() 667 .to_string() 668 .into_bytes()[..err.len()], 669 err 670 ); 671 // Mismatched `type`. 672 err = Error::invalid_value(Unexpected::Str("webauthn.create"), &"webauthn.get") 673 .to_string() 674 .into_bytes(); 675 let input = serde_json::json!({ 676 "challenge": "ABABABABABABABABABABAA", 677 "type": "webauthn.create", 678 "origin": "https://example.com", 679 "crossOrigin": true, 680 "topOrigin": "https://example.org" 681 }) 682 .to_string(); 683 assert_eq!( 684 RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()) 685 .unwrap_err() 686 .to_string() 687 .into_bytes()[..err.len()], 688 err 689 ); 690 // Mismatched `type`. 691 err = Error::invalid_value(Unexpected::Str("webauthn.get"), &"webauthn.create") 692 .to_string() 693 .into_bytes(); 694 let input = serde_json::json!({ 695 "challenge": "ABABABABABABABABABABAA", 696 "type": "webauthn.get", 697 "origin": "https://example.com", 698 "crossOrigin": true, 699 "topOrigin": "https://example.org" 700 }) 701 .to_string(); 702 assert_eq!( 703 RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()) 704 .unwrap_err() 705 .to_string() 706 .into_bytes()[..err.len()], 707 err 708 ); 709 // `crossOrigin` can be `false` even when `topOrigin` exists. 710 let input = serde_json::json!({ 711 "challenge": "ABABABABABABABABABABAA", 712 "type": "webauthn.get", 713 "origin": "https://example.com", 714 "crossOrigin": false, 715 "topOrigin": "https://example.org" 716 }) 717 .to_string(); 718 assert!(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).is_ok()); 719 // `crossOrigin` can be `true` even when `topOrigin` does not exist. 720 let input = serde_json::json!({ 721 "challenge": "ABABABABABABABABABABAA", 722 "type": "webauthn.get", 723 "origin": "https://example.com", 724 "crossOrigin": true, 725 }) 726 .to_string(); 727 assert!(RelaxedClientDataJsonParser::<false>::parse(input.as_bytes()).is_ok()); 728 // BOM is removed. 729 let input = "\u{feff}{ 730 \"challenge\": \"ABABABABABABABABABABAA\", 731 \"type\": \"webauthn.create\", 732 \"origin\": \"https://example.com\", 733 \"crossOrigin\": true, 734 \"topOrigin\": \"https://example.org\" 735 }"; 736 assert!(RelaxedClientDataJsonParser::<true>::parse(input.as_bytes()).is_ok()); 737 // Invalid Unicode is replaced. 738 let input = b"{ 739 \"challenge\": \"ABABABABABABABABABABAA\", 740 \"type\": \"webauthn.create\", 741 \"origin\": \"https://\xffexample.com\", 742 \"crossOrigin\": true, 743 \"topOrigin\": \"https://example.org\" 744 }"; 745 assert!( 746 RelaxedClientDataJsonParser::<true>::parse(input.as_slice()).map_or(false, |c| { 747 matches!(c.origin.0, Cow::Owned(o) if o == "https://\u{fffd}example.com") 748 }) 749 ); 750 // Escape characters are de-escaped. 751 let input = b"{ 752 \"challenge\": \"ABABABABABABABABABABAA\", 753 \"type\": \"webauthn\\u002ecreate\", 754 \"origin\": \"https://examp\\\\le.com\", 755 \"crossOrigin\": true, 756 \"topOrigin\": \"https://example.org\" 757 }"; 758 assert!( 759 RelaxedClientDataJsonParser::<true>::parse(input.as_slice()).map_or(false, |c| { 760 matches!(c.origin.0, Cow::Owned(o) if o == "https://examp\\le.com") 761 }) 762 ); 763 } 764 #[test] 765 fn relaxed_challenge() { 766 // Base case is correct. 767 let input = serde_json::json!({ 768 "challenge": "ABABABABABABABABABABAA", 769 "type": "webauthn.create", 770 "origin": "https://example.com", 771 "crossOrigin": true, 772 "topOrigin": "https://example.org" 773 }) 774 .to_string(); 775 assert!( 776 RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).map_or( 777 false, 778 |c| { 779 c.0 == u128::from_le_bytes([ 780 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 781 ]) 782 } 783 ) 784 ); 785 // Base case is correct. 786 let input = serde_json::json!({ 787 "challenge": "ABABABABABABABABABABAA", 788 "type": "webauthn.get", 789 "origin": "https://example.com", 790 "crossOrigin": true, 791 "topOrigin": "https://example.org" 792 }) 793 .to_string(); 794 assert!( 795 RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).map_or( 796 false, 797 |c| { 798 c.0 == u128::from_le_bytes([ 799 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 16, 1, 0, 800 ]) 801 } 802 ) 803 ); 804 // Unknown keys are allowed. 805 let input = serde_json::json!({ 806 "challenge": "ABABABABABABABABABABAA", 807 "type": "webauthn.create", 808 "origin": "https://example.com", 809 "crossOrigin": true, 810 "topOrigin": "https://example.org", 811 "foo": true 812 }) 813 .to_string(); 814 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 815 // Duplicate keys are ignored. 816 let input = "{ 817 \"challenge\": \"ABABABABABABABABABABAA\", 818 \"type\": \"webauthn.create\", 819 \"origin\": \"https://example.com\", 820 \"crossOrigin\": true, 821 \"topOrigin\": \"https://example.org\", 822 \"crossOrigin\": true 823 }"; 824 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 825 // `null` `crossOrigin`. 826 let input = serde_json::json!({ 827 "challenge": "ABABABABABABABABABABAA", 828 "type": "webauthn.create", 829 "origin": "https://example.com", 830 "crossOrigin": null, 831 "topOrigin": "https://example.org" 832 }) 833 .to_string(); 834 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 835 // Missing `crossOrigin`. 836 let input = serde_json::json!({ 837 "challenge": "ABABABABABABABABABABAA", 838 "type": "webauthn.create", 839 "origin": "https://example.com", 840 "topOrigin": "https://example.org" 841 }) 842 .to_string(); 843 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 844 // `null` `topOrigin`. 845 let input = serde_json::json!({ 846 "challenge": "ABABABABABABABABABABAA", 847 "type": "webauthn.create", 848 "origin": "https://example.com", 849 "crossOrigin": true, 850 "topOrigin": null 851 }) 852 .to_string(); 853 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 854 // Missing `topOrigin`. 855 let input = serde_json::json!({ 856 "challenge": "ABABABABABABABABABABAA", 857 "type": "webauthn.create", 858 "origin": "https://example.com", 859 "crossOrigin": true, 860 }) 861 .to_string(); 862 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 863 // `null` `challenge`. 864 let mut err = Error::invalid_type( 865 Unexpected::Other("null"), 866 &"base64 encoding of the 16-byte challenge in a URL safe way without padding", 867 ) 868 .to_string() 869 .into_bytes(); 870 let input = serde_json::json!({ 871 "challenge": null, 872 "type": "webauthn.create", 873 "origin": "https://example.com", 874 "crossOrigin": true, 875 "topOrigin": "https://example.org" 876 }) 877 .to_string(); 878 assert_eq!( 879 RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()) 880 .unwrap_err() 881 .to_string() 882 .into_bytes()[..err.len()], 883 err 884 ); 885 // Missing `challenge`. 886 err = Error::missing_field("challenge").to_string().into_bytes(); 887 let input = serde_json::json!({ 888 "type": "webauthn.create", 889 "origin": "https://example.com", 890 "crossOrigin": true, 891 "topOrigin": "https://example.org" 892 }) 893 .to_string(); 894 assert_eq!( 895 RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()) 896 .unwrap_err() 897 .to_string() 898 .into_bytes()[..err.len()], 899 err 900 ); 901 // `null` `type`. 902 let input = serde_json::json!({ 903 "challenge": "ABABABABABABABABABABAA", 904 "type": null, 905 "origin": "https://example.com", 906 "crossOrigin": true, 907 "topOrigin": "https://example.org" 908 }) 909 .to_string(); 910 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 911 // Missing `type`. 912 let input = serde_json::json!({ 913 "challenge": "ABABABABABABABABABABAA", 914 "origin": "https://example.com", 915 "crossOrigin": true, 916 "topOrigin": "https://example.org" 917 }) 918 .to_string(); 919 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 920 // `null` `origin`. 921 let input = serde_json::json!({ 922 "challenge": "ABABABABABABABABABABAA", 923 "type": "webauthn.get", 924 "origin": null, 925 "crossOrigin": true, 926 "topOrigin": "https://example.org" 927 }) 928 .to_string(); 929 assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); 930 // Missing `origin`. 931 let input = serde_json::json!({ 932 "challenge": "ABABABABABABABABABABAA", 933 "type": "webauthn.get", 934 "crossOrigin": true, 935 "topOrigin": "https://example.org" 936 }) 937 .to_string(); 938 assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); 939 // Mismatched `type`. 940 let input = serde_json::json!({ 941 "challenge": "ABABABABABABABABABABAA", 942 "type": "webauthn.create", 943 "origin": "https://example.com", 944 "crossOrigin": true, 945 "topOrigin": "https://example.org" 946 }) 947 .to_string(); 948 assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); 949 // Mismatched `type`. 950 let input = serde_json::json!({ 951 "challenge": "ABABABABABABABABABABAA", 952 "type": "webauthn.get", 953 "origin": "https://example.com", 954 "crossOrigin": true, 955 "topOrigin": "https://example.org" 956 }) 957 .to_string(); 958 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 959 // `crossOrigin` can be `false` even when `topOrigin` exists. 960 let input = serde_json::json!({ 961 "challenge": "ABABABABABABABABABABAA", 962 "type": "webauthn.get", 963 "origin": "https://example.com", 964 "crossOrigin": false, 965 "topOrigin": "https://example.org" 966 }) 967 .to_string(); 968 assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); 969 // `crossOrigin` can be `true` even when `topOrigin` does not exist. 970 let input = serde_json::json!({ 971 "challenge": "ABABABABABABABABABABAA", 972 "type": "webauthn.get", 973 "origin": "https://example.com", 974 "crossOrigin": true, 975 }) 976 .to_string(); 977 assert!(RelaxedClientDataJsonParser::<false>::get_sent_challenge(input.as_bytes()).is_ok()); 978 // BOM is removed. 979 let input = "\u{feff}{ 980 \"challenge\": \"ABABABABABABABABABABAA\", 981 \"type\": \"webauthn.create\", 982 \"origin\": \"https://example.com\", 983 \"crossOrigin\": true, 984 \"topOrigin\": \"https://example.org\" 985 }"; 986 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_bytes()).is_ok()); 987 // Invalid Unicode is replaced. 988 let input = b"{ 989 \"challenge\": \"ABABABABABABABABABABAA\", 990 \"type\": \"webauthn.create\", 991 \"origin\": \"https://\xffexample.com\", 992 \"crossOrigin\": true, 993 \"topOrigin\": \"https://example.org\" 994 }"; 995 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_slice()).is_ok()); 996 // Escape characters are de-escaped. 997 let input = b"{ 998 \"challenge\": \"ABABABABABABABABABABAA\", 999 \"type\": \"webauthn\\u002ecreate\", 1000 \"origin\": \"https://examp\\\\le.com\", 1001 \"crossOrigin\": true, 1002 \"topOrigin\": \"https://example.org\" 1003 }"; 1004 assert!(RelaxedClientDataJsonParser::<true>::get_sent_challenge(input.as_slice()).is_ok()); 1005 } 1006 }