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