webauthn_rp

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

ser_relaxed.rs (21625B)


      1 #[cfg(test)]
      2 mod tests;
      3 #[cfg(doc)]
      4 use super::super::{super::request::register::CoseAlgorithmIdentifier, Challenge, CredentialId};
      5 use super::{
      6     super::{
      7         register::ser::{
      8             AUTH_ATTEST_FIELDS, AttObj, AuthenticatorAttestationVisitor,
      9             ClientExtensionsOutputsVisitor, EXT_FIELDS,
     10         },
     11         ser::{
     12             AuthenticationExtensionsPrfOutputsHelper, Base64DecodedVal, ClientExtensions,
     13             PublicKeyCredential, Type,
     14         },
     15         ser_relaxed::AuthenticationExtensionsPrfValuesRelaxed,
     16     },
     17     AttestationObject, AuthenticationExtensionsPrfOutputs, AuthenticatorAttachment,
     18     AuthenticatorAttestation, ClientExtensionsOutputs, CredentialPropertiesOutput, Registration,
     19     ser::{AuthAttest, CredentialPropertiesOutputVisitor, PROPS_FIELDS},
     20 };
     21 use core::{
     22     fmt::{self, Formatter},
     23     marker::PhantomData,
     24 };
     25 use serde::de::{Deserialize, Deserializer, Error, MapAccess, Unexpected, Visitor};
     26 /// `newtype` around `CredentialPropertiesOutput` with a "relaxed" [`Self::deserialize`] implementation.
     27 #[derive(Clone, Copy, Debug)]
     28 pub struct CredentialPropertiesOutputRelaxed(pub CredentialPropertiesOutput);
     29 impl From<CredentialPropertiesOutputRelaxed> for CredentialPropertiesOutput {
     30     #[inline]
     31     fn from(value: CredentialPropertiesOutputRelaxed) -> Self {
     32         value.0
     33     }
     34 }
     35 impl<'de> Deserialize<'de> for CredentialPropertiesOutputRelaxed {
     36     /// Same as [`CredentialPropertiesOutput::deserialize`] except unknown keys are ignored.
     37     ///
     38     /// Note that duplicate keys are still forbidden.
     39     #[inline]
     40     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     41     where
     42         D: Deserializer<'de>,
     43     {
     44         deserializer
     45             .deserialize_struct(
     46                 "CredentialPropertiesOutputRelaxed",
     47                 PROPS_FIELDS,
     48                 CredentialPropertiesOutputVisitor::<true>,
     49             )
     50             .map(Self)
     51     }
     52 }
     53 /// `newtype` around `AuthenticationExtensionsPrfOutputs` with a "relaxed" [`Self::deserialize`] implementation.
     54 #[derive(Clone, Copy, Debug)]
     55 pub struct AuthenticationExtensionsPrfOutputsRelaxed(AuthenticationExtensionsPrfOutputs);
     56 impl From<AuthenticationExtensionsPrfOutputsRelaxed> for AuthenticationExtensionsPrfOutputs {
     57     #[inline]
     58     fn from(value: AuthenticationExtensionsPrfOutputsRelaxed) -> Self {
     59         value.0
     60     }
     61 }
     62 impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfOutputsRelaxed {
     63     /// Same as [`AuthenticationExtensionsPrfOutputs::deserialize`] except unknown keys are ignored.
     64     ///
     65     /// Note that duplicate keys are still forbidden;
     66     /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled) must still exist
     67     /// (and not be `null`); and
     68     /// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) must not exist,
     69     /// be `null`, or be an
     70     /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues)
     71     /// such that unknown keys are ignored, duplicate keys are forbidden,
     72     /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) is not required but
     73     /// if it exists it must be `null`, and
     74     /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) can exist but
     75     /// must be `null` if so.
     76     #[inline]
     77     #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
     78     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     79     where
     80         D: Deserializer<'de>,
     81     {
     82         AuthenticationExtensionsPrfOutputsHelper::<
     83             true,
     84             true,
     85             AuthenticationExtensionsPrfValuesRelaxed,
     86         >::deserialize(deserializer)
     87         .map(|v| {
     88             Self(AuthenticationExtensionsPrfOutputs {
     89                 enabled: v.0.unwrap_or_else(|| {
     90                     unreachable!(
     91                         "there is a bug in AuthenticationExtensionsPrfOutputsHelper::deserialize"
     92                     )
     93                 }),
     94             })
     95         })
     96     }
     97 }
     98 /// `newtype` around `ClientExtensionsOutputs` with a "relaxed" [`Self::deserialize`] implementation.
     99 #[derive(Clone, Copy, Debug)]
    100 pub struct ClientExtensionsOutputsRelaxed(pub ClientExtensionsOutputs);
    101 impl ClientExtensions for ClientExtensionsOutputsRelaxed {
    102     fn empty() -> Self {
    103         Self(ClientExtensionsOutputs::empty())
    104     }
    105 }
    106 impl<'de> Deserialize<'de> for ClientExtensionsOutputsRelaxed {
    107     /// Same as [`ClientExtensionsOutputs::deserialize`] except unknown keys are ignored,
    108     /// [`credProps`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-credprops) is
    109     /// `null` or deserialized via [`CredentialPropertiesOutputRelaxed::deserialize`], and
    110     /// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf) is
    111     /// `null` or deserialized via [`AuthenticationExtensionsPrfOutputsRelaxed::deserialize`].
    112     ///
    113     /// Note that duplicate keys are still forbidden.
    114     #[inline]
    115     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    116     where
    117         D: Deserializer<'de>,
    118     {
    119         deserializer
    120             .deserialize_struct(
    121                 "ClientExtensionsOutputsRelaxed",
    122                 EXT_FIELDS,
    123                 ClientExtensionsOutputsVisitor::<
    124                     true,
    125                     CredentialPropertiesOutputRelaxed,
    126                     AuthenticationExtensionsPrfOutputsRelaxed,
    127                 >(PhantomData),
    128             )
    129             .map(Self)
    130     }
    131 }
    132 /// `newtype` around `AuthAttest` with a "relaxed" [`Self::deserialize`] implementation.
    133 struct AuthAttestRelaxed(pub AuthAttest);
    134 impl<'de> Deserialize<'de> for AuthAttestRelaxed {
    135     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    136     where
    137         D: Deserializer<'de>,
    138     {
    139         deserializer
    140             .deserialize_struct(
    141                 "AuthenticatorAttestation",
    142                 AUTH_ATTEST_FIELDS,
    143                 AuthenticatorAttestationVisitor::<true>,
    144             )
    145             .map(Self)
    146     }
    147 }
    148 /// `newtype` around `AuthenticatorAttestation` with a "relaxed" [`Self::deserialize`] implementation.
    149 #[derive(Debug)]
    150 pub struct AuthenticatorAttestationRelaxed(pub AuthenticatorAttestation);
    151 impl<'de> Deserialize<'de> for AuthenticatorAttestationRelaxed {
    152     /// Same as [`AuthenticatorAttestation::deserialize`] except unknown keys are ignored and only
    153     /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-clientdatajson)
    154     /// and
    155     /// [`attestationObject`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-attestationobject)
    156     /// are required (and must not be `null`). For the other fields, they are allowed to not exist or be `null`.
    157     ///
    158     /// Note that duplicate keys are still forbidden, and data matching still applies when applicable.
    159     #[inline]
    160     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    161     where
    162         D: Deserializer<'de>,
    163     {
    164         AuthAttestRelaxed::deserialize(deserializer).map(|v| Self(v.0.attest))
    165     }
    166 }
    167 /// `newtype` around `Registration` with a "relaxed" [`Self::deserialize`] implementation.
    168 #[derive(Debug)]
    169 pub struct RegistrationRelaxed(pub Registration);
    170 impl<'de> Deserialize<'de> for RegistrationRelaxed {
    171     /// Same as [`Registration::deserialize`] except unknown keys are ignored,
    172     /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-response) is deserialized
    173     /// via [`AuthenticatorAttestationRelaxed::deserialize`],
    174     /// [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-clientextensionresults)
    175     /// is `null` or deserialized via [`ClientExtensionsOutputsRelaxed::deserialize`], and only `response` is required.
    176     /// `id`, `rawId`, and `type` are allowed to not exist. For the other fields, they are allowed to not exist or
    177     /// be `null`.
    178     ///
    179     /// Note that duplicate keys are still forbidden, and data matching still applies when applicable.
    180     #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
    181     #[inline]
    182     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    183     where
    184         D: Deserializer<'de>,
    185     {
    186         PublicKeyCredential::<true, true, AuthAttestRelaxed, ClientExtensionsOutputsRelaxed>::deserialize(deserializer).and_then(|cred| {
    187             cred.id.map_or_else(|| Ok(()), |id| {
    188                 cred.response.0.cred_info.map_or_else(
    189                     || AttestationObject::try_from(cred.response.0.attest.attestation_object()).map_err(Error::custom).and_then(|att_obj| {
    190                         if id.as_ref() == att_obj.auth_data.attested_credential_data.credential_id.as_ref() {
    191                             Ok(())
    192                         } else {
    193                             Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", att_obj.auth_data.attested_credential_data.credential_id.0).as_str()))
    194                         }
    195                     }),
    196                     // `start` and `last` were calculated based on `cred.response.attest.attestation_object()`
    197                     // and represent the starting and ending index of the `CredentialId`; therefore this is correct
    198                     // let alone won't `panic`.
    199                     |(start, last)| if *id.0 == cred.response.0.attest.attestation_object()[start..last] {
    200                         Ok(())
    201                     } else {
    202                         Err(Error::invalid_value(Unexpected::Bytes(id.as_ref()), &format!("id, rawId, and the credential id in the attested credential data to all match: {:?}", &cred.response.0.attest.attestation_object()[start..last]).as_str()))
    203                     }
    204                 )
    205             }).map(|()| {
    206                 Self(Registration { response: cred.response.0.attest, authenticator_attachment: cred.authenticator_attachment, client_extension_results: cred.client_extension_results.0 })
    207             })
    208         })
    209     }
    210 }
    211 /// `newtype` around `Registration` with a custom [`Self::deserialize`] implementation.
    212 #[derive(Debug)]
    213 pub struct CustomRegistration(pub Registration);
    214 impl<'de> Deserialize<'de> for CustomRegistration {
    215     /// Despite the spec having a
    216     /// [pre-defined format](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson) that clients
    217     /// can follow, the downside is the superfluous data it contains.
    218     ///
    219     /// There simply is no reason to send the [`CredentialId`] _four_ times. This redundant data puts RPs in
    220     /// a position where they either ignore the data or parse the data to ensure no contradictions exist
    221     /// (e.g., [FIDO conformance requires one to verify `id` and `rawId` exist and match](https://github.com/w3c/webauthn/issues/2119#issuecomment-2287875401)).
    222     ///
    223     /// While [`Registration::deserialize`] _strictly_ adheres to the JSON definition (e.g., it requires `publicKey`
    224     /// to exist and match with what is in both `authenticatorData` and `attestationObject` when the underlying
    225     /// algorithm is not [`CoseAlgorithmIdentifier::Es384`]), this implementation
    226     /// strictly disallows superfluous data. Specifically the following JSON is required to be sent where duplicate
    227     /// and unknown keys are disallowed:
    228     ///
    229     /// ```json
    230     /// {
    231     ///   "attestationObject": <base64url string>,
    232     ///   "authenticatorAttachment": null | "platform" | "cross-platform",
    233     ///   "clientDataJSON": <base64url string>,
    234     ///   "clientExtensionResults": <see ClientExtensionsOutputs::deserialize>,
    235     ///   "transports": <see AuthTransports::deserialize>,
    236     ///   "type": "public-key"
    237     /// }
    238     /// ```
    239     ///
    240     /// All of the above keys are required with the exceptions of `"authenticatorAttachment"` and `"type"`.
    241     ///
    242     /// # Examples
    243     ///
    244     /// ```
    245     /// # use webauthn_rp::response::register::ser_relaxed::CustomRegistration;
    246     /// assert!(
    247     ///     // The below payload is technically valid, but `RegistrationServerState::verify` will fail
    248     ///     // since the attestationObject is not valid. This is true for `Registration::deserialize`
    249     ///     // as well since attestationObject parsing is always deferred.
    250     ///     serde_json::from_str::<CustomRegistration>(
    251     ///         r#"{
    252     ///             "transports": ["usb"],
    253     ///             "attestationObject": "AA",
    254     ///             "authenticatorAttachment": "cross-platform",
    255     ///             "clientExtensionResults": {},
    256     ///             "clientDataJSON": "AA",
    257     ///             "type": "public-key"
    258     ///         }"#
    259     ///     ).is_ok());
    260     /// ```
    261     #[expect(
    262         clippy::too_many_lines,
    263         reason = "want to hide; thus don't want to put in an outer scope"
    264     )]
    265     #[inline]
    266     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    267     where
    268         D: Deserializer<'de>,
    269     {
    270         /// `Visitor` for `CustomRegistration`.
    271         struct CustomRegistrationVisitor;
    272         impl<'d> Visitor<'d> for CustomRegistrationVisitor {
    273             type Value = CustomRegistration;
    274             fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    275                 formatter.write_str("CustomRegistration")
    276             }
    277             #[expect(
    278                 clippy::too_many_lines,
    279                 reason = "want to hide; thus don't want to put in an outer scope"
    280             )]
    281             fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    282             where
    283                 A: MapAccess<'d>,
    284             {
    285                 /// Fields in the JSON.
    286                 enum Field {
    287                     /// `attestationObject` key.
    288                     AttestationObject,
    289                     /// `authenticatorAttachment` key.
    290                     AuthenticatorAttachment,
    291                     /// `clientDataJSON` key.
    292                     ClientDataJson,
    293                     /// `clientExtensionResults` key.
    294                     ClientExtensionResults,
    295                     /// `transports` key.
    296                     Transports,
    297                     /// `type` key.
    298                     Type,
    299                 }
    300                 impl<'e> Deserialize<'e> for Field {
    301                     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    302                     where
    303                         D: Deserializer<'e>,
    304                     {
    305                         /// `Visitor` for `Field`.
    306                         struct FieldVisitor;
    307                         impl Visitor<'_> for FieldVisitor {
    308                             type Value = Field;
    309                             fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    310                                 write!(
    311                                     formatter,
    312                                     "'{ATTESTATION_OBJECT}', '{AUTHENTICATOR_ATTACHMENT}', '{CLIENT_DATA_JSON}', '{CLIENT_EXTENSION_RESULTS}', '{TRANSPORTS}', or '{TYPE}'"
    313                                 )
    314                             }
    315                             fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    316                             where
    317                                 E: Error,
    318                             {
    319                                 match v {
    320                                     ATTESTATION_OBJECT => Ok(Field::AttestationObject),
    321                                     AUTHENTICATOR_ATTACHMENT => Ok(Field::AuthenticatorAttachment),
    322                                     CLIENT_DATA_JSON => Ok(Field::ClientDataJson),
    323                                     CLIENT_EXTENSION_RESULTS => Ok(Field::ClientExtensionResults),
    324                                     TRANSPORTS => Ok(Field::Transports),
    325                                     TYPE => Ok(Field::Type),
    326                                     _ => Err(E::unknown_field(v, FIELDS)),
    327                                 }
    328                             }
    329                         }
    330                         deserializer.deserialize_identifier(FieldVisitor)
    331                     }
    332                 }
    333                 let mut attestation_object = None;
    334                 let mut authenticator_attachment = None;
    335                 let mut client_data_json = None;
    336                 let mut ext = None;
    337                 let mut transports = None;
    338                 let mut typ = false;
    339                 while let Some(key) = map.next_key()? {
    340                     match key {
    341                         Field::AttestationObject => {
    342                             if attestation_object.is_some() {
    343                                 return Err(Error::duplicate_field(ATTESTATION_OBJECT));
    344                             }
    345                             attestation_object =
    346                                 map.next_value::<AttObj>().map(|val| Some(val.0))?;
    347                         }
    348                         Field::AuthenticatorAttachment => {
    349                             if authenticator_attachment.is_some() {
    350                                 return Err(Error::duplicate_field(AUTHENTICATOR_ATTACHMENT));
    351                             }
    352                             authenticator_attachment = map.next_value::<Option<_>>().map(Some)?;
    353                         }
    354                         Field::ClientDataJson => {
    355                             if client_data_json.is_some() {
    356                                 return Err(Error::duplicate_field(CLIENT_DATA_JSON));
    357                             }
    358                             client_data_json = map
    359                                 .next_value::<Base64DecodedVal>()
    360                                 .map(|val| Some(val.0))?;
    361                         }
    362                         Field::ClientExtensionResults => {
    363                             if ext.is_some() {
    364                                 return Err(Error::duplicate_field(CLIENT_EXTENSION_RESULTS));
    365                             }
    366                             ext = map.next_value().map(Some)?;
    367                         }
    368                         Field::Transports => {
    369                             if transports.is_some() {
    370                                 return Err(Error::duplicate_field(TRANSPORTS));
    371                             }
    372                             transports = map.next_value().map(Some)?;
    373                         }
    374                         Field::Type => {
    375                             if typ {
    376                                 return Err(Error::duplicate_field(TYPE));
    377                             }
    378                             typ = map.next_value::<Type>().map(|_| true)?;
    379                         }
    380                     }
    381                 }
    382                 attestation_object
    383                     .ok_or_else(|| Error::missing_field(ATTESTATION_OBJECT))
    384                     .and_then(|att_obj| {
    385                         client_data_json
    386                             .ok_or_else(|| Error::missing_field(CLIENT_DATA_JSON))
    387                             .and_then(|c_data| {
    388                                 ext.ok_or_else(|| Error::missing_field(CLIENT_EXTENSION_RESULTS))
    389                                     .and_then(|client_extension_results| {
    390                                         transports
    391                                             .ok_or_else(|| Error::missing_field(TRANSPORTS))
    392                                             .map(|trans| {
    393                                                 CustomRegistration(Registration {
    394                                                     response: AuthenticatorAttestation::new(
    395                                                         c_data, att_obj, trans,
    396                                                     ),
    397                                                     authenticator_attachment:
    398                                                         authenticator_attachment.map_or(
    399                                                             AuthenticatorAttachment::None,
    400                                                             |auth_attach| {
    401                                                                 auth_attach.unwrap_or(
    402                                                                     AuthenticatorAttachment::None,
    403                                                                 )
    404                                                             },
    405                                                         ),
    406                                                     client_extension_results,
    407                                                 })
    408                                             })
    409                                     })
    410                             })
    411                     })
    412             }
    413         }
    414         /// `attestationObject` key.
    415         const ATTESTATION_OBJECT: &str = "attestationObject";
    416         /// `authenticatorAttachment` key.
    417         const AUTHENTICATOR_ATTACHMENT: &str = "authenticatorAttachment";
    418         /// `clientDataJSON` key.
    419         const CLIENT_DATA_JSON: &str = "clientDataJSON";
    420         /// `clientExtensionResults` key.
    421         const CLIENT_EXTENSION_RESULTS: &str = "clientExtensionResults";
    422         /// `transports` key.
    423         const TRANSPORTS: &str = "transports";
    424         /// `type` key.
    425         const TYPE: &str = "type";
    426         /// Fields.
    427         const FIELDS: &[&str; 6] = &[
    428             ATTESTATION_OBJECT,
    429             AUTHENTICATOR_ATTACHMENT,
    430             CLIENT_DATA_JSON,
    431             CLIENT_EXTENSION_RESULTS,
    432             TRANSPORTS,
    433             TYPE,
    434         ];
    435         deserializer.deserialize_struct("CustomRegistration", FIELDS, CustomRegistrationVisitor)
    436     }
    437 }