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