U2F Key Support in OpenSSH
Nick Mooney November 1st, 2019 (Last Updated: November 1st, 2019)00. Introduction
This morning, Damien Miller announced experimental U2F/FIDO support for OpenSSH. This functionality lowers the barrier to entry for users that want hardware-backed SSH keypairs.
Organizations are already using YubiKeys for SSH access, so what’s different about this? Let’s look at how this functionality is currently implemented and what’s different about the new, native SSH support.
01. How it works right now
YubiKeys support the PIV card interface. This allows us to use YubiKeys like smartcards "through common interfaces like PKCS#11".
OpenSSH has had support for PKCS#11 keys for some time, and Yubico's documentation explains how to use YubiKeys for SSH access.
What's the catch?
The current state of YubiKey / SSH compatibility requires this PIV card support, which is not part of the WebAuthn/FIDO standards -- it's a totally separate interface that Yubico has decided to include.
The PIV card interface is only available on the full-featured YubiKeys, not the cheaper "Security Key" product, nor most of the other commercial security keys. If you'd like to use a cheap Feitian key for SSH authentication, you've been out of luck until now.
02. A reminder about U2F keys
The U2F specification allows the creation of EC keys on the NIST P-256 curve, and assertions via ECDSA signatures with those keys. These are the basic building blocks of a U2F interaction. WebAuthn/FIDO2 expanded on the possibilities, but U2F and its CTAP1 transport are a functional subset of FIDO2.
OpenSSH only needs the functionality provided by U2F, so they decided to integrate U2F as a key type. That said, this should work with any FIDO1 or FIDO2 compatible security key.
03. The protocol
The U2F protocol as it interacts with OpenSSH is defined here. OpenSSH has had support for ECDSA P-256 keys for some time, but the specifics of the U2F protocol require changes to the signed payload, which necessitates the new key type.
The key format is not too surprising and is detailed well in the protocol document, so I won't rehash it here. The interesting bit to me is how they decided to support the hardware side of things. There are many different transports through which you can talk to a U2F token (USB, bluetooth, etc.), and the implementors of this feature didn't want to include transport-specific code in OpenSSH, so they instead defined a middleware API and built an implementation of this middleware around Yubico's libfido2
.
OpenSSH security key middleware
The full middlware definition is as follows:
/* Flags */ #define SSH_SK_USER_PRESENCE_REQD 0x01struct sk_enroll_response { uint8_t *public_key; size_t public_key_len; uint8_t *key_handle; size_t key_handle_len; uint8_t *signature; size_t signature_len; uint8_t *attestation_cert; size_t attestation_cert_len; };
struct sk_sign_response { uint8_t flags; uint32_t counter; uint8_t *sig_r; size_t sig_r_len; uint8_t *sig_s; size_t sig_s_len; };
/* Return the version of the middleware API */ uint32_t sk_api_version(void);
/* Enroll a U2F key (private key generation) */ int sk_enroll(const uint8_t *challenge, size_t challenge_len, const char *application, uint8_t flags, struct sk_enroll_response **enroll_response);
/* Sign a challenge */ int sk_sign(const uint8_t *message, size_t message_len, const char *application, const uint8_t *key_handle, size_t key_handle_len, uint8_t flags, struct sk_sign_response **sign_response);
An implementor of this middleware needs only to implement sk_enroll
and sk_sign
, and then provide a path to the middleware library, which will be dynamically loaded:
$ SSH_SK_PROVIDER=/path/to/libsk-libfido2.so
$ export SSH_SK_PROVIDER
$ ssh-keygen -t ecdsa-sk
The first implementation of this middleware lives in Yubico's libfido2
source tree, in sk-libfido2.c
. This implementation is very readable, and should give you a good idea of how libfido2
operations are massaged into OpenSSH-compatible structures.
Locally stored information
The private keys are stored in hardware, so what ends up in your id_ecdsa_sk
?
Your local "private key" stores the U2F key handle associated with the credential. In most security key implementations, this key handle is actually a wrapped credential or a seed required to re-derive the same key material. You can further protect the key handle with a passphrase (just like you can protect any other type of SSH private key with a passphrase), but a key handle is traditionally treated as non-sensitive information.
Limitations
The libfido2
API is fundamentally synchronous: operations like fido_dev_get_assert
are blocking, so there is no way (as far as I know) to send assertion requests to multiple security keys. Furthermore, the U2F specification is designed such that tokens don't fail to produce an assertion until the user interacts with them -- in other words, if my SSH key is stored on my second YubiKey, my first YubiKey will still hang while blinking until I tap it, so there's no way (by design) to enumerate which credentials are stored on which keys.
The ideal flow would be asynchronous, causing all your security keys to blink at once. Chrome seems to do this currently, but libfido2
doesn't yet support this type of interaction. An addition to libfido2
adding a poll/select-style interface could enable this without too much overhead.
04. Try it yourself
Follow Damien's guide to build a version of OpenSSH with security key support and try it out!