Using the same key with different OpenPGP cards

I recently got a Yubikey for PGP because I didn’t want to store my subkeys’ private key on disk. When doing this, it’s generally a good idea to have a backup key in case your primary is unusable for any reason – you might break it, drop in a water, or it might just stop working all of a sudden.123 In such events, the backup will come handy.

The way GnuPG works with card-based PGP is as follows. A stub secret key that references your card’s serial number is stored locally in ~/.gnupg/private-keys-v1.d. This is why gpg --list-secret-keys displays your keys even if your card isn’t connected and they’re not actually stored locally.

To query this information:

$ gpg-connect-agent 'keyinfo --list' /bye

The T in the fourth column signifies that the key is stored on a smartcard. See help keyinfo to learn more about the format.

Each line of the output is derived from a corresponding file in ~/.gnupg/private-keys-v1.d with the keygrip as its name. Removing that file removes all references to the secret key existing. To rebuild that reference, all one needs to do is run gpg --card-status after inserting a card.

But, of course, we’d like to have this automated. In order to automatically choose the card that’s inserted, we’ll make use of udev rules that get triggered when a card is inserted or removed.

Plug in your Yubikey and run lsusb | grep -i Yubikey to get your vendor and product ID. Mine were 1050:0407 for both of my keys. Add the appropriate udev rule to /etc/udev/rules.d/ (remember to set $USER appropriately):

$ cat /etc/udev/rules.d/40-yubikey.rules
ACTION=="add|change", \
  SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0407", \
  RUN+="/usr/local/bin/clean-card-private-keys -u $USER"

The rule calls a clean-card-private-keys script whenever a Yubikey that that matching 1050:0407 is inserted. automatically deletes all card-based secret key stubs from ~/.gnupg/private-keys-v1.d and re-generates the stubs by running gpg --card-status (full script):

$ cat /usr/local/bin/clean-card-private-keys
clean_card_private_keys() {
  if [[ "$run_as" == "" ]]; then
      gpg-connect-agent 'keyinfo --list' /bye 2>/dev/null \
        | grep -v OK \
        | awk '{if ($4 == "T") { print $3 ".key" }}')
    for f in $keygrips; do
      rm -v ~/.gnupg/private-keys-v1.d/$f
    gpg --card-status 2>/dev/null 1>/dev/null
    echo ${BASH_SOURCE[0]}
    su "$run_as" -c "${BASH_SOURCE[0]}"

To test, run watch gpg-connect-agent "'keyinfo --list'" /bye and hot-swap your card a bunch of times to watch the serial numbers get updated every time. Of course, you could also not use the udev rules and just manually run the script every time you really need to scrub your private key stubs.

Also, the script could probably be made better to remove only those keygrips associated with the newly inserted card, but I couldn’t be bothered. Pull requests to the script welcome!

  1. Remember that you need to generate your encryption, signing and authentication subkeys off-key because you need to store them in two different cards. 

  2. I recommend getting a USB-C one as a backup to a USB-A primary (or vice versa) so that you’re not locked out by the lack of appropriate ports. You shouldn’t carry your backup with you anywhere, but just in case your primary can’t be used at the same place where you store your backup, you have an extra option now. 

  3. A backup Yubikey is useless when you lose your primary because you should be looking for your backed up revocation certificates for compromised keys instead.