webauthn_rp

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

ser_relaxed.rs (17987B)


      1 extern crate alloc;
      2 #[cfg(test)]
      3 mod tests;
      4 #[cfg(doc)]
      5 use super::{Challenge, LimitedVerificationParser};
      6 use super::{
      7     ClientDataJsonParser, CollectedClientData, Origin, SentChallenge,
      8     ser::{
      9         AuthenticationExtensionsPrfValues, AuthenticationExtensionsPrfValuesVisitor,
     10         PRF_VALUES_FIELDS,
     11     },
     12 };
     13 use alloc::borrow::Cow;
     14 use core::{
     15     fmt::{self, Formatter},
     16     marker::PhantomData,
     17 };
     18 use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor};
     19 #[cfg(doc)]
     20 use serde_json::de;
     21 /// Category returned by [`SerdeJsonErr::classify`].
     22 pub use serde_json::error::Category;
     23 /// Error returned by [`CollectedClientData::from_client_data_json_relaxed`] or any of the [`Deserialize`]
     24 /// implementations when relying on [`de::Deserializer`] or [`de::StreamDeserializer`].
     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                     fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
    294                     where
    295                         E: Error,
    296                     {
    297                         Ok(OriginWrapper(Origin(Cow::Owned(
    298                             String::from_utf8_lossy(v.as_slice()).into_owned(),
    299                         ))))
    300                     }
    301                     fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
    302                     where
    303                         E: Error,
    304                     {
    305                         self.visit_byte_buf(v.to_owned())
    306                     }
    307                 }
    308                 deserializer.deserialize_bytes(OriginWrapperVisitor(PhantomData))
    309             }
    310         }
    311         let mut typ = false;
    312         let mut chall = None;
    313         let mut orig = None;
    314         let mut cross = None;
    315         let mut top_orig = None;
    316         while let Some(key) = map.next_key()? {
    317             match key {
    318                 Field::Type => {
    319                     if typ {
    320                         return Err(Error::duplicate_field(TYPE));
    321                     }
    322                     typ = map.next_value::<Type>().and_then(|v| {
    323                         if v.0 {
    324                             if R {
    325                                 Ok(true)
    326                             } else {
    327                                 Err(Error::invalid_value(Unexpected::Str(CREATE), &GET))
    328                             }
    329                         } else if R {
    330                             Err(Error::invalid_value(Unexpected::Str(GET), &CREATE))
    331                         } else {
    332                             Ok(true)
    333                         }
    334                     })?;
    335                 }
    336                 Field::Challenge => {
    337                     if chall.is_some() {
    338                         return Err(Error::duplicate_field(CHALLENGE));
    339                     }
    340                     chall = map.next_value().map(Some)?;
    341                 }
    342                 Field::Origin => {
    343                     if orig.is_some() {
    344                         return Err(Error::duplicate_field(ORIGIN));
    345                     }
    346                     orig = map.next_value::<OriginWrapper<'_>>().map(|o| Some(o.0))?;
    347                 }
    348                 Field::CrossOrigin => {
    349                     if cross.is_some() {
    350                         return Err(Error::duplicate_field(CROSS_ORIGIN));
    351                     }
    352                     cross = map.next_value().map(Some)?;
    353                 }
    354                 Field::TopOrigin => {
    355                     if top_orig.is_some() {
    356                         return Err(Error::duplicate_field(TOP_ORIGIN));
    357                     }
    358                     top_orig = map.next_value::<Option<OriginWrapper<'_>>>().map(Some)?;
    359                 }
    360                 // `IgnoredAny` ignores invalid UTF-8 in and only in JSON strings.
    361                 Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
    362             }
    363         }
    364         if typ {
    365             chall
    366                 .ok_or_else(|| Error::missing_field(CHALLENGE))
    367                 .and_then(|challenge| {
    368                     orig.ok_or_else(|| Error::missing_field(ORIGIN))
    369                         .map(|origin| CollectedClientData {
    370                             challenge,
    371                             origin,
    372                             cross_origin: cross.flatten().unwrap_or_default(),
    373                             top_origin: top_orig.flatten().map(|o| o.0),
    374                         })
    375                 })
    376         } else {
    377             Err(Error::missing_field(TYPE))
    378         }
    379     }
    380 }
    381 /// `newtype` around [`CollectedClientData`] to avoid implementing [`Deserialize`] for `CollectedClientData`.
    382 struct CDataJsonHelper<'a, const REGISTRATION: bool>(CollectedClientData<'a>);
    383 impl<'de: 'a, 'a, const R: bool> Deserialize<'de> for CDataJsonHelper<'a, R> {
    384     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    385     where
    386         D: Deserializer<'de>,
    387     {
    388         deserializer
    389             .deserialize_struct(
    390                 "CollectedClientData",
    391                 FIELDS,
    392                 RelaxedHelper::<R>(PhantomData),
    393             )
    394             .map(Self)
    395     }
    396 }
    397 /// `newtype` around `AuthenticationExtensionsPrfValues` with a "relaxed" [`Self::deserialize`] implementation.
    398 pub(super) struct AuthenticationExtensionsPrfValuesRelaxed(pub AuthenticationExtensionsPrfValues);
    399 impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfValuesRelaxed {
    400     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    401     where
    402         D: Deserializer<'de>,
    403     {
    404         deserializer
    405             .deserialize_struct(
    406                 "AuthenticationExtensionsPrfValuesRelaxed",
    407                 PRF_VALUES_FIELDS,
    408                 AuthenticationExtensionsPrfValuesVisitor::<true>,
    409             )
    410             .map(Self)
    411     }
    412 }