webauthn_rp

WebAuthn Level 3 RP library.
git clone https://git.philomathiclife.com/repos/webauthn_rp
Log | Files | Refs | README

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 }