Chrome Key - An Extension Story
We will be tackling the skeleton of a Chrome Extension that is capable of detecting Webauthn requests, both for registering and logging in, how different pieces of the extension communicate with each other, and lay the foundation for implementing an authenticator.
We will be building a Chrome Extension that emulates a Hardware Authentication Device (HAD).
While exploring the Webauthn protocol I was trying to add it to a personal project of mine. Although I own a HAD, during development I wanted to be able to play around with the protocol without worrying too much about polluting the device's memory with rubbish key material, among other things. After searching for some sort of emulator or SDK, I couldn't find any, which brings us here... why not create an emulator that a developer or tester can use to interact with a page, without requiring immediate access to a HAD?
This is the first of a three-post series, the ultimate goal is for us to be able to register and authenticate with web applications that support Webauthn without requiring an actual device (except for your browser of course).
On this post we will be tackling the skeleton of a Chrome Extension that is capable of detecting Webauthn requests, both for registering and logging in, how different pieces of the extension communicate with each other, and lay the foundation for implementing an authenticator.
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.
As any good project, we should begin by identifying the problem we are trying to solve and design a solution for it.
Upon receiving a request from a web page for Webauthn credential creation or verification, the browser will request all eligible devices for a response. The extension will detect this request provide a way for the user to answer the web page request with it.
Because the extension emulates a HAD, it should require the user to pro-actively interact with it. Once that happens it will generate or fetch the necessary key material, produce a valid protocol response and return it to the web page that performed the request in the first place.
Most extensions have three main building blocks:
- Background script, where most magic happens
- Content script, which is capable of injecting code into a web page and extending its capabilities as well as communicate with the extension
- Popup, allowing the user to see and react to requests
The components described above are executed with varying sets of restrictions, and for a good reason.
- Background scripts are usually not persistent.
The only occasion to keep a background script persistently active is if the extension uses chrome.webRequest API to block or modify network requests.
- Content scripts work in an isolated world.
- Injected scripts do not have access to the
- Popups are only instantiated when the user clicks the extension icon.
For this project, the background script will be performing the heavy lifting. Each request for generating a new key or signing a challenge in order to log in, will be performed within it. So let's see how we can get messages to and from the different pieces.
Background & Content Scripts
Communication between the background and content scripts is simple, since both have access to
chrome.runtime, see "Message Passing". With this API available, synchronous and asynchronous communication between the two is trivial.
Content & Injected Scripts
We already noted that injected scripts do not have access to
chrome.runtime, yet they need to communicate with the content script in order to relay requests from the webapp to the background script.
The only thing these two scripts share is the
window object, so they can take advantage of
addEventListener to communicate.
Background & Popup Scripts
These two scripts also have access to
chrome.runtime. Popup scripts however are only active when the popup is open. This means that if your background script tries to send a message to a closed popup, it will never be delivered.
It is simpler to think of this problem as having two parts:
- Send a message from background to the popup and getting a response
- Send a message from popup to background and getting a response
The only communication channel that would allow for an asynchronous consumption of a message in the first scenario is
background.ts will write to the local storage using a deterministic key and the popup will try to fetch this information "on open".
For the second scenario we can fortunately rely on
We chose to use Page Actions instead of Browser Actions so you can authenticate simultaneously on multiple sites without mixing them, and since both scripts can be made aware of the tab ID that bootstrapped the request, we can use it to infer a deterministic key.
Extending the Browser
With communication handled, we now move to extending the browser and augment the navigator's CredentialsContainer, allowing our extension to participate in authentication requests.
A user may have multiple authenticators registered, so the orderly thing to do is to add ours to the list. This ultimately leaves it up to the user to decide which device to use, either a real HAD (if any) or our extension.
And just like that, the page you are accessing has an augmented
navigator.credentials which relays the Webauthn requests to the extension using the communication mechanisms we looked at earlier.
Request User Action
This extension aims at testing user presence to sign any requests made.
Popups cannot be opened programmatically so one could argue that the simple fact that the user opened the popup is proof enough of "user presence". However, in addition to this, we will also request a 4-digit pin. The codepen below served as a big source of inspiration.
All keys will be managed by the extension and will be shared using sync storage, allowing them to be used across browsers.
Confidential user information should not be stored! The storage area isn't encrypted.
Storing plain private keys on synced storage is a very bad idea! To somewhat circumvent this we will use the provided pin to encrypt the private key using AES-256 GCM, serving as a passphrase to the algorithm.
Some of you may be grimacing at this suggestion, and rightly so. Any storage mechanism that requires private keys to be loaded into user land and to be manipulated by user code opens a slew of attack vectors. We will discuss these on the last part of this series, and remember, do not use it in production!
We now have a good starting point for plugging in our implementation of the Webauthn protocol. Key creation and sign requests are relayed to the interested parties and user presence is verified.
If you have any comments or suggestions please send me a tweet @joaomppeixoto.
On the next post of the series we will implement the relevant bits of the protocol such as key generation, encoding responses with the expected payloads and explain different types of attestation.