MetaMask / metamask-extension

:globe_with_meridians: :electric_plug: The MetaMask browser extension enables browsing Ethereum blockchain enabled websites
https://metamask.io
Other
12.08k stars 4.93k forks source link

RFC: Multi Signer Types #2532

Open danfinlay opened 7 years ago

danfinlay commented 7 years ago

Not to be confused with "Multi Vault" or "Multi Keyring", two previous features I've promoted. This RFC takes the concept of generic signing to new heights.

Multi Sign-Type Proposal

Intended Audience

Maintainers and influencers of cryptographically-secured peer-to-peer protocols, especially those concerned with the user experience and thus real adoption of these secure protocols. Obviously thinking of Ethereum & blockchains here, but also other cool p2p crypto protocols like ssb, (@dominictarr) too.

Context

Today MetaMask is an Ethereum account manager and blockchain API provider, and it's made Ethereum useful to lay-people in ways it was never before.

The accounts it manages don't need to only be Ethereum-type keys, and with account abstraction, increasingly these key types will need to become more modular than they are today.

API

Here let's re-imagine the method web3.eth.getAccounts(cb). By including an options object, we give the web3 signer liberty to provide any of a variety of key types.

For these code examples, a hypothetical pseudo-web3 object will be used.

const accounts = await web3.requestAccount({
  type: 'ethereum' || 'ssb' || 'rsa2048',
})

Here we show a variety of possible account types that a site might request a user to have. The cool thing about this method, is that the user does not need to have an account of this type created at this point in order to create one now.

The signer browser (MetaMask, Mist, @beakerbrowser, whatever) would only need to prompt the user to generate a new account at this time, or select an existing one, and so now "account creation" could be on a per-protocol basis, instead of per-site.

Selective Disclosure

Additionally, this API would be favorable to verifiable claims compatible identity solutions with minor additions.

For example, here let's imagine a uPort-style DID (Decentralized Identifier) based site. There are a variety of projects following a general DID-type format, and so a variety of different account types should all be able to answer similar questions:

const accounts = await web3.requestAccount({
  type: 'ethereum' || 'SOVRN' || 'civic',
  withPersonalInfo: {
    'dob': {
      required: true,
      min: '18y',
    },
    'city': {
      required: false,
    }
  },
})

Here I've laid out a simple basic schema for requesting proof of some personal details, along with a public key of a given type.

This raises a question of the return type. If some additional metadata should be supplied with an account, then the return type should not just be an address. Especially given that a site might not care what type of account you're using, as long as it can return the given personal information:

const accounts = await web3.requestAccount({
  withPersonalInfo: {
    'dob': {
      required: true,
      min: '18y',
    },
    'city': {
      required: false,
    }
  },
})

/*
   {
      type: 'BHRARER', // User selected, who cares?
      id: '0x1234567890abcdef', // An identifier for the given account.
                                // Used for requesting it signs messages.
      details: {
        'dob': { min: '18y', proof: <PROOF_FORMAT> },
      }
   }
*/

That's just a rough example of a request & response for information, without caring about the specifics of the key pair. If a user wants to trust some kind of strange encryption, who cares? It's securing their account, after all, they should be able to decide on their own security model.

For the purpose of key-type agnosticism, all signing methods should define the key or account type as part of the signing request, avoiding specialized key-type-dependent APIs like web3.eth.sendTransaction. Maybe something more like:

const account = await web3.requestAccount(opts)

const opts = {
  value: '0x123',
  to: RECIPIENT,
}

const signature = await web3.signAndHandle(account, opts)

In this context, the account type might dictate the way the opts are interpreted.

User Interface Modularity

Part of the difficulty of implementing this is that different key types have different implications for signing different types of information. For example, a bitcoin key signature mostly needs to render the amount of bitcoin being sent, and a PGP key pair mostly needs to show the information being signed as its native file type.

It may be best that each key type is much like a full module or extension, with its own support team maintaining its own approval screens. I can vouch that representing Ethereum transactions alone is worthy of a team of software developers, and I expect that as protocols evolve and analysis matures, there will be no shortage of ways that thoughtful designers and engineers can make the approval of signatures increasingly informative and useful to people of all skill levels. Consent can not be left to experts. Users should be able to reason about the terms they agree to.

Not only is the method that a signature is represented & conducted important, but some protocols (like blockchains) also include some form of client-managed transmission or storage of that signature. When the specifics of how a proposed transaction is processed, represented, handled, and returned are this open-ended, the value of a modular system becomes increasingly important.

To be honest, I was thinking about a modular ethereum signing mechanism just last year, and I went "simple" on it for the sake of pragmatism. In retrospect, I might've gone even more pragmatic to save myself time, except for the experience & insight I got from pursuing a more modular architecture.

A Pile of Relevant Tools

If something like the proposed interface were to be added to MetaMask, making it a highly generic client-side account manager, we would need a very secure and extensible interface for using any number of keys.

Sandboxing the account's relevant views in iFrames could be a powerful way to control a web site's API access to a higher-level context. For this reason, I think allowing a signing strategy to specify an iFrame URL or set of URLs would likely be a useful tool in creating a highly modular signing framework.

It's also worth noting that every signature does not necessarily require a user confirmation. SSH is a good example, and the analogy can be extended to even the internet of value and blockchains with state channels. An advanced signer might get user approval for transactions of up to a certain limit per day for a certain website. All of this could be managed by a modular account manager, and would not need to be the concern of MetaMask core. Instead, MetaMask core would be concerned with providing that trusted signing module a framework within to optionally provide a trusted signing interface.

alex-miller-0 commented 7 years ago

One thing we're dealing with at Grid+ is the idea of cosigning messages from trusted accounts. It's maybe a bit outside the scope of this proposal, but the general idea is that you set up a whitelist of accounts (in this case, the accounts would be public keys of the websites main signers). If the user gets a message that is signed by a trusted account, it proceeds through the pipeline. If it gets something from an unknown account, the message gets thrown away.

That might be useful here because it would be similar to SSL - a trusted website would need to sign a message before requesting your signature. This would still require trust of the website, but it would make MIM attacks very difficult and furthermore, I think a TCR with trusted websites and cosign addresses might emerge.

danfinlay commented 7 years ago

I think that's actually very in-scope. In your case, you have a special signer type that should only sign messages of a certain format, given some external network conditions, and would probably need to render specialized information to make the signature meaningful to a user. That's totally what I'm talking about here.

An open question I didn't touch on when I whipped this up is the method a user goes through to first add a new signing strategy. For example, if a dapp asked for a new account type:

const accounts = await web3.requestAccount({
  type: 'grid-plus',
})

And if the user had no accounts of that type, the web3 providing wallet/browser would need to present the user with the opportunity of creating an account of that type.

Now if we want a very open ended ecosystem, there might be many different interfaces for accounts of that type. They might store the keys in different places (like on a key FOB vs on the computer), or they might simply visually represent the signature differently.

This would mean we would need some kind of signer discovery system. It could necessitate a sort of signer-store. This might be a good place for some central curation, or maybe a shared registry with some governance mechanism (Token Curated Registry is a buzzword I'm aware of, but I wouldn't want to sell these tokens, I would want to carefully spread them around the ecosystem as shares of trust).

This would definitely need design, whether a popup or new tab "create a new account with one of these signers", along with ratings, reviews, etc, ideally all statically hash-linked and themselves published by registry.

alex-miller-0 commented 7 years ago

Our thought with accounts is more generally a superset of public keys (since the user may or may not have the keys on-device). This metadata could be shared between devices since it is just a whitelisting mechanism for middleware. Different devices may choose to update their permissions from new metadata depending on the device's desired level of security.

For example, an account that I am imagining might look like this:

{
  protocol: {
    ethereum: {
      userAddresses: {
        alexmiller.eth: true,
        alex.metamask.eth: true,
        0x123abc...: true,
      },    
      trustedAddresses: {
        metamask.eth: true,     
      }
    }
  }
}

With the agent we're also designing withdrawal limits, but those don't translate well to untrusted devices since you have to trust their resetting.

danfinlay commented 7 years ago

You bring up a couple really important points there, I'm glad to think of them early. Especially, users might want to connect different accounts, even from different protocols, on the same page. This could allow cool things like publishing a contract to ethereum and paying for its UI to be hosted on IPFS while posting an SSB blog post about the release for example, just instantly integrating as many protocols and accounts as the site requires.

It would seem like a lot of the above thinking would apply here, except to return the account format you're suggesting, which supports multiple protocols + accounts, the account request API would need to be open ended enough to request that. Maybe something similar to that form, like:

web3.requestAccounts({
  protocols: {
    { ethereum: { quantity: 2 } },
    { ssb: { quantity: 1 } },
  }
})

Now, that could obviously add some serious user experience bloat, similar to the effect of asking a user to connect multiple external APIs just to sign in.

One way we could potentially simplify this would be if groups of accounts (even from different protocols) could be defined as a common "identity type".

In some ways, if a group were a single "identity", it could just as easily be said that this new group of keys is really just one new account type, and so the core wallet/browser doesn't need to care that it's a group of keys, just that they are defined as an account type. This would have the hazard that if sites often defined new account types, users could be creating new accounts about as often as they use new sites. (Not saying that's awful, just pointing it out!)

One way the new-site experience could be streamlined is if by default, keys were always generated per site, without user interaction at all, deterministically from the user's seed phrase. The user would only need to interact with their account manager to either:

This is an interesting abstraction to me, because it embraces the notion that when you're on a site, it becomes aware of all the public keys you use during that session, so they are inherently linked, so they might as well be from the user's perspective too. Also, by generating keys freely without user interaction, sites can start up with secure p2p protocols from the very first interaction, even without the user consenting to any identifying details at all.

danfinlay commented 7 years ago

I'm not sure I fully grokked the trusting other accounts point, though. I think that mostly refers to an IoT device, right? So it could interface well with a user interface that is able to provide a set of keys to trust as itself?

danfinlay commented 7 years ago

Multi-Signer Pseudo-Protocol:

The difficulty with creating a modular signer protocol is that every signer could have very different requirements. I'm going to draw on personal experience from MetaMask, and any other use cases I can, to describe a signing protocol that could serve as generic.

Last year's work on eth-keyring-controller was originally intended to be extended to something like this, so I'll draw lightly from its Keyring API.

I'm going to write an abstract pseudo-class here.

const ObservableStore = require('obs-store')

class Keyring {

  // class method:
  getKeyringMetaData() returns KeyringMetadata

  // constructor:
  constructor(state, opts) {
    this.store = new ObservableStore(state)
    // Instances must call this.super()
    // state must encode full Keyring state.
    // Changes to state must be submitted as:
    // this.store.updateState(state)
    // Allowing the wallet to detect & persist these changes.
  }

  // instance methods:
  getAccounts() returns Array<Address>
  shouldGetUserApprovalFor(withAccount Address, opts MessageRequest) returns Boolean
  handleMessage(withAccount Address, opts MessageRequest)
}

class KeyringMetadata {
  accountSetupForm: Multihash,
  approvalForm: Multihash,
  configForm: Multihash,
  storeArt: Multihash
}

The ObservableStore subclass allows the keyring to manage some internal storage that will be subscribed to by the wallet/browser, and stored securely in the user's posession for re-initialization of the class during future usages.

Class Method

The one class method I'm defining here would be for retrieving a metadata object. This object should statically include access to a variety of resources that could either be bundled with the keyring or linked using a static hash linking protocol like IPFS or Swarm.

The form keys are all ways of the class statically defining a web view via iframe. This gives wallets importing this class confidence in the consistency and auditability of these components of the interface.

Each of these HashLinkedSites would be initailized in an iFrame, and provided with necessary API access via an iFrame port.

Account Setup Form (Hash Linked Site)

This form would be shown when a user tells their wallet to create an account of this type. The form would assume no prior state, and would have one method via iframe port, createKeyring(state, opts). That state and opts format would be used by the Keyring constructor to create a new instance.

Approval Form (Hash Linked Site)

This form would be shown to the user when the instance returned true from shouldGetUserApprovalFor(opts). It would be passed the opts and keyring.store.getData() result (the serialized form of the keyring), and thus fully trusted with the signing of the transaction.

The signing would need to be passed to this iFrame in case the signer is remote, and not actually owned by MetaMask at all. This has the downside of putting the crypto back into the UI, but this could still be mitigated by spinning up a WebWorker or other optimization tricks.

A nice benefit of having a generic "approval form"

It could be a message is a transaction, a packet of information, a post, or maybe even an encryption. More on this under ``

Config Form (Hash Linked Site)

A site to allow the keyring to adjust its config. Would take a serialized data dump from the Keyring, and would re-initialize a fresh keyring with the output result (if the form called an update method via iframe port).

Store Art

I'm just imagining some JSON data for displaying this Keyring, what it does, showing it in a Keyring store. This could be a flexible schema, wahtever.

Constructor (opts)

Would require opts, which are either generated by the Account Setup Form, the Config Form, or emitted regularly from the class's ObservableStore.

Instance Methods

getAccounts()

Requests the keyring identify the accounts it manages. These accounts could either be defined by a Universally Unique ID, or maybe we could allow a Locally Unique ID, if we namespaced the accounts to the dapps being used.

shouldGetUserApprovalFor (withAccount Address, opts MessageRequest)

Allows the Keyring to define whether this type of signature requires user confirmation or not.

If returns true, the user will be shown Approval Form.

If returns false, the keyring will next be asked to:

handleMessage (withAccount Address, opts MessageRequest)

A meta-method to encompass signing messages, transactions, or potentially any arbitrary keyring-dependent message that might be related to a specific account.

Some Closing Thoughts on "Handling Messages"

The keyring may need to transmit some data as part of this message, which is a good argument for passing it a transport provider.

While MetaMask today only keeps a single provider initialized at once, this is not the first time I'll suggest that when "Selecting an Account", a network/provider should be selected at the same time. (Obviously this kind of account requires user interaction, and not all account types require custom data providers).

This would suggest that the providers within MetaMask are also absolutely worthy of modularization (especially as we're adding our first non-RPC provider, IPFS, and are now considering what multi-protocol evolution might look like), but that's for another post.

alex-miller-0 commented 7 years ago

This would have the hazard that if sites often defined new account types

The context of what I was describing is really just a whitelisting scheme. So the "account" is retained across many sites and the purpose of this metadata is to be able to trust that sites requesting signatures are the same actors who you trusted initially (or, alternatively, who a whitelisting group trusted). To me, an "account" is a set of public keys and permissions, all grouped together. This is similar to what metamask has now, except that you can group keys together.

One way the new-site experience could be streamlined is if by default, keys were always generated per site, without user interaction at all, deterministically from the user's seed phrase.

This is interesting, but I don't see any advantage over the metamask model (you hold your key, which interacts with every site).

For the whitelisting, I think the easiest pattern might be something like adding a whitelist that you trust (this is maintained by a TCR or something like it) and for each site you visit in that whitelist, any requested messages have a little lock or green check mark to indicate that this is a reputable site.

So for my schema, one of these whitelist sets could be imported into your account as just that -- a set of trustedAccounts

I'm not sure I fully grokked the trusting other accounts point, though. I think that mostly refers to an IoT device, right? So it could interface well with a user interface that is able to provide a set of keys to trust as itself?

So if my account originates from my ledger, but I have put all of this effort into curating a whitelist, it would be nice if I could import it to metamask. Generally, I would say metamask is lower security than my ledger. As such, I would expect metamask to import it (or else to make importing it fairly easy). The opposite wouldn't be true - I would not expect to easily import a whitelist created on my browser into a ledger.

I guess the emergent property here might be that you have hierarchical accounts -- e.g. alexmiller.eth trusts one site: myetherwallet.com on my ledger, but that exists as a "sub-accont" in my metamask app, which has a much more robust whitelist set associated with funstuff.alexmiller.eth, sheebs.alexmiller.eth, etc.

The difficulty with creating a modular signer protocol is that every signer could have very different requirements

Agree, but again I would group them based on accounts, which would contain 1 or more signers (if by signer you mean key)