webauthn_rp

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

ser_relaxed.rs (41079B)


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