Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EIP-5131: ENS Subdomain Authentication #5131

Merged
merged 36 commits into from
Jul 9, 2022
Merged
Changes from 8 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
02fdd31
Add EIP-603: ENS Auth Linking
wwhchung Jun 3, 2022
d926f0a
rename eip-603 to eip-5131 and remove external links
wwhchung Jun 3, 2022
3e3b45c
changing to an ERC
wwhchung Jun 3, 2022
069848e
additional context
wwhchung Jun 3, 2022
03d63ba
add refrence implementations
wwhchung Jun 3, 2022
31fd495
rename
wwhchung Jun 3, 2022
d779068
edits as per suggestions
wwhchung Jun 3, 2022
a24fa63
further content edits
wwhchung Jun 3, 2022
e51e8ed
grammar fix
wwhchung Jun 3, 2022
5676cce
short description change
wwhchung Jun 3, 2022
3c70189
update discussion link
wwhchung Jun 3, 2022
c0c8ded
update ref implementation
wwhchung Jun 3, 2022
713c03f
update with new reference code
wwhchung Jun 3, 2022
51a0753
format edits
wwhchung Jun 3, 2022
dc3ad8a
update reference implementation
wwhchung Jun 3, 2022
1c3abd7
Update eip-5131.md
wwhchung Jun 3, 2022
54a20ef
add additional code for sample implementation
wwhchung Jun 5, 2022
00458df
clean up Abstract and move contents into Motivation
wwhchung Jun 6, 2022
47558a2
Update eip-5131.md
wwhchung Jun 6, 2022
8360e7e
Update EIPS/eip-5131.md
wwhchung Jun 10, 2022
bb9dace
edits
wwhchung Jun 10, 2022
fc02098
Update EIPS/eip-5131.md
wwhchung Jun 13, 2022
248dad3
Update EIPS/eip-5131.md
wwhchung Jun 13, 2022
c1b51af
Update EIPS/eip-5131.md
wwhchung Jun 14, 2022
b59c292
Update EIPS/eip-5131.md
wwhchung Jun 14, 2022
9bd601c
Update EIPS/eip-5131.md
wwhchung Jun 14, 2022
7d59810
Update EIPS/eip-5131.md
wwhchung Jun 14, 2022
ace2c39
Update EIPS/eip-5131.md
wwhchung Jun 14, 2022
8ce77e1
Update EIPS/eip-5131.md
wwhchung Jun 27, 2022
1f7f35c
Update EIPS/eip-5131.md
wwhchung Jun 27, 2022
0b880a1
Update EIPS/eip-5131.md
wwhchung Jun 28, 2022
43f8ba4
update Specification for EIP-5131
wwhchung Jun 28, 2022
bf02f83
update Rationale section
wwhchung Jul 3, 2022
c3c8687
improve Motivation section
wwhchung Jul 3, 2022
a9fe2e6
Update EIPS/eip-5131.md
wwhchung Jul 8, 2022
a7c605a
Update EIPS/eip-5131.md
wwhchung Jul 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions EIPS/eip-5131.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
---
eip: 5131
title: ENS Subdomain Authentication
description: Use ENS to link hot wallets as approved signers for a root ENS address.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
author: Wilkins Chung (@wwhchung)
discussions-to: https://ethereum-magicians.org/t/eip-603-ens-authentication-link/9458
status: Draft
type: Standards Track
category: ERC
created: 2022-06-03
requires: 137
---
wwhchung marked this conversation as resolved.
Show resolved Hide resolved

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

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.

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.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved

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.
Copy link
Contributor

@MicahZoltu MicahZoltu Jun 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this sounds like motivation, rather than abstract. The abstract should be a simplified and human readable description of the specification and should not include commentary on "why".



## 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.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved

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.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved


## 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` represents the wallet address we are trying to authenticate or prove asset ownership for.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
- `mainENS` represents the reverse lookup ENS string for `mainAddress`.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be beneficial for the readers understanding to underline the importance of the reverse record here. Maybe adding something like

This is crucial because it acts as proof that `mainENS` is operated by or on behalf of the `mainAddress` owner.

- `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]*.<mainENS>`.


### 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`
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
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.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved

### 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]*.<mainENS>` 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.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
wwhchung marked this conversation as resolved.
Show resolved Hide resolved

## 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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think this section is reading more like motivation than rationale. I think you should consider moving a fair bit of this content above and focus on answering why certain design decisions were chosen over other decisions here.


### Example:
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
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).
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
- My iPad 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.


## Reference Implementation

### 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<LinkedAddress | null> {
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
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
};
}
```

### Contract side

### With a secure server
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
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.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved

#### Without a secure server (web client only)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have trouble understanding what is meant by secure server. Does this refer to the node provider? Otherwise, I could run the above js code also on a web client.

Provided is a refrence implementation for an internal function to verify that the message sender has an authentication link to the main address.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved

``````
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
// 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
* @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) 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);
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
}
}

function _bytes1ToChar(bytes1 b) private pure returns (bytes1 c) {
if (uint8(b) < 10) return bytes1(uint8(b) + 0x30);
else return bytes1(uint8(b) + 0x57);
}

}
```

## 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.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
wwhchung marked this conversation as resolved.
Show resolved Hide resolved
wwhchung marked this conversation as resolved.
Show resolved Hide resolved


## Copyright
Copyright and related rights waived via [CC0](../LICENSE.md).