@@ -1040,6 +1040,104 @@ mod tests {
10401040 assert ! ( ext. skip_serializing( ) ) ;
10411041 }
10421042
1043+ #[ test]
1044+ fn native_prf_request_serializes_extensions_at_0x04 ( ) {
1045+ // Regression guard for the original bug: the prf input used to vanish
1046+ // from the serialized request entirely.
1047+ let request = prf_request (
1048+ vec ! [ ] ,
1049+ Some ( PrfInputValue {
1050+ first : b"input" . to_vec ( ) ,
1051+ second : None ,
1052+ } ) ,
1053+ HashMap :: new ( ) ,
1054+ ) ;
1055+ let info = info_with_extensions ( & [ "prf" ] ) ;
1056+ let ctap2 = Ctap2GetAssertionRequest :: from_webauthn_request ( & request, & info) . unwrap ( ) ;
1057+
1058+ let bytes = crate :: proto:: ctap2:: cbor:: to_vec ( & ctap2) . unwrap ( ) ;
1059+ let parsed: BTreeMap < u64 , Value > = crate :: proto:: ctap2:: cbor:: from_slice ( & bytes) . unwrap ( ) ;
1060+ let Some ( Value :: Map ( extensions) ) = parsed. get ( & 0x04 ) else {
1061+ panic ! ( "extensions (0x04) missing from the wire" )
1062+ } ;
1063+ assert ! ( extensions. contains_key( & Value :: Text ( "prf" . to_string( ) ) ) ) ;
1064+ }
1065+
1066+ #[ test]
1067+ fn native_prf_composes_with_large_blob_write ( ) {
1068+ let mut request = prf_request (
1069+ vec ! [ make_credential( b"cred-1" ) ] ,
1070+ Some ( PrfInputValue {
1071+ first : b"input" . to_vec ( ) ,
1072+ second : None ,
1073+ } ) ,
1074+ HashMap :: new ( ) ,
1075+ ) ;
1076+ request. extensions . as_mut ( ) . unwrap ( ) . large_blob =
1077+ Some ( GetAssertionLargeBlobExtension :: Write ( b"blob" . to_vec ( ) ) ) ;
1078+ let info = Ctap2GetInfoResponse {
1079+ extensions : Some ( vec ! [ "prf" . to_string( ) ] ) ,
1080+ options : Some (
1081+ [ ( "largeBlobs" . to_string ( ) , true ) , ( "uv" . to_string ( ) , true ) ]
1082+ . into_iter ( )
1083+ . collect ( ) ,
1084+ ) ,
1085+ ..Default :: default ( )
1086+ } ;
1087+
1088+ let ctap2 = Ctap2GetAssertionRequest :: from_webauthn_request ( & request, & info) . unwrap ( ) ;
1089+ let ext = ctap2. extensions . as_ref ( ) . unwrap ( ) ;
1090+ assert ! ( ext. prf. is_some( ) ) ;
1091+ assert ! ( ext. hmac_or_prf. is_none( ) ) ;
1092+ assert_eq ! ( ext. large_blob_key, Some ( true ) ) ;
1093+ // The pinUvAuthToken is still negotiated for the lbw permission.
1094+ assert ! ( ctap2. needs_pin_uv_auth_token( & info) ) ;
1095+ assert ! ( ctap2. needs_shared_secret( & info) ) ;
1096+ }
1097+
1098+ #[ test]
1099+ fn native_prf_forwards_all_matching_eval_by_credential_entries ( ) {
1100+ let mut by_cred = HashMap :: new ( ) ;
1101+ for ( id, salt) in [
1102+ ( & b"cred-1" [ ..] , & b"salt-1" [ ..] ) ,
1103+ ( b"cred-2" , b"salt-2" ) ,
1104+ ( b"unknown-cred" , b"salt-3" ) ,
1105+ ] {
1106+ by_cred. insert (
1107+ base64_url:: encode ( id) ,
1108+ PrfInputValue {
1109+ first : salt. to_vec ( ) ,
1110+ second : None ,
1111+ } ,
1112+ ) ;
1113+ }
1114+ let request = prf_request (
1115+ vec ! [ make_credential( b"cred-1" ) , make_credential( b"cred-2" ) ] ,
1116+ None ,
1117+ by_cred,
1118+ ) ;
1119+ let info = info_with_extensions ( & [ "prf" ] ) ;
1120+
1121+ let ctap2 = Ctap2GetAssertionRequest :: from_webauthn_request ( & request, & info) . unwrap ( ) ;
1122+ let prf = ctap2. extensions . as_ref ( ) . unwrap ( ) . prf . as_ref ( ) . unwrap ( ) ;
1123+ let by_cred = prf. eval_by_credential . as_ref ( ) . unwrap ( ) ;
1124+ assert_eq ! ( by_cred. len( ) , 2 ) ;
1125+ assert_eq ! (
1126+ by_cred
1127+ . get( & ByteBuf :: from( b"cred-1" . to_vec( ) ) )
1128+ . unwrap( )
1129+ . first,
1130+ hashed_salt( b"salt-1" )
1131+ ) ;
1132+ assert_eq ! (
1133+ by_cred
1134+ . get( & ByteBuf :: from( b"cred-2" . to_vec( ) ) )
1135+ . unwrap( )
1136+ . first,
1137+ hashed_salt( b"salt-2" )
1138+ ) ;
1139+ }
1140+
10431141 #[ test]
10441142 fn native_prf_invalid_eval_by_credential_keys_are_syntax_errors ( ) {
10451143 let info = info_with_extensions ( & [ "prf" ] ) ;
@@ -1195,6 +1293,62 @@ mod tests {
11951293 let parsed =
11961294 parse_unsigned_prf ( & unsigned_prf_outputs ( & [ 0xAB ; 32 ] , Some ( & [ 0xCD ; 16 ] ) ) ) . unwrap ( ) ;
11971295 assert ! ( parsed. results. is_none( ) ) ;
1296+
1297+ // Non-bool enabled is ignored
1298+ let mut prf = BTreeMap :: new ( ) ;
1299+ prf. insert ( Value :: Text ( "enabled" . to_string ( ) ) , Value :: Integer ( 1 ) ) ;
1300+ let mut outputs = BTreeMap :: new ( ) ;
1301+ outputs. insert ( Value :: Text ( "prf" . to_string ( ) ) , Value :: Map ( prf) ) ;
1302+ let parsed = parse_unsigned_prf ( & outputs) . unwrap ( ) ;
1303+ assert ! ( parsed. enabled. is_none( ) ) ;
1304+ }
1305+
1306+ #[ test]
1307+ fn surfaces_passthrough_prf_results_in_client_extension_results ( ) {
1308+ use crate :: ops:: webauthn:: idl:: response:: { JsonFormat , WebAuthnIDLResponse } ;
1309+
1310+ // End-to-end GPM shape: results only in unsignedExtensionOutputs (0x08),
1311+ // no signed extensions, ED flag unset.
1312+ let mut auth_data = vec ! [ 0u8 ; 37 ] ;
1313+ auth_data[ 32 ] = AuthenticatorDataFlags :: USER_PRESENT . bits ( ) ;
1314+
1315+ let mut response: BTreeMap < u64 , Value > = BTreeMap :: new ( ) ;
1316+ response. insert ( 0x02 , Value :: Bytes ( auth_data) ) ;
1317+ response. insert ( 0x03 , Value :: Bytes ( vec ! [ 0xAAu8 ; 64 ] ) ) ;
1318+ response. insert (
1319+ 0x08 ,
1320+ Value :: Map ( unsigned_prf_outputs ( & [ 0xAB ; 32 ] , Some ( & [ 0xCD ; 32 ] ) ) ) ,
1321+ ) ;
1322+
1323+ let bytes = crate :: proto:: ctap2:: cbor:: to_vec ( & response) . unwrap ( ) ;
1324+ let parsed: Ctap2GetAssertionResponse =
1325+ crate :: proto:: ctap2:: cbor:: from_slice ( & bytes) . unwrap ( ) ;
1326+
1327+ let request = prf_request (
1328+ vec ! [ make_credential( b"cred-1" ) ] ,
1329+ Some ( PrfInputValue {
1330+ first : b"input" . to_vec ( ) ,
1331+ second : None ,
1332+ } ) ,
1333+ HashMap :: new ( ) ,
1334+ ) ;
1335+ let assertion = parsed. into_assertion_output ( & request, None ) ;
1336+ let json_str = assertion
1337+ . to_json_string ( & request, JsonFormat :: default ( ) )
1338+ . unwrap ( ) ;
1339+ let json: serde_json:: Value = serde_json:: from_str ( & json_str) . unwrap ( ) ;
1340+
1341+ let prf = & json[ "clientExtensionResults" ] [ "prf" ] ;
1342+ assert_eq ! (
1343+ prf[ "results" ] [ "first" ] ,
1344+ serde_json:: json!( base64_url:: encode( & [ 0xAB ; 32 ] ) )
1345+ ) ;
1346+ assert_eq ! (
1347+ prf[ "results" ] [ "second" ] ,
1348+ serde_json:: json!( base64_url:: encode( & [ 0xCD ; 32 ] ) )
1349+ ) ;
1350+ // Exactly one prf member: the raw passthrough must not emit a duplicate.
1351+ assert_eq ! ( json_str. matches( "\" prf\" " ) . count( ) , 1 ) ;
11981352 }
11991353
12001354 #[ test]
0 commit comments