diff --git a/siwe/defs.py b/siwe/defs.py index adf28fc..c1442b8 100644 --- a/siwe/defs.py +++ b/siwe/defs.py @@ -1,5 +1,6 @@ """Regexes for the various fields.""" +SCHEME = "((?P([a-zA-Z][a-zA-Z0-9\\+\\-\\.]*))://)?" DOMAIN = "(?P([^/?#]+)) wants you to sign in with your Ethereum account:\\n" ADDRESS = "(?P
0x[a-zA-Z0-9]{40})\\n\\n" STATEMENT = "((?P[^\\n]+)\\n)?\\n" @@ -19,6 +20,6 @@ REQUEST_ID = "(\\nRequest ID: (?P[-._~!$&'()*+,;=:@%a-zA-Z0-9]*))?" RESOURCES = f"(\\nResources:(?P(\\n- {URI})+))?" REGEX_MESSAGE = ( - f"^{DOMAIN}{ADDRESS}{STATEMENT}{URI_LINE}{VERSION}{CHAIN_ID}{NONCE}" + f"^{SCHEME}{DOMAIN}{ADDRESS}{STATEMENT}{URI_LINE}{VERSION}{CHAIN_ID}{NONCE}" f"{ISSUED_AT}{EXPIRATION_TIME}{NOT_BEFORE}{REQUEST_ID}{RESOURCES}$" ) diff --git a/siwe/grammars/eip4361.py b/siwe/grammars/eip4361.py index 3e37eac..1d76ac7 100644 --- a/siwe/grammars/eip4361.py +++ b/siwe/grammars/eip4361.py @@ -14,6 +14,7 @@ # RFC 3986 ("URI", rfc3986.Rule("URI")), ("authority", rfc3986.Rule("authority")), + ("scheme", rfc3986.Rule("scheme")), ("reserved", rfc3986.Rule("reserved")), ("unreserved", rfc3986.Rule("unreserved")), ("reserved", rfc3986.Rule("reserved")), @@ -31,12 +32,12 @@ class Rule(_Rule): """Rules from EIP-4361.""" grammar: ClassVar[List] = [ - 'sign-in-with-ethereum = domain %s" wants you to sign in with your Ethereum ' - 'account:" LF address LF LF [ statement LF ] LF %s"URI: " uri LF %s"Version: "' - ' version LF %s"Chain ID: " chain-id LF %s"Nonce: " nonce LF %s"Issued At: " ' - 'issued-at [ LF %s"Expiration Time: " expiration-time ] [ LF %s"Not Before: " ' - 'not-before ] [ LF %s"Request ID: " request-id ] [ LF %s"Resources:" resources ' - "]", + 'sign-in-with-ethereum = [ scheme "://" ] domain %s" wants you to sign in with ' + 'your Ethereum account:" LF address LF LF [ statement LF ] LF %s"URI: " uri LF ' + '%s"Version: " version LF %s"Chain ID: " chain-id LF %s"Nonce: " nonce LF %s"' + 'Issued At: " issued-at [ LF %s"Expiration Time: " expiration-time ] [ LF %s"' + 'Not Before: " not-before ] [ LF %s"Request ID: " request-id ] [ LF %s"' + 'Resources:" resources ]', "domain = authority", 'address = "0x" 40HEXDIG', 'statement = 1*( reserved / unreserved / " " )', diff --git a/siwe/parsed.py b/siwe/parsed.py index ca9311f..5c78fd2 100644 --- a/siwe/parsed.py +++ b/siwe/parsed.py @@ -20,6 +20,7 @@ def __init__(self, message: str): raise ValueError("Message did not match the regular expression.") self.match = match + self.scheme = match.group(expr.groupindex["scheme"]) self.domain = match.group(expr.groupindex["domain"]) self.address = match.group(expr.groupindex["address"]) self.statement = match.group(expr.groupindex["statement"]) @@ -49,6 +50,7 @@ def __init__(self, message: str): for child in node.children: if child.name in [ + "scheme", "domain", "address", "statement", diff --git a/siwe/siwe.py b/siwe/siwe.py index a2b5ac7..2442016 100644 --- a/siwe/siwe.py +++ b/siwe/siwe.py @@ -72,6 +72,12 @@ class NotYetValidMessage(VerificationError): pass +class SchemeMismatch(VerificationError): + """The message does not contain the expected scheme.""" + + pass + + class DomainMismatch(VerificationError): """The message does not contain the expected domain.""" @@ -157,6 +163,8 @@ def utc_now() -> datetime: class SiweMessage(BaseModel): """A Sign-in with Ethereum (EIP-4361) message.""" + scheme: Optional[str] = None + """RFC 3986 URI scheme for the authority that is requesting the signing.""" domain: str = Field(pattern="^[^/?#]+$") """RFC 4501 dns authority that is requesting the signing.""" address: ChecksumAddress @@ -234,6 +242,8 @@ def prepare_message(self) -> str: :return: EIP-4361 formatted message, ready for EIP-191 signing. """ header = f"{self.domain} wants you to sign in with your Ethereum account:" + if self.scheme: + header = f"{self.scheme}://{header}" uri_field = f"URI: {self.uri}" @@ -281,6 +291,7 @@ def verify( self, signature: str, *, + scheme: Optional[str] = None, domain: Optional[str] = None, nonce: Optional[str] = None, timestamp: Optional[datetime] = None, @@ -302,6 +313,8 @@ def verify( message = encode_defunct(text=self.prepare_message()) w3 = Web3(provider=provider) + if scheme is not None and self.scheme != scheme: + raise SchemeMismatch() if domain is not None and self.domain != domain: raise DomainMismatch() if nonce is not None and self.nonce != nonce: diff --git a/tests/siwe b/tests/siwe index 903faa9..4290e1f 160000 --- a/tests/siwe +++ b/tests/siwe @@ -1 +1 @@ -Subproject commit 903faa93c3125e4e1ac144b76cd5b43ba0fdd3e9 +Subproject commit 4290e1fb3702c97ffee16112492216ad4c4654c1 diff --git a/tests/test_siwe.py b/tests/test_siwe.py index ba85c95..edb9d08 100644 --- a/tests/test_siwe.py +++ b/tests/test_siwe.py @@ -33,7 +33,7 @@ def test_valid_message(self, abnf, test_name, test): siwe_message = SiweMessage(message=test["message"], abnf=abnf) for key, value in test["fields"].items(): v = getattr(siwe_message, key) - if not isinstance(v, int) and not isinstance(v, list): + if not (isinstance(v, int) or isinstance(v, list) or v is None): v = str(v) assert v == value