ser_relaxed.rs (17987B)
1 extern crate alloc; 2 #[cfg(test)] 3 mod tests; 4 #[cfg(doc)] 5 use super::{Challenge, LimitedVerificationParser}; 6 use super::{ 7 ClientDataJsonParser, CollectedClientData, Origin, SentChallenge, 8 ser::{ 9 AuthenticationExtensionsPrfValues, AuthenticationExtensionsPrfValuesVisitor, 10 PRF_VALUES_FIELDS, 11 }, 12 }; 13 use alloc::borrow::Cow; 14 use core::{ 15 fmt::{self, Formatter}, 16 marker::PhantomData, 17 }; 18 use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, Unexpected, Visitor}; 19 #[cfg(doc)] 20 use serde_json::de; 21 /// Category returned by [`SerdeJsonErr::classify`]. 22 pub use serde_json::error::Category; 23 /// Error returned by [`CollectedClientData::from_client_data_json_relaxed`] or any of the [`Deserialize`] 24 /// implementations when relying on [`de::Deserializer`] or [`de::StreamDeserializer`]. 25 pub use serde_json::error::Error as SerdeJsonErr; 26 /// "Relaxed" [`ClientDataJsonParser`]. 27 /// 28 /// Unlike [`LimitedVerificationParser`] which requires 29 /// [JSON-compatible serialization of client data](https://www.w3.org/TR/webauthn-3/#collectedclientdata-json-compatible-serialization-of-client-data) 30 /// to be parsed _exactly_ as required by 31 /// the [limited verification algorithm](https://www.w3.org/TR/webauthn-3/#clientdatajson-verification), 32 /// this is a "relaxed" parser. 33 /// 34 /// L1 clients predate the JSON-compatible serialization of client data; additionally there are L2 and L3 clients 35 /// that don't adhere to the JSON-compatible serialization of client data despite being required to. These clients 36 /// serialize `CollectedClientData` so that it's valid JSON and conforms to the Web IDL `dictionary` and nothing more. 37 /// Furthermore the spec requires that data be decoded in a way equivalent 38 /// to [UTF-8 decode](https://encoding.spec.whatwg.org/#utf-8-decode) which both interprets a leading zero 39 /// width no-breaking space (i.e., U+FEFF) as a byte-order mark (BOM) as well as replaces any sequences of invalid 40 /// UTF-8 code units with the replacement character (i.e., U+FFFD). That is precisely what this parser does. 41 /// 42 /// In particular the parser errors iff any of the following is true: 43 /// 44 /// * The payload is not valid JSON _after_ ignoring a leading U+FEFF and replacing any sequences of invalid 45 /// UTF-8 code units with U+FFFD. 46 /// * The JSON does not conform to the Web IDL `dictionary`. 47 /// * [`type`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-type) is not `"webauthn.create"` 48 /// or `"webauthn.get"` when `REGISTRATION` and `!REGISTRATION` respectively. 49 /// * [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-challenge) is not a 50 /// base64url-encoded [`Challenge`]. 51 /// * Existence of duplicate keys in the root object _including_ keys that otherwise would have been ignored. 52 pub(super) struct RelaxedClientDataJsonParser<const REGISTRATION: bool>; 53 impl<const R: bool> ClientDataJsonParser for RelaxedClientDataJsonParser<R> { 54 type Err = SerdeJsonErr; 55 fn parse(json: &[u8]) -> Result<CollectedClientData<'_>, Self::Err> { 56 /// U+FEFF encoded in UTF-8. 57 const BOM: [u8; 3] = [0xef, 0xbb, 0xbf]; 58 // We avoid first calling `String::from_utf8_lossy` since `CDataJsonHelper` relies on 59 // [`Deserializer::deserialize_bytes`] instead of [`Deserializer::deserialize_str`] and 60 // [`Deserializer::deserialize_identifier`]. Additionally [`CollectedClientData::origin`] and 61 // [`CollectedClientData::top_origin`] are the only fields that need to actually replace invalid 62 // UTF-8 code units, and this is achieved via the inner `OriginWrapper` type. 63 serde_json::from_slice::<CDataJsonHelper<'_, R>>(json.split_at_checked(BOM.len()).map_or( 64 json, 65 |(bom, rem)| { 66 if bom == BOM { rem } else { json } 67 }, 68 )) 69 .map(|val| val.0) 70 } 71 fn get_sent_challenge(json: &[u8]) -> Result<SentChallenge, Self::Err> { 72 /// U+FEFF encoded in UTF-8. 73 const BOM: [u8; 3] = [0xef, 0xbb, 0xbf]; 74 // We avoid first calling `String::from_utf8_lossy` since `Chall` relies on 75 // [`Deserializer::deserialize_bytes`] instead of [`Deserializer::deserialize_identifier`]. 76 serde_json::from_slice::<Chall>(json.split_at_checked(BOM.len()).map_or( 77 json, 78 |(bom, rem)| { 79 if bom == BOM { rem } else { json } 80 }, 81 )) 82 .map(|c| c.0) 83 } 84 } 85 /// Used by [`RelaxedClientDataJsonParser::get_sent_challenge`] to minimally deserialize the JSON. 86 struct Chall(SentChallenge); 87 impl<'de> Deserialize<'de> for Chall { 88 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 89 where 90 D: Deserializer<'de>, 91 { 92 /// `Visitor` for `Chall`. 93 struct ChallVisitor; 94 impl<'d> Visitor<'d> for ChallVisitor { 95 type Value = Chall; 96 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 97 formatter.write_str("Chall") 98 } 99 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> 100 where 101 A: MapAccess<'d>, 102 { 103 /// Fields in `clientDataJSON`. 104 enum Field { 105 /// `"challenge"`. 106 Challenge, 107 /// All other fields. 108 Other, 109 } 110 impl<'e> Deserialize<'e> for Field { 111 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 112 where 113 D: Deserializer<'e>, 114 { 115 /// `Visitor` for `Field`. 116 struct FieldVisitor; 117 impl Visitor<'_> for FieldVisitor { 118 type Value = Field; 119 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 120 write!(formatter, "'{CHALLENGE}'") 121 } 122 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> 123 where 124 E: Error, 125 { 126 if v == b"challenge" { 127 Ok(Field::Challenge) 128 } else { 129 Ok(Field::Other) 130 } 131 } 132 } 133 deserializer.deserialize_bytes(FieldVisitor) 134 } 135 } 136 let mut chall = None; 137 while let Some(key) = map.next_key()? { 138 match key { 139 Field::Challenge => { 140 if chall.is_some() { 141 return Err(Error::duplicate_field(CHALLENGE)); 142 } 143 chall = map.next_value().map(Some)?; 144 } 145 Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?, 146 } 147 } 148 chall 149 .ok_or_else(|| Error::missing_field(CHALLENGE)) 150 .map(Chall) 151 } 152 } 153 /// Fields we care about. 154 const FIELDS: &[&str; 1] = &[CHALLENGE]; 155 deserializer.deserialize_struct("Chall", FIELDS, ChallVisitor) 156 } 157 } 158 /// "type". 159 const TYPE: &str = "type"; 160 /// "challenge". 161 const CHALLENGE: &str = "challenge"; 162 /// "origin". 163 const ORIGIN: &str = "origin"; 164 /// "crossOrigin". 165 const CROSS_ORIGIN: &str = "crossOrigin"; 166 /// "topOrigin". 167 const TOP_ORIGIN: &str = "topOrigin"; 168 /// Fields for `CollectedClientData`. 169 const FIELDS: &[&str; 5] = &[TYPE, CHALLENGE, ORIGIN, CROSS_ORIGIN, TOP_ORIGIN]; 170 /// Helper for [`RelaxedClientDataJsonParser`]. 171 struct RelaxedHelper<'a, const REGISTRATION: bool>(PhantomData<fn() -> &'a ()>); 172 impl<'de: 'a, 'a, const R: bool> Visitor<'de> for RelaxedHelper<'a, R> { 173 type Value = CollectedClientData<'a>; 174 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 175 formatter.write_str("CollectedClientData") 176 } 177 #[expect( 178 clippy::too_many_lines, 179 reason = "don't want to move code to an outer scope" 180 )] 181 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> 182 where 183 A: MapAccess<'de>, 184 { 185 /// "webauthn.create". 186 const CREATE: &str = "webauthn.create"; 187 /// "webauthn.get". 188 const GET: &str = "webauthn.get"; 189 /// `CollectedClientData` fields. 190 enum Field { 191 /// "type" field. 192 Type, 193 /// "challenge" field. 194 Challenge, 195 /// "origin" field. 196 Origin, 197 /// "crossOrigin" field. 198 CrossOrigin, 199 /// "topOrigin" field. 200 TopOrigin, 201 /// Unknown field. 202 Other, 203 } 204 impl<'d> Deserialize<'d> for Field { 205 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 206 where 207 D: Deserializer<'d>, 208 { 209 /// `Visitor` for `Field`. 210 struct FieldVisitor; 211 impl Visitor<'_> for FieldVisitor { 212 type Value = Field; 213 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 214 write!( 215 formatter, 216 "'{TYPE}', '{CHALLENGE}', '{ORIGIN}', '{CROSS_ORIGIN}', or '{TOP_ORIGIN}'" 217 ) 218 } 219 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> 220 where 221 E: Error, 222 { 223 match v { 224 b"type" => Ok(Field::Type), 225 b"challenge" => Ok(Field::Challenge), 226 b"origin" => Ok(Field::Origin), 227 b"crossOrigin" => Ok(Field::CrossOrigin), 228 b"topOrigin" => Ok(Field::TopOrigin), 229 _ => Ok(Field::Other), 230 } 231 } 232 } 233 // MUST NOT call `Deserializer::deserialize_identifier` since that will call 234 // `FieldVisitor::visit_str` which obviously requires decoding the field as UTF-8 first. 235 deserializer.deserialize_bytes(FieldVisitor) 236 } 237 } 238 /// Deserializes the type value. 239 /// Contains `true` iff the type is `"webauthn.create"`; otherwise the 240 /// value is `"webauthn.get"`. 241 struct Type(bool); 242 impl<'d> Deserialize<'d> for Type { 243 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 244 where 245 D: Deserializer<'d>, 246 { 247 /// `Visitor` for `Type`. 248 struct TypeVisitor; 249 impl Visitor<'_> for TypeVisitor { 250 type Value = Type; 251 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 252 write!(formatter, "'{CREATE}' or '{GET}'") 253 } 254 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> 255 where 256 E: Error, 257 { 258 match v { 259 b"webauthn.create" => Ok(Type(true)), 260 b"webauthn.get" => Ok(Type(false)), 261 _ => Err(Error::invalid_value( 262 Unexpected::Bytes(v), 263 &format!("'{CREATE}' or '{GET}'").as_str(), 264 )), 265 } 266 } 267 } 268 deserializer.deserialize_bytes(TypeVisitor) 269 } 270 } 271 /// `newtype` around `Origin` that implements [`Deserialize`] such that invalid UTF-8 code units are first 272 /// replaced with the replacement character. We don't do this for `Origin` since we want its public API 273 /// to forbid invalid UTF-8. 274 struct OriginWrapper<'d>(Origin<'d>); 275 impl<'d: 'e, 'e> Deserialize<'d> for OriginWrapper<'e> { 276 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 277 where 278 D: Deserializer<'d>, 279 { 280 /// `Visitor` for `OriginWrapper`. 281 struct OriginWrapperVisitor<'f>(PhantomData<fn() -> &'f ()>); 282 impl<'f: 'g, 'g> Visitor<'f> for OriginWrapperVisitor<'g> { 283 type Value = OriginWrapper<'g>; 284 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 285 formatter.write_str("OriginWrapper") 286 } 287 fn visit_borrowed_bytes<E>(self, v: &'f [u8]) -> Result<Self::Value, E> 288 where 289 E: Error, 290 { 291 Ok(OriginWrapper(Origin(String::from_utf8_lossy(v)))) 292 } 293 fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E> 294 where 295 E: Error, 296 { 297 Ok(OriginWrapper(Origin(Cow::Owned( 298 String::from_utf8_lossy(v.as_slice()).into_owned(), 299 )))) 300 } 301 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> 302 where 303 E: Error, 304 { 305 self.visit_byte_buf(v.to_owned()) 306 } 307 } 308 deserializer.deserialize_bytes(OriginWrapperVisitor(PhantomData)) 309 } 310 } 311 let mut typ = false; 312 let mut chall = None; 313 let mut orig = None; 314 let mut cross = None; 315 let mut top_orig = None; 316 while let Some(key) = map.next_key()? { 317 match key { 318 Field::Type => { 319 if typ { 320 return Err(Error::duplicate_field(TYPE)); 321 } 322 typ = map.next_value::<Type>().and_then(|v| { 323 if v.0 { 324 if R { 325 Ok(true) 326 } else { 327 Err(Error::invalid_value(Unexpected::Str(CREATE), &GET)) 328 } 329 } else if R { 330 Err(Error::invalid_value(Unexpected::Str(GET), &CREATE)) 331 } else { 332 Ok(true) 333 } 334 })?; 335 } 336 Field::Challenge => { 337 if chall.is_some() { 338 return Err(Error::duplicate_field(CHALLENGE)); 339 } 340 chall = map.next_value().map(Some)?; 341 } 342 Field::Origin => { 343 if orig.is_some() { 344 return Err(Error::duplicate_field(ORIGIN)); 345 } 346 orig = map.next_value::<OriginWrapper<'_>>().map(|o| Some(o.0))?; 347 } 348 Field::CrossOrigin => { 349 if cross.is_some() { 350 return Err(Error::duplicate_field(CROSS_ORIGIN)); 351 } 352 cross = map.next_value().map(Some)?; 353 } 354 Field::TopOrigin => { 355 if top_orig.is_some() { 356 return Err(Error::duplicate_field(TOP_ORIGIN)); 357 } 358 top_orig = map.next_value::<Option<OriginWrapper<'_>>>().map(Some)?; 359 } 360 // `IgnoredAny` ignores invalid UTF-8 in and only in JSON strings. 361 Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?, 362 } 363 } 364 if typ { 365 chall 366 .ok_or_else(|| Error::missing_field(CHALLENGE)) 367 .and_then(|challenge| { 368 orig.ok_or_else(|| Error::missing_field(ORIGIN)) 369 .map(|origin| CollectedClientData { 370 challenge, 371 origin, 372 cross_origin: cross.flatten().unwrap_or_default(), 373 top_origin: top_orig.flatten().map(|o| o.0), 374 }) 375 }) 376 } else { 377 Err(Error::missing_field(TYPE)) 378 } 379 } 380 } 381 /// `newtype` around [`CollectedClientData`] to avoid implementing [`Deserialize`] for `CollectedClientData`. 382 struct CDataJsonHelper<'a, const REGISTRATION: bool>(CollectedClientData<'a>); 383 impl<'de: 'a, 'a, const R: bool> Deserialize<'de> for CDataJsonHelper<'a, R> { 384 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 385 where 386 D: Deserializer<'de>, 387 { 388 deserializer 389 .deserialize_struct( 390 "CollectedClientData", 391 FIELDS, 392 RelaxedHelper::<R>(PhantomData), 393 ) 394 .map(Self) 395 } 396 } 397 /// `newtype` around `AuthenticationExtensionsPrfValues` with a "relaxed" [`Self::deserialize`] implementation. 398 pub(super) struct AuthenticationExtensionsPrfValuesRelaxed(pub AuthenticationExtensionsPrfValues); 399 impl<'de> Deserialize<'de> for AuthenticationExtensionsPrfValuesRelaxed { 400 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 401 where 402 D: Deserializer<'de>, 403 { 404 deserializer 405 .deserialize_struct( 406 "AuthenticationExtensionsPrfValuesRelaxed", 407 PRF_VALUES_FIELDS, 408 AuthenticationExtensionsPrfValuesVisitor::<true>, 409 ) 410 .map(Self) 411 } 412 }