@@ -111,6 +111,7 @@ where
111111 req
112112 )
113113 } ?;
114+ self . set_cred_mgmt_preview ( req. use_legacy_preview ) ;
114115 Ok ( (
115116 Ctap2RPData :: new (
116117 unwrap_field ! ( resp. rp) ,
@@ -122,24 +123,8 @@ where
122123
123124 async fn enumerate_rps_next_rp ( & mut self , timeout : Duration ) -> Result < Ctap2RPData , Error > {
124125 let mut req = Ctap2CredentialManagementRequest :: new_enumerate_rps_next_rp ( ) ;
125- let resp = loop {
126- let uv_auth_used = user_verification (
127- self ,
128- UserVerificationRequirement :: Preferred ,
129- & mut req,
130- timeout,
131- )
132- . await ?;
133-
134- // On success, this is an all-empty Ctap2AuthenticatorConfigResponse
135- handle_errors ! (
136- self ,
137- self . ctap2_credential_management( & req, timeout) . await ,
138- uv_auth_used,
139- timeout,
140- req
141- )
142- } ?;
126+ req. use_legacy_preview = self . cred_mgmt_preview ( ) ;
127+ let resp = self . ctap2_credential_management ( & req, timeout) . await ?;
143128 Ok ( Ctap2RPData :: new (
144129 unwrap_field ! ( resp. rp) ,
145130 unwrap_field ! ( resp. rp_id_hash) . into_vec ( ) ,
@@ -170,6 +155,7 @@ where
170155 req
171156 )
172157 } ?;
158+ self . set_cred_mgmt_preview ( req. use_legacy_preview ) ;
173159 let cred = Ctap2CredentialData :: new (
174160 unwrap_field ! ( resp. user) ,
175161 unwrap_field ! ( resp. credential_id) ,
@@ -186,24 +172,8 @@ where
186172 timeout : Duration ,
187173 ) -> Result < Ctap2CredentialData , Error > {
188174 let mut req = Ctap2CredentialManagementRequest :: new_enumerate_credentials_next ( ) ;
189- let resp = loop {
190- let uv_auth_used = user_verification (
191- self ,
192- UserVerificationRequirement :: Preferred ,
193- & mut req,
194- timeout,
195- )
196- . await ?;
197-
198- // On success, this is an all-empty Ctap2AuthenticatorConfigResponse
199- handle_errors ! (
200- self ,
201- self . ctap2_credential_management( & req, timeout) . await ,
202- uv_auth_used,
203- timeout,
204- req
205- )
206- } ?;
175+ req. use_legacy_preview = self . cred_mgmt_preview ( ) ;
176+ let resp = self . ctap2_credential_management ( & req, timeout) . await ?;
207177 let cred = Ctap2CredentialData :: new (
208178 unwrap_field ! ( resp. user) ,
209179 unwrap_field ! ( resp. credential_id) ,
@@ -367,9 +337,11 @@ mod test {
367337 use crate :: proto:: ctap2:: cbor:: { self , CborRequest , CborResponse } ;
368338 use crate :: proto:: ctap2:: {
369339 Ctap2CommandCode , Ctap2CredentialManagementRequest , Ctap2GetInfoResponse ,
370- Ctap2PublicKeyCredentialRpEntity ,
340+ Ctap2PublicKeyCredentialDescriptor , Ctap2PublicKeyCredentialRpEntity ,
341+ Ctap2PublicKeyCredentialType , Ctap2PublicKeyCredentialUserEntity ,
371342 } ;
372343 use crate :: transport:: mock:: channel:: MockChannel ;
344+ use std:: collections:: { BTreeMap , HashMap } ;
373345
374346 const TIMEOUT : Duration = Duration :: from_secs ( 1 ) ;
375347
@@ -413,4 +385,135 @@ mod test {
413385 assert_eq ! ( rp_data. rp_id_hash. len( ) , 32 ) ;
414386 assert_eq ! ( rp_data. rp_id_hash, hash. to_vec( ) ) ;
415387 }
388+
389+ // GetNextRP returns only rp (0x03) and rpIDHash (0x04).
390+ #[ derive( SerializeIndexed ) ]
391+ struct EnumerateRpsNextResponse {
392+ #[ serde( index = 0x03 ) ]
393+ rp : Ctap2PublicKeyCredentialRpEntity ,
394+ #[ serde( index = 0x04 ) ]
395+ rp_id_hash : ByteBuf ,
396+ }
397+
398+ // GetNextCredential returns user (0x06), credentialID (0x07), publicKey (0x08), credProtect (0x0A).
399+ #[ derive( SerializeIndexed ) ]
400+ struct EnumerateCredsNextResponse {
401+ #[ serde( index = 0x06 ) ]
402+ user : Ctap2PublicKeyCredentialUserEntity ,
403+ #[ serde( index = 0x07 ) ]
404+ credential_id : Ctap2PublicKeyCredentialDescriptor ,
405+ #[ serde( index = 0x08 ) ]
406+ public_key : BTreeMap < i8 , i8 > ,
407+ #[ serde( index = 0x0A ) ]
408+ cred_protect : u64 ,
409+ }
410+
411+ #[ tokio:: test]
412+ async fn enumerate_rps_next_rp_sends_only_the_subcommand ( ) {
413+ let req = Ctap2CredentialManagementRequest :: new_enumerate_rps_next_rp ( ) ;
414+ let cbor_req: CborRequest = ( & req) . try_into ( ) . unwrap ( ) ;
415+ assert_eq ! (
416+ cbor_req. command,
417+ Ctap2CommandCode :: AuthenticatorCredentialManagement
418+ ) ;
419+ // CTAP 2.1 §6.8.3: {subCommand: enumerateRPsGetNextRP}, no pinUvAuth keys.
420+ assert_eq ! ( cbor_req. encoded_data, vec![ 0xA1 , 0x01 , 0x03 ] ) ;
421+
422+ let hash = [ 0xCD_u8 ; 32 ] ;
423+ let fixture = EnumerateRpsNextResponse {
424+ rp : Ctap2PublicKeyCredentialRpEntity :: new ( "example.org" , "Example" ) ,
425+ rp_id_hash : ByteBuf :: from ( hash. to_vec ( ) ) ,
426+ } ;
427+ let resp = CborResponse :: new_success_from_slice ( & cbor:: to_vec ( & fixture) . unwrap ( ) ) ;
428+ // Queue only the GetNext exchange: any interleaved command panics the mock.
429+ let mut channel = MockChannel :: new ( ) ;
430+ channel. push_command_pair ( cbor_req, resp) ;
431+
432+ let rp_data = channel. enumerate_rps_next_rp ( TIMEOUT ) . await . unwrap ( ) ;
433+ assert_eq ! ( rp_data. rp_id_hash, hash. to_vec( ) ) ;
434+ }
435+
436+ #[ tokio:: test]
437+ async fn enumerate_credentials_next_sends_only_the_subcommand ( ) {
438+ let req = Ctap2CredentialManagementRequest :: new_enumerate_credentials_next ( ) ;
439+ let cbor_req: CborRequest = ( & req) . try_into ( ) . unwrap ( ) ;
440+ assert_eq ! (
441+ cbor_req. command,
442+ Ctap2CommandCode :: AuthenticatorCredentialManagement
443+ ) ;
444+ // CTAP 2.1 §6.8.4: {subCommand: enumerateCredentialsGetNextCredential}, no pinUvAuth keys.
445+ assert_eq ! ( cbor_req. encoded_data, vec![ 0xA1 , 0x01 , 0x05 ] ) ;
446+
447+ let fixture = EnumerateCredsNextResponse {
448+ user : Ctap2PublicKeyCredentialUserEntity :: new ( & [ 0x0B ; 16 ] , "bob" , "bob" ) ,
449+ credential_id : Ctap2PublicKeyCredentialDescriptor {
450+ id : ByteBuf :: from ( vec ! [ 0x1D ; 32 ] ) ,
451+ r#type : Ctap2PublicKeyCredentialType :: PublicKey ,
452+ transports : None ,
453+ } ,
454+ public_key : BTreeMap :: from ( [ ( 1 , 2 ) , ( 3 , -7 ) ] ) ,
455+ cred_protect : 1 ,
456+ } ;
457+ let resp = CborResponse :: new_success_from_slice ( & cbor:: to_vec ( & fixture) . unwrap ( ) ) ;
458+ let mut channel = MockChannel :: new ( ) ;
459+ channel. push_command_pair ( cbor_req, resp) ;
460+
461+ let cred = channel. enumerate_credentials_next ( TIMEOUT ) . await . unwrap ( ) ;
462+ assert_eq ! ( cred. user. id, ByteBuf :: from( vec![ 0x0B ; 16 ] ) ) ;
463+ assert_eq ! ( cred. cred_protect, 1 ) ;
464+ assert ! ( cred. large_blob_key. is_none( ) ) ;
465+ }
466+
467+ #[ tokio:: test]
468+ async fn get_next_reuses_preview_command_resolved_by_begin ( ) {
469+ let mut channel = MockChannel :: new ( ) ;
470+
471+ // Device advertises credentialMgmtPreview only: Begin must resolve 0x41.
472+ let info = Ctap2GetInfoResponse {
473+ options : Some ( HashMap :: from ( [ ( "credentialMgmtPreview" . to_string ( ) , true ) ] ) ) ,
474+ ..Default :: default ( )
475+ } ;
476+ let info_req = CborRequest :: new ( Ctap2CommandCode :: AuthenticatorGetInfo ) ;
477+ let info_resp = CborResponse :: new_success_from_slice ( & cbor:: to_vec ( & info) . unwrap ( ) ) ;
478+ channel. push_command_pair ( info_req, info_resp) ;
479+
480+ let mut begin_req = Ctap2CredentialManagementRequest :: new_enumerate_rps_begin ( ) ;
481+ begin_req. use_legacy_preview = true ;
482+ let begin_cbor: CborRequest = ( & begin_req) . try_into ( ) . unwrap ( ) ;
483+ assert_eq ! (
484+ begin_cbor. command,
485+ Ctap2CommandCode :: AuthenticatorCredentialManagementPreview
486+ ) ;
487+ let begin_fixture = EnumerateRpsResponse {
488+ rp : Ctap2PublicKeyCredentialRpEntity :: new ( "example.com" , "Example" ) ,
489+ rp_id_hash : ByteBuf :: from ( vec ! [ 0xEF ; 32 ] ) ,
490+ total_rps : 2 ,
491+ } ;
492+ channel. push_command_pair (
493+ begin_cbor,
494+ CborResponse :: new_success_from_slice ( & cbor:: to_vec ( & begin_fixture) . unwrap ( ) ) ,
495+ ) ;
496+
497+ let mut next_req = Ctap2CredentialManagementRequest :: new_enumerate_rps_next_rp ( ) ;
498+ next_req. use_legacy_preview = true ;
499+ let next_cbor: CborRequest = ( & next_req) . try_into ( ) . unwrap ( ) ;
500+ assert_eq ! (
501+ next_cbor. command,
502+ Ctap2CommandCode :: AuthenticatorCredentialManagementPreview
503+ ) ;
504+ assert_eq ! ( next_cbor. encoded_data, vec![ 0xA1 , 0x01 , 0x03 ] ) ;
505+ let next_fixture = EnumerateRpsNextResponse {
506+ rp : Ctap2PublicKeyCredentialRpEntity :: new ( "example.org" , "Example Two" ) ,
507+ rp_id_hash : ByteBuf :: from ( vec ! [ 0x11 ; 32 ] ) ,
508+ } ;
509+ channel. push_command_pair (
510+ next_cbor,
511+ CborResponse :: new_success_from_slice ( & cbor:: to_vec ( & next_fixture) . unwrap ( ) ) ,
512+ ) ;
513+
514+ let ( _, total) = channel. enumerate_rps_begin ( TIMEOUT ) . await . unwrap ( ) ;
515+ assert_eq ! ( total, 2 ) ;
516+ let rp_data = channel. enumerate_rps_next_rp ( TIMEOUT ) . await . unwrap ( ) ;
517+ assert_eq ! ( rp_data. rp_id_hash, vec![ 0x11 ; 32 ] ) ;
518+ }
416519}
0 commit comments