Setting up multiple YubiKeys + git signing on macOS | Glenn Gillen
Setting up multiple YubiKeys + git signing on macOS

Setting up multiple YubiKeys + git signing on macOS

May 02, 2023

Over the past few years, as an industry, we've all become a lot more aware of supply-chain risks with the things we're building. In particular, how humans are often the weakest link and that a well targeted phishing attempt can grant some remote person access to a system, and from there they can continue to escalate their access until they reach something of value. I've generally tried to err slightly on the side of paranoid about those risks, but my latest job means that more than ever I need to take as many precautions as possible to keep myself (and our customers) safe.

The 'what' and 'why' for YubiKeys

A YubiKey is a hardware device that you use as an additional factor to verify you are who you claim to be. If you're at all familiar with any existing multi-factor/two-factor authentication approaches, like where you use an app or receive a code in an SMS, these devices solve a similar problem. The difference being that they're generally plugged into your USB port rather than requiring you to open an app on your phone. Some models are very slim and are designed to be permanently plugged into your laptop.

"But hang on, if it's plugged into my laptop all the time doesn't that defeat the purpose? If someone steals my laptop then they've stolen my second factor too!"
— Someone (probably)

Something you need to consider with the security solutions you're adopting is exactly what threats you're trying to protect yourself against. Someone physically stealing my devices is definitely a concern, and one that I hope is mitigated by the use of strong local passwords, disk encryption, aggressive auto-locking of my system if I'm idle for a minute, etc. so I'm not adding YubiKeys into my process to strengthen that. Rather it's there to protect me from someone who isn't physically near me but is trying to impersonate me to gain access to a system. Maybe they're trying to brute force their way into my account somewhere? Have they somehow managed to actually work out what my password is? Are they trying to submit code changes to a repository I have commit access to by using my email address? YubiKeys are the way that all these things can be blocked until I push a little button to confirm I'm the one trying to take that action. All that said, we're going to try and further mitigate the physical risks too by setting PINs on our keys.

It's just another layer of defense. One that is difficult to clone or impersonate, while still being quite convenient.

Getting a YubiKey

For a start, don't get 1. You should probably always have a backup. I've got 3, a USB-C one that's permanently in my laptop, a USB one that is at home, and a USB-C + NFC that is portable. It's much easier to set them all up at the same time rather than trying to add to the collection incrementally so order whatever you want directly from Yubico in a single order and then you jump through all the hoops below when they arrive.

Verifying you've got legit keys

Imagine if you thought your keys had arrived, only to some time later discover all the effort you went to was wasted because some crafty individual had sent you fake keys and knew your secrets all along?! I'm not aware of that being an actual in-the-wild attack that's happening but you don't need to take the risk, as soon as you unbox you new keys you can verify they're genuine at https://www.yubico.com/genuine/.

Change the default settings

There's a few default settings we need to change ASAP. Download the YubiKey Manager, plug in one of your YubiKeys, open the YubiKey manager and change these values:

  • Applications > FIDO2 > FIDO2 PIN - You'll be asked for this whenever you try to use the YubiKey to login to a website. Can be up 63 characters, stick to alphanumeric though so that it will work reliably with anything implementing the WebAuthn spec.
  • Applications > PIV > PIN Management > Configure PINs > Change PIN - Is used when accessing the smartcard features of your YubiKey (e.g., logging into your workstation). Between 6 and 8 characters long.
  • Applications > PIV > PIN Management > Configure PINs > Change PUK - PUK = "PIN Unlock", you'll need this if you exceed the retry limit for entering your PIN. If you lock yourself out of this too you'll have to reset the YubiKey back to defaults and lose anything (e.g., certificates) you might have saved to it.
  • Applications > PIV > PIN Management > Configure PINs > Change Management Key - This is used to control access to this PIV (smartcard) section we've been updating. I'd suggest not ticking the "Protect with PIN" box here. It'll switch the device from using the Management Key to using one derived from the PIN, which in turn means the key changes if you change your PIN (including if you accidentally lock yourself out). It'll also potentially cause problems if you use anything other than YubiKey Manager we downloaded earlier to manage your keys. Make sure you know what you're doing, and the consequences, if you disagree and decide to tick this box.
  • Applications > OTP > Swap - If you've ever worked anywhere that has mandated the use of YubiKeys you've almost certainly seen someone accidentally put cccccbhciggvdudgvidcjhvfnjbvkiubitbkibubrbub into a chat room. That's someone accidentally hitting the key and having the code it generated pasted in. If you switch this setting so that OTP touch is configured for the "Long Touch" you'll need to hold the button on your key down for a full second to get the code. Not much of an additional inconvenience for day-to-day use, and will all but stop accidentally spamming chat rooms with your codes.

We're going to need some CLI tools installed for the latter steps, so let's get them all installed now:

brew install gnupg ykman pinentry-mac

The GPG interface on your YubiKey has its own PINs and reset codes which are still set to their default values, but they're not able to be managed via the YubiKey Manager GUI. So we'll need to change them via the ykman CLI:

ykman openpgp access change-admin-pin

Lets do the regular PIN too:

ykman openpgp access change-pin

PINs, passphrases, and Admin keys... oh my! There's a lot to remember here and it can all get a bit confusing later when you're trying to remember which particular value applies to the question you're being asked to provide. The one that's especially worth calling out right now is the GPG Admin PIN (which can actually have alphanumeric + other characters in it! 🤦‍♂️), which you set using the ykman openpgp access change-admin-pin above. Whenever we want to load a GPG key onto one of our YubiKeys you'll need this value... so you're going to need it quite a few times later. Remember that's what I mean when I say GPG Admin PIN.

For convenience sake the YubiKey will automatically sign approve signing and encryption requests if GPG requests it to do so (assuming it's plugged in!). I want the convenience of keeping my YubiKey plugged in, but I don't love the idea of it just silently approving actions in the background. I'll happily take the minor inconvenience of having to touch the key to ensure these actions only happen when I explicitly want them to, so let's change these default settings to a more secure posture:

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

If you bought multiple keys (you've got a backup key, right?) then remove the key you just setup and repeat all of the above steps with your other keys.

Configuring online services to use your keys

Now we're getting to the business end of being able to use the keys! I started by prioritising the highest value services (i.e., the ones that would cause the most inconvenience if they were compromised). For me that was:

Keep in mind that for all of these you'll want to register all of your keys. This is why having them all available so you can set them up at the one time is much more convenient than trying to remember to add them in later. Go to your first service, add your key, remove it and repeat the process with your other keys, then move on to the next service. I primarily use Safari is my browser, and it took me a while to notice that on some websites when the popup appeared to ask for access I'd have to choose the small "Other Options" link at the bottom of the popup, which would then present "Security key" as an option. You can see a demo of what to expect by going through the first couple of steps in the Yubico demo page.

Once you've got the first few sites setup it's worth taking a look at the Yubico setup page. Choose your key type and then click the "Compatible Services" link, it'll show you the other services you can configure. It's an easy way to initially go through a checklist of services you can setup while you're in that configuration mindset.

Signing your git commits with your keys

As I mentioned earlier, I'm working at a company that is especially security conscious. One of the requirements is that every commit I make to any repo I have access to is "signed" to verify it did indeed come from me. It's a good practice to put in place, and for the most part it's pretty low friction after a one-time investment in setting it up. Unfortunately the default approach to this still leaves a bit to be desired, as my keys are still on my workstation and so if someone was able to remotely gain access to my machine it'd be easy to make some commits in the background and have them appear to be signed by me. The signing here is less "signed by Glenn" and more "signed by Glenn's machine".

We can do better.

Setting up your primary/parent key

We need already installed the gpg tools we need earlier, but if you skipped that bit:

brew install gnupg pinentry-mac

Now we're going to generate a new primary key:

gpg --expert --full-generate-key

The options to select are:

  • 9 - ECC sign & encrypt
  • Curve 25519
  • Does not expire
  • Choose a password. Something strong but memorable.

The password you just set is your GPG Password. Now we need to grab the Key ID of the key we just generated:

gpg --list-secret-keys --keyid-format=long

You'll see output like the following:

-------------------------------
sec ed25519/A9C4550328B2FA3C 2023-01-06 [SC]
048282B36C1D7502380C5E8FB9F3220428C2EA3C
uid [ultimate] Your Name <you@email.comn>
ssb cv25519/6CD2394A1E1F4C10 2023-01-06 [E]

The Key ID is the value after the ed25519/ part, so in this case it is A9C4550328B2FA3C. We'll need that again later so we'll temporarily stash it into an environment variable:

export KEY_ID="your-key-id-here"

Adding additional identities

I have a separate email address associated with my git commits, and yet another that I use from my work-issued machine. If you're in a similar position and need to associate more than one email/identity with you key, this is the time to do it. Run the adduid command from the GPG prompt and answer the questions:

gpg --expert --edit-key $KEY_ID
> adduid

Type save to save once you're done.

Creating subkeys

I'm not going to go into the weeds on how exactly GPG works, other than to say an important part of it is to have separate keys for encryption, signing, and authenticating. Your YubiKey will store each separately too, so we need to set them all up now. We'll also set expiration dates on the subkeys so we can rotate them regularly, without having to rotate our parent key every few weeks:

gpg --expert --edit-key $KEY_ID

You'll be dropped into an interactive prompt within the gpg command, where you'll want to run addkey:

> addkey

Choose (11) ECC (set your own capabilities), (A) Toggle the sign capability, (Q) Finished, (1) Curve 25519 *default*, 3m (3 months).

Now we'll do the same for a signing key:

> addkey

Choose (10) ECC (sign only), (1) Curve 25519 *default*, 3m (3 months).

save all those changes and quit GPG.

Exporting your keys

gpg --armor --export-secret-keys $KEY_ID > parent.key
gpg --armor --export-secret-subkeys $KEY_ID > sub.key
gpg --armor --export $KEY_ID > public.key

We'll need the Key ID in a easy to reference format for future sessions (i.e., if we need to replace a lost YubiKey), so lets save that detail in a local file too:

cat $KEY_ID > key_id

Now you'll want to zip that file (and encrypt/add a pssword) and store it somewhere safe:

zip -e yubikey-gpg-keys.zip keyid parent.key public.key sub.key

Configuring GPG to use your first YubiKey

Now we can get on to moving our keys onto the YubiKey! We start by opening our key in edit mode:

gpg --expert --edit-key $KEY_ID

And then we tell GPG to move the key to our card:

> keytocard

Choose (1) Signature key, it'll prompt for the password you put on your GPG key earlier (your GPG Password), then it'll ask for the GPG Admin PIN. Next:

> key 1
> keytocard

Choose (2) Encryption key and answer the password/PIN prompts as before.

> key 1
> key 2
> keytocard

Choose (3) Authentication key and passwords/PINs again.

Finally save and quit:

> save

Repeating the process with multiple YubiKeys

Now we need to take that backup we made of our GPG keys earlier, and use it to bootstrap our other YubiKeys. We're essentially forced to reset GPG back to a state of finding the keys on disk (which is why we needed the backup), and then have it move those keys onto the next next. In doing that though, it'll remove the keys from the disk. So you'll repeat this restoration process for each subsequent key you need to setup:

killall gpg-agent
export GNUPGHOME=$(mktemp -d)
cd $GNUPGHOME
echo "pinentry-program /opt/homebrew/bin/pinentry-mac" > gpg-agent.conf
unzip /path/to/yubikey-gpg-keys.zip
gpg --import parent.key
gpg --edit-key $(cat keyid)

Note: at this point I initially kept getting errors saying gpg import from failed: No such file or directory. That error was from doing the work above in a temp directory and not having the pinentry config in place. Adding this note to try and help any others who might end up here via search results.

Now to move the keys to the card:

> key 1 (it should put an asterisk next to the subkey that has "usage: e", for encryption)
> keytocard (choose encryption, 2)
> key 1 (deselects/removes the asterisk)
> key 2 (it should put an asterisk next to the subkey that has "usage: sa", for signing + auth)
> keytocard (authentication)
> key 2
> key 3 (asterisk next to the subkey that has "usage: s", for signing)
> keytocard (signature)
> key 3
> save

Done. Remove your YubiKey and repeat with any others you want to setup.

Securing git operations on GitHub

We're going to need some of those files from the backup again, this time it's the keyid and public.key files. From the machine(s) where you're committing code run the following:

git config --global user.signingkey $(cat keyid)
git config --global commit.gpgsign true
gpg --import public.key

Then go to your GitHub > Settings > Keys page, click "New GPG Key", and paste the value of public.key into the key box.

Now when you commit changes you'll receive a prompt to enter your GPG PIN. If you enter that successfully everything will just seemingly come to a complete stop, but your YubiKey will be quietly sitting there flashing it's little green light essentially asking you to complete the operation. Hold your finger on the YubiKey touch pad and it'll sign the commit and you're done. I point this out specifically because 1) it's not entirely obvious and 2) if you're doing a batch operation (e.g., rebase) you'll only be prompted once for the PIN but you'll need to touch the YubiKey for each commit it needs to touch. So keep an eye out for the green light when you're making change to ensure you're confirming when required otherwise you'll just sit there indefinitely waiting for things to complete.

Committing changes after switching keys

If at some point in the future you try to use one of your other YubiKeys you'll get an error such as:

Pinentry Mac
Please insert the card with serial number:
XX XXX XXX

That's because GPG isn't expecting you to switch them about so freely. You can fix that by adding the following as a function in whatever local shell configuration you have (e.g, in ~/.zshrc)

function switchyubi() {
rm -r ~/.gnupg/private-keys-v1.d
gpgconf --kill gpg-agent
gpg --card-status
}

And then any time you switch keys and/or get the error above you can run:

switchyubi

Recapping on all the good things we've accomplished

👋 If you've made it this far, I'm assuming you're interested in building secure systems. You should come take a look at what we're currently building at Ockam.

You could probably speed run through this in about 15 minutes for 3x YubiKeys once you know what you're doing (i.e., you've done it 6 times in a row to make sure you've got the details right while writing a post like this 😉). In reality, you should allow an hour or so while you try and understand exactly what is going on and the swearing that might ensue. But it's definitely worth it! Because:

  • Now all of my commits to GitHub can be signed and marked as verified
  • We've all got confidence that at the time those commits were made it was infact me (or if you want to be pedantic: from a device that was configured to auth as me + had one of my yubikeys plugged into it + had a person touch the yubikey to confirm presence + that person new the PIN/password on my GPG key)
  • None of my private keys are stored on my laptop any more
  • Nobody can impersonate me and push verified commits to GitHub from a device that doesn't have my YubiKey plugged in
  • If someone were to remotely gain access to my machine, it's incredibly difficult (I'm reluctant to claim "impossible") to use my keys to impersonate me

Hopefully you've found this useful and can use it to quickly get your own keys setup. We've all got our own small parts to play to help keep the systems we build, and the people who depend on them, safe from attack.

Hi, I'm Glenn! 👋 I've spent most of my career working with or at startups. I'm currently the Director of Product @ Ockam where I'm helping developers build applications and systems that are secure-by-design. It's time we started securely connecting apps, not networks.

Previously I led the Terraform product team @ HashiCorp, where we launched Terraform Cloud and set the stage for a successful IPO. Prior to that I was part of the Startup Team @ AWS, and earlier still an early employee @ Heroku. I've also invested in a couple of dozen early stage startups.