For my GPG setup, I wanted the following things.

  • Use a hardware smartcard, e.g. YubiKey.
  • Have backups of the smartcard, i.e. the keys cannot be generated on the smartcard (fundamentally, there should be no way to extract the private keys out of a hardware smartcard).
  • Generate the private keys on an offline computer, e.g. using a Raspberry Pi or a Tails USB.
  • Use modern elliptic-curve cryptography (commonly Ed25519), as opposed to RSA keys.

Generating the offline master key and subkeys

In order to have our GPG keys on multiple smartcards, we need to generate the keys on a regular/typical computer. We should minimize the surface from which the computer we are generating our keys on may have been compromised. Often, this is referred to as an “offline” computer or an “air-gapped” machine. Of course, the hardware could still be compromised - even with open source hardware, it’s impractical/impossible to audit the supply chain. Also, practically speaking, we will have downloaded the OS for the offline computer from the internet (as well as the software used to flash the OS on to a drive). At minimum, the computer we are generating our keys on should never be connected to the internet after using it to generate the keys.

I originally used a Raspberry Pi with Ubuntu, but neither Ubuntu or the official Raspberry Pi OS include the necessary GPG components to interact with a smartcard (at least the last time I tried). These instructions are for Tails which is a privacy-oriented OS that can be installed on a USB and booted from.

A few notes about Tails: - When booting Tails, you can completely disable networking for the session, so there’s no way you could accidentally connect to the internet. - The trackpad for my laptop (System76) did not work with Tails; I needed to use a USB mouse.

After installing and booting into Tails, open the terminal (under Applications -> Utilities). For me, gpg --version returns 2.2.27. There is no real need to use a non-default --homedir for GPG, but I will here just to be explicit. Now, we can generate our master key. The --expert flag enables use to choose eliptic-curve cryptography keys.

# Make the directory we will operate GPG with.
cd ~
mkdir gpg

# Set permissions to avoid GPG warnings.
chmod 700 gpg

cd gpg

# Create the master key.
gpg --homedir . --expert --full-gen-key

You should be prompted with Please select what kind of key you want:. We wish to use ECC (set your own capabilities) - for me, this was number (11).

Next, by default, you should see Currently allowed actions: Sign Certify. We wish to only allow certification, so enter s to toggle (disable) the signing capability. Then you can enter q to proceed.

Now, you should be prompted with Please elect which elliptic curve you want:. I’m no expert, but I often see Curve 25519 used (e.g. for SSH), and after some (brief) research, that is what I went with. For me, that was number (1) (in the GPG prompt).

Next, we need to set the expiry for this (master) key. The expiry date may be extended with access to the private part of the key. Personally, I did not set an expiry for my master key. GPG asks me to confirm the expiry.

GPG then asks for a user ID consisting of a real name, an email address, and a comment. An ID cannot be changed or removed from a key, but you can add additional IDs to a key.

Finally, we enter a passphrase to locally encrypt the private key we are about to generate. After we enter our passphrase, GPG prints the following.

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.

One may check the “available entropy” on the system by reading /proc/sys/kernel/random/entropy_avail. Note that in version 5.10.119 of the Linux Kernel, the pool size was changed to 256 - as opposed to the old value of 4096 - if you’re reading older threads talking about entropy, don’t necessarily be alarmed if you see 256. My Tails install today has kernel version 5.10.149-2 (uname --kernel-version).

You can see your newly generated key with gpg --homedir . --list-keys.

Generating subkeys for signing, encryption, and authentication

GPG supports multiple ways to specify a user ID. One can read more on the GPG manual (man gpg - search for/scroll down to “HOW TO SPECIFY A USER ID”). Using the name from the previous part should be sufficient (especially given that we only have 1 key so far). Again, we include --expert to enable ECC keys.

Run gpg --homedir . --expert --edit-key "your name". The prompt should include Secret key is available.

Now, we’ll create a signing key by entering addkey. Signing is used, for example, to sign git commits. We should be prompted with a list (similar to when we generated our master key) that includes ECC (sign only); for me, this was number (10). We’ll go with that first. As before, we need to specify a curve (e.g. Curve 25519).

Next, we need to set the expiry date. For subkeys, unlike for my master key, I set an expiry of 1 year (1y). GPG will confirm the expiry date, and then confirm really creating the key. GPG should prompt you to unlock your master key by entering your passphrase from before. Note that the key isn’t actually saved unless you tell GPG to save before quitting.

Next, we’ll create an encryption key by entering addkey again. There should be an option for ECC (encrypt only); for me, this was number (12). The rest of the steps are the same as for our signing key.

Finally, we’ll create an authentication key - enter addkey one more time. Authentication is used, for example, for GPG to emulate SSH agent/keys. Now, choose ECC (set your own capabilities); for me, this was number (11). The prompt should say Currently allowed actions: Sign. We want to disable signing and enable authenticating, so toggle with s (enter), a (enter), then q (enter) to finish. As before, we select a curve, expiry, confirm, and enter our passphrase.

Note: when listing your keys, the encryption key will show up as “cv25519” whereas the other keys all show up as “ed25519”.

Finally, enter save. You now have all your keys! You can see them with gpg --homedir . --list-keys.

Export your public keys

We will move our private keys on to the YubiKey, but on the computer using the YubiKeys, we will still need to tell GPG about those keys. To do this, export your public keys as follows.

gpg --homedir . --armor --export > public-keys.gpg

If you plug in a USB drive, Tails should automatically mount it under /media/amnesia. You can ls /media/amnesia to find the drive’s name, then copy this file to it.

cp public-keys.gpg /media/amnesia/<USB drive>/

Now use the USB to copy your public keys to another computer.

Back up your keys

Tails by default has no persistent storage. To prevent losing your private keys, you need to copy them to a USB (that should never be plugged into online/arbitrary computers). We could reuse the USB from the previous section - just be careful about where you plug it in after copying your private keys on to the USB! Hence, in the previous section, we need to copy our public keys off the USB before this section.

Personally, I like to tar my GPG folder first so it’s harder to accidentally modify.

# Compressing our GPG folder.
cd ~
tar -czvf gpg.tar.gz gpg

# Copy it to the USB drive.
cp gpg.tar.gz /media/amnesia/<USB drive>/

Configuring the YubiKey

Note: moving keys to the YubiKey deletes the private keys that were on the filesystem, so you have to back up your keys before this!

Plug in your YubiKey and check that GPG sees it.

# Change back into our GPG folder.
cd ~/gpg

gpg --homedir . --card-status

If that is successful, you can run gpg --homedir . --card-edit.

First, we will enable key derived format so that our YubiKey PINs are not stored or transmitted in plain text. This has to be done before any other edits to the card. Enter admin to enable admin-only commands, then enter kdf-setup. You should be prompted to enter the “admin PIN”, which is 12345678 by default for YubiKeys. There is nothing else to do for KDF; you can verify it’s status by entering list to print the card status.

Next, we’ll change the PINs from the defaults. Run passwd and proceed accordingly. The default user PIN is 123456. The reset code is optional.

Personally, I also ran forcesig to toggle (enable) forcing a PIN for signing (the default is not forced). Unfortunately, there is no corresponding option for the encryption or authentication key.

That should be sufficient here; run quit to exit.

Finally, we’ll move our private subkeys to the YubiKey. Run gpg --homedir . --edit-key "your name" (no need for --expert this time). The prompt should include Secret key is available.

Now run the following commands.

  • key 1: toggle/select your first subkey (in the order above, the signing key).
  • keytocard: when prompted for where to store the key, enter 1 for the signature slot of the YubiKey.
  • key 1: toggle/unselect the previous subkey.
  • key 2: toggle/select your second subkey (in the order above, the encryption key).
  • keytocard: when prompted for where to store the key, enter 2 for the encryption slot of the YubiKey.
  • key 2: toggle/unselect the previous subkey.
  • key 3: toggle/select your third subkey (in the order above, the authentication key).
  • keytocard: when prompted for where to store the key, enter 3 for the authentication slot of the YubiKey.

Finally, you have to enter save to actually apply the changes.

Note: saving the previous operations will delete those private subkeys from your filesystem. As previously mentioned: back up your GPG folder before this!

If you repeat the process using additional YubiKeys, at this point you can delete the GPG folder and recreate it from a backup (since the current folder nolonger contains the private keys). For example, using gpg.tar.gz from above, you can do the following.

cd ~
rm -rf gpg
tar -zxvf gpg.tar.gz

At this point you nolonger need the offline computer - don’t plug in the USB containing your private keys into any online/arbitrary computer - but otherwise, the YubiKey can be attached to your normal computer/OS from now on.

Additional YubiKey settings using the YubiKey Manager CLI

This can/should be done on your “normal” computer (the offline computer is nolonger necessary for the rest of this post). Install the YubiKey Manager CLI. On Fedora, this is available in the yubikey-manager package from the official repositories (sudo dnf install yubikey-manager).

Check that it sees our YubiKey: ykman list.

I configured my YubiKeys to require a touch before all OpenPGP operations.

ykman openpgp keys set-touch sig on
ykman openpgp keys set-touch enc on
ykman openpgp keys set-touch aut on

The YubiKey 5 series has two OTP slots - by default, the first which is enabled by default and triggered by a short touch, and the second which is empty by default and triggered by a long touch. The short touch is easy to trigger accidentally; one can swap the two slots so that the OTP is only triggered by a long touch.

# Show current slot status.
ykman otp info

# Swap the two slots.
ykman otp swap

Importing your GPG keys

On our “normal” computer, I’ll assume we’re using the default --homedir (unspecified/~/.gnupg). Given public-keys.gpg from “Export your public keys” above, import it as follows.

gpg --import public-keys.gpg

Without this step, GPG won’t recognize the private keys on your YubiKey.

GPG “remembers” a single smartcard. If you have and swap between multiple YubiKeys, you will need to run the following command whenever you switch to a different YubiKey (optionally specifying --homedir).

gpg-connect-agent 'scd serialno' 'learn --force' /bye

SSH using GPG keys

Add a line enable-ssh-support to gpg-agent.conf in your --homedir (~/.gnupg by default). You may have to restart gpg-agent: gpgconf --kill all.

Note that GPG’s SSH agent emulation doesn’t get triggered automatically when SSH tries to contact the agent. We can start it by running gpg-connect-agent /bye. You may want to put that in your ~/.bashrc (or in a file in ~/.bashrc.d/ on Fedora). As well, you will need to add export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" - this is how SSH knows how to talk to GPG’s SSH agent.

Forwarding GPG agent

We can forward GPG’s sockets when SSHing to a remote machine, so we can, for example, sign commits when developing on a remote machine. We can find the sockets as follows.

# Normal GPG agent socket.
gpgconf --list-dirs agent-socket

# Less privileged GPG agent socket.
gpgconf --list-dirs agent-extra-socket

# GPG's SSH agent socket.
gpgconf --list-dirs agent-ssh-socket

Note that as of GPG 2.1.13:

  • the GPG agent socket location cannot be configured; as a result, GPG on the remote server will always look for a socket at (run on the remote server) gpgconf --list-dirs agent-socket; and
  • on systemd machines, the socket location will be in /run/user/$UID/gnupg (assuming default/unspecified --homedir).

Forward the Unix socket using this flag to SSH; alternatively, specify it in your SSH config file (without -o).

-o 'RemoteForward /path/on/remote/to/its/socket /path/on/local/to/its/socket'

I also like to use -o 'ExitOnForwardFailure yes' so that unexpected things don’t happen. However, on systemd machines, /run/user/$UID/gnupg won’t exist before I SSH/log in, and the above will fail. Because the socket location cannot be configured, there is no official way to deal with this. The (unaccepted) contribution in that issue is to use a systemd user service, which is what I have done. You can create the file ~/.config/systemd/user/create-gnupg-socket-dirs.service with the following contents.

[Unit]
Description=Create GnuPG socket directories

[Service]
Type=oneshot
# Ensure GPG isn't already accidentally running.
ExecStart=/usr/bin/gpgconf --kill all
ExecStart=/usr/bin/gpgconf --create-socketdir

[Install]
WantedBy=default.target

Enable it as follows: systemctl --user enable --now create-gnupg-socket-dirs.service.

Finally, we should add the following line to our server’s sshd configuration (e.g. a file in /etc/ssh/sshd_config.d/ on Fedora) to allow replacing old/stale socket files.

StreamLocalBindUnlink yes

Forward GPG’s SSH agent

We can forward GPG’s SSH agent socket (gpgconf --list-dirs agent-ssh-socket on the local machine) similarly.

As a note, since SSH doesn’t require its socket to be in a fixed location (it reads $SSH_AUTH_SOCK, as we defined for our local machine previously), so we can forward it anywhere on the remote machine (e.g. ~/.ssh/S.gpg-agent.ssh) and set $SSH_AUTH_SOCK (on the remote machine) accordingly.

Other notes

GPG has its own way of accessing smartcards, different from the “standard” way to access smartcards, which the YubiKey Manager uses. The stanard way is implemented by pcsc-lite, which in Fedora is available in the official repositories (pcsc-lite would have been installed as a dependency if you installed yubikey-manager). Disable GPG’s own method by adding a line disable-ccid to scdaemon.conf in your GPG home (~/gpg above, ~/.gnupg by default/when unspecified). If gpg-agent is already running, you need to restart it: gpgconf --kill all (optionally specify --homedir).

References