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 all 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
316 changes: 316 additions & 0 deletions EIPS/eip-5131.md
@@ -0,0 +1,316 @@
---
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-5131-ens-subdomain-authentication/9458
status: Draft
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.

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

*** Examples: ***

Choose a reason for hiding this comment

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

This EIP seems highly useful given these examples. At the moment there is a real pain point of holding assets securely while still being able to prove ownership via a hot wallet especially in mobile settings. I'd be excited to see this proposal go through

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.

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


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


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

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`, 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.

### Authenticating `mainAddress` via `authAddress`
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`
2. Parse `auth[0-9A-Za-z]*.<mainENS>` 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 `<mainENS>`
- 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.
wwhchung marked this conversation as resolved.
Show resolved Hide resolved

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

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

### 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<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-9A-Za-z]*)\.(.*)/);
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 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 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.

```
// 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
*/
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-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.
*/
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-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.
*/
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), "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
{
uint256 len = mainENSParts.length;
bytes memory ensCheckBuffer = bytes(mainENSParts[0]);
unchecked {
for (uint256 idx = 1; idx < len; ++idx) {
ensCheckBuffer = abi.encodePacked(ensCheckBuffer, ".", mainENSParts[idx]);
}
}
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 registered");
require(authAddress == Resolver(authResolver).addr(authNameHash), "Not authenticated");

// 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--) {
bytes1 char = authENSLabel[i];
require(
(char >= 0x30 && char <= 0x39) ||
(char >= 0x41 && char <= 0x5A) ||
(char >= 0x61 && char <= 0x7A),
"Invalid char"
);
}
}

return true;
}

// *********************
// Helper Functions
// *********************

function _computeNamehash(string[] calldata _nameParts)
private
pure
returns (bytes32 namehash)
{
namehash = 0x0000000000000000000000000000000000000000000000000000000000000000;
unchecked {
for (uint256 i = _nameParts.length; i > 0; --i) {
namehash = _computeNamehash(namehash, _nameParts[i - 1]);
}
}
}

function _computeNamehash(bytes32 parentNamehash, string calldata name)
private
pure
returns (bytes32 namehash)
{
namehash = keccak256(abi.encodePacked(parentNamehash, keccak256(bytes(name))));
}

// _computeNamehash('addr.reverse')
bytes32 constant ADDR_REVERSE_NODE =
0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2;

function _computeReverseNamehash(address _address) private pure returns (bytes32 namehash) {
namehash = keccak256(abi.encodePacked(ADDR_REVERSE_NODE, sha3HexAddress(_address)));
}

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)
}
}
}
```

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