@@ -13,7 +13,7 @@ use libwebauthn::proto::ctap2::{
1313 Ctap2PublicKeyCredentialUserEntity ,
1414} ;
1515use libwebauthn:: transport:: { Channel , ChannelSettings , Device } ;
16- use libwebauthn:: webauthn:: WebAuthn ;
16+ use libwebauthn:: webauthn:: { Error , PlatformError , WebAuthn } ;
1717use libwebauthn:: UvUpdate ;
1818use libwebauthn_tests:: virt:: get_virtual_device;
1919use rand:: { thread_rng, Rng } ;
@@ -301,7 +301,7 @@ fn ga_request(
301301#[ test( tokio:: test) ]
302302async fn test_webauthn_large_blob_write_then_read_returns_blob ( ) {
303303 let mut device = get_virtual_device ( ) ;
304- let mut channel = device. channel ( ) . await . unwrap ( ) ;
304+ let mut channel = device. channel ( ChannelSettings :: default ( ) ) . await . unwrap ( ) ;
305305 let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
306306
307307 let state_recv = channel. get_ux_update_receiver ( ) ;
@@ -350,7 +350,7 @@ async fn test_webauthn_large_blob_write_then_read_returns_blob() {
350350#[ test( tokio:: test) ]
351351async fn test_webauthn_large_blob_write_replaces_existing_entry ( ) {
352352 let mut device = get_virtual_device ( ) ;
353- let mut channel = device. channel ( ) . await . unwrap ( ) ;
353+ let mut channel = device. channel ( ChannelSettings :: default ( ) ) . await . unwrap ( ) ;
354354 let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
355355
356356 let state_recv = channel. get_ux_update_receiver ( ) ;
@@ -403,7 +403,7 @@ async fn test_webauthn_large_blob_write_replaces_existing_entry() {
403403#[ test( tokio:: test) ]
404404async fn test_webauthn_large_blob_delete_without_existing_entry_reports_false ( ) {
405405 let mut device = get_virtual_device ( ) ;
406- let mut channel = device. channel ( ) . await . unwrap ( ) ;
406+ let mut channel = device. channel ( ChannelSettings :: default ( ) ) . await . unwrap ( ) ;
407407 let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
408408
409409 let state_recv = channel. get_ux_update_receiver ( ) ;
@@ -436,7 +436,7 @@ async fn test_webauthn_large_blob_delete_without_existing_entry_reports_false()
436436#[ test( tokio:: test) ]
437437async fn test_webauthn_large_blob_delete_removes_entry ( ) {
438438 let mut device = get_virtual_device ( ) ;
439- let mut channel = device. channel ( ) . await . unwrap ( ) ;
439+ let mut channel = device. channel ( ChannelSettings :: default ( ) ) . await . unwrap ( ) ;
440440 let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
441441
442442 let state_recv = channel. get_ux_update_receiver ( ) ;
@@ -489,3 +489,240 @@ async fn test_webauthn_large_blob_delete_removes_entry() {
489489
490490 update_handle. await . unwrap ( ) ;
491491}
492+
493+ async fn write_large_blob (
494+ channel : & mut libwebauthn:: transport:: hid:: channel:: HidChannel < ' _ > ,
495+ credential : & Ctap2PublicKeyCredentialDescriptor ,
496+ challenge : & [ u8 ; 32 ] ,
497+ blob : Vec < u8 > ,
498+ ) {
499+ let resp = channel
500+ . webauthn_get_assertion ( & ga_request (
501+ credential,
502+ challenge,
503+ GetAssertionLargeBlobExtension :: Write ( blob) ,
504+ ) )
505+ . await
506+ . expect ( "write assertion should succeed" ) ;
507+ assert_eq ! (
508+ resp. assertions[ 0 ]
509+ . unsigned_extensions_output
510+ . as_ref( )
511+ . and_then( |u| u. large_blob. as_ref( ) )
512+ . and_then( |lb| lb. written) ,
513+ Some ( true ) ,
514+ "largeBlob.written should be true"
515+ ) ;
516+ }
517+
518+ async fn read_large_blob (
519+ channel : & mut libwebauthn:: transport:: hid:: channel:: HidChannel < ' _ > ,
520+ credential : & Ctap2PublicKeyCredentialDescriptor ,
521+ challenge : & [ u8 ; 32 ] ,
522+ ) -> Option < Vec < u8 > > {
523+ let resp = channel
524+ . webauthn_get_assertion ( & ga_request (
525+ credential,
526+ challenge,
527+ GetAssertionLargeBlobExtension :: Read ,
528+ ) )
529+ . await
530+ . expect ( "read assertion should succeed" ) ;
531+ resp. assertions [ 0 ]
532+ . unsigned_extensions_output
533+ . as_ref ( )
534+ . and_then ( |u| u. large_blob . as_ref ( ) )
535+ . and_then ( |lb| lb. blob . clone ( ) )
536+ }
537+
538+ /// Two largeBlob-capable credentials coexist on one authenticator. A's write
539+ /// (replace) and delete must leave B's foreign entry intact (CTAP 2.2 §6.10.6).
540+ #[ test( tokio:: test) ]
541+ async fn test_webauthn_large_blob_foreign_entry_survives_write_and_delete ( ) {
542+ let mut device = get_virtual_device ( ) ;
543+ let mut channel = device. channel ( ChannelSettings :: default ( ) ) . await . unwrap ( ) ;
544+ let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
545+
546+ let state_recv = channel. get_ux_update_receiver ( ) ;
547+ // 2 registrations + 3 writes + 4 reads + 1 delete = 10 ceremonies.
548+ let update_handle = tokio:: spawn ( handle_updates_n ( state_recv, 10 ) ) ;
549+
550+ let cred_a = register_with_large_blob ( & mut channel, "alice" , & challenge) . await ;
551+ let cred_b = register_with_large_blob ( & mut channel, "bob" , & challenge) . await ;
552+
553+ let blob_a = b"alice's blob" . to_vec ( ) ;
554+ let blob_b = b"bob's blob must survive" . to_vec ( ) ;
555+
556+ write_large_blob ( & mut channel, & cred_a, & challenge, blob_a. clone ( ) ) . await ;
557+ write_large_blob ( & mut channel, & cred_b, & challenge, blob_b. clone ( ) ) . await ;
558+
559+ assert_eq ! (
560+ read_large_blob( & mut channel, & cred_a, & challenge)
561+ . await
562+ . as_deref( ) ,
563+ Some ( blob_a. as_slice( ) ) ,
564+ "A reads back its own blob"
565+ ) ;
566+ assert_eq ! (
567+ read_large_blob( & mut channel, & cred_b, & challenge)
568+ . await
569+ . as_deref( ) ,
570+ Some ( blob_b. as_slice( ) ) ,
571+ "B reads back its own blob"
572+ ) ;
573+
574+ let blob_a2 = b"alice's replacement blob, longer than the first" . to_vec ( ) ;
575+ write_large_blob ( & mut channel, & cred_a, & challenge, blob_a2. clone ( ) ) . await ;
576+ assert_eq ! (
577+ read_large_blob( & mut channel, & cred_b, & challenge)
578+ . await
579+ . as_deref( ) ,
580+ Some ( blob_b. as_slice( ) ) ,
581+ "B's blob survives A's replace"
582+ ) ;
583+
584+ let del_resp = channel
585+ . webauthn_get_assertion ( & ga_request (
586+ & cred_a,
587+ & challenge,
588+ GetAssertionLargeBlobExtension :: Delete ,
589+ ) )
590+ . await
591+ . expect ( "delete A should succeed" ) ;
592+ assert_eq ! (
593+ del_resp. assertions[ 0 ]
594+ . unsigned_extensions_output
595+ . as_ref( )
596+ . and_then( |u| u. large_blob. as_ref( ) )
597+ . and_then( |lb| lb. written) ,
598+ Some ( true ) ,
599+ "delete A reports written=true"
600+ ) ;
601+ assert_eq ! (
602+ read_large_blob( & mut channel, & cred_b, & challenge)
603+ . await
604+ . as_deref( ) ,
605+ Some ( blob_b. as_slice( ) ) ,
606+ "B's blob survives A's delete"
607+ ) ;
608+
609+ update_handle. await . unwrap ( ) ;
610+ }
611+
612+ /// WebAuthn L3 §10.1.5: largeBlob.write/delete requires exactly one allowCredentials
613+ /// entry. The platform guard in get_assertion_fido2 rejects a write with two (or zero)
614+ /// allowed credentials as NotSupported, before any CTAP traffic.
615+ #[ test( tokio:: test) ]
616+ async fn test_webauthn_large_blob_write_requires_single_allow_credential ( ) {
617+ let mut device = get_virtual_device ( ) ;
618+ let mut channel = device. channel ( ChannelSettings :: default ( ) ) . await . unwrap ( ) ;
619+ let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
620+
621+ let state_recv = channel. get_ux_update_receiver ( ) ;
622+ // Two registrations, one PresenceRequired each. The rejected writes emit none.
623+ let update_handle = tokio:: spawn ( handle_updates_n ( state_recv, 2 ) ) ;
624+
625+ let cred_a = register_with_large_blob ( & mut channel, "alice" , & challenge) . await ;
626+ let cred_b = register_with_large_blob ( & mut channel, "bob" , & challenge) . await ;
627+
628+ let mut two = ga_request (
629+ & cred_a,
630+ & challenge,
631+ GetAssertionLargeBlobExtension :: Write ( b"blob" . to_vec ( ) ) ,
632+ ) ;
633+ two. allow = vec ! [ cred_a. clone( ) , cred_b] ;
634+ let err = channel
635+ . webauthn_get_assertion ( & two)
636+ . await
637+ . expect_err ( "write with two allowCredentials must be rejected" ) ;
638+ assert_eq ! ( err, Error :: Platform ( PlatformError :: NotSupported ) ) ;
639+
640+ let mut none = ga_request (
641+ & cred_a,
642+ & challenge,
643+ GetAssertionLargeBlobExtension :: Write ( b"blob" . to_vec ( ) ) ,
644+ ) ;
645+ none. allow = vec ! [ ] ;
646+ let err = channel
647+ . webauthn_get_assertion ( & none)
648+ . await
649+ . expect_err ( "write with empty allowCredentials must be rejected" ) ;
650+ assert_eq ! ( err, Error :: Platform ( PlatformError :: NotSupported ) ) ;
651+
652+ update_handle. await . unwrap ( ) ;
653+ }
654+
655+ /// Replacing a blob with a strictly smaller one shrinks the stored array: the
656+ /// read-modify-write rebuild must drop the larger entry entirely so the read
657+ /// returns exactly the smaller blob (no stale trailing bytes).
658+ #[ test( tokio:: test) ]
659+ async fn test_webauthn_large_blob_write_replaces_with_smaller_blob ( ) {
660+ let mut device = get_virtual_device ( ) ;
661+ let mut channel = device. channel ( ChannelSettings :: default ( ) ) . await . unwrap ( ) ;
662+ let challenge: [ u8 ; 32 ] = thread_rng ( ) . gen ( ) ;
663+
664+ let state_recv = channel. get_ux_update_receiver ( ) ;
665+ // MakeCredential + write(large) + read + write(small) + read = 5 PresenceRequired updates.
666+ let update_handle = tokio:: spawn ( handle_updates_n ( state_recv, 5 ) ) ;
667+
668+ let credential = register_with_large_blob ( & mut channel, "erin" , & challenge) . await ;
669+
670+ let large = b"compressible largeBlob payload " . repeat ( 20 ) ;
671+ let small = b"tiny" . to_vec ( ) ;
672+
673+ channel
674+ . webauthn_get_assertion ( & ga_request (
675+ & credential,
676+ & challenge,
677+ GetAssertionLargeBlobExtension :: Write ( large. clone ( ) ) ,
678+ ) )
679+ . await
680+ . expect ( "large write" ) ;
681+
682+ let read_large = channel
683+ . webauthn_get_assertion ( & ga_request (
684+ & credential,
685+ & challenge,
686+ GetAssertionLargeBlobExtension :: Read ,
687+ ) )
688+ . await
689+ . expect ( "read after large write" ) ;
690+ let blob_large = read_large. assertions [ 0 ]
691+ . unsigned_extensions_output
692+ . as_ref ( )
693+ . and_then ( |u| u. large_blob . as_ref ( ) )
694+ . and_then ( |lb| lb. blob . as_ref ( ) )
695+ . expect ( "blob present after large write" ) ;
696+ assert_eq ! ( blob_large. as_slice( ) , large. as_slice( ) ) ;
697+
698+ channel
699+ . webauthn_get_assertion ( & ga_request (
700+ & credential,
701+ & challenge,
702+ GetAssertionLargeBlobExtension :: Write ( small. clone ( ) ) ,
703+ ) )
704+ . await
705+ . expect ( "small write" ) ;
706+
707+ let read_small = channel
708+ . webauthn_get_assertion ( & ga_request (
709+ & credential,
710+ & challenge,
711+ GetAssertionLargeBlobExtension :: Read ,
712+ ) )
713+ . await
714+ . expect ( "read after small write" ) ;
715+ let blob_small = read_small. assertions [ 0 ]
716+ . unsigned_extensions_output
717+ . as_ref ( )
718+ . and_then ( |u| u. large_blob . as_ref ( ) )
719+ . and_then ( |lb| lb. blob . as_ref ( ) )
720+ . expect ( "blob present after small write" ) ;
721+ assert_eq ! (
722+ blob_small. as_slice( ) ,
723+ small. as_slice( ) ,
724+ "smaller write replaced larger"
725+ ) ;
726+
727+ update_handle. await . unwrap ( ) ;
728+ }
0 commit comments