UP | HOME
Published
2017-09-29
NixOS release
17.03

In this article we will setup NixOS to use GPG-keys for SSH authentication, while storing the keys securely on a Yubikey. When I did this myself, I had to read a lot of different sources to understand all the steps of this process. So to save some time for someone else I compiled all these details into a (hopefully) comprehensible article.

GPG basics

A SSH key pair consists of a public key (stored on the server) and a private key (stored on the client). The anatomy of these keys are simple, they are just RSA keys that don't contain any meta data used in the authentication process.

Anatomy of GPG keys

GPG keys on the other hand, are much more complex, they are more like a data structure with different components than a "key". In essence a GPG key consists of 4 parts:

Primary key pair
Also called master key
Primary identity
Also just called user identity or uid
Extra identities
Extra user identities linked to the primary key pair
Subkey pairs
Extra keys derived from the primary key pair

key-anatomy1.png

In earlier versions when you created a GPG key, it would only consist of the primary key pair and the primary identity. Which probably why it is called a GPG key, and not something like GPG structure or object.

Identities

When you create a new GPG key, you are asked for the name and email of the user that they key belongs to. When creating the actual the GPG key, a signature is created from the name and email using the primary private key. This way the identity is associated with the primary key pair.

The extra identities is there so that you can associate other email adresses to your primary key pair. For example, you might want to have both your private and work email associated with the key for simplicity.

Subkey pairs

Subkey pairs are basically normal key pairs that has a signature that associates them to the primary key pair. When a subkey pair is created, the primary private key is used to create this signature.

The reason why you would want to create subkey pairs is because in GPG you can specify what actions a key pair is allowed to perform. There are 4 different actions a key pair can perform. The most powerful action is called Certify, which allows a key pair to create subkey pairs from itself. A subkey pair is just a key pair but with a signature that associates it to another key pair. The primary key pair will therefor always have the Certify capability, so that it can create subkey pairs.

There are 3 other actions a key pair can perform:

Sign
Signing files, emails, etc.
Encrypt
Encrypting files, emails, etc.
Authenticate
Authenticating against external services, such as SSH

Normally you remove all actions but the Certify action from the primary key pair and then create subkey pairs for each of the other 3 actions. This way you can use the 3 subkey pairs in your day-to-day work, and store the primary key pair offline safely.

Smart cards

A smart card (like Yubikey) is basically a physical device that can store private keys and perform the cryptographic operations directly on the device. Smart cards are designed so that once the private keys are imported to the device they cannot be extracted.

So if your computer would become compromised, the attacker would not be able to get the private key since it is stored outside the computer and no cryptographic operations are performed on the computer. The only thing the attacker would be able to extract is the input and output of the device.

Our goal

Now that we better understand how GPG works, lets define the goal of this article:

  • Create a GPG key
    • With a primary keypair that can only Certify
    • With a single user identity
    • With a subkey pair that can only Sign
    • With a subkey pair that can only Encrypt
    • With a subkey pair that can only Authenticate
  • Backup the primary key pair and it's subkey pairs offline on a USB-stick
  • Configure our Yubikey
  • Move the 3 subkey pairs to our Yubikey
  • Configure NixOS to use GPG and our Yubikey for SSH authentication:

key-usage.png

Figure 2: Agent replacement

Lets start by creating our new key!

Creating a new key

All key creation should be performed on a live booted machine without network access. A common way is to live boot Tails and perform all key generation steps inside it. Then copy the generated keys to a USB-stick that is kept in a safe place offline. From here on out all commands should be performed on a live booted Tail machine without network access.

Like mentioned in the goals section, we'll start by creating a GPG key that has primary key pair that can only Certify. To start the process we run:

$ gpg2 --expert --full-generate-key

(We use the flag --expert and --full-generate-key to get all available options when generating our key.)

The first prompt asks what kind of key we want to generate, here we select (8) RSA (set your own capabilities), so that we get to choose what actions the key can perform. In the actions prompt we untoggle all actions except for Certify. In the keysize prompt we input 4096 bits. The Yubikey supports storing keys that are 4096-bits so we will use 4096 bits on all key pairs.

After the GPG key has been created we can verify that it actually exists:

$ gpg2 --list-keys
gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: next trustdb check due at 2018-09-17
/home/me/.gnupg/pubring.kbx
----------------------------
pub   rsa4096 2017-09-17 [C] [expires: 2018-09-17]
      FAB89F7D27C399063F600B036B3D0050CAE0C3C4
uid           [ultimate] Richard Zetterberg <richard.zetterberg@example.com>

Next step is to create our 3 subkey pairs from that primary key pair. We use the key ID from the previous step and run:

$ gpg2 --expert --edit-key FAB89F7D27C399063F600B036B3D0050CAE0C3C4

When starting that command we are presented with a overview of the key and a gpg> prompt that accept commands. We start by entering:

gpg> addkey

We are now shown the key creation prompt again. This time we also select (8) RSA (set your own capabilities) for key type. However, this time we only select Sign capability. Same key size as before, 4096-bits.

After creating our Sign subkey, we should see the overview and the gpg> command prompt again:

sec  rsa4096/6B3D0050CAE0C3C4
     created: 2017-09-17  expires: 2018-09-17  usage: C
     trust: ultimate      validity: ultimate
ssb  rsa4096/425E1FD67112482A
     created: 2017-09-17  expires: 2018-09-17  usage: S
[ultimate] (1). Richard Zetterberg <richard.zetterberg@example.com>

gpg>

By the way, when gpg list keys, it uses the following terminology:

sec
SECret key
ssb
Secret SuBkey
pub
PUBlic key
sub
public SUBkey

Next we perform the same addkey steps for the separate Encrypt and Authenticate keys. When done we should see this:

sec  rsa4096/6B3D0050CAE0C3C4
     created: 2017-09-17  expires: 2018-09-17  usage: C
     trust: ultimate      validity: ultimate
ssb  rsa4096/425E1FD67112482A
     created: 2017-09-17  expires: 2018-09-17  usage: S
ssb  rsa4096/5993DBCB1C9AC33B
     created: 2017-09-17  expires: 2018-09-17  usage: E
ssb  rsa4096/8DB918AB30C19F1C
     created: 2017-09-17  expires: 2018-09-17  usage: A
[ultimate] (1). Richard Zetterberg <richard.zetterberg@example.com>

gpg>

We then save our changes:

gpg> quit
Save changes? (y/N) y

Now when listing keys we will see our primary key pair and 3 subkey pairs:

$ gpg2 --list-keys
/home/me/.gnupg/pubring.kbx
----------------------------
pub   rsa4096 2017-09-17 [C] [expires: 2018-09-17]
      FAB89F7D27C399063F600B036B3D0050CAE0C3C4
uid           [ultimate] Richard Zetterberg <richard.zetterberg@example.com>
sub   rsa4096 2017-09-17 [S] [expires: 2018-09-17]
sub   rsa4096 2017-09-17 [E] [expires: 2018-09-17]
sub   rsa4096 2017-09-17 [A] [expires: 2018-09-17]

Backing up the keys

Next we mount our USB-stick that we will back up our keys on and export the keys by running:

$ gpg2 --armor --output /path/to/usb/stick/secret-keys.txt \
    --export-secret-key richard.zetterberg@example.com

This will export both the primary key and all its subkeys. Now we can unmount the USB-stick and store that in a safe place.

Before storing our public keys on our Yubikey, we need to remove the primary key from our GPG key so that it only exists offline on our USB-stick. To do this we:

  • Export subkeys only
  • Delete the whole key
  • Re-import the exported subkeys

This will leave us with a GPG key that has all 3 subkeys but not the primary key.

First we export the subkeys:

$ gpg2 --armor --output ./secret-subkeys.txt \
    --export-secret-subkeys richard.zetterberg@example.com

Then we delete the whole key:

$ gpg2 --delete-secret-key richard.zetterberg@example.com

We answer Delete key for all prompts that ask if we should delete the subkeys. After doing this we should not see any keys when running gpg2 --list-keys.

Finally we import the subkeys again:

$ gpg2 --import ./secret-subkeys.txt

After the subkeys have been imported we should see the following when running gpg2 --list-secret-keys:

$ gpg2 --list-secret-keys
/home/me/.gnupg/pubring.kbx
----------------------------
sec#  rsa4096 2017-09-17 [C] [expires: 2018-09-17]
      FAB89F7D27C399063F600B036B3D0050CAE0C3C4
uid           [ultimate] Richard Zetterberg <richard.zetterberg@example.com>
ssb   rsa4096 2017-09-17 [S] [expires: 2018-09-17]
ssb   rsa4096 2017-09-17 [E] [expires: 2018-09-17]
ssb   rsa4096 2017-09-17 [A] [expires: 2018-09-17]

Note that instead of saying sec, it now says sec#. This means that the key is now stored somewhere else. Now we are ready to store the subkeys on our Yubikey.

Importing subkeys on Yubikey

Before importing the subkeys on our Yubikey we need to configure it. We plug in the Yubikey and check that we can communicate with it:

$ gpg-connect-agent --hex "scd apdu 00 f1 00 00" /bye
D[0000]  04 03 04 90 00                                     .....
OK

We then configure the Yubikey to be used as a Smart card:

$ ykpersonalize -m82
Firmware version 4.3.4 Touch level 773 Program sequence 1

The USB mode will be set to: 0x82

Commit? (y/n) [n]: y

Now that the Yubikey is configured as a smart card, we can import our subkeys to it, by running --edit-key for our key:

$ gpg2 --edit-key FAB89F7D27C399063F600B036B3D0050CAE0C3C4
gpg (GnuPG) 2.1.18; Copyright (C) 2017 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  rsa4096/6B3D0050CAE0C3C4
     created: 2017-09-17  expires: 2018-09-17  usage: C
     trust: ultimate      validity: ultimate
ssb  rsa4096/425E1FD67112482A
     created: 2017-09-17  expires: 2018-09-17  usage: S
ssb  rsa4096/5993DBCB1C9AC33B
     created: 2017-09-17  expires: 2018-09-17  usage: E
ssb  rsa4096/8DB918AB30C19F1C
     created: 2017-09-17  expires: 2018-09-17  usage: A
[ultimate] (1). Richard Zetterberg <richard.zetterberg@example.com>

gpg>

In the gpg> prompt we type:

gpg> key 1
gpg> keytocard

This will select the first subkey and import it to our Yubikey. Gpg will ask us where to store it, we just select the appropriate usage. When storing the key on the Yubikey we will be asked for the admin pin, which is 12345678 by factory default (here's how you change it).

We then import the second subkey:

gpg> key 2
gpg> keytocard

And the last subkey:

gpg> key 3
gpg> keytocard

After all 3 keys have been imported we quit and save:

gpg> quit
Save changes? (y/N) y

Now the subkeys have been moved from the live booted computer into the Yubikey. If we list the keys we will see that they now are pointers to a smart card and not stored locally:

$ gpg2 --list-secret-keys
/home/me/.gnupg/pubring.kbx
----------------------------
sec#  rsa4096 2017-09-17 [C] [expires: 2018-09-17]
      FAB89F7D27C399063F600B036B3D0050CAE0C3C4
uid           [ultimate] Richard Zetterberg <richard.zetterberg@example.com>
ssb>  rsa4096 2017-09-17 [S] [expires: 2018-09-17]
ssb>  rsa4096 2017-09-17 [E] [expires: 2018-09-17]
ssb>  rsa4096 2017-09-17 [A] [expires: 2018-09-17]

The > suffix signifies "pointer to key".

Using the Yubikey

Now that the Yubikey has been configured and contains the subkeys we can start using it. To test this out we first need to export the public key:

$ gpg2 --armor --export richard.zetterberg@example.com > public_key

To simulate that we want to start using our Yubikey on a new computer we remove the .gnupg directory:

rm -rf ~/.gnupg

Now when we run gpg2 --list-secret-keys or gpg2 --list-keys we will not see any keys:

$ gpg2 --list-secret-keys
gpg: directory '/home/me/.gnupg' created
gpg: new configuration file '/home/me/.gnupg/dirmngr.conf' created
gpg: new configuration file '/home/me/.gnupg/gpg.conf' created
gpg: keybox '/home/me/.gnupg/pubring.kbx' created
$ gpg2 --list-keys

To start using the Yubikey on a new computer we need the public key. Sadly the Yubikey cannot store the public key, so we either need to bring it with us to the new computer or host it somewhere on the internet.

Bringing the public key along

Let's suppose we have the public key with us, we can just import it:

$ gpg2 --import ./public_key
gpg: /home/me/.gnupg/trustdb.gpg: trustdb created
gpg: key 6B3D0050CAE0C3C4: public key "Richard Zetterberg <richard.zetterberg@example.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1

After importing the public key, we can list the public keys, but not the secret keys:

$ gpg2 --list-public-keys
/home/me/.gnupg/pubring.kbx
----------------------------
pub   rsa4096 2017-09-17 [C] [expires: 2018-09-17]
      FAB89F7D27C399063F600B036B3D0050CAE0C3C4
uid           [ unknown] Richard Zetterberg <richard.zetterberg@example.com>
sub   rsa4096 2017-09-17 [S] [expires: 2018-09-17]
sub   rsa4096 2017-09-17 [E] [expires: 2018-09-17]
sub   rsa4096 2017-09-17 [A] [expires: 2018-09-17]
$ gpg2 --list-secret-keys

This is because we need to tell gpg that the secret keys for those public keys are stored on our Yubikey:

$ gpg2 --card-status

We'll see a lot of output of about the Yubikey, and at the end we see the keys stored on it:

sec#  rsa4096/6B3D0050CAE0C3C4  created: 2017-09-17  expires: 2018-09-17
ssb>  rsa4096/425E1FD67112482A  created: 2017-09-17  expires: 2018-09-17
                                card-no: 0006 05578763
ssb>  rsa4096/5993DBCB1C9AC33B  created: 2017-09-17  expires: 2018-09-17
                                card-no: 0006 05578763
ssb>  rsa4096/8DB918AB30C19F1C  created: 2017-09-17  expires: 2018-09-17
                                card-no: 0006 05578763

After doing that we now see the secret keys in the list:

$ gpg2 --list-secret-keys
/home/me/.gnupg/pubring.kbx
----------------------------
sec#  rsa4096 2017-09-17 [C] [expires: 2018-09-17]
      FAB89F7D27C399063F600B036B3D0050CAE0C3C4
uid           [ unknown] Richard Zetterberg <richard.zetterberg@example.com>
ssb>  rsa4096 2017-09-17 [S] [expires: 2018-09-17]
ssb>  rsa4096 2017-09-17 [E] [expires: 2018-09-17]
ssb>  rsa4096 2017-09-17 [A] [expires: 2018-09-17]

Installing and configuring dependencies

Now that we have created our GPG key and configured our Yubikey it's time to configure NixOS to use the Yubikey.

First, we'll make sure all tools and services are installed and configured. To make it easier for ourselves we will create a new file called yubikey-gpg.nix and import it in our configuration.nix:

/etc/nixos/configuration.nix

{ config, pkgs, ... }:

{
  imports = [
    ./yubikey-gpg.nix
  ];
}

Yubikey personalization tool

The first tool we will install is the Yubikey Personalization tool. This tool is used to communicate with the yubikey to update its configuration:

/etc/nixos/yubikey-gpg.nix

{ config, lib, pkgs, ... }:

{
  environment.systemPackages = with pkgs; [
    yubikey-personalization
  ];
}

After running nixos-rebuild switch we will have the command ykpersonalize available. After plugging in our Yubikey we can use ykpersonalize -V to check the firmware version of it:

$ ykpersonalize -V
USB error: Access denied (insufficient permissions)

The reason we are seeing this error is because the device manager udev doesn't allow access to USB-devices unless there is a rule for it. The good news is that the ykpersonalize program comes packaged with a udev rule file that allows user access the device.

In NixOS there is a simple way of using udev rules packaged with a nixpkg. You simply supply services.udev.packages with a list of packages you want to use udev rules from:

/etc/nixos/yubikey-gpg.nix

{ config, lib, pkgs, ... }:

{
  environment.systemPackages = with pkgs; [
    yubikey-personalization
  ];

  services.udev.packages = with pkgs; [
    yubikey-personalization
  ];
}

The nixpkgs added to this list simply needs to populate the directory $out/lib/udev/rules.d with udev rule files, and then NixOS picks up these files and adds them to /etc/udev/rules.d when nixos-rebuild is run. We can see that the yubikey-personalization nixpkg installs a udev rule into this directory.

After updating yubikey-gpg.nix and running nixos-rebuild switch we need to tell udev to reload the rules. We can either do that by rebooting or running the following commands:

sudo -- udevadm control --reload-rules && udevadm trigger

Then we can re-run ykpersonalize and we'll get the firmware version of our Yubikey:

$ ykpersonalize -V
Firmware version 4.3.4 Touch level 773 Program sequence 1
1.17.3

GnuPG

After we know that we can communicate with our Yubikey we'll install GnuPG and the needed tools to use the Yubikey as a smartcard. Besides installing GnuPG we need to enable pcscd, which is a daemon that exposes a generic API that allows programs to communicate with smartcards without knowing the specifics about the device.

/etc/nixos/yubikey-gpg.nix

{ config, lib, pkgs, ... }:

{
  services.pcscd.enable = true;

  environment.systemPackages = with pkgs; [
    gnupg
    yubikey-personalization
  ];

  services.udev.packages = with pkgs; [
    yubikey-personalization
  ];
}

After running nixos-rebuild switch we now have everything to use GnuPG and Yubikey together.

GPG/SSH Agent

The last step is to setup gpg-agent instead of ssh-agent. Basically gpg-agent emulates ssh-agent but lets you use normal SSH keys and GPG keys.

key-usage.png

So ssh-add ~/.ssh/id.rsa will work like before, so you don't need to change your workflow if you just want to try out using GnuPG for SSH authentication.

/etc/nixos/yubikey-gpg.nix

{ config, lib, pkgs, ... }:

{
  programs.ssh.startAgent = false;

  services.pcscd.enable = true;

  environment.systemPackages = with pkgs; [
    gnupg
    yubikey-personalization
  ];

  environment.shellInit = ''
    gpg-connect-agent /bye
    export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
  '';

  services.udev.packages = with pkgs; [
    yubikey-personalization
  ];
}

Now after we run nixos-rebuild switch we'll have to manually kill ssh-agent, then log in and out. After that we check that gpg-agent is running and not ssh-agent:

$ ps aux | grep ssh-agent

$ ps aux | grep gpg-agent
ps aux | grep gpg-agent
me       1505  0.0  0.0 348084  3160 ?        Ss   07:12   0:03 gpg-agent --homedir /home/me/.gnupg --use-standard-socket --daemon

We can use ssh-add like we did before, and we'll see the cached keys along with our Yubikey card:

$ ssh-add -L
ssh-rsa [truncated for readability] /home/me/.ssh/id.rsa
ssh-rsa [truncated for readability] cardno:0000239725

Now we only need to export our public key and put it on the server:

$ gpg2 --export-ssh-key richard.zetterberg@example.com > ~/.ssh/yubikey.pub

Then you just need to change your SSH config to use that public key and GPG will handle everything else for you:

Host myserver
    Hostname 8.8.8.8
    User admin
    IdentityFile ~/.ssh/yubikey.pub