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