UCAN secure key management in the browser - Prose Edition

Presentation given on Nov 26th, 2020 by @benjaminbollen

warning: this is a long, but mostly self-contained post


Prologue.

This post is a first technical update on the Filecoin grant project to build a backup app that runs natively in the browser on the Fission Web Native Filesystem.
The first phase of the grant is focused on solution research, as such here we’ll break down the underlying problem and outline initial research towards a proposed solution.
To interact with Filecoin, just like with Ethereum or Bitcoin, we have to sign messages (or transactions). Filecoin is a storage marketplace, and the underlying workings are intricate, but for our purposes it suffices to consider a user can store data on the network by making a storage deal with a miner who provides (proof of) the storage.

What makes a wallet.

More than a decade since the start of cryptocurrencies, a legion of cryptowallets have been built, of all shapes and sizes. Roughly four ways of securely (non-custodially) storing private keys have so far emerged:

  • desktop apps/browser plugins,
  • mobile app wallets (using secure elements),
  • hardware wallets, and
  • paper wallets/cold storage.

Most notably, to date there are no browser-based wallets that run natively in the browser, without a plugin or additional device (known to the authors). This is actually imprecise, the concept of the “burner wallet” has been pioneered, where for some applications, the ease of onboarding new users outweighs security and other considerations.
In most burner wallets the private keys are stored in the local storage provided by the browser (possibly encrypted with a user password). However, this exposes the private key to malicious javascript code on the webpage, or to malicious browser plugins who can read them from memory or local storage.

Clearly, secure storage of the private keys is a critical dimension to evaluate a wallet solution on. A second important, and easily overlooked, dimension is the permission system for the wallet to be authorised to sign transactions for the user.

As an example for a hardware wallet, the permission system requires the user to physically interact with the device, which is great for high value transactions; it clearly involves the users consent.
When we look at DApps on Ethereum, a user often has to sign multiple transactions, to advance through a user flow. When they connect to Web3 over Metamask or WalletConnect this interrupts the user experience for every signing confirmation. This is acceptable for the DeFi apps as they handle important financial decisions for a select audience.
This will quickly become a roadblock though for the majority of internet users as we try to expand the capability of web3 apps into everyday life. Imagine backing up a video album of a holiday trip to Filecoin for archiving, and needing to confirm with Metamask - it’s too complicated for most people.

As a result, when desiging a wallet, we need more than secure storage of keys, we also need a clear and transparant way to ask permission from the user, while staying out of the way of the user to complete their job in the app.

Part 1. Building a web wallet on WNFS and UCANs.

Before we can start, it is worthwhile to refresh two approaches to securely managing permissions in computer systems. In a first one, an object maintains a list of all the users of who and what actions they are permitted to perform on the object. This is a common security model in databases, and in particular in blockchains, where ownership of the private key defines who the user is, and what they can do (eg. spend their balance, execute scripts, call on contracts). This model is called Access-Control-List (ACL), aptly named as the object centrally keeps a list of all the users and what their permissions to manipulate it are.

Orthogonal to ACL, is a second security model: Object Capability model (OCAP). Here the object can issue a cryptoghraphic token that delegates permissions to a user. The user can present this token to perform actions on the object, or can further delegate permissions to different users with equal or less permissions than the original token. A common use of the OCAP model are JSON Web Tokens (JWT) and OAuth authentication on the web.

Web Crypto API

When we want to build a native web wallet, we will have to bring these two disjoint security models together: ACL with private keys as a way to securely interact with blockchains, and OCAP where cryptographic bearer tokens securely delegate permissions between actors on the web.

By 2017 the W3C released a recommendation for a Web Crypto API to perform basic cryptographic operations in the browser. By now in all major browsers the Web Crypto API has replaced the old, often ill-defined window.crypto API with window.crypto.subtle API.

The Web Crypto API allows to generate keys as non-exportable in the browser, and thereby shields the raw key data from malicious javascript code (or browser plugins). Of course malicious code can still request the API to sign specific messages, but an attack has to happen live, on the user-device as the key cannot be copied out.

However, all is not solved for us. The Web Crypto API supports RSA (encryption and signing), AES (symmetric encryption), and three Elliptic Curve Digital Signing Algorithms (ECDSA) - but three NIST curves P-256, P-384, and P-512 (and only on Chrome). These algorithms are heavily used in the OCAP model for security on the web; they don’t match the cryptographic curves that most blockchains use.

Two important blockchain curves of interest are secp256k1 (adoption driven by Bitcoin addresses, later Ethereum and a supported Actor in Filecoin), and more recently BLS12-381 (natively supported in Filecoin, and Ethereum1 after Berlin hard fork).

As a result, we cannot directly sign Filecoin messages with the Web Crypto API. We can, however, securely sign bearer tokens to delegate rights, and below we explain how we intend to use this capability to securely sign BLS12-381 signatures, natively in the browser.

First a word on the Web Native Filesystem (WNFS)

Fission has built an end-to-end encrypted filesystem on IPFS that runs natively in the browser, as such called the Web Native FileSystem (WNFS). There are many internal workings of WNFS that are worth their own blog post, and good starting point for those eager is whitepaper.fission.codes. Each users’ WNFS also has a public directory, but we won’t need that for our web wallet.

For our purposes, let’s briefly highlight some workings. WNFS is based on CrypTree, an encrypted file system directory tree where each file and directory is encrypted with its own AES key, and access to a parent directory gives access to all its children.
When Alice creates a new filesystem in the browser on https://auth.fission.codes, she generates a non-exportable RSA signing key did:key:zAlice, and an exportable symmetric AES key which encrypts the root of her filesystem. These keys are stored in the browser’s local storage, but shielded by the Web Crypto API.

When Alice visits the Filecoin backup app - which is served p2p over js-ipfs - and wants to authenicate, the app does not require a backend. Instead the app generates a did:key:zAliceInFilecoinApp and redirects her to auth.fission.codes, where Alice is presented with a prompt to allow the app to access the directory /private/Apps/fission/FilecoinBackup for some time.

Example of the Quotes app, at quotes.fission.app

Under the hood, the authoriZation page, decrypts the AES key for the directory /private/Apps/fission/FilecoinBackup and encrypts it with did:key:zAliceInFilecoinApp to pass back when returning Alice to the application. This gives the app read access to this directory and all its children.
In addition the authorization page issues a bearer token that delegates append-only access to this directory to did:key:zAliceInFilecoinApp.

Whenever the app writes to the filesystem, it recalculates the root of a Modifed Merkle Patricia Trie (MMPT) (which covers the whole WNFS, and doesn’t expose metadata or file paths, details in the whitepaper). The app can publish these changes over the Fission API to update Alice’s DNSLink (alice.fission.name) of her updated filesystem. The app can access this API to update the DNSLink, because it has a bearer token delegating append-rights for this subdirectory from did:key:zAlice to did:key:zAliceInFilecoinApp for some time duration.

Storing private keys on WNFS.

We’ve covered quite a few topics, to now embark at the task at hand. It is helpful though, to have elaborated for a moment on how the OCAP model is already applied within the Fission stack to give write access to the filesystem (updating the DNSLink under alice.fission.name) from an application, as we will need to apply an OCAP model for the web wallet.

The general protocol for granting these permissions is specified under UCAN v0.5.0, “User Controlled Authorization Network”. It formalises how to (recursively) delegate rights on resources from an issuer to an audience, where these rights can only ever be attentuated or narrowed in time-scope. (UCAN itself applies work by Google “Macaroons” to SPKI/SDSI certificates.) UCAN_SAM-1

What makes a good wallet, as discussed, is a permission system that is easy to understand, and fine-grained, all the while not obstructing the user. We have the ingredients to accomplish this now.

The private keys of Alice’s Filecoin backup app can be stored in her WNFS under a reserved private/Keychain system path for each app:

alice.fission.name/private/Keychain/{creator}/{app}/{publicKey}.json

and the Backup app needs to get explicit authorization from auth.fission.codes to use the signing key for a small time window (eg 10 min), and with a maximum spending limit during this session and possible restrictions (eg. the app can’t call changeOwner on a MultiSig Actor).

A careful reader may raise two objections at this point:

  1. such capability restictions work in an OCAP security model, but a blockchain has an ACL security model; how would any such restrictions be enforced? Surely the app can sign any transaction it pleases once it has read access to the private key.
  2. it is good that these private keys are encrypted with AES-256, and are portable across devices over WNFS, but Web Crypto does not support these curves, therefore the raw key data still has to be decrypted in memory of the js code; what security have we gained compared to burner wallets who store raw key data in local storage?

In the following section we will address both concerns. Before we go there, a quick word on account recovery.

Account recovery

By storing the private keys in the Web Native Filesystem, Alice now can “recover” all her keys, when she can recover her complete filesystem, once for all apps. Currently redundancy for the filesystem is encouraged by linking multiple devices to your account (eg. your laptop and your phone’s browser).

We understand that for different audiences, more recovery solutions are required. Because Fission is an open application platform, different recovery apps can be built (among those paperwallets, social recovery and crypto-hard server-aided recovery). Each app can direct users to suitable recovery apps, or progressively recommend options.

What’s not there, can’t be stolen.

As a quick recap, the RSA keys to securely issue UCAN permissions (and create identities did:key:z...) we can shield from the browser code using the Web Crypto API. Unfortunately, the elliptic curves needed to sign messages (ACL) for Filecoin (secp256k1, or BLS12-381) are not covered by the Web Crypto API specification and as such not supported by the leading browsers (Chrome, Firefox, Safari, Edge).

Therefore the best we can do is store a private key encrypted in WNFS, and decrypt it in memory to sign transactions natively in the browser. This exposes the private key to malicious code (XSS attacks, malicious browser plugins, …) which can copy the private key out and use it at any later time to steal funds, eg. when the account receives enough FIL above a treshold.

The solution to this conundrum lies in breaking the direct relationship between the address that has significance on the chain, and the private key that controls it. Or conversely, make the private key that is stored in WNFS by itself worthless.

For Filecoin (and other chains) which supports BLS12-381, the most elegant implementation is to default to using 2-of-2 BLS signatures. We’ll consider this in more detail in part 2. and an alternative implementation for Ethereum Muir Glacier, or chains which don’t support BLS12-381.

BLS12-381 is a particular elliptic curve with suitable properties for public key aggregation, and signature aggregation under the Boneh-Lynn-Shacham signature schema. In particular it has a “pairing function” which balances well the verification speed of aggregated signatures, without leaking the private keys. Good write-ups exist, with varying levels of math exposure. For interested readers this blog post is a good entry into the subject.

For now, we just need to know that (among other magic) the following is possible: we can combine any two BLS public keys without any setup phase, into a new public key - which after merge cannot be told apart from a “normal” BLS public key. However, the only way to validly sign a message for this merged public key, is to separately sign this message with the private keys of the public keys that merged to form the merged public key, and then merge both the individual signatures of that message into a single signature that will correctly verify the message for this merged public key - but the private key to this merged public key never exists!

This may sound complicated, but in a real sense it “as simple as” adding up the two public keys of Alice and Bob to form the 2-of-2 BLS public key

PK = PK_A + PK_B

and likewise to merge the signatures of the message, we add up the signatures of Alice and Bob - but note that this “addition” happens in a highly non-trivial space of points on the elliptic curve, and that to protect against the Rogue Key Attack we can’t simple add them, but have to make the function non-linear by introducing coefficients which depend on the public keys of Alice and Bob. (The referenced blog post above explains this in more detail.)

All good, how does this solve our problem though? For sure, we could use 2-of-2 BLS signatures to construct a 2FA device, and this would be secure: we can merge the public key from the browser app stored in WNFS, with a public key the user has stored in a mobile phone app, or on their laptop; and for each signature the user must confirm on the second device to actually sign for the transaction.
… this is a lot of work for what we already have: Metamask, WalletConnect already accomplish this permission flow, and security level with a much simpler solution: just store the secp256k1 private key in the more secure device. We seemingly have gained nothing. :exploding_head:

UCAN do secure signing in the browser

It would be cruel to have you, our reader, come all the way here, and leave you empty-handed.
What we haven’t touched on yet, is that every web3 DApp that runs in the browser still needs some service which accepts a signed transaction over http and pushes it into the mempool of a node on the network.

Some examples:

  • in Ethereum web3.js the provider (eg metamask) needs to push the signed transaction to the chain, or
  • Infura provides an http endpoint for many Ethereum DApps to connect to the Ethereum network
  • some projects have gone further and adopted the concept of meta-transactions (eg. EIP-1077, Gnosis SAFE, and many others) that get encapsulated in a relay-transaction by a relayer service, for example to enable gasless transactions for the user.

In short, there is no point to connect directly from the browser to the blockchain’s peer-2-peer network, so we need an interface between the http requests of the webpage, and the gossip protocol of a node.

And here we find the final resolution: we can bind the users’ public key in WNFS with a public key of this interface service into a 2-of-2 BLS public key. This way the interface service becomes a cosigner service. This cosigner service may receive a signed transaction by Alice, but will only sign the transaction as well if the request is accompagnied with a valid UCAN that delegates to the cosigner the right to sign this transaction.

But what if…

Two important concerns need to be raised now though.

Unavailable cosigners

The cosigner with which Alice paired her key, may become unavailable, or worse has the ability to censor Alice’s transaction by refusing to cosign her transaction, despite a valid UCAN for that transaction. First it is worth noting, that in practice this is equivalent to eg. Infura going down, however, the principle remains, and that is not unimportant because a cosigner could be pressured into refusing service to some account - effectively freezing Alice’s assets.

Therefore we must always initialise Alice’s address not as the 2-2 BLS public key, but rather deploy a 1-of-N MultiSig actor (or contract) for Alice as her interaction point on the chain. Independent cosigner services can advertise their public keys, and at minimal overhead Alice can pair her private keys with independent cosigner services, and add these 2-2 BLS public keys as owners in her MultiSig on deployment.

For advanced users, who may have critical data attached to their account, or are at higher risk of being targeted, they can add the public key of a hardware device (Ledger supports Filecoin already), as an owner to the MultiSig, effectively functioning as a master key / recovery key in case access to WNFS is lost.

Stolen UCANs

if an attacker can steal the private key from the applications’ browser window (half of the 2-of-2 BLS key), then surely the attacker can also steal the UCAN which authorizes the application to sign with this key?
Correct, but for each transaction the application must delegate a new UCAN specifically for this transaction to the cosigner. Because the Web Crypto API protects the RSA signing key that needs to sign off on new UCAN delegations for additional transactions, it follows that copying out the private key and the parent UCAN authorizing the application for this session is of no value, as no new valid UCANs can be signed outside of the browser. The attacker is therefore constrained to the active application session.
So an attacker can try to enter the active application and insert malicious code to send transactions within the constraints of the active authorization session, which may have a 10 min duration window, a maximum total spending limit over all transactions, and crucially exclude transactions directly to the MultiSig.
This way an attacker cannot insert their own secp256k1 address, or remove ownership of Alice’s legitimate addresses from the 1-of-N MultiSig. This prevents the race between Alice and the attacker that would otherwise occur to replace the ownership keys of the account on theft of keys (and UCAN).

A final remark, the signing session UCAN the application received was itself delegated from the root key did:key:zAlice kept by Web Crypto at auth.fission.codes. For unusually critical use-cases this root authorization can be done from the Fission CLI (possible later feature) or moved to any secure origin point outside of the browser (desktop app, mobile phone, or even start from an air-gapped computer with a cascade of attentuated signing permissions to increasingly warm devices.
For the vast majority of users and use-cases though, we argue the security of the auth.fission.codes page (which is itself served as a static page, content-addressed over IPFS), is highly secure.

A diagram.

Summary.

We propose a way to combine two different security models (OCAP on web and ACL on blockchains) to build a native web wallet without plugins required. To achieve this we leverage WNFS and UCANs.

We warmly welcome critical review and questions. From this early proposal we will in parallel both attempt to further formalise the specification, and implement a proof-of-concept.

3 Likes