Yubikey PIV for SSH on Macs

We generally use Duo for two factor authentication, including SSH. We have some scenarios where people would like to use two factor authentication, but Duo is considered too intrusive. For example, when using Duo for SSH-based git push and git pull there's no Duo prompt, it only works with Duo push, and you have to unlock your phone, tap on the Duo notification, then tap on 'approve'. You may also need to make changes to your git server; with GitLab we have to use a non-suid copy of login_duo, with a second configuration file, and manually update the git account's authorized_keys file to use login_duo for every SSH key (that needs to use two factor):

command="/usr/sbin/login_duo_git -c \
  /etc/security/login_duo_git.conf -f alice \
  /home/git/gitlab-shell/bin/gitlab-shell key-2",... \
  ssh-rss AAAA... alice@example.org

I've had a few Yubikeys lying around, and I finally decided to try one for SSH. I found Thomas Habets' Yubikey 4 for SSH with physical presence proof instructions for Linux, and modified them to work on Macs. I've tested with OS X 10.11 (El Capitan) and macOS 10.12 (Sierra), but if you're using an older version of OS X you should upgrade, or follow Yubikey's instructions to ensure that Yubikeys are recognized.

Setup

Install the Yubikey personalization tool (ykpers), the Yubikey PIV tool (yubico-piv-tool), and the OpenSC tools (opensc):

% brew install ykpers yubico-piv-tool opensc

You can also install Yubico's GUI PIV Manager.

Check that your Yubikey can be used as a PIV card:

% opensc-tool --list-readers
# Detected readers (pcsc)
Nr.  Card  Features  Name
0    Yes             Yubico Yubikey 4 OTP+U2F+CCID

The Name field should include the string CCID; if it does not, and you have a Yubikey NEO, this might work (untested):

ykpersonalize -m1

Change defaults

Yubikeys come with default settings for PIN and PUK. The PIN is used to unlock your Yubikey, and the PUK is used to reset your Yubikey if you forget your PIN.Choose (or generate) a 4-8 character PIN (despite the name, letters and [some symbols](https://developers.yubico.com/yubikey-piv-manager/PIN_and_Manag ement_Key.html) are allowed), and an 8 character PUK.

Save your new PUK somewhere secure, then change your PIN and PUK with these commands:

% yubico-piv-tool -a change-pin
Enter pin: 123456
Enter new pin:
Verifying - Enter new pin:
Successfully changed the pin code.

% yubico-piv-tool -a change-puk
Enter puk: 12345678
Enter new puk:
Verifying - Enter new puk:
Successfully changed the puk code.

You should also change your Yubikey's management key. The new key needs to be a 48 character hexadecimal string, which can be generated with:

% hexdump -v -e '24/1 "%02X" "\n"' -n 24 /dev/random
10493A81CC0802C7CB4C7C5B44774FBCE741DB97CB492D7D

Save your new key somewhere secure, then change the key with

% yubico-piv-tool -a set-mgm-key -n KEY -v 9 --touch-policy=always

The --touch-policy=always option configures the Yubikey to always require a touch when changing the management key in the future.

Generate and self-sign a new key

First you'll need to generate a new RSA public/private key pair: it will be stored in slot 9a on the card (which is reserved for PIV Authentication), it will always require a touch to use it, and require a PIN when unlocking. You'll need to enter your management key when prompted, then touch the Yubikey within 15 seconds (while the LED blinks slowly; it blinks more quickly when generating the key). The command outputs the new public key to public.pem.

% yubico-piv-tool -k -a generate -s 9a -o public.pem \
> --touch-policy=always 
Enter management key:

Touch the Yubikey

Successfully generated a new private key.

Next, self-sign the public key:

% yubico-piv-tool -a verify-pin -a selfsign-certificate -s 9a \
> -S '/CN=swl@stanford.edu SSH key/' -i public.pem -o cert.pem
Enter PIN:
Successfully verified PIN.

Touch the Yubikey

Successfully generated a new self signed certificate.

Finally, re-import the signed certificate back onto the Yubikey:

% yubico-piv-tool -k -a import-certificate -s 9a -i cert.pem
Enter management key:

Touch the Yubikey

Successfully imported a new certificate.

You can now remove cert.pem and public.pem.

Export the public key in SSH format

ssh-keygen can export the SSH public key from the Yubikey:

% ssh-keygen -D ${HOMEBREW_ROOT}/lib/opensc-pkcs11.so -e
ssh-rsa AAAAB...

Add this SSH key to your ~/.ssh/authorized_keys file on systems you want to authenticate to with the Yubikey.

Using the SSH key

There are multiple ways to use the key; the most basic is to use it on the command line, using the -I flag to specify the PKCS11 library:

% ssh -I ${HOMEBREW_ROOT}/lib/opensc-pkcs11.so remotehost
Enter PIN for 'PIV_II (PIV Card Holder pin)':

Touch the Yubikey

Last login: ...

The PKCS11 library can also be specified in the SSH configuration (the real path is needed):

Host remotehost
  PKCS11Provider /opt/boxen/homebrew/lib/opensc-pkcs11.so

ssh will then use that library for remotehost, but not others:

% ssh -v remotehost
OpenSSH_6.9p1, LibreSSL 2.1.8
debug1: Reading configuration data /Users/alice/.ssh/config
debug1: /Users/alice/.ssh/config line 87: Applying options for remotehost
debug1: /Users/alice/.ssh/config line 228: Applying options for *
...
debug1: Connection established.
debug1: manufacturerID <OpenSC (www.opensc-project.org)> cryptokiVersion 2.20 libraryDescription <Smart card PKCS#11 API> libraryVersion 0.0
debug1: label <PIV_II (PIV Card Holder pin)> manufacturerID <piv_II> model <PKCS#15 emulate> serial <1c532bc57e664e5> flags 0x40d
debug1: have 1 keys
...
debug1: Next authentication method: publickey
debug1: Offering RSA public key: /opt/boxen/homebrew/lib/opensc-pkcs11.so
debug1: Server accepts key: pkalg ssh-rsa blen 279
Enter PIN for 'PIV_II (PIV Card Holder pin)':

Enter PIN, then touch the Yubikey

debug1: Authentication succeeded (publickey).
...
Last login: ...

These mechanisms all require the PIN and a touch for every connection. With the SSH Agent, and careful use of ~/.ssh/config settings, it can be changed to only requiring the PIN once after the Yubikey is inserted, then only touch is required for each connection.

First, clean up any PKCS11Provider entries in ~/.ssh/config, then add the PKCS11 library to SSH Agent:

% ssh-add -s ${HOMEBREW_ROOT}/lib/opensc-pkcs11.so
Enter passphrase for PKCS#11:
Card added: /opt/boxen/homebrew/lib/opensc-pkcs11.so
% ssh-add -l
2048 SHA256:... /opt/boxen/homebrew/lib/opensc-pkcs11.so (RSA)

Now just SSH to any account that is configured to allow the Yubikey SSH key:

% ssh -v remotehost
OpenSSH_6.9p1, LibreSSL 2.1.8
debug1: Reading configuration data /Users/alice/.ssh/config
debug1: /Users/alice/.ssh/config line 87: Applying options for remotehost
debug1: /Users/alice/.ssh/config line 228: Applying options for *
debug1: Reading configuration data /etc/ssh/ssh_config
...
debug1: Next authentication method: publickey
debug1: Offering RSA public key: /opt/boxen/homebrew/lib/opensc-pkcs11.so
debug1: Server accepts key: pkalg ssh-rsa blen 279

Touch the Yubikey

debug1: Authentication succeeded (publickey).
...
Last login: ...

The only complication with this method is that if you remove the Yubikey then re-insert it, you'll need to manually unload then load the PKCS11 library (and type in the PIN again), otherwise it will silently fail to use the Yubikey and proceed to try other keys, then any other mechanisms supported by the server.

To unload and re-load the library:

% ssh-add -e ${HOMEBREW_ROOT}/lib/opensc-pkcs11.so
Card removed: /opt/boxen/homebrew/lib/opensc-pkcs11.so
% ssh-add -s ${HOMEBREW_ROOT}/lib/opensc-pkcs11.so
Enter passphrase for PKCS#11:
Card added: /opt/boxen/homebrew/lib/opensc-pkcs11.so