11use std:: time:: Duration ;
22
33use ctap_types:: ctap2:: credential_management:: CredentialProtectionPolicy as Ctap2CredentialProtectionPolicy ;
4- use serde:: { Deserialize , Serialize } ;
5- use serde_json:: { self , Value as JsonValue } ;
4+ use serde:: { Deserialize , Deserializer , Serialize } ;
65use sha2:: { Digest , Sha256 } ;
76use tracing:: { debug, instrument, trace} ;
87
@@ -12,16 +11,17 @@ use crate::{
1211 client_data:: ClientData ,
1312 idl:: {
1413 create:: PublicKeyCredentialCreationOptionsJSON ,
14+ get:: PrfValuesJson ,
1515 origin:: is_registrable_domain_suffix_or_equal,
1616 response:: {
1717 AuthenticationExtensionsClientOutputsJSON , AuthenticatorAttestationResponseJSON ,
18- CredentialPropertiesOutputJSON , LargeBlobOutputJSON , PRFOutputJSON ,
18+ CredentialPropertiesOutputJSON , LargeBlobOutputJSON , PRFOutputJSON , PRFValuesJSON ,
1919 RegistrationResponseJSON , ResponseSerializationError , WebAuthnIDLResponse ,
2020 } ,
2121 Base64UrlString , FromIdlModel , JsonError , WebAuthnIDL ,
2222 } ,
2323 psl:: PublicSuffixList ,
24- Operation , RelyingPartyId , RequestOrigin ,
24+ Operation , PrfInputValue , PrfOutputValue , RelyingPartyId , RequestOrigin ,
2525 } ,
2626 proto:: {
2727 ctap1:: { Ctap1RegisteredKey , Ctap1Version } ,
@@ -32,6 +32,7 @@ use crate::{
3232 Ctap2PublicKeyCredentialUserEntity ,
3333 } ,
3434 } ,
35+ transport:: AuthTokenData ,
3536} ;
3637
3738use super :: timeout:: DEFAULT_TIMEOUT ;
@@ -176,11 +177,16 @@ impl MakeCredentialResponse {
176177 } ) ;
177178 }
178179
179- // PRF extension
180180 if let Some ( prf) = & unsigned_ext. prf {
181181 results. prf = Some ( PRFOutputJSON {
182182 enabled : prf. enabled ,
183- results : None ,
183+ results : prf. results . as_ref ( ) . map ( |v| PRFValuesJSON {
184+ first : Base64UrlString :: from ( v. first . as_slice ( ) ) ,
185+ second : v
186+ . second
187+ . as_ref ( )
188+ . map ( |s| Base64UrlString :: from ( s. as_slice ( ) ) ) ,
189+ } ) ,
184190 } ) ;
185191 }
186192
@@ -216,19 +222,31 @@ impl MakeCredentialsResponseUnsignedExtensions {
216222 signed_extensions : & Option < Ctap2MakeCredentialsResponseExtensions > ,
217223 request : & MakeCredentialRequest ,
218224 info : Option < & Ctap2GetInfoResponse > ,
225+ auth_data : Option < & AuthTokenData > ,
219226 ) -> MakeCredentialsResponseUnsignedExtensions {
220227 let mut hmac_create_secret = None ;
221228 let mut prf = None ;
222229 if let Some ( signed_extensions) = signed_extensions {
223230 if let Some ( incoming_ext) = & request. extensions {
224- // hmacCreateSecret and prf can both be requested and returned independently.
225- // Both map to the same underlying CTAP2 hmac-secret extension.
226231 if incoming_ext. hmac_create_secret . is_some ( ) {
227232 hmac_create_secret = signed_extensions. hmac_secret ;
228233 }
229234 if incoming_ext. prf . is_some ( ) {
235+ let results = signed_extensions
236+ . hmac_secret_mc
237+ . as_ref ( )
238+ . zip ( auth_data)
239+ . and_then ( |( out, auth) | {
240+ let uv_proto = auth. protocol_version . create_protocol_object ( ) ;
241+ out. decrypt_output ( & auth. shared_secret , uv_proto. as_ref ( ) )
242+ } )
243+ . map ( |decrypted| PrfOutputValue {
244+ first : decrypted. output1 ,
245+ second : decrypted. output2 ,
246+ } ) ;
230247 prf = Some ( MakeCredentialPrfOutput {
231248 enabled : signed_extensions. hmac_secret ,
249+ results,
232250 } ) ;
233251 }
234252 }
@@ -448,19 +466,32 @@ impl WebAuthnIDL<MakeCredentialRequestParsingError> for MakeCredentialRequest {
448466 type IdlModel = PublicKeyCredentialCreationOptionsJSON ;
449467}
450468
451- #[ derive( Debug , Clone , Deserialize , PartialEq ) ]
469+ #[ derive( Debug , Default , Clone , Deserialize , PartialEq ) ]
452470pub struct MakeCredentialPrfInput {
453- /// The `eval` field is parsed but not used during credential creation.
454- /// PRF evaluation only occurs during assertion (getAssertion), not registration.
455- /// We parse it here to accept valid WebAuthn JSON input without errors.
456- #[ serde( rename = "eval" ) ]
457- pub _eval : Option < JsonValue > ,
471+ #[ serde( default , deserialize_with = "deserialize_prf_eval" ) ]
472+ pub eval : Option < PrfInputValue > ,
473+ }
474+
475+ fn deserialize_prf_eval < ' de , D > ( deserializer : D ) -> Result < Option < PrfInputValue > , D :: Error >
476+ where
477+ D : Deserializer < ' de > ,
478+ {
479+ let Some ( json) = Option :: < PrfValuesJson > :: deserialize ( deserializer) ? else {
480+ return Ok ( None ) ;
481+ } ;
482+ // WebAuthn L3 §10.1.4: PRF salt inputs are BufferSources of any length.
483+ Ok ( Some ( PrfInputValue {
484+ first : json. first . as_slice ( ) . to_vec ( ) ,
485+ second : json. second . map ( |s| s. as_slice ( ) . to_vec ( ) ) ,
486+ } ) )
458487}
459488
460489#[ derive( Debug , Default , Clone , Serialize , PartialEq ) ]
461490pub struct MakeCredentialPrfOutput {
462491 #[ serde( skip_serializing_if = "Option::is_none" ) ]
463492 pub enabled : Option < bool > ,
493+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
494+ pub results : Option < PrfOutputValue > ,
464495}
465496
466497#[ derive( Debug , Clone , Deserialize , PartialEq ) ]
@@ -798,19 +829,55 @@ mod tests {
798829 #[ test]
799830 fn test_request_from_json_prf_extension ( ) {
800831 let request_origin: RequestOrigin = "https://example.org" . parse ( ) . unwrap ( ) ;
801- let req_json = json_field_add (
802- REQUEST_BASE_JSON ,
803- "extensions" ,
804- r#"{"prf": {"eval": {"first": "second"}}}"# ,
805- ) ;
832+ let first = base64_url:: encode ( & [ 1u8 ; 32 ] ) ;
833+ let second = base64_url:: encode ( & [ 2u8 ; 32 ] ) ;
834+ let ext = format ! ( r#"{{"prf": {{"eval": {{"first": "{first}", "second": "{second}"}}}}}}"# ) ;
835+ let req_json = json_field_add ( REQUEST_BASE_JSON , "extensions" , & ext) ;
806836
807837 let req: MakeCredentialRequest =
808838 MakeCredentialRequest :: from_json ( & request_origin, & MockPublicSuffixList , & req_json)
809839 . unwrap ( ) ;
810- assert ! ( matches!(
811- req. extensions,
812- Some ( MakeCredentialsRequestExtensions { prf: Some ( _) , .. } )
813- ) ) ;
840+ let prf = req
841+ . extensions
842+ . as_ref ( )
843+ . and_then ( |e| e. prf . as_ref ( ) )
844+ . and_then ( |p| p. eval . as_ref ( ) )
845+ . expect ( "prf.eval parsed" ) ;
846+ assert_eq ! ( prf. first, vec![ 1u8 ; 32 ] ) ;
847+ assert_eq ! ( prf. second, Some ( vec![ 2u8 ; 32 ] ) ) ;
848+ }
849+
850+ #[ test]
851+ fn test_request_from_json_prf_extension_empty ( ) {
852+ let request_origin: RequestOrigin = "https://example.org" . parse ( ) . unwrap ( ) ;
853+ let req_json = json_field_add ( REQUEST_BASE_JSON , "extensions" , r#"{"prf": {}}"# ) ;
854+
855+ let req: MakeCredentialRequest =
856+ MakeCredentialRequest :: from_json ( & request_origin, & MockPublicSuffixList , & req_json)
857+ . unwrap ( ) ;
858+ let prf = req. extensions . unwrap ( ) . prf . unwrap ( ) ;
859+ assert ! ( prf. eval. is_none( ) ) ;
860+ }
861+
862+ #[ test]
863+ fn test_request_from_json_prf_extension_short_input ( ) {
864+ // WebAuthn L3 §10.1.4: PRF salt inputs are BufferSources of any length.
865+ let request_origin: RequestOrigin = "https://example.org" . parse ( ) . unwrap ( ) ;
866+ let short = base64_url:: encode ( & [ 0u8 ; 16 ] ) ;
867+ let ext = format ! ( r#"{{"prf": {{"eval": {{"first": "{short}"}}}}}}"# ) ;
868+ let req_json = json_field_add ( REQUEST_BASE_JSON , "extensions" , & ext) ;
869+
870+ let req: MakeCredentialRequest =
871+ MakeCredentialRequest :: from_json ( & request_origin, & MockPublicSuffixList , & req_json)
872+ . unwrap ( ) ;
873+ let prf = req
874+ . extensions
875+ . as_ref ( )
876+ . and_then ( |e| e. prf . as_ref ( ) )
877+ . and_then ( |p| p. eval . as_ref ( ) )
878+ . expect ( "prf.eval parsed" ) ;
879+ assert_eq ! ( prf. first, vec![ 0u8 ; 16 ] ) ;
880+ assert ! ( prf. second. is_none( ) ) ;
814881 }
815882
816883 #[ test]
@@ -1131,6 +1198,7 @@ mod tests {
11311198 large_blob : None ,
11321199 prf : Some ( MakeCredentialPrfOutput {
11331200 enabled : Some ( true ) ,
1201+ results : None ,
11341202 } ) ,
11351203 } ;
11361204
0 commit comments