webauthn_rp

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

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 }