Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ public enum Fido2KeyCollectorOperation
GetAssertion = 2,
Reset = 3,
Verify = 4,
AuthenticatorSelection = 5,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ public enum Fido2MainMenuItem
ToggleAlwaysUv = 24,
SetPinConfig = 25,

Reset = 26,
AuthenticatorSelection = 26,

Exit = 27,
Reset = 27,

Exit = 29,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -71,6 +71,7 @@ public bool RunMenuItem(Fido2MainMenuItem menuItem)
Fido2MainMenuItem.ToggleAlwaysUv => RunToggleAlwaysUv(),
Fido2MainMenuItem.SetPinConfig => RunSetPinConfig(),
Fido2MainMenuItem.Reset => RunReset(),
Fido2MainMenuItem.AuthenticatorSelection => RunAuthenticatorSelection(),
_ => RunUnimplementedOperation(),
};
}
Expand All @@ -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();
Expand Down Expand Up @@ -1580,4 +1592,4 @@ private UserEntity GetUpdatedInfo(UserEntity original)
return returnValue;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ private bool DefaultChooseYubiKey(Fido2MainMenuItem menuItem)
{
case Fido2MainMenuItem.ListYubiKeys:
case Fido2MainMenuItem.ChooseYubiKey:
case Fido2MainMenuItem.AuthenticatorSelection:
case Fido2MainMenuItem.Exit:
return true;

Expand Down Expand Up @@ -135,4 +136,4 @@ private bool RunChooseYubiKey()
return _chosenByUser;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<KeyEntryData, bool> 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<KeyEntryData, bool> keyCollector,
out AuthenticatorSelectionResponse response)
{
using var session = new Fido2Session(device)
{
KeyCollector = keyCollector,
};

return session.TryAuthenticatorSelection(out response);
}
}
}
11 changes: 10 additions & 1 deletion Yubico.YubiKey/src/Resources/ResponseStatusMessages.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Yubico.YubiKey/src/Resources/ResponseStatusMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@
<data name="Fido2AuthInvalid" xml:space="preserve">
<value>The authentication was invalid for the requested FIDO2 operation.</value>
</data>
<data name="Fido2AuthenticatorSelectionDenied" xml:space="preserve">
<value>User presence was denied for authenticator selection.</value>
</data>
<data name="Fido2OperationCanceled" xml:space="preserve">
<value>The operation was canceled by the caller or user.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// The partner response class is <see cref="AuthenticatorSelectionResponse"/>.
/// Specified in CTAP 2.1 §6.9 as <c>authenticatorSelection</c> (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 <see cref="Yubico.YubiKey.Fido2.CtapStatus.InvalidCommand"/>.
/// </remarks>
public sealed class AuthenticatorSelectionCommand : IYubiKeyCommand<AuthenticatorSelectionResponse>
{
/// <inheritdoc />
public YubiKeyApplication Application => YubiKeyApplication.Fido2;

/// <summary>
/// Constructs an instance of the <see cref="AuthenticatorSelectionCommand"/> class.
/// </summary>
public AuthenticatorSelectionCommand()
{
}

/// <inheritdoc />
public CommandApdu CreateCommandApdu()
{
byte[] payload = new byte[] { CtapConstants.CtapAuthenticatorSelectionCmd };
return new CommandApdu()
{
Ins = CtapConstants.CtapHidCbor,
Data = payload
};
}

/// <inheritdoc />
public AuthenticatorSelectionResponse CreateResponseForApdu(ResponseApdu responseApdu) =>
new AuthenticatorSelectionResponse(responseApdu);
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The response to the <see cref="AuthenticatorSelectionCommand"/>.
/// </summary>
/// <remarks>
/// On success there is no response payload. If the authenticator does not implement
/// <c>authenticatorSelection</c>, expect <see cref="Yubico.YubiKey.Fido2.CtapStatus.InvalidCommand"/> or another CTAP error;
/// that reflects authenticator support, not an SDK defect.
/// </remarks>
public sealed class AuthenticatorSelectionResponse : Fido2Response, IYubiKeyResponse
{
/// <summary>
/// Constructs an <see cref="AuthenticatorSelectionResponse"/> from the YubiKey APDU response.
/// </summary>
/// <param name="responseApdu">The response APDU returned by the YubiKey.</param>
public AuthenticatorSelectionResponse(ResponseApdu responseApdu) :
base(responseApdu)
{
}

/// <inheritdoc />
protected override ResponseStatusPair StatusCodeMap => CtapStatus switch
{
CtapStatus.OperationDenied => new ResponseStatusPair(ResponseStatus.Failed, ResponseStatusMessages.Fido2AuthenticatorSelectionDenied),
_ => base.StatusCodeMap,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading
Loading