webauthn_rp

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

ser.rs (46030B)


      1 #[cfg(test)]
      2 mod tests;
      3 use super::{
      4     super::{
      5         super::request::register::CoseAlgorithmIdentifier,
      6         ser::{
      7             AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues,
      8             Base64DecodedVal, ClientExtensions, PublicKeyCredential,
      9         },
     10     },
     11     AttestationObject, AttestedCredentialData, AuthTransports, AuthenticationExtensionsPrfOutputs,
     12     AuthenticatorAttestation, ClientExtensionsOutputs, CredentialPropertiesOutput, FromCbor as _,
     13     Registration, UncompressedPubKey,
     14 };
     15 #[cfg(doc)]
     16 use super::{AuthenticatorAttachment, CredentialId};
     17 use core::{
     18     fmt::{self, Formatter},
     19     marker::PhantomData,
     20     str,
     21 };
     22 use rsa::sha2::{Sha256, digest::OutputSizeUser as _};
     23 use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor};
     24 /// Functionality for deserializing DER-encoded `SubjectPublicKeyInfo` _without_ making copies of data or
     25 /// verifying the key is valid. This exists purely to ensure that the public key we receive in JSON is the same as
     26 /// the public key in the attestation object.
     27 mod spki;
     28 /// Helper type returned from [`AuthenticatorAttestationVisitor::visit_map`].
     29 ///
     30 /// The purpose of this type is to hopefully avoid re-parsing the raw attestation object multiple times. In
     31 /// particular [`Registration`] and [`super::ser_relaxed::RegistrationRelaxed`] will attempt to validate `id` is the
     32 /// same as the [`CredentialId`] within the attestation object.
     33 pub(super) struct AuthAttest {
     34     /// The data we care about.
     35     pub attest: AuthenticatorAttestation,
     36     /// [`CredentialId`] information. This is `None` iff `authenticatorData`, `publicKey`, and
     37     /// `publicKeyAlgorithm` do not exist and we are performing a `RELAXED` parsing. When `Some`, the first
     38     /// `usize` is the starting index of `CredentialId` within the attestation object; and the second `usize` is
     39     /// 1 past the last index of `CredentialId`.
     40     pub cred_info: Option<(usize, usize)>,
     41 }
     42 /// Fields in `AuthenticatorAttestationResponseJSON`.
     43 enum AttestField<const IGNORE_UNKNOWN: bool> {
     44     /// `clientDataJSON`.
     45     ClientDataJson,
     46     /// `attestationObject`.
     47     AttestationObject,
     48     /// `authenticatorData`.
     49     AuthenticatorData,
     50     /// `transports`.
     51     Transports,
     52     /// `publicKey`.
     53     PublicKey,
     54     /// `publicKeyAlgorithm`.
     55     PublicKeyAlgorithm,
     56     /// Unknown fields.
     57     Other,
     58 }
     59 impl<'e, const I: bool> Deserialize<'e> for AttestField<I> {
     60     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     61     where
     62         D: Deserializer<'e>,
     63     {
     64         /// `Visitor` for `AttestField`.
     65         struct AttestFieldVisitor<const IGNORE_UNKNOWN: bool>;
     66         impl<const IG: bool> Visitor<'_> for AttestFieldVisitor<IG> {
     67             type Value = AttestField<IG>;
     68             fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
     69                 write!(
     70                     formatter,
     71                     "'{CLIENT_DATA_JSON}', '{ATTESTATION_OBJECT}', '{AUTHENTICATOR_DATA}', '{TRANSPORTS}', '{PUBLIC_KEY}', or '{PUBLIC_KEY_ALGORITHM}'"
     72                 )
     73             }
     74             fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
     75             where
     76                 E: Error,
     77             {
     78                 match v {
     79                     CLIENT_DATA_JSON => Ok(AttestField::ClientDataJson),
     80                     ATTESTATION_OBJECT => Ok(AttestField::AttestationObject),
     81                     AUTHENTICATOR_DATA => Ok(AttestField::AuthenticatorData),
     82                     TRANSPORTS => Ok(AttestField::Transports),
     83                     PUBLIC_KEY => Ok(AttestField::PublicKey),
     84                     PUBLIC_KEY_ALGORITHM => Ok(AttestField::PublicKeyAlgorithm),
     85                     _ => {
     86                         if IG {
     87                             Ok(AttestField::Other)
     88                         } else {
     89                             Err(E::unknown_field(v, AUTH_ATTEST_FIELDS))
     90                         }
     91                     }
     92                 }
     93             }
     94         }
     95         deserializer.deserialize_identifier(AttestFieldVisitor::<I>)
     96     }
     97 }
     98 /// Attestation object. We use this instead of `Base64DecodedVal` since we want to manually
     99 /// allocate the `Vec` in order to avoid re-allocation. Internally `AuthenticatorAttestation::new`
    100 /// appends the SHA-256 hash to the passed attestation object `Vec` to avoid temporarily allocating
    101 /// a `Vec` that contains the attestation object and hash for signature verification. Calling code
    102 /// can avoid any reallocation that would occur when the capacity is not large enough by ensuring the
    103 /// passed `Vec` has at least 32 bytes of available capacity.
    104 pub(super) struct AttObj(pub Vec<u8>);
    105 impl<'e> Deserialize<'e> for AttObj {
    106     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    107     where
    108         D: Deserializer<'e>,
    109     {
    110         /// `Visitor` for `AttObj`.
    111         struct AttObjVisitor;
    112         impl Visitor<'_> for AttObjVisitor {
    113             type Value = AttObj;
    114             fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    115                 formatter.write_str("base64url-encoded attestation object")
    116             }
    117             #[expect(
    118                 clippy::arithmetic_side_effects,
    119                 reason = "comment justifies their correctness"
    120             )]
    121             fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    122             where
    123                 E: Error,
    124             {
    125                 base64url_nopad::decode_len(v.len())
    126                     .ok_or_else(|| E::invalid_value(Unexpected::Str(v), &"base64url-encoded value"))
    127                     .and_then(|len| {
    128                         // The decoded length is 3/4 of the encoded length, so overflow could only occur
    129                         // if usize::MAX / 4 < 32 => usize::MAX < 128 < u8::MAX; thus overflow is not
    130                         // possible. We add 32 since the SHA-256 hash of `clientDataJSON` will be added to
    131                         // the raw attestation object by `AuthenticatorAttestation::new`.
    132                         let mut att_obj = vec![0; len + Sha256::output_size()];
    133                         att_obj.truncate(len);
    134                         base64url_nopad::decode_buffer_exact(v.as_bytes(), &mut att_obj)
    135                             .map_err(E::custom)
    136                             .map(|()| AttObj(att_obj))
    137                     })
    138             }
    139         }
    140         deserializer.deserialize_str(AttObjVisitor)
    141     }
    142 }
    143 /// `Visitor` for `AuthenticatorAttestation`.
    144 ///
    145 /// Unknown fields are ignored and only `clientDataJSON` and `attestationObject` are required iff `RELAXED`.
    146 pub(super) struct AuthenticatorAttestationVisitor<const RELAXED: bool>;
    147 impl<'d, const R: bool> Visitor<'d> for AuthenticatorAttestationVisitor<R> {
    148     type Value = AuthAttest;
    149     fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    150         formatter.write_str("AuthenticatorAttestation")
    151     }
    152     #[expect(clippy::too_many_lines, reason = "find it easier to reason about")]
    153     #[expect(
    154         clippy::arithmetic_side_effects,
    155         clippy::indexing_slicing,
    156         reason = "comments justify their correctness"
    157     )]
    158     fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    159     where
    160         A: MapAccess<'d>,
    161     {
    162         use spki::SubjectPublicKeyInfo as _;
    163         let mut client_data = None;
    164         let mut attest = None;
    165         let mut auth = None;
    166         let mut pub_key = None;
    167         let mut key_alg = None;
    168         let mut trans = None;
    169         while let Some(key) = map.next_key::<AttestField<R>>()? {
    170             match key {
    171                 AttestField::ClientDataJson => {
    172                     if client_data.is_some() {
    173                         return Err(Error::duplicate_field(CLIENT_DATA_JSON));
    174                     }
    175                     client_data = map
    176                         .next_value::<Base64DecodedVal>()
    177                         .map(|c_data| Some(c_data.0))?;
    178                 }
    179                 AttestField::AttestationObject => {
    180                     if attest.is_some() {
    181                         return Err(Error::duplicate_field(ATTESTATION_OBJECT));
    182                     }
    183                     attest = map.next_value::<AttObj>().map(|att_obj| Some(att_obj.0))?;
    184                 }
    185                 AttestField::AuthenticatorData => {
    186                     if auth.is_some() {
    187                         return Err(Error::duplicate_field(AUTHENTICATOR_DATA));
    188                     }
    189                     auth = map.next_value::<Option<Base64DecodedVal>>().map(Some)?;
    190                 }
    191                 AttestField::Transports => {
    192                     if trans.is_some() {
    193                         return Err(Error::duplicate_field(TRANSPORTS));
    194                     }
    195                     trans = map.next_value::<Option<_>>().map(Some)?;
    196                 }
    197                 AttestField::PublicKey => {
    198                     if pub_key.is_some() {
    199                         return Err(Error::duplicate_field(PUBLIC_KEY));
    200                     }
    201                     pub_key = map.next_value::<Option<Base64DecodedVal>>().map(Some)?;
    202                 }
    203                 AttestField::PublicKeyAlgorithm => {
    204                     if key_alg.is_some() {
    205                         return Err(Error::duplicate_field(PUBLIC_KEY_ALGORITHM));
    206                     }
    207                     key_alg = map
    208                         .next_value::<Option<CoseAlgorithmIdentifier>>()
    209                         .map(Some)?;
    210                 }
    211                 AttestField::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
    212             }
    213         }
    214         // Note the order of this matters from a performance perspective. In particular `auth` must be evaluated
    215         // before `pub_key` which must be evaluated before `key_alg` as this allows us to parse the attestation
    216         // object at most once and allow us to prioritize parsing `authenticatorData` over the attestation object.
    217         client_data.ok_or_else(|| Error::missing_field(CLIENT_DATA_JSON)).and_then(|client_data_json| attest.ok_or_else(|| Error::missing_field(ATTESTATION_OBJECT)).and_then(|attestation_object| {
    218             trans.ok_or(false).and_then(|opt_trans| opt_trans.ok_or(true)).or_else(
    219                 |flag| {
    220                     if R {
    221                         Ok(AuthTransports::new())
    222                     } else if flag {
    223                         Err(Error::invalid_type(Unexpected::Other("null"), &format!("{TRANSPORTS} to be a sequence of AuthenticatorTransports").as_str()))
    224                     } else {
    225                         Err(Error::missing_field(TRANSPORTS))
    226                     }
    227                 },
    228             ).and_then(|transports| {
    229                 auth.ok_or(false).and_then(|opt_auth| opt_auth.ok_or(true)).as_ref().map_or_else(
    230                     |flag| {
    231                         if R {
    232                             Ok(None)
    233                         } else if *flag {
    234                             Err(Error::invalid_type(Unexpected::Other("null"), &format!("{AUTHENTICATOR_DATA} to be a base64url-encoded AuthenticatorData").as_str()))
    235                         } else {
    236                             Err(Error::missing_field(AUTHENTICATOR_DATA))
    237                         }
    238                     },
    239                     |a_data| {
    240                         if a_data.0.len() > 37 {
    241                             // The last portion of attestation object is always authenticator data.
    242                             attestation_object.len().checked_sub(a_data.0.len()).ok_or_else(|| Error::invalid_value(Unexpected::Bytes(a_data.0.as_slice()), &format!("authenticator data to match the authenticator data portion of attestation object: {attestation_object:?}").as_str())).and_then(|idx| {
    243                                 // Indexing is fine; otherwise the above check would have returned `None`.
    244                                 if *a_data.0 == attestation_object[idx..] {
    245                                     // We know `a_data.len() > 37`; thus indexing is fine.
    246                                     // We start at 37 since that is the beginning of `attestedCredentialData`.
    247                                     // Recall the first 32 bytes are `rpIdHash`, then a 1 byte `flags`, then a
    248                                     // 4-byte big-endian integer `signCount`.
    249                                     // The starting index of `credentialId` is 18 within `attestedCredentialData`.
    250                                     // Recall the first 16 bytes are `aaguid`, then a 2-byte big-endian integer
    251                                     // `credentialIdLength`. Consequently the starting index within
    252                                     // `attestation_object` is `idx + 37 + 18` = `idx + 55`. Overflow cannot occur
    253                                     // since we successfully parsed `AttestedCredentialData`.
    254                                     AttestedCredentialData::from_cbor(&a_data.0[37..]).map_err(Error::custom).map(|success| Some((success.value, idx + 55)))
    255                                 } else {
    256                                     Err(Error::invalid_value(Unexpected::Bytes(a_data.0.as_slice()), &format!("authenticator data to match the authenticator data portion of attestation object: {:?}", &attestation_object[idx..]).as_str()))
    257                                 }
    258                             })
    259                         } else {
    260                             Err(Error::invalid_value(Unexpected::Bytes(a_data.0.as_slice()), &"authenticator data to be long enough to contain attested credential data"))
    261                         }
    262                     }
    263                 ).and_then(|attested_info| {
    264                     pub_key.ok_or(false).and_then(|opt_key| opt_key.ok_or(true)).map_or_else(
    265                         |flag| {
    266                             if R {
    267                                 attested_info.as_ref().map_or(Ok(None), |&(ref attested_data, cred_id_start)| Ok(Some((match attested_data.credential_public_key {
    268                                     UncompressedPubKey::MlDsa87(_) => CoseAlgorithmIdentifier::Mldsa87,
    269                                     UncompressedPubKey::MlDsa65(_) => CoseAlgorithmIdentifier::Mldsa65,
    270                                     UncompressedPubKey::MlDsa44(_) => CoseAlgorithmIdentifier::Mldsa44,
    271                                     UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
    272                                     UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
    273                                     UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
    274                                     UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
    275                                     // Overflow won't occur since this is correct as
    276                                     // `AttestedCredentialData::from_cbor` would have erred if not.
    277                                 }, cred_id_start, cred_id_start + attested_data.credential_id.0.len()))))
    278                             } else {
    279                                 // `publicKey` is only allowed to not exist when `CoseAlgorithmIdentifier::Eddsa`,
    280                                 // `CoseAlgorithmIdentifier::Es256`, or `CoseAlgorithmIdentifier::Rs256` is not
    281                                 // used.
    282                                 attested_info.as_ref().map_or_else(
    283                                     || AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
    284                                         match att_obj.auth_data.attested_credential_data.credential_public_key {
    285                                             UncompressedPubKey::MlDsa87(_) => {
    286                                                 // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
    287                                                 // is the start of the raw authenticator data which itself contains the raw Credential ID.
    288                                                 Ok(Some((CoseAlgorithmIdentifier::Mldsa87, auth_idx,  auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
    289                                             }
    290                                             UncompressedPubKey::MlDsa65(_) => {
    291                                                 // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
    292                                                 // is the start of the raw authenticator data which itself contains the raw Credential ID.
    293                                                 Ok(Some((CoseAlgorithmIdentifier::Mldsa65, auth_idx,  auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
    294                                             }
    295                                             UncompressedPubKey::MlDsa44(_) => {
    296                                                 // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
    297                                                 // is the start of the raw authenticator data which itself contains the raw Credential ID.
    298                                                 Ok(Some((CoseAlgorithmIdentifier::Mldsa44, auth_idx,  auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
    299                                             }
    300                                             UncompressedPubKey::P384(_) => {
    301                                                 // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
    302                                                 // is the start of the raw authenticator data which itself contains the raw Credential ID.
    303                                                 Ok(Some((CoseAlgorithmIdentifier::Es384, auth_idx,  auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
    304                                             }
    305                                             UncompressedPubKey::Ed25519(_) | UncompressedPubKey::P256(_) | UncompressedPubKey::Rsa(_) => Err(Error::missing_field(PUBLIC_KEY)),
    306                                         }
    307                                     }),
    308                                     |&(ref attested_data, cred_id_start)| {
    309                                         match attested_data.credential_public_key {
    310                                             UncompressedPubKey::MlDsa87(_) => {
    311                                                 // Overflow won't occur since this is correct. This is correct since we successfully parsed
    312                                                 // `AttestedCredentialData` and calculated `cred_id_start` from it.
    313                                                 Ok(Some((CoseAlgorithmIdentifier::Mldsa87, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
    314                                             }
    315                                             UncompressedPubKey::MlDsa65(_) => {
    316                                                 // Overflow won't occur since this is correct. This is correct since we successfully parsed
    317                                                 // `AttestedCredentialData` and calculated `cred_id_start` from it.
    318                                                 Ok(Some((CoseAlgorithmIdentifier::Mldsa65, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
    319                                             }
    320                                             UncompressedPubKey::MlDsa44(_) => {
    321                                                 // Overflow won't occur since this is correct. This is correct since we successfully parsed
    322                                                 // `AttestedCredentialData` and calculated `cred_id_start` from it.
    323                                                 Ok(Some((CoseAlgorithmIdentifier::Mldsa44, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
    324                                             }
    325                                             UncompressedPubKey::P384(_) => {
    326                                                 // Overflow won't occur since this is correct. This is correct since we successfully parsed
    327                                                 // `AttestedCredentialData` and calculated `cred_id_start` from it.
    328                                                 Ok(Some((CoseAlgorithmIdentifier::Es384, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
    329                                             }
    330                                             UncompressedPubKey::Ed25519(_) | UncompressedPubKey::P256(_) | UncompressedPubKey::Rsa(_) => if flag { Err(Error::invalid_type(Unexpected::Other("null"), &format!("{PUBLIC_KEY} to be a base64url-encoded DER-encoded SubjectPublicKeyInfo").as_str())) } else { Err(Error::missing_field(PUBLIC_KEY)) },
    331                                         }
    332                                     }
    333                                 )
    334                             }
    335                         },
    336                         |der| {
    337                             UncompressedPubKey::from_der(der.0.as_slice()).map_err(Error::custom).and_then(|key| {
    338                                 attested_info.as_ref().map_or_else(
    339                                     || AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
    340                                         if key == att_obj.auth_data.attested_credential_data.credential_public_key {
    341                                             let alg = match att_obj.auth_data.attested_credential_data.credential_public_key {
    342                                                 UncompressedPubKey::MlDsa87(_) => CoseAlgorithmIdentifier::Mldsa87,
    343                                                 UncompressedPubKey::MlDsa65(_) => CoseAlgorithmIdentifier::Mldsa65,
    344                                                 UncompressedPubKey::MlDsa44(_) => CoseAlgorithmIdentifier::Mldsa44,
    345                                                 UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
    346                                                 UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
    347                                                 UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
    348                                                 UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
    349                                             };
    350                                             // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
    351                                             // is the start of the raw authenticator data which itself contains the raw Credential ID.
    352                                             Ok(Some((alg, auth_idx, auth_idx+ att_obj.auth_data.attested_credential_data.credential_id.0.len())))
    353                                         } else {
    354                                             Err(Error::invalid_value(Unexpected::Bytes(der.0.as_slice()), &format!("DER-encoded public key to match the public key within the attestation object: {:?}", att_obj.auth_data.attested_credential_data.credential_public_key).as_str()))
    355                                         }
    356                                     }),
    357                                     |&(ref attested_data, cred_id_start)| {
    358                                         if key == attested_data.credential_public_key {
    359                                             let alg = match attested_data.credential_public_key {
    360                                                 UncompressedPubKey::MlDsa87(_) => CoseAlgorithmIdentifier::Mldsa87,
    361                                                 UncompressedPubKey::MlDsa65(_) => CoseAlgorithmIdentifier::Mldsa65,
    362                                                 UncompressedPubKey::MlDsa44(_) => CoseAlgorithmIdentifier::Mldsa44,
    363                                                 UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
    364                                                 UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
    365                                                 UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
    366                                                 UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
    367                                             };
    368                                             // Overflow won't occur since this is correct. This is correct since we successfully parsed
    369                                             // `AttestedCredentialData` and calculated `cred_id_start` from it.
    370                                             Ok(Some((alg, cred_id_start, cred_id_start + attested_data.credential_id.0.len())))
    371                                         } else {
    372                                             Err(Error::invalid_value(Unexpected::Bytes(der.0.as_slice()), &format!("DER-encoded public key to match the public key within the attestation object: {:?}", attested_data.credential_public_key).as_str()))
    373                                         }
    374                                     }
    375                                 )
    376                             })
    377                         }
    378                     ).and_then(|cred_key_alg_cred_info| {
    379                         key_alg.ok_or(false).and_then(|opt_alg| opt_alg.ok_or(true)).map_or_else(
    380                             |flag| {
    381                                 if R {
    382                                     Ok(cred_key_alg_cred_info.map(|info| (info.1, info.2)))
    383                                 } else if flag {
    384                                     Err(Error::invalid_type(Unexpected::Other("null"), &format!("{PUBLIC_KEY_ALGORITHM} to be a base64url-encoded DER-encoded SubjectPublicKeyInfo").as_str()))
    385                                 } else {
    386                                     Err(Error::missing_field(PUBLIC_KEY_ALGORITHM))
    387                                 }
    388                             },
    389                             |alg| {
    390                                 cred_key_alg_cred_info.map_or_else(
    391                                     || AttestationObject::parse_data(attestation_object.as_slice()).map_err(Error::custom).and_then(|(att_obj, auth_idx)| {
    392                                         let att_obj_alg = match att_obj.auth_data.attested_credential_data.credential_public_key {
    393                                             UncompressedPubKey::MlDsa87(_) => CoseAlgorithmIdentifier::Mldsa87,
    394                                             UncompressedPubKey::MlDsa65(_) => CoseAlgorithmIdentifier::Mldsa65,
    395                                             UncompressedPubKey::MlDsa44(_) => CoseAlgorithmIdentifier::Mldsa44,
    396                                             UncompressedPubKey::Ed25519(_) => CoseAlgorithmIdentifier::Eddsa,
    397                                             UncompressedPubKey::P256(_) => CoseAlgorithmIdentifier::Es256,
    398                                             UncompressedPubKey::P384(_) => CoseAlgorithmIdentifier::Es384,
    399                                             UncompressedPubKey::Rsa(_) => CoseAlgorithmIdentifier::Rs256,
    400                                         };
    401                                         if alg == att_obj_alg {
    402                                             // This won't overflow since `AttestationObject::parse_data` succeeded and `auth_idx`
    403                                             // is the start of the raw authenticator data which itself contains the raw Credential ID.
    404                                             Ok(Some((auth_idx, auth_idx + att_obj.auth_data.attested_credential_data.credential_id.0.len())))
    405                                         } else {
    406                                             Err(Error::invalid_value(Unexpected::Other(format!("{alg:?}").as_str()), &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {att_obj_alg:?}").as_str()))
    407                                         }
    408                                     }),
    409                                     |(a, start, last)| if alg == a {
    410                                         Ok(Some((start, last)))
    411                                     } else {
    412                                         Err(Error::invalid_value(Unexpected::Other(format!("{alg:?}").as_str()), &format!("public key algorithm to match the algorithm associated with the public key within the attestation object: {a:?}").as_str()))
    413                                     },
    414                                 )
    415                             }
    416                         ).map(|cred_info| AuthAttest{ attest: AuthenticatorAttestation::new(client_data_json, attestation_object, transports), cred_info, })
    417                     })
    418                 })
    419             })
    420         }))
    421     }
    422 }
    423 /// `"clientDataJSON"`
    424 const CLIENT_DATA_JSON: &str = "clientDataJSON";
    425 /// `"attestationObject"`
    426 const ATTESTATION_OBJECT: &str = "attestationObject";
    427 /// `"authenticatorData"`
    428 const AUTHENTICATOR_DATA: &str = "authenticatorData";
    429 /// `"transports"`
    430 const TRANSPORTS: &str = "transports";
    431 /// `"publicKey"`
    432 const PUBLIC_KEY: &str = "publicKey";
    433 /// `"publicKeyAlgorithm"`
    434 const PUBLIC_KEY_ALGORITHM: &str = "publicKeyAlgorithm";
    435 /// Fields in `AuthenticatorAttestationResponseJSON`.
    436 pub(super) const AUTH_ATTEST_FIELDS: &[&str; 6] = &[
    437     CLIENT_DATA_JSON,
    438     ATTESTATION_OBJECT,
    439     AUTHENTICATOR_DATA,
    440     TRANSPORTS,
    441     PUBLIC_KEY,
    442     PUBLIC_KEY_ALGORITHM,
    443 ];
    444 impl<'de> Deserialize<'de> for AuthAttest {
    445     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    446     where
    447         D: Deserializer<'de>,
    448     {
    449         deserializer.deserialize_struct(
    450             "AuthenticatorAttestation",
    451             AUTH_ATTEST_FIELDS,
    452             AuthenticatorAttestationVisitor::<false>,
    453         )
    454     }
    455 }
    456 impl<'de> Deserialize<'de> for AuthenticatorAttestation {
    457     /// Deserializes a `struct` based on
    458     /// [`AuthenticatorAttestationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorattestationresponsejson).
    459     ///
    460     /// Note unknown keys and duplicate keys are forbidden;
    461     /// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-clientdatajson),
    462     /// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-authenticatordata),
    463     /// [`publicKey`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-publickey)
    464     /// and
    465     /// [`attestationObject`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-attestationobject)
    466     /// are base64url-decoded;
    467     /// [`transports`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-transports)
    468     /// is deserialized via [`AuthTransports::deserialize`]; the decoded `publicKey` is parsed according to the
    469     /// applicable DER-encoded ASN.1 `SubjectPublicKeyInfo` schema;
    470     /// [`publicKeyAlgorithm`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-publickeyalgorithm)
    471     /// is deserialized according to
    472     /// [`CoseAlgorithmIdentifier`](https://www.w3.org/TR/webauthn-3/#typedefdef-cosealgorithmidentifier); all `required`
    473     /// fields in the `AuthenticatorAttestationResponseJSON` Web IDL `dictionary` exist (and must not be `null`); `publicKey`
    474     /// exists when Ed25519, P-256 with SHA-256, or RSASSA-PKCS1-v1_5 with SHA-256 is used (and must not be `null`)
    475     /// [per WebAuthn](https://www.w3.org/TR/webauthn-3/#sctn-public-key-easy); the `publicKeyAlgorithm` aligns
    476     /// with
    477     /// [`credentialPublicKey`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialpublickey)
    478     /// within
    479     /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata) within the
    480     /// decoded `authenticatorData`; the decoded `publicKey` is the same as `credentialPublicKey` within
    481     /// `attestedCredentialData` within the decoded `authenticatorData`; and the decoded `authenticatorData` is the
    482     /// same as [`authData`](https://www.w3.org/TR/webauthn-3/#attestation-object) within the decoded
    483     /// `attestationObject`.
    484     #[inline]
    485     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    486     where
    487         D: Deserializer<'de>,
    488     {
    489         AuthAttest::deserialize(deserializer).map(|val| val.attest)
    490     }
    491 }
    492 /// `Visitor` for `CredentialPropertiesOutput`.
    493 ///
    494 /// Unknown fields are ignored iff `RELAXED`.
    495 pub(super) struct CredentialPropertiesOutputVisitor<const RELAXED: bool>;
    496 impl<'d, const R: bool> Visitor<'d> for CredentialPropertiesOutputVisitor<R> {
    497     type Value = CredentialPropertiesOutput;
    498     fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    499         formatter.write_str("CredentialPropertiesOutput")
    500     }
    501     fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    502     where
    503         A: MapAccess<'d>,
    504     {
    505         /// Allowed fields.
    506         enum Field<const IGNORE_UNKNOWN: bool> {
    507             /// `rk` field.
    508             Rk,
    509             /// Unknown field.
    510             Other,
    511         }
    512         impl<'e, const I: bool> Deserialize<'e> for Field<I> {
    513             fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    514             where
    515                 D: Deserializer<'e>,
    516             {
    517                 /// `Visitor` for `Field`.
    518                 ///
    519                 /// Unknown fields are ignored iff `IGNORE_UNKNOWN`.
    520                 struct FieldVisitor<const IGNORE_UNKNOWN: bool>;
    521                 impl<const IG: bool> Visitor<'_> for FieldVisitor<IG> {
    522                     type Value = Field<IG>;
    523                     fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    524                         write!(formatter, "'{RK}'")
    525                     }
    526                     fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    527                     where
    528                         E: Error,
    529                     {
    530                         match v {
    531                             RK => Ok(Field::Rk),
    532                             _ => {
    533                                 if IG {
    534                                     Ok(Field::Other)
    535                                 } else {
    536                                     Err(E::unknown_field(v, PROPS_FIELDS))
    537                                 }
    538                             }
    539                         }
    540                     }
    541                 }
    542                 deserializer.deserialize_identifier(FieldVisitor)
    543             }
    544         }
    545         let mut rk = None;
    546         while let Some(key) = map.next_key::<Field<R>>()? {
    547             match key {
    548                 Field::Rk => {
    549                     if rk.is_some() {
    550                         return Err(Error::duplicate_field(RK));
    551                     }
    552                     rk = map.next_value().map(Some)?;
    553                 }
    554                 Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
    555             }
    556         }
    557         Ok(CredentialPropertiesOutput { rk: rk.flatten() })
    558     }
    559 }
    560 /// `"rk"`
    561 const RK: &str = "rk";
    562 /// `CredentialPropertiesOutput` fields.
    563 pub(super) const PROPS_FIELDS: &[&str; 1] = &[RK];
    564 impl<'de> Deserialize<'de> for CredentialPropertiesOutput {
    565     /// Deserializes a `struct` based on
    566     /// [`CredentialPropertiesOutput`](https://www.w3.org/TR/webauthn-3/#dictdef-credentialpropertiesoutput).
    567     ///
    568     /// Note unknown and duplicate keys are forbidden.
    569     #[inline]
    570     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    571     where
    572         D: Deserializer<'de>,
    573     {
    574         deserializer.deserialize_struct(
    575             "CredentialPropertiesOutput",
    576             PROPS_FIELDS,
    577             CredentialPropertiesOutputVisitor::<false>,
    578         )
    579     }
    580 }
    581 impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfOutputs {
    582     /// Deserializes a `struct` based on
    583     /// [`AuthenticationExtensionsPRFOutputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfoutputsjson).
    584     ///
    585     /// Note unknown and duplicate keys are forbidden;
    586     /// [`enabled`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-enabled)
    587     /// must exist (and not be `null`); and
    588     /// [`results`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfoutputs-results) must not exist,
    589     /// be `null`, or be an
    590     /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues)
    591     /// with no unknown or duplicate keys,
    592     /// [`first`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-first) must exist but be
    593     /// `null`, and
    594     /// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) can exist but
    595     /// must be `null` if so.
    596     #[inline]
    597     #[expect(clippy::unreachable, reason = "we want to crash when there is a bug")]
    598     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    599     where
    600         D: Deserializer<'de>,
    601     {
    602         AuthenticationExtensionsPrfOutputsHelper::<false, true, AuthenticationExtensionsPrfValues>::deserialize(deserializer).map(|val| Self {
    603             enabled: val.0.unwrap_or_else(|| {
    604                 unreachable!(
    605                     "there is a bug in AuthenticationExtensionsPrfOutputsHelper::deserialize"
    606                 )
    607             }),
    608         })
    609     }
    610 }
    611 /// `Visitor` for `ClientExtensionsOutputs`.
    612 ///
    613 /// Unknown fields are ignored iff `RELAXED`.
    614 pub(super) struct ClientExtensionsOutputsVisitor<const RELAXED: bool, PROPS, PRF>(
    615     pub PhantomData<fn() -> (PROPS, PRF)>,
    616 );
    617 impl<'d, const R: bool, C, P> Visitor<'d> for ClientExtensionsOutputsVisitor<R, C, P>
    618 where
    619     C: for<'a> Deserialize<'a> + Into<CredentialPropertiesOutput>,
    620     P: for<'a> Deserialize<'a> + Into<AuthenticationExtensionsPrfOutputs>,
    621 {
    622     type Value = ClientExtensionsOutputs;
    623     fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    624         formatter.write_str("ClientExtensionsOutputs")
    625     }
    626     fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    627     where
    628         A: MapAccess<'d>,
    629     {
    630         /// Allowed fields.
    631         enum Field<const IGNORE_UNKNOWN: bool> {
    632             /// `credProps` field.
    633             CredProps,
    634             /// `prf` field.
    635             Prf,
    636             /// Unknown field.
    637             Other,
    638         }
    639         impl<'e, const I: bool> Deserialize<'e> for Field<I> {
    640             fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    641             where
    642                 D: Deserializer<'e>,
    643             {
    644                 /// `Visitor` for `Field`.
    645                 ///
    646                 /// Unknown fields are ignored iff `IGNORE_UNKNOWN`.
    647                 struct FieldVisitor<const IGNORE_UNKNOWN: bool>;
    648                 impl<const IG: bool> Visitor<'_> for FieldVisitor<IG> {
    649                     type Value = Field<IG>;
    650                     fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    651                         write!(formatter, "'{CRED_PROPS}' or '{PRF}'")
    652                     }
    653                     fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    654                     where
    655                         E: Error,
    656                     {
    657                         match v {
    658                             CRED_PROPS => Ok(Field::CredProps),
    659                             PRF => Ok(Field::Prf),
    660                             _ => {
    661                                 if IG {
    662                                     Ok(Field::Other)
    663                                 } else {
    664                                     Err(E::unknown_field(v, EXT_FIELDS))
    665                                 }
    666                             }
    667                         }
    668                     }
    669                 }
    670                 deserializer.deserialize_identifier(FieldVisitor)
    671             }
    672         }
    673         let mut cred_props = None;
    674         let mut prf = None;
    675         while let Some(key) = map.next_key::<Field<R>>()? {
    676             match key {
    677                 Field::CredProps => {
    678                     if cred_props.is_some() {
    679                         return Err(Error::duplicate_field(CRED_PROPS));
    680                     }
    681                     cred_props = map.next_value::<Option<C>>().map(Some)?;
    682                 }
    683                 Field::Prf => {
    684                     if prf.is_some() {
    685                         return Err(Error::duplicate_field(PRF));
    686                     }
    687                     prf = map.next_value::<Option<P>>().map(Some)?;
    688                 }
    689                 Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?,
    690             }
    691         }
    692         Ok(ClientExtensionsOutputs {
    693             cred_props: cred_props.flatten().map(Into::into),
    694             prf: prf.flatten().map(Into::into),
    695         })
    696     }
    697 }
    698 impl ClientExtensions for ClientExtensionsOutputs {
    699     fn empty() -> Self {
    700         Self {
    701             prf: None,
    702             cred_props: None,
    703         }
    704     }
    705 }
    706 /// `"credProps"`
    707 const CRED_PROPS: &str = "credProps";
    708 /// `"prf"`
    709 const PRF: &str = "prf";
    710 /// `AuthenticationExtensionsClientOutputsJSON` fields.
    711 pub(super) const EXT_FIELDS: &[&str; 2] = &[CRED_PROPS, PRF];
    712 impl<'de> Deserialize<'de> for ClientExtensionsOutputs {
    713     /// Deserializes a `struct` based on
    714     /// [`AuthenticationExtensionsClientOutputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientoutputsjson).
    715     ///
    716     /// Note that unknown and duplicate keys are forbidden;
    717     /// [`credProps`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-credprops) is
    718     /// `null` or deserialized via [`CredentialPropertiesOutput::deserialize`]; and
    719     /// [`prf`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsclientoutputs-prf) is `null`
    720     /// or deserialized via [`AuthenticationExtensionsPrfOutputs::deserialize`].
    721     #[inline]
    722     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    723     where
    724         D: Deserializer<'de>,
    725     {
    726         deserializer.deserialize_struct(
    727             "ClientExtensionsOutputs",
    728             EXT_FIELDS,
    729             ClientExtensionsOutputsVisitor::<
    730                 false,
    731                 CredentialPropertiesOutput,
    732                 AuthenticationExtensionsPrfOutputs,
    733             >(PhantomData),
    734         )
    735     }
    736 }
    737 impl<'de> Deserialize<'de> for Registration {
    738     /// Deserializes a `struct` based on
    739     /// [`RegistrationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson).
    740     ///
    741     /// Note that unknown and duplicate keys are forbidden;
    742     /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-id) and
    743     /// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-rawid) are deserialized
    744     /// via [`CredentialId::deserialize`];
    745     /// [`response`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-response) is deserialized
    746     /// via [`AuthenticatorAttestation::deserialize`];
    747     /// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-authenticatorattachment)
    748     /// is `null` or deserialized via [`AuthenticatorAttachment::deserialize`];
    749     /// [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-clientextensionresults)
    750     /// is deserialized via [`ClientExtensionsOutputs::deserialize`]; all `required` fields in the
    751     /// `RegistrationResponseJSON` Web IDL `dictionary` exist (and are not `null`);
    752     /// [`type`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-type) is `"public-key"`;
    753     /// and the decoded `id`, decoded `rawId`, and
    754     /// [`credentialId`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata-credentialid) within
    755     /// [`attestedCredentialData`](https://www.w3.org/TR/webauthn-3/#authdata-attestedcredentialdata) within
    756     /// [`authData`](https://www.w3.org/TR/webauthn-3/#attestation-object) within the decoded
    757     /// [`attestationObject`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponsejson-attestationobject)
    758     /// are all the same.
    759     #[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
    760     #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
    761     #[inline]
    762     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    763     where
    764         D: Deserializer<'de>,
    765     {
    766         PublicKeyCredential::<false, true, AuthAttest, ClientExtensionsOutputs>::deserialize(deserializer).and_then(|cred| {
    767             let id = cred.id.unwrap_or_else(|| unreachable!("there is a bug in PublicKeyCredential::deserialize"));
    768             cred.response.cred_info.map_or_else(
    769                 || AttestationObject::try_from(cred.response.attest.attestation_object()).map_err(Error::custom).and_then(|att_obj| {
    770                     if id.as_ref() == att_obj.auth_data.attested_credential_data.credential_id.as_ref() {
    771                         Ok(())
    772                     } else {
    773                         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()))
    774                     }
    775                 }),
    776                 // `start` and `last` were calculated based on `cred.response.attest.attestation_object()`
    777                 // and represent the starting and ending index of the `CredentialId`; therefore this is correct
    778                 // let alone won't `panic`.
    779                 |(start, last)| if *id.0 == cred.response.attest.attestation_object()[start..last] {
    780                     Ok(())
    781                 } else {
    782                     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.attest.attestation_object()[start..last]).as_str()))
    783                 },
    784             ).map(|()| Self { response: cred.response.attest, authenticator_attachment: cred.authenticator_attachment, client_extension_results: cred.client_extension_results, })
    785         })
    786     }
    787 }