ethereum / EIPs

The Ethereum Improvement Proposal repository

Home Page:https://eips.ethereum.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ERC-1654 Dapp-wallet authentication process with contract wallets support

pazams opened this issue · comments


eip:
title: Dapp-wallet authentication process with contract wallets support
author: Maor Zamski (@pazams)
discussions-to:
status: Draft
type: Meta
created: 2018-12-12

First draft

Simple Summary

An off-chain process for dapps to prove actionable control (informally, "ownership") over a public Ethereum address using eth_sign. Supports both external wallets and contract wallets.

Definitions

  • contract wallet A contract account deployed with the intent to be used as the ownership address for on-chain assets (including ether, ERC-20 tokens, and ERC-721 NFTs). It has the ability to transfer ether or dynamically execute actions on other contracts (acting as the owner of assets controlled by those contracts). Common examples of contract wallets are multisig wallets (such as the ones provided by Gnosis and Parity) and identity contracts, as defined in ERC-725.
  • external wallet An externally owned account, controlled by a private key. Currently, most on-chain assets are owned by such accounts. A common example for an external wallet are the wallets generated by MetaMask.
  • actionable control A public key is defined to have actionable control over an address if either:
    • It is an external wallet AND the key is determined to correspond to the address.
    • It is a contract wallet AND the key exists in the contract account state and has a purpose of ACTION as defined in EIP-725.

Abstract

The authentication process starts with the dapp client component requesting a message signature from the wallet.
The client then proceeds to send the result to the dapp backend component along with the requested address to be used for authentication. The dapp backend recovers a public key from the signature, and checks if it has actionable control over the requested address. This check is done under consideration that the address may represent either an external wallet or a contract wallet. This process works with external wallets and EIP-725 contract wallets. For this process to be compatible with any other contract wallet, it requires the wallet to implement a small subset of EIP-725.

Motivation

Dapps frequently offer a customised off-chain user experience in addition to their smart-contract interface. For example, a dapp may provide a push notification feature to their users, allowing them to stay notified about successful state changes associated with their public addresses. For these type of features, a dapp needs a way to authenticate that a user has actionable control over the public address associated with their account.

A common practice dapps use in an authentication process is to only check if a recovered public key matches the requested authentication address. For contract wallets, this check is broken, as there is no corresponding private key to which to generate a signed message, and hence why some dapps are inaccessible for contract wallet users. It is therefore argued that a broader approach is needed.

Specification

Dapp

On the dapp side, the dapp-wallet authentication process MUST follow these steps:

  1. Dapp client requests the wallet software to sign a challenge message via eth_sign.
  2. Dapp client sends the signature to the dapp's backend component, along with the wallet address to be authenticated with. The address may be obtained via eth_accounts.
  3. Dapp backend recovers a public key from the signature.
  4. Dapp backend checks if the recovered key has actionable control over the provided wallet address under the assumption it could represent an external wallet OR a contract wallet. For the case of a contract wallet, it MUST be determined it supports the entirety of the EIP-725 interface via a EIP-165 interfaceID 0xdc3d2a7b or just the keyHasPurpose method as a subset of it using the EIP-165 interfaceID 0xd202158d.
  5. The result of the actionable control check is returned as the result of the authentication and the flow is complete.

A challenge message SHOULD contain a random component. This will reduce the risk of replay attacks.

A challenge message SHOULD be generated by the dapp backend AND not get sent back as input from the dapp client, but be persisted in the backend for at least the entirety of the authentication process. This will remove the risk of accepting forged challenges.

The following algorithm MAY be used by dapp backend when authenticating users with personal signed messages:

FUNCTION isSignerActionableOnAddress(challengeString, signature, walletAddress) RETURNS (successFlag, errorMsg)

  SET challengeHash to the hash of: challengeString prepended with `"\x19Ethereum Signed Message:\n" + len(challengeString)`

  SET recoveredKey to the public key recovered from signature and challengeHash

  SET recoveredAddress to the address corresponding with recoveredKey

  // try external wallet
  IF walletAddress EQUALS recoveredAddress
    RETURN true, nil
  END IF

  // else try contract wallet
  SET isSupportedContract to TRUE IF walletAddress is a smart contract AND (has interfaceID 0xd202158d OR has interfaceID 0xdc3d2a7b)

  IF isSupportedContract resulted in an error
    RETURN false, ERROR
  END IF

  IF isSupportedContract equals FALSE
    RETURN false, nil
  END IF

  SET keyHasActionPurpose to the result of calling a contract method keyHasPurpose with recoveredKey and ACTION parameters

  IF keyHasActionPurpose in an error
    RETURN false, ERROR
  END IF

  RETURN keyHasActionPurpose, nil

END FUNCTION

Wallet

External wallet

Any software agents managing external wallets are not required to make any changes to continue to work with this process.

Contract wallet

  1. The contract MUST implement the keyHasPurpose method as in EIP-725:
function keyHasPurpose(bytes32 _key, uint256 purpose) constant returns(bool exists);

When passed the ACTION purpose parameter of 2, the method MUST return true if a key is present AND it can perform actions in wallet's name (signing, logins, transactions, etc.)

When passed the ACTION purpose parameter of 2, the method MUST return false if a key is not present OR it cannot perform actions in wallet's name (signing, logins, transactions, etc.)

  1. The contract MUST implement the EIP-165 method:
function supportsInterface(bytes4 interfaceID) external view returns (bool);

It MUST return true if passed an interfaceID of 0xd202158d OR 0xdc3d2a7b. The former value represents a minimal subset of EIP-725 with just the keyHasPurpose method, while the later represents the full EIP-725 interface.

Rationale

There has been a great body of work in standardizing contracts wallets, namely #725. However, for the current process of dapp-wallet authentication, interfaces for claims and key management are not required. Instead, a single contract method and a modification for the current process suffices. The small surface area of this proposal should allow it to be easily compatible across different types of contract wallets.

Backwards Compatibility

  • External wallets are backwards compatible with this process.
  • Contract wallets with EIP-725 support, are compatible with this process without modification.
  • Contract wallets without EIP-725 support must implement the proposed subset of EIP-725 to be compatible with this process.

Implementation

Packages implementing the purposed algorithm:

Simple Summary

An off-chain process for dapps to assert whether an entity has authorized control (informally, "ownership") over a public Ethereum address using eth_sign. Supports both external wallets and contract wallets.

Definitions

  • contract wallet A contract account deployed with the intent to be used as the ownership address for on-chain assets (including ether, ERC-20 tokens, and ERC-721 NFTs). It has the ability to transfer ether or dynamically execute actions on other contracts (acting as the owner of assets controlled by those contracts). Common examples of contract wallets are multisig wallets (such as the ones provided by Gnosis and Parity) and identity contracts, as defined in ERC-725.
  • external wallet An externally owned account, controlled by a private key. Currently, most on-chain assets are owned by such accounts. A common example for an external wallet are the wallets generated by MetaMask.
  • authorized signer An entity is considered to have authorized control over a wallet if either:
    • It produced a signature of which the recovered address matches the wallet address.
    • It produced a signature of which the contract at the wallet address responds with the magic value 0x1626ba7e to the IsValidSignature contract call.

Abstract

The authentication process starts with the dapp client component requesting a message signature from the wallet.
The client then proceeds to send the result to the dapp backend component along with the requested address to be used for authentication. The dapp backend recovers a public key from the signature, and checks if it has authorized control over the requested address. This check is done under consideration that the address may represent either an external wallet or a contract wallet. This process works with external wallets and contract wallets that support EIP-1271 with 0x1626ba7e as a magic return value.

Motivation

Dapps frequently offer a customised off-chain user experience in addition to their smart-contract interface. For example, a dapp may provide a push notification feature to their users, allowing them to stay notified about successful state changes associated with their public addresses. For these type of features, a dapp needs a way to assert that a user has authorized control over the public address associated with their account.

A common practice dapps use in an authentication process is to only check if a recovered public key matches the requested authentication address. For contract wallets, this check is broken, as there is no corresponding private key to which to generate a signed message, and hence why some dapps are inaccessible for contract wallet users. It is therefore argued that a more broader approach is needed.

Specification

Dapp

On the dapp side, the dapp-wallet authentication process MUST follow these steps:

  1. Dapp client requests the wallet software to sign a challenge message via eth_sign.
  2. Dapp client sends the signature to the dapp's backend component, along with the wallet address to be authenticated with. The address may be obtained via eth_accounts.
  3. Dapp backend recovers a public key from the signature.
  4. Dapp backend checks if the recovered key has authorized control over the provided wallet address under the assumption it could represent an external wallet OR a contract wallet. For the case of a contract wallet, it MUST call IsValidSignature and expect the value 0x1626ba7e to determine whether the entity who signed the challenge has authorized control over the wallet.
  5. The result of the authorized control check is returned as the result of the authentication and the flow is complete.

A challenge message SHOULD contain a random component. This will reduce the risk of replay attacks.

A challenge message SHOULD be generated by the dapp backend AND not get sent back as input from the dapp client, but be persisted in the backend for at least the entirety of the authentication process. This will remove the risk of accepting forged challenges.

The following algorithm MAY be used by dapp backend for authenticating users with personal signed messages:

FUNCTION IsAuthorizedSigner(challengeString, signature, walletAddress) RETURNS (success)

  SET personalChallengeHash to the hash of: challengeString prepended with `"\x19Ethereum Signed Message:\n" + len(challengeString)`

  SET recoveredKey to the public key recovered from signature and personalChallengeHash

  SET recoveredAddress to the address corresponding with recoveredKey

  // try external wallet
  IF walletAddress EQUALS recoveredAddress
    RETURN true
  END IF

  SET challengeHash to the hash of: challengeString . We send just a regular Keccak256 hash, which then the smart contract hashes ontop to an erc191 hash.

  SET contractResult to the result of calling IsValidSignature(challengeHash, signature) on the contract at walletAddress 

  IF contractResult EQUALS 0x1626ba7e
    RETURN true
  ELSE
    RETURN false
  END IF

END FUNCTION

Wallet

External wallet

Any software agents managing external wallets are not required to make any changes to continue to work with this process.

Contract wallet

Contract

The contract MUST implement the isValidSignature method as suggested by EIP-1271, yet in this variation:

function isValidSignature(bytes32 hash, bytes _signature) returns(bytes4 magicValue);

Before recovering a public key, the bytes32 hash parameter MUST get hashed again with EIP-191, with 0 for "version" and the wallet address for "version specific data".

The bytes _signature parameter MAY contain multiple concatenated signatures in case of a multi-sig wallet.

The method MUST return 0x1626ba7e if the public key (or keys) recovered from the signature (or signatures) are as expected according to the wallet's own key management logic. Otherwise the method MUST return 0x00000000.

User agent

A user agent intended to work with the contract MUST generate signatures over a EIP-191 hash of a regular Keccak256 hash of the challenge message.

Rationale

EIP-1271 has done a great work with starting the discussion on a standard signature validation method for contracts. At the time of writing, it is still in draft, with several suggestions for the shape of the interface (e.g see here). This proposal takes one of the variations mentioned in the discussion, and builds on top of it a process for dapp-wallet authentication.

Backwards Compatibility

  • external wallets are backwards compatible with this process.
  • contract wallets with EIP-1271 support, are compatible with this process to the extent of supporting the method format suggested here and it's corresponding magic value.

Implementation

Packages implementing the purposed algorithm:

Copyright

Copyright and related rights waived via CC0.


Thanks to @dete @Arachnid @chrisaxiom @igorbarbashin @turbolent @jordanschalm @hwrdtm for feedback and suggestions

Thanks to @pazams for writing this up. We think that there are lots of good reasons to use smart contract wallets, even for individual users. Hopefully, lots of Dapps will make this simple change to make that use-case viable (as we will for CryptoKitties!).

Thanks for putting this together!

Quick questions:

  1. How does the function keyHasPurpose allow for passing a multi-sig in one-go? Also, this methods assumes that keys are pre-registered on the wallet contract, correct?

  2. How does one check whether a given signature is valid for a given key? Do you first recover the signer via ECDSA method and then call walletContract.keyHasPurpose(signer, action)?

@PhABC thanks for these questions!

How does one check whether a given signature is valid for a given key? Do you first recover the signer via ECDSA method and then call walletContract.keyHasPurpose(signer, action)?

Correct. For an authentication flow, we have to first recover the signer in any case since we want to first validate in case of an external wallet. If that doesn't match, we can check under assumption of a contract wallet, which then we can already use the recover result from previous step.

How does the function keyHasPurpose allow for passing a multi-sig in one-go? Also, this methods assumes that keys are pre-registered on the wallet contract, correct?

Yes, this proposal assumes keys are pre-registered. It be may done with https://github.com/ethereum/EIPs/blob/master/EIPS/eip-725.md#addkey for EIP-725 wallets, but it's also valid for the keys to be hardcoded - the implementation strategy is up to the wallet.

As for support for multi-sig, that's an excellent point!
We therefore think we should pivot this suggestion, and based it on isValidSignature as defined in EIP-1271, instead of keyHasPurpose as defined in EIP-725. We keep in mind that this suggestion is describing an off-chain process, rather than an interface, so it should fit nicely on top of EIP-1271.

@PhABC What are your thoughts?

Correct. For an authentication flow, we have to first recover the signer in any case since we want to first validate in case of an external wallet. If that doesn't match, we can check under assumption of a contract wallet, which then we can already use the recover result from previous step.

I think this is fine for some use cases, but it's a bit less general than 1271 since it prevents other types of signatures schemes than ECDSA.

As for support for multi-sig, that's an excellent point! We therefore think we should pivot this suggestion, and based it on isValidSignature as defined in EIP-1271, instead of keyHasPurpose as defined in EIP-725. We keep in mind that this suggestion is describing an off-chain process, rather than an interface, so it should fit nicely on top of EIP-1271.

That makes sense! Yes, I think a guideline for off-chain processes like the one you are proposing can be very useful. Would you mind commenting on #1271 with your thoughts? I would like to see it being finalized pretty soon as it's relatively simple, but not many people are participating in the discussion as of now.

Hey, I just published an article on "automatic authentication signatures"
This would allow the scheme you describe here to be performed securely without user input (no signing popup):
https://medium.com/@wighawag/automatic-authentication-signatures-for-web3-dcbcbc64d6b5

I guess this could be part of a different EIP but thought worth mentioning here.

@PhABC, done.

@wighawag, you have some interesting points. Here you wrote: "Upon signing, the origin (could be the hash of the origin) is inserted as part of the message to be signed". I definitely see the value there. How will that work with a wallet like MetaMask? Since the interface of dapp-wallet communication in web wallets is javascript, how can such thing be enforced with today's tech? where's the best thread to continue this discussion?

@pazams
To reply quickly here, the idea is that Metamask using the browser plugin sdk, should be aware of the origin of the document (including the embeded javascript) requesting the signature. This is how a web3 plugin like metamask can ensure an application with a different origin cannot request signature aimed at another origin: If the origin of the document is different from the one added to EIP712 envelope, the web3 browser will refuse such signature to be performed.

For discussion regarding such "origin based" signature scheme, I initially thought (and still think) it should be part of #712 but the consensus (at least for now) seems to be that a separate EIP would be a better option. I am planning to write such EIP but for now the best place of discussion might be the ethereum magicians forum where I posted the article link and a quick summary at https://ethereum-magicians.org/t/3-proposals-for-making-web3-a-better-experience/1586
Feel free to comment there

The 3 proposals also include an encryption/decryption scheme which should allow a seamless syncing mechanism for dapps. Encryption/decryption is currently being implemented by Metamask (though I am not sure what their exact plan is in this regard) and some discussion happened over there : https://ethereum-magicians.org/t/the-ux-of-eip-1024-encrypt-decrypt/1243

As for the authentication signature (that do not require origin checks) I added a link and a quick summary to the ethereum magicians forum, see here : https://ethereum-magicians.org/t/automatic-authentication-signature/2429

I think this should be revisted for the new ERC725 v2, where the owner account at key 0x0 is a key manager contract, that can have purposes etc.

I'll plan on updating the #725 issue soon.

@frozeman thanks for pointing this out!

Let's assume a dapp get's a 130 byte signature as result of sign-in with personal sign flow. That's twice as long from the expected signature (user is using a multi sig wallet). The be able to query 725 methods, it first needs to split the signature into 65 byte chunks, recover each of the public keys, and query each of them.

That could work, however, with isValidSignature, we can forward the signature as-is, and let the identity/wallet contract deal with the splitting logic. I think lifting that burden from dapps is important.

@pazams @dete Should add that the 0x1626ba7e value was achieved via bytes4(keccak256("isValidSignature(bytes32,bytes)"))

I can't find the EIP in the EIP folder ; https://github.com/ethereum/EIPs/tree/master/EIPS

Any help?

I don't understand why EIP1271 cant be used to achieve exactly the same as proposed here, but 1654 seems limited to the signature format of bytes32.

As a DApp developer, why should I use 1654 instead of 1271?

I can't find the EIP in the EIP folder ; https://github.com/ethereum/EIPs/tree/master/EIPS

@PhABC , that's because the PR has been stuck since June with no reviewers follow up 😞
@PhABC , I think it might be beneficial if we can both get on a call (and any other stake holder of this EIP), so we can discuss ways to move forward? I think a call at will point would be great. I'll also do my best to have @dete on the line.

I don't understand why EIP1271 cant be used to achieve exactly the same as proposed here, but 1654 seems limited to the signature format of bytes32.

@3esmit , see #1271 (comment) for an explanation.

@pazams Any special reason for not using the latest spec of 1271? The comment you linked suggests it as an arbitrary decision. I hope this standard becomes compatible with final 1271.

I agree with the variation of ERC1271 as added here, and discussions in #1271 are ongoing to adopt the standard to the version with bytes32.
You should then make sure your standard names ERC1271 as a requirement, and make sure the fail return value is 0xffffffff, to differentiate from silent fails.

@frozeman
💯 Once the discussions in 1271 also result in that change, I'll be more than happy to require ERC1271 and drop the interface spec from here.

@pazams Any special reason for not using the latest spec of 1271? The comment you linked suggests it as an arbitrary decision. I hope this standard becomes compatible with final 1271.

@3esmit I still see a non-compatible version here https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1271.md .
However with the recent discussions going on in 1271 I'm hopeful that can change soon!

Once we finalised we can make a new PR to the EIP doc

@PhABC @frozeman @3esmit
New PR for 1654 that now also requires 1271 as the two now converged to the same interface.

hey all, this might be relevant to check out: https://github.com/arcadeum/ethauth.js / https://github.com/arcadeum/go-ethauth -- it is an authorization scheme using eip712 which supports EOA's and contract wallets implemented in both Typescript and Go. The idea is a dapp makes an auth request of some claims (dapp name, expiry, origin domain), asks a wallet to sign the payload with eip712, and then encodes an ethauth-proof string. You can use the ethauth-proof directly even as an http handler/middleware, but since for contract wallets you need to call isValidSignature remotely, its not ideal to do the check per request. Instead you can think of it somewhat like OAuth, and use the ethauth claims proof and exchange it for a JWT token.

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment.