diff --git a/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2KeyCollectorOperation.cs b/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2KeyCollectorOperation.cs index 1e2fb8cc3..b6280e043 100644 --- a/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2KeyCollectorOperation.cs +++ b/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2KeyCollectorOperation.cs @@ -21,5 +21,6 @@ public enum Fido2KeyCollectorOperation GetAssertion = 2, Reset = 3, Verify = 4, + AuthenticatorSelection = 5, } } diff --git a/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2SampleKeyCollector.cs b/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2SampleKeyCollector.cs index 88b4771d0..fcd8501ae 100644 --- a/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2SampleKeyCollector.cs +++ b/Yubico.YubiKey/examples/Fido2SampleCode/KeyCollector/Fido2SampleKeyCollector.cs @@ -165,6 +165,11 @@ private void ReportOperation() SampleMenu.WriteMessage(MessageType.Title, 0, "\nThe YubiKey is trying to reset the FIDO2 application,"); break; + + case Fido2KeyCollectorOperation.AuthenticatorSelection: + SampleMenu.WriteMessage(MessageType.Title, 0, + "\nThe YubiKey is waiting for authenticatorSelection (user presence),"); + break; } } diff --git a/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2MainMenuItem.cs b/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2MainMenuItem.cs index 0cd590bc0..876e4a5a1 100644 --- a/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2MainMenuItem.cs +++ b/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2MainMenuItem.cs @@ -43,8 +43,10 @@ public enum Fido2MainMenuItem ToggleAlwaysUv = 24, SetPinConfig = 25, - Reset = 26, + AuthenticatorSelection = 26, - Exit = 27, + Reset = 27, + + Exit = 29, } } diff --git a/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2SampleRun.Operations.cs b/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2SampleRun.Operations.cs index d20e2ce12..5c98a12af 100644 --- a/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2SampleRun.Operations.cs +++ b/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2SampleRun.Operations.cs @@ -35,7 +35,7 @@ public partial class Fido2SampleRun public bool RunMenuItem(Fido2MainMenuItem menuItem) { if (menuItem >= Fido2MainMenuItem.MakeCredential - && menuItem < Fido2MainMenuItem.Reset) + && menuItem < Fido2MainMenuItem.AuthenticatorSelection) { SampleMenu.WriteMessage( MessageType.Title, 0, @@ -71,6 +71,7 @@ public bool RunMenuItem(Fido2MainMenuItem menuItem) Fido2MainMenuItem.ToggleAlwaysUv => RunToggleAlwaysUv(), Fido2MainMenuItem.SetPinConfig => RunSetPinConfig(), Fido2MainMenuItem.Reset => RunReset(), + Fido2MainMenuItem.AuthenticatorSelection => RunAuthenticatorSelection(), _ => RunUnimplementedOperation(), }; } @@ -87,6 +88,17 @@ public static bool RunUnimplementedOperation() return true; } + public bool RunAuthenticatorSelection() + { + _keyCollector.Operation = Fido2KeyCollectorOperation.AuthenticatorSelection; + + _ = Fido2AuthenticatorSelection.Run( + _keyCollector.Fido2SampleKeyCollectorDelegate, + ref _yubiKeyChosen); + + return true; + } + public bool RunReset() { string versionNumber = _yubiKeyChosen.FirmwareVersion.ToString(); @@ -1580,4 +1592,4 @@ private UserEntity GetUpdatedInfo(UserEntity original) return returnValue; } } -} +} \ No newline at end of file diff --git a/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2SampleRun.cs b/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2SampleRun.cs index 864677592..9896d8fd7 100644 --- a/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2SampleRun.cs +++ b/Yubico.YubiKey/examples/Fido2SampleCode/Run/Fido2SampleRun.cs @@ -106,6 +106,7 @@ private bool DefaultChooseYubiKey(Fido2MainMenuItem menuItem) { case Fido2MainMenuItem.ListYubiKeys: case Fido2MainMenuItem.ChooseYubiKey: + case Fido2MainMenuItem.AuthenticatorSelection: case Fido2MainMenuItem.Exit: return true; @@ -135,4 +136,4 @@ private bool RunChooseYubiKey() return _chosenByUser; } } -} +} \ No newline at end of file diff --git a/Yubico.YubiKey/examples/Fido2SampleCode/YubiKeyOperations/Fido2AuthenticatorSelection.cs b/Yubico.YubiKey/examples/Fido2SampleCode/YubiKeyOperations/Fido2AuthenticatorSelection.cs new file mode 100644 index 000000000..293ebe103 --- /dev/null +++ b/Yubico.YubiKey/examples/Fido2SampleCode/YubiKeyOperations/Fido2AuthenticatorSelection.cs @@ -0,0 +1,123 @@ +// Copyright 2026 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Linq; +using Yubico.YubiKey; +using Yubico.YubiKey.Fido2; +using Yubico.YubiKey.Fido2.Commands; +using Yubico.YubiKey.Sample.SharedCode; + +namespace Yubico.YubiKey.Sample.Fido2SampleCode +{ + // This file demonstrates CTAP 2.1 §6.9 authenticatorSelection (0x0B) via + // Fido2Session.TryAuthenticatorSelection over USB (HID FIDO). The YubiKey + // prompts for user presence (touch) by blinking; on success the host can + // treat that YubiKey as the chosen authenticator for subsequent commands. + public static class Fido2AuthenticatorSelection + { + public static bool Run( + Func keyCollector, + ref IYubiKeyDevice yubiKeyChosen) + { + if (keyCollector is null) + { + throw new ArgumentNullException(nameof(keyCollector)); + } + + // Look for YubiKeys over the FIDO HID (USB) transport. + IYubiKeyDevice[] keys = YubiKeyDevice.FindByTransport(Transport.HidFido).ToArray(); + if (keys.Length == 0) + { + SampleMenu.WriteMessage(MessageType.Title, 0, "\nNo YubiKeys found over HID FIDO.\n"); + PauseBeforeMainMenu(); + return true; + } + + SampleMenu.WriteMessage(MessageType.Title, 0, "\nTouch a YubiKey when it blinks.\n"); + + bool anyUnsupported = false; + foreach (IYubiKeyDevice device in keys) + { + try + { + if (TrySelection(device, keyCollector, out AuthenticatorSelectionResponse response)) + { + yubiKeyChosen = device; + SampleMenu.WriteMessage(MessageType.Title, 0, "\nOK\n"); + PauseBeforeMainMenu(); + return true; + } + + // CTAP INVALID_COMMAND: this firmware does not support authenticatorSelection (requires 5.5.1+); try the next key. + if (response.CtapStatus == CtapStatus.InvalidCommand) + { + anyUnsupported = true; + continue; + } + } + catch (TimeoutException) + { + // No touch (or wrong YubiKey) before the session timeout; try another YubiKey. + } + catch (OperationCanceledException ex) + { + // Key collector returned false (e.g. user ignored the prompt). + SampleMenu.WriteMessage(MessageType.Title, 0, ex.Message + "\n"); + PauseBeforeMainMenu(); + return true; + } + catch (Fido2Exception ex) + { + // Other FIDO2 errors: show and continue to the next YubiKey, if any. + SampleMenu.WriteMessage(MessageType.Title, 0, ex.Message + "\n"); + } + } + + if (anyUnsupported) + { + SampleMenu.WriteMessage( + MessageType.Title, + 0, + "\nOne or more YubiKeys does not support authenticatorSelection (requires firmware 5.5.1 or later).\n"); + PauseBeforeMainMenu(); + return true; + } + + SampleMenu.WriteMessage(MessageType.Title, 0, "\nSelection did not complete.\n"); + PauseBeforeMainMenu(); + return true; + } + + // Wait so the user can read messages before the sample redraws the main menu. + private static void PauseBeforeMainMenu() + { + SampleMenu.WriteMessage(MessageType.Title, 0, "Press Enter to return to the main menu."); + _ = SampleMenu.ReadResponse(out string _); + } + + private static bool TrySelection( + IYubiKeyDevice device, + Func keyCollector, + out AuthenticatorSelectionResponse response) + { + using var session = new Fido2Session(device) + { + KeyCollector = keyCollector, + }; + + return session.TryAuthenticatorSelection(out response); + } + } +} \ No newline at end of file diff --git a/Yubico.YubiKey/src/Resources/ResponseStatusMessages.Designer.cs b/Yubico.YubiKey/src/Resources/ResponseStatusMessages.Designer.cs index 56f06c031..ebf0c842e 100644 --- a/Yubico.YubiKey/src/Resources/ResponseStatusMessages.Designer.cs +++ b/Yubico.YubiKey/src/Resources/ResponseStatusMessages.Designer.cs @@ -465,6 +465,15 @@ internal static string Fido2AuthInvalid { } } + /// + /// Looks up a localized string similar to User presence was denied for authenticator selection.. + /// + internal static string Fido2AuthenticatorSelectionDenied { + get { + return ResourceManager.GetString("Fido2AuthenticatorSelectionDenied", resourceCulture); + } + } + /// /// Looks up a localized string similar to The credential was rejected because it is on the relying party's exclude list.. /// @@ -807,4 +816,4 @@ internal static string YubiHsmAuthTouchRequired { } } } -} +} \ No newline at end of file diff --git a/Yubico.YubiKey/src/Resources/ResponseStatusMessages.resx b/Yubico.YubiKey/src/Resources/ResponseStatusMessages.resx index 06d4ebb34..9f2f2ee3f 100644 --- a/Yubico.YubiKey/src/Resources/ResponseStatusMessages.resx +++ b/Yubico.YubiKey/src/Resources/ResponseStatusMessages.resx @@ -312,6 +312,9 @@ The authentication was invalid for the requested FIDO2 operation. + + User presence was denied for authenticator selection. + The operation was canceled by the caller or user. diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionCommand.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionCommand.cs new file mode 100644 index 000000000..576f5e29b --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionCommand.cs @@ -0,0 +1,55 @@ +// Copyright 2026 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Fido2.Commands +{ + /// + /// Requests User Presence (UP) on the connected YubiKey so the user may indicate their intention to use the YubiKey by touching it. This method can be useful in situations where a user has more than one YubiKey and the application needs to determine which key to use for a subsequent FIDO2 operation. + /// + /// + /// The partner response class is . + /// Specified in CTAP 2.1 §6.9 as authenticatorSelection (command byte 0x0B). Supported by YubiKey firmware 5.5.1 and later. + /// There are no command parameters. Whether the authenticator implements this command + /// is firmware-specific; unsupported devices typically return . + /// + public sealed class AuthenticatorSelectionCommand : IYubiKeyCommand + { + /// + public YubiKeyApplication Application => YubiKeyApplication.Fido2; + + /// + /// Constructs an instance of the class. + /// + public AuthenticatorSelectionCommand() + { + } + + /// + public CommandApdu CreateCommandApdu() + { + byte[] payload = new byte[] { CtapConstants.CtapAuthenticatorSelectionCmd }; + return new CommandApdu() + { + Ins = CtapConstants.CtapHidCbor, + Data = payload + }; + } + + /// + public AuthenticatorSelectionResponse CreateResponseForApdu(ResponseApdu responseApdu) => + new AuthenticatorSelectionResponse(responseApdu); + } +} \ No newline at end of file diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionResponse.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionResponse.cs new file mode 100644 index 000000000..bebbbbcb8 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionResponse.cs @@ -0,0 +1,45 @@ +// Copyright 2026 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Fido2.Commands +{ + /// + /// The response to the . + /// + /// + /// On success there is no response payload. If the authenticator does not implement + /// authenticatorSelection, expect or another CTAP error; + /// that reflects authenticator support, not an SDK defect. + /// + public sealed class AuthenticatorSelectionResponse : Fido2Response, IYubiKeyResponse + { + /// + /// Constructs an from the YubiKey APDU response. + /// + /// The response APDU returned by the YubiKey. + public AuthenticatorSelectionResponse(ResponseApdu responseApdu) : + base(responseApdu) + { + } + + /// + protected override ResponseStatusPair StatusCodeMap => CtapStatus switch + { + CtapStatus.OperationDenied => new ResponseStatusPair(ResponseStatus.Failed, ResponseStatusMessages.Fido2AuthenticatorSelectionDenied), + _ => base.StatusCodeMap, + }; + } +} \ No newline at end of file diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/CtapConstants.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/CtapConstants.cs index 19a50e15d..20fab620c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/CtapConstants.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Commands/CtapConstants.cs @@ -27,5 +27,6 @@ internal static class CtapConstants public const byte CtapMakeCredentialCmd = 0x01; public const byte CtapGetAssertionCmd = 0x02; public const byte CtapClientPinCmd = 0x06; + public const byte CtapAuthenticatorSelectionCmd = 0x0B; } } diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.AuthenticatorSelection.cs b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.AuthenticatorSelection.cs new file mode 100644 index 000000000..f97b242d2 --- /dev/null +++ b/Yubico.YubiKey/src/Yubico/YubiKey/Fido2/Fido2Session.AuthenticatorSelection.cs @@ -0,0 +1,98 @@ +// Copyright 2026 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using Microsoft.Extensions.Logging; +using Yubico.YubiKey.Fido2.Commands; + +namespace Yubico.YubiKey.Fido2 +{ + // CTAP 2.1 §6.9 authenticatorSelection (0x0B): User Presence (UP) for single or multi-YubiKey selection. YubiKey firmware 5.5.1+. + public sealed partial class Fido2Session + { + /// + /// Requests User Presence (UP) on the connected YubiKey so the user may indicate their intention to use the YubiKey by touching it. This method can be useful in situations where a user has more than one YubiKey and the application needs to determine which key to use for a subsequent FIDO2 operation. + /// (CTAP 2.1 §6.9 authenticatorSelection, command byte 0x0B). Requires YubiKey firmware 5.5.1 or later. + /// + /// + /// + /// Per the CTAP specification, after a successful selection the platform should send cancel + /// to other authenticators. This SDK does not manage multiple devices; callers orchestrate that. + /// + /// + /// This method calls the with + /// while waiting for touch, the same as . + /// + /// + /// Returns false if the YubiKey returns (command + /// not implemented) or (user declined). There is no SDK + /// firmware gate; behavior depends on the YubiKey's firmware version (5.5.1+). + /// + /// + /// The response from the YubiKey, including the CTAP status (see ). + /// true if the operation completed with ; otherwise false for unsupported or denied selection. + /// The is not set. + /// The authenticator timed out waiting for user action. + /// The operation was canceled (e.g. keepalive cancel). + /// Another CTAP error occurred. + public bool TryAuthenticatorSelection(out AuthenticatorSelectionResponse response) + { + Logger.LogInformation("Authenticator selection."); + + var keyCollector = EnsureKeyCollector(); + var keyEntryData = new KeyEntryData + { + Request = KeyEntryRequest.TouchRequest, + }; + + using var touchTask = new TouchFingerprintTask( + keyCollector, + keyEntryData, + Connection, + CtapConstants.CtapAuthenticatorSelectionCmd); + + try + { + response = Connection.SendCommand(new AuthenticatorSelectionCommand()); + CtapStatus ctapStatus = touchTask.IsUserCanceled ? CtapStatus.KeepAliveCancel : response.CtapStatus; + + switch (ctapStatus) + { + case CtapStatus.Ok: + return true; + + case CtapStatus.InvalidCommand: + case CtapStatus.OperationDenied: + return false; + + case CtapStatus.KeepAliveCancel: + throw new OperationCanceledException(ExceptionMessages.OperationCancelled); + + case CtapStatus.ActionTimeout: + case CtapStatus.UserActionTimeout: + throw new TimeoutException(ExceptionMessages.Fido2TouchTimeout); + + default: + throw new Fido2Exception(response.CtapStatus, response.StatusMessage); + } + } + finally + { + keyEntryData.Clear(); + keyEntryData.Request = KeyEntryRequest.Release; + touchTask.SdkUpdate(keyEntryData); + } + } + } +} \ No newline at end of file diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionCommandTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionCommandTests.cs new file mode 100644 index 000000000..ede5cbf88 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionCommandTests.cs @@ -0,0 +1,63 @@ +// Copyright 2026 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using Xunit; +using Yubico.Core.Iso7816; + +namespace Yubico.YubiKey.Fido2.Commands +{ + // Unit tests for AuthenticatorSelectionCommand (CTAP 2.1 §6.9 authenticatorSelection, command byte 0x0B). + public class AuthenticatorSelectionCommandTests + { + [Fact] + public void Constructor_Succeeds() + { + var command = new AuthenticatorSelectionCommand(); + + Assert.NotNull(command); + } + + [Fact] + public void CreateCommandApdu_CreatesCorrectApdu() + { + var command = new AuthenticatorSelectionCommand(); + CommandApdu apdu = command.CreateCommandApdu(); + + // Payload is the CTAP command byte only; no CBOR parameters (CTAP 2.1 §6.9 authenticatorSelection). + byte[] expectedData = new byte[] + { + 0x0B, // authenticatorSelection + }; + + Assert.Equal(0, apdu.Cla); + Assert.Equal(0x10, apdu.Ins); // CTAPHID_CBOR / FIDO2 extended APDU INS, same as other CTAP-via-APDU commands + Assert.Equal(0, apdu.P1); + Assert.Equal(0, apdu.P2); + Assert.True(apdu.Data.Span.SequenceEqual(expectedData)); + } + + [Fact] + public void CreateResponseForApdu_ReturnsAuthenticatorSelectionResponse() + { + var command = new AuthenticatorSelectionCommand(); + // Empty response body and 9000; maps to CTAP OK in AuthenticatorSelectionResponse. + var responseApdu = new ResponseApdu(System.Array.Empty(), SWConstants.Success); + + AuthenticatorSelectionResponse response = command.CreateResponseForApdu(responseApdu); + + Assert.NotNull(response); + } + } +} \ No newline at end of file diff --git a/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionResponseTests.cs b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionResponseTests.cs new file mode 100644 index 000000000..c2ca839f9 --- /dev/null +++ b/Yubico.YubiKey/tests/unit/Yubico/YubiKey/Fido2/Commands/AuthenticatorSelectionResponseTests.cs @@ -0,0 +1,61 @@ +// Copyright 2026 Yubico AB +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Xunit; +using Yubico.Core.Iso7816; +using Yubico.YubiKey; +using Yubico.YubiKey.Fido2; + +namespace Yubico.YubiKey.Fido2.Commands +{ + // Unit tests for AuthenticatorSelectionResponse (CTAP 2.1 §6.9 authenticatorSelection; partner to AuthenticatorSelectionCommand). + public class AuthenticatorSelectionResponseTests + { + [Fact] + public void Constructor_GivenSuccessApdu_SetsOkAndSuccess() + { + // Empty response body and 9000; maps to CTAP OK (mirrors successful authenticatorSelection). + var responseApdu = new ResponseApdu(System.Array.Empty(), SWConstants.Success); + var response = new AuthenticatorSelectionResponse(responseApdu); + + Assert.Equal(CtapStatus.Ok, response.CtapStatus); + Assert.Equal(ResponseStatus.Success, response.Status); + } + + [Fact] + public void Constructor_GivenInvalidCommand_SetsCtapStatus() + { + // CTAP error encoding: SW1=0x6F, SW2=CTAP status byte (see CtapToApduResponse.GetSwForCtapError). + short sw = unchecked((short)((SW1Constants.NoPreciseDiagnosis << 8) | (byte)CtapStatus.InvalidCommand)); // InvalidCommand / CTAP1_ERR_INVALID_COMMAND + var responseApdu = new ResponseApdu(System.Array.Empty(), sw); + var response = new AuthenticatorSelectionResponse(responseApdu); + + Assert.Equal(CtapStatus.InvalidCommand, response.CtapStatus); + Assert.Equal(ResponseStatus.Failed, response.Status); + } + + [Fact] + public void Constructor_GivenOperationDenied_UsesSelectionDeniedMessage() + { + // Same SW packing as InvalidCommand test; OperationDenied when user presence is refused for selection. + short sw = unchecked((short)((SW1Constants.NoPreciseDiagnosis << 8) | (byte)CtapStatus.OperationDenied)); // CTAP2_ERR_OPERATION_DENIED + var responseApdu = new ResponseApdu(System.Array.Empty(), sw); + var response = new AuthenticatorSelectionResponse(responseApdu); + + Assert.Equal(CtapStatus.OperationDenied, response.CtapStatus); + Assert.Equal(ResponseStatus.Failed, response.Status); + Assert.Equal(ResponseStatusMessages.Fido2AuthenticatorSelectionDenied, response.StatusMessage); + } + } +} \ No newline at end of file diff --git a/docs/users-manual/application-fido2/apdu/authenticator-selection.md b/docs/users-manual/application-fido2/apdu/authenticator-selection.md new file mode 100644 index 000000000..bffd1b2ab --- /dev/null +++ b/docs/users-manual/application-fido2/apdu/authenticator-selection.md @@ -0,0 +1,85 @@ +--- +uid: Fido2AuthenticatorSelectionApdu +--- + + + +## Select an authenticator + +### Command APDU info + +| CLA | INS | P1 | P2 | Lc | Data | Le | +|:---:|:---:|:--:|:--:|:--:|:----:|:--------:| +| 00 | 10 | 00 | 00 | 01 | 0B | (absent) | + +The Ins byte (instruction) is 10, which is the byte for CTAPHID_CBOR. +That means the command information is in the Data. + +The data consists of the CTAP Command Byte. In this case, the CTAP +Command Byte is `0B`, which is the command "`authenticatorSelection`". +There are no command parameters. + +### Response APDU info + +#### Response APDU for a successful selection + +Total Length: 2\ +Data Length: 0 + +| Data | SW1 | SW2 | +|:---------:|:---:|:---:| +| (no data) | 90 | 00 | + +#### Response APDU when the command is not supported + +If the authenticator does not implement `authenticatorSelection`, it +may return `CTAP1_ERR_INVALID_COMMAND` (`0x01`). + +Total Length: 2\ +Data Length: 0 + +| Data | SW1 | SW2 | +|:---------:|:---:|:---:| +| (no data) | 6F | 01 | + +#### Response APDU when the YubiKey times out + +This happens when the user does not touch the contact within the timeout +period. + +Total Length: 2\ +Data Length: 0 + +| Data | SW1 | SW2 | +|:---------:|:---:|:---:| +| (no data) | 6F | 2F | + +#### Response APDU when user presence is denied + +This happens when user presence (UP) is explicitly denied. + +Total Length: 2\ +Data Length: 0 + +| Data | SW1 | SW2 | +|:---------:|:---:|:---:| +| (no data) | 6F | 27 | + +> [!NOTE] +> On the YubiKey, the user can either touch the key to select it or wait for the +> operation to time out—there is no separate deny or cancel control on the security key +> itself. When the user does not complete UP you will usually see +> `CTAP2_ERR_USER_ACTION_TIMEOUT`. However, `CTAP2_ERR_OPERATION_DENIED` +> may be returned if the user engages a platform dialog to cancel the request. diff --git a/docs/users-manual/application-fido2/fido2-commands.md b/docs/users-manual/application-fido2/fido2-commands.md index e1c5b2350..3c6cf8d0c 100644 --- a/docs/users-manual/application-fido2/fido2-commands.md +++ b/docs/users-manual/application-fido2/fido2-commands.md @@ -40,6 +40,7 @@ what information is needed from the caller for that command. * [Enumerate RPs Get Next RP](#enumerate-rps-get-next-rp) * [Get Large Blob](#get-large-blob) * [Set Large Blob](#set-large-blob) +* [Authenticator Selection](#authenticator-selection) * [Reset](#reset) ___ @@ -613,6 +614,35 @@ None [Technical APDU Details](apdu/set-large-blob.md) ___ +## Authenticator selection + +Request user presence (UP) so the user can indicate _which_ authenticator to use for a subsequent operation. This can be useful in situations where more than one YubiKey is connected. + +### Available + +YubiKeys with FIDO2 firmware `5.5.1` or later. The underlying command, `authenticatorSelection` (0x0B), is specified in [CTAP 2.1 §6.9](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorSelection). + +### SDK classes + +[Fido2Session.TryAuthenticatorSelection](xref:Yubico.YubiKey.Fido2.Fido2Session.TryAuthenticatorSelection%2a) + +[AuthenticatorSelectionCommand](xref:Yubico.YubiKey.Fido2.Commands.AuthenticatorSelectionCommand) + +[AuthenticatorSelectionResponse](xref:Yubico.YubiKey.Fido2.Commands.AuthenticatorSelectionResponse) + +### Input + +None + +### Output + +None + +### APDU + +[Technical APDU Details](apdu/authenticator-selection.md) +___ + ## Reset Reset the FIDO2 application on a YubiKey. This will delete all existing FIDO2 keys and diff --git a/docs/users-manual/application-fido2/fido2-touch-notification.md b/docs/users-manual/application-fido2/fido2-touch-notification.md index 951bdeb6b..7db09927d 100644 --- a/docs/users-manual/application-fido2/fido2-touch-notification.md +++ b/docs/users-manual/application-fido2/fido2-touch-notification.md @@ -23,8 +23,9 @@ verify their fingerprint on the YubiKey's fingerprint reader (this applies to th Bio series only). In addition, some operations, such as -[MakeCredential](xref:Yubico.YubiKey.Fido2.Fido2Session.MakeCredential%2a) or -[GetAssertion](xref:Yubico.YubiKey.Fido2.Fido2Session.GetAssertions%2a), will not complete +[MakeCredential](xref:Yubico.YubiKey.Fido2.Fido2Session.MakeCredential%2a), +[GetAssertion](xref:Yubico.YubiKey.Fido2.Fido2Session.GetAssertions%2a), or +[TryAuthenticatorSelection](xref:Yubico.YubiKey.Fido2.Fido2Session.TryAuthenticatorSelection%2a), will not complete until the user touches the contact. For example, a YubiKey will begin an operation, but at some point will stop processing until the contact has been touched. Once touched, it will finish the operation. diff --git a/docs/users-manual/toc.yml b/docs/users-manual/toc.yml index 2dc87f0fe..628642687 100644 --- a/docs/users-manual/toc.yml +++ b/docs/users-manual/toc.yml @@ -381,6 +381,8 @@ href: application-fido2/apdu/enum-rps-begin.md - name: Enumerate RPs next href: application-fido2/apdu/enum-rps-next.md + - name: Authenticator selection + href: application-fido2/apdu/authenticator-selection.md - name: Reset href: application-fido2/apdu/reset.md