0x05. YubiKey + SSH/Git (macOS)
0x00. Intro
There have already been two parts about YubiKey + SSH: Windows (0x00) and Linux (0x01). Then a Mac arrived, and I needed to hook up the same YubiKey that already handles SSH on Linux and Windows. The one hard rule: do not touch the key itself. No new keys, no resets, no PIN changes.
Previous parts: 0x00. YubiKey + SSH/Git (Windows) and 0x01. YubiKey + SSH/Git (Linux).
Spoiler: it works, and the actual setup takes about five minutes. But along the way I stepped on a rake that deserves its own section, because it cost me more time than everything else combined.
0x01. The rake: FIDO2 ≠ PIV
The first search result for “yubikey ssh mac” is Yubico’s official guide “Securing SSH with FIDO2”. It tells you to run ssh-keygen -t ed25519-sk, i.e. generate a new key. I did not want a new one. I already have one. So I tried to “download” my existing key from the YubiKey:
└─$ ssh-keygen -K
Enter PIN for authenticator:
You may need to touch your authenticator to authorize key download.
Provider "internal" returned failure -1
Unable to load resident keys: invalid format
This is the point to stop and remember that a YubiKey is several independent applications in one shell:
- FIDO2 is what you use to sign in to Google or GitHub in the browser. OpenSSH 8.2+ can use it too (key types
ed25519-sk/ecdsa-sk). Such a key can be made “resident”, and thenssh-keygen -Kpulls it off the token onto a new machine. - PIV is a smart card. The private key lives in a slot (9a, 9c, …) and SSH talks to it through a PKCS#11 library.
- OpenPGP is yet another option, via gpg-agent. Not our case.
In parts 0x00 and 0x01 we set up PIV (slot 9c, libykcs11). So the FIDO2 guide and ssh-keygen -K have nothing to do with our key, and “invalid format” is just OpenSSH’s clumsy way of saying “there are no resident FIDO2 keys here”. The command changes nothing on the key, it only reads. Still, do not guess the PIN repeatedly: eight wrong attempts lock the FIDO2 application.
Second important point: with PIV there is nothing to copy from Linux. There is no private key file. The key exists only inside the YubiKey and cannot be exported. Each machine just needs the PKCS#11 library to reach it.
0x02. Tools
On Linux we installed ykcs11, yubico-piv-tool, yubikey-manager and pcscd. macOS is simpler:
- No
pcscdneeded. Smart card support (CryptoTokenKit) ships with the OS. - Everything else is in Homebrew.
└─$ brew install ykman yubico-piv-tool
yubico-piv-tool brings along libykcs11.dylib, which is the same libykcs11.so from the Linux part, built for macOS:
└─$ ls -la /opt/homebrew/lib/libykcs11.dylib
lrwxr-xr-x 1 user admin 51 Oct 14 09:35 /opt/homebrew/lib/libykcs11.dylib -> ../Cellar/yubico-piv-tool/2.7.3/lib/libykcs11.dylib
On Intel Macs Homebrew lives in /usr/local, so the path is /usr/local/lib/libykcs11.dylib. Substitute yours below.
Check that the Mac sees the key:
└─$ ykman list
YubiKey 5C NFC (5.4.3) [OTP+FIDO+CCID] Serial: XXXXXXXX
└─$ ykman piv info
0x03. OpenSSH
Read the public key exactly as on Linux, just with a different library path. No PIN needed for this:
└─$ ssh-keygen -D /opt/homebrew/lib/libykcs11.dylib -e
ecdsa-sha2-nistp256 AAAA... Public key for PIV Authentication
ssh-rsa AAAA... Public key for Digital Signature
ssh-rsa AAAA... Public key for PIV Attestation
“Digital Signature” is slot 9c, the one we generated in 0x01. This public key is already in authorized_keys on the servers and in GitHub, so nothing changes on the server side.
One-off login:
└─$ ssh -I /opt/homebrew/lib/libykcs11.dylib user@host
To avoid typing -I every time, in the Linux part we appended PKCS11Provider to /etc/ssh/ssh_config. On macOS I recommend the user config instead, since the system one gets overwritten by OS updates now and then:
└─$ cat >> ~/.ssh/config <<'EOF'
Host *
PKCS11Provider /opt/homebrew/lib/libykcs11.dylib
EOF
└─$ chmod 600 ~/.ssh/config
Verify that ssh picked it up:
└─$ ssh -G github.com | grep -i pkcs11
pkcs11provider /opt/homebrew/lib/libykcs11.dylib
And the actual test:
└─$ ssh -T git@github.com
Enter PIN for 'YubiKey PIV #XXXXXXXX':
Hi user! You've successfully authenticated, but GitHub does not provide shell access.
Git works automatically because it goes through the same ssh.
0x04. The ssh-agent caveat
The natural next wish is to add the key to ssh-agent with ssh-add -s /opt/homebrew/lib/libykcs11.dylib, so the PIN is asked once per session. On Apple Silicon this hits:
Could not add card "/opt/homebrew/lib/libykcs11.dylib": agent refused operation
The reason: ssh-agent has an allowlist of paths for PKCS#11 libraries, and the default is /usr/lib*/*,/usr/local/lib*/*. /opt/homebrew is not in it (see man ssh-agent, option -P).
Options:
- Do nothing. We picked slot 9c precisely so the PIN is requested on every connection.
PKCS11Providerin the config does exactly that. No agent needed. - If you really want the agent, run your own
ssh-agent -P '/opt/homebrew/lib/*', or symlink the library into/usr/local/lib/, which is on the allowlist.
I went with option 1. Paranoia is what it is.
0x05. Summary
| Linux | macOS | |
|---|---|---|
| Library | /usr/lib/x86_64-linux-gnu/libykcs11.so | /opt/homebrew/lib/libykcs11.dylib |
| Packages | apt install ykcs11 yubico-piv-tool yubikey-manager pcscd | brew install ykman yubico-piv-tool |
| Smart card daemon | pcscd | built in |
| Config | /etc/ssh/ssh_config | ~/.ssh/config |
Nothing changed on the YubiKey. Same key in slot 9c, same PIN, same authorized_keys.
Refs
- 0x00. YubiKey + SSH/Git (Windows)
- 0x01. YubiKey + SSH/Git (Linux)
- https://developers.yubico.com/PIV/Guides/SSH_with_PIV_and_PKCS11.html
- https://developers.yubico.com/SSH/Securing_SSH_with_FIDO2.html (FIDO2, not our case, but useful to know the difference)
- https://developers.yubico.com/yubico-piv-tool/
- https://formulae.brew.sh/formula/yubico-piv-tool
- https://docs.yubico.com/software/yubikey/tools/ykman/Install_ykman.html#macos
man ssh-agent, option-P