ser.rs (20859B)
1 use super::{ 2 AllowedCredential, AllowedCredentials, Credentials as _, DiscoverableAuthenticationClientState, 3 Extension, NonDiscoverableAuthenticationClientState, PrfInput, PrfInputOwned, 4 PublicKeyCredentialRequestOptions, 5 }; 6 use data_encoding::BASE64URL_NOPAD; 7 use serde::ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer}; 8 impl Serialize for PrfInput<'_> { 9 /// Serializes `self` to conform with 10 /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues). 11 /// 12 /// # Examples 13 /// 14 /// ``` 15 /// # use webauthn_rp::request::{auth::PrfInput, ExtensionReq}; 16 /// assert_eq!( 17 /// serde_json::to_string(&PrfInput { 18 /// first: [0; 4].as_slice(), 19 /// second: Some([2; 1].as_slice()), 20 /// ext_info: ExtensionReq::Require 21 /// })?, 22 /// r#"{"first":"AAAAAA","second":"Ag"}"# 23 /// ); 24 /// # Ok::<_, serde_json::Error>(()) 25 /// ``` 26 #[expect( 27 clippy::arithmetic_side_effects, 28 reason = "comment justifies how overflow is not possible" 29 )] 30 #[inline] 31 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 32 where 33 S: Serializer, 34 { 35 serializer 36 // The max value is 1 + 1 = 2, so overflow is not an issue. 37 .serialize_struct("PrfInput", 1 + usize::from(self.second.is_some())) 38 .and_then(|mut ser| { 39 ser.serialize_field("first", BASE64URL_NOPAD.encode(self.first).as_str()) 40 .and_then(|()| { 41 self.second 42 .as_ref() 43 .map_or(Ok(()), |second| { 44 ser.serialize_field( 45 "second", 46 BASE64URL_NOPAD.encode(second).as_str(), 47 ) 48 }) 49 .and_then(|()| ser.end()) 50 }) 51 }) 52 } 53 } 54 impl Serialize for PrfInputOwned { 55 /// See [`PrfInput::serialize`] 56 #[inline] 57 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 58 where 59 S: Serializer, 60 { 61 PrfInput { 62 first: self.first.as_slice(), 63 second: self.second.as_deref(), 64 ext_info: self.ext_info, 65 } 66 .serialize(serializer) 67 } 68 } 69 impl Serialize for AllowedCredential { 70 /// Serializes `self` to conform with 71 /// [`PublicKeyCredentialDescriptorJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptorjson). 72 /// 73 /// # Examples 74 /// 75 /// ``` 76 /// # #[cfg(all(feature = "bin", feature = "custom"))] 77 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 78 /// # use webauthn_rp::{ 79 /// # request::{auth::AllowedCredential, PublicKeyCredentialDescriptor}, 80 /// # response::{AuthTransports, CredentialId}, 81 /// # }; 82 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 83 /// /// from the database. 84 /// # #[cfg(all(feature = "bin", feature = "custom"))] 85 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 86 /// // ⋮ 87 /// # AuthTransports::decode(32) 88 /// } 89 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 90 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 91 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 92 /// # #[cfg(all(feature = "bin", feature = "custom"))] 93 /// let id = CredentialId::try_from(vec![0; 16])?; 94 /// # #[cfg(all(feature = "bin", feature = "custom"))] 95 /// let transports = get_transports((&id).into())?; 96 /// # #[cfg(all(feature = "bin", feature = "custom"))] 97 /// assert_eq!( 98 /// serde_json::to_string(&AllowedCredential::from(PublicKeyCredentialDescriptor { 99 /// id, 100 /// transports 101 /// })).unwrap(), 102 /// r#"{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}"# 103 /// ); 104 /// # Ok::<_, webauthn_rp::AggErr>(()) 105 /// ``` 106 #[inline] 107 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 108 where 109 S: Serializer, 110 { 111 self.credential.serialize(serializer) 112 } 113 } 114 impl Serialize for AllowedCredentials { 115 /// Serializes `self` to conform with 116 /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-allowcredentials). 117 /// 118 /// # Examples 119 /// 120 /// ``` 121 /// # #[cfg(all(feature = "bin", feature = "custom"))] 122 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 123 /// # use webauthn_rp::{ 124 /// # request::{auth::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials}, 125 /// # response::{AuthTransports, CredentialId}, 126 /// # }; 127 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 128 /// /// from the database. 129 /// # #[cfg(all(feature = "bin", feature = "custom"))] 130 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 131 /// // ⋮ 132 /// # AuthTransports::decode(32) 133 /// } 134 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 135 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 136 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 137 /// # #[cfg(all(feature = "bin", feature = "custom"))] 138 /// let id = CredentialId::try_from(vec![0; 16])?; 139 /// # #[cfg(all(feature = "bin", feature = "custom"))] 140 /// let transports = get_transports((&id).into())?; 141 /// let mut creds = AllowedCredentials::with_capacity(1); 142 /// # #[cfg(all(feature = "bin", feature = "custom"))] 143 /// creds.push(PublicKeyCredentialDescriptor { id, transports }.into()); 144 /// # #[cfg(all(feature = "bin", feature = "custom"))] 145 /// assert_eq!( 146 /// serde_json::to_string(&creds).unwrap(), 147 /// r#"[{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}]"# 148 /// ); 149 /// # Ok::<_, webauthn_rp::AggErr>(()) 150 /// ``` 151 #[inline] 152 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 153 where 154 S: Serializer, 155 { 156 self.creds.serialize(serializer) 157 } 158 } 159 /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential). 160 struct PrfCreds<'a>(&'a AllowedCredentials); 161 impl Serialize for PrfCreds<'_> { 162 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 163 where 164 S: Serializer, 165 { 166 serializer 167 .serialize_map(Some(self.0.prf_count)) 168 .and_then(|mut ser| { 169 self.0 170 .creds 171 .iter() 172 .try_fold((), |(), cred| { 173 cred.extension.prf.as_ref().map_or(Ok(()), |input| { 174 ser.serialize_entry(&cred.credential.id, input) 175 }) 176 }) 177 .and_then(|()| ser.end()) 178 }) 179 } 180 } 181 /// [`AuthenticationExtensionsPRFInputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfinputs). 182 struct PrfInputs<'a, 'b> { 183 /// [`eval`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-eval). 184 eval: Option<PrfInput<'a>>, 185 /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential). 186 eval_by_credential: PrfCreds<'b>, 187 } 188 impl Serialize for PrfInputs<'_, '_> { 189 #[expect( 190 clippy::arithmetic_side_effects, 191 reason = "comment explains how overflow is not possible" 192 )] 193 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 194 where 195 S: Serializer, 196 { 197 serializer 198 .serialize_struct( 199 "PrfInputs", 200 // The max is 1 + 1 = 2, so overflow is not an issue. 201 usize::from(self.eval.is_some()) 202 + usize::from(self.eval_by_credential.0.prf_count > 0), 203 ) 204 .and_then(|mut ser| { 205 self.eval 206 .map_or(Ok(()), |eval| ser.serialize_field("eval", &eval)) 207 .and_then(|()| { 208 if self.eval_by_credential.0.prf_count == 0 { 209 Ok(()) 210 } else { 211 ser.serialize_field("evalByCredential", &self.eval_by_credential) 212 } 213 }) 214 .and_then(|()| ser.end()) 215 }) 216 } 217 } 218 /// Serializes `self` to conform with 219 /// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson). 220 struct ExtensionHelper<'a, 'b> { 221 /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions). 222 extension: &'a Extension<'b>, 223 /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions). 224 /// 225 /// Some extensions contain records, so we need both this and above. 226 allow_credentials: &'a AllowedCredentials, 227 } 228 impl Serialize for ExtensionHelper<'_, '_> { 229 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 230 where 231 S: Serializer, 232 { 233 let ext_count = 234 usize::from(self.extension.prf.is_some() || self.allow_credentials.prf_count > 0); 235 serializer 236 .serialize_struct("Extension", ext_count) 237 .and_then(|mut ser| { 238 if ext_count == 0 { 239 Ok(()) 240 } else { 241 ser.serialize_field( 242 "prf", 243 &PrfInputs { 244 eval: self.extension.prf, 245 eval_by_credential: PrfCreds(self.allow_credentials), 246 }, 247 ) 248 } 249 .and_then(|()| ser.end()) 250 }) 251 } 252 } 253 /// Helper type that peforms the serialization for both [`DiscoverableAuthenticationClientState`] and 254 /// [`NonDiscoverableAuthenticationClientState`] and 255 struct AuthenticationClientState<'rp_id, 'prf, 'opt, 'cred>( 256 &'opt PublicKeyCredentialRequestOptions<'rp_id, 'prf>, 257 &'cred AllowedCredentials, 258 ); 259 impl Serialize for AuthenticationClientState<'_, '_, '_, '_> { 260 #[inline] 261 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 262 where 263 S: Serializer, 264 { 265 serializer 266 .serialize_struct("AuthenticationClientState", 9) 267 .and_then(|mut ser| { 268 ser.serialize_field("challenge", &self.0.challenge) 269 .and_then(|()| { 270 ser.serialize_field("timeout", &self.0.timeout) 271 .and_then(|()| { 272 ser.serialize_field("rpId", &self.0.rp_id).and_then(|()| { 273 ser.serialize_field("allowCredentials", &self.1).and_then( 274 |()| { 275 ser.serialize_field( 276 "userVerification", 277 &self.0.user_verification, 278 ) 279 .and_then( 280 |()| { 281 ser.serialize_field("hints", &self.0.hints) 282 .and_then(|()| { 283 ser.serialize_field( 284 "extensions", 285 &ExtensionHelper { 286 extension: &self.0.extensions, 287 allow_credentials: self.1, 288 }, 289 ) 290 .and_then(|()| ser.end()) 291 }) 292 }, 293 ) 294 }, 295 ) 296 }) 297 }) 298 }) 299 }) 300 } 301 } 302 impl Serialize for DiscoverableAuthenticationClientState<'_, '_> { 303 /// Serializes `self` to conform with 304 /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson). 305 /// 306 /// # Examples 307 /// 308 /// ``` 309 /// # use webauthn_rp::{ 310 /// # request::{ 311 /// # auth::{ 312 /// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension, 313 /// # PrfInput, PrfInputOwned, DiscoverableCredentialRequestOptions 314 /// # }, 315 /// # AsciiDomain, ExtensionReq, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, 316 /// # }, 317 /// # response::{AuthTransports, CredentialId}, 318 /// # }; 319 /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); 320 /// let mut options = DiscoverableCredentialRequestOptions::passkey(&rp_id); 321 /// options.0.hints = Hint::SecurityKey; 322 /// options.0.extensions = Extension { 323 /// prf: Some(PrfInput { 324 /// first: [0; 4].as_slice(), 325 /// second: None, 326 /// ext_info: ExtensionReq::Require, 327 /// }), 328 /// }; 329 /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap(); 330 /// let json = serde_json::json!({ 331 /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA", 332 /// "timeout":300000, 333 /// "rpId":"example.com", 334 /// "allowCredentials":[], 335 /// "userVerification":"required", 336 /// "hints":[ 337 /// "security-key" 338 /// ], 339 /// "extensions":{ 340 /// "prf":{ 341 /// "eval":{ 342 /// "first":"AAAAAA" 343 /// }, 344 /// } 345 /// } 346 /// }).to_string(); 347 /// // Since `Challenge`s are randomly generated, we don't know what it will be; thus 348 /// // we test the JSON string for everything except it. 349 /// # #[cfg(all(feature = "bin", feature = "custom"))] 350 /// assert_eq!(client_state.get(..14), json.get(..14)); 351 /// # #[cfg(all(feature = "bin", feature = "custom"))] 352 /// assert_eq!(client_state.get(36..), json.get(36..)); 353 /// # Ok::<_, webauthn_rp::AggErr>(()) 354 /// ``` 355 #[inline] 356 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 357 where 358 S: Serializer, 359 { 360 AuthenticationClientState(&self.0.0, &AllowedCredentials::with_capacity(0)) 361 .serialize(serializer) 362 } 363 } 364 impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_> { 365 /// Serializes `self` to conform with 366 /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson). 367 /// 368 /// # Examples 369 /// 370 /// ``` 371 /// # #[cfg(all(feature = "bin", feature = "custom"))] 372 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 373 /// # use webauthn_rp::{ 374 /// # request::{ 375 /// # auth::{ 376 /// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension, 377 /// # PrfInput, PrfInputOwned, NonDiscoverableCredentialRequestOptions 378 /// # }, 379 /// # AsciiDomain, ExtensionReq, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, 380 /// # }, 381 /// # response::{AuthTransports, CredentialId}, 382 /// # }; 383 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 384 /// /// from the database. 385 /// # #[cfg(all(feature = "bin", feature = "custom"))] 386 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 387 /// // ⋮ 388 /// # AuthTransports::decode(32) 389 /// } 390 /// let mut creds = AllowedCredentials::with_capacity(1); 391 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 392 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 393 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 394 /// # #[cfg(all(feature = "bin", feature = "custom"))] 395 /// let id = CredentialId::try_from(vec![0; 16])?; 396 /// # #[cfg(all(feature = "bin", feature = "custom"))] 397 /// let transports = get_transports((&id).into())?; 398 /// # #[cfg(all(feature = "bin", feature = "custom"))] 399 /// creds.push(AllowedCredential { 400 /// credential: PublicKeyCredentialDescriptor { id, transports }, 401 /// extension: CredentialSpecificExtension { 402 /// prf: Some(PrfInputOwned { 403 /// first: vec![2; 6], 404 /// second: Some(vec![3; 2]), 405 /// ext_info: ExtensionReq::Require, 406 /// }), 407 /// }, 408 /// }); 409 /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); 410 /// # #[cfg(all(feature = "bin", feature = "custom"))] 411 /// let mut options = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds)?; 412 /// # #[cfg(all(feature = "bin", feature = "custom"))] 413 /// let opts = options.options(); 414 /// # #[cfg(not(all(feature = "bin", feature = "custom")))] 415 /// # let mut opts = webauthn_rp::DiscoverableCredentialRequestOptions::passkey(&rp_id).0; 416 /// opts.hints = Hint::SecurityKey; 417 /// // This is actually useless since `CredentialSpecificExtension` takes priority 418 /// // when the client receives the payload. We set it for illustration purposes only. 419 /// // If `creds` contained an `AllowedCredential` that didn't set 420 /// // `CredentialSpecificExtension::prf`, then this would be used for it. 421 /// opts.extensions = Extension { 422 /// prf: Some(PrfInput { 423 /// first: [0; 4].as_slice(), 424 /// second: None, 425 /// ext_info: ExtensionReq::Require, 426 /// }), 427 /// }; 428 /// // Since we are requesting the PRF extension, we must require user verification; otherwise 429 /// // `NonDiscoverableCredentialRequestOptions::start_ceremony` would error. 430 /// opts.user_verification = UserVerificationRequirement::Required; 431 /// # #[cfg(all(feature = "bin", feature = "custom"))] 432 /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap(); 433 /// let json = serde_json::json!({ 434 /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA", 435 /// "timeout":300000, 436 /// "rpId":"example.com", 437 /// "allowCredentials":[ 438 /// { 439 /// "type":"public-key", 440 /// "id":"AAAAAAAAAAAAAAAAAAAAAA", 441 /// "transports":["usb"] 442 /// } 443 /// ], 444 /// "userVerification":"required", 445 /// "hints":[ 446 /// "security-key" 447 /// ], 448 /// "extensions":{ 449 /// "prf":{ 450 /// "eval":{ 451 /// "first":"AAAAAA" 452 /// }, 453 /// "evalByCredential":{ 454 /// "AAAAAAAAAAAAAAAAAAAAAA":{ 455 /// "first":"AgICAgIC", 456 /// "second":"AwM" 457 /// } 458 /// } 459 /// } 460 /// } 461 /// }).to_string(); 462 /// // Since `Challenge`s are randomly generated, we don't know what it will be; thus 463 /// // we test the JSON string for everything except it. 464 /// # #[cfg(all(feature = "bin", feature = "custom"))] 465 /// assert_eq!(client_state.get(..14), json.get(..14)); 466 /// # #[cfg(all(feature = "bin", feature = "custom"))] 467 /// assert_eq!(client_state.get(36..), json.get(36..)); 468 /// # Ok::<_, webauthn_rp::AggErr>(()) 469 /// ``` 470 #[inline] 471 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 472 where 473 S: Serializer, 474 { 475 AuthenticationClientState(&self.0.options, &self.0.allow_credentials).serialize(serializer) 476 } 477 }