diff --git a/packages/siwe-parser/lib/abnf.ts b/packages/siwe-parser/lib/abnf.ts index 8815fe6d..a7e5fcfa 100644 --- a/packages/siwe-parser/lib/abnf.ts +++ b/packages/siwe-parser/lib/abnf.ts @@ -4,7 +4,7 @@ import { isEIP55Address, parseIntegerNumber } from "./utils"; 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 ] @@ -161,6 +161,7 @@ class GrammarApi { } export class ParsedMessage { + scheme: string | null; domain: string; address: string; statement: string | null; @@ -179,6 +180,19 @@ export class ParsedMessage { parser.ast = new apgLib.ast(); const id = apgLib.ids; + const scheme = function (state, chars, phraseIndex, phraseLength, data) { + const ret = id.SEM_OK; + if (state === id.SEM_PRE && phraseIndex === 0) { + data.scheme = apgLib.utils.charsToString( + chars, + phraseIndex, + phraseLength + ); + } + return ret; + }; + parser.ast.callbacks.scheme = scheme; + const domain = function (state, chars, phraseIndex, phraseLength, data) { const ret = id.SEM_OK; if (state === id.SEM_PRE) { diff --git a/packages/siwe-parser/lib/parsers.test.ts b/packages/siwe-parser/lib/parsers.test.ts index 9bc5c1b8..2fff8b0a 100644 --- a/packages/siwe-parser/lib/parsers.test.ts +++ b/packages/siwe-parser/lib/parsers.test.ts @@ -10,7 +10,10 @@ describe("Successfully parses with ABNF Client", () => { (test_name, test) => { const parsedMessage = new ParsedMessage(test.message); for (const [field, value] of Object.entries(test.fields)) { - if (typeof value === "object") { + if (value === null) { + expect(parsedMessage[field]).toBeUndefined(); + } + else if (typeof value === "object") { expect(parsedMessage[field]).toStrictEqual(value); } else { expect(parsedMessage[field]).toBe(value); diff --git a/packages/siwe/lib/client.test.ts b/packages/siwe/lib/client.test.ts index 1e903a6c..c59255ae 100644 --- a/packages/siwe/lib/client.test.ts +++ b/packages/siwe/lib/client.test.ts @@ -57,6 +57,7 @@ describe(`Message verification without suppressExceptions`, () => { .verify({ signature: test_fields.signature, time: (test_fields as any).time || test_fields.issuedAt, + scheme: (test_fields as any).scheme, domain: (test_fields as any).domainBinding, nonce: (test_fields as any).matchNonce, }) @@ -85,6 +86,7 @@ describe(`Message verification without suppressExceptions`, () => { .verify({ signature: test_fields.signature, time: (test_fields as any).time || test_fields.issuedAt, + scheme: (test_fields as any).scheme, domain: (test_fields as any).domainBinding, nonce: (test_fields as any).matchNonce, }) @@ -109,6 +111,7 @@ describe(`Message verification with suppressExceptions`, () => { { signature: test_fields.signature, time: (test_fields as any).time || test_fields.issuedAt, + scheme: (test_fields as any).scheme, domain: (test_fields as any).domainBinding, nonce: (test_fields as any).matchNonce, }, diff --git a/packages/siwe/lib/client.ts b/packages/siwe/lib/client.ts index 19f8690f..1fd53a45 100644 --- a/packages/siwe/lib/client.ts +++ b/packages/siwe/lib/client.ts @@ -25,6 +25,8 @@ import { } from './utils'; export class SiweMessage { + /**RFC 3986 URI scheme for the authority that is requesting the signing. */ + scheme?: string; /**RFC 4501 dns authority that is requesting the signing. */ domain: string; /**Ethereum address performing the signing conformant to capitalization @@ -69,6 +71,7 @@ export class SiweMessage { constructor(param: string | Partial) { if (typeof param === 'string') { const parsedMessage = new ParsedMessage(param); + this.scheme = parsedMessage.scheme; this.domain = parsedMessage.domain; this.address = parsedMessage.address; this.statement = parsedMessage.statement; @@ -82,6 +85,7 @@ export class SiweMessage { this.chainId = parsedMessage.chainId; this.resources = parsedMessage.resources; } else { + this.scheme = param?.scheme; this.domain = param.domain; this.address = param.address; this.statement = param?.statement; @@ -113,8 +117,8 @@ export class SiweMessage { toMessage(): string { /** Validates all fields of the object */ this.validateMessage(); - - const header = `${this.domain} wants you to sign in with your Ethereum account:`; + const headerPrefx = this.scheme ? `${this.scheme}://${this.domain}` : this.domain; + const header = `${headerPrefx} wants you to sign in with your Ethereum account:`; const uriField = `URI: ${this.uri}`; let prefix = [header, this.address].join('\n'); const versionField = `Version: ${this.version}`; @@ -246,7 +250,20 @@ export class SiweMessage { }); } - const { signature, domain, nonce, time } = params; + const { signature, scheme, domain, nonce, time } = params; + + /** Scheme for domain binding */ + if (scheme && scheme !== this.scheme) { + fail({ + success: false, + data: this, + error: new SiweError( + SiweErrorType.SCHEME_MISMATCH, + scheme, + this.scheme + ), + }); + } /** Domain binding */ if (domain && domain !== this.domain) { diff --git a/packages/siwe/lib/types.ts b/packages/siwe/lib/types.ts index acf5b969..66bcdfd8 100644 --- a/packages/siwe/lib/types.ts +++ b/packages/siwe/lib/types.ts @@ -5,6 +5,9 @@ export interface VerifyParams { /** Signature of the message signed by the wallet */ signature: string; + /** RFC 3986 URI scheme for the authority that is requesting the signing. */ + scheme?: string; + /** RFC 4501 dns authority that is requesting the signing. */ domain?: string; @@ -17,6 +20,7 @@ export interface VerifyParams { export const VerifyParamsKeys: Array = [ 'signature', + 'scheme', 'domain', 'nonce', 'time', @@ -63,8 +67,8 @@ export class SiweError { this.received = received; } - /** Type of the error. */ - type: SiweErrorType | string; + /** Type of the error. */ + type: SiweErrorType | string; /** Expected value or condition to pass. */ expected?: string; @@ -83,6 +87,9 @@ export enum SiweErrorType { /** `domain` is not a valid authority or is empty. */ INVALID_DOMAIN = 'Invalid domain.', + /** `scheme` don't match the scheme provided for verification. */ + SCHEME_MISMATCH = 'Scheme does not match provided scheme for verification.', + /** `domain` don't match the domain provided for verification. */ DOMAIN_MISMATCH = 'Domain does not match provided domain for verification.', diff --git a/test/parsing_positive.json b/test/parsing_positive.json index 05221a55..95ca7540 100644 --- a/test/parsing_positive.json +++ b/test/parsing_positive.json @@ -216,5 +216,33 @@ "nonce": "15050747", "issuedAt": "2022-06-30T14:08:51.382Z" } + }, + "domain contains optional scheme": { + "message": "https://example.com wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ExampleOrg Terms of Service: https://example.com/tos\n\nURI: https://example.com/login\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24Z", + "fields": { + "scheme": "https", + "domain": "example.com", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "statement": "I accept the ExampleOrg Terms of Service: https://example.com/tos", + "uri": "https://example.com/login", + "version": "1", + "chainId": 1, + "nonce": "32891756", + "issuedAt": "2021-09-30T16:25:24Z" + } + }, + "scheme is not parsed from elsehwere in message": { + "message": "localhost:3030 wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ExampleOrg Terms of Service: http://localhost:3030/tos\n\nURI: http://localhost:3030/login\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24Z", + "fields": { + "scheme": null, + "domain": "localhost:3030", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "statement": "I accept the ExampleOrg Terms of Service: http://localhost:3030/tos", + "uri": "http://localhost:3030/login", + "version": "1", + "chainId": 1, + "nonce": "32891756", + "issuedAt": "2021-09-30T16:25:24Z" + } } }