GPG on a Mac with a Yubikey

This tutorial is a step-by-step guide on how to setup your GPG key on a Mac, transfer it to a Yubikey for security and later use it for Git Signing as well as SSH authentication.

Disclaimer

I am pretty new to GPG management myself so the steps in this post might not be the most efficient/effective but they did work for me. Just trying to share it with others as I had to combine multiple sources to make everything work. Also documenting all the steps for myself for future use since Im sure I will forget everything in about a month.

Prerequisites

You can use Homebrew to install all necessary packages.

❯❯❯ brew install \
    gpg \
    paperkey \
    qrencode \
    zbar \
    pinentry-mac \
    homebrew/cask-drivers/yubico-yubikey-manager

Note that gpg2 is required however brew’s recipy for gpg installs gpg2 by default. You can check that by looking up recipy information:

❯❯❯ brew info gpg | head -n1
gnupg: stable 2.2.12 (bottled)

GPG will not allow to enter passwords unless TTY is configured:

❯❯❯ export GPG_TTY=$(tty)

Generate keys

GPG has a notion of a keyring. Keyring as the name implies is very similar to a physical keyring which holds multiple of your keys where each key is used for a different purpose - encryption, signing or authenticating your identity. In addition keys can be derived from a master key. This property is useful for security reasons as it allows to revoke individual keys without needing to revoke the complete master key. Speaking of security, its also a good practice to expire the keys in few years just in case any issues are found with RSA or the key gets compromised.

Master key

The following will generate the master key. Note that even though EC crypto is pretty awesome, it is currently unsupported by any of the Yubikeys hence RSA keys need to be used. In this example we will create 2048 byte RSA keys expiring in 5 years. Newest Yubikeys (4 and above) support 4096 byte keys.

❯❯❯ gpg --expert --full-generate-key
gpg (GnuPG) 2.2.12; Copyright (C) 2018 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
   (9) ECC and ECC
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (13) Existing key
Your selection? 4
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 2048
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 5y
Key expires at Sat Dec 23 01:09:57 2023 EST
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: GPG Test
Email address: test@example.com
Comment:
You selected this USER-ID:
    "GPG Test <test@example.com>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: key 48621A68A9AD7551 marked as ultimately trusted
gpg: directory '/Users/miki725/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/Users/miki725/.gnupg/openpgp-revocs.d/ED5CA76BDA3809828BB5646A48621A68A9AD7551.rev'
public and secret key created and signed.

Note that this key cannot be used for encryption.  You may want to use
the command "--edit-key" to generate a subkey for this purpose.
pub   rsa2048 2018-12-24 [SC] [expires: 2023-12-23]
      ED5CA76BDA3809828BB5646A48621A68A9AD7551
uid                      GPG Test <test@example.com>

You can verify master key was successfully created:

❯❯❯ gpg --list-keys --keyid-format LONG
/Users/miki725/.gnupg/pubring.kbx
---------------------------------
pub   rsa2048/48621A68A9AD7551 2018-12-24 [SC] [expires: 2023-12-23]
      ED5CA76BDA3809828BB5646A48621A68A9AD7551
uid                 [ultimate] GPG Test <test@example.com>

The created master key can either be referred by its id 48621A68A9AD7551 or by the email address test@example.com.

Subkeys

Now subkeys can be added for encryption, signing and authenticating. Encryption and signing keys are pretty straight forward. Authentication key requires to customize key’s capabilities (see below).

❯❯❯ gpg --expert --edit-key test@example.com
gpg (GnuPG) 2.2.12; Copyright (C) 2018 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret key is available.

sec  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
[ultimate] (1). GPG Test <test@example.com>

gpg> addkey
Please select what kind of key you want:
   (3) DSA (sign only)
   (4) RSA (sign only)
   (5) Elgamal (encrypt only)
   (6) RSA (encrypt only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (12) ECC (encrypt only)
  (13) Existing key
Your selection? 6
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 2048
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 5y
Key expires at Sat Dec 23 01:20:19 2023 EST
Is this correct? (y/N) y
Really create? (y/N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

sec  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
[ultimate] (1). GPG Test <test@example.com>

gpg> addkey
Please select what kind of key you want:
   (3) DSA (sign only)
   (4) RSA (sign only)
   (5) Elgamal (encrypt only)
   (6) RSA (encrypt only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (12) ECC (encrypt only)
  (13) Existing key
Your selection? 4
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 2048
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 5y
Key expires at Sat Dec 23 01:20:47 2023 EST
Is this correct? (y/N) y
Really create? (y/N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

sec  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
[ultimate] (1). GPG Test <test@example.com>

gpg> addkey
Please select what kind of key you want:
   (3) DSA (sign only)
   (4) RSA (sign only)
   (5) Elgamal (encrypt only)
   (6) RSA (encrypt only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (12) ECC (encrypt only)
  (13) Existing key
Your selection? 8

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Sign Encrypt

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? s

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Encrypt

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? e

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions:

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? a

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Authenticate

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? q
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 2048
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 5y
Key expires at Sat Dec 23 01:21:51 2023 EST
Is this correct? (y/N) y
Really create? (y/N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

sec  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> save

You can verify subkeys were created by listing all keys:

❯❯❯ gpg --list-keys --keyid-format LONG
/Users/miki725/.gnupg/pubring.kbx
---------------------------------
pub   rsa2048/48621A68A9AD7551 2018-12-24 [SC] [expires: 2023-12-23]
      ED5CA76BDA3809828BB5646A48621A68A9AD7551
uid                 [ultimate] GPG Test <test@example.com>
sub   rsa2048/7934F75F8D18C1DD 2018-12-24 [E] [expires: 2023-12-23]
sub   rsa2048/FF46BE567B8D4C88 2018-12-24 [S] [expires: 2023-12-23]
sub   rsa2048/2A136E849931EB24 2018-12-24 [A] [expires: 2023-12-23]

Backup Master Key

Encrypted Archive

Since subkeys provide all the necessary functionality, master key is no longer necessary. Therefore master key can be backed up and removed from the keyring for security reasons. The backup should be kept in a secure location like on an encrypted disk or paper in a safe.

Since we will be exporting sensitive information it’s not a good idea to save any sensitive files on disk. Ramdisk to the rescue!

>>> diskutil erasevolume HFS+ 'ramdisk' $(hdiutil attach -nomount ram://262144)
Started erase on disk2
Unmounting disk
Erasing
Initialized /dev/rdisk2 as a 128 MB case-insensitive HFS Plus volume
Mounting disk
Finished erase on disk2 ramdisk

Now both private and public keys can be written to ramdisk. In addition its a good idea to generate revocation certificate in case master key will need to be revoked without having access to master key.

❯❯❯ gpg --export test@example.com > \
        /Volumes/ramdisk/public.key
❯❯❯ gpg --export-secret-key test@example.com > \
        /Volumes/ramdisk/private.key
❯❯❯ gpg --gen-revoke test@example.com > \
        /Volumes/ramdisk/revoke.cert

sec  rsa2048/48621A68A9AD7551 2018-12-24 GPG Test <test@example.com>

Create a revocation certificate for this key? (y/N) y
Please select the reason for the revocation:
  0 = No reason specified
  1 = Key has been compromised
  2 = Key is superseded
  3 = Key is no longer used
  Q = Cancel
(Probably you want to select 1 here)
Your decision? 1
Enter an optional description; end it with an empty line:
>
Reason for revocation: Key has been compromised
(No description given)
Is this okay? (y/N) y
ASCII armored output forced.
Revocation certificate created.

Please move it to a medium which you can hide away; if Mallory gets
access to this certificate he can use it to make your key unusable.
It is smart to print this certificate and store it away, just in case
your media become unreadable.  But have some caution:  The print system of
your machine might store the data and make it available to others!

For convenience and security all files can be combined into an encrypted archive.

❯❯❯ tar cz -C /Volumes/ramdisk/ \
        private.key \
        public.key \
        revoke.cert | \
    gpg --cipher-algo AES256 \
        -z 0 \
        --symmetric \
        --armor > \
    /Volumes/ramdisk/master.tar.gz.gpg

The archive can be safely backed up, hopefully to an encrypted storage and stored offline.

Paper

Alternatively there are couple of methods of printing the key to paper. Paperkey seems to be the most mature solution. It only extracts the secret bits out of the key hence no need to print the complete private key as the secret keys can be combined with public key to restore private key. Still, secret bits are larger than QR code can accommodate hence they need to be split to multiple files.

Below will:

  1. extracts secret bits out of the private key
  2. combine secret bits with revocation certificate to an archive
  3. encrypt archive for safety
  4. splits archive into 2.5K chunks to accommodate QR code max limit
  5. QR encodes each chunk which generates PNG files
❯❯❯ cat /Volumes/ramdisk/private.key | \
    paperkey --output-type=raw > \
    /Volumes/ramdisk/secret.gpg
❯❯❯ tar cz -C /Volumes/ramdisk/ \
        secret.gpg \
        revoke.cert | \
    gpg --cipher-algo AES256 \
        -z 0 \
        --symmetric \
        --armor > \
    /Volumes/ramdisk/paper.tar.gz.gpg
❯❯❯ split -C 2500 \
        /Volumes/ramdisk/paper.tar.gz.gpg \
        /Volumes/ramdisk/paper.tar.gz.gpg-
❯❯❯ for file in /Volumes/ramdisk/paper.tar.gz.gpg-??;
    do qrencode -d 600 -r $file -o $file.png;
    done
❯❯❯ ls /Volumes/ramdisk/paper.tar.gz.gpg-??.png
/Volumes/ramdisk/paper.tar.gz.gpg-aa.png
/Volumes/ramdisk/paper.tar.gz.gpg-ab.png
/Volumes/ramdisk/paper.tar.gz.gpg-ac.png

PNGs can be safely printed and stored in a safe place.

Restore Master Key from Paper

Master key can be fully restored either from master archive or from printed QR codes. This will demonstrate how to restore from QR codes.

❯❯❯ for file in /Volumes/ramdisk/paper.tar.gz.gpg-??.png;
    do zbarimg -q --raw $file | \
        head -n -1 > \
        "$file.decoded";
    done
❯❯❯ cat /Volumes/ramdisk/paper.tar.gz.gpg-??.png.decoded > \
    /Volumes/ramdisk/paper.tar.gz.gpg.restored
❯❯❯ gpg \
        --cipher-algo AES256 \
        -z 0 \
        --decrypt /Volumes/ramdisk/paper.tar.gz.gpg.restored | \
    tar zxv
gpg: AES256 encrypted data
gpg: encrypted with 1 passphrase
secret.gpg
revoke.cert
❯❯❯ paperkey \
        --pubring /Volumes/ramdisk/public.key \
        --secrets /Volumes/ramdisk/secret.gpg > \
    /Volumes/ramdisk/private.key.restored

To simulate complete loss of GPG keyring, GPG home directory can be deleted. Then we can restore the master key.

❯❯❯ rm -rf ~/.gnupg
❯❯❯ gpg --import /Volumes/ramdisk/private.key.restored
❯❯❯ gpg --list-secret-keys --keyid-format LONG
/Users/miki725/.gnupg/pubring.kbx
---------------------------------
sec   rsa2048/48621A68A9AD7551 2018-12-24 [SC] [expires: 2023-12-23]
      ED5CA76BDA3809828BB5646A48621A68A9AD7551
uid                 [ unknown] GPG Test <test@example.com>
ssb   rsa2048/7934F75F8D18C1DD 2018-12-24 [E] [expires: 2023-12-23]
ssb   rsa2048/FF46BE567B8D4C88 2018-12-24 [S] [expires: 2023-12-23]
ssb   rsa2048/2A136E849931EB24 2018-12-24 [A] [expires: 2023-12-23]

Only issue is that the key is not trusted so it needs to be edited:

❯❯❯ gpg --edit-key test@example.com
gpg (GnuPG) 2.2.12; Copyright (C) 2018 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret key is available.

sec  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: unknown       validity: unknown
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ unknown] (1). GPG Test <test@example.com>

gpg> trust
sec  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: unknown       validity: unknown
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ unknown] (1). GPG Test <test@example.com>

Please decide how far you trust this user to correctly verify other users' keys
(by looking at passports, checking fingerprints from different sources, etc.)

  1 = I don't know or won't say
  2 = I do NOT trust
  3 = I trust marginally
  4 = I trust fully
  5 = I trust ultimately
  m = back to the main menu

Your decision? 5
Do you really want to set this key to ultimate trust? (y/N) y

sec  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: unknown
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ unknown] (1). GPG Test <test@example.com>
Please note that the shown key validity is not necessarily correct
unless you restart the program.

gpg> save

Master key is restored now which can be verified by listing all secret keys.

❯❯❯ gpg --list-secret-keys --keyid-format LONG
/Users/miki725/.gnupg/pubring.kbx
---------------------------------
sec   rsa2048/48621A68A9AD7551 2018-12-24 [SC] [expires: 2023-12-23]
      ED5CA76BDA3809828BB5646A48621A68A9AD7551
uid                 [ultimate] GPG Test <test@example.com>
ssb   rsa2048/7934F75F8D18C1DD 2018-12-24 [E] [expires: 2023-12-23]
ssb   rsa2048/FF46BE567B8D4C88 2018-12-24 [S] [expires: 2023-12-23]
ssb   rsa2048/2A136E849931EB24 2018-12-24 [A] [expires: 2023-12-23]

Remove Master Key

Like mentioned previously, master key is very sensitive hence it should be deleted locally after secure backup is made. With newest versions of GPG that is relatively easy as private keys are stored in GPG home directory.

❯❯❯ ls ~/.gnupg/private-keys-v1.d/
16567857EE938FED11A47969F40451BF014993F4.key
8254BE2A4A37393C9E20CD6F7E42E4F37909D09E.key
781682C203E3D31D9FFF964FFD0A398689B4CBBF.key
A72DAE0F8F392E69F5EE4ED60361F490EBAB31D1.key

The key names are keygrip ids. To correlate the ids you can list all keys with their keygrip ids.

❯❯❯ gpg --list-keys --keyid-format LONG --with-keygrip
/Users/miki725/.gnupg/pubring.kbx
---------------------------------
pub   rsa2048/48621A68A9AD7551 2018-12-24 [SC] [expires: 2023-12-23]
      ED5CA76BDA3809828BB5646A48621A68A9AD7551
      Keygrip = 16567857EE938FED11A47969F40451BF014993F4
uid                 [ultimate] GPG Test <test@example.com>
sub   rsa2048/7934F75F8D18C1DD 2018-12-24 [E] [expires: 2023-12-23]
      Keygrip = A72DAE0F8F392E69F5EE4ED60361F490EBAB31D1
sub   rsa2048/FF46BE567B8D4C88 2018-12-24 [S] [expires: 2023-12-23]
      Keygrip = 781682C203E3D31D9FFF964FFD0A398689B4CBBF
sub   rsa2048/2A136E849931EB24 2018-12-24 [A] [expires: 2023-12-23]
      Keygrip = 8254BE2A4A37393C9E20CD6F7E42E4F37909D09E

16567857EE938FED11A47969F40451BF014993F4 is the master key so that key can be removed.

❯❯❯ rm ~/.gnupg/private-keys-v1.d/16567857EE938FED11A47969F40451BF014993F4.key

You can verify master key is removed by listing all keys.

❯❯❯ gpg --list-secret-keys --keyid-format LONG
/Users/miki725/.gnupg/pubring.kbx
---------------------------------
sec#  rsa2048/48621A68A9AD7551 2018-12-24 [SC] [expires: 2023-12-23]
      ED5CA76BDA3809828BB5646A48621A68A9AD7551
uid                 [ultimate] GPG Test <test@example.com>
ssb   rsa2048/7934F75F8D18C1DD 2018-12-24 [E] [expires: 2023-12-23]
ssb   rsa2048/FF46BE567B8D4C88 2018-12-24 [S] [expires: 2023-12-23]
ssb   rsa2048/2A136E849931EB24 2018-12-24 [A] [expires: 2023-12-23]

Note sec is displayed as sec# now. The # signifies private key is not available.

Key Server

Public key can be submitted to a keyserver so that other people can find your public key to be able to communicate with you. I like keybase.io. After uploading the public key to a keyserver, you should be able to get a URL of where your public key can be downloaded. Mine is for example:

https://keybase.io/miki725/pgp_keys.asc

This URL will be used later.

Yubikey

Better Defaults

Before subkeys can be moved to a Yubikey, Yubikey itself needs to be configured to support smart card mode. It can be done with CLI however I prefer the official Yubico “Yubikey Manager” as it will forbid me from messing anything up. Yubikey Manager was installed with brew above so simply open the application.

Yubikey Manager enabling CCID mode

After enabling CCID mode, you can verify its working with GPG:

❯❯❯ gpg --card-status
Reader ...........: Yubico Yubikey 4 OTP U2F CCID
Application ID ...: XXXXX
Version ..........: 2.1
Manufacturer .....: Yubico
Serial number ....: XXXXX
Name of cardholder: [not set]
Language prefs ...: [not set]
Sex ..............: unspecified
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 127 127 127
PIN retry counter : 3 3 3
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]

Before transferring keys, it’s a good idea to change a few card configurations:

Default pin is 123456 and admin pin is 12345678.

❯❯❯ gpg --edit-card

Reader ...........: Yubico Yubikey 4 OTP U2F CCID
Application ID ...: XXXXX
Version ..........: 2.1
Manufacturer .....: Yubico
Serial number ....: XXXXX
Name of cardholder: [not set]
Language prefs ...: [not set]
Sex ..............: unspecified
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 127 127 127
PIN retry counter : 3 3 3
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]

gpg/card> admin
Admin commands are allowed

gpg/card> passwd
gpg: OpenPGP card no. XXXXX detected

1 - change PIN
2 - unblock PIN
3 - change Admin PIN
4 - set the Reset Code
Q - quit

Your selection? 1
PIN changed.

1 - change PIN
2 - unblock PIN
3 - change Admin PIN
4 - set the Reset Code
Q - quit

Your selection? 3
PIN changed.

1 - change PIN
2 - unblock PIN
3 - change Admin PIN
4 - set the Reset Code
Q - quit

Your selection? 4
Reset Code set.

1 - change PIN
2 - unblock PIN
3 - change Admin PIN
4 - set the Reset Code
Q - quit

Your selection? q

gpg/card> forcesig

gpg/card> name
Cardholder's surname: Shubernetskiy
Cardholder's given name: Miroslav

gpg/card> lang
Language preferences: en

gpg/card> url
URL to retrieve public key: https://keybase.io/miki725/pgp_keys.asc

gpg/card> login
Login data (account name): miki725

gpg/card> quit

You can verify all settings were applied by checking card status again.

❯❯❯ gpg --card-status
Reader ...........: Yubico Yubikey 4 OTP U2F CCID
Application ID ...: XXXXX
Version ..........: 2.1
Manufacturer .....: Yubico
Serial number ....: XXXXX
Name of cardholder: Miroslav Shubernetskiy
Language prefs ...: en
Sex ..............: unspecified
URL of public key : https://keybase.io/miki725/pgp_keys.asc
Login data .......: miki725
Signature PIN ....: forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 127 127 127
PIN retry counter : 3 3 3
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]

Touch Required

In addition Yubikey supports a feature which forces you to touch Yubikey anytime it tries to interact with private keys stored on the card. This protects you against malicious scripts attempting to use GPG without your consent.

Full docs for that are located on Yubico Edit Card page. yubitouch repo contains the script which allows to change touch mode of a Yubikey. By default the script does not work on a Mac since it requires a different pinentry program unless you change your default GPG config. I opted to change the script instead to use pinentry-mac previously installed with brew.

❯❯❯ curl -s https://raw.githubusercontent.com/a-dma/yubitouch/master/yubitouch.sh | \
    sed 's/pinentry/pinentry-mac/' | \
    bash -s sig on
All done!

Transfer keys

Now individual keys can be transferred to the Yubikey. Note that each subkey needs to be moved independently.

❯❯❯ gpg --edit-key test@example.com
gpg (GnuPG) 2.2.12; Copyright (C) 2018 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret subkeys are available.

pub  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> key 1

pub  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb* rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> keytocard
Please select where to store the key:
   (2) Encryption key
Your selection? 2

pub  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb* rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> key 1

pub  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> key 2

pub  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb* rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> keytocard
Please select where to store the key:
   (1) Signature key
   (3) Authentication key
Your selection? 1

pub  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb* rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> key 2

pub  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb  rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> key 3

pub  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb* rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> keytocard
Please select where to store the key:
   (3) Authentication key
Your selection? 3

pub  rsa2048/48621A68A9AD7551
     created: 2018-12-24  expires: 2023-12-23  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/7934F75F8D18C1DD
     created: 2018-12-24  expires: 2023-12-23  usage: E
ssb  rsa2048/FF46BE567B8D4C88
     created: 2018-12-24  expires: 2023-12-23  usage: S
ssb* rsa2048/2A136E849931EB24
     created: 2018-12-24  expires: 2023-12-23  usage: A
[ultimate] (1). GPG Test <test@example.com>

gpg> save

You can verify all keys were moved by checking card status again.

❯❯❯ gpg --card-status
Reader ...........: Yubico Yubikey 4 OTP U2F CCID
Application ID ...: XXXXX
Version ..........: 2.1
Manufacturer .....: Yubico
Serial number ....: XXXXX
Name of cardholder: Miroslav Shubernetskiy
Language prefs ...: en
Sex ..............: unspecified
URL of public key : https://keybase.io/miki725/pgp_keys.asc
Login data .......: miki725
Signature PIN ....: forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 127 127 127
PIN retry counter : 3 3 3
Signature counter : 0
Signature key ....: 658F 79F5 F344 AF38 A1BB  E941 FF46 BE56 7B8D 4C88
      created ....: 2018-12-24 06:20:30
Encryption key....: 607C BF0E 6082 25D0 E91E  923D 7934 F75F 8D18 C1DD
      created ....: 2018-12-24 06:20:01
Authentication key: 2957 0C23 4DFA EDD1 A23D  1183 2A13 6E84 9931 EB24
      created ....: 2018-12-24 06:20:53
General key info..: sub  rsa2048/FF46BE567B8D4C88 2018-12-24 GPG Test <test@example.com>
sec#  rsa2048/48621A68A9AD7551  created: 2018-12-24  expires: 2023-12-23
ssb>  rsa2048/7934F75F8D18C1DD  created: 2018-12-24  expires: 2023-12-23
                                card-no: 0006 08753000
ssb>  rsa2048/FF46BE567B8D4C88  created: 2018-12-24  expires: 2023-12-23
                                card-no: 0006 08753000
ssb>  rsa2048/2A136E849931EB24  created: 2018-12-24  expires: 2023-12-23
                                card-no: 0006 08753000

You can attempt to sign something which will require you to enter your PIN as well as touch the Yubikey.

❯❯❯ echo hello | gpg --armor --sign
-----BEGIN PGP MESSAGE-----

owGbwMvMwMH4321fWHWvTwfjaZ4khhhFz3kZqTk5+VydjMYsDIwcDLJiiiyp/ZVf
P7ust1i4+6UjTDkrE0gtAxenAEzkyDkOhhN2C7xXn1L996n0lucnwYwPh6WEd72y
U3Bq5nne2/rSac7HLhmHC9+fSE6dJ8gXUBip9ydM2e6Aesdbs1PeZYL3p65c8UA3
ffeUwKWrZnSVlE6+v/C9ZGaqhv0snbPfzv+IsPH5JO+QpDRlSub9pWvu2T0MX66u
rj7159XXdv7L259xrNpSYHnm37brKSmuFb7a0wXlTFYIr+E9Fbl9seKFnxHLu8yn
1WzoL1Kz1W+ynvn4tJ7m3X3cWUs9kz//nLlZX/l4w8WqO/IHbRt72Oo5D+dICH5z
tIycttz500fh/Eq3B8zqtdyCS3jcjlptfLLbIPTR3NTQIx43Mlk+cL2YJKfAb+Jq
aiPHtHxyJAA=
=GTbX
-----END PGP MESSAGE-----

If you remove Yubikey you should not be able to sign anything anymore.

❯❯❯ echo hello | gpg --armor --sign
┌─────────────────────────────────────────────┐
│ Please insert the card with serial number:  │
│                                             │
│ XXXX XXXXXXXX                               │
│                                             │
│      <OK>                       <Cancel>    │
└─────────────────────────────────────────────┘

Git Signing

Config

Before we continue you need to find the signing key id.

❯❯❯ gpg --list-keys --keyid-format LONG
/Users/miki725/.gnupg/pubring.kbx
---------------------------------
pub   rsa2048/48621A68A9AD7551 2018-12-24 [SC] [expires: 2023-12-23]
      ED5CA76BDA3809828BB5646A48621A68A9AD7551
uid                 [ultimate] GPG Test <test@example.com>
sub   rsa2048/7934F75F8D18C1DD 2018-12-24 [E] [expires: 2023-12-23]
sub   rsa2048/FF46BE567B8D4C88 2018-12-24 [S] [expires: 2023-12-23]
sub   rsa2048/2A136E849931EB24 2018-12-24 [A] [expires: 2023-12-23]

In this case the key id is FF46BE567B8D4C88.

Now you can configure a couple of git parameters to make it all work.

❯❯❯ git config --global commit.gpgsign true
❯❯❯ git config --global gpg.program /usr/local/bin/gpg
❯❯❯ git config --global user.signingkey FF46BE567B8D4C88

Demo

Create repo and sign first commit.

❯❯❯ mkdir /tmp/test
❯❯❯ cd /tmp/test/
❯❯❯ git init
Initialized empty Git repository in /private/tmp/test/.git/
❯❯❯ touch hello
❯❯❯ git add hello
❯❯❯ git commit -m 'test' -S
[master (root-commit) 149e91d] test
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 hello

You can verify commit was signing by checking git log.

❯❯❯ git log --show-signature
commit 149e91df86cfe412bfc54a447f4dc7945f63855e (HEAD -> master)
gpg: Signature made Mon Dec 24 16:14:40 2018 EST
gpg:                using RSA key 658F79F5F344AF38A1BBE941FF46BE567B8D4C88
gpg: Good signature from "GPG Test <test@example.com>" [ultimate]
Author: Miroslav Shubernetskiy <miroslav@miki725.com>
Date:   Mon Dec 24 16:14:40 2018 -0500

    test

SSH

GPG Agent

By default GPG agent does not support SSH so that needs to be changed. Later it should be restarted to take effect.

❯❯❯ echo "enable-ssh-support" >> \
    ~/.gnupg/gpg-agent.conf
❯❯❯ gpg-agent $(gpg-connect-agent reloadagent /bye)
gpg-agent[33544]: gpg-agent running and available

For some reason though SSH was not honored for me. Not sure if its a GPG bug but for now I need to explicitly update agent config. See mailing list for more information.

❯❯❯ gpg-connect-agent updatestartuptty /bye > /dev/null

Also GPG agent needs to be told which key to use for SSH auth. It needs keygrip id of the auth key.

❯❯❯ gpg --list-keys --keyid-format LONG --with-keygrip
/Users/miki725/.gnupg/pubring.kbx
---------------------------------
pub   rsa2048/48621A68A9AD7551 2018-12-24 [SC] [expires: 2023-12-23]
      ED5CA76BDA3809828BB5646A48621A68A9AD7551
      Keygrip = 16567857EE938FED11A47969F40451BF014993F4
uid                 [ultimate] GPG Test <test@example.com>
sub   rsa2048/7934F75F8D18C1DD 2018-12-24 [E] [expires: 2023-12-23]
      Keygrip = A72DAE0F8F392E69F5EE4ED60361F490EBAB31D1
sub   rsa2048/FF46BE567B8D4C88 2018-12-24 [S] [expires: 2023-12-23]
      Keygrip = 781682C203E3D31D9FFF964FFD0A398689B4CBBF
sub   rsa2048/2A136E849931EB24 2018-12-24 [A] [expires: 2023-12-23]
      Keygrip = 8254BE2A4A37393C9E20CD6F7E42E4F37909D09E

In this case that’s 8254BE2A4A37393C9E20CD6F7E42E4F37909D09E.

❯❯❯ echo 8254BE2A4A37393C9E20CD6F7E42E4F37909D09E >> \
    ~/.gnupg/sshcontrol

SSH Config

Few configurations are required for SSH to work. Main change is that ssh agent needs to be switched to use gpg agent.

export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
export SSH_AGENT_PID=""

If all worked well you should see your key in SSH:

❯❯❯ ssh-add -l
2048 SHA256:OlsOOlrx46NgGm2l/Rmz/cCOEdJppNWD7B8fOKDfE4k cardno:XXXXX (RSA)
2048 SHA256:OlsOOlrx46NgGm2l/Rmz/cCOEdJppNWD7B8fOKDfE4k (none) (RSA)

The double entry is normal and is due to the Yubikey storing the same key.

To see the SSH public key you can either explicitly export it by using GPG key id. In this case its 2A136E849931EB24. Alternatively you can view it in ssh-add.

❯❯❯ gpg --export-ssh-key 2A136E849931EB24
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqzT5cnRJ2a8bxZi0CZqIkP5EYxAaRJnBwX9ZwiVaCDTYc8DupCVrrsohLhNyzIHxZMGOS7IfN5mcHGpJt8L2ZT8WqhHvJlH/46T0UyYbdiQWeA7zdEJeoQg4T1qe701K5j88cWPszzpskKT3hQbSJ28KeqjTqK8vr/DftIM+t9RqEbmiRlg78/6bqsG/KaT+ALMu7BakeDVpwzuWTcKOJFAnigxk/3Fqgjc3f2depNeOZkY0nGTHJb02bkN9zdRaeF8kTt6M3PJ+MhfZrnZoohWsr3mih538uWdgQrx4mYobRvS7XbEywDkigfKUB/t98Xr1he1QWxoSMwj4qs+xJ openpgp:0x9931EB24

❯❯❯ ssh-add -L | tail -n1
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqzT5cnRJ2a8bxZi0CZqIkP5EYxAaRJnBwX9ZwiVaCDTYc8DupCVrrsohLhNyzIHxZMGOS7IfN5mcHGpJt8L2ZT8WqhHvJlH/46T0UyYbdiQWeA7zdEJeoQg4T1qe701K5j88cWPszzpskKT3hQbSJ28KeqjTqK8vr/DftIM+t9RqEbmiRlg78/6bqsG/KaT+ALMu7BakeDVpwzuWTcKOJFAnigxk/3Fqgjc3f2depNeOZkY0nGTHJb02bkN9zdRaeF8kTt6M3PJ+MhfZrnZoohWsr3mih538uWdgQrx4mYobRvS7XbEywDkigfKUB/t98Xr1he1QWxoSMwj4qs+xJ (none)

Conclusion

Congratulations. You are done.

As a recap you did the following:

*****
Written by Miroslav Shubernetskiy on 23 December 2018