@@ -334,6 +334,78 @@ class MockLibFido2Bindings extends LibFido2Bindings {
334334 (_) => fidoOk;
335335}
336336
337+ /// Mock bindings that report `rk: true` in CBOR info options,
338+ /// simulating a key that supports resident/discoverable credentials
339+ /// (e.g. YubiKey 5).
340+ class MockLibFido2BindingsWithRk extends MockLibFido2Bindings {
341+ MockLibFido2BindingsWithRk ({
342+ super .mockDeviceCount,
343+ super .mockManifestResult,
344+ super .mockOpenResult,
345+ super .mockMakeCredResult,
346+ super .mockGetAssertResult,
347+ });
348+
349+ late Pointer <Pointer <Utf8 >> _optionNames;
350+ late Pointer <Bool > _optionValues;
351+
352+ @override
353+ int Function (Pointer ci) get fidoCborInfoOptionsLen =>
354+ (_) => 2 ;
355+
356+ @override
357+ Pointer <Pointer <Utf8 >> Function (Pointer ci) get fidoCborInfoOptionsNamePtr =>
358+ (_) {
359+ _optionNames = calloc <Pointer <Utf8 >>(2 );
360+ _optionNames[0 ] = 'rk' .toNativeUtf8 ();
361+ _optionNames[1 ] = 'clientPin' .toNativeUtf8 ();
362+ return _optionNames;
363+ };
364+
365+ @override
366+ Pointer <Bool > Function (Pointer ci) get fidoCborInfoOptionsValuePtr => (_) {
367+ _optionValues = calloc <Bool >(2 );
368+ _optionValues[0 ] = true ; // rk = true
369+ _optionValues[1 ] = false ; // clientPin = false
370+ return _optionValues;
371+ };
372+ }
373+
374+ /// Mock bindings that report `rk: false` in CBOR info options,
375+ /// simulating a key that does NOT support resident/discoverable
376+ /// credentials (e.g. ZUKEY 2 FIDO).
377+ class MockLibFido2BindingsWithoutRk extends MockLibFido2Bindings {
378+ MockLibFido2BindingsWithoutRk ({
379+ super .mockDeviceCount,
380+ super .mockManifestResult,
381+ super .mockOpenResult,
382+ super .mockMakeCredResult,
383+ super .mockGetAssertResult,
384+ });
385+
386+ late Pointer <Pointer <Utf8 >> _optionNames;
387+ late Pointer <Bool > _optionValues;
388+
389+ @override
390+ int Function (Pointer ci) get fidoCborInfoOptionsLen =>
391+ (_) => 1 ;
392+
393+ @override
394+ Pointer <Pointer <Utf8 >> Function (Pointer ci) get fidoCborInfoOptionsNamePtr =>
395+ (_) {
396+ _optionNames = calloc <Pointer <Utf8 >>(1 );
397+ _optionNames[0 ] = 'clientPin' .toNativeUtf8 ();
398+ return _optionNames;
399+ };
400+
401+ @override
402+ Pointer <Bool > Function (Pointer ci) get fidoCborInfoOptionsValuePtr => (_) {
403+ _optionValues = calloc <Bool >(1 );
404+ _optionValues[0 ] = false ;
405+ return _optionValues;
406+ };
407+ }
408+
337409void main () {
338410 TestWidgetsFlutterBinding .ensureInitialized ();
339411
@@ -513,6 +585,128 @@ void main() {
513585 );
514586 });
515587
588+ group ('resident key capability check' , () {
589+ test (
590+ 'succeeds when residentKey=required and device supports rk' ,
591+ () async {
592+ final bindings = MockLibFido2BindingsWithRk ();
593+ final platform = LinuxWebAuthnPlatform (bindings: bindings);
594+
595+ const optionsJson = '''
596+ {
597+ "rp": {"id": "example.com", "name": "Example"},
598+ "user": {"id": "dXNlcjEyMw", "name": "testuser", "displayName": "Test User"},
599+ "challenge": "Y2hhbGxlbmdl",
600+ "pubKeyCredParams": [{"type": "public-key", "alg": -7}],
601+ "authenticatorSelection": {"residentKey": "required", "userVerification": "preferred"}
602+ }
603+ ''' ;
604+
605+ final result = await platform.createCredential (optionsJson);
606+ final decoded = jsonDecode (result) as Map <String , dynamic >;
607+ expect (decoded['type' ], 'public-key' );
608+ expect (decoded['id' ], isNotEmpty);
609+ },
610+ );
611+
612+ test (
613+ 'throws PasskeyRegistrationFailedException when residentKey=required '
614+ 'and device does NOT support rk' ,
615+ () async {
616+ final bindings = MockLibFido2BindingsWithoutRk ();
617+ final platform = LinuxWebAuthnPlatform (bindings: bindings);
618+
619+ const optionsJson = '''
620+ {
621+ "rp": {"id": "example.com", "name": "Example"},
622+ "user": {"id": "dXNlcjEyMw", "name": "testuser", "displayName": "Test User"},
623+ "challenge": "Y2hhbGxlbmdl",
624+ "pubKeyCredParams": [{"type": "public-key", "alg": -7}],
625+ "authenticatorSelection": {"residentKey": "required", "userVerification": "preferred"}
626+ }
627+ ''' ;
628+
629+ expect (
630+ () => platform.createCredential (optionsJson),
631+ throwsA (
632+ isA <PasskeyRegistrationFailedException >().having (
633+ (e) => e.message,
634+ 'message' ,
635+ contains ('does not support passkeys' ),
636+ ),
637+ ),
638+ );
639+ },
640+ );
641+
642+ test (
643+ 'throws PasskeyRegistrationFailedException when residentKey=preferred '
644+ 'and device does NOT support rk' ,
645+ () async {
646+ final bindings = MockLibFido2BindingsWithoutRk ();
647+ final platform = LinuxWebAuthnPlatform (bindings: bindings);
648+
649+ const optionsJson = '''
650+ {
651+ "rp": {"id": "example.com", "name": "Example"},
652+ "user": {"id": "dXNlcjEyMw", "name": "testuser", "displayName": "Test User"},
653+ "challenge": "Y2hhbGxlbmdl",
654+ "pubKeyCredParams": [{"type": "public-key", "alg": -7}],
655+ "authenticatorSelection": {"residentKey": "preferred", "userVerification": "preferred"}
656+ }
657+ ''' ;
658+
659+ expect (
660+ () => platform.createCredential (optionsJson),
661+ throwsA (isA <PasskeyRegistrationFailedException >()),
662+ );
663+ },
664+ );
665+
666+ test (
667+ 'succeeds without authenticatorSelection (no rk check needed)' ,
668+ () async {
669+ final bindings = MockLibFido2Bindings ();
670+ final platform = LinuxWebAuthnPlatform (bindings: bindings);
671+
672+ const optionsJson = '''
673+ {
674+ "rp": {"id": "example.com", "name": "Example"},
675+ "user": {"id": "dXNlcjEyMw", "name": "testuser", "displayName": "Test User"},
676+ "challenge": "Y2hhbGxlbmdl",
677+ "pubKeyCredParams": [{"type": "public-key", "alg": -7}]
678+ }
679+ ''' ;
680+
681+ final result = await platform.createCredential (optionsJson);
682+ final decoded = jsonDecode (result) as Map <String , dynamic >;
683+ expect (decoded['type' ], 'public-key' );
684+ },
685+ );
686+
687+ test (
688+ 'succeeds when residentKey=discouraged even without rk support' ,
689+ () async {
690+ final bindings = MockLibFido2BindingsWithoutRk ();
691+ final platform = LinuxWebAuthnPlatform (bindings: bindings);
692+
693+ const optionsJson = '''
694+ {
695+ "rp": {"id": "example.com", "name": "Example"},
696+ "user": {"id": "dXNlcjEyMw", "name": "testuser", "displayName": "Test User"},
697+ "challenge": "Y2hhbGxlbmdl",
698+ "pubKeyCredParams": [{"type": "public-key", "alg": -7}],
699+ "authenticatorSelection": {"residentKey": "discouraged"}
700+ }
701+ ''' ;
702+
703+ final result = await platform.createCredential (optionsJson);
704+ final decoded = jsonDecode (result) as Map <String , dynamic >;
705+ expect (decoded['type' ], 'public-key' );
706+ },
707+ );
708+ });
709+
516710 group ('getCredential' , () {
517711 test ('returns JSON response on success' , () async {
518712 final bindings = MockLibFido2Bindings ();
0 commit comments