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 , PRFValue , 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| PRFValue {
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,40 @@ 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 < PRFValue > ,
473+ }
474+
475+ fn deserialize_prf_eval < ' de , D > ( deserializer : D ) -> Result < Option < PRFValue > , D :: Error >
476+ where
477+ D : Deserializer < ' de > ,
478+ {
479+ let Some ( json) = Option :: < PrfValuesJson > :: deserialize ( deserializer) ? else {
480+ return Ok ( None ) ;
481+ } ;
482+ let first: [ u8 ; 32 ] = json. first . as_slice ( ) . try_into ( ) . map_err ( |_| {
483+ serde:: de:: Error :: invalid_length (
484+ json. first . as_slice ( ) . len ( ) ,
485+ & "32 bytes (base64url-decoded)" ,
486+ )
487+ } ) ?;
488+ let second = match json. second {
489+ Some ( s) => Some ( s. as_slice ( ) . try_into ( ) . map_err ( |_| {
490+ serde:: de:: Error :: invalid_length ( s. as_slice ( ) . len ( ) , & "32 bytes (base64url-decoded)" )
491+ } ) ?) ,
492+ None => None ,
493+ } ;
494+ Ok ( Some ( PRFValue { first, second } ) )
458495}
459496
460497#[ derive( Debug , Default , Clone , Serialize , PartialEq ) ]
461498pub struct MakeCredentialPrfOutput {
462499 #[ serde( skip_serializing_if = "Option::is_none" ) ]
463500 pub enabled : Option < bool > ,
501+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
502+ pub results : Option < PRFValue > ,
464503}
465504
466505#[ derive( Debug , Clone , Deserialize , PartialEq ) ]
@@ -772,18 +811,48 @@ mod tests {
772811 #[ test]
773812 fn test_request_from_json_prf_extension ( ) {
774813 let request_origin: RequestOrigin = "https://example.org" . parse ( ) . unwrap ( ) ;
775- let req_json = json_field_add (
776- REQUEST_BASE_JSON ,
777- "extensions" ,
778- r#"{"prf": {"eval": {"first": "second"}}}"# ,
779- ) ;
814+ let first = base64_url:: encode ( & [ 1u8 ; 32 ] ) ;
815+ let second = base64_url:: encode ( & [ 2u8 ; 32 ] ) ;
816+ let ext = format ! ( r#"{{"prf": {{"eval": {{"first": "{first}", "second": "{second}"}}}}}}"# ) ;
817+ let req_json = json_field_add ( REQUEST_BASE_JSON , "extensions" , & ext) ;
780818
781819 let req: MakeCredentialRequest =
782820 MakeCredentialRequest :: from_json ( & request_origin, & MockPublicSuffixList , & req_json)
783821 . unwrap ( ) ;
822+ let prf = req
823+ . extensions
824+ . as_ref ( )
825+ . and_then ( |e| e. prf . as_ref ( ) )
826+ . and_then ( |p| p. eval . as_ref ( ) )
827+ . expect ( "prf.eval parsed" ) ;
828+ assert_eq ! ( prf. first, [ 1u8 ; 32 ] ) ;
829+ assert_eq ! ( prf. second, Some ( [ 2u8 ; 32 ] ) ) ;
830+ }
831+
832+ #[ test]
833+ fn test_request_from_json_prf_extension_empty ( ) {
834+ let request_origin: RequestOrigin = "https://example.org" . parse ( ) . unwrap ( ) ;
835+ let req_json = json_field_add ( REQUEST_BASE_JSON , "extensions" , r#"{"prf": {}}"# ) ;
836+
837+ let req: MakeCredentialRequest =
838+ MakeCredentialRequest :: from_json ( & request_origin, & MockPublicSuffixList , & req_json)
839+ . unwrap ( ) ;
840+ let prf = req. extensions . unwrap ( ) . prf . unwrap ( ) ;
841+ assert ! ( prf. eval. is_none( ) ) ;
842+ }
843+
844+ #[ test]
845+ fn test_request_from_json_prf_extension_invalid_length ( ) {
846+ let request_origin: RequestOrigin = "https://example.org" . parse ( ) . unwrap ( ) ;
847+ let short = base64_url:: encode ( & [ 0u8 ; 16 ] ) ;
848+ let ext = format ! ( r#"{{"prf": {{"eval": {{"first": "{short}"}}}}}}"# ) ;
849+ let req_json = json_field_add ( REQUEST_BASE_JSON , "extensions" , & ext) ;
850+
851+ let res =
852+ MakeCredentialRequest :: from_json ( & request_origin, & MockPublicSuffixList , & req_json) ;
784853 assert ! ( matches!(
785- req . extensions ,
786- Some ( MakeCredentialsRequestExtensions { prf : Some ( _) , .. } )
854+ res ,
855+ Err ( MakeCredentialRequestParsingError :: EncodingError ( _) )
787856 ) ) ;
788857 }
789858
@@ -1105,6 +1174,7 @@ mod tests {
11051174 large_blob : None ,
11061175 prf : Some ( MakeCredentialPrfOutput {
11071176 enabled : Some ( true ) ,
1177+ results : None ,
11081178 } ) ,
11091179 } ;
11101180
0 commit comments