@@ -3,7 +3,7 @@ use std::time::Duration;
33
44use libwebauthn:: ops:: webauthn:: {
55 GetAssertionRequest , GetAssertionRequestExtensions , MakeCredentialPrfInput ,
6- MakeCredentialPrfOutput , MakeCredentialsRequestExtensions , PRFValue , PrfInput ,
6+ MakeCredentialPrfOutput , MakeCredentialsRequestExtensions , PrfInput , PrfInputValue ,
77} ;
88use libwebauthn:: pin:: PinManagement ;
99use libwebauthn:: proto:: ctap2:: { Ctap2PinUvAuthProtocol , Ctap2PublicKeyCredentialDescriptor } ;
@@ -189,8 +189,8 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
189189 let mut eval_by_credential = HashMap :: new ( ) ;
190190 eval_by_credential. insert (
191191 base64_url:: encode ( & credential. id ) ,
192- PRFValue {
193- first : [ 1 ; 32 ] ,
192+ PrfInputValue {
193+ first : vec ! [ 1 ; 32 ] ,
194194 second : None ,
195195 } ,
196196 ) ;
@@ -210,16 +210,16 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
210210 . await ;
211211
212212 // Test 2: eval and eval_with_credential with cred_id we got
213- let eval = Some ( PRFValue {
214- first : [ 2 ; 32 ] ,
213+ let eval = Some ( PrfInputValue {
214+ first : vec ! [ 2 ; 32 ] ,
215215 second : None ,
216216 } ) ;
217217
218218 let mut eval_by_credential = HashMap :: new ( ) ;
219219 eval_by_credential. insert (
220220 base64_url:: encode ( & credential. id ) ,
221- PRFValue {
222- first : [ 1 ; 32 ] ,
221+ PrfInputValue {
222+ first : vec ! [ 1 ; 32 ] ,
223223 second : None ,
224224 } ,
225225 ) ;
@@ -239,8 +239,8 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
239239 . await ;
240240
241241 // Test 3: eval only
242- let eval = Some ( PRFValue {
243- first : [ 1 ; 32 ] ,
242+ let eval = Some ( PrfInputValue {
243+ first : vec ! [ 1 ; 32 ] ,
244244 second : None ,
245245 } ) ;
246246
@@ -261,38 +261,38 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
261261 . await ;
262262
263263 // Test 4: eval and a full list of eval_by_credential
264- let eval = Some ( PRFValue {
265- first : [ 2 ; 32 ] ,
264+ let eval = Some ( PrfInputValue {
265+ first : vec ! [ 2 ; 32 ] ,
266266 second : None ,
267267 } ) ;
268268
269269 let mut eval_by_credential = HashMap :: new ( ) ;
270270 eval_by_credential. insert (
271271 base64_url:: encode ( & [ 5 ; 54 ] ) ,
272- PRFValue {
273- first : [ 5 ; 32 ] ,
272+ PrfInputValue {
273+ first : vec ! [ 5 ; 32 ] ,
274274 second : None ,
275275 } ,
276276 ) ;
277277 eval_by_credential. insert (
278278 base64_url:: encode ( & [ 7 ; 54 ] ) ,
279- PRFValue {
280- first : [ 7 ; 32 ] ,
281- second : Some ( [ 7 ; 32 ] ) ,
279+ PrfInputValue {
280+ first : vec ! [ 7 ; 32 ] ,
281+ second : Some ( vec ! [ 7 ; 32 ] ) ,
282282 } ,
283283 ) ;
284284 eval_by_credential. insert (
285285 base64_url:: encode ( & [ 8 ; 54 ] ) ,
286- PRFValue {
287- first : [ 8 ; 32 ] ,
288- second : Some ( [ 8 ; 32 ] ) ,
286+ PrfInputValue {
287+ first : vec ! [ 8 ; 32 ] ,
288+ second : Some ( vec ! [ 8 ; 32 ] ) ,
289289 } ,
290290 ) ;
291291 eval_by_credential. insert (
292292 base64_url:: encode ( & credential. id ) ,
293- PRFValue {
294- first : [ 1 ; 32 ] ,
295- second : Some ( [ 7 ; 32 ] ) ,
293+ PrfInputValue {
294+ first : vec ! [ 1 ; 32 ] ,
295+ second : Some ( vec ! [ 7 ; 32 ] ) ,
296296 } ,
297297 ) ;
298298 let prf = PrfInput {
@@ -311,31 +311,31 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
311311 . await ;
312312
313313 // Test 5: eval and non-fitting list of eval_by_credential
314- let eval = Some ( PRFValue {
315- first : [ 1 ; 32 ] ,
314+ let eval = Some ( PrfInputValue {
315+ first : vec ! [ 1 ; 32 ] ,
316316 second : None ,
317317 } ) ;
318318
319319 let mut eval_by_credential = HashMap :: new ( ) ;
320320 eval_by_credential. insert (
321321 base64_url:: encode ( & [ 5 ; 54 ] ) ,
322- PRFValue {
323- first : [ 5 ; 32 ] ,
322+ PrfInputValue {
323+ first : vec ! [ 5 ; 32 ] ,
324324 second : None ,
325325 } ,
326326 ) ;
327327 eval_by_credential. insert (
328328 base64_url:: encode ( & [ 7 ; 54 ] ) ,
329- PRFValue {
330- first : [ 7 ; 32 ] ,
331- second : Some ( [ 7 ; 32 ] ) ,
329+ PrfInputValue {
330+ first : vec ! [ 7 ; 32 ] ,
331+ second : Some ( vec ! [ 7 ; 32 ] ) ,
332332 } ,
333333 ) ;
334334 eval_by_credential. insert (
335335 base64_url:: encode ( & [ 8 ; 54 ] ) ,
336- PRFValue {
337- first : [ 8 ; 32 ] ,
338- second : Some ( [ 8 ; 32 ] ) ,
336+ PrfInputValue {
337+ first : vec ! [ 8 ; 32 ] ,
338+ second : Some ( vec ! [ 8 ; 32 ] ) ,
339339 } ,
340340 ) ;
341341 let prf = PrfInput {
@@ -359,23 +359,23 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
359359 let mut eval_by_credential = HashMap :: new ( ) ;
360360 eval_by_credential. insert (
361361 base64_url:: encode ( & [ 5 ; 54 ] ) ,
362- PRFValue {
363- first : [ 5 ; 32 ] ,
362+ PrfInputValue {
363+ first : vec ! [ 5 ; 32 ] ,
364364 second : None ,
365365 } ,
366366 ) ;
367367 eval_by_credential. insert (
368368 base64_url:: encode ( & [ 7 ; 54 ] ) ,
369- PRFValue {
370- first : [ 7 ; 32 ] ,
371- second : Some ( [ 7 ; 32 ] ) ,
369+ PrfInputValue {
370+ first : vec ! [ 7 ; 32 ] ,
371+ second : Some ( vec ! [ 7 ; 32 ] ) ,
372372 } ,
373373 ) ;
374374 eval_by_credential. insert (
375375 base64_url:: encode ( & [ 8 ; 54 ] ) ,
376- PRFValue {
377- first : [ 8 ; 32 ] ,
378- second : Some ( [ 8 ; 32 ] ) ,
376+ PrfInputValue {
377+ first : vec ! [ 8 ; 32 ] ,
378+ second : Some ( vec ! [ 8 ; 32 ] ) ,
379379 } ,
380380 ) ;
381381 let prf = PrfInput {
@@ -394,16 +394,16 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
394394 . await ;
395395
396396 // Test 7: Wrongly encoded credential_id
397- let eval = Some ( PRFValue {
398- first : [ 2 ; 32 ] ,
397+ let eval = Some ( PrfInputValue {
398+ first : vec ! [ 2 ; 32 ] ,
399399 second : None ,
400400 } ) ;
401401
402402 let mut eval_by_credential = HashMap :: new ( ) ;
403403 eval_by_credential. insert (
404404 String :: from ( "ÄöoLfwekldß^" ) ,
405- PRFValue {
406- first : [ 1 ; 32 ] ,
405+ PrfInputValue {
406+ first : vec ! [ 1 ; 32 ] ,
407407 second : None ,
408408 } ,
409409 ) ;
@@ -426,8 +426,8 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
426426 let mut eval_by_credential = HashMap :: new ( ) ;
427427 eval_by_credential. insert (
428428 String :: new ( ) ,
429- PRFValue {
430- first : [ 1 ; 32 ] ,
429+ PrfInputValue {
430+ first : vec ! [ 1 ; 32 ] ,
431431 second : None ,
432432 } ,
433433 ) ;
@@ -450,8 +450,8 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
450450 let mut eval_by_credential = HashMap :: new ( ) ;
451451 eval_by_credential. insert (
452452 String :: new ( ) ,
453- PRFValue {
454- first : [ 1 ; 32 ] ,
453+ PrfInputValue {
454+ first : vec ! [ 1 ; 32 ] ,
455455 second : None ,
456456 } ,
457457 ) ;
@@ -581,3 +581,130 @@ async fn run_failed_test(
581581 assert_eq ! ( response, Err ( expected_error) , "{printoutput}:" ) ;
582582 println ! ( "Success for test: {printoutput}" )
583583}
584+
585+ /// W3C WebAuthn L3 §10.1.4: PRF salt inputs are `BufferSource`s of any length.
586+ /// Regression test for #209: end-to-end PRF assertion succeeds for empty,
587+ /// sub-32-byte, and super-32-byte salts, and is deterministic.
588+ #[ test( tokio:: test) ]
589+ async fn test_webauthn_prf_variable_length_input ( ) {
590+ let mut device = get_virtual_device ( ) ;
591+ let mut channel = device. channel ( ) . await . unwrap ( ) ;
592+
593+ let user_id: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
594+ let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
595+
596+ let make_credentials_request = MakeCredentialRequest {
597+ origin : "example.org" . to_owned ( ) ,
598+ challenge : Vec :: from ( challenge) ,
599+ relying_party : Ctap2PublicKeyCredentialRpEntity :: new ( "example.org" , "example.org" ) ,
600+ user : Ctap2PublicKeyCredentialUserEntity :: new ( & user_id, "mario.rossi" , "Mario Rossi" ) ,
601+ resident_key : Some ( ResidentKeyRequirement :: Discouraged ) ,
602+ user_verification : UserVerificationRequirement :: Preferred ,
603+ algorithms : vec ! [ Ctap2CredentialType :: default ( ) ] ,
604+ exclude : None ,
605+ extensions : Some ( MakeCredentialsRequestExtensions {
606+ prf : Some ( MakeCredentialPrfInput { _eval : None } ) ,
607+ ..Default :: default ( )
608+ } ) ,
609+ timeout : TIMEOUT ,
610+ top_origin : None ,
611+ } ;
612+
613+ let state_recv = channel. get_ux_update_receiver ( ) ;
614+ let expected_updates = vec ! [
615+ UvUpdateShim :: PresenceRequired , // MakeCredential
616+ UvUpdateShim :: PresenceRequired , // assert empty
617+ UvUpdateShim :: PresenceRequired , // assert 7 bytes
618+ UvUpdateShim :: PresenceRequired , // assert 100 bytes
619+ UvUpdateShim :: PresenceRequired , // determinism re-check (same 7 bytes)
620+ ] ;
621+ let uv_handle = tokio:: spawn ( handle_updates ( state_recv, expected_updates) ) ;
622+
623+ let response = channel
624+ . webauthn_make_credential ( & make_credentials_request)
625+ . await
626+ . expect ( "Failed to register credential" ) ;
627+ let credential: Ctap2PublicKeyCredentialDescriptor =
628+ ( & response. authenticator_data ) . try_into ( ) . unwrap ( ) ;
629+
630+ async fn assert_prf (
631+ channel : & mut HidChannel < ' _ > ,
632+ credential : & Ctap2PublicKeyCredentialDescriptor ,
633+ challenge : & [ u8 ; 32 ] ,
634+ first : Vec < u8 > ,
635+ label : & str ,
636+ ) -> [ u8 ; 32 ] {
637+ let get_assertion = GetAssertionRequest {
638+ relying_party_id : "example.org" . to_owned ( ) ,
639+ origin : "example.org" . to_owned ( ) ,
640+ challenge : Vec :: from ( challenge. as_slice ( ) ) ,
641+ allow : vec ! [ credential. clone( ) ] ,
642+ user_verification : UserVerificationRequirement :: Preferred ,
643+ extensions : Some ( GetAssertionRequestExtensions {
644+ prf : Some ( PrfInput {
645+ eval : Some ( PrfInputValue {
646+ first,
647+ second : None ,
648+ } ) ,
649+ eval_by_credential : HashMap :: new ( ) ,
650+ } ) ,
651+ ..Default :: default ( )
652+ } ) ,
653+ timeout : TIMEOUT ,
654+ top_origin : None ,
655+ } ;
656+ let response = channel
657+ . webauthn_get_assertion ( & get_assertion)
658+ . await
659+ . unwrap_or_else ( |_| panic ! ( "get_assertion failed: {label}" ) ) ;
660+ let results = response. assertions [ 0 ]
661+ . unsigned_extensions_output
662+ . as_ref ( )
663+ . unwrap_or_else ( || panic ! ( "no unsigned ext: {label}" ) )
664+ . prf
665+ . as_ref ( )
666+ . unwrap_or_else ( || panic ! ( "no prf: {label}" ) )
667+ . results
668+ . as_ref ( )
669+ . unwrap_or_else ( || panic ! ( "no results: {label}" ) ) ;
670+ assert_ne ! ( results. first, [ 0u8 ; 32 ] , "{label}" ) ;
671+ assert ! ( results. second. is_none( ) , "{label}" ) ;
672+ results. first
673+ }
674+
675+ let empty = assert_prf ( & mut channel, & credential, & challenge, vec ! [ ] , "empty" ) . await ;
676+ let short = assert_prf (
677+ & mut channel,
678+ & credential,
679+ & challenge,
680+ vec ! [ 0xAB ; 7 ] ,
681+ "7 bytes" ,
682+ )
683+ . await ;
684+ let long = assert_prf (
685+ & mut channel,
686+ & credential,
687+ & challenge,
688+ vec ! [ 0xCD ; 100 ] ,
689+ "100 bytes" ,
690+ )
691+ . await ;
692+ let short_again = assert_prf (
693+ & mut channel,
694+ & credential,
695+ & challenge,
696+ vec ! [ 0xAB ; 7 ] ,
697+ "7 bytes (repeat)" ,
698+ )
699+ . await ;
700+
701+ // Different inputs hash to different salts and therefore yield distinct outputs.
702+ assert_ne ! ( empty, short) ;
703+ assert_ne ! ( short, long) ;
704+ assert_ne ! ( empty, long) ;
705+ // Same input → same output: PRF is deterministic per (credential, salt).
706+ assert_eq ! ( short, short_again) ;
707+
708+ let mut state_recv = uv_handle. await . unwrap ( ) ;
709+ assert_eq ! ( state_recv. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
710+ }
0 commit comments