@@ -8,7 +8,7 @@ use libwebauthn::ops::webauthn::{
88use libwebauthn:: pin:: PinManagement ;
99use libwebauthn:: proto:: ctap2:: { Ctap2PinUvAuthProtocol , Ctap2PublicKeyCredentialDescriptor } ;
1010use libwebauthn:: transport:: hid:: channel:: HidChannel ;
11- use libwebauthn:: transport:: { Channel , Device } ;
11+ use libwebauthn:: transport:: { Channel , Ctap2AuthTokenStore , Device } ;
1212use libwebauthn:: webauthn:: { Error as WebAuthnError , PlatformError , WebAuthn } ;
1313use libwebauthn:: UvUpdate ;
1414use libwebauthn:: {
@@ -70,6 +70,7 @@ async fn test_webauthn_prf_with_pin_set_forced_pin_protocol_two() {
7070enum UvUpdateShim {
7171 PresenceRequired ,
7272 PinRequired ,
73+ PinNotSet ,
7374}
7475
7576async fn handle_updates (
@@ -90,6 +91,13 @@ async fn handle_updates(
9091 panic ! ( "Did not get PinRequired-update as expected!" ) ;
9192 }
9293 }
94+ UvUpdateShim :: PinNotSet => {
95+ if let UvUpdate :: PinNotSet ( update) = update {
96+ let _ = update. set_pin ( "1234" ) ;
97+ } else {
98+ panic ! ( "Did not get PinNotSet-update as expected!" ) ;
99+ }
100+ }
93101 }
94102 }
95103 state_recv
@@ -122,9 +130,13 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
122130 let state_recv = channel. get_ux_update_receiver ( ) ;
123131
124132 let mut expected_updates = Vec :: new ( ) ;
125- // First make cred
133+ // First make cred: PRF forces userVerification=required (W3C webauthn#2337),
134+ // so without a PIN we must drive the interactive PIN setup flow.
126135 if using_pin {
127136 expected_updates. push ( UvUpdateShim :: PinRequired ) ;
137+ } else {
138+ expected_updates. push ( UvUpdateShim :: PinNotSet ) ;
139+ expected_updates. push ( UvUpdateShim :: PinRequired ) ;
128140 }
129141 expected_updates. push ( UvUpdateShim :: PresenceRequired ) ; // First MakeCredential
130142
@@ -612,8 +624,11 @@ async fn test_webauthn_prf_variable_length_input() {
612624
613625 let state_recv = channel. get_ux_update_receiver ( ) ;
614626 let expected_updates = vec ! [
627+ // PRF forces UV=required (webauthn#2337); no-PIN device drives PIN setup.
628+ UvUpdateShim :: PinNotSet , // MakeCredential: set PIN
629+ UvUpdateShim :: PinRequired , // MakeCredential: auth with new PIN
615630 UvUpdateShim :: PresenceRequired , // MakeCredential
616- UvUpdateShim :: PresenceRequired , // assert empty
631+ UvUpdateShim :: PresenceRequired , // assert empty (cached pinUvAuthToken)
617632 UvUpdateShim :: PresenceRequired , // assert 7 bytes
618633 UvUpdateShim :: PresenceRequired , // assert 100 bytes
619634 UvUpdateShim :: PresenceRequired , // determinism re-check (same 7 bytes)
@@ -708,3 +723,177 @@ async fn test_webauthn_prf_variable_length_input() {
708723 let mut state_recv = uv_handle. await . unwrap ( ) ;
709724 assert_eq ! ( state_recv. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
710725}
726+
727+ fn basic_make_credential_request (
728+ user_id : & [ u8 ; 32 ] ,
729+ challenge : & [ u8 ; 32 ] ,
730+ user_verification : UserVerificationRequirement ,
731+ extensions : Option < MakeCredentialsRequestExtensions > ,
732+ ) -> MakeCredentialRequest {
733+ MakeCredentialRequest {
734+ origin : "example.org" . to_owned ( ) ,
735+ challenge : Vec :: from ( challenge. as_slice ( ) ) ,
736+ relying_party : Ctap2PublicKeyCredentialRpEntity :: new ( "example.org" , "example.org" ) ,
737+ user : Ctap2PublicKeyCredentialUserEntity :: new ( user_id, "mario.rossi" , "Mario Rossi" ) ,
738+ resident_key : Some ( ResidentKeyRequirement :: Discouraged ) ,
739+ user_verification,
740+ algorithms : vec ! [ Ctap2CredentialType :: default ( ) ] ,
741+ exclude : None ,
742+ extensions,
743+ timeout : TIMEOUT ,
744+ top_origin : None ,
745+ }
746+ }
747+
748+ // W3C webauthn#2337: PRF presence forces userVerification=required. With a PIN
749+ // already set, Discouraged + PRF must now trigger the PIN auth flow (PinRequired)
750+ // instead of being skipped as it would have been pre-upgrade.
751+ #[ test( tokio:: test) ]
752+ async fn test_webauthn_prf_upgrades_uv_at_registration ( ) {
753+ let mut device = get_virtual_device ( ) ;
754+ let mut channel = device. channel ( ) . await . unwrap ( ) ;
755+ channel. change_pin ( "1234" . into ( ) , TIMEOUT ) . await . unwrap ( ) ;
756+
757+ let state_recv = channel. get_ux_update_receiver ( ) ;
758+ let updates = tokio:: spawn ( handle_updates (
759+ state_recv,
760+ vec ! [ UvUpdateShim :: PinRequired , UvUpdateShim :: PresenceRequired ] ,
761+ ) ) ;
762+
763+ let user_id: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
764+ let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
765+ let request = basic_make_credential_request (
766+ & user_id,
767+ & challenge,
768+ UserVerificationRequirement :: Discouraged ,
769+ Some ( MakeCredentialsRequestExtensions {
770+ prf : Some ( MakeCredentialPrfInput { _eval : None } ) ,
771+ ..Default :: default ( )
772+ } ) ,
773+ ) ;
774+
775+ let response = channel
776+ . webauthn_make_credential ( & request)
777+ . await
778+ . expect ( "Failed to register credential" ) ;
779+ assert_eq ! (
780+ response. unsigned_extensions_output. prf,
781+ Some ( MakeCredentialPrfOutput {
782+ enabled: Some ( true )
783+ } )
784+ ) ;
785+
786+ let mut state_recv = updates. await . unwrap ( ) ;
787+ assert_eq ! ( state_recv. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
788+ }
789+
790+ // Negative: without PRF, Discouraged + PIN-set device must NOT trigger the PIN
791+ // flow. Guards against the upgrade leaking into non-PRF requests.
792+ #[ test( tokio:: test) ]
793+ async fn test_webauthn_no_prf_no_upgrade ( ) {
794+ let mut device = get_virtual_device ( ) ;
795+ let mut channel = device. channel ( ) . await . unwrap ( ) ;
796+ channel. change_pin ( "1234" . into ( ) , TIMEOUT ) . await . unwrap ( ) ;
797+
798+ let state_recv = channel. get_ux_update_receiver ( ) ;
799+ let updates = tokio:: spawn ( handle_updates (
800+ state_recv,
801+ vec ! [ UvUpdateShim :: PresenceRequired ] ,
802+ ) ) ;
803+
804+ let user_id: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
805+ let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
806+ let request = basic_make_credential_request (
807+ & user_id,
808+ & challenge,
809+ UserVerificationRequirement :: Discouraged ,
810+ None ,
811+ ) ;
812+
813+ channel
814+ . webauthn_make_credential ( & request)
815+ . await
816+ . expect ( "Failed to register credential" ) ;
817+
818+ let mut state_recv = updates. await . unwrap ( ) ;
819+ assert_eq ! ( state_recv. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
820+ }
821+
822+ // W3C webauthn#2337: same upgrade applies at assertion time. We clear the
823+ // cached PinUvAuthToken between registration and assertion so the assertion
824+ // must obtain fresh UV; without the clear, the cached (mc|ga, rpid) token
825+ // would cover the assertion regardless of whether the upgrade engaged.
826+ #[ test( tokio:: test) ]
827+ async fn test_webauthn_prf_upgrades_uv_at_assertion ( ) {
828+ let mut device = get_virtual_device ( ) ;
829+ let mut channel = device. channel ( ) . await . unwrap ( ) ;
830+ channel. change_pin ( "1234" . into ( ) , TIMEOUT ) . await . unwrap ( ) ;
831+
832+ let user_id: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
833+ let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
834+
835+ let registration = basic_make_credential_request (
836+ & user_id,
837+ & challenge,
838+ UserVerificationRequirement :: Required ,
839+ Some ( MakeCredentialsRequestExtensions {
840+ prf : Some ( MakeCredentialPrfInput { _eval : None } ) ,
841+ ..Default :: default ( )
842+ } ) ,
843+ ) ;
844+ let state_recv = channel. get_ux_update_receiver ( ) ;
845+ let setup_updates = tokio:: spawn ( handle_updates (
846+ state_recv,
847+ vec ! [ UvUpdateShim :: PinRequired , UvUpdateShim :: PresenceRequired ] ,
848+ ) ) ;
849+ let response = channel
850+ . webauthn_make_credential ( & registration)
851+ . await
852+ . expect ( "Failed to register credential" ) ;
853+ let state_recv = setup_updates. await . unwrap ( ) ;
854+
855+ let credential: Ctap2PublicKeyCredentialDescriptor =
856+ ( & response. authenticator_data ) . try_into ( ) . unwrap ( ) ;
857+
858+ channel. clear_uv_auth_token_store ( ) ;
859+
860+ let prf = PrfInput {
861+ eval : Some ( PrfInputValue {
862+ first : vec ! [ 1 ; 32 ] ,
863+ second : None ,
864+ } ) ,
865+ eval_by_credential : HashMap :: new ( ) ,
866+ } ;
867+ let get_assertion = GetAssertionRequest {
868+ relying_party_id : "example.org" . to_owned ( ) ,
869+ origin : "example.org" . to_owned ( ) ,
870+ challenge : Vec :: from ( challenge) ,
871+ allow : vec ! [ credential] ,
872+ user_verification : UserVerificationRequirement :: Discouraged ,
873+ extensions : Some ( GetAssertionRequestExtensions {
874+ prf : Some ( prf) ,
875+ ..Default :: default ( )
876+ } ) ,
877+ timeout : TIMEOUT ,
878+ top_origin : None ,
879+ } ;
880+ let assertion_updates = tokio:: spawn ( handle_updates (
881+ state_recv,
882+ vec ! [ UvUpdateShim :: PinRequired , UvUpdateShim :: PresenceRequired ] ,
883+ ) ) ;
884+ let assertion = channel
885+ . webauthn_get_assertion ( & get_assertion)
886+ . await
887+ . expect ( "Failed to get assertion" ) ;
888+ let prf_output = assertion. assertions [ 0 ]
889+ . unsigned_extensions_output
890+ . as_ref ( )
891+ . expect ( "Missing unsigned_extensions_output" )
892+ . prf
893+ . as_ref ( )
894+ . expect ( "Missing PRF output" ) ;
895+ assert ! ( prf_output. results. is_some( ) ) ;
896+
897+ let mut state_recv = assertion_updates. await . unwrap ( ) ;
898+ assert_eq ! ( state_recv. try_recv( ) , Err ( TryRecvError :: Empty ) ) ;
899+ }
0 commit comments