@@ -685,4 +685,138 @@ mod tests {
685685 let got = storage. read ( b"cred-A" ) . await . expect ( "read" ) ;
686686 assert ! ( got. is_none( ) ) ;
687687 }
688+
689+ /// End-to-end check of the read path through `webauthn_get_assertion`:
690+ /// drives the CTAP exchange, array parsing, AES-256-GCM decrypt, deflate
691+ /// decompress, and surfaces the plaintext as the WebAuthn JSON output.
692+ #[ tokio:: test]
693+ async fn webauthn_get_assertion_returns_decrypted_large_blob ( ) {
694+ use crate :: ops:: webauthn:: {
695+ GetAssertionLargeBlobExtension , GetAssertionRequest , GetAssertionRequestExtensions ,
696+ UserVerificationRequirement ,
697+ } ;
698+ use crate :: proto:: ctap2:: cbor:: { to_vec, CborRequest , CborResponse , Value } ;
699+ use crate :: proto:: ctap2:: {
700+ Ctap2CommandCode , Ctap2GetInfoResponse , Ctap2LargeBlobsResponse ,
701+ } ;
702+ use crate :: transport:: mock:: channel:: MockChannel ;
703+ use crate :: webauthn:: WebAuthn ;
704+ use std:: collections:: { BTreeMap , HashMap } ;
705+
706+ let large_blob_key = [ 0x77u8 ; 32 ] ;
707+ let nonce = [ 0x22u8 ; 12 ] ;
708+ let plaintext = b"webauthn end-to-end largeBlob" . to_vec ( ) ;
709+ let entry = encrypt_entry ( & large_blob_key, & nonce, & plaintext) . unwrap ( ) ;
710+ let serialized_array = build_serialized_array ( & [ entry] ) ;
711+
712+ // Build the assertion-response CBOR by hand so we can populate field
713+ // 0x07 (largeBlobKey) without adding a Serialize impl to the response
714+ // model.
715+ let credential_id = b"cred-id" . to_vec ( ) ;
716+ let mut auth_data = vec ! [ 0u8 ; 37 ] ;
717+ auth_data[ 32 ] = 0x01 ; // USER_PRESENT flag
718+ let mut cred_id_map = BTreeMap :: new ( ) ;
719+ cred_id_map. insert ( Value :: Text ( "type" . into ( ) ) , Value :: Text ( "public-key" . into ( ) ) ) ;
720+ cred_id_map. insert (
721+ Value :: Text ( "id" . into ( ) ) ,
722+ Value :: Bytes ( credential_id. clone ( ) ) ,
723+ ) ;
724+ let mut response_map = BTreeMap :: new ( ) ;
725+ response_map. insert ( Value :: Integer ( 1 ) , Value :: Map ( cred_id_map) ) ;
726+ response_map. insert ( Value :: Integer ( 2 ) , Value :: Bytes ( auth_data) ) ;
727+ response_map. insert ( Value :: Integer ( 3 ) , Value :: Bytes ( vec ! [ 0u8 ; 32 ] ) ) ;
728+ response_map. insert ( Value :: Integer ( 7 ) , Value :: Bytes ( large_blob_key. to_vec ( ) ) ) ;
729+ let assertion_resp_cbor = to_vec ( & Value :: Map ( response_map) ) . unwrap ( ) ;
730+
731+ // GetInfo response advertising support for largeBlobs.
732+ let mut info = Ctap2GetInfoResponse {
733+ versions : vec ! [ "FIDO_2_1" . into( ) ] ,
734+ ..Default :: default ( )
735+ } ;
736+ let mut options = HashMap :: new ( ) ;
737+ options. insert ( "largeBlobs" . into ( ) , true ) ;
738+ info. options = Some ( options) ;
739+ let info_cbor = to_vec ( & info) . unwrap ( ) ;
740+
741+ let mut channel = MockChannel :: new ( ) ;
742+
743+ // 1. _webauthn_get_assertion_fido2 calls ctap2_get_info().
744+ channel. push_command_pair (
745+ CborRequest :: new ( Ctap2CommandCode :: AuthenticatorGetInfo ) ,
746+ CborResponse :: new_success_from_slice ( & info_cbor) ,
747+ ) ;
748+ // 2. user_verification calls ctap2_get_info() again.
749+ channel. push_command_pair (
750+ CborRequest :: new ( Ctap2CommandCode :: AuthenticatorGetInfo ) ,
751+ CborResponse :: new_success_from_slice ( & info_cbor) ,
752+ ) ;
753+ // 3. ctap2_get_assertion. The default From<GetAssertionRequest>
754+ // impl produces options { up: true, uv: false }, matching the
755+ // Discouraged UV path; that's what we exercise here.
756+ let req = crate :: proto:: ctap2:: Ctap2GetAssertionRequest :: from ( GetAssertionRequest {
757+ relying_party_id : "example.com" . into ( ) ,
758+ challenge : vec ! [ 0u8 ; 32 ] ,
759+ origin : "example.com" . into ( ) ,
760+ cross_origin : None ,
761+ allow : vec ! [ ] ,
762+ extensions : Some ( GetAssertionRequestExtensions {
763+ cred_blob : false ,
764+ prf : None ,
765+ large_blob : Some ( GetAssertionLargeBlobExtension :: Read ) ,
766+ } ) ,
767+ user_verification : UserVerificationRequirement :: Discouraged ,
768+ timeout : Duration :: from_secs ( 5 ) ,
769+ } ) ;
770+ let assertion_req_cbor = crate :: proto:: ctap2:: cbor:: to_vec ( & req) . unwrap ( ) ;
771+ channel. push_command_pair (
772+ CborRequest {
773+ command : Ctap2CommandCode :: AuthenticatorGetAssertion ,
774+ encoded_data : assertion_req_cbor,
775+ } ,
776+ CborResponse :: new_success_from_slice ( & assertion_resp_cbor) ,
777+ ) ;
778+ // 4. authenticatorLargeBlobs(get).
779+ let blobs_req = Ctap2LargeBlobsRequest :: new_get ( 0 , LARGE_BLOB_DEFAULT_CHUNK ) ;
780+ let blobs_resp = Ctap2LargeBlobsResponse {
781+ config : Some ( serde_bytes:: ByteBuf :: from ( serialized_array) ) ,
782+ } ;
783+ channel. push_command_pair (
784+ CborRequest {
785+ command : Ctap2CommandCode :: AuthenticatorLargeBlobs ,
786+ encoded_data : crate :: proto:: ctap2:: cbor:: to_vec ( & blobs_req) . unwrap ( ) ,
787+ } ,
788+ CborResponse :: new_success_from_slice (
789+ & crate :: proto:: ctap2:: cbor:: to_vec ( & blobs_resp) . unwrap ( ) ,
790+ ) ,
791+ ) ;
792+
793+ let request = GetAssertionRequest {
794+ relying_party_id : "example.com" . into ( ) ,
795+ challenge : vec ! [ 0u8 ; 32 ] ,
796+ origin : "example.com" . into ( ) ,
797+ cross_origin : None ,
798+ allow : vec ! [ ] ,
799+ extensions : Some ( GetAssertionRequestExtensions {
800+ cred_blob : false ,
801+ prf : None ,
802+ large_blob : Some ( GetAssertionLargeBlobExtension :: Read ) ,
803+ } ) ,
804+ user_verification : UserVerificationRequirement :: Discouraged ,
805+ timeout : Duration :: from_secs ( 5 ) ,
806+ } ;
807+
808+ let response = channel
809+ . webauthn_get_assertion ( & request)
810+ . await
811+ . expect ( "webauthn_get_assertion should succeed" ) ;
812+ assert_eq ! ( response. assertions. len( ) , 1 ) ;
813+ let large_blob = response. assertions [ 0 ]
814+ . unsigned_extensions_output
815+ . as_ref ( )
816+ . expect ( "unsigned extensions present" )
817+ . large_blob
818+ . as_ref ( )
819+ . expect ( "largeBlob extension output present" ) ;
820+ assert_eq ! ( large_blob. blob. as_deref( ) , Some ( plaintext. as_slice( ) ) ) ;
821+ }
688822}
0 commit comments