@@ -521,7 +521,11 @@ impl Ctap2GetAssertionResponseExtensions {
521521 pub ( crate ) fn to_unsigned_extensions (
522522 & self ,
523523 request : & GetAssertionRequest ,
524- response : & Ctap2GetAssertionResponse ,
524+ // Not used while authenticatorLargeBlobs is unimplemented; see comment below
525+ // (the `large_blob_key` was previously routed to the WebAuthn response, which
526+ // is a key-disclosure bug). The follow-up PR that implements
527+ // `authenticatorLargeBlobs` will use this field again.
528+ _response : & Ctap2GetAssertionResponse ,
525529 auth_data : Option < & AuthTokenData > ,
526530 ) -> GetAssertionResponseUnsignedExtensions {
527531 let decrypted_hmac = self . hmac_secret . as_ref ( ) . and_then ( |x| {
@@ -548,16 +552,25 @@ impl Ctap2GetAssertionResponseExtensions {
548552 } )
549553 } ) ;
550554
551- // LargeBlobs was requested
555+ // LargeBlobs was requested.
556+ //
557+ // The CTAP-level `largeBlobKey` (32-byte AES-256-GCM key) MUST NOT be returned
558+ // to the RP as the WebAuthn `large_blob.blob` value. Returning it would both
559+ // (a) fail to deliver the actual blob (the RP expects the decrypted payload),
560+ // and (b) disclose key material that is meant to stay platform-side.
561+ //
562+ // The proper flow is for the platform to invoke CTAP 2.1
563+ // `authenticatorLargeBlobs(get)`, locate the credential's entry in the
564+ // largeBlobArray, and decrypt it under `largeBlobKey`. That flow is not yet
565+ // implemented; until it is, we expose no blob to the RP. The
566+ // `Ctap2GetAssertionResponse.large_blob_key` field is preserved so the
567+ // upcoming implementation can use it.
552568 let large_blob = request
553569 . extensions
554570 . as_ref ( )
555571 . and_then ( |ext| ext. large_blob . as_ref ( ) )
556572 . map ( |_| GetAssertionLargeBlobExtensionOutput {
557- blob : response
558- . large_blob_key
559- . as_ref ( )
560- . map ( |x| x. clone ( ) . into_vec ( ) ) ,
573+ blob : None ,
561574 // Not yet supported
562575 // written: None,
563576 } ) ;
@@ -658,4 +671,58 @@ mod tests {
658671 let assertion = response. into_assertion_output ( & request, None ) ;
659672 assert_eq ! ( assertion. credential_id, None ) ;
660673 }
674+
675+ /// Regression test for the largeBlob key-disclosure bug.
676+ ///
677+ /// When the RP requests `largeBlob: { read: true }`, libwebauthn must NOT
678+ /// populate the WebAuthn `large_blob.blob` field with the CTAP-level
679+ /// `largeBlobKey` (a 32-byte AES-256-GCM key). The proper CTAP 2.1
680+ /// `authenticatorLargeBlobs(get)` flow is not yet implemented; the safe
681+ /// behaviour is to drop the key from the WebAuthn response.
682+ ///
683+ /// The CTAP-level `large_blob_key` field on the response is preserved so
684+ /// the upcoming implementation can use it internally.
685+ #[ test]
686+ fn large_blob_read_does_not_leak_key_into_webauthn_response ( ) {
687+ let cred = make_credential ( b"cred-1" ) ;
688+ // Simulate an authenticator that returns a (32-byte) largeBlobKey.
689+ let device_returned_key = vec ! [ 0xAAu8 ; 32 ] ;
690+ let mut response = make_response ( Some ( cred. clone ( ) ) ) ;
691+ response. large_blob_key = Some ( ByteBuf :: from ( device_returned_key. clone ( ) ) ) ;
692+ // Attach extension data so `to_unsigned_extensions` is invoked.
693+ response. authenticator_data . extensions = Some ( Ctap2GetAssertionResponseExtensions {
694+ cred_blob : None ,
695+ hmac_secret : None ,
696+ } ) ;
697+
698+ // The RP requested largeBlob.read.
699+ let mut request = make_request ( vec ! [ cred] ) ;
700+ request. extensions = Some ( GetAssertionRequestExtensions {
701+ cred_blob : false ,
702+ prf : None ,
703+ large_blob : Some ( GetAssertionLargeBlobExtension :: Read ) ,
704+ } ) ;
705+
706+ let assertion = response. into_assertion_output ( & request, None ) ;
707+ let unsigned = assertion
708+ . unsigned_extensions_output
709+ . expect ( "unsigned extensions should be present when the response has extensions" ) ;
710+ let large_blob = unsigned
711+ . large_blob
712+ . expect ( "largeBlob extension output should be present when requested" ) ;
713+
714+ // The WebAuthn-facing blob MUST NOT contain the largeBlobKey.
715+ assert ! (
716+ large_blob. blob. is_none( ) ,
717+ "WebAuthn large_blob.blob must not be populated until authenticatorLargeBlobs is implemented; got {:?}" ,
718+ large_blob. blob
719+ ) ;
720+
721+ // The CTAP-level largeBlobKey is still preserved on the Assertion model
722+ // for use by the upcoming implementation.
723+ assert_eq ! (
724+ assertion. large_blob_key. as_deref( ) ,
725+ Some ( & device_returned_key[ ..] )
726+ ) ;
727+ }
661728}
0 commit comments