ser.rs (17587B)
1 use super::{ 2 super::super::BASE64URL_NOPAD_ENC, AllowedCredential, AllowedCredentials, 3 AuthenticationClientState, Extension, PrfInput, PrfInputOwned, 4 }; 5 use serde::ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer}; 6 impl Serialize for PrfInput<'_> { 7 /// Serializes `self` to conform with 8 /// [`AuthenticationExtensionsPRFValues`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfvalues). 9 /// 10 /// # Examples 11 /// 12 /// ``` 13 /// # use webauthn_rp::request::{auth::PrfInput, ExtensionReq}; 14 /// assert_eq!( 15 /// serde_json::to_string(&PrfInput { 16 /// first: [0; 4].as_slice(), 17 /// second: Some([2; 1].as_slice()), 18 /// ext_info: ExtensionReq::Require 19 /// })?, 20 /// r#"{"first":"AAAAAA","second":"Ag"}"# 21 /// ); 22 /// # Ok::<_, serde_json::Error>(()) 23 /// ``` 24 #[expect( 25 clippy::arithmetic_side_effects, 26 reason = "comment justifies how overflow is not possible" 27 )] 28 #[inline] 29 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 30 where 31 S: Serializer, 32 { 33 serializer 34 // The max value is 1 + 1 = 2, so overflow is not an issue. 35 .serialize_struct("PrfInput", 1 + usize::from(self.second.is_some())) 36 .and_then(|mut ser| { 37 ser.serialize_field("first", BASE64URL_NOPAD_ENC.encode(self.first).as_str()) 38 .and_then(|()| { 39 self.second 40 .as_ref() 41 .map_or(Ok(()), |second| { 42 ser.serialize_field( 43 "second", 44 BASE64URL_NOPAD_ENC.encode(second).as_str(), 45 ) 46 }) 47 .and_then(|()| ser.end()) 48 }) 49 }) 50 } 51 } 52 impl Serialize for PrfInputOwned { 53 /// See [`PrfInput::serialize`] 54 #[inline] 55 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 56 where 57 S: Serializer, 58 { 59 PrfInput { 60 first: self.first.as_slice(), 61 second: self.second.as_deref(), 62 ext_info: self.ext_info, 63 } 64 .serialize(serializer) 65 } 66 } 67 impl Serialize for AllowedCredential { 68 /// Serializes `self` to conform with 69 /// [`PublicKeyCredentialDescriptorJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptorjson). 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::AllowedCredential, PublicKeyCredentialDescriptor}, 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 /// # #[cfg(all(feature = "bin", feature = "custom"))] 95 /// assert_eq!( 96 /// serde_json::to_string(&AllowedCredential::from(PublicKeyCredentialDescriptor { 97 /// id, 98 /// transports 99 /// })).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.credential.serialize(serializer) 110 } 111 } 112 impl Serialize for AllowedCredentials { 113 /// Serializes `self` to conform with 114 /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-allowcredentials). 115 /// 116 /// # Examples 117 /// 118 /// ``` 119 /// # #[cfg(all(feature = "bin", feature = "custom"))] 120 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 121 /// # use webauthn_rp::{ 122 /// # request::{auth::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials}, 123 /// # response::{AuthTransports, CredentialId}, 124 /// # }; 125 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 126 /// /// from the database. 127 /// # #[cfg(all(feature = "bin", feature = "custom"))] 128 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 129 /// // ⋮ 130 /// # AuthTransports::decode(32) 131 /// } 132 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 133 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 134 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 135 /// # #[cfg(all(feature = "bin", feature = "custom"))] 136 /// let id = CredentialId::try_from(vec![0; 16])?; 137 /// # #[cfg(all(feature = "bin", feature = "custom"))] 138 /// let transports = get_transports((&id).into())?; 139 /// let mut creds = AllowedCredentials::with_capacity(1); 140 /// # #[cfg(all(feature = "bin", feature = "custom"))] 141 /// creds.push(PublicKeyCredentialDescriptor { id, transports }.into()); 142 /// # #[cfg(all(feature = "bin", feature = "custom"))] 143 /// assert_eq!( 144 /// serde_json::to_string(&creds).unwrap(), 145 /// r#"[{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}]"# 146 /// ); 147 /// # Ok::<_, webauthn_rp::AggErr>(()) 148 /// ``` 149 #[inline] 150 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 151 where 152 S: Serializer, 153 { 154 self.creds.serialize(serializer) 155 } 156 } 157 /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential). 158 struct PrfCreds<'a>(&'a AllowedCredentials); 159 impl Serialize for PrfCreds<'_> { 160 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 161 where 162 S: Serializer, 163 { 164 serializer 165 .serialize_map(Some(self.0.prf_count)) 166 .and_then(|mut ser| { 167 self.0 168 .creds 169 .iter() 170 .try_fold((), |(), cred| { 171 cred.extension.prf.as_ref().map_or(Ok(()), |input| { 172 ser.serialize_entry(&cred.credential.id, input) 173 }) 174 }) 175 .and_then(|()| ser.end()) 176 }) 177 } 178 } 179 /// [`AuthenticationExtensionsPRFInputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfinputs). 180 struct PrfInputs<'a, 'b> { 181 /// [`eval`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-eval). 182 eval: Option<PrfInput<'a>>, 183 /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential). 184 eval_by_credential: PrfCreds<'b>, 185 } 186 impl Serialize for PrfInputs<'_, '_> { 187 #[expect( 188 clippy::arithmetic_side_effects, 189 reason = "comment explains how overflow is not possible" 190 )] 191 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 192 where 193 S: Serializer, 194 { 195 serializer 196 .serialize_struct( 197 "PrfInputs", 198 // The max is 1 + 1 = 2, so overflow is not an issue. 199 usize::from(self.eval.is_some()) 200 + usize::from(self.eval_by_credential.0.prf_count > 0), 201 ) 202 .and_then(|mut ser| { 203 self.eval 204 .map_or(Ok(()), |eval| ser.serialize_field("eval", &eval)) 205 .and_then(|()| { 206 if self.eval_by_credential.0.prf_count == 0 { 207 Ok(()) 208 } else { 209 ser.serialize_field("evalByCredential", &self.eval_by_credential) 210 } 211 }) 212 .and_then(|()| ser.end()) 213 }) 214 } 215 } 216 /// Serializes `self` to conform with 217 /// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson). 218 struct ExtensionHelper<'a, 'b> { 219 /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions). 220 extension: &'a Extension<'b>, 221 /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions). 222 /// 223 /// Some extensions contain records, so we need both this and above. 224 allow_credentials: &'a AllowedCredentials, 225 } 226 impl Serialize for ExtensionHelper<'_, '_> { 227 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 228 where 229 S: Serializer, 230 { 231 let ext_count = 232 usize::from(self.extension.prf.is_some() || self.allow_credentials.prf_count > 0); 233 serializer 234 .serialize_struct("Extension", ext_count) 235 .and_then(|mut ser| { 236 if ext_count == 0 { 237 Ok(()) 238 } else { 239 ser.serialize_field( 240 "prf", 241 &PrfInputs { 242 eval: self.extension.prf, 243 eval_by_credential: PrfCreds(self.allow_credentials), 244 }, 245 ) 246 } 247 .and_then(|()| ser.end()) 248 }) 249 } 250 } 251 impl Serialize for AuthenticationClientState<'_, '_> { 252 /// Serializes `self` to conform with 253 /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson). 254 /// 255 /// # Examples 256 /// 257 /// ``` 258 /// # #[cfg(all(feature = "bin", feature = "custom"))] 259 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 260 /// # use webauthn_rp::{ 261 /// # request::{ 262 /// # auth::{ 263 /// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension, 264 /// # PrfInput, PrfInputOwned, PublicKeyCredentialRequestOptions 265 /// # }, 266 /// # AsciiDomain, ExtensionReq, Hint, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, 267 /// # }, 268 /// # response::{AuthTransports, CredentialId}, 269 /// # }; 270 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 271 /// /// from the database. 272 /// # #[cfg(all(feature = "bin", feature = "custom"))] 273 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 274 /// // ⋮ 275 /// # AuthTransports::decode(32) 276 /// } 277 /// let mut creds = AllowedCredentials::with_capacity(1); 278 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 279 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 280 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 281 /// # #[cfg(all(feature = "bin", feature = "custom"))] 282 /// let id = CredentialId::try_from(vec![0; 16])?; 283 /// # #[cfg(all(feature = "bin", feature = "custom"))] 284 /// let transports = get_transports((&id).into())?; 285 /// # #[cfg(all(feature = "bin", feature = "custom"))] 286 /// creds.push(AllowedCredential { 287 /// credential: PublicKeyCredentialDescriptor { id, transports }, 288 /// extension: CredentialSpecificExtension { 289 /// prf: Some(PrfInputOwned { 290 /// first: vec![2; 6], 291 /// second: Some(vec![3; 2]), 292 /// ext_info: ExtensionReq::Require, 293 /// }), 294 /// }, 295 /// }); 296 /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); 297 /// # #[cfg(all(feature = "bin", feature = "custom"))] 298 /// let mut options = PublicKeyCredentialRequestOptions::second_factor(&rp_id, creds)?; 299 /// # #[cfg(not(all(feature = "bin", feature = "custom")))] 300 /// # let mut options = PublicKeyCredentialRequestOptions::passkey(&rp_id); 301 /// options.hints = Hint::SecurityKey; 302 /// // This is actually useless since `CredentialSpecificExtension` takes priority 303 /// // when the client receives the payload. We set it for illustration purposes only. 304 /// // If `creds` contained an `AllowedCredential` that didn't set 305 /// // `CredentialSpecificExtension::prf`, then this would be used for it. 306 /// options.extensions = Extension { 307 /// prf: Some(PrfInput { 308 /// first: [0; 4].as_slice(), 309 /// second: None, 310 /// ext_info: ExtensionReq::Require, 311 /// }), 312 /// }; 313 /// // Since we are requesting the PRF extension, we must require user verification; otherwise 314 /// // `PublicKeyCredentialRequestOptions::start_ceremony` would error. 315 /// options.user_verification = UserVerificationRequirement::Required; 316 /// # #[cfg(all(feature = "bin", feature = "custom"))] 317 /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap(); 318 /// let json = serde_json::json!({ 319 /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA", 320 /// "timeout":300000, 321 /// "rpId":"example.com", 322 /// "allowCredentials":[ 323 /// { 324 /// "type":"public-key", 325 /// "id":"AAAAAAAAAAAAAAAAAAAAAA", 326 /// "transports":["usb"] 327 /// } 328 /// ], 329 /// "userVerification":"required", 330 /// "hints":[ 331 /// "security-key" 332 /// ], 333 /// "extensions":{ 334 /// "prf":{ 335 /// "eval":{ 336 /// "first":"AAAAAA" 337 /// }, 338 /// "evalByCredential":{ 339 /// "AAAAAAAAAAAAAAAAAAAAAA":{ 340 /// "first":"AgICAgIC", 341 /// "second":"AwM" 342 /// } 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 serializer 361 .serialize_struct("AuthenticationClientState", 9) 362 .and_then(|mut ser| { 363 ser.serialize_field("challenge", &self.0.challenge) 364 .and_then(|()| { 365 ser.serialize_field("timeout", &self.0.timeout) 366 .and_then(|()| { 367 ser.serialize_field("rpId", &self.0.rp_id).and_then(|()| { 368 ser.serialize_field( 369 "allowCredentials", 370 &self.0.allow_credentials, 371 ) 372 .and_then(|()| { 373 ser.serialize_field( 374 "userVerification", 375 &self.0.user_verification, 376 ) 377 .and_then(|()| { 378 ser.serialize_field("hints", &self.0.hints).and_then( 379 |()| { 380 ser.serialize_field( 381 "extensions", 382 &ExtensionHelper { 383 extension: &self.0.extensions, 384 allow_credentials: &self 385 .0 386 .allow_credentials, 387 }, 388 ) 389 .and_then(|()| ser.end()) 390 }, 391 ) 392 }) 393 }) 394 }) 395 }) 396 }) 397 }) 398 } 399 }