diff --git a/ERCS/erc-4361.md b/ERCS/erc-4361.md index a2bd2284f7f..720f7832cb8 100644 --- a/ERCS/erc-4361.md +++ b/ERCS/erc-4361.md @@ -29,16 +29,18 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S Sign-In with Ethereum (SIWE) works as follows: -1. The relying party generates a SIWE Message and prefixes the SIWE Message with `\x19Ethereum Signed Message:\n` as defined in [ERC-191](./eip-191.md). -2. The wallet presents the user with a structured plaintext message or equivalent interface for signing the SIWE Message with the [ERC-191](./eip-191.md) signed data format. +1. The relying party generates a SIWE Message (the plaintext payload, without any prefix) and transmits it to the wallet. +2. The wallet presents the user with a structured plaintext message or equivalent interface, and, upon user consent, signs the SIWE Message using the [ERC-191](./erc-191.md) signed data format. The ERC-191 prefix (`\x19Ethereum Signed Message:\n`) is applied by the signing primitive exactly once; the relying party MUST NOT pre-prefix the message. 3. The signature is then presented to the relying party, which checks the signature's validity and SIWE Message content. -4. The relying party might further fetch data associated with the Ethereum address, such as from the Ethereum blockchain (e.g., ENS, account balances, [ERC-20](./eip-20.md)/[ERC-721](./eip-721.md)/[ERC-1155](./eip-1155.md) asset ownership), or other data sources that might or might not be permissioned. +4. The relying party might further fetch data associated with the Ethereum address, such as from the Ethereum blockchain (e.g., ENS, account balances, [ERC-20](./erc-20.md)/[ERC-721](./erc-721.md)/[ERC-1155](./erc-1155.md) asset ownership), or other data sources that might or might not be permissioned. ### Message Format #### ABNF Message Format -A SIWE Message MUST conform with the following Augmented Backus–Naur Form (ABNF, [RFC 5234](https://www.rfc-editor.org/rfc/rfc5234)) expression (note that `%s` denotes case sensitivity for a string term, as per [RFC 7405](https://www.rfc-editor.org/rfc/rfc7405)). +A SIWE Message MUST conform with the following Augmented Backus–Naur Form (ABNF, [RFC 5234](https://www.rfc-editor.org/rfc/rfc5234)) expression. `%s` denotes case sensitivity for a string term, as per [RFC 7405](https://www.rfc-editor.org/rfc/rfc7405). The grammar also normatively imports the following productions from [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986): `URI`, `authority`, `host`, `port`, `pchar`, `reserved`, and `unreserved`. + +A SIWE Message is a sequence of Unicode code points encoded as UTF-8. All ABNF productions below describe the UTF-8 byte stream of the message. The `` component of the ERC-191 prefix (`\x19Ethereum Signed Message:\n`) MUST be computed as the UTF-8 byte length of the SIWE Message. ```abnf sign-in-with-ethereum = @@ -58,25 +60,30 @@ sign-in-with-ethereum = [ LF %s"Resources:" resources ] +; Imported from RFC 3986 Section 3: +; URI, authority, host, port, pchar, reserved, unreserved. +; Imported from RFC 5234 Appendix B.1: +; ALPHA, DIGIT, HEXDIG, LF. +; Imported from RFC 7405: +; %s (case-sensitive string literal prefix). + scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ; See RFC 3986 for the fully contextualized ; definition of "scheme". -domain = authority - ; From RFC 3986: - ; authority = [ userinfo "@" ] host [ ":" port ] - ; See RFC 3986 for the fully contextualized - ; definition of "authority". +domain = host [ ":" port ] + ; host and port are imported from RFC 3986. + ; The userinfo component of RFC 3986 + ; authority is intentionally excluded + ; from SIWE domain values. -address = "0x" 40*40HEXDIG - ; Must also conform to capitalization - ; checksum encoding specified in EIP-55 - ; where applicable (EOAs). +address = %s"0x" 40*40HEXDIG + ; If the address format is mixed-case, it + ; MUST conform to its ERC-55 checksum. -statement = *( reserved / unreserved / " " ) - ; See RFC 3986 for the definition - ; of "reserved" and "unreserved". - ; The purpose is to exclude LF (line break). +statement = *( %x20-7E ) + ; Printable ASCII excluding LF (0x0A) + ; and other control characters. uri = URI ; See RFC 3986 for the definition of "URI". @@ -93,8 +100,9 @@ nonce = 8*( ALPHA / DIGIT ) issued-at = date-time expiration-time = date-time not-before = date-time - ; See RFC 3339 (ISO 8601) for the - ; definition of "date-time". + ; See RFC 3339 for date-time. RFC 3339 is + ; a profile of ISO 8601; only RFC 3339 + ; forms are permitted here. request-id = *pchar ; See RFC 3986 for the definition of "pchar". @@ -109,25 +117,25 @@ resource = "- " URI This specification defines the following SIWE Message fields that can be parsed from a SIWE Message by following the rules in [ABNF Message Format](#abnf-message-format): - `scheme` OPTIONAL. The URI scheme of the origin of the request. Its value MUST be an RFC 3986 URI scheme. -- `domain` REQUIRED. The domain that is requesting the signing. Its value MUST be an RFC 3986 authority. The authority includes an OPTIONAL port. If the port is not specified, the default port for the provided `scheme` is assumed (e.g., 443 for HTTPS). If `scheme` is not specified, HTTPS is assumed by default. -- `address` REQUIRED. The Ethereum address performing the signing. Its value SHOULD be conformant to mixed-case checksum address encoding specified in [ERC-55](./eip-55.md) where applicable. -- `statement` OPTIONAL. A human-readable ASCII assertion that the user will sign which MUST NOT include `'\n'` (the byte `0x0a`). +- `domain` REQUIRED. The domain that is requesting the signing. Its value is an RFC 3986 `host` optionally followed by a `":"` and a port (the `userinfo` subcomponent of RFC 3986 `authority` is excluded). The `host` subcomponent MUST be non-empty. If the port is not specified, the default port for the effective `scheme` is assumed (e.g., 443 for HTTPS). If `scheme` is not specified in the message, the wallet's `defaultScheme` (see the Wallet Implementer Steps) is used as the effective scheme for origin-comparison purposes. +- `address` REQUIRED. The Ethereum address performing the signing. Its value SHOULD be conformant to mixed-case checksum address encoding specified in [ERC-55](./erc-55.md) where applicable. +- `statement` OPTIONAL. A human-readable assertion that the user will sign. If emitted, the statement is a sequence of zero or more printable ASCII characters (bytes `0x20` through `0x7E`) and MUST NOT include `'\n'` (the byte `0x0a`) or any other control character. Producers SHOULD omit the `[ statement LF ]` production entirely when no statement is present rather than emitting an empty statement. Parsers MUST accept both empty and omitted statements. For authentication semantics, an empty statement is equivalent to an omitted statement. - `uri` REQUIRED. An RFC 3986 URI referring to the resource that is the subject of the signing (as in the _subject of a claim_). - `version` REQUIRED. The current version of the SIWE Message, which MUST be `1` for this specification. -- `chain-id` REQUIRED. The [EIP-155](./eip-155.md) Chain ID to which the session is bound, and the network where Contract Accounts MUST be resolved. +- `chain-id` REQUIRED. The [ERC-155](./erc-155.md) Chain ID to which the session is bound, and the network where Contract Accounts MUST be resolved. - `nonce` REQUIRED. A random string typically chosen by the relying party and used to prevent replay attacks, at least 8 alphanumeric characters. - `issued-at` REQUIRED. The time when the message was generated, typically the current time. Its value MUST be an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) datetime string. - `expiration-time` OPTIONAL. The time when the signed authentication message is no longer valid. Its value MUST be an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) datetime string. - `not-before` OPTIONAL. The time when the signed authentication message will become valid. Its value MUST be an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) datetime string. -- `request-id` OPTIONAL. A system-specific identifier that MAY be used to uniquely refer to the sign-in request. -- `resources` OPTIONAL. A list of information or references to information the user wishes to have resolved as part of authentication by the relying party. Every resource MUST be an RFC 3986 URI separated by `"\n- "` where `\n` is the byte `0x0a`. +- `request-id` OPTIONAL. A system-specific identifier that MAY be used to uniquely refer to the sign-in request. Producers SHOULD NOT emit an empty `request-id`; when no identifier is available, the `Request ID:` header is to be omitted entirely. Parsers MUST continue to accept messages that contain an empty `request-id` value, for backwards compatibility with existing message streams. +- `resources` OPTIONAL. A list of information or references to information the user wishes to have resolved as part of authentication by the relying party. Every resource MUST be an RFC 3986 URI separated by `"\n- "` where `\n` is the byte `0x0a`. Producers SHOULD NOT emit a bare `Resources:` header with no following resources; when no resources are present, the entire `[ LF %s"Resources:" resources ]` production is to be omitted. Parsers MUST accept a bare `Resources:` header as semantically equivalent to an omitted one, for backwards compatibility. #### Informal Message Template A Bash-like informal template of the full SIWE Message is presented below for readability and ease of understanding, and it does not reflect the allowed optionality of the fields. Field descriptions are provided in the following section. A full ABNF description is provided in [ABNF Message Format](#abnf-message-format). ``` -${scheme}:// ${domain} wants you to sign in with your Ethereum account: +${scheme}://${domain} wants you to sign in with your Ethereum account: ${address} ${statement} @@ -205,17 +213,17 @@ Resources: ### Signing and Verifying Messages with Ethereum Accounts -- For Externally Owned Accounts (EOAs), the verification method specified in [ERC-191](./eip-191.md) MUST be used. +- For Externally Owned Accounts (EOAs), the verification method specified in [ERC-191](./erc-191.md) MUST be used. - For Contract Accounts, - - The verification method specified in [ERC-1271](./eip-1271.md) SHOULD be used, and if it is not, the implementer MUST clearly define the verification method to attain security and interoperability for both wallets and relying parties. - - When performing [ERC-1271](./eip-1271.md) signature verification, the contract performing the verification MUST be resolved from the specified `chain-id`. - - Implementers SHOULD take into consideration that [ERC-1271](./eip-1271.md) implementations are not required to be pure functions, and can return different results for the same inputs depending on blockchain state. This can affect the security model and session validation rules. For example, a service with [ERC-1271](./eip-1271.md) signing enabled could rely on webhooks to receive notifications when state affecting the results is changed. When it receives a notification, it invalidates any matching sessions. + - The verification method specified in [ERC-1271](./erc-1271.md) SHOULD be used, and if it is not, the implementer MUST clearly define the verification method to attain security and interoperability for both wallets and relying parties. + - When performing [ERC-1271](./erc-1271.md) signature verification, the contract performing the verification MUST be resolved from the specified `chain-id`. + - Implementers SHOULD take into consideration that [ERC-1271](./erc-1271.md) implementations are not required to be pure functions, and can return different results for the same inputs depending on blockchain state. This can affect the security model and session validation rules. For example, a service with [ERC-1271](./erc-1271.md) signing enabled could rely on webhooks to receive notifications when state affecting the results is changed. When it receives a notification, it invalidates any matching sessions. ### Resolving Ethereum Name Service (ENS) Data - The relying party or wallet MAY additionally perform resolution of ENS data, as this can improve the user experience by displaying human-friendly information that is related to the `address`. Resolvable ENS data include: - - The [primary ENS name](./eip-181.md). + - The [primary ENS name](./erc-181.md). - The ENS avatar. - Any other resolvable resources specified in the ENS documentation. - If resolution of ENS data is performed, implementers SHOULD take precautions to preserve user privacy and consent, as their `address` could be forwarded to third party services as part of the resolution process. @@ -228,7 +236,7 @@ Resources: #### Verifying a signed Message -- The SIWE Message MUST be checked for conformance to the ABNF Message Format in the previous sections, checked against expected values after parsing (e.g., `expiration-time`, `nonce`, `request-uri` etc.), and its signature MUST be checked as defined in [Signing and Verifying Messages with Ethereum Accounts](#signing-and-verifying-messages-with-ethereum-accounts). +- The SIWE Message MUST be checked for conformance to the ABNF Message Format in the previous sections, checked against expected values after parsing (e.g., `expiration-time`, `nonce`, `uri` etc.), and its signature MUST be checked as defined in [Signing and Verifying Messages with Ethereum Accounts](#signing-and-verifying-messages-with-ethereum-accounts). #### Creating Sessions @@ -245,13 +253,13 @@ Resources: - The full SIWE message MUST be checked for conformance to the ABNF defined in [ABNF Message Format](#abnf-message-format). - Wallet implementers SHOULD warn users if the substring `"wants you to sign in - with your Ethereum account"` appears anywhere in an [ERC-191](./eip-191.md) message signing + with your Ethereum account"` appears anywhere in an [ERC-191](./erc-191.md) message signing request unless the message fully conforms to the format defined [ABNF Message Format](#abnf-message-format). #### Verifying the Request Origin - Wallet implementers MUST prevent phishing attacks by verifying the origin of the request against the `scheme` and `domain` fields in the SIWE Message. For example, when processing the SIWE message beginning with `"example.com wants you to sign in..."`, the wallet checks that the request actually originated from `https://example.com`. -- The origin SHOULD be read from a trusted data source such as the browser window or over WalletConnect ([ERC-1328](./eip-1328.md)) sessions for comparison against the signing message contents. +- The origin SHOULD be read from a trusted data source such as the browser window or over WalletConnect ([ERC-1328](./erc-1328.md)) sessions for comparison against the signing message contents. - Wallet implementers MAY warn instead of rejecting the verification if the origin is pointing to localhost. The following is a RECOMMENDED algorithm for Wallets to conform with the requirements on request origin verification defined by this specification. @@ -261,7 +269,7 @@ The algorithm takes the following input variables: - fields from the SIWE message. - `origin` of the signing request - in the case of a browser wallet implementation - the origin of the page which requested the signin via the provider. - `allowedSchemes` - a list of schemes allowed by the Wallet. -- `defaultScheme` - a scheme to assume when none was provided. Wallet implementers in the browser SHOULD use `https`. +- `defaultScheme` - the scheme to assume when none was provided in the message. Wallet implementers in the browser SHOULD use `https`. - developer mode indication - a setting deciding if certain risks should be a warning instead of rejection. Can be manually configured or derived from `origin` being localhost. The algorithm is described as follows: @@ -269,8 +277,7 @@ The algorithm is described as follows: - If `scheme` was not provided, then assign `defaultScheme` as `scheme`. - If `scheme` is not contained in `allowedSchemes`, then the `scheme` is not expected and the Wallet MUST reject the request. Wallet implementers in the browser SHOULD limit the list of `allowedSchemes` to just `'https'` unless a developer mode is activated. - If `scheme` does not match the scheme of `origin`, the Wallet SHOULD reject the request. Wallet implementers MAY show a warning instead of rejecting the request if a developer mode is activated. In that case the Wallet continues processing the request. -- If the `host` part of the `domain` and `origin` do not match, the Wallet MUST reject the request unless the Wallet is in developer mode. In developer mode the Wallet MAY show a warning instead and continues processing the request. -- If `domain` and `origin` have mismatching subdomains, the Wallet SHOULD reject the request unless the Wallet is in developer mode. In developer mode the Wallet MAY show a warning instead and continues processing the request. +- If the `host` subcomponent of `domain` and the host of `origin` do not match exactly, the Wallet MUST reject the request unless the Wallet is in developer mode. In developer mode the Wallet MAY show a warning instead and continue processing the request. Because `host` per RFC 3986 already includes the full hostname (all labels, including subdomains), a mismatched subdomain is a mismatched host and is covered by this rule. - Let `port` be the port component of `domain`, and if no port is contained in `domain`, assign `port` the default port specified for the `scheme`. - If `port` is not empty, then the Wallet SHOULD show a warning if the `port` does not match the port of `origin`. - If `port` is empty, then the Wallet MAY show a warning if `origin` contains a specific port. (Note 'https' has a default port of 443 so this only applies if `allowedSchemes` contain unusual schemes) @@ -309,12 +316,12 @@ Additional functional requirements: ### Technical Decisions -- Why [ERC-191](./eip-191.md) (Signed Data Standard) over [EIP-712](./eip-712.md) (Ethereum typed structured data hashing and signing) - - [ERC-191](./eip-191.md) is already broadly supported across wallets UX, while [EIP-712](./eip-712.md) support for friendly user display is pending. **(1, 2, 3, 4)** - - [ERC-191](./eip-191.md) is simple to implement using a pre-set prefix prior to signing, while [EIP-712](./eip-712.md) is more complex to implement requiring the further implementations of a bespoke Solidity-inspired type system, RLP-based encoding format, and custom keccak-based hashing scheme. **(2)** - - [ERC-191](./eip-191.md) produces more human-readable messages, while [EIP-712](./eip-712.md) creates signing outputs for machine consumption, with most wallets not displaying the payload to be signed in a manner friendly to humans. **(1)**![](../assets/eip-4361/signing.png) +- Why [ERC-191](./erc-191.md) (Signed Data Standard) over [EIP-712](https://eips.ethereum.org/EIPS/eip-712) (Ethereum typed structured data hashing and signing) + - [ERC-191](./erc-191.md) is already broadly supported across wallets UX, while [EIP-712](https://eips.ethereum.org/EIPS/eip-712) support for friendly user display is pending. **(1, 2, 3, 4)** + - [ERC-191](./erc-191.md) is simple to implement using a pre-set prefix prior to signing, while [EIP-712](https://eips.ethereum.org/EIPS/eip-712) is more complex to implement requiring the further implementations of a bespoke Solidity-inspired type system, RLP-based encoding format, and custom keccak-based hashing scheme. **(2)** + - [ERC-191](./erc-191.md) produces more human-readable messages, while [EIP-712](https://eips.ethereum.org/EIPS/eip-712) creates signing outputs for machine consumption, with most wallets not displaying the payload to be signed in a manner friendly to humans. **(1)**![](../assets/eip-4361/signing.png) - - [EIP-712](./eip-712.md) has the advantage of on-chain representation and on-chain verifiability, such as for their use in metatransactions, but this feature is not relevant for the specification's scope. **(2)** + - [EIP-712](https://eips.ethereum.org/EIPS/eip-712) has the advantage of on-chain representation and on-chain verifiability, such as for their use in metatransactions, but this feature is not relevant for the specification's scope. **(2)** - Why not use JWTs? Wallets don't support JWTs. The keccak hash function is not assigned by IANA for use as a JOSE algorithm. **(2, 3)** - Why not use YAML or YAML with exceptions? YAML is loose compared to ABNF, which can readily express character set limiting, required ordering, and strict whitespacing. **(2, 3)** @@ -336,12 +343,12 @@ The following items are considered for future support either through an iteratio - Possible support for Decentralized Identifiers and Verifiable Credentials. - Possible cross-chain support. - Possible SIOPv2 support. -- Possible future support for [EIP-712](./eip-712.md). +- Possible future support for [EIP-712](https://eips.ethereum.org/EIPS/eip-712). - Version interpretation rules, e.g., sign with minor revision greater than understood, but not greater major version. ## Backwards Compatibility -- Most wallet implementations already support [ERC-191](./eip-191.md), so this is used as a base pattern with additional features. +- Most wallet implementations already support [ERC-191](./erc-191.md), so this is used as a base pattern with additional features. - Requirements were gathered from existing implementations of similar sign-in workflows, including statements to allow the user to accept a Terms of Service, nonces for replay protection, and inclusion of the Ethereum address itself in the message. ## Reference Implementation @@ -360,7 +367,7 @@ A reference implementation is available [here](../assets/eip-4361/example.js). - Sign-In with Ethereum gives users control through their keys. This is additional responsibility that mainstream users may not be accustomed to accepting, and key management is a hard problem especially for individuals. For example, there is no "forgot password" button as centralized identity providers commonly implement. - Early adopters of this specification are likely to be already adept at key management, so this consideration becomes more relevant with mainstream adoption. -- Certain wallets can use smart contracts and multisigs to provide an enhanced user experience with respect to key usage and key recovery, and these can be supported via [ERC-1271](./eip-1271.md) signing. +- Certain wallets can use smart contracts and multisigs to provide an enhanced user experience with respect to key usage and key recovery, and these can be supported via [ERC-1271](./erc-1271.md) signing. ### Wallet and Relying Party combined Security @@ -390,7 +397,7 @@ A reference implementation is available [here](../assets/eip-4361/example.js). There are several cases where an implementer SHOULD check for state changes as they relate to sessions. -- If an [ERC-1271](./eip-1271.md) implementation or dependent data changes the signature computation, the server SHOULD invalidate sessions appropriately. +- If an [ERC-1271](./erc-1271.md) implementation or dependent data changes the signature computation, the server SHOULD invalidate sessions appropriately. - If any resources specified in `resources` change, the server SHOULD invalidate sessions appropriately. However, the interpretation of `resources` is out of scope of this specification. ### Maximum Lengths for ABNF Terms diff --git a/assets/erc-4361/example.js b/assets/erc-4361/example.js index 12ec7ee74a5..bdfbf44cada 100644 --- a/assets/erc-4361/example.js +++ b/assets/erc-4361/example.js @@ -1,15 +1,42 @@ // To run this example, navigate to this directory and run `npm i && node example.js` +// +// ---------------------------------------------------------------------------- +// Regression tests - the following messages from the ERC-4361 specification +// body (Examples section) MUST parse successfully against the grammar below. +// If you edit the grammar, verify these still parse before committing. +// ---------------------------------------------------------------------------- +// +// Example 1 - implicit scheme: +// +// example.com wants you to sign in with your Ethereum account: +// 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 +// +// I accept the ExampleOrg Terms of Service: https://example.com/tos +// +// URI: https://example.com/login +// Version: 1 +// Chain ID: 1 +// Nonce: 32891756 +// Issued At: 2021-09-30T16:25:24Z +// Resources: +// - ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/ +// - https://example.com/my-web2-claim.json +// +// Example 2 - implicit scheme, explicit port (example.com:3388 in place of +// example.com on the first line; otherwise identical to Example 1). +// +// Example 3 - explicit scheme (https://example.com in place of example.com +// on the first line; otherwise identical to Example 1). const apgApi = require('apg-js/src/apg-api/api'); const apgLib = require('apg-js/src/apg-lib/node-exports'); const GRAMMAR = ` sign-in-with-ethereum = - domain %s" wants you to sign in with your Ethereum account:" LF + [ scheme "://" ] domain %s" wants you to sign in with your Ethereum account:" LF address LF LF - [ statement LF ] - LF + statement-section %s"URI: " URI LF %s"Version: " version LF %s"Chain ID: " chain-id LF @@ -21,15 +48,19 @@ sign-in-with-ethereum = [ LF %s"Resources:" resources ] -domain = authority +domain = host [ ":" port ] + ; userinfo subcomponent of RFC 3986 authority + ; is excluded; host MUST NOT be empty. + +address = %s"0x" 40*40HEXDIG + ; If the address format is mixed-case, it + ; MUST conform to its ERC-55 checksum. -address = "0x" 40*40HEXDIG - ; Must also conform to captilization - ; checksum encoding specified in EIP-55 - ; where applicable (EOAs). +statement = *( %x20-7E ) + ; Printable ASCII excluding LF (0x0A) + ; and other control characters. -statement = 1*( reserved / unreserved / " " ) - ; The purpose is to exclude LF (line breaks). +statement-section = statement LF LF / [ LF ] version = "1" @@ -164,28 +195,18 @@ const parseMessage = (message) => { return ret; }; - const domain = getField("domain"); - parser.ast.callbacks.domain = domain; - const address = getField("address"); - parser.ast.callbacks.address = address; - const statement = getField("statement"); - parser.ast.callbacks.statement = statement; - const uri = getField("uri"); - parser.ast.callbacks.uri = uri; - const version = getField("version"); - parser.ast.callbacks.version = version; - const chainId = getField("chainId"); - parser.ast.callbacks['chain-id'] = chainId; - const nonce = getField("nonce"); - parser.ast.callbacks.nonce = nonce; - const issuedAt = getField("issuedAt"); - parser.ast.callbacks['issued-at'] = issuedAt; - const expirationTime = getField("expirationTime"); - parser.ast.callbacks['expiration-time'] = expirationTime; - const notBefore = getField("notBefore"); - parser.ast.callbacks['not-before'] = notBefore; - const requestId = getField("requestId"); - parser.ast.callbacks['request-id'] = requestId; + parser.ast.callbacks.scheme = getField("scheme"); + parser.ast.callbacks.domain = getField("domain"); + parser.ast.callbacks.address = getField("address"); + parser.ast.callbacks.statement = getField("statement"); + parser.ast.callbacks.uri = getField("uri"); + parser.ast.callbacks.version = getField("version"); + parser.ast.callbacks['chain-id'] = getField("chainId"); + parser.ast.callbacks.nonce = getField("nonce"); + parser.ast.callbacks['issued-at'] = getField("issuedAt"); + parser.ast.callbacks['expiration-time'] = getField("expirationTime"); + parser.ast.callbacks['not-before'] = getField("notBefore"); + parser.ast.callbacks['request-id'] = getField("requestId"); const resources = function (state, chars, phraseIndex, phraseLength, data) { const ret = id.SEM_OK; @@ -212,14 +233,39 @@ const parseMessage = (message) => { return obj; } -const createMessage = ({ domain, address, uri, version, chainId, nonce, issuedAt }) => { - const header = `${domain} wants you to sign in with your Ethereum account:\n${address}\n\n\n`; - const uriField = `URI: ${uri}\n`; - const versionField = `Version: ${version}\n`; - const chainField = `Chain ID: ${chainId}\n`; - const nonceField = `Nonce: ${nonce}\n`; - const issuedAtField = `Issued At: ${issuedAt}`; - return [header, uriField, versionField, chainField, nonceField, issuedAtField].join(''); +const createMessage = ({ + scheme, + domain, + address, + statement, + uri, + version, + chainId, + nonce, + issuedAt, + expirationTime, + notBefore, + requestId, + resources, +}) => { + const prefix = scheme ? `${scheme}://${domain}` : domain; + const header = `${prefix} wants you to sign in with your Ethereum account:\n${address}\n\n`; + const statementSection = statement ? `${statement}\n\n` : `` + const requiredFields = [ + `URI: ${uri}\n`, + `Version: ${version}\n`, + `Chain ID: ${chainId}\n`, + `Nonce: ${nonce}\n`, + `Issued At: ${issuedAt}`, + ]; + const optionalFields = []; + if (expirationTime) optionalFields.push(`\nExpiration Time: ${expirationTime}`); + if (notBefore) optionalFields.push(`\nNot Before: ${notBefore}`); + if (requestId) optionalFields.push(`\nRequest ID: ${requestId}`); + if (Array.isArray(resources) && resources.length >= 1) { + optionalFields.push(`\nResources:\n- ${resources.join('\n- ')}`); + } + return [header, statementSection, ...requiredFields, ...optionalFields].join(''); } const message = createMessage({