Evilham

Evilham.com

YubiKey: PGP i autenticació SSH

Introduction

I own a few YubiKeys and use them, a lot.

If not familiar with YubiKeys, they are hardware tokens that help improve security in multiple ways:

  • They can do WebAuthn (modern web-based two factor authentication)
  • They can hold secret keys in a way that cannot be extracted, supporting these operations:
    • Signature
    • Encryption
    • Authentication

The most common use, and what is already very useful, is using these hardware tokens for two-factor authentication. For that there are plenty of online resources (though, people certainly can use help understanding and setting that up).

What I really care about is the latter bit: securing secret keys, particularly when it comes to securing SSH access to servers, and how that fits with PGP.

Incidentally, this is where I see most online documentation falling short.

There are similar hardware tokens, YubiKeys are what I’m familiar with, and what I’ll assume here.

Table of contents

PGP, subkeys and SSH

Keys (and subkeys, more below) in PGP may be marked as having one or multiple uses:

  • [C]ertification: a key marked like this acts as a source of truth key that can modify subkeys and the key itself
  • [S]ignature: a key marked like this can produce valid signatures that will be recognised as coming from the main key
  • [E]encryption: a key marked like this will be associated with the main key, and it will be used by other programs to encrypt things, so only the owner of the main key can read them. Note though: this is worded in a somewhat complicated fashion, because should the Encryption key be changed, messages encrypted for the older key cannot be read
  • [A]uthentication: a key marked like this will be associated with the main key, will be able to perform authentication e.g. against OpenSSH servers

The bit about PGP being usable for SSH [A]uthentication is not very widely known, and neither is the concept of subkeys.

Instead of thinking as a PGP key as: a single key for all things, we should think about it as: a [C]ertification key that I keep offline except when necessary, that is associated with multiple subkeys, one for each functionality.

Common state

When it comes to PGP, most advice results in keys with attributes [SCEA] or [SCA] with a [E] subkey.

What we achieve here

What we will achieve here is an offline [C] key with multiple subkeys: [S], [E], [A], with the secret parts loaded in a YubiKey (or possibly other hardware tokens).

(Roughly) why we do it

Commonly people have their PGP keys locally in a file that is secured by a password, or even separated PGP and SSH keys, protected in a similar fashion (sometimes even without a password).

When there are so many sneaky attack vectors, making a mistake once, can mean secrets being exfiltrated and possibly systems being compromised.

By not having such files anywhere but instead relying on the hardware token, we mitigate many parts of such risks.

PGP key generation

There are a bunch of security considerations as to the system used for this, many people have written better about it.

We will focus on:

  • Using a separated directory for the keyring and settings
  • Considering the directory as a temporary a in-memory thing or equivalent

Software requisites

TODO: Check debian - gnupg2 -

Setup the directory

On FreeBSD, to use an in-memory temporary directory, we can use following:

# Create directory
mkdir pgptmp
# Mount in-memory filesystem
mount -t tmpfs tmpfs pgptmp
# Secure permissions
chmod 700 pgptmp

Nothing that gets written to pgptmp, will be saved to disk, instead, it is temporary and is held in RAM.

Configuring gpghome

We must export the GNUPGHOME variable accordingly:

cd pgptmp
export GNUPGHOME="$(pwd)"

And it is convenient to set up certain configurations:

$ cat <<EOF > scdaemon.conf
# This is sometimes necessary
# TODO: Add mention fo scdaemon
disable-ccid
EOF

Generating the key

We use gpg --expert --full-generate-key, as it is the only way we get asked all the questions.

$ gpg --expert --full-generate-key

Please select what kind of key you want:
   (1) RSA and RSA
   (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 (sign and encrypt) *default*
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (13) Existing key
  (14) Existing key from card
Your selection?

The first question we get asked is, which kind of key we want to create.

Unless we are cryptography with good reasons (please share!), I’d pick the ECC, which is the default, but setting our own capabilities (Option 11), that is:

Your selection? 11

Possible actions for this ECC key: Sign Certify Authenticate
Current allowed actions: Sign Certify

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

Your selection?

We will want to generate a key with only [C]ertification capabilities, so we will ensure [S] and [A] are disabled:

Your selection? S

Possible actions for this ECC key: Sign Certify Authenticate
Current allowed actions: Certify

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

Your selection? Q
Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (2) Curve 448
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection?

As with the key type selection, unless we have cryptography knowledge that says otherwise, we will go with Curve 25519 (Option 1):

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

The use-case with which I am documenting this, requires a key that does not expire, but my usual key expires usually ~2 years in the future, which I try to extend every year. This choice is yours, more often might increase maintenance burden.

Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y

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

Real name:

Now, PGP has a weird concept of identity, and signatures are bound to the combination of Name, Email and Comment, it is usually not convenient to change this.

After thinking about these decisions (usually Comment is left blank):

Real name: YourName
Email address: YourEmail@example.org
Comment:
You selected this USER-ID:
    "YourName <YourEmail@Example.org>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit?

Now, when we confirm the identity, the key will be generated after we enter a passphrase to protect it.

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: GNUPGHOME/trustdb.gpg: trustdb created
gpg: directory 'GNUPGHOME/openpgp-revocs.d' created
gpg: revocation certificate stored as 'GNUPGHOME/openpgp-revocs.d/KEY_ID.rev'
public and secret key created and signed.

pub   ed25519 2023-10-06 [C]
      KEY_ID
uid                      YourName <YourEmail@Example.org>

This KEY_ID will uniquely identify our PGP key.

Generating the key

Since our key only has the [C]ertification capability, we want to add some subkeys so we can take advantage of all options.

For this, we start editing the key in --expert mode, else we won’t be able to access the [A]uthentication capability:

$ gpg --expert --edit-key KEY_ID
sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
[ultimate] (1). YourName <YourEmail@example.org>

gpg>

And now we create one key for each of the missing capabilities ([S]ignature, [E]ncryption, [A]uthentication), trusting the current cryptography recommendations unless we have very good reasons not to, and setting the expiry according to our use-case.

Note too that the [A]uthentication capability is not available directly, but rather through the ECC (set your own capabilities) option:

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
  (14) Existing key from card
Your selection? 10
Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (2) Curve 448
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection?
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)
Key does not expire at all
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  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
[ultimate] (1). YourName <YourEmail@example.org>

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
  (14) Existing key from card
Your selection? 12
Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (2) Curve 448
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection?
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)
Key does not expire at all
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  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb  cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
[ultimate] (1). YourName <YourEmail@example.org>

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
  (14) Existing key from card
Your selection? 11

Possible actions for this ECC key: Sign Authenticate
Current allowed actions: Sign

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

Your selection? S

Possible actions for this ECC key: Sign Authenticate
Current allowed actions:

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

Your selection? A

Possible actions for this ECC key: Sign Authenticate
Current allowed actions: Authenticate

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

Your selection? Q
Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (2) Curve 448
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection?
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)
Key does not expire at all
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  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb  cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb  ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

And finally we save the modified key

gpg> save

Putting the Key into the YubiKey

This is done by editing the key we just created:

$ gpg --expert --edit-key KEY_ID
Secret key is available.

sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb  cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb  ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

We have to select and de-select each subkey and send them to the key with keytocard, and selecting the destination slot that best matches the capability:

gpg> key 1

sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb* ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb  cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb  ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

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

sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb* ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb  cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb  ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

Note: the local copy of the secret key will only be deleted with "save".
gpg> key 1

sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb  cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb  ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

gpg> key 2

sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb* cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb  ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

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

sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb* cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb  ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

Note: the local copy of the secret key will only be deleted with "save".

gpg> key 2

sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb  cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb  ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

gpg> key 3

sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb  cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb* ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

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

sec  ed25519/KEY_ID
     created: 2023-10-06  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/KEY_ID_S
     created: 2023-10-06  expires: never       usage: S
ssb  cv25519/KEY_ID_E
     created: 2023-10-06  expires: never       usage: E
ssb* ed25519/KEY_ID_A
     created: 2023-10-06  expires: never       usage: A
[ultimate] (1). YourName <YourEmail@example.org>

Note: the local copy of the secret key will only be deleted with "save".
gpg>
Save changes? (y/N) N
Quit without saving? (y/N) y

Note that I exit without saving, because I want to back up the [C]ertification key offline, and saving the key here will remove the private part from the keyring.

It will be contained in the YubiKey and can be used with the password, but it cannot be retrieved out of it.

Copy the public key

We now want to export the public key:

$ gpg --export KEY_ID > pgp.pubkey.bin
$ gpg --export -a KEY_ID > pgp.pubkey.pem

Which we will need to publish and import on our daily driver.

Using the YubiKey

Now we can disconnect the backed up [C]ertification key, reboot into our persistent environment or change computers, depending on what our security precautions were, and move to our daily driver system.

Once there, we import the public key into our regular keyring:

# This is not the same system we were in before(!)
gpg --import -a < pgp.pubkey.bin

And regular PGP operations should request unlocking the corresponding private key with its regular password.

SSH authentication

In order to take advantage of SSH authentication, we want to use the gpg-agent as an SSH agent.

This is usually done by setting up the gpg-agent.conf

$ cat <<EOF > gpg-agent.conf
enable-ssh-support
pinentry-program /usr/local/bin/pinentry-gtk2
# Possibly on Debian-based:
#pinentry-program /usr/bin/pinentry-gtk2
# 2 min
default-cache-ttl 120
# 5 min
max-cache-ttl 300
# Grab input
grab
EOF
# Applying this might require restarting the gpg-agent, you can use:
# gpgconf --kill gpg-agent

This is usually done adding to the ${HOME}/.profile file something similar to:

# Setup the environment variable so the correct SSH agent is contacted
export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
# Tell the gpg-agent to start and update its tty
echo "UPDATESTARTUPTTY" | gpg-connect-agent /bye

Note that if you use gnome, KDE or similar, their agents might have support for something like this already.

Once the SSH and GPG agents have been set up, the SSH agent will list the key contained in the YubiKey as available for authentication:

$ ssh-add -L
ssh-ed25519 AAAA...J cardno:YY_YYY_YYY

By adding that line to an authorized_keys file, we should be able to use the [A]uthentication private key to enter with SSH on that system.

Managing expiry dates and the [C]ertification key

This is similar to the creation and sending to card process, except we use the expiry gpg command after editing.

We can change the expiry date of multiple subkeys by selecting more than one and export the resulting public key.

Conclusion

Given the implications of such setup, some thought and personal/corporation-wide trade-offs must be considered, it is certainly not a 5 minute task.

But it also isn’t as scary as it sounds and the full described process (along with documenting it here) took about 90 minutes.

The result is actually more usable and secure than before:

  • In order to login to places, we need both something physical (the YubiKey) and something we know (its protection password)
  • If we want to be sure no keys are loaded, we just unplug the YubiKey
  • If we want to log in places, sign something or read an encrypted email, we plug it in
  • We can backup and revoke the keys as necessary, if we ever lose the YubiKey, we can revoke the subkeys and generate new ones from the offline [C]ertification key, while still having access to previously encrypted content
  • If something bad happens with our daily driver, odds are that not having id_rsa files or keys in the keyring will save us a lot of headaches and lateral jumps