All Articles

Yubikey OpenPGP Setup for SSH and Commit Signing

Goal: to remove my ssh key entirely from disk and use my yubikey instead.

Why? Hopefully better security: it will be much more difficult to steal my SSH key and access all the things.

What I use my SSH key for:

  • GitHub push/pull access
  • Accessing servers that I run

Update 2022: added notes to ensure you have scdaemon installed, or gpg won’t detect your smartcard.

Update 2022 Q4: Keys expired, so I geuss time for the annual refresh. Added a section for extending the expiry. Next year maybe I’ll try creating new keys.

Update 2024: New install on Ubuntu 23.04 - added some new stuff to get the agent to place nice with gnome (IdentityAgent), also better to use .profile instead of .bashrc. Thanks to: https://mlohr.com/gpg-agent-for-ssh-authentication-update/

Which method to use

YubiKey offers two different methods that can be used for SSH: PIV or OpenPGP. So which one should I use? Luckily yubico already provides a good discussion on this topic.

https://developers.yubico.com/PIV/Guides/Securing_SSH_with_OpenPGP_or_PIV.html

Because the SSH communication is done through the GnuPG agent, the OpenPGP option is more complicated than PIV to set up. However, developers who use Git to sign their commits typically choose the OpenPGP path because they already have it up and running.

I’m a developer, so interested in signing my commits too. Guess I should go the hard route. While rare, sometimes I might also want to perform some openpgp stuff too.

Planning for failure

Before I rm .ssh/id_rsa*, I brainstormed on what would happen if I were to lose or have my yubikey stolen.

If I lose the SSH key:

  • I can still restore access my servers via rescue mode, or via a friend in some cases.
  • GitHub also authenticates via password and MFA and I can simply add a new key.
  • Revocation is pretty easy once I regain access. On GitHub I can easily remove the old key via account management. On servers I can remove the key from the authorized_keys. If I want to be extra diligent most of my servers can be rebuilt from scratch as they are mostly stateless.

If I lose a signing key:

  • Revocation is slightly more troublesome, as usually revocation has to occur using a slightly complicated certificate revocation process. If used to verify packages for example, this could be problematic and I need to ensure that I save the revocation key.
  • … however, I currently only plan to use this for git commit signing which is not as critical.

Depending on how it’s set up, the yubikey itself will also ask for local authentication before doing any signing or authentication. This should also harden against adversaries that manage to steal the yubikey (which I would think is extremely unlikely in my case).

I don’t see a need to keep a backup of the key, as revocation and resetting should be fairly easy. I will only keep a backup of the revocation certificate.

Setup

Installing yubikey manager

Reference: https://developers.yubico.com/yubikey-manager/

sudo apt-add-repository ppa:yubico/stable
sudo apt update
sudo apt install yubikey-manager

Once installed, can see some info on the plugged in key:

tethik@mimikyu:~$ ykman info
Device type: YubiKey 4
Serial number: <snip>
Firmware version: 4.3.7
Enabled USB interfaces: OTP+FIDO+CCID

Applications
OTP     	Enabled
FIDO U2F	Enabled
OpenPGP 	Enabled
PIV     	Enabled
OATH    	Enabled
FIDO2   	Not available

Reset the yubikey

As good practice, it’s probably a good idea to reset the key before use. In my case I had experimented with the yubikey previously and wanted to start from a clean slate.

ykman openpgp reset
WARNING! This will delete all stored OpenPGP keys and data and restore factory settings? [y/N]: y
Resetting OpenPGP data, don't remove your YubiKey...
Success! All data has been cleared and default PINs are set.
PIN:         123456
Reset code:  NOT SET
Admin PIN:   12345678

Generating new gpg key

Reference: https://developers.yubico.com/PGP/Importing_keys.html

2022 Update: Ensure you have scdaemon installed, or gpg won’t detect your smartcard. If you get this error message when connecting to the card:

$ gpg-connect-agent --hex "scd apdu 00 f1 00 00" /bye
ERR 67108983 No SmartCard daemon <GPG Agent>

Installing scdaemon seems to resolve it.

$ sudo apt install scdaemon

What follows is my own notes from doing the same and may be useful in the future if the above link no longer works.

The basic procedure here is to generate the keypair on your own computer first, then import it to the yubikey. The yubikey itself does not support generating the PGP key. The key itself is also has two subkeys attached, one for signing and one for authentication, as per recommendations in the above reference. If needed, a further one could be added for encryption.

My yubikey (version 4) only supports 2048 bit rsa keys, so make sure to pick a cipher that your version of yubikey supports. Otherwise it will fail when you import the key to the yubikey.

I generate key with…

gpg --full-gen-key

From the output, grab the revocation certificate and store it somewhere safe.

gpg: key 79A936FAEE8C3B4A marked as ultimately trusted
gpg: revocation certificate stored as '/home/tethik/.gnupg/openpgp-revocs.d/644892D6680B2FABB541FA5779A936FAEE8C3B4A.rev'
public and secret key created and signed.

Using gpg --expert --edit-key <key-id> we will add two subkeys. One for authentication and one for signing. The authentication key will be used for SSH, and the signing key for commit signing.

gpg --expert --edit-key 13AFCE85

gpg (GnuPG) 2.0.22; Copyright (C) 2013 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.

pub  2048R/13AFCE85  created: 2014-03-07  expires: 2014-06-15  usage: SC
                     trust: ultimate      validity: ultimate
sub  2048R/D7421CDF  created: 2014-03-07  expires: 2014-06-15  usage: E
[ultimate] (1). Foo Bar <foo@example.com>

gpg> addkey

2048-bit RSA key, ID 13AFCE85, created 2014-03-07

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)
Your selection?

Finally, the key is imported to the yubikey. Note the keytocard command.

gpg --edit-key 13AFCE85

gpg (GnuPG) 2.0.22; Copyright (C) 2013 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.

pub  2048R/13AFCE85  created: 2014-03-07  expires: 2014-06-15  usage: SC
                     trust: ultimate      validity: ultimate
sub  2048R/D7421CDF  created: 2014-03-07  expires: 2014-06-15  usage: E
sub  2048R/B4000C55  created: 2014-03-07  expires: 2014-06-15  usage: A
[ultimate] (1). Foo Bar <foo@example.com>

gpg> toggle

sec  2048R/13AFCE85  created: 2014-03-07  expires: 2014-06-15
ssb  2048R/D7421CDF  created: 2014-03-07  expires: never
ssb  2048R/B4000C55  created: 2014-03-07  expires: never
(1)  Foo Bar <foo@example.com>

gpg> keytocard
Really move the primary key? (y/N) y
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]

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

SSH

Generate the public key via the following. The string it returns can be used for the authorized_keys file as well as GitHub auth.

gpg --export-ssh-key 4B4C120067AC0E52 (authentication key)

Switch from OpenSSH ssh-agent to GnuPG as ssh-agent.

First find path to the unix socket created by gnupg

gpgconf --list-dirs | grep ssh

Then set the SSH_AUTH_SOCK variable with that path. E.g.

export SSH_AUTH_SOCK=/run/user/1000/gnupg/S.gpg-agent.ssh

Consider adding it to your .profile to automatically apply it for new shells7.

You may also want to inform ssh that it should use gpg-agent, you can do so via:

echo "IdentityAgent /run/user/1000/gnupg/S.gpg-agent.ssh" >> ~/.ssh/config

Git Commit Signing

https://developers.yubico.com/PGP/Git_signing.html

Enable commit signing in your git config:

git config --global user.signingkey 79A936FAEE8C3B4A (signing key)
git config --global commit.gpgSign true

You should ensure also that the email you configured for git is the same as the one you generated the gpg key for, otherwise github will show your commits as unverified.

To verify that your commits are signed, you should see them show up if you use the --show-signature option.

git log --show-signature

To get the public key, for e.g import into github

gpg --export -a 79A936FAEE8C3B4A > yubikey.pub

Changing the OpenGPG PIN

When using the key now, it should ask you for a PIN. There are two pins to keep track of: the Normal PIN which is the one used for the daily operations of signing/encrypting using the card, and an Admin PIN which authorizes you to change settings on the card (e.g. force signature) as well as reset the Normal PIN.

By default, the Admin PIN’s value is 12345678. The normal PIN is 123456. The Admin PIN seems to require at least 8 digits, while the normal PIN can be shorter.

You can and should change these via the gpg --change-pin command. This will provide some protection if your key from being used if it is stolen

tethik@mimikyu:~$ gpg --change-pin
gpg: OpenPGP card no. <snip> detected

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

Option 2) allows you to change the normal PIN using the admin PIN, which is why it’s important to change both.

Extending the expiration

If you set your key to expire after some time, you’ll eventually need to refresh it.

You can either refresh the expiry date, or if you are more paranoid create new keys altogether. Here I show the former, how to refresh the expiry date.

$ gpg --expert --edit-key 644892D6680B2FABB541FA5779A936FAEE8C3B4A
> expire # sets a new expiry date on the primary key
> key 1 # selects subkey 1
> key 2 # selects subkey 1
> expire # expires both key 1 and 2
> save # saves the changes you made

Here is a log of me doing it for my own key:

$ gpg --expert --edit-key 644892D6680B2FABB541FA5779A936FAEE8C3B4A
gpg (GnuPG) 2.2.27; Copyright (C) 2021 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/79A936FAEE8C3B4A
     created: 2020-11-05  expired: 2022-11-05  usage: SC
     card-no: 0006 06917271
     trust: ultimate      validity: expired
ssb  rsa2048/0641BA0FD747AB91
     created: 2020-11-05  expired: 2022-11-05  usage: E
ssb  rsa2048/4B4C120067AC0E52
     created: 2020-11-05  expired: 2022-11-05  usage: A
     card-no: 0006 06917271
[ expired] (1). Joakim Uddholm <tethik@blacknode.se>
gpg> expire
Changing expiration time for the primary key.
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) 1y
gpg> save

gpg> key 1

sec  rsa2048/79A936FAEE8C3B4A
     created: 2020-11-05  expires: 2023-11-06  usage: SC
     card-no: 0006 06917271
     trust: ultimate      validity: ultimate
ssb* rsa2048/0641BA0FD747AB91
     created: 2020-11-05  expires: 2023-11-06  usage: E
ssb  rsa2048/4B4C120067AC0E52
     created: 2020-11-05  expires: 2023-11-06  usage: A
     card-no: 0006 06917271
[ultimate] (1). Joakim Uddholm <tethik@blacknode.se>

gpg> key 2

sec  rsa2048/79A936FAEE8C3B4A
     created: 2020-11-05  expires: 2023-11-06  usage: SC
     card-no: 0006 06917271
     trust: ultimate      validity: ultimate
ssb* rsa2048/0641BA0FD747AB91
     created: 2020-11-05  expires: 2023-11-06  usage: E
ssb* rsa2048/4B4C120067AC0E52
     created: 2020-11-05  expires: 2023-11-06  usage: A
     card-no: 0006 06917271
[ultimate] (1). Joakim Uddholm <tethik@blacknode.se>

gpg> expire
Are you sure you want to change the expiration time for multiple subkeys? (y/N) y
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) 1y
Key expires at mån  6 nov 2023 14:19:21 CET
Is this correct? (y/N) y

sec  rsa2048/79A936FAEE8C3B4A
     created: 2020-11-05  expires: 2023-11-06  usage: SC
     card-no: 0006 06917271
     trust: ultimate      validity: ultimate
ssb* rsa2048/0641BA0FD747AB91
     created: 2020-11-05  expires: 2023-11-06  usage: E
ssb* rsa2048/4B4C120067AC0E52
     created: 2020-11-05  expires: 2023-11-06  usage: A
     card-no: 0006 06917271
[ultimate] (1). Joakim Uddholm <tethik@blacknode.se>

For SSH, your public key should be the same as before (I guess it doesn’t contain an expiry date?), so you don’t need to send out an updated id_yubikey.pub to github or your authorized_keys-files.

For signed commits and github, you’ll need to generate a new public key again.

gpg --export -a 79A936FAEE8C3B4A > yubikey.pub

Published Jan 4, 2021

Security Engineer with a dash of software. Originally from Stockholm, now in Berlin. I like to hack things.