From 02fdd315ee0fb4a181d656ebf14f25af8265ed9a Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Thu, 2 Jun 2022 20:54:12 -0700 Subject: [PATCH 01/36] Add EIP-603: ENS Auth Linking --- EIPS/eip-603.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 EIPS/eip-603.md diff --git a/EIPS/eip-603.md b/EIPS/eip-603.md new file mode 100644 index 00000000000000..a1897f38634607 --- /dev/null +++ b/EIPS/eip-603.md @@ -0,0 +1,85 @@ +--- +eip: 603 +title: ENS Auth Linking +description: Use ENS to link hot wallets as approved signers for a root ENS address. +author: Wilkins Chung (@wwhchung) +discussions-to: https://ethereum-magicians.org/t/eip-603-ens-authentication-link/9458 +status: Draft +type: Standards Track +category: Interface +created: 2022-06-03 +requires: 137 +--- + +## Abstract +At current, web2 and contracts validate asset ownership and wallet control by requiring you to sign a message or transaction with the wallet that owns the asset. + +Examples: + - In order for you to edit your profile on OpenSea, you must sign a message with your wallet address. + - In order to access NFT gated content, you must sign a message with the wallet containing the NFT + - In order to claim an airdrop, you must interact with the smart contract with the qualifying wallet address. + +This method of validation is problematic from a security standpoint (interacting with a malicious site or contract can compromise your wallet's assets) and a convenience standpoint (e.g. if your assets are on a hardware wallet that is not easily accessible). + +This EIP proposes a solution which uses the Ethereum Name Service Specification (EIP-137) as a way to link one or more signing wallets (presumably less secure) to authenticate a main wallet. + +## Motivation +As explained in the abstract, the current method of using a person's main wallet for authentication of control and asset ownership is both insecure and inconvenient. It is as if, in order to authenticate, you must give an application root access to your wallet. This proposal provides an easy way to do 'read only' access to a wallet by leveraging existing infrastructure. + +Some solutions propose dedicated registry smart contracts to create this link, or new protocols to be supported. Rather than 're-invent the wheel', this proposal aims to use the widely adopted Ethereum Name Service in order to bootstrap a safer and more convenient way to sign and authenticate. + + +## Specification +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + + +Let: + - `mainAddress` represent the wallet address we are trying to authenticate or very asset ownership for + - `mainENS` represent the reverse lookup ENS string for `mainAddress` + - `authAddress` represent the address we want to sign with in lieu of `mainAddress` + - `authENS` represent the reverse lookup ENS string for `authAddress`. It must be in the format `auth[0-9]*.`. + +### Setting up one or many `authAddress` records +The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record configured. + +1. Using your `mainAddress` wallet, sign into ens.domains and create a subdomain record for `mainENS` called `auth[0-9]*`. This becomes the `authENS` +2. Set the ETH resolver record for this `auth[0-9]*` subdomain to the `authAddress` +3. Using `authAddress`, sign into ens.domains and set the ENS reverse record to `authENS` + +Repeat this process with as many addresses as you would like. + +### Authenticating `mainAddress` via `authAddress` +Control of `mainAddress` and ownership of `mainAddress` assets is proven if any one of associated `authAddress` is the msg.sender or has signed the message. + +Practically, this would work by performing the following operations: +1. Get the reverse ENS record for `authAddress` +2. Parse `auth[0-9]*.` to determine the linked ENS +3. Do a lookup on the linked ENS record to determine the linked `mainAddress` + + +## Rationale +The proposed specification allows one to link multiple addresses as 'authentication addresses' to a core main address. This is beneficial from a security standpoint (if the authentication address is compromised, the assets held by the main address is not), and convenience (if the authentication address is a simple MetaMask wallet but the main address is a hardware wallet). + +### Example: +I need to own an NFT to get into an event. I have my phone (hot wallet linked to auth.wilkins.eth), but not my ledger (wilkins.eth) If I sign with my phone, this verifies I own the main wallet and the NFTs in the ledger, letting me into the event safely. + +Further, I have multiple devices: + - My iPhone has mobile metamask hot wallet, and I can add it as an auth account by setting auth1.wilkins.eth to that wallet (and the corresponding reverse record). + - My iPad has the same and can be set to auth2.wilkins.eth, etc. + +Lost my device? No problem, just delete the record. No need to import/reimport seed phrases. + + +## Reference Implementation +Reference typescript client side libraries can be found here: +https://github.com/manifoldxyz/ens-auth-ethers + +Reference solidity libraries can be found here: +https://github.com/manifoldxyz/ens-auth-solidity + + +## Security Considerations +The core purpose of this EIP is to enhance security and promote a safer way to authenticate wallet control and asset ownership when the main wallet is not needed and assets held by the main wallet do not need to be moved. Consider it a way to do 'read only' authentication. + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file From d926f0ad6268d01e3298907997d4e363706c7063 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 06:16:39 -0700 Subject: [PATCH 02/36] rename eip-603 to eip-5131 and remove external links --- EIPS/{eip-603.md => eip-5131.md} | 44 ++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) rename EIPS/{eip-603.md => eip-5131.md} (78%) diff --git a/EIPS/eip-603.md b/EIPS/eip-5131.md similarity index 78% rename from EIPS/eip-603.md rename to EIPS/eip-5131.md index a1897f38634607..2ef84381963a2e 100644 --- a/EIPS/eip-603.md +++ b/EIPS/eip-5131.md @@ -1,5 +1,5 @@ --- -eip: 603 +eip: 5131 title: ENS Auth Linking description: Use ENS to link hot wallets as approved signers for a root ENS address. author: Wilkins Chung (@wwhchung) @@ -23,6 +23,7 @@ This method of validation is problematic from a security standpoint (interacting This EIP proposes a solution which uses the Ethereum Name Service Specification (EIP-137) as a way to link one or more signing wallets (presumably less secure) to authenticate a main wallet. + ## Motivation As explained in the abstract, the current method of using a person's main wallet for authentication of control and asset ownership is both insecure and inconvenient. It is as if, in order to authenticate, you must give an application root access to your wallet. This proposal provides an easy way to do 'read only' access to a wallet by leveraging existing infrastructure. @@ -71,15 +72,48 @@ Lost my device? No problem, just delete the record. No need to import/reimport ## Reference Implementation -Reference typescript client side libraries can be found here: -https://github.com/manifoldxyz/ens-auth-ethers -Reference solidity libraries can be found here: -https://github.com/manifoldxyz/ens-auth-solidity +### Client Side +In typescript, the validation function, using ethers.js would be as follows: +``` +async function getLinkedAddress(provider: ethers.providers.Provider, address: string): Promise { + const addressENS = await provider.lookupAddress(address); + if (!addressENS) return null; + + const authMatch = addressENS.match(/^(auth[0-9]*)\.(.*)/); + if (!authMatch) return null; + + const linkedENS = authMatch[2]; + const linkedAddress = await provider.resolveName(linkedENS); + + if (!linkedAddress) return null; + + return { + ens: linkedENS, + address: linkedAddress + }; +} +``` + +### Solidity +In solidity, the validation signature would be: +``` +/** + * Validate that the message sender is an authentication address for the mainAddress + * + * @param ensRegistry Address of ENS registry + * @param senderENS Sender ENS. This is passed in for gas efficient checking against main address ENS + * @param mainAddress The main address we are checking against + * @param mainENSParts The array of the main address ENS domain parts (e.g. wilkins.eth == ['wilkins', 'eth']). + * This is used vs. the full ENS a a single string name hash computations are gas efficient. + */ +function validate(address ensRegistry, bytes calldata senderENS, address mainAddress, string[] memory mainENSParts) view returns(bool); +``` ## Security Considerations The core purpose of this EIP is to enhance security and promote a safer way to authenticate wallet control and asset ownership when the main wallet is not needed and assets held by the main wallet do not need to be moved. Consider it a way to do 'read only' authentication. + ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file From 3e3b45cf8b4b56fa384237b65d30c7d686f179ca Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 06:30:50 -0700 Subject: [PATCH 03/36] changing to an ERC --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 2ef84381963a2e..67fef3e8abab06 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -6,7 +6,7 @@ author: Wilkins Chung (@wwhchung) discussions-to: https://ethereum-magicians.org/t/eip-603-ens-authentication-link/9458 status: Draft type: Standards Track -category: Interface +category: ERC created: 2022-06-03 requires: 137 --- From 069848e5fbf422212944a36242024aeba0bad0b6 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 06:46:14 -0700 Subject: [PATCH 04/36] additional context --- EIPS/eip-5131.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 67fef3e8abab06..c74e72e32bd59b 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -57,6 +57,7 @@ Practically, this would work by performing the following operations: 2. Parse `auth[0-9]*.` to determine the linked ENS 3. Do a lookup on the linked ENS record to determine the linked `mainAddress` +Note that this specification allows for both contract level and client/server side validation of signatures. It is not limited to smart contracts, which is why there is no proposed external interface definition. ## Rationale The proposed specification allows one to link multiple addresses as 'authentication addresses' to a core main address. This is beneficial from a security standpoint (if the authentication address is compromised, the assets held by the main address is not), and convenience (if the authentication address is a simple MetaMask wallet but the main address is a hardware wallet). @@ -96,7 +97,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st ``` ### Solidity -In solidity, the validation signature would be: +In solidity, the validation signature could look like: ``` /** * Validate that the message sender is an authentication address for the mainAddress @@ -110,6 +111,7 @@ In solidity, the validation signature would be: function validate(address ensRegistry, bytes calldata senderENS, address mainAddress, string[] memory mainENSParts) view returns(bool); ``` +This can be an internal validate function to the smart contract, that is used in conjunction with specs like EIP-1721: Standard Signature Validation Method for Contracts and Merkle Proof Verification. ## Security Considerations The core purpose of this EIP is to enhance security and promote a safer way to authenticate wallet control and asset ownership when the main wallet is not needed and assets held by the main wallet do not need to be moved. Consider it a way to do 'read only' authentication. From 03d63ba976f7d1ec2774272ed5deec8dbadb34bc Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 07:00:26 -0700 Subject: [PATCH 05/36] add refrence implementations --- EIPS/eip-5131.md | 126 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 119 insertions(+), 7 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index c74e72e32bd59b..bfa12fb1174465 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -74,7 +74,7 @@ Lost my device? No problem, just delete the record. No need to import/reimport ## Reference Implementation -### Client Side +### Client/Server Side In typescript, the validation function, using ethers.js would be as follows: ``` async function getLinkedAddress(provider: ethers.providers.Provider, address: string): Promise { @@ -96,10 +96,42 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st } ``` -### Solidity -In solidity, the validation signature could look like: -``` +### Contract side + +### With a secure server +If there is a secure, you could run the client/server code above, then use the result in conjunction with EIP-1721 for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. + +#### Without a secure server (web client only) +Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. + +`````` +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @author: manifold.xyz + /** + * ENS Registry Interface + */ +interface ENS { + function resolver(bytes32 node) external view returns (address); +} + +/** + * ENS Resolver Interface + */ +interface Resolver{ + function addr(bytes32 node) external view returns (address); + function name(bytes32 node) external view returns (string memory); +} + +/** + * Validate a signing address is associtaed with a linked address + */ +abstract contract LinkedAddress { + + /** * Validate that the message sender is an authentication address for the mainAddress * * @param ensRegistry Address of ENS registry @@ -108,10 +140,90 @@ In solidity, the validation signature could look like: * @param mainENSParts The array of the main address ENS domain parts (e.g. wilkins.eth == ['wilkins', 'eth']). * This is used vs. the full ENS a a single string name hash computations are gas efficient. */ -function validate(address ensRegistry, bytes calldata senderENS, address mainAddress, string[] memory mainENSParts) view returns(bool); -``` + function validate(address ensRegistry, bytes calldata senderENS, address mainAddress, string[] memory mainENSParts) internal view returns(bool) { + bytes32 mainNameHash = _computeNamehash(mainENSParts); + address mainResolver = ENS(ensRegistry).resolver(mainNameHash); + require(mainResolver != address(0), "Invalid"); + require(mainAddress == Resolver(mainResolver).addr(mainNameHash), "Invalid"); + bytes32 senderReverseNameHash = _computeReverseNamehash(); + address senderResolver = ENS(ensRegistry).resolver(senderReverseNameHash); + require(senderResolver != address(0), "Invalid"); + string memory senderENSLookup = Resolver(senderResolver).name(senderReverseNameHash); + require(keccak256(senderENS) == keccak256(bytes(senderENSLookup)), "Invalid"); + + // Quick substring comparison + // Get the total theoretical length of mainENS + bytes memory ensCheckBuffer; + for (uint i = mainENSParts.length; i > 0;) { + ensCheckBuffer = abi.encodePacked('.', mainENSParts[i-1], ensCheckBuffer); + unchecked { + i--; + } + } + bytes32 ensCheck = keccak256(ensCheckBuffer); + + // Length of senderENS must be >= ensCheckBuffer.length+4 (since it needs to be of format auth[0-9]*.mainENS) + require(senderENS.length >= ensCheckBuffer.length+4, "Invalid"); + // Check ending substring of the senderENS matches + require(ensCheck == keccak256(senderENS[senderENS.length-ensCheckBuffer.length:]), "Invalid"); + // Check prefix matches auth[0-9]*. + require(keccak256(abi.encodePacked('auth')) == keccak256(senderENS[:4]), "Invalid"); + for (uint i = senderENS.length-ensCheckBuffer.length; i > 4;) { + require(senderENS[0] >= 0x30 && senderENS[i] <= 0x39, "Invalid"); + unchecked { + i--; + } + } + return true; + } + + // ********************* + // Helper Functions + // ********************* + + function _computeNamehash(string[] memory _nameParts) private pure returns (bytes32 namehash) { + namehash = 0x0000000000000000000000000000000000000000000000000000000000000000; + for (uint i = _nameParts.length; i > 0;) { + namehash = keccak256( + abi.encodePacked(namehash, keccak256(abi.encodePacked(_nameParts[i-1]))) + ); + unchecked { + i--; + } + } + } + + function _computeReverseNamehash() private view returns (bytes32 namehash) { + namehash = 0x0000000000000000000000000000000000000000000000000000000000000000; + namehash = keccak256( + abi.encodePacked(namehash, keccak256(abi.encodePacked('reverse'))) + ); + namehash = keccak256( + abi.encodePacked(namehash, keccak256(abi.encodePacked('addr'))) + ); + namehash = keccak256( + abi.encodePacked(namehash, keccak256(_addressToStringLowercase(msg.sender))) + ); + } + + function _addressToStringLowercase(address _address) private pure returns (bytes memory addressString) { + addressString = new bytes(40); + for (uint i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint(uint160(_address)) / (2**(8*(19 - i))))); + bytes1 hi = bytes1(uint8(b) / 16); + bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); + addressString[2*i] = _bytes1ToChar(hi); + addressString[2*i+1] = _bytes1ToChar(lo); + } + } + + function _bytes1ToChar(bytes1 b) private pure returns (bytes1 c) { + if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); + else return bytes1(uint8(b) + 0x57); + } -This can be an internal validate function to the smart contract, that is used in conjunction with specs like EIP-1721: Standard Signature Validation Method for Contracts and Merkle Proof Verification. +} +``` ## Security Considerations The core purpose of this EIP is to enhance security and promote a safer way to authenticate wallet control and asset ownership when the main wallet is not needed and assets held by the main wallet do not need to be moved. Consider it a way to do 'read only' authentication. From 31fd4959a6dd83773423395a4b762d76557f3993 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 07:40:02 -0700 Subject: [PATCH 06/36] rename --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index bfa12fb1174465..c97fedc6962082 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -1,6 +1,6 @@ --- eip: 5131 -title: ENS Auth Linking +title: ENS Subdomain Authentication description: Use ENS to link hot wallets as approved signers for a root ENS address. author: Wilkins Chung (@wwhchung) discussions-to: https://ethereum-magicians.org/t/eip-603-ens-authentication-link/9458 From d779068cfc2d58e13a8e77e52343044d29c82894 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 07:45:44 -0700 Subject: [PATCH 07/36] edits as per suggestions --- EIPS/eip-5131.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index c97fedc6962082..bfa1d23711adae 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -12,17 +12,22 @@ requires: 137 --- ## Abstract -At current, web2 and contracts validate asset ownership and wallet control by requiring you to sign a message or transaction with the wallet that owns the asset. +Proving the ownership of an asset to a third party application in the Ethereum ecosystem is a common occurrence. Users frequently sign payloads of data presented to them via these applications in order to prove ownership. The process of proving ownership of an address is commonly used to authenticate users before allowing them to perform some operation. Examples: - In order for you to edit your profile on OpenSea, you must sign a message with your wallet address. - - In order to access NFT gated content, you must sign a message with the wallet containing the NFT + - In order to access NFT gated content, you must sign a message with the wallet containing the NFT. - In order to claim an airdrop, you must interact with the smart contract with the qualifying wallet address. + - In order to prove ownership of an NFT, you must sign a payload with the address that owns that NFT. -This method of validation is problematic from a security standpoint (interacting with a malicious site or contract can compromise your wallet's assets) and a convenience standpoint (e.g. if your assets are on a hardware wallet that is not easily accessible). +Unfortunately, we've seen many cases where users have accidentally signed a malicious payload. The result is almost always a significant loss of assets associated with the signing address. + +In addition to this, many users keep significant portions of their assets in 'cold storage'. With the increased security from 'cold storage' solutions, we usually see decreased accessibility because users naturally increase the barriers required to access these wallets. This EIP proposes a solution which uses the Ethereum Name Service Specification (EIP-137) as a way to link one or more signing wallets (presumably less secure) to authenticate a main wallet. +From there, the benefits are twofold. This EIP gives users increased security via outsourcing potentially malicious signing operations to wallets that are more accessible (hot wallets), while being able to maintain the intended security assumptions of wallets that are not frequently used for signing operations. + ## Motivation As explained in the abstract, the current method of using a person's main wallet for authentication of control and asset ownership is both insecure and inconvenient. It is as if, in order to authenticate, you must give an application root access to your wallet. This proposal provides an easy way to do 'read only' access to a wallet by leveraging existing infrastructure. @@ -99,7 +104,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st ### Contract side ### With a secure server -If there is a secure, you could run the client/server code above, then use the result in conjunction with EIP-1721 for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. +If there is a secure, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1721](./eip-1721.md): `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. #### Without a secure server (web client only) Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. From a24fa63f8ab0e47605505da9e779e8c12d80da73 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 09:35:08 -0700 Subject: [PATCH 08/36] further content edits --- EIPS/eip-5131.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index bfa1d23711adae..49e1cc2933381e 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -40,19 +40,20 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL Let: - - `mainAddress` represent the wallet address we are trying to authenticate or very asset ownership for - - `mainENS` represent the reverse lookup ENS string for `mainAddress` - - `authAddress` represent the address we want to sign with in lieu of `mainAddress` - - `authENS` represent the reverse lookup ENS string for `authAddress`. It must be in the format `auth[0-9]*.`. + - `mainAddress` represents the wallet address we are trying to authenticate or prove asset ownership for. + - `mainENS` represents the reverse lookup ENS string for `mainAddress`. + - `authAddress` represents the address we want to use for signing in lieu of `mainAddress`. + - `authENS` represents the reverse lookup ENS string for `authAddress`. It must be in the format `auth[0-9]*.`. + ### Setting up one or many `authAddress` records The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record configured. 1. Using your `mainAddress` wallet, sign into ens.domains and create a subdomain record for `mainENS` called `auth[0-9]*`. This becomes the `authENS` -2. Set the ETH resolver record for this `auth[0-9]*` subdomain to the `authAddress` +2. Set the ETH resolver record for the subdomain created in step 1 (`authENS`) to the `authAddress`. 3. Using `authAddress`, sign into ens.domains and set the ENS reverse record to `authENS` -Repeat this process with as many addresses as you would like. +Currently this EIP does not enforce an upper-bound on the number of `authAddress` entries you can include. Users can repeat this process as with as many address as they like. ### Authenticating `mainAddress` via `authAddress` Control of `mainAddress` and ownership of `mainAddress` assets is proven if any one of associated `authAddress` is the msg.sender or has signed the message. @@ -74,7 +75,9 @@ Further, I have multiple devices: - My iPhone has mobile metamask hot wallet, and I can add it as an auth account by setting auth1.wilkins.eth to that wallet (and the corresponding reverse record). - My iPad has the same and can be set to auth2.wilkins.eth, etc. -Lost my device? No problem, just delete the record. No need to import/reimport seed phrases. +Lost Access to Signing Key/Device: +In the event that a user losses access to the signing key of an `authAddress` they can simply delete the corresponding ENS record. An attractive side effect of this is that there is no need to import/reimport seed phrases. +In the unfortunate event that you lose access to the signing key of the `mainAddress`, users will lose access to that top-level domain and all related subdomains. As always, this private key data should be treated as highly sensitive. ## Reference Implementation @@ -104,7 +107,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st ### Contract side ### With a secure server -If there is a secure, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1721](./eip-1721.md): `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. +If your application operates a secure backend server, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1721](./eip-1721.md): `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. #### Without a secure server (web client only) Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. From e51e8edd918a02be2658b4026b4ee2a89088be50 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 09:42:35 -0700 Subject: [PATCH 09/36] grammar fix --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 49e1cc2933381e..a33d7aa3340df2 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -53,7 +53,7 @@ The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record 2. Set the ETH resolver record for the subdomain created in step 1 (`authENS`) to the `authAddress`. 3. Using `authAddress`, sign into ens.domains and set the ENS reverse record to `authENS` -Currently this EIP does not enforce an upper-bound on the number of `authAddress` entries you can include. Users can repeat this process as with as many address as they like. +Currently this EIP does not enforce an upper-bound on the number of `authAddress` entries you can include. Users can repeat this process with as many address as they like. ### Authenticating `mainAddress` via `authAddress` Control of `mainAddress` and ownership of `mainAddress` assets is proven if any one of associated `authAddress` is the msg.sender or has signed the message. From 5676cce50a468475d5669bb8840b754bff139aa8 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 09:43:15 -0700 Subject: [PATCH 10/36] short description change --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index a33d7aa3340df2..cb06f7d2eafe20 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -1,7 +1,7 @@ --- eip: 5131 title: ENS Subdomain Authentication -description: Use ENS to link hot wallets as approved signers for a root ENS address. +description: Using ENS subdomains to facilitate safer and more convenient signing operations. author: Wilkins Chung (@wwhchung) discussions-to: https://ethereum-magicians.org/t/eip-603-ens-authentication-link/9458 status: Draft From 3c701892ec8b6d01e03bfa39ea70133fa8a23042 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 09:50:53 -0700 Subject: [PATCH 11/36] update discussion link --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index cb06f7d2eafe20..19a0b08d41eb96 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -3,7 +3,7 @@ eip: 5131 title: ENS Subdomain Authentication description: Using ENS subdomains to facilitate safer and more convenient signing operations. author: Wilkins Chung (@wwhchung) -discussions-to: https://ethereum-magicians.org/t/eip-603-ens-authentication-link/9458 +discussions-to: https://ethereum-magicians.org/t/eip-5131-ens-subdomain-authentication/9458 status: Draft type: Standards Track category: ERC From c0c8ded6b9a911145b236675aba5e51ae3b13749 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 12:48:06 -0700 Subject: [PATCH 12/36] update ref implementation --- EIPS/eip-5131.md | 114 ++++++++++++++++++----------------------------- 1 file changed, 43 insertions(+), 71 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 19a0b08d41eb96..8408c00e0506ed 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -110,7 +110,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st If your application operates a secure backend server, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1721](./eip-1721.md): `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. #### Without a secure server (web client only) -Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. +Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. Note: We do NOT have to do a reverse lookup on the smart contract, as we can simply validate that the forward records are properly set. This makes the gas cost of doing the validation about 14,563 gas. `````` // SPDX-License-Identifier: MIT @@ -137,51 +137,44 @@ interface Resolver{ /** * Validate a signing address is associtaed with a linked address */ -abstract contract LinkedAddress { - +library LinkedAddress { /** * Validate that the message sender is an authentication address for the mainAddress * * @param ensRegistry Address of ENS registry - * @param senderENS Sender ENS. This is passed in for gas efficient checking against main address ENS - * @param mainAddress The main address we are checking against - * @param mainENSParts The array of the main address ENS domain parts (e.g. wilkins.eth == ['wilkins', 'eth']). + * @param authAddress The address of the wallet that wants to perform the authentication. + * @param authENSLabel The ENS label of the authentication wallet (must be `auth[0-9]*`) + * @param mainAddress The main address we want to authenticate for. + * @param mainENSParts The array of the main address ENS domain parts (e.g. wilkins.eth == ['wilkins', 'eth']). * This is used vs. the full ENS a a single string name hash computations are gas efficient. */ - function validate(address ensRegistry, bytes calldata senderENS, address mainAddress, string[] memory mainENSParts) internal view returns(bool) { + function validate( + address ensRegistry, + address authAddress, + bytes calldata authENSLabel, + address mainAddress, + string[] calldata mainENSParts + ) internal view returns (bool) { + // Check if the ENS nodes resolve correctly to the provided addresses bytes32 mainNameHash = _computeNamehash(mainENSParts); address mainResolver = ENS(ensRegistry).resolver(mainNameHash); - require(mainResolver != address(0), "Invalid"); - require(mainAddress == Resolver(mainResolver).addr(mainNameHash), "Invalid"); - bytes32 senderReverseNameHash = _computeReverseNamehash(); - address senderResolver = ENS(ensRegistry).resolver(senderReverseNameHash); - require(senderResolver != address(0), "Invalid"); - string memory senderENSLookup = Resolver(senderResolver).name(senderReverseNameHash); - require(keccak256(senderENS) == keccak256(bytes(senderENSLookup)), "Invalid"); - - // Quick substring comparison - // Get the total theoretical length of mainENS - bytes memory ensCheckBuffer; - for (uint i = mainENSParts.length; i > 0;) { - ensCheckBuffer = abi.encodePacked('.', mainENSParts[i-1], ensCheckBuffer); - unchecked { - i--; + require(mainResolver != address(0), "Main ENS not registered"); + require(mainAddress == Resolver(mainResolver).addr(mainNameHash), "Main address is wrong"); + + bytes32 authNameHash = _computeNamehash(mainNameHash, string(authENSLabel)); + address authResolver = ENS(ensRegistry).resolver(authNameHash); + require(authResolver != address(0), "Auth ENS not registed"); + require(authAddress == Resolver(authResolver).addr(authNameHash), "Not authenticated"); + + // Check that the subdomain name has the correct format auth[0-9]*. + bytes4 authPart = bytes4(authENSLabel[:4]); + require(authPart == "auth", "Invalid prefix"); + unchecked { + for (uint256 i = authENSLabel.length; i > 4; i--) { + require(authENSLabel[i] >= 0x30 && authENSLabel[i] <= 0x39, "Invalid char"); } } - bytes32 ensCheck = keccak256(ensCheckBuffer); - - // Length of senderENS must be >= ensCheckBuffer.length+4 (since it needs to be of format auth[0-9]*.mainENS) - require(senderENS.length >= ensCheckBuffer.length+4, "Invalid"); - // Check ending substring of the senderENS matches - require(ensCheck == keccak256(senderENS[senderENS.length-ensCheckBuffer.length:]), "Invalid"); - // Check prefix matches auth[0-9]*. - require(keccak256(abi.encodePacked('auth')) == keccak256(senderENS[:4]), "Invalid"); - for (uint i = senderENS.length-ensCheckBuffer.length; i > 4;) { - require(senderENS[0] >= 0x30 && senderENS[i] <= 0x39, "Invalid"); - unchecked { - i--; - } - } + return true; } @@ -189,47 +182,26 @@ abstract contract LinkedAddress { // Helper Functions // ********************* - function _computeNamehash(string[] memory _nameParts) private pure returns (bytes32 namehash) { + function _computeNamehash(string[] calldata _nameParts) + private + pure + returns (bytes32 namehash) + { namehash = 0x0000000000000000000000000000000000000000000000000000000000000000; - for (uint i = _nameParts.length; i > 0;) { - namehash = keccak256( - abi.encodePacked(namehash, keccak256(abi.encodePacked(_nameParts[i-1]))) - ); - unchecked { - i--; + unchecked { + for (uint256 i = _nameParts.length; i > 0; --i) { + namehash = _computeNamehash(namehash, _nameParts[i - 1]); } } } - function _computeReverseNamehash() private view returns (bytes32 namehash) { - namehash = 0x0000000000000000000000000000000000000000000000000000000000000000; - namehash = keccak256( - abi.encodePacked(namehash, keccak256(abi.encodePacked('reverse'))) - ); - namehash = keccak256( - abi.encodePacked(namehash, keccak256(abi.encodePacked('addr'))) - ); - namehash = keccak256( - abi.encodePacked(namehash, keccak256(_addressToStringLowercase(msg.sender))) - ); - } - - function _addressToStringLowercase(address _address) private pure returns (bytes memory addressString) { - addressString = new bytes(40); - for (uint i = 0; i < 20; i++) { - bytes1 b = bytes1(uint8(uint(uint160(_address)) / (2**(8*(19 - i))))); - bytes1 hi = bytes1(uint8(b) / 16); - bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); - addressString[2*i] = _bytes1ToChar(hi); - addressString[2*i+1] = _bytes1ToChar(lo); - } - } - - function _bytes1ToChar(bytes1 b) private pure returns (bytes1 c) { - if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); - else return bytes1(uint8(b) + 0x57); + function _computeNamehash(bytes32 parentNamehash, string calldata name) + private + pure + returns (bytes32 namehash) + { + namehash = keccak256(abi.encodePacked(parentNamehash, keccak256(bytes(name)))); } - } ``` From 713c03f835ebcf6bc63f7543726346c6843f0aca Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 13:12:18 -0700 Subject: [PATCH 13/36] update with new reference code --- EIPS/eip-5131.md | 60 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 8408c00e0506ed..e45d0562c36904 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -47,7 +47,7 @@ Let: ### Setting up one or many `authAddress` records -The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record configured. +The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record and reverse record configured. 1. Using your `mainAddress` wallet, sign into ens.domains and create a subdomain record for `mainENS` called `auth[0-9]*`. This becomes the `authENS` 2. Set the ETH resolver record for the subdomain created in step 1 (`authENS`) to the `authAddress`. @@ -110,7 +110,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st If your application operates a secure backend server, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1721](./eip-1721.md): `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. #### Without a secure server (web client only) -Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. Note: We do NOT have to do a reverse lookup on the smart contract, as we can simply validate that the forward records are properly set. This makes the gas cost of doing the validation about 14,563 gas. +Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. `````` // SPDX-License-Identifier: MIT @@ -137,12 +137,12 @@ interface Resolver{ /** * Validate a signing address is associtaed with a linked address */ + library LinkedAddress { /** * Validate that the message sender is an authentication address for the mainAddress * * @param ensRegistry Address of ENS registry - * @param authAddress The address of the wallet that wants to perform the authentication. * @param authENSLabel The ENS label of the authentication wallet (must be `auth[0-9]*`) * @param mainAddress The main address we want to authenticate for. * @param mainENSParts The array of the main address ENS domain parts (e.g. wilkins.eth == ['wilkins', 'eth']). @@ -150,7 +150,6 @@ library LinkedAddress { */ function validate( address ensRegistry, - address authAddress, bytes calldata authENSLabel, address mainAddress, string[] calldata mainENSParts @@ -161,10 +160,32 @@ library LinkedAddress { require(mainResolver != address(0), "Main ENS not registered"); require(mainAddress == Resolver(mainResolver).addr(mainNameHash), "Main address is wrong"); + bytes32 mainReverseHash = _computeReverseNamehash(mainAddress); + address mainReverseResolver = ENS(ensRegistry).resolver(mainReverseHash); + require(mainReverseResolver != address(0), "Main ENS reverse lookup not registered"); + + // Verify that the reverse lookup for mainAddress matches the mainENSParts + { + bytes memory ensCheckBuffer; + uint256 i = mainENSParts.length; + for (; i > 1; ) { + ensCheckBuffer = abi.encodePacked(".", mainENSParts[i - 1], ensCheckBuffer); + unchecked { + i--; + } + } + ensCheckBuffer = abi.encodePacked(mainENSParts[i - 1], ensCheckBuffer); + require( + keccak256(abi.encodePacked(Resolver(mainReverseResolver).name(mainReverseHash))) == + keccak256(ensCheckBuffer), + "Main ENS mismatch" + ); + } + bytes32 authNameHash = _computeNamehash(mainNameHash, string(authENSLabel)); address authResolver = ENS(ensRegistry).resolver(authNameHash); require(authResolver != address(0), "Auth ENS not registed"); - require(authAddress == Resolver(authResolver).addr(authNameHash), "Not authenticated"); + require(msg.sender == Resolver(authResolver).addr(authNameHash), "Not authenticated"); // Check that the subdomain name has the correct format auth[0-9]*. bytes4 authPart = bytes4(authENSLabel[:4]); @@ -202,6 +223,35 @@ library LinkedAddress { { namehash = keccak256(abi.encodePacked(parentNamehash, keccak256(bytes(name)))); } + + function _computeReverseNamehash(address _address) private view returns (bytes32 namehash) { + namehash = 0x0000000000000000000000000000000000000000000000000000000000000000; + namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked("reverse")))); + namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked("addr")))); + namehash = keccak256( + abi.encodePacked(namehash, keccak256(_addressToStringLowercase(_address))) + ); + } + + function _addressToStringLowercase(address _address) + private + pure + returns (bytes memory addressString) + { + addressString = new bytes(40); + for (uint256 i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint256(uint160(_address)) / (2**(8 * (19 - i))))); + bytes1 hi = bytes1(uint8(b) / 16); + bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); + addressString[2 * i] = _bytes1ToChar(hi); + addressString[2 * i + 1] = _bytes1ToChar(lo); + } + } + + function _bytes1ToChar(bytes1 b) private pure returns (bytes1 c) { + if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); + else return bytes1(uint8(b) + 0x57); + } } ``` From 51a07532ea2f77a63b724f6a68ef2229c2166cae Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 13:19:01 -0700 Subject: [PATCH 14/36] format edits --- EIPS/eip-5131.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index e45d0562c36904..cc16c562c0347a 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -62,16 +62,23 @@ Practically, this would work by performing the following operations: 1. Get the reverse ENS record for `authAddress` 2. Parse `auth[0-9]*.` to determine the linked ENS 3. Do a lookup on the linked ENS record to determine the linked `mainAddress` +4. MUST get the reverse ENS record for `mainAddress` and verify that it matches `` + - Otherwise one could set up other ENS nodes (with auths) that point to mainAddress and authenticate via those. Note that this specification allows for both contract level and client/server side validation of signatures. It is not limited to smart contracts, which is why there is no proposed external interface definition. ## Rationale The proposed specification allows one to link multiple addresses as 'authentication addresses' to a core main address. This is beneficial from a security standpoint (if the authentication address is compromised, the assets held by the main address is not), and convenience (if the authentication address is a simple MetaMask wallet but the main address is a hardware wallet). -### Example: -I need to own an NFT to get into an event. I have my phone (hot wallet linked to auth.wilkins.eth), but not my ledger (wilkins.eth) If I sign with my phone, this verifies I own the main wallet and the NFTs in the ledger, letting me into the event safely. +### Example 1: Event Access +An event requires attendees to prove ownership of an NFT to gain access. The attendee in question only has their phone available to perform a signing operation upon the presentation of a challenge. The phone is considered the attendees 'hot wallet' which is linked to auth.wilkins.eth. +The NFT is owned by the Ethereum address that corresponds to wilkins.eth, which is controlled by the attendees' cold storage device (Ledger, Trezor etc). The attendee does not have this device present at the event. +The attendee is able to prove ownership of the NFT by signing with the 'hot wallet' key. -Further, I have multiple devices: +This is made possible because the attendee completed the necessary upfront authorization steps with the cold storage device to allow the 'hot wallet' to be used for this operation. + +### Example 2: Multiple Devices +I have multiple devices and wish to sign with any of them for convenience. In this situation: - My iPhone has mobile metamask hot wallet, and I can add it as an auth account by setting auth1.wilkins.eth to that wallet (and the corresponding reverse record). - My iPad has the same and can be set to auth2.wilkins.eth, etc. @@ -107,7 +114,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st ### Contract side ### With a secure server -If your application operates a secure backend server, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1721](./eip-1721.md): `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. +If your application operates a secure backend server, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1271](./eip-1271.md) : `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. #### Without a secure server (web client only) Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. From dc3ad8add4a7ae7c3ffd34b75c36010f7e6b7f9c Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 3 Jun 2022 14:47:15 -0700 Subject: [PATCH 15/36] update reference implementation --- EIPS/eip-5131.md | 80 ++++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index cc16c562c0347a..ca2af17795d64b 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -144,12 +144,29 @@ interface Resolver{ /** * Validate a signing address is associtaed with a linked address */ - library LinkedAddress { /** - * Validate that the message sender is an authentication address for the mainAddress + * Validate that the message sender is an authentication address for mainAddress + * @param ensRegistry Address of ENS registry + * @param authENSLabel The ENS label of the authentication wallet (must be `auth[0-9]*`) + * @param mainAddress The main address we want to authenticate for. + * @param mainENSParts The array of the main address ENS domain parts (e.g. wilkins.eth == ['wilkins', 'eth']). + * This is used vs. the full ENS a a single string name hash computations are gas efficient. + */ + function validateSender( + address ensRegistry, + bytes calldata authENSLabel, + address mainAddress, + string[] calldata mainENSParts + ) internal view returns (bool) { + return validate(ensRegistry, msg.sender, authENSLabel, mainAddress, mainENSParts); + } + + /** + * Validate that the authAddress is an authentication address for mainAddress * * @param ensRegistry Address of ENS registry + * @param authAddress The address of the authentication wallet * @param authENSLabel The ENS label of the authentication wallet (must be `auth[0-9]*`) * @param mainAddress The main address we want to authenticate for. * @param mainENSParts The array of the main address ENS domain parts (e.g. wilkins.eth == ['wilkins', 'eth']). @@ -157,6 +174,7 @@ library LinkedAddress { */ function validate( address ensRegistry, + address authAddress, bytes calldata authENSLabel, address mainAddress, string[] calldata mainENSParts @@ -173,15 +191,13 @@ library LinkedAddress { // Verify that the reverse lookup for mainAddress matches the mainENSParts { - bytes memory ensCheckBuffer; - uint256 i = mainENSParts.length; - for (; i > 1; ) { - ensCheckBuffer = abi.encodePacked(".", mainENSParts[i - 1], ensCheckBuffer); - unchecked { - i--; + uint256 len = mainENSParts.length; + bytes memory ensCheckBuffer = bytes(mainENSParts[0]); + unchecked { + for (uint256 idx = 1; idx < len; ++idx) { + ensCheckBuffer = abi.encodePacked(ensCheckBuffer, ".", mainENSParts[idx]); } } - ensCheckBuffer = abi.encodePacked(mainENSParts[i - 1], ensCheckBuffer); require( keccak256(abi.encodePacked(Resolver(mainReverseResolver).name(mainReverseHash))) == keccak256(ensCheckBuffer), @@ -192,7 +208,7 @@ library LinkedAddress { bytes32 authNameHash = _computeNamehash(mainNameHash, string(authENSLabel)); address authResolver = ENS(ensRegistry).resolver(authNameHash); require(authResolver != address(0), "Auth ENS not registed"); - require(msg.sender == Resolver(authResolver).addr(authNameHash), "Not authenticated"); + require(authAddress == Resolver(authResolver).addr(authNameHash), "Not authenticated"); // Check that the subdomain name has the correct format auth[0-9]*. bytes4 authPart = bytes4(authENSLabel[:4]); @@ -231,33 +247,29 @@ library LinkedAddress { namehash = keccak256(abi.encodePacked(parentNamehash, keccak256(bytes(name)))); } - function _computeReverseNamehash(address _address) private view returns (bytes32 namehash) { - namehash = 0x0000000000000000000000000000000000000000000000000000000000000000; - namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked("reverse")))); - namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked("addr")))); - namehash = keccak256( - abi.encodePacked(namehash, keccak256(_addressToStringLowercase(_address))) - ); - } + // _computeNamehash('addr.reverse') + bytes32 constant ADDR_REVERSE_NODE = + 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; - function _addressToStringLowercase(address _address) - private - pure - returns (bytes memory addressString) - { - addressString = new bytes(40); - for (uint256 i = 0; i < 20; i++) { - bytes1 b = bytes1(uint8(uint256(uint160(_address)) / (2**(8 * (19 - i))))); - bytes1 hi = bytes1(uint8(b) / 16); - bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); - addressString[2 * i] = _bytes1ToChar(hi); - addressString[2 * i + 1] = _bytes1ToChar(lo); - } + function _computeReverseNamehash(address _address) private pure returns (bytes32 namehash) { + namehash = keccak256(abi.encodePacked(ADDR_REVERSE_NODE, sha3HexAddress(_address))); } - function _bytes1ToChar(bytes1 b) private pure returns (bytes1 c) { - if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); - else return bytes1(uint8(b) + 0x57); + function sha3HexAddress(address addr) private pure returns (bytes32 ret) { + assembly { + let lookup := 0x3031323334353637383961626364656600000000000000000000000000000000 + let i := 40 + for { + + } gt(i, 0) { + + } { + i := sub(i, 1) + mstore8(i, byte(and(addr, 0xf), lookup)) + addr := div(addr, 0x10) + } + ret := keccak256(0, 40) + } } } ``` From 1c3abd70e0e3b0bdeb5097bbe82ca726168d11f4 Mon Sep 17 00:00:00 2001 From: wwhchung Date: Fri, 3 Jun 2022 16:00:13 -0700 Subject: [PATCH 16/36] Update eip-5131.md --- EIPS/eip-5131.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index ca2af17795d64b..34c84a5b1423d6 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -79,8 +79,8 @@ This is made possible because the attendee completed the necessary upfront autho ### Example 2: Multiple Devices I have multiple devices and wish to sign with any of them for convenience. In this situation: - - My iPhone has mobile metamask hot wallet, and I can add it as an auth account by setting auth1.wilkins.eth to that wallet (and the corresponding reverse record). - - My iPad has the same and can be set to auth2.wilkins.eth, etc. + - My phone has mobile metamask hot wallet, and I can add it as an auth account by setting auth1.wilkins.eth to that wallet (and the corresponding reverse record). + - My tablet has the same and can be set to auth2.wilkins.eth, etc. Lost Access to Signing Key/Device: In the event that a user losses access to the signing key of an `authAddress` they can simply delete the corresponding ENS record. An attractive side effect of this is that there is no need to import/reimport seed phrases. @@ -119,7 +119,7 @@ If your application operates a secure backend server, you could run the client/s #### Without a secure server (web client only) Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. -`````` +``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; @@ -279,4 +279,4 @@ The core purpose of this EIP is to enhance security and promote a safer way to a ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file +Copyright and related rights waived via [CC0](../LICENSE.md). From 54a20ef23f9d48ad29a629a2d13ec56e5ab661b3 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Sun, 5 Jun 2022 10:18:08 -0700 Subject: [PATCH 17/36] add additional code for sample implementation --- EIPS/eip-5131.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 34c84a5b1423d6..7ad4302a8ae74a 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -92,6 +92,11 @@ In the unfortunate event that you lose access to the signing key of the `mainAdd ### Client/Server Side In typescript, the validation function, using ethers.js would be as follows: ``` +export interface LinkedAddress { + ens: string, + address: string, +} + async function getLinkedAddress(provider: ethers.providers.Provider, address: string): Promise { const addressENS = await provider.lookupAddress(address); if (!addressENS) return null; From 00458dfa796eda7fefd9ec7cef0240b45f49c188 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Mon, 6 Jun 2022 07:20:51 -0700 Subject: [PATCH 18/36] clean up Abstract and move contents into Motivation --- EIPS/eip-5131.md | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 7ad4302a8ae74a..6ea6f0920d7ddd 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -12,27 +12,28 @@ requires: 137 --- ## Abstract -Proving the ownership of an asset to a third party application in the Ethereum ecosystem is a common occurrence. Users frequently sign payloads of data presented to them via these applications in order to prove ownership. The process of proving ownership of an address is commonly used to authenticate users before allowing them to perform some operation. +This EIP uses the Ethereum Name Service Specification (EIP-137) as a way to link one or more signing wallets (presumably less secure) to prove control and asset ownership of a main wallet. -Examples: +## Motivation +Proving the ownership of an asset to a third party application in the Ethereum ecosystem is a common occurrence. Users frequently sign payloads of data presented to them via these applications in order to prove ownership. The process of proving ownership of an address is commonly used to authenticate users before allowing them to perform some operation. However, the current method of using a person's main wallet for authentication of control and asset ownership is both insecure and inconvenient. It is as if, in order to authenticate, you must give an application root access to your wallet. + +*** Examples: *** - In order for you to edit your profile on OpenSea, you must sign a message with your wallet address. - In order to access NFT gated content, you must sign a message with the wallet containing the NFT. - In order to claim an airdrop, you must interact with the smart contract with the qualifying wallet address. - In order to prove ownership of an NFT, you must sign a payload with the address that owns that NFT. +### Problems with existing methods and solutions ### Unfortunately, we've seen many cases where users have accidentally signed a malicious payload. The result is almost always a significant loss of assets associated with the signing address. In addition to this, many users keep significant portions of their assets in 'cold storage'. With the increased security from 'cold storage' solutions, we usually see decreased accessibility because users naturally increase the barriers required to access these wallets. -This EIP proposes a solution which uses the Ethereum Name Service Specification (EIP-137) as a way to link one or more signing wallets (presumably less secure) to authenticate a main wallet. - -From there, the benefits are twofold. This EIP gives users increased security via outsourcing potentially malicious signing operations to wallets that are more accessible (hot wallets), while being able to maintain the intended security assumptions of wallets that are not frequently used for signing operations. +Some solutions propose dedicated registry smart contracts to create this link, or new protocols to be supported. This is problematic from an adoption standpoint, and there have not been any standards created for them. +### Proposal: Use the Ethereum Name Service (EIP-137) ### +Rather than 're-invent the wheel', this proposal aims to use the widely adopted Ethereum Name Service in order to bootstrap a safer and more convenient way to sign and authenticate, and provide 'read only' access to a main wallet. -## Motivation -As explained in the abstract, the current method of using a person's main wallet for authentication of control and asset ownership is both insecure and inconvenient. It is as if, in order to authenticate, you must give an application root access to your wallet. This proposal provides an easy way to do 'read only' access to a wallet by leveraging existing infrastructure. - -Some solutions propose dedicated registry smart contracts to create this link, or new protocols to be supported. Rather than 're-invent the wheel', this proposal aims to use the widely adopted Ethereum Name Service in order to bootstrap a safer and more convenient way to sign and authenticate. +From there, the benefits are twofold. This EIP gives users increased security via outsourcing potentially malicious signing operations to wallets that are more accessible (hot wallets), while being able to maintain the intended security assumptions of wallets that are not frequently used for signing operations. ## Specification @@ -43,13 +44,13 @@ Let: - `mainAddress` represents the wallet address we are trying to authenticate or prove asset ownership for. - `mainENS` represents the reverse lookup ENS string for `mainAddress`. - `authAddress` represents the address we want to use for signing in lieu of `mainAddress`. - - `authENS` represents the reverse lookup ENS string for `authAddress`. It must be in the format `auth[0-9]*.`. + - `authENS` represents the reverse lookup ENS string for `authAddress`. It must be in the format `auth[0-9A-za-z]*.`. ### Setting up one or many `authAddress` records The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record and reverse record configured. -1. Using your `mainAddress` wallet, sign into ens.domains and create a subdomain record for `mainENS` called `auth[0-9]*`. This becomes the `authENS` +1. Using your `mainAddress` wallet, sign into ens.domains and create a subdomain record for `mainENS` called `auth[0-9A-za-z]*`. This becomes the `authENS` 2. Set the ETH resolver record for the subdomain created in step 1 (`authENS`) to the `authAddress`. 3. Using `authAddress`, sign into ens.domains and set the ENS reverse record to `authENS` @@ -60,7 +61,7 @@ Control of `mainAddress` and ownership of `mainAddress` assets is proven if any Practically, this would work by performing the following operations: 1. Get the reverse ENS record for `authAddress` -2. Parse `auth[0-9]*.` to determine the linked ENS +2. Parse `auth[0-9A-za-z]*.` to determine the linked ENS 3. Do a lookup on the linked ENS record to determine the linked `mainAddress` 4. MUST get the reverse ENS record for `mainAddress` and verify that it matches `` - Otherwise one could set up other ENS nodes (with auths) that point to mainAddress and authenticate via those. @@ -101,7 +102,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st const addressENS = await provider.lookupAddress(address); if (!addressENS) return null; - const authMatch = addressENS.match(/^(auth[0-9]*)\.(.*)/); + const authMatch = addressENS.match(/^(auth[0-9A-za-z]*)\.(.*)/); if (!authMatch) return null; const linkedENS = authMatch[2]; @@ -153,7 +154,7 @@ library LinkedAddress { /** * Validate that the message sender is an authentication address for mainAddress * @param ensRegistry Address of ENS registry - * @param authENSLabel The ENS label of the authentication wallet (must be `auth[0-9]*`) + * @param authENSLabel The ENS label of the authentication wallet (must be `auth[0-9A-Za-z]*`) * @param mainAddress The main address we want to authenticate for. * @param mainENSParts The array of the main address ENS domain parts (e.g. wilkins.eth == ['wilkins', 'eth']). * This is used vs. the full ENS a a single string name hash computations are gas efficient. @@ -172,7 +173,7 @@ library LinkedAddress { * * @param ensRegistry Address of ENS registry * @param authAddress The address of the authentication wallet - * @param authENSLabel The ENS label of the authentication wallet (must be `auth[0-9]*`) + * @param authENSLabel The ENS label of the authentication wallet (must be `auth[0-9A-Za-z]*`) * @param mainAddress The main address we want to authenticate for. * @param mainENSParts The array of the main address ENS domain parts (e.g. wilkins.eth == ['wilkins', 'eth']). * This is used vs. the full ENS a a single string name hash computations are gas efficient. @@ -215,12 +216,18 @@ library LinkedAddress { require(authResolver != address(0), "Auth ENS not registed"); require(authAddress == Resolver(authResolver).addr(authNameHash), "Not authenticated"); - // Check that the subdomain name has the correct format auth[0-9]*. + // Check that the subdomain name has the correct format auth[0-9A-Za-z]*. bytes4 authPart = bytes4(authENSLabel[:4]); require(authPart == "auth", "Invalid prefix"); unchecked { for (uint256 i = authENSLabel.length; i > 4; i--) { - require(authENSLabel[i] >= 0x30 && authENSLabel[i] <= 0x39, "Invalid char"); + bytes1 char = authENSLabel[i]; + require( + (char >= 0x30 && char <= 0x39) || + (char >= 0x41 && char <= 0x5A) || + (char >= 0x61 && char <= 0x7A), + "Invalid char" + ); } } From 47558a2367656803b318766c6e6e5935796bf941 Mon Sep 17 00:00:00 2001 From: wwhchung Date: Mon, 6 Jun 2022 13:29:07 -0700 Subject: [PATCH 19/36] Update eip-5131.md --- EIPS/eip-5131.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 6ea6f0920d7ddd..b978903a3bd384 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -50,7 +50,7 @@ Let: ### Setting up one or many `authAddress` records The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record and reverse record configured. -1. Using your `mainAddress` wallet, sign into ens.domains and create a subdomain record for `mainENS` called `auth[0-9A-za-z]*`. This becomes the `authENS` +1. Using your `mainAddress` wallet, sign into ens.domains and create a subdomain record for `mainENS` called `auth[0-9A-Za-z]*`. This becomes the `authENS` 2. Set the ETH resolver record for the subdomain created in step 1 (`authENS`) to the `authAddress`. 3. Using `authAddress`, sign into ens.domains and set the ENS reverse record to `authENS` @@ -61,10 +61,10 @@ Control of `mainAddress` and ownership of `mainAddress` assets is proven if any Practically, this would work by performing the following operations: 1. Get the reverse ENS record for `authAddress` -2. Parse `auth[0-9A-za-z]*.` to determine the linked ENS +2. Parse `auth[0-9A-Za-z]*.` to determine the linked ENS 3. Do a lookup on the linked ENS record to determine the linked `mainAddress` 4. MUST get the reverse ENS record for `mainAddress` and verify that it matches `` - - Otherwise one could set up other ENS nodes (with auths) that point to mainAddress and authenticate via those. + - Otherwise one could set up other ENS nodes (with auths) that point to `mainAddress` and authenticate via those. Note that this specification allows for both contract level and client/server side validation of signatures. It is not limited to smart contracts, which is why there is no proposed external interface definition. @@ -102,7 +102,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st const addressENS = await provider.lookupAddress(address); if (!addressENS) return null; - const authMatch = addressENS.match(/^(auth[0-9A-za-z]*)\.(.*)/); + const authMatch = addressENS.match(/^(auth[0-9A-Za-z]*)\.(.*)/); if (!authMatch) return null; const linkedENS = authMatch[2]; @@ -119,7 +119,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st ### Contract side -### With a secure server +#### With a secure server If your application operates a secure backend server, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1271](./eip-1271.md) : `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. #### Without a secure server (web client only) From 8360e7eeed3e3fe7aa47ebc6c8fbc1a723431563 Mon Sep 17 00:00:00 2001 From: wwhchung Date: Fri, 10 Jun 2022 11:44:36 -0700 Subject: [PATCH 20/36] Update EIPS/eip-5131.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index b978903a3bd384..f4ce0a670cf19e 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -12,7 +12,7 @@ requires: 137 --- ## Abstract -This EIP uses the Ethereum Name Service Specification (EIP-137) as a way to link one or more signing wallets (presumably less secure) to prove control and asset ownership of a main wallet. +This EIP uses the Ethereum Name Service Specification ([EIP-137](./eip-137.md)) as a way to link one or more signing wallets (presumably less secure) to prove control and asset ownership of a main wallet. ## Motivation Proving the ownership of an asset to a third party application in the Ethereum ecosystem is a common occurrence. Users frequently sign payloads of data presented to them via these applications in order to prove ownership. The process of proving ownership of an address is commonly used to authenticate users before allowing them to perform some operation. However, the current method of using a person's main wallet for authentication of control and asset ownership is both insecure and inconvenient. It is as if, in order to authenticate, you must give an application root access to your wallet. From bb9dace2e51387ef77ea03dc42e9340eb28d86a1 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Fri, 10 Jun 2022 11:47:46 -0700 Subject: [PATCH 21/36] edits --- EIPS/eip-5131.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index f4ce0a670cf19e..efb3e988630df5 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -23,14 +23,14 @@ Proving the ownership of an asset to a third party application in the Ethereum e - In order to claim an airdrop, you must interact with the smart contract with the qualifying wallet address. - In order to prove ownership of an NFT, you must sign a payload with the address that owns that NFT. -### Problems with existing methods and solutions ### +### Problems with existing methods and solutions Unfortunately, we've seen many cases where users have accidentally signed a malicious payload. The result is almost always a significant loss of assets associated with the signing address. In addition to this, many users keep significant portions of their assets in 'cold storage'. With the increased security from 'cold storage' solutions, we usually see decreased accessibility because users naturally increase the barriers required to access these wallets. Some solutions propose dedicated registry smart contracts to create this link, or new protocols to be supported. This is problematic from an adoption standpoint, and there have not been any standards created for them. -### Proposal: Use the Ethereum Name Service (EIP-137) ### +### Proposal: Use the Ethereum Name Service (EIP-137) Rather than 're-invent the wheel', this proposal aims to use the widely adopted Ethereum Name Service in order to bootstrap a safer and more convenient way to sign and authenticate, and provide 'read only' access to a main wallet. From there, the benefits are twofold. This EIP gives users increased security via outsourcing potentially malicious signing operations to wallets that are more accessible (hot wallets), while being able to maintain the intended security assumptions of wallets that are not frequently used for signing operations. @@ -41,18 +41,18 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL Let: - - `mainAddress` represents the wallet address we are trying to authenticate or prove asset ownership for. - - `mainENS` represents the reverse lookup ENS string for `mainAddress`. - - `authAddress` represents the address we want to use for signing in lieu of `mainAddress`. - - `authENS` represents the reverse lookup ENS string for `authAddress`. It must be in the format `auth[0-9A-za-z]*.`. + - `mainAddress` represent the wallet address we are trying to authenticate or prove asset ownership for. + - `mainENS` represent the reverse lookup ENS string for `mainAddress`. + - `authAddress` represent the address we want to use for signing in lieu of `mainAddress`. + - `authENS` represent the reverse lookup ENS string for `authAddress`. It must be in the format `auth[0-9A-za-z]*.`. ### Setting up one or many `authAddress` records The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record and reverse record configured. -1. Using your `mainAddress` wallet, sign into ens.domains and create a subdomain record for `mainENS` called `auth[0-9A-Za-z]*`. This becomes the `authENS` +1. Using your `mainAddress` wallet create a subdomain record for `mainENS` called `auth[0-9A-Za-z]*`. This becomes the `authENS` 2. Set the ETH resolver record for the subdomain created in step 1 (`authENS`) to the `authAddress`. -3. Using `authAddress`, sign into ens.domains and set the ENS reverse record to `authENS` +3. Using `authAddress`, set the ENS reverse record to `authENS` Currently this EIP does not enforce an upper-bound on the number of `authAddress` entries you can include. Users can repeat this process with as many address as they like. From fc0209892a778f675ab6de312f32edd4dff0f2bc Mon Sep 17 00:00:00 2001 From: wwhchung Date: Mon, 13 Jun 2022 08:44:18 -0700 Subject: [PATCH 22/36] Update EIPS/eip-5131.md Co-authored-by: Daniel Tedesco --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index efb3e988630df5..3000ffabe981d3 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -12,7 +12,7 @@ requires: 137 --- ## Abstract -This EIP uses the Ethereum Name Service Specification ([EIP-137](./eip-137.md)) as a way to link one or more signing wallets (presumably less secure) to prove control and asset ownership of a main wallet. +This EIP links one or more signing wallets via Ethereum Name Service Specification ([EIP-137](./eip-137.md)) to prove control and asset ownership of a main wallet. ## Motivation Proving the ownership of an asset to a third party application in the Ethereum ecosystem is a common occurrence. Users frequently sign payloads of data presented to them via these applications in order to prove ownership. The process of proving ownership of an address is commonly used to authenticate users before allowing them to perform some operation. However, the current method of using a person's main wallet for authentication of control and asset ownership is both insecure and inconvenient. It is as if, in order to authenticate, you must give an application root access to your wallet. From 248dad3e02b381867c82b93c39c9eeaf15afe80a Mon Sep 17 00:00:00 2001 From: wwhchung Date: Mon, 13 Jun 2022 08:44:37 -0700 Subject: [PATCH 23/36] Update EIPS/eip-5131.md Co-authored-by: Daniel Tedesco --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 3000ffabe981d3..2ea01ed78c7561 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -15,7 +15,7 @@ requires: 137 This EIP links one or more signing wallets via Ethereum Name Service Specification ([EIP-137](./eip-137.md)) to prove control and asset ownership of a main wallet. ## Motivation -Proving the ownership of an asset to a third party application in the Ethereum ecosystem is a common occurrence. Users frequently sign payloads of data presented to them via these applications in order to prove ownership. The process of proving ownership of an address is commonly used to authenticate users before allowing them to perform some operation. However, the current method of using a person's main wallet for authentication of control and asset ownership is both insecure and inconvenient. It is as if, in order to authenticate, you must give an application root access to your wallet. +Proving ownership of an asset to a third party application in the Ethereum ecosystem is common. Users frequently sign payloads of data to authenticate themselves before gaining access to perform some operation. However, this method--akin to giving the third party root access to one's main wallet--is both insecure and inconvenient. *** Examples: *** - In order for you to edit your profile on OpenSea, you must sign a message with your wallet address. From c1b51afd578e0b744db177f4d8561048bafc2c8d Mon Sep 17 00:00:00 2001 From: wwhchung Date: Tue, 14 Jun 2022 09:00:20 -0700 Subject: [PATCH 24/36] Update EIPS/eip-5131.md Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 2ea01ed78c7561..49ca7ba69e4707 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -15,7 +15,7 @@ requires: 137 This EIP links one or more signing wallets via Ethereum Name Service Specification ([EIP-137](./eip-137.md)) to prove control and asset ownership of a main wallet. ## Motivation -Proving ownership of an asset to a third party application in the Ethereum ecosystem is common. Users frequently sign payloads of data to authenticate themselves before gaining access to perform some operation. However, this method--akin to giving the third party root access to one's main wallet--is both insecure and inconvenient. +Proving ownership of an asset to a third party application in the Ethereum ecosystem is common. Users frequently sign payloads of data to authenticate themselves before gaining access to perform some operation. However, this method--akin to giving the third party root access to one's main wallet--is both insecure and inconvenient. *** Examples: *** - In order for you to edit your profile on OpenSea, you must sign a message with your wallet address. From b59c29249725d1c19e2e08e935e67a464c9b4a15 Mon Sep 17 00:00:00 2001 From: wwhchung Date: Tue, 14 Jun 2022 09:00:29 -0700 Subject: [PATCH 25/36] Update EIPS/eip-5131.md Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 49ca7ba69e4707..e54c5f8fe846b0 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -213,7 +213,7 @@ library LinkedAddress { bytes32 authNameHash = _computeNamehash(mainNameHash, string(authENSLabel)); address authResolver = ENS(ensRegistry).resolver(authNameHash); - require(authResolver != address(0), "Auth ENS not registed"); + require(authResolver != address(0), "Auth ENS not registered"); require(authAddress == Resolver(authResolver).addr(authNameHash), "Not authenticated"); // Check that the subdomain name has the correct format auth[0-9A-Za-z]*. From 9bd601c7113c510f30973976cd8853bf7aa1798d Mon Sep 17 00:00:00 2001 From: wwhchung Date: Tue, 14 Jun 2022 09:00:39 -0700 Subject: [PATCH 26/36] Update EIPS/eip-5131.md Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index e54c5f8fe846b0..02f3b05fff138a 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -9,7 +9,7 @@ type: Standards Track category: ERC created: 2022-06-03 requires: 137 ---- +--- ## Abstract This EIP links one or more signing wallets via Ethereum Name Service Specification ([EIP-137](./eip-137.md)) to prove control and asset ownership of a main wallet. From 7d59810b483462e4764da0ab96ef25a32dc3ceae Mon Sep 17 00:00:00 2001 From: wwhchung Date: Tue, 14 Jun 2022 09:00:47 -0700 Subject: [PATCH 27/36] Update EIPS/eip-5131.md Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 02f3b05fff138a..e5868375f5955e 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -287,7 +287,7 @@ library LinkedAddress { ``` ## Security Considerations -The core purpose of this EIP is to enhance security and promote a safer way to authenticate wallet control and asset ownership when the main wallet is not needed and assets held by the main wallet do not need to be moved. Consider it a way to do 'read only' authentication. +The core purpose of this EIP is to enhance security and promote a safer way to authenticate wallet control and asset ownership when the main wallet is not needed and assets held by the main wallet do not need to be moved. Consider it a way to do 'read only' authentication. ## Copyright From ace2c3983e3f203666fba871a36ab27a698f40e4 Mon Sep 17 00:00:00 2001 From: wwhchung Date: Tue, 14 Jun 2022 09:00:56 -0700 Subject: [PATCH 28/36] Update EIPS/eip-5131.md Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index e5868375f5955e..381a00dafe1909 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -79,7 +79,7 @@ The attendee is able to prove ownership of the NFT by signing with the 'hot wall This is made possible because the attendee completed the necessary upfront authorization steps with the cold storage device to allow the 'hot wallet' to be used for this operation. ### Example 2: Multiple Devices -I have multiple devices and wish to sign with any of them for convenience. In this situation: +I have multiple devices and wish to sign with any of them for convenience. In this situation: - My phone has mobile metamask hot wallet, and I can add it as an auth account by setting auth1.wilkins.eth to that wallet (and the corresponding reverse record). - My tablet has the same and can be set to auth2.wilkins.eth, etc. From 8ce77e189edad29cefb4f57c9b6bb6e2befc991e Mon Sep 17 00:00:00 2001 From: wwhchung Date: Mon, 27 Jun 2022 16:35:55 -0700 Subject: [PATCH 29/36] Update EIPS/eip-5131.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 381a00dafe1909..36c4eeb3017da9 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -72,7 +72,7 @@ Note that this specification allows for both contract level and client/server si The proposed specification allows one to link multiple addresses as 'authentication addresses' to a core main address. This is beneficial from a security standpoint (if the authentication address is compromised, the assets held by the main address is not), and convenience (if the authentication address is a simple MetaMask wallet but the main address is a hardware wallet). ### Example 1: Event Access -An event requires attendees to prove ownership of an NFT to gain access. The attendee in question only has their phone available to perform a signing operation upon the presentation of a challenge. The phone is considered the attendees 'hot wallet' which is linked to auth.wilkins.eth. +An event requires attendees to prove ownership of an NFT to gain access. The attendee in question only has their phone available to perform a signing operation upon the presentation of a challenge. The phone is considered the attendee's 'hot wallet' which is linked to `auth.wilkins.eth`. The NFT is owned by the Ethereum address that corresponds to wilkins.eth, which is controlled by the attendees' cold storage device (Ledger, Trezor etc). The attendee does not have this device present at the event. The attendee is able to prove ownership of the NFT by signing with the 'hot wallet' key. From 1f7f35c2fe042fe352eef568dfccc72911e6cf41 Mon Sep 17 00:00:00 2001 From: wwhchung Date: Mon, 27 Jun 2022 16:36:03 -0700 Subject: [PATCH 30/36] Update EIPS/eip-5131.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 36c4eeb3017da9..cfd6e02642be11 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -73,7 +73,7 @@ The proposed specification allows one to link multiple addresses as 'authenticat ### Example 1: Event Access An event requires attendees to prove ownership of an NFT to gain access. The attendee in question only has their phone available to perform a signing operation upon the presentation of a challenge. The phone is considered the attendee's 'hot wallet' which is linked to `auth.wilkins.eth`. -The NFT is owned by the Ethereum address that corresponds to wilkins.eth, which is controlled by the attendees' cold storage device (Ledger, Trezor etc). The attendee does not have this device present at the event. +The NFT is owned by the Ethereum address that corresponds to `wilkins.eth`, which is controlled by the attendee's cold storage device (Ledger, Trezor etc). The attendee does not have this device present at the event. The attendee is able to prove ownership of the NFT by signing with the 'hot wallet' key. This is made possible because the attendee completed the necessary upfront authorization steps with the cold storage device to allow the 'hot wallet' to be used for this operation. From 0b880a1b9fbfad5213bed4628f54b3b8e66fabd3 Mon Sep 17 00:00:00 2001 From: wwhchung Date: Tue, 28 Jun 2022 11:26:56 -0700 Subject: [PATCH 31/36] Update EIPS/eip-5131.md Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index cfd6e02642be11..69eafd797d85a8 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -123,7 +123,7 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st If your application operates a secure backend server, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1271](./eip-1271.md) : `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. #### Without a secure server (web client only) -Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address. +Provided is a reference implementation for an internal function to verify that the message sender has an authentication link to the main address. ``` // SPDX-License-Identifier: MIT From 43f8ba426bad942ceaabed6432516f3b5417d603 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Tue, 28 Jun 2022 12:32:54 -0700 Subject: [PATCH 32/36] update Specification for EIP-5131 --- EIPS/eip-5131.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 69eafd797d85a8..0462c2fef6078c 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -47,6 +47,12 @@ Let: - `authENS` represent the reverse lookup ENS string for `authAddress`. It must be in the format `auth[0-9A-za-z]*.`. +Control of `mainAddress` and ownership of `mainAddress` assets by `authAddress` is proven if all the following conditions are met: + - `mainAddress` has an ENS ETH resolver record and a reverse record set to `mainENS`. + - `mainENS` has a subdomain record matching the format `auth[0-9A-Za-z]*.`. This subdomain record is referred to as `authENS`. + - `authAddress` has an ENS reverse record set to `authENS` + + ### Setting up one or many `authAddress` records The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record and reverse record configured. @@ -57,7 +63,7 @@ The pre-requisite assumes that the `mainAddress` has an ENS ETH resolver record Currently this EIP does not enforce an upper-bound on the number of `authAddress` entries you can include. Users can repeat this process with as many address as they like. ### Authenticating `mainAddress` via `authAddress` -Control of `mainAddress` and ownership of `mainAddress` assets is proven if any one of associated `authAddress` is the msg.sender or has signed the message. +Control of `mainAddress` and ownership of `mainAddress` assets is proven if any associated `authAddress` is the msg.sender or has signed the message. Practically, this would work by performing the following operations: 1. Get the reverse ENS record for `authAddress` From bf02f8369dba0efbb19a9079299f64441aa8eb24 Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Sun, 3 Jul 2022 07:53:01 -0700 Subject: [PATCH 33/36] update Rationale section --- EIPS/eip-5131.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 0462c2fef6078c..396107b759b8ee 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -74,17 +74,16 @@ Practically, this would work by performing the following operations: Note that this specification allows for both contract level and client/server side validation of signatures. It is not limited to smart contracts, which is why there is no proposed external interface definition. -## Rationale -The proposed specification allows one to link multiple addresses as 'authentication addresses' to a core main address. This is beneficial from a security standpoint (if the authentication address is compromised, the assets held by the main address is not), and convenience (if the authentication address is a simple MetaMask wallet but the main address is a hardware wallet). +### Example Use Cases -### Example 1: Event Access +#### Example 1: Event Access An event requires attendees to prove ownership of an NFT to gain access. The attendee in question only has their phone available to perform a signing operation upon the presentation of a challenge. The phone is considered the attendee's 'hot wallet' which is linked to `auth.wilkins.eth`. The NFT is owned by the Ethereum address that corresponds to `wilkins.eth`, which is controlled by the attendee's cold storage device (Ledger, Trezor etc). The attendee does not have this device present at the event. The attendee is able to prove ownership of the NFT by signing with the 'hot wallet' key. -This is made possible because the attendee completed the necessary upfront authorization steps with the cold storage device to allow the 'hot wallet' to be used for this operation. +This is made possible because the attendee completed the necessary upfront ENS configuration with the cold storage device to allow the 'hot wallet' to be used for this operation. -### Example 2: Multiple Devices +#### Example 2: Multiple Devices I have multiple devices and wish to sign with any of them for convenience. In this situation: - My phone has mobile metamask hot wallet, and I can add it as an auth account by setting auth1.wilkins.eth to that wallet (and the corresponding reverse record). - My tablet has the same and can be set to auth2.wilkins.eth, etc. @@ -93,6 +92,19 @@ Lost Access to Signing Key/Device: In the event that a user losses access to the signing key of an `authAddress` they can simply delete the corresponding ENS record. An attractive side effect of this is that there is no need to import/reimport seed phrases. In the unfortunate event that you lose access to the signing key of the `mainAddress`, users will lose access to that top-level domain and all related subdomains. As always, this private key data should be treated as highly sensitive. +## Rationale + +### Usage of EIP-137 +The proposed specification makes use of EIP-137 rather than introduce another registry paradigm. The reason for this is due to the existing wide adoption of EIP-137 and ENS. + +However, the drawback to EIP-137 is that any linked `authAddress` must contain some ETH in order to set the `authENS` reverse record. This can be solved by a separate reverse lookup registry that enables `mainAddress` to set a reverse record with a message signed by `authAddress`. + +### One-to-Many Authenitcation Relationship +This proposed specification allows for a one (`mainAddress`) to many (`authAddress`) authentication relationship. i.e. one `mainAddress` can authorize many `authAddress` to authenticate, but an `authAddress` can only authenticate itself or a single `mainAddress`. + +The reason for this design choice is to allow for simplicity of authentication via client and smart contract code. You can determine which `mainAddress` the `authAddress` is signing for without any additional user input. + +Further, you can design UX without any user interaction necessary to 'pick' the interacting address by display assets owned by `authAddress` and `mainAddress` and use the appropriate address dependent on the asset the user is attempting to authenticate with. ## Reference Implementation From c3c8687d0140c5f2f57187e50c62153f2e509f4c Mon Sep 17 00:00:00 2001 From: Wilkins Chung Date: Sun, 3 Jul 2022 08:37:26 -0700 Subject: [PATCH 34/36] improve Motivation section --- EIPS/eip-5131.md | 50 ++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 396107b759b8ee..060c4e5bfe68fa 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -18,10 +18,17 @@ This EIP links one or more signing wallets via Ethereum Name Service Specificati Proving ownership of an asset to a third party application in the Ethereum ecosystem is common. Users frequently sign payloads of data to authenticate themselves before gaining access to perform some operation. However, this method--akin to giving the third party root access to one's main wallet--is both insecure and inconvenient. *** Examples: *** - - In order for you to edit your profile on OpenSea, you must sign a message with your wallet address. - - In order to access NFT gated content, you must sign a message with the wallet containing the NFT. - - In order to claim an airdrop, you must interact with the smart contract with the qualifying wallet address. - - In order to prove ownership of an NFT, you must sign a payload with the address that owns that NFT. + 1. In order for you to edit your profile on OpenSea, you must sign a message with your wallet. + 2. In order to access NFT gated content, you must sign a message with the wallet containing the NFT in order to prove ownership. + 3. In order to gain access to an event, you must sign a message with the wallet containing a required NFT in order to prove ownership. + 4. In order to claim an airdrop, you must interact with the smart contract with the qualifying wallet. + 5. In order to prove ownership of an NFT, you must sign a payload with the wallet that owns that NFT. + + In all the above examples, one interacts with the dApp or smart contract using the wallet itself, which may be + - inconvenient (if it is controlled via a hardware wallet or a multi-sig) + - insecure (since the above operations are read-only, but you are signing/interacting via a wallet that has write access) + +Instead, one should be able to approve multiple wallets to authenticate on behalf of a given wallet. ### Problems with existing methods and solutions Unfortunately, we've seen many cases where users have accidentally signed a malicious payload. The result is almost always a significant loss of assets associated with the signing address. @@ -31,10 +38,25 @@ In addition to this, many users keep significant portions of their assets in 'co Some solutions propose dedicated registry smart contracts to create this link, or new protocols to be supported. This is problematic from an adoption standpoint, and there have not been any standards created for them. ### Proposal: Use the Ethereum Name Service (EIP-137) -Rather than 're-invent the wheel', this proposal aims to use the widely adopted Ethereum Name Service in order to bootstrap a safer and more convenient way to sign and authenticate, and provide 'read only' access to a main wallet. +Rather than 're-invent the wheel', this proposal aims to use the widely adopted Ethereum Name Service in order to bootstrap a safer and more convenient way to sign and authenticate, and provide 'read only' access to a main wallet via one or more secondary wallets. From there, the benefits are twofold. This EIP gives users increased security via outsourcing potentially malicious signing operations to wallets that are more accessible (hot wallets), while being able to maintain the intended security assumptions of wallets that are not frequently used for signing operations. +#### Improving dApp Interaction Security +Many dApps requires one to prove control of a wallet to gain access. At the moment, this means that you must interact with the dApp using the wallet itself. This is a security issue, as malicious dApps or phishing sites can lead to the assets of the wallet being compromised by having them sign malicious payloads. + +However, this risk would be mitigated if one were to use a secondary wallet for these interactions. Malicious interactions would be isolated to the assets held in the secondary wallet, which can be set up to contain little to nothing of value. + +#### Improving Multiple Device Access Security +In order for a non-hardware wallet to be used on multiple devices, you must import the seed phrase to each device. Each time a seed phrase is entered on a new device, the risk of the wallet being compromised increases as you are increasing the surface area of devices that have knowledge of the seed phrase. + +Instead, each device can have its own unique wallet that is an authorized secondary wallet of the main wallet. If a device specific wallet was ever compromised or lost, you could simply remove the authorization to authenticate. + +#### Improving Convenience +Many invididuals use hardware wallets for maximum security. However, this is often inconvenient, since many do not want to carry their hardware wallet with them at all times. + +Instead, if you approve a non-hardware wallet for authentication activities (such as a mobile device), you would be able to use most dApps without the need to have your hardware wallet on hand. + ## Specification The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. @@ -74,24 +96,6 @@ Practically, this would work by performing the following operations: Note that this specification allows for both contract level and client/server side validation of signatures. It is not limited to smart contracts, which is why there is no proposed external interface definition. -### Example Use Cases - -#### Example 1: Event Access -An event requires attendees to prove ownership of an NFT to gain access. The attendee in question only has their phone available to perform a signing operation upon the presentation of a challenge. The phone is considered the attendee's 'hot wallet' which is linked to `auth.wilkins.eth`. -The NFT is owned by the Ethereum address that corresponds to `wilkins.eth`, which is controlled by the attendee's cold storage device (Ledger, Trezor etc). The attendee does not have this device present at the event. -The attendee is able to prove ownership of the NFT by signing with the 'hot wallet' key. - -This is made possible because the attendee completed the necessary upfront ENS configuration with the cold storage device to allow the 'hot wallet' to be used for this operation. - -#### Example 2: Multiple Devices -I have multiple devices and wish to sign with any of them for convenience. In this situation: - - My phone has mobile metamask hot wallet, and I can add it as an auth account by setting auth1.wilkins.eth to that wallet (and the corresponding reverse record). - - My tablet has the same and can be set to auth2.wilkins.eth, etc. - -Lost Access to Signing Key/Device: -In the event that a user losses access to the signing key of an `authAddress` they can simply delete the corresponding ENS record. An attractive side effect of this is that there is no need to import/reimport seed phrases. -In the unfortunate event that you lose access to the signing key of the `mainAddress`, users will lose access to that top-level domain and all related subdomains. As always, this private key data should be treated as highly sensitive. - ## Rationale ### Usage of EIP-137 From a9fe2e61b0eb24339d26273c4b9dd3d8fff54d5d Mon Sep 17 00:00:00 2001 From: wwhchung Date: Fri, 8 Jul 2022 08:34:58 -0700 Subject: [PATCH 35/36] Update EIPS/eip-5131.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5131.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 060c4e5bfe68fa..86818d38c13f71 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -141,10 +141,10 @@ async function getLinkedAddress(provider: ethers.providers.Provider, address: st ### Contract side -#### With a secure server +#### With a backend If your application operates a secure backend server, you could run the client/server code above, then use the result in conjunction with specs like [EIP-1271](./eip-1271.md) : `Standard Signature Validation Method for Contracts` for a cheap and secure way to validate that the the message signer is indeed authenticated for the main address. -#### Without a secure server (web client only) +#### Without a backend (JavaScript only) Provided is a reference implementation for an internal function to verify that the message sender has an authentication link to the main address. ``` From a7c605a57fe9815b7ec07da9a4f26521872e2eb9 Mon Sep 17 00:00:00 2001 From: wwhchung Date: Sat, 9 Jul 2022 14:18:43 -0700 Subject: [PATCH 36/36] Update EIPS/eip-5131.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5131.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5131.md b/EIPS/eip-5131.md index 86818d38c13f71..c7ee0e5b131768 100644 --- a/EIPS/eip-5131.md +++ b/EIPS/eip-5131.md @@ -103,7 +103,7 @@ The proposed specification makes use of EIP-137 rather than introduce another re However, the drawback to EIP-137 is that any linked `authAddress` must contain some ETH in order to set the `authENS` reverse record. This can be solved by a separate reverse lookup registry that enables `mainAddress` to set a reverse record with a message signed by `authAddress`. -### One-to-Many Authenitcation Relationship +### One-to-Many Authentication Relationship This proposed specification allows for a one (`mainAddress`) to many (`authAddress`) authentication relationship. i.e. one `mainAddress` can authorize many `authAddress` to authenticate, but an `authAddress` can only authenticate itself or a single `mainAddress`. The reason for this design choice is to allow for simplicity of authentication via client and smart contract code. You can determine which `mainAddress` the `authAddress` is signing for without any additional user input.