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 }