webauthn_rp

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

ser.rs (17587B)


      1 use super::{
      2     super::super::BASE64URL_NOPAD_ENC, AllowedCredential, AllowedCredentials,
      3     AuthenticationClientState, Extension, PrfInput, PrfInputOwned,
      4 };
      5 use serde::ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer};
      6 impl Serialize for PrfInput<'_> {
      7     /// Serializes `self` to conform with
      8     /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues).
      9     ///
     10     /// # Examples
     11     ///
     12     /// ```
     13     /// # use webauthn_rp::request::{auth::PrfInput, ExtensionReq};
     14     /// assert_eq!(
     15     ///     serde_json::to_string(&PrfInput {
     16     ///         first: [0; 4].as_slice(),
     17     ///         second: Some([2; 1].as_slice()),
     18     ///         ext_info: ExtensionReq::Require
     19     ///     })?,
     20     ///     r#"{"first":"AAAAAA","second":"Ag"}"#
     21     /// );
     22     /// # Ok::<_, serde_json::Error>(())
     23     /// ```
     24     #[expect(
     25         clippy::arithmetic_side_effects,
     26         reason = "comment justifies how overflow is not possible"
     27     )]
     28     #[inline]
     29     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     30     where
     31         S: Serializer,
     32     {
     33         serializer
     34             // The max value is 1 + 1 = 2, so overflow is not an issue.
     35             .serialize_struct("PrfInput", 1 + usize::from(self.second.is_some()))
     36             .and_then(|mut ser| {
     37                 ser.serialize_field("first", BASE64URL_NOPAD_ENC.encode(self.first).as_str())
     38                     .and_then(|()| {
     39                         self.second
     40                             .as_ref()
     41                             .map_or(Ok(()), |second| {
     42                                 ser.serialize_field(
     43                                     "second",
     44                                     BASE64URL_NOPAD_ENC.encode(second).as_str(),
     45                                 )
     46                             })
     47                             .and_then(|()| ser.end())
     48                     })
     49             })
     50     }
     51 }
     52 impl Serialize for PrfInputOwned {
     53     /// See [`PrfInput::serialize`]
     54     #[inline]
     55     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     56     where
     57         S: Serializer,
     58     {
     59         PrfInput {
     60             first: self.first.as_slice(),
     61             second: self.second.as_deref(),
     62             ext_info: self.ext_info,
     63         }
     64         .serialize(serializer)
     65     }
     66 }
     67 impl Serialize for AllowedCredential {
     68     /// Serializes `self` to conform with
     69     /// [`PublicKeyCredentialDescriptorJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptorjson).
     70     ///
     71     /// # Examples
     72     ///
     73     /// ```
     74     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     75     /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
     76     /// # use webauthn_rp::{
     77     /// #     request::{auth::AllowedCredential, PublicKeyCredentialDescriptor},
     78     /// #     response::{AuthTransports, CredentialId},
     79     /// # };
     80     /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
     81     /// /// from the database.
     82     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     83     /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
     84     ///     // ⋮
     85     /// #     AuthTransports::decode(32)
     86     /// }
     87     /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
     88     /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
     89     /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
     90     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     91     /// let id = CredentialId::try_from(vec![0; 16])?;
     92     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     93     /// let transports = get_transports((&id).into())?;
     94     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     95     /// assert_eq!(
     96     ///     serde_json::to_string(&AllowedCredential::from(PublicKeyCredentialDescriptor {
     97     ///         id,
     98     ///         transports
     99     ///     })).unwrap(),
    100     ///     r#"{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}"#
    101     /// );
    102     /// # Ok::<_, webauthn_rp::AggErr>(())
    103     /// ```
    104     #[inline]
    105     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    106     where
    107         S: Serializer,
    108     {
    109         self.credential.serialize(serializer)
    110     }
    111 }
    112 impl Serialize for AllowedCredentials {
    113     /// Serializes `self` to conform with
    114     /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-allowcredentials).
    115     ///
    116     /// # Examples
    117     ///
    118     /// ```
    119     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    120     /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
    121     /// # use webauthn_rp::{
    122     /// #     request::{auth::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials},
    123     /// #     response::{AuthTransports, CredentialId},
    124     /// # };
    125     /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
    126     /// /// from the database.
    127     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    128     /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
    129     ///     // ⋮
    130     /// #     AuthTransports::decode(32)
    131     /// }
    132     /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
    133     /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
    134     /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
    135     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    136     /// let id = CredentialId::try_from(vec![0; 16])?;
    137     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    138     /// let transports = get_transports((&id).into())?;
    139     /// let mut creds = AllowedCredentials::with_capacity(1);
    140     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    141     /// creds.push(PublicKeyCredentialDescriptor { id, transports }.into());
    142     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    143     /// assert_eq!(
    144     ///     serde_json::to_string(&creds).unwrap(),
    145     ///     r#"[{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}]"#
    146     /// );
    147     /// # Ok::<_, webauthn_rp::AggErr>(())
    148     /// ```
    149     #[inline]
    150     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    151     where
    152         S: Serializer,
    153     {
    154         self.creds.serialize(serializer)
    155     }
    156 }
    157 /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential).
    158 struct PrfCreds<'a>(&'a AllowedCredentials);
    159 impl Serialize for PrfCreds<'_> {
    160     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    161     where
    162         S: Serializer,
    163     {
    164         serializer
    165             .serialize_map(Some(self.0.prf_count))
    166             .and_then(|mut ser| {
    167                 self.0
    168                     .creds
    169                     .iter()
    170                     .try_fold((), |(), cred| {
    171                         cred.extension.prf.as_ref().map_or(Ok(()), |input| {
    172                             ser.serialize_entry(&cred.credential.id, input)
    173                         })
    174                     })
    175                     .and_then(|()| ser.end())
    176             })
    177     }
    178 }
    179 /// [`AuthenticationExtensionsPRFInputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfinputs).
    180 struct PrfInputs<'a, 'b> {
    181     /// [`eval`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-eval).
    182     eval: Option<PrfInput<'a>>,
    183     /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential).
    184     eval_by_credential: PrfCreds<'b>,
    185 }
    186 impl Serialize for PrfInputs<'_, '_> {
    187     #[expect(
    188         clippy::arithmetic_side_effects,
    189         reason = "comment explains how overflow is not possible"
    190     )]
    191     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    192     where
    193         S: Serializer,
    194     {
    195         serializer
    196             .serialize_struct(
    197                 "PrfInputs",
    198                 // The max is 1 + 1 = 2, so overflow is not an issue.
    199                 usize::from(self.eval.is_some())
    200                     + usize::from(self.eval_by_credential.0.prf_count > 0),
    201             )
    202             .and_then(|mut ser| {
    203                 self.eval
    204                     .map_or(Ok(()), |eval| ser.serialize_field("eval", &eval))
    205                     .and_then(|()| {
    206                         if self.eval_by_credential.0.prf_count == 0 {
    207                             Ok(())
    208                         } else {
    209                             ser.serialize_field("evalByCredential", &self.eval_by_credential)
    210                         }
    211                     })
    212                     .and_then(|()| ser.end())
    213             })
    214     }
    215 }
    216 /// Serializes `self` to conform with
    217 /// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson).
    218 struct ExtensionHelper<'a, 'b> {
    219     /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions).
    220     extension: &'a Extension<'b>,
    221     /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions).
    222     ///
    223     /// Some extensions contain records, so we need both this and above.
    224     allow_credentials: &'a AllowedCredentials,
    225 }
    226 impl Serialize for ExtensionHelper<'_, '_> {
    227     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    228     where
    229         S: Serializer,
    230     {
    231         let ext_count =
    232             usize::from(self.extension.prf.is_some() || self.allow_credentials.prf_count > 0);
    233         serializer
    234             .serialize_struct("Extension", ext_count)
    235             .and_then(|mut ser| {
    236                 if ext_count == 0 {
    237                     Ok(())
    238                 } else {
    239                     ser.serialize_field(
    240                         "prf",
    241                         &PrfInputs {
    242                             eval: self.extension.prf,
    243                             eval_by_credential: PrfCreds(self.allow_credentials),
    244                         },
    245                     )
    246                 }
    247                 .and_then(|()| ser.end())
    248             })
    249     }
    250 }
    251 impl Serialize for AuthenticationClientState<'_, '_> {
    252     /// Serializes `self` to conform with
    253     /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
    254     ///
    255     /// # Examples
    256     ///
    257     /// ```
    258     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    259     /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
    260     /// # use webauthn_rp::{
    261     /// #     request::{
    262     /// #         auth::{
    263     /// #             AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension,
    264     /// #             PrfInput, PrfInputOwned, PublicKeyCredentialRequestOptions
    265     /// #         },
    266     /// #         AsciiDomain, ExtensionReq, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
    267     /// #     },
    268     /// #     response::{AuthTransports, CredentialId},
    269     /// # };
    270     /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
    271     /// /// from the database.
    272     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    273     /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
    274     ///     // ⋮
    275     /// #     AuthTransports::decode(32)
    276     /// }
    277     /// let mut creds = AllowedCredentials::with_capacity(1);
    278     /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
    279     /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
    280     /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
    281     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    282     /// let id = CredentialId::try_from(vec![0; 16])?;
    283     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    284     /// let transports = get_transports((&id).into())?;
    285     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    286     /// creds.push(AllowedCredential {
    287     ///     credential: PublicKeyCredentialDescriptor { id, transports },
    288     ///     extension: CredentialSpecificExtension {
    289     ///         prf: Some(PrfInputOwned {
    290     ///             first: vec![2; 6],
    291     ///             second: Some(vec![3; 2]),
    292     ///             ext_info: ExtensionReq::Require,
    293     ///         }),
    294     ///     },
    295     /// });
    296     /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
    297     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    298     /// let mut options = PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?;
    299     /// # #[cfg(not(all(feature = "bin", feature = "custom")))]
    300     /// # let mut options = PublicKeyCredentialRequestOptions::passkey(&rp_id);
    301     /// options.hints = Hint::SecurityKey;
    302     /// // This is actually useless since `CredentialSpecificExtension` takes priority
    303     /// // when the client receives the payload. We set it for illustration purposes only.
    304     /// // If `creds` contained an `AllowedCredential` that didn't set
    305     /// // `CredentialSpecificExtension::prf`, then this would be used for it.
    306     /// options.extensions = Extension {
    307     ///     prf: Some(PrfInput {
    308     ///         first: [0; 4].as_slice(),
    309     ///         second: None,
    310     ///         ext_info: ExtensionReq::Require,
    311     ///     }),
    312     /// };
    313     /// // Since we are requesting the PRF extension, we must require user verification; otherwise
    314     /// // `PublicKeyCredentialRequestOptions::start_ceremony` would error.
    315     /// options.user_verification = UserVerificationRequirement::Required;
    316     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    317     /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap();
    318     /// let json = serde_json::json!({
    319     ///     "challenge":"AAAAAAAAAAAAAAAAAAAAAA",
    320     ///     "timeout":300000,
    321     ///     "rpId":"example.com",
    322     ///     "allowCredentials":[
    323     ///         {
    324     ///             "type":"public-key",
    325     ///             "id":"AAAAAAAAAAAAAAAAAAAAAA",
    326     ///             "transports":["usb"]
    327     ///         }
    328     ///     ],
    329     ///     "userVerification":"required",
    330     ///     "hints":[
    331     ///         "security-key"
    332     ///     ],
    333     ///     "extensions":{
    334     ///         "prf":{
    335     ///             "eval":{
    336     ///                 "first":"AAAAAA"
    337     ///             },
    338     ///             "evalByCredential":{
    339     ///                 "AAAAAAAAAAAAAAAAAAAAAA":{
    340     ///                     "first":"AgICAgIC",
    341     ///                     "second":"AwM"
    342     ///                 }
    343     ///             }
    344     ///         }
    345     ///     }
    346     /// }).to_string();
    347     /// // Since `Challenge`s are randomly generated, we don't know what it will be; thus
    348     /// // we test the JSON string for everything except it.
    349     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    350     /// assert_eq!(client_state.get(..14), json.get(..14));
    351     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    352     /// assert_eq!(client_state.get(36..), json.get(36..));
    353     /// # Ok::<_, webauthn_rp::AggErr>(())
    354     /// ```
    355     #[inline]
    356     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    357     where
    358         S: Serializer,
    359     {
    360         serializer
    361             .serialize_struct("AuthenticationClientState", 9)
    362             .and_then(|mut ser| {
    363                 ser.serialize_field("challenge", &self.0.challenge)
    364                     .and_then(|()| {
    365                         ser.serialize_field("timeout", &self.0.timeout)
    366                             .and_then(|()| {
    367                                 ser.serialize_field("rpId", &self.0.rp_id).and_then(|()| {
    368                                     ser.serialize_field(
    369                                         "allowCredentials",
    370                                         &self.0.allow_credentials,
    371                                     )
    372                                     .and_then(|()| {
    373                                         ser.serialize_field(
    374                                             "userVerification",
    375                                             &self.0.user_verification,
    376                                         )
    377                                         .and_then(|()| {
    378                                             ser.serialize_field("hints", &self.0.hints).and_then(
    379                                                 |()| {
    380                                                     ser.serialize_field(
    381                                                         "extensions",
    382                                                         &ExtensionHelper {
    383                                                             extension: &self.0.extensions,
    384                                                             allow_credentials: &self
    385                                                                 .0
    386                                                                 .allow_credentials,
    387                                                         },
    388                                                     )
    389                                                     .and_then(|()| ser.end())
    390                                                 },
    391                                             )
    392                                         })
    393                                     })
    394                                 })
    395                             })
    396                     })
    397             })
    398     }
    399 }