The primary purpose of this document is to define how Ethereum accounts authenticate with off-chain services. By signing a standard message format parameterized by scope, session details, and a nonce.
While decentralized identity is not a novel concept, the most common implementations of blockchain-based credentials are either certificate-based or rely on centralized providers. We're proposing an alternative that doesn't require a trusted third party.
The specification for Sign In With Ethereum is based on https://eips.ethereum.org/EIPS/eip-4361 with the intention to make it compatible with https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-74.md
The message created follows the following structure :-
header: Headers;
payload: Payload;
signature: Signature;
/** RFC 4501 dns authority that is requesting the signing. */
domain: string;
/** Ethereum address performing the signing */
address: string;
/** Human-readable ASCII assertion that the user will sign, and it must not contain newline characters. */
statement?: string;
/** RFC 3986 URI referring to the resource that is the subject of the signing
* (as in the __subject__ of a claim). */
uri: string;
/** Current version of the message. */
version: string;
/** Chain ID to which the session is bound, and the network where
* Contract Accounts must be resolved. */
chainId?: number;
/** Randomized token used to prevent replay attacks, at least 8 alphanumeric
* characters. */
nonce: string;
/** ISO 8601 datetime string of the current time. */
issuedAt: string;
/** ISO 8601 datetime string that, if present, indicates when the signed
* authentication message is no longer valid. */
expirationTime?: string;
/** ISO 8601 datetime string that, if present, indicates when the signed
* authentication message will become valid. */
notBefore?: string;
/** System-specific identifier that may be used to uniquely refer to the
* sign-in request. */
requestId?: string;
/** List of information or references to information the user wishes to have
* resolved as part of authentication by the relying party. They are
* expressed as RFC 3986 URIs separated by `\n- `. */
resources?: Array<string>;
t: string; // signature scheme
m?: SignatureMeta; // signature related metadata (optional)
s: string; // signature
A sample sign in message would look like :-
localhost:8080 wants you to sign in with your Ethereum account:
4Cw1koUQtqybLFem7uqhzMBznMPGARbFS4cjaYbM9RnR
Sign in with Ethereum to the app.
URI: http://localhost:8080
Version: 1
Chain ID: 1
Nonce: zNTPldYfb8ESmhPmL
Issued At: 2022-04-25T14:51:12.040Z
- The user connects the wallet to the website.
- From the frontend pass the domain, address, statement, uri, version, nonce, issuedAt, expirationTime, notBefore, requestId, resources (Array) to the SignInWithEthereumMessage constructor. There is additional regex validation in place as mentioned in the below sections
- Nonce is needed as a security mechanism from replay attacks and hence it is generated at the server side.
- The created message needs to be prepared in a wallet friendly format for which .prepareMessage() needs to be called
- The resultant has to be passed to signMessage method of window.ethereum.request
- This function would return the signedMessage
Each field specified in the Specification section needs to follow the following regex rules
DOMAIN = "(?<domain>([^?#]*)) wants you to sign in with your Ethereum account:";
const ADDRESS = "\\n(?<address>0x[a-zA-Z0-9]{40})\\n\\n";
STATEMENT = "((?<statement>[^\\n]+)\\n)?";
URI = "(([^:?#]+):)?(([^?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))";
URI_LINE = `\\nURI: (?<uri>${URI}?)`;
VERSION = "\\nVersion: (?<version>1)";
CHAIN_ID = "\\nChain ID: (?<chainId>[0-9]+)";
NONCE = "\\nNonce: (?<nonce>[a-zA-Z0-9]{8,})";
DATETIME = `([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(.[0-9]+)?(([Zz])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))`;
ISSUED_AT = `\\nIssued At: (?<issuedAt>${DATETIME})`;
EXPIRATION_TIME = `(\\nExpiration Time: (?<expirationTime>${DATETIME}))?`;
NOT_BEFORE = `(\\nNot Before: (?<notBefore>${DATETIME}))?`;
REQUEST_ID = "(\\nRequest ID: (?<requestId>[-._~!$&'()*+,;=:@%a-zA-Z0-9]*))?";
RESOURCES = `(\\nResources:(?<resources>(\\n- ${URI}?)+))?`;
The verify function takes in the following params (as VerifyParams)
signature
publicKey
domain
nonce
time (optional)
There are certain checks in place such as invalid domain check,nonce binding check, expiry checks etc
import nacl from "tweetnacl";
.
.
nacl.sign.detached.verify(encodedMessage, base58.decode(signature), base58.decode(publicKey))
If this function returns a true value then it is a valid signature
We haven't undergone a Formal Security Audit yet.