Show / Hide Table of Contents

The KeyCollector and alternatives

Note

The sample code contains sample key collectors. One sample key collector can be found at Yubico.YubiKey/examples/PivSampleCode/KeyCollector. It collects PINs and keys from the command line. See also the section below, Building a KeyCollector.

In the SDK, there is the concept of a KeyCollector. This is a user-supplied delegate, a callback method the SDK calls when it needs a PIN, password, key, or some other secret value in order to complete verification or authentication.

Note

The key collector is also used to notify the caller that touch or a fingerprint is needed. See the article on the KeyCollector and touch for a more detailed description of how to handle touch notifications.

For example, with the PIV application, you can make a call to sign data. That requires the PIN to be verified in order to execute.

    using (var pivSession = new PivSession(yubiKey))
    {
        pivSession.KeyCollector = CallerSuppliedKeyCollector;

        byte[] signature = pivSession.Sign(PivSlot.Authentication, dataToSign);
    }

The SDK's Sign method will determine it needs the PIN, so it will make a call to CallerSuppliedKeyCollector, requesting the PIN. That method will do what it needs to get the PIN from the end user, such as creating a new Window with a PIN box or writing/reading from the command line. The KeyCollector returns the PIN and the SDK verifies it. The data can now be signed.

It is also possible to call the VerifyPin method directly.

    using (var pivSession = new PivSession(yubiKey))
    {
        pivSession.KeyCollector = SomeKeyCollector;

        pivSession.VerifyPin();
    }

As with the Sign method, the VerifyPin will call on the KeyCollector to retrieve the PIN. Normally, an application would never call the VerifyPin method because there's no need. The SDK will automatically make the necessary calls to verify a PIN when it needs the PIN to be verified, and simply won't verify if it does not need it.

Once a PIN has been verified in a session, generally, there is no need to verify it again in that session (there are exceptions discussed below). For example, suppose you want to create two signatures.

    using (var pivSession = new PivSession(yubiKey))
    {
        pivSession.KeyCollector = SomeKeyCollector;

        byte[] signature1 = pivSession.Sign(PivSlot.Authentication, dataToSign1);
        byte[] signature2 = pivSession.Sign(PivSlot.Authentication, dataToSign2);
    }

    using (var pivSession = new PivSession(yubiKey))
    {
        pivSession.KeyCollector = SomeKeyCollector;

        pivSession.VerifyPin();

        byte[] signature1 = pivSession.Sign(PivSlot.Authentication, dataToSign1);
        byte[] signature2 = pivSession.Sign(PivSlot.Authentication, dataToSign2);
    }

In the first session above, the first call to Sign will, "under the covers", make a call to verify the PIN. For the second call to Sign, there is no need, the PIN has already been verified. In the second session above, the PIN has been verified by the call to VerifyPin, so both calls to Sign will execute without calling the VerifyPin method under the covers.

Verification and authentication without the KeyCollector

The SDK contains methods to verify and authenticate where the secret value is provided by the caller, rather than the KeyCollector. For example, with PIV, there are these methods.

    bool TryVerifyPin(ReadOnlyMemory<byte> pin, out int? retriesRemaining);
    bool TryChangePin(ReadOnlyMemory<byte> currentPin, ReadOnlyMemory<byte> newPin, out int? retriesRemaining);
    bool TryChangePuk(ReadOnlyMemory<byte> currentPuk, ReadOnlyMemory<byte> newPuk, out int? retriesRemaining);
    bool TryResetPin(ReadOnlyMemory<byte> puk, ReadOnlyMemory<byte> newPin, out int? retriesRemaining);
    bool TryChangePinAndPukRetryCounts(
            ReadOnlyMemory<byte> managementKey,
            ReadOnlyMemory<byte> pin,
            byte newRetryCountPin,
            byte newRetryCountPuk,
            out int? retriesRemaining);

    bool TryAuthenticateManagementKey(ReadOnlyMemory<byte> managementKey, bool mutualAuthentication = true);
    bool TryChangeManagementKey(
            ReadOnlyMemory<byte> currentKey,
            ReadOnlyMemory<byte> newKey,
            PivTouchPolicy touchPolicy = PivTouchPolicy.Default)

There is no need to build a KeyCollector. You simply verify the PIN and authenticate the management key each session.

The case for a KeyCollector

If you want to use the caller-supplied verify and authenticate methods, you are still going to need to collect the PIN, password, or key. That is, your application most likely contains a "Key Collector" component already. For example, you have code to collect the PIN from the user, otherwise where did the PIN come from?

Because you probably already have code to collect the PIN/Password/Key, it is likely not going to be difficult to extend it to fit within the SDK's KeyCollector framework.

Furthermore, there are advantages to using the KeyCollector.

The SDK manages PIN, password, and key requirements

With PIV, for example, some operations require the PIN, other operations require the management key, and still other operations that require both.

With a KeyCollector, you do not need to make sure your code is written to fulfill the correct verify/auth logic. The SDK takes care of that for you.

Collected only if needed

The SDK will only ask for a secret to be collected if it is needed. This reduces the exposure to attack. See the User's Manual article on sensitive data.

One alternative is for your app to manage the logic of when each secret is needed. Another alternative is to simply use the caller-supplied secret verification methods at the beginning of each session. Generally, once a secret has been verified in a session, there is no need to verify it again. Hence, just make sure each secret is authenticated in each session and there is no futher management needed.

For example, with PIV, you could do this.

    using (var pivSession = new PivSession(yubiKey))
    {
        bool isAuth = pivSession.TryAuthenticateManagementKey(mgmtKey);
        bool isVerified = pivSession.TryVerifyPin(pin);

         . . .
    }

One downside to this is that you will be authenticating a secret even when it is not needed. If your application will be doing something that needs the PIN, but it won't be doing anything that needs the management key, you will be requiring the user to provide the management key anyway, making them perform some task that is not needed. In addition, it increases the exposure to attack.

Another downside is that there are rare cases when a PIN might be needed more than once. These exceptions are discussed below.

The SDK manages retries

Suppose your application creates a PIV session and you will be doing something that requires PIN verification. You collect the PIN from the user and call the following.

    using (var pivSession = new PivSession(yubiKey))
    {
        bool isVerified = pivSession.TryVerifyPin(pin, out int? retriesRemaining);
    }

Suppose the return is false. Maybe the user typed Paris167, but the PIN is really Paris16` Now what? How about the following?

    using (var pivSession = new PivSession(yubiKey))
    {
        ReadOnlyMemory<byte> pin = CollectPin();
        while (!pivSession.TryVerifyPin(pin, out int? retriesRemaining))
        {
            if (!CollectPin(someMessage, retriesRemaining, out ReadOnlyMemory<byte> pin))
            {
                throw OperationCanceledException(message);
            }
        }
    }

Much of this logic is already handled by the SDK. You can call the following.

    using (var pivSession = new PivSession(yubiKey))
    {
        pivSession.KeyCollector = SomeKeyCollector;

        // false is returned if the user cancels.
        bool isVerified = pivSession.TryVerifyPin();

        // An exception is thrown if the user cancels.
        pivSession.VerifyPin();
    }

In these cases the SDK will handle the retries. It will call the KeyCollector until the correct PIN is entered, the delegate returns canceled, or the retry count goes to zero. The KeyCollector you provide will have to decide how to present the retry count to the user and offer a way to cancel. But any solution will need to do that.

The SDK tells the KeyCollector to Release

Once the secret has been authenticated, it is a good idea to overwrite sensitive data. With a KeyCollector you write that code once. The SDK will call the KeyCollector with KeyEntryRequest.Release, indicating that the SDK no longer needs the collected value. The KeyCollector knows it can now overwrite any data and release any other resources.

If you don't use the KeyCollector, you will have to write the release code every time. For example, here is a possibility.

    using (var pivSession = new PivSession(yubiKey))
    {
        var pinData = new Memory<byte>(new byte[8]);

        try
        {
            int pinLength = CollectPin(pinData);
            while (!pivSession.TryVerifyPin(pinData.Slice(0, pinLength, out int? retriesRemaining))
            {
                pinLength = CollectPin(someMessage, retriesRemaining, pinData))
                if (pinLength == 0)
                {
                    throw OperationCanceledException(message);
                }
            }
        }
        finally
        {
            CryptographicOperations.ZeroMemory(pinData.Span);
        }
    }

Called "just in time"

The SDK will not request a secret until it is needed. That means you don't need to collect it and have it waiting around in memory, just in case it is needed. Of course, you can collect it at the beginning of a session, authenticate it, and release it. But that has its own problems.

Called if needed more than once

There are rare cases where authentication of a secret is needed more than once per session. In such a case, using a KeyCollector will be the most convenient.

For example, with PIV, it is possible to generate or load a private key with the PIN policy "Always". This means the PIN must be verified each time it is used, even if the PIN has already been verified in the session.

Under the covers, the Verify command must be the only command executed before the Sign command.

If using the KeyCollector, the SDK will make sure that happens. Without it, you will be required to manage it.

Might be needed for PIV PIN-only mode

Many applications will set a YubiKey to PIN-only. This means that PIV operations that require management key authentication will be able to execute with the caller supplying onl the PIN.

If a YubiKey is set to the PIN-only mode of PinDerived, the SDK will require a KeyCollector to obtain the PIN. Note that Yubico recommends applications NOT use PinDerived. It is provided only for backwards compatibility. You should only use PinProtected.

If a YubiKey is set for PinProtected, it will generally be possible to use the YubiKey without a KeyCollector. However, there are odd cases where a YubiKey can be set for PinProtected and a KeyCollector is needed to obtain the PIN.

Building a KeyCollector

At a minimum, your key collector will be a single method with this signature.

   public bool KeyCollectingFunction(KeyEntryData keyEntryData);

The return is a boolean, if it was able to collect the value or values requested, return true. If not, return false. Almost always, a false means the user canceled the operation.

It's certainly possible to write a "standalone" method (e.g. a static method in a static class), but more likely, your key collector will be a class.

using Yubico.YubiKey;

    public class MyKeyCollector
    {
        // fields and properties

        // Create a new KeyCollector object that pops up a window.
        // This window will be a child of the parentWindow and will
        // have boxes for entering the PINs and keys, along with OK
        // and Cancel buttons.
        public MyKeyCollector(Handle parentWindow)
        {
        }

        // Create a new KeyCollector object that pops up a window.
        // This window will be a standalone window, no parent. It
        // will have boxes for entering the PINs and keys, along
        // with OK and Cancel buttons.
        public MyKeyCollector()
        {
        }

        // This is the method passed to the Yubico SDK as the
        // KeyCollector delegate.
        public bool KeyCollectorDelegate(KeyEntryData keyEntryData)
        {
        }
    }

Using this class would look something like this.

        var keyCollectorObject = new MyKeyCollector(parentWindow);

         . . .

        using (var pivSession = new PivSession(yubiKey))
        {
            pivSession.KeyCollector = keyCollectorObject.KeyCollectorDelegate;

             . . .
        }

There's a good chance you already have some class that is a key collector. It might not be called "key collector", but it is likely you already have a class built to collect PINs, passwords or other such user-supplied secrets. If so, you will possibly need to simply add a method that fulfills the SDK's delegate requirement.

The KeyCollector delegate

Your method, at its foundation, will be something like this.

        public bool KeyCollectorDelegate(KeyEntryData keyEntryData)
        {
            switch (keyEntryData.Request)
            {
                default:
                    return false;

                case KeyEntryRequest.Release:
                    // Do release work.

                case KeyEntryRequest.VerifyPivPin:
                    // Collect a PIN to be used to verify a PIV session.

                case KeyEntryRequest.ChangePivPin:
                    // Collect two PINs, the current and a new one, to be used
                    // to change the PIV PIN.

                case KeyEntryRequest.ChangePivPuk:
                    // Collect two PUKs, the current and a new one, to be used
                    // to change the PIV PUK.

                case KeyEntryRequest.ResetPivPinWithPuk:
                    // Collect the PUK and a new PIN to be used to recover the
                    // PIV PIN.

                case KeyEntryRequest.AuthenticatePivManagementKey:
                    // Collect a management key to be used to authenticate a
                    // PIV session.

                case KeyEntryRequest.ChangePivManagementKey:
                    // Collect two management keys, the current and a new one,
                    // to be used to change the PIV management key.
            }
        }

When the SDK calls your key collector, it will pass an enum parameter indicating what is requested. Your code will now know what it has to present to the user. Will it be a message saying, "Enter the PIV PIN"? Or a message saying, "Enter the current PIV PIN and a new PIN, in order to change the PIN"?

There are at least eight enum values indicating what the SDK needs the key collector to collect (and probably more in the future as more features are added to the SDK). Your key collector does not have to support all of them. If you build a key collector that only verifies or changes a PIV PIN, then your switch statement only needs to support those values (and Release).

The value indicating the request is the KeyEntryData.Request property. There is more information in the KeyEntryData object.

    public sealed class KeyEntryData
    {
        public KeyEntryRequest Request { get; set; }
        public bool IsRetry { get; set; }
        public int? RetriesRemaining { get; set; }
    }

Suppose the Request is KeyEntryRequest.VerifyPivPin. Your code now knows that the SDK wants you to collect the PIV PIN. But your code can also look at the IsRetry property. If that is true, your code now knows that a PIN had already been collected, but it was incorrect. Maybe you want to present a message to the user, "The previous PIN attempt failed. Do you want to try again?" Furthermore, you can look at the RetriesRemaining property. You can let the user know how many retries they have before the PIN is blocked.

   Enter the PIV PIN
   The previous PIN attempt failed.
   You have  4 attempts remaining before
   the PIN is blocked.

   PIN:_______________

   OK        CANCEL

If the caller decides to cancel, your key collector delegate can return false.

Once your code has collected the PIN, it must return it. That is done using the SubmitValue and SubmitValues methods inside the KeyEntryData.

Just as the KeyEntryData has information letting you know all about what is being requested, it contains methods that allow you to return the values collected. If you are to return one value (e.g. a PIV PIN for verification), then return that value using KeyEntryData.SubmitValue. If you are to return two values (e.g. a current and a new PIV PIN), return them using KeyEntryData.SubmitValues.

using System.Security.Cryptography;
using Yubico.YubiKey;

    public class MyKeyCollector
    {
        private byte[] _currentValue = new byte[MaxValueLength]
        private int _currentLength;
        public Memory<byte> CurrentValue = new Memory<byte>(_currentValue);

        public bool SampleKeyCollectorDelegate(KeyEntryData keyEntryData)
        {
            if (keyEntryData is null)
            {
                return false;
            }

            switch (keyEntryData.Request)
            {
                case KeyEntryRequest.Release:
                    CryptographicOperations.ZeroMemory(CurrentValue.Span)
                    break;

                case KeyEntryRequest.VerifyPivPin:
                    // The CollectValue method will collect the PIN and store
                    // it in the CurrentValue property.
                    isCollected = CollectValue(
                        "PIN", keyEntryData.IsRetry, keyEntryData.RetriesRemaining);
                    if (isCollected)
                    {
                        keyEntryData.SubmitValue(CurrentValue.Slice(0, _currentLength).Span);
                    }
                    break;
            }

            return isCollected;
        }
    }

Release

One possible value of KeyEntryRequest is Release. This is how the SDK tells the delegate that it has used the PIN (or key or whatever was requested), and your code can release any resources. At this point, the delegate will likely overwrite sensitive data, close handles, and so on.

Your KeyCollector delegate MUST NOT throw an exception when the request is Release. Most KeyCollector delegates will likely be written to never throw an exception in any situation (just return false if something goes wrong), but it is vitally important that it never throw an exception when the request is Release. The Release is called from inside a finally block, and it is a bad idea to throw exceptions from inside finally.

  • Improve this Doc
In this article
Back to top Generated by DocFX