Quick and dirty developer guide to U2F

I recently had the chance to implement U2F authentication in a small side project. During the process, I learned a lot about both the protocol and the current state of U2F documentation, or lack thereof. I hope that if you’re in the same place I was, this will get you up to speed more quickly than the other guides available.

If you learn better with hands on examples (and you’ve got a U2F key), head over to my U2F Demo and play around with it there.

A quick overview of U2F

U2F can be boiled down to the following overview:

  • The end user possesses a U2F hardware token
  • The browser issues a challenge for the hardware token to sign
  • The user presses a button (proving presence) and the hardware token signs the challenge
  • The signed data is sent back the script calling it
  • The script sends the signed data back to the server where it it verified against the public key on file for the user.

As is the case with anything involving cryptography, there’s a hell of a lot of other complexity we don’t usually see. If we really want to understand the benefits of U2F, and better understand how to implement it, we’ll need to dive in a bit deeper.

The Client Side

U2F isn’t a first-class citizen in the world of browsers and Javascript. To interact with the hardware key, a browser must have additional software installed. In the case of Chrome, users formerly had to install the U2F extension themselves, but today it’s included in all versions by default.

Interfacing with this extension is not trivial (Lots of async message passing, and you’ll need to keep track of which request matches to which response). The most accessible route is to use the U2F polyfill that Google provides. Github extracted this into a separate project and created an NPM module for easier developer consumption. I suggest using this polyfill rather than talking directly to the extension.

Once we’ve got the client side details sorted, our next step is to understand the two modes of operation — registration and authentication.

Registration Mode

The first time we use U2F, we’ll need to ask the user to provide their key for registration. We pass an ‘App ID’ along with the challenge to the ‘register’ function of the U2F extension. The App ID, in the case of browser-based U2F, is the current domain of the page issuing the call for registration.

var registerRequest = {
challenge: 'RegisterChallenge',
version: 'U2F_V2'
}
u2f.register('https://mdp.github.io', [registerRequest], [],
(response) => {
console.log(response)
console.log("Send 'response' to the server")
}
)

When the user presses the button on their token, the callback above will return with the following information:

  • A key handle
  • The public key
  • The challenge, signed with the public key
  • Some other extraneous stuff that we don’t need to worry about (Serial, Public certificate, manufacturer)

You’ll want to send the ‘key handle’ and the ‘public key’ to the server to attach to the authenticating users record.

I’ll explain key handles in more detail later, but for now, just know the following:

  • Key handles are generated by the U2F device for each call of ‘register’. You can generate as many as you like, there’s no limit.
  • Key handles are tied to the Application ID, in this case, the domain name. This means that if someone tries to use that key handle on another site, the U2F device will refuse to sign the challenge.

Authentication Mode

From now on, when a user attempts to log into your system, we’ll need to look up their ‘Key Handle’ and pass it along to the client to then authenticate with the U2F device. In the following example, we pass both the ‘Key Handle’ and the ‘Challenge’ to the U2F device for signing.

var registeredKey = {
keyHandle: 'UycRIFmw7L1qG194G8bhhLSdPNqLar6Wr6HgrzJPKROC90mKq_u9Jl0K-ode0EdWc04bAA1QfAopM8Met3us8w',
version: 'U2F_V2'
}
u2f.sign(https://mdp.github.io,
'SigningChallenge', [registeredKey], [],
(response) => {
console.log("Send 'response' to the server")
}
)

The user then pushes the button on the U2F device, which will in turn sign the challenge and AppId with the public key generated from the KeyHandle. You’ll get back the following:

  • The key handle used to sign the challenge (In case you passed multiple key handles)
  • The signature data which includes the signature as well as the counter (a global count of all signatures issued by the device)

We’ll send this signed response back up the to the server, where we validate it with the public key on record for the user. If it’s correctly signed, then the user has proven to us that they are in possession of the token they initially registered with.


Addendum: The not so small details

While the above should give you a good overview of how U2F works, there’s plenty of smaller details that I glossed over. The rest of this post tries to answer any questions you might have if you’re still curious.

Key Handles and Public Keys

As explained, Key Handles are tied cryptographically to the site that registered them. But why not just use the public key directly?

The problem with this method is that the U2F device would need to remember which public key belongs to which site. U2F keys would then need to include memory, memory which could be exhausted over time. With Key Handles, the U2F key can encode a hash of the AppID into the handle along with some entropy. Then it can use a function to derive a private key from the key handle by combining it with its never revealed, secret key encoded on the device. This way we can have unlimited keys for any site, and upon authentication, the device simply needs to take the key handle, ensure that it belongs to the site, generate the private key and sign the challenge.

And unlimited, site-specific, keys are excellent for privacy. You can’t link users across sites, and they can even share keys (Ex. Your significant other could use the same key and register it with the site. The key handle would be new and entirely different for that user, providing no easy way to match the users*)

*One problem here, there’s a global counter included on every U2F device that ticks up with each use. U2F protocol provides a counter along with the signature data it returns. With enough logins you could compare the counters of two users on your site and see if they sync up (Bob signs in #433, Alice later signs in #434, Bob signs in with #435 and so on. With enough data, we could infer a connection)

Key Handle Generation

Key generation is up to the vendor to implement. Here’s how Yubico does it, which is probably the best implementation and the best documented implementation.

Challenges

U2F keys don’t merely sign the Challenge data. They first take the challenge data and add a few other items to it before signing. Things like the Application ID and counter, which adds to the security (The Application ID is additional protection from a Man In The Middle attack, and the counter is extra protection against a replay attack)

  • The application parameter [32 bytes] from the authentication request message.
  • The user presence byte [1 byte].
  • The counter [4 bytes].
  • The challenge parameter [32 bytes] from the authentication request message.

Multiple Key Handles

What if you want to accept multiple U2F keys for a user? Github supports this, allowing you to register multiple U2F devices for your account. In this case, we would pass the signature function an array of the Key Handles on file for the user, and the U2F device used would pick its valid key handle from the list and return the result along with the key handle used.

Resources

The U2F spec: https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html

U2F Demo: https://mdp.github.io/u2fdemo