ethereum / EIPs

The Ethereum Improvement Proposal repository
https://eips.ethereum.org/
Creative Commons Zero v1.0 Universal
12.74k stars 5.18k forks source link

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

Closed pazams closed 2 years ago

pazams commented 5 years ago

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](https://github.com/ethereum/wiki/wiki/White-Paper#ethereum-accounts) 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](https://github.com/Gnosis/MultiSigWallet) and [Parity](https://github.com/ConsenSys/MultiSigWallet)) and `identity contracts`, as defined in [ERC-725](https://github.com/ethereum/EIPs/issues/725). - `external wallet` An externally owned [account](https://github.com/ethereum/wiki/wiki/White-Paper#ethereum-accounts), 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`](https://github.com/ethereum/wiki/wiki/JSON-RPC#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`](https://github.com/ethereum/wiki/wiki/JSON-RPC#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](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-725.md#keyhaspurpose) method as in EIP-725: ```Solidity 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.) 2. The contract MUST implement the EIP-165 method: ```Solidity 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 https://github.com/ethereum/EIPs/issues/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: - Javascript: https://github.com/dapperlabs/dappauth.js - Go: https://github.com/dapperlabs/dappauth

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

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

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

dete commented 5 years ago

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!).

PhABC commented 5 years ago

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)?

pazams commented 5 years ago

@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?

PhABC commented 5 years ago

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.

wighawag commented 5 years ago

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.

pazams commented 5 years ago

@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?

wighawag commented 5 years ago

@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

frozeman commented 5 years ago

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.

pazams commented 5 years ago

@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.

PhABC commented 4 years ago

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

PhABC commented 4 years ago

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

Any help?

3esmit commented 4 years ago

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?

pazams commented 4 years ago

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 https://github.com/ethereum/EIPs/issues/1271#issuecomment-511509273 for an explanation.

3esmit commented 4 years ago

@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.

frozeman commented 4 years ago

I agree with the variation of ERC1271 as added here, and discussions in https://github.com/ethereum/EIPs/issues/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.

pazams commented 4 years ago

@frozeman :100: 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 commented 4 years ago

@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!

frozeman commented 4 years ago

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

pazams commented 3 years ago

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

pkieltyka commented 3 years ago

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.

github-actions[bot] commented 2 years ago

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.

github-actions[bot] commented 2 years ago

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.