webauthn_rp

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

ser.rs (22048B)


      1 use super::{
      2     AllowedCredential, AllowedCredentials, Credentials as _, DiscoverableAuthenticationClientState,
      3     DiscoverableCredentialRequestOptions, Extension, NonDiscoverableAuthenticationClientState,
      4     NonDiscoverableCredentialRequestOptions, PrfInput, PrfInputOwned,
      5     PublicKeyCredentialRequestOptions,
      6 };
      7 use serde::ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer};
      8 impl Serialize for PrfInputOwned {
      9     /// See [`PrfInput::serialize`]
     10     #[inline]
     11     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     12     where
     13         S: Serializer,
     14     {
     15         PrfInput {
     16             first: self.first.as_slice(),
     17             second: self.second.as_deref(),
     18         }
     19         .serialize(serializer)
     20     }
     21 }
     22 impl Serialize for AllowedCredential {
     23     /// Serializes `self` to conform with
     24     /// [`PublicKeyCredentialDescriptorJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptorjson).
     25     ///
     26     /// # Examples
     27     ///
     28     /// ```
     29     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     30     /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
     31     /// # use webauthn_rp::{
     32     /// #     request::{auth::AllowedCredential, PublicKeyCredentialDescriptor},
     33     /// #     response::{AuthTransports, CredentialId},
     34     /// # };
     35     /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
     36     /// /// from the database.
     37     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     38     /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
     39     ///     // ⋮
     40     /// #     AuthTransports::decode(32)
     41     /// }
     42     /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
     43     /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
     44     /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
     45     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     46     /// let id = CredentialId::try_from(vec![0; 16])?;
     47     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     48     /// let transports = get_transports((&id).into())?;
     49     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     50     /// assert_eq!(
     51     ///     serde_json::to_string(&AllowedCredential::from(PublicKeyCredentialDescriptor {
     52     ///         id,
     53     ///         transports
     54     ///     })).unwrap(),
     55     ///     r#"{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}"#
     56     /// );
     57     /// # Ok::<_, webauthn_rp::AggErr>(())
     58     /// ```
     59     #[inline]
     60     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     61     where
     62         S: Serializer,
     63     {
     64         self.credential.serialize(serializer)
     65     }
     66 }
     67 impl Serialize for AllowedCredentials {
     68     /// Serializes `self` to conform with
     69     /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-allowcredentials).
     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::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials},
     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     /// let mut creds = AllowedCredentials::with_capacity(1);
     95     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     96     /// creds.push(PublicKeyCredentialDescriptor { id, transports }.into());
     97     /// # #[cfg(all(feature = "bin", feature = "custom"))]
     98     /// assert_eq!(
     99     ///     serde_json::to_string(&creds).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.creds.serialize(serializer)
    110     }
    111 }
    112 /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential).
    113 struct PrfCreds<'a>(&'a AllowedCredentials);
    114 impl Serialize for PrfCreds<'_> {
    115     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    116     where
    117         S: Serializer,
    118     {
    119         serializer
    120             .serialize_map(Some(self.0.prf_count))
    121             .and_then(|mut ser| {
    122                 self.0
    123                     .creds
    124                     .iter()
    125                     .try_fold((), |(), cred| {
    126                         cred.extension.prf.as_ref().map_or(Ok(()), |input| {
    127                             ser.serialize_entry(&cred.credential.id, input)
    128                         })
    129                     })
    130                     .and_then(|()| ser.end())
    131             })
    132     }
    133 }
    134 /// [`AuthenticationExtensionsPRFInputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfinputs).
    135 struct PrfInputs<'a, 'b, 'c> {
    136     /// [`eval`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-eval).
    137     eval: Option<PrfInput<'a, 'b>>,
    138     /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential).
    139     eval_by_credential: PrfCreds<'c>,
    140 }
    141 impl Serialize for PrfInputs<'_, '_, '_> {
    142     #[expect(
    143         clippy::arithmetic_side_effects,
    144         reason = "comment explains how overflow is not possible"
    145     )]
    146     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    147     where
    148         S: Serializer,
    149     {
    150         serializer
    151             .serialize_struct(
    152                 "PrfInputs",
    153                 // The max is 1 + 1 = 2, so overflow is not an issue.
    154                 usize::from(self.eval.is_some())
    155                     + usize::from(self.eval_by_credential.0.prf_count > 0),
    156             )
    157             .and_then(|mut ser| {
    158                 self.eval
    159                     .map_or(Ok(()), |eval| ser.serialize_field("eval", &eval))
    160                     .and_then(|()| {
    161                         if self.eval_by_credential.0.prf_count == 0 {
    162                             Ok(())
    163                         } else {
    164                             ser.serialize_field("evalByCredential", &self.eval_by_credential)
    165                         }
    166                     })
    167                     .and_then(|()| ser.end())
    168             })
    169     }
    170 }
    171 /// Serializes `self` to conform with
    172 /// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson).
    173 struct ExtensionHelper<'a, 'b, 'c> {
    174     /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions).
    175     extension: &'a Extension<'b, 'c>,
    176     /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions).
    177     ///
    178     /// Some extensions contain records, so we need both this and above.
    179     allow_credentials: &'a AllowedCredentials,
    180 }
    181 impl Serialize for ExtensionHelper<'_, '_, '_> {
    182     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    183     where
    184         S: Serializer,
    185     {
    186         let ext_count =
    187             usize::from(self.extension.prf.is_some() || self.allow_credentials.prf_count > 0);
    188         serializer
    189             .serialize_struct("Extension", ext_count)
    190             .and_then(|mut ser| {
    191                 if ext_count == 0 {
    192                     Ok(())
    193                 } else {
    194                     ser.serialize_field(
    195                         "prf",
    196                         &PrfInputs {
    197                             eval: self.extension.prf.map(|prf| prf.0),
    198                             eval_by_credential: PrfCreds(self.allow_credentials),
    199                         },
    200                     )
    201                 }
    202                 .and_then(|()| ser.end())
    203             })
    204     }
    205 }
    206 /// Helper type that peforms the serialization for both [`DiscoverableAuthenticationClientState`] and
    207 /// [`NonDiscoverableAuthenticationClientState`] and
    208 struct AuthenticationClientState<'rp_id, 'prf_first, 'prf_second, 'opt, 'cred>(
    209     &'opt PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
    210     &'cred AllowedCredentials,
    211 );
    212 impl Serialize for AuthenticationClientState<'_, '_, '_, '_, '_> {
    213     #[inline]
    214     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    215     where
    216         S: Serializer,
    217     {
    218         serializer
    219             .serialize_struct("PublicKeyCredentialRequestOptions", 7)
    220             .and_then(|mut ser| {
    221                 ser.serialize_field("challenge", &self.0.challenge)
    222                     .and_then(|()| {
    223                         ser.serialize_field("timeout", &self.0.timeout)
    224                             .and_then(|()| {
    225                                 ser.serialize_field("rpId", &self.0.rp_id).and_then(|()| {
    226                                     ser.serialize_field("allowCredentials", &self.1).and_then(
    227                                         |()| {
    228                                             ser.serialize_field(
    229                                                 "userVerification",
    230                                                 &self.0.user_verification,
    231                                             )
    232                                             .and_then(
    233                                                 |()| {
    234                                                     ser.serialize_field("hints", &self.0.hints)
    235                                                         .and_then(|()| {
    236                                                             ser.serialize_field(
    237                                                                 "extensions",
    238                                                                 &ExtensionHelper {
    239                                                                     extension: &self.0.extensions,
    240                                                                     allow_credentials: self.1,
    241                                                                 },
    242                                                             )
    243                                                             .and_then(|()| ser.end())
    244                                                         })
    245                                                 },
    246                                             )
    247                                         },
    248                                     )
    249                                 })
    250                             })
    251                     })
    252             })
    253     }
    254 }
    255 /// `"mediation"`.
    256 const MEDIATION: &str = "mediation";
    257 /// `"publicKey"`.
    258 const PUBLIC_KEY: &str = "publicKey";
    259 impl Serialize for DiscoverableCredentialRequestOptions<'_, '_, '_> {
    260     /// Serializes `self` to conform with
    261     /// [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions).
    262     ///
    263     /// Note [`signal`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-signal)
    264     /// is not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry)
    265     /// is serialized to conform to
    266     /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
    267     #[inline]
    268     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    269     where
    270         S: Serializer,
    271     {
    272         serializer
    273             .serialize_struct("DiscoverableCredentialRequestOptions", 2)
    274             .and_then(|mut ser| {
    275                 ser.serialize_field(MEDIATION, &self.mediation)
    276                     .and_then(|()| {
    277                         ser.serialize_field(
    278                             PUBLIC_KEY,
    279                             &AuthenticationClientState(
    280                                 &self.public_key,
    281                                 &AllowedCredentials::with_capacity(0),
    282                             ),
    283                         )
    284                         .and_then(|()| ser.end())
    285                     })
    286             })
    287     }
    288 }
    289 impl Serialize for NonDiscoverableCredentialRequestOptions<'_, '_, '_> {
    290     /// Serializes `self` to conform with
    291     /// [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions).
    292     ///
    293     /// Note [`signal`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-signal)
    294     /// is not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry)
    295     /// is serialized to conform to
    296     /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
    297     #[inline]
    298     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    299     where
    300         S: Serializer,
    301     {
    302         serializer
    303             .serialize_struct("NonDiscoverableCredentialRequestOptions", 2)
    304             .and_then(|mut ser| {
    305                 ser.serialize_field(MEDIATION, &self.mediation)
    306                     .and_then(|()| {
    307                         ser.serialize_field(
    308                             PUBLIC_KEY,
    309                             &AuthenticationClientState(&self.options, &self.allow_credentials),
    310                         )
    311                         .and_then(|()| ser.end())
    312                     })
    313             })
    314     }
    315 }
    316 impl Serialize for DiscoverableAuthenticationClientState<'_, '_, '_> {
    317     /// Serializes `self` according to [`DiscoverableCredentialRequestOptions::serialize`].
    318     ///
    319     /// # Examples
    320     ///
    321     /// ```
    322     /// # use webauthn_rp::{
    323     /// #     request::{
    324     /// #         auth::{
    325     /// #             AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension,
    326     /// #             PrfInputOwned, DiscoverableCredentialRequestOptions
    327     /// #         },
    328     /// #         AsciiDomain, ExtensionReq, Hint, PrfInput, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
    329     /// #     },
    330     /// #     response::{AuthTransports, CredentialId},
    331     /// # };
    332     /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
    333     /// let mut options = DiscoverableCredentialRequestOptions::passkey(&rp_id);
    334     /// options.public_key.hints = Hint::SecurityKey;
    335     /// options.public_key.extensions = Extension {
    336     ///     prf: Some((PrfInput {
    337     ///         first: [0; 4].as_slice(),
    338     ///         second: None,
    339     ///     }, ExtensionReq::Require)),
    340     /// };
    341     /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap();
    342     /// let json = serde_json::json!({
    343     ///     "mediation":"optional",
    344     ///     "publicKey":{
    345     ///         "challenge":"AAAAAAAAAAAAAAAAAAAAAA",
    346     ///         "timeout":300000,
    347     ///         "rpId":"example.com",
    348     ///         "allowCredentials":[],
    349     ///         "userVerification":"required",
    350     ///         "hints":[
    351     ///             "security-key"
    352     ///         ],
    353     ///         "extensions":{
    354     ///             "prf":{
    355     ///                 "eval":{
    356     ///                     "first":"AAAAAA"
    357     ///                 },
    358     ///             }
    359     ///         }
    360     ///     }
    361     /// }).to_string();
    362     /// // Since `Challenge`s are randomly generated, we don't know what it will be; thus
    363     /// // we test the JSON string for everything except it.
    364     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    365     /// assert_eq!(client_state.get(..50), json.get(..50));
    366     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    367     /// assert_eq!(client_state.get(72..), json.get(72..));
    368     /// # Ok::<_, webauthn_rp::AggErr>(())
    369     /// ```
    370     #[inline]
    371     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    372     where
    373         S: Serializer,
    374     {
    375         self.0.serialize(serializer)
    376     }
    377 }
    378 impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_, '_> {
    379     /// Serializes `self` according to [`NonDiscoverableCredentialRequestOptions::serialize`].
    380     ///
    381     /// # Examples
    382     ///
    383     /// ```
    384     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    385     /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr};
    386     /// # use webauthn_rp::{
    387     /// #     request::{
    388     /// #         auth::{
    389     /// #             AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension,
    390     /// #             PrfInputOwned, NonDiscoverableCredentialRequestOptions
    391     /// #         },
    392     /// #         AsciiDomain, ExtensionReq, Hint, PrfInput, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement,
    393     /// #     },
    394     /// #     response::{AuthTransports, CredentialId},
    395     /// # };
    396     /// /// Retrieves the `AuthTransports` associated with the unique `cred_id`
    397     /// /// from the database.
    398     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    399     /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> {
    400     ///     // ⋮
    401     /// #     AuthTransports::decode(32)
    402     /// }
    403     /// let mut creds = AllowedCredentials::with_capacity(1);
    404     /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is
    405     /// // likely never needed since the `CredentialId` was originally sent from the client and is likely
    406     /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`.
    407     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    408     /// let id = CredentialId::try_from(vec![0; 16])?;
    409     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    410     /// let transports = get_transports((&id).into())?;
    411     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    412     /// creds.push(AllowedCredential {
    413     ///     credential: PublicKeyCredentialDescriptor { id, transports },
    414     ///     extension: CredentialSpecificExtension {
    415     ///         prf: Some(PrfInputOwned {
    416     ///             first: vec![2; 6],
    417     ///             second: Some(vec![3; 2]),
    418     ///             ext_req: ExtensionReq::Require,
    419     ///         }),
    420     ///     },
    421     /// });
    422     /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?);
    423     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    424     /// let mut options = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?;
    425     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    426     /// let opts = options.options();
    427     /// # #[cfg(not(all(feature = "bin", feature = "custom")))]
    428     /// # let mut opts = webauthn_rp::DiscoverableCredentialRequestOptions::passkey(&rp_id).public_key;
    429     /// opts.hints = Hint::SecurityKey;
    430     /// // This is actually useless since `CredentialSpecificExtension` takes priority
    431     /// // when the client receives the payload. We set it for illustration purposes only.
    432     /// // If `creds` contained an `AllowedCredential` that didn't set
    433     /// // `CredentialSpecificExtension::prf`, then this would be used for it.
    434     /// opts.extensions = Extension {
    435     ///     prf: Some((PrfInput {
    436     ///         first: [0; 4].as_slice(),
    437     ///         second: None,
    438     ///     }, ExtensionReq::Require)),
    439     /// };
    440     /// // Since we are requesting the PRF extension, we must require user verification; otherwise
    441     /// // `NonDiscoverableCredentialRequestOptions::start_ceremony` would error.
    442     /// opts.user_verification = UserVerificationRequirement::Required;
    443     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    444     /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap();
    445     /// let json = serde_json::json!({
    446     ///     "mediation":"optional",
    447     ///     "publicKey":{
    448     ///         "challenge":"AAAAAAAAAAAAAAAAAAAAAA",
    449     ///         "timeout":300000,
    450     ///         "rpId":"example.com",
    451     ///         "allowCredentials":[
    452     ///             {
    453     ///                 "type":"public-key",
    454     ///                 "id":"AAAAAAAAAAAAAAAAAAAAAA",
    455     ///                 "transports":["usb"]
    456     ///             }
    457     ///         ],
    458     ///         "userVerification":"required",
    459     ///         "hints":[
    460     ///             "security-key"
    461     ///         ],
    462     ///         "extensions":{
    463     ///             "prf":{
    464     ///                 "eval":{
    465     ///                     "first":"AAAAAA"
    466     ///                 },
    467     ///                 "evalByCredential":{
    468     ///                     "AAAAAAAAAAAAAAAAAAAAAA":{
    469     ///                         "first":"AgICAgIC",
    470     ///                         "second":"AwM"
    471     ///                     }
    472     ///                 }
    473     ///             }
    474     ///         }
    475     ///     }
    476     /// }).to_string();
    477     /// // Since `Challenge`s are randomly generated, we don't know what it will be; thus
    478     /// // we test the JSON string for everything except it.
    479     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    480     /// assert_eq!(client_state.get(..50), json.get(..50));
    481     /// # #[cfg(all(feature = "bin", feature = "custom"))]
    482     /// assert_eq!(client_state.get(72..), json.get(72..));
    483     /// # Ok::<_, webauthn_rp::AggErr>(())
    484     /// ```
    485     #[inline]
    486     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    487     where
    488         S: Serializer,
    489     {
    490         self.0.serialize(serializer)
    491     }
    492 }