Chrome Key - A Standards Story
Building a WebAuthn protocol response for a credentials creation request, relying on standards such as the Web Cryptography API, CBOR, JSON Web Key among others.
On the second post of this series we will construct a valid WebAuthn protocol response for a credentials creation request, relying on standards such as the Web Cryptography API, Concise Binary Object Representation, JSON Web Key among others.
On the previous post we set out to build a Chrome Extension that emulates a Hardware Authentication Device (HAD) and created the foundation for the whole project.
Today we will breakdown the different parts of the protocol and the role they play. We will work on each one of them separately so we grasp their purpose and guarantees and finalize with the proper response.
I cannot emphasize enough that this project by no means replaces a Hardware Authentication Device. The use of this extension is aimed exclusively at development, testing and debugging. If you use it in a production environment, do so at your own risk. We will elaborate on the guarantees (and lack of) that this project provides on the last post of the series.
Building the Attestation Object
Webauthn is a challenge-response protocol which can take advantage of asymmetric key pairs to replace password-based authentication.
What does that actually entail? How does the protocol solve the problem of proving that you are who you say you are?
There are two major parts to the challenge portion of the protocol:
- The challenge: self explanatory at first, but it is crucial to understand that this challenge is generated by the web application, i.e. the Relying Party. This gives confidence to the server that our response is actually aimed at them and not some other web application. This protects us against some attack vectors such as Replay Attacks.
- The Relying Party ID: This is provided by the server too and enforced by the browser. This provides some guarantees to the client that it is authenticating against the right web application and not some malicious intermediary, i.e. Man-in-the-middle attacks.
Now that we know the basics of the challenge, it is our objective to produce a valid response, named "Attestation Object".
The attestation object is divided in three sections:
- Authenticator Data: This holds all the information that we, the authenticator, want to share with the relying party.
- Format: Will always be "packed".
- Attestation Statement: Signature over the challenge (plus a few other bits), and the certificate of the HAD if applicable.
We will start with the last portion of the protocol, the attestation statement.
The purpose of the attestation statement is to cryptographically prove that the keys originated from our authenticator and are tied with the challenge provided by the Relying Party.
To achieve this goal, an object named "Client Data" is created and signed by the authenticator private key. This will prove before the Relying Party that the public key we are sharing with them is the counterpart of the private key that we hold and share with no one. This form of cryptography allows anyone, not only the original recipient, to verify independently that the information was not tampered with.
clientData contents are the only items that are cryptographically secured according to the specification.
Authenticator Data (Create Credentials)
The authenticator data provides the requesting web application with information about the credentials that were created, for whom, and with which flags.
The purpose of this portion is to inform the Relying Party of the configurations used for the key generation. From the previous section we know that these are not cryptographically protected, so one could argue "what if an attacker tampers with these values"?
AAGUID(Authenticator Attestation GUID) as an example, which follows the format specified under RFC4122.
It is left at the discretion of the server to limit authorized devices to fall within a fixed set of AAGUIDs. An attacker that spoofs this ID may pose as an authorized device, but will not gain access to key material hosted in such a device (private keys never leave the device).
Some authenticators have an AAGUID, which is a 128-bit identifier that indicates the type (e.g. make and model) of the authenticator. The AAGUID must be chosen by the manufacturer to be identical across all substantially identical authenticators made by that manufacturer, and different (with probability 1-2-128 or greater) from the AAGUIDs of all other types of authenticators.
Chrome Key's AAGUIDs was generated with
Flags are used to assess if the request was satisfied appropriately given the parameters passed by the Relying Party. A HAD that does not respect these will not be considered compliant. And again, no access is granted to key material by tampering with it.
It's left as an exercise to the reader to understand the potential impact of tampering with the different options. For now, enough talk -- let's build it!
Webauthn's premise is rather simple, really: instead of saving a password with a webpage you save a public key.
A big problem with passwords is that we humans are not very good at it. Even with techniques like hashing and salting, given enough time an attacker may find low hanging fruit and crack some of them.
Enter asymmetric keys. If all we share with a site is a public key (which by definition is public), then all of a sudden compromising your account is a lot harder. You don't have to remember the private key bits (nor could you -- the HAD will help you here), and the computational cost of breaking the public key will be much greater. On the last post of the series we will focus more on attack costs, but over simplifying a little, the "scale" of an attack refers to how much value you can potentially get from a system given the cost of breaching it, both in terms of time and money. In this case, it's cheap(er) to compromise a database with all the passwords in it than to compromise each individual's HAD!
Generally, native or builtin implementations are preferable wherever possible. They tend to be more reliable, battle tested and performant.
We will have to generate key pairs for sure, in order to satisfy the protocol. Maybe unbeknownst to some developers, most browsers already implement the standard Web Cryptography API, a W3C recommendation. Fair warning about it...
This API provides a number of low-level cryptographic primitives. It's very easy to misuse them, and the pitfalls involved can be very subtle.
We already used some of its APIs on code samples above. Within the context of asymmetric keys, the first of the primitives we will be using is
crypto.subtle.generateKey, which can be used like so.
On the previous post we mentioned that we will be storing the key material in sync storage, therefore we must allow the key to be extractable. Different browsers support different key algorithms, but we will focus on Chromium's since we are creating a Chrome Extension, and export it in PKCS #8 format (RFC5208) for storing.
A Relying Party is a web application that requests Webauthn credentials. We should use a separate key for each one. This is the analogous principle behind not using the same password everywhere.
Although Web Cryptography API and Webauthn are complementary, their representations require some translation logic.
Take Webauthn for example, the protocol uses Concise Binary Object Representation (CBOR) to encode its messages, and CBOR Object Signing and Encryption (COSE) to encode keys.
On the example above we generated an ECDSA key with P-256 curve. The equivalent in the COSE format is the algorithm
-7. RSASSA-PSS with SHA-256 for example is
First let's get the private key through the Web Cryptography API in a more suitable format such as JSON Web Key (JWK) format.
In JWK the X and Y portions are Base64URL encoded, which is just the right type for COSE encoding, we just need to convert it to a byte array.
And just like that, we have all the pieces needed to encode the authenticator's public key and produce the final missing piece needed.
Putting it all together
We can finally reply to the Webauthn credential creation request!
Through the use of existing standards and a few libraries we were able to create a valid Webauthn response to a key creation request. We looked at the different parts of it and the different roles those parts play in the protocol.
If you have any comments or suggestions please send me a tweet @joaomppeixoto.
On the next post we will deep dive into some of the cryptographic guarantees associated with this protocol and the lack of guarantees of extension as well as some common pitfalls when it comes to cryptography.
Guess what, we will be brute force attacking out extension too!
Final teaser, to commemorate the end of the series we will release Chrome Key to the world.
I want to thank Krypton for open sourcing their work which served as a huge source of inspiration for this project and for giving me permission to use portions of it.