Yubico / Yubico.NET.SDK

A YubiKey SDK for .NET developers
Apache License 2.0
96 stars 48 forks source link

How to get the number of PIN attempts remaining on Yubikey 4? #2

Closed DSBloom closed 2 years ago

DSBloom commented 2 years ago

I know I already emailed @GregDomzalski about this issue, but I thought maybe someone else out there might have some ideas or might also want this feature implemented.

I need a way to determine if the PIN on a Yubikey 4 or Yubikey 5 key has been blocked. Currently I am using ykman.exe and parsing its output, but I would love to move over to this SDK.

I poked around the Python code for Yubikey Manager in the hopes I could figure out how it is accomplished in that tool with hopes that it would help me figure out how to do it with the SDK but I didn't have any luck.

GregDomzalski commented 2 years ago

We will work on some sample code and reply hopefully by the end of this week. Thank you for your patience.

GregDomzalski commented 2 years ago

Here's a translation of what ykman does into the .NET version of the SDK. I've tested this with various versions of YubiKey 5's and YubiKey 4's. We'll try to incorporate this code into our sample projects first, but we are also evaluating bringing some of these enhancements into the SDK itself.

Please let me know how this works out for you!

using System;
using Yubico.Core.Iso7816;
using Yubico.YubiKey;
using Yubico.YubiKey.Piv;
using Yubico.YubiKey.Piv.Commands;

namespace PivExtensionSamples
{
    /// <summary>
    /// Adds extension methods to the PivSession class that allows for reading out the PIN and PUK retry counters,
    /// if they are available.
    /// </summary>
    public static class PinRetriesExtensions
    {
        /// <summary>
        /// Returns a tuple of the remaining and total retry counts for PIN.
        /// </summary>
        /// <remarks>
        /// The total is only available if PIV Metadata is available. This is in firmware 5.3.x and higher.
        /// </remarks>
        public static (int? remaining, int? total) GetPinRetries(this PivSession session) =>
            GetRetriesForSlot(session, PivSlot.Pin);

        /// <summary>
        /// Returns a tuple of the remaining and total retry counts for the PUK.
        /// </summary>
        /// <remarks>
        /// the total is only available if PIV Metadata is available. This is in firmware 5.3.x and higher.
        /// </remarks>
        public static (int? remaining, int? total) GetPukRetries(this PivSession session) =>
            GetRetriesForSlot(session, PivSlot.Puk);

        private static (int? remaining, int? total) GetRetriesForSlot(this PivSession session, byte slot)
        {
            // There are two main ways to get the retry count. The most reliable way is reading out the PIV metadata
            // object. This is only available in firmware versions 5.3.x and later. Unfortunately, we do not have an easy
            // way to check for the firmware version from the PivSession class, so we will rely on the NotSupportedException
            // to fall back to the second method: Attempting to "verify" the PIN or the PUK slot. This is available on
            // older versions of the YubiKey. For completeness sake, there is a third method: Reading the lower byte from
            // the status word of the verify command, however the first two methods should be reliable for most modern
            // YubiKey 4's and 5's. Lastly, it is not guaranteed that we will be able to retrieve the retry count.
            try
            {
                // For versions 5.3.x and higher
                return GetRetriesFromMetadata(session, slot);
            }
            catch (NotSupportedException)
            {
                // For versions 5.2.x and lower.
                // Only the PIN is supported by this method. PUK cannot be retrieved.
                return (slot == PivSlot.Pin ? GetRetriesFromVerify(session, slot) : null, null);
            }
        }

        private static int? GetRetriesFromVerify(PivSession session, byte slot)
        {
            var response = session.Connection.SendCommand(new VerifyWithoutChecks(slot));

            return response.GetData();
        }

        private static (int remaining, int total) GetRetriesFromMetadata(PivSession session, byte slot)
        {
            var metadata = session.GetMetadata(slot);
            return (metadata.RetriesRemaining, metadata.RetryCount);
        }

        // The built-in verify command tries to validate the PIN length, and will not allow sending a zero-length buffer
        // which is required to get the retry count. We can create an alternate command to use instead, and leverage
        // the existing response class.
        private class VerifyWithoutChecks : IYubiKeyCommand<VerifyPinResponse>
        {
            private readonly byte _slot;

            public VerifyWithoutChecks(byte slot)
            {
                _slot = slot;
            }

            public CommandApdu CreateCommandApdu() =>
                new() { Ins = 0x20, P2 = _slot };

            public VerifyPinResponse CreateResponseForApdu(ResponseApdu responseApdu) =>
                new(responseApdu);

            public YubiKeyApplication Application => YubiKeyApplication.Piv;
        }
    }
}

The following code uses these extension methods:

using System;
using System.Linq;
using Yubico.YubiKey;
using Yubico.YubiKey.Piv;
using PivExtensionSamples;

var yubiKey = YubiKeyDevice.FindAll().First();

using (var piv = new PivSession(yubiKey))
{
    var pinRetries = piv.GetPinRetries();
    Console.WriteLine($"PIN retries: {pinRetries.remaining?.ToString() ?? "Unknown"} out of {pinRetries.total?.ToString() ?? "Unknown"}.");

    var pukRetries = piv.GetPukRetries();
    Console.WriteLine($"PUK retries: {pukRetries.remaining?.ToString() ?? "Unknown"} out of {pukRetries.total?.ToString() ?? "Unknown"}.");
}
DSBloom commented 2 years ago

This is working well for me. Thanks!

gnida-rada commented 2 years ago

Confirm: works well with firmware v4 for me. Thanks!