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 }