With NixOS containers in ChromeOS, I use an SSH key in a Yubikey to authenticate to remote hosts and keep the key separated from the container.

Creating keys on the Yubikey and ensuring good UX while using them can be tricky, but yubikey-agent makes it seamless. There’s a catch though! It supports one SSH key only, while I’d like to use two:

  1. One to authenticate (e.g., to GitHub).
  2. The other to sign git commits.

This way, I can implement separation of duties and can “decommission”1 each key independently.

A few PRs in the yubikey-agent repository add support for multiple keys and even different policies (touch, PIN). Most try to develop a generic approach, while I only need to support two keys. So, I decided to scratch my own itch and implement a solution.

Luckily, the agent is written in Go, is easy to understand. The piv-go does the heavy lifting. Under the hood:

  • It asks the Yubikey to generate an elliptic curve (ECC256) key pair in the PIV Authentication slot.
  • Then it creates a self-signed X509 certificate to wrap the public key and make it available to clients (our SSH agent) through the PKCS#11 interface.

This Yubikey tutorial describes roughly the same process, but I find the Go code easier to read and more precise. In addition, the setup guides the user through the PIN/PUK setup and removes the need for a management key by delegating its control to the PIN/PUK.

To support two keys, I decided to go the simplest approach and make the minimum set of changes to the code to generate a new key (during the setup phase) and then serve it through the SSH agent. I initially considered generating the key through the CLI, but the delegation of the management key made it hard to do, so I just went with code for the setup as well. By poking around the piv-go code and the PIV standard, I decided to use slot 9c, which is used for Signature (the Yubikey docs even mention using it for git commit).

The result is in my fork of the project. I intentionally kept the code as simple as possible, so that it is easy to maintain and follow upstream (in case I need to). If you want to try it out, setup the Yubikey as usual and then run yubikey-agent -setup-sign to generate the additional key. When running the agent, it will gracefully try loading both keys and ignore the one for Signatures if it cannot be found.

I have also ensured that the ordering of the keys doesn’t change and have configured my git client to sign with the second key returned by the agent. This is convenient to use and matches my configuration on other hosts.

Here is the result:

[I] aldur@lxc-nixos ~> ssh-add -L
ecdsa-sha2-nistp256 AAAAE2V...iUkW4JQUDA= YubiKey #25972834 PIV Slot 9a
ecdsa-sha2-nistp256 AAAAE2V...pKYi/Zh/HA= YubiKey #25972834 PIV Slot 9c

To deploy my changes, I wrote a small Nix overlay that applies my patch:

(final: prev: {
  yubikey-agent = (
    prev.yubikey-agent.overrideAttrs (old: {
      # Used a patch instead of overriding the source so that it will keep
      # working (or explicitly break) on upstream updates.
      patches = (old.patches or [ ]) ++ [
        (prev.fetchurl {
          url = "https://github.com/aldur/yubikey-agent/commit/f7a6769fd832a867e62228c8ddb0133174db64bf.patch";
          hash = "sha256-swQb3N89yAJSQ4pkUq2DDKvEFBlzhr/tbNMdC2p60VE=";
        })
      ];
    })
  );
})
  1. Ideally I’d want to revoke keys, but there is no built-in way to do it for SSH (that I am aware of). The PIV standard relies on certificates, so there might be a way to revoke them – but then, consumers would need to check a revocation list, and I don’t think they do. GPG keys can be revoked, but come with other downsides.