YubiKey 5 Nano “YubiKey 5 Nano” by Dennis S. Hurd is licensed under CC BY 2.0

By Joel Nordell, Engineering

Introduction

In a recent post, Drew discussed the important topic of usability and how it relates to security. In particular, one specific shortcoming he mentioned was the fact that when a YubiKey is configured to require a tap to complete an operation through GPG, there is no visible on-screen feedback to the user.

At Unit 410, YubiKeys are a very important part of our workflow: we use them for, among other things, signing git commits and authenticating SSH connections. For maximum security we always configure them to require a physical touch before signing or authenticating. The lack of user feedback for these very common parts of the engineering workflow is, as one might imagine, a significant productivity impediment.

So I decided to do something about it.

Silently waiting for YubiKey

Finding a Solution

GPG already provides a mechanism to prompt the user when it needs some interaction. It is used, for example, to enter an unlock PIN. This mechanism is provided by one of the components of the GPG suite: pinentry.

I wondered: could pinentry be used to produce a new type of prompt? And could GPG be made to invoke pinentry while waiting for me to touch my YubiKey?

Exploring Pinentry

Pinentry is a collection of utilities provided by the GPG suite (and, on the Mac, also provided by MacGPG) for prompting the user in a variety of ways. Two useful flavors are: pinentry-mac (provided by the MacGPG package) and pinentry-curses (built-in to GPG).

Reading the pinentry documentation, I discovered that it is controlled using a simple text-based protocol on its standard input. Running pinentry and typing some commands makes it do my bidding!

Pinentry, at my command

So, now I can use pinentry to display a custom prompt, but can I make GPG invoke it at the right time? I decided to investigate by first finding how it produces the “Please unlock the card” prompt.

GPG asking for my PIN

GnuPG Architecture

At this point, let’s take a brief detour through the GPG suite architecture.

GnuPG module overview GnuPG module overview (source)

GPG is called a suite for a reason. It is composed of many small parts that work together to produce the overall result. When I invoke GPG to sign a git commit, the following components are involved:

  • gpg: the entrypoint to the suite; for example git calls this to obtain the commit signature.
  • gpg-agent: a background process that provides encryption services to gpg.
  • scdaemon: the smartcard daemon that gpg-agent uses to interact with the YubiKey.
  • pinentry: a small program that gpg-agent uses to prompt the user for information.

So, gpg talks to gpg-agent, gpg-agent talks to scdaemon and to pinentry, but only scdaemon knows when it’s waiting for the YubiKey.

Let’s look around and see if we can find where the call to pinentry happens.

Exploring GnuPG Source Code

Searching for the “Please unlock the card” text leads us to this, in the file scd/app-openpgp.c:

  const char *firstline = _("||Please unlock the card");
  char *infoblock = get_prompt_info (app, chvno, sigcount,
                                     remaining < 3? remaining : -1);

Looks promising! This is inside the function verify_a_chv, which has the following comment:

  /* Verify a CHV either using the pinentry or if possible by
     using a pinpad.  PINCB and PINCB_ARG describe the usual callback
     for the pinentry.
     */

Let’s look for how pincb is used:

  rc = pincb (pincb_arg, prompt, NULL);
  prompt = NULL;
  xfree (prompt_buffer);
  prompt_buffer = NULL;

This function appears to be the mechanism by which scdaemon calls back to gpg-agent and asks it to issue a prompt to the user using pinentry.

Looking in the do_sign function in scd/app-openpgp.c, here is a place that looks like a good candidate for where we should display our message:

  rc = iso7816_compute_ds (app_get_slot (app), exmode, data, datalen, le_value,
                           outdata, outdatalen);
  if (gpg_err_code (rc) == GPG_ERR_TIMEOUT)
    clear_chv_status (app, ctrl, 1);

This is where the YubiKey is actually invoked for signing. (A big clue here is the check for GPG_ERR_TIMEOUT which suggests an operation that might be slow, such as calling a USB-connected device.)

What happens if we add a call to pincb just before?

  // Prompt the user.
  pincb (pincb_arg, _("Please touch your Yubikey."), NULL);

  rc = iso7816_compute_ds (app_get_slot (app), exmode, data, datalen, le_value,
                           outdata, outdatalen);

No longer silent

This is great and very close to what we were looking for! But there’s some extra text that wasn’t expected. Can we improve that?

Searching for that text leads us to the file agent/divert-scd.c where the function getpin_cb appends this string. This must be the other side of pincb.

Reading through this function reveals this tidbit:

  if (!strcmp (info, "--ack"))
    {
      desc2 = L_("Push ACK button on card/token.");

Interesting! What happens if we use the literal string “–ack” as our pincb message?

  // Prompt to touch/ack the card.
  pincb (pincb_arg, _("--ack"), NULL);

A nice prompt

This is perfect. It displays an appropriate message, and it’s using a string that already existed in the GnuPG codebase, so it has already been localized!

Cleaning Up

We now have the prompt we’re looking for, but there are still a couple of small problems. The biggest problem is that we’re leaving pinentry in an invalid state and subsequent calls do not work properly.

While looking at the getpin_cb function, I had noticed that calling it without a prompt string will close the pinentry dialog. Let’s add this after the YubiKey sign operation has returned:

  // Dismiss prompt after signing (or timing out)
  pincb (pincb_arg, NULL, NULL);

Finally, we should make these prompts optional, because there are many different types of smartcards that GPG works with, and some users might be annoyed by these new prompts. I don’t want to assume that my situation is universal.

Conclusion

I set out to improve my own YubiKey workflow, and achieved a great outcome by writing a small patch to the core GPG tools.

It turns out that GPG already had all the pieces to achieve the usability improvement I was after, and it was just a matter of putting them into the right places. This is significant because it allowed my patch to be very minimal.

When patching an open-source project, particularly when that project is a security cornerstone like GPG, it is always a good idea to change as little as possible. This provides several benefits. First, it’s more likely to be accepted into the upstream project. But even if not accepted, it’s easier to maintain a smaller patch as the project evolves. Finally, adding less code and reusing existing pieces helps ensure that no new security bugs are introduced.

With this patch, I now have fixed one of the most glaring (to me) usability issues of GPG and YubiKeys.

You can view my finished GnuPG patch on GitHub.