From 9741fcf4244df781fcfbbc7b7aa5ed0603ea164a Mon Sep 17 00:00:00 2001 From: Jiatu Liu Date: Fri, 11 Oct 2024 23:26:19 +0800 Subject: [PATCH] Fix Safe wallet signature verification (#76) Close #63 --------- Co-authored-by: Simon Bihel --- pyproject.toml | 2 +- siwe/siwe.py | 12 ++++++++---- tests/test_siwe.py | 23 +++++++++++++++++++++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ed05d05..a628e56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "siwe" -version = "4.3.0" +version = "4.4.0" description = "A Python implementation of Sign-In with Ethereum (EIP-4361)." license = "MIT OR Apache-2.0" authors = [ diff --git a/siwe/siwe.py b/siwe/siwe.py index c4afab5..263e528 100644 --- a/siwe/siwe.py +++ b/siwe/siwe.py @@ -21,7 +21,7 @@ from pydantic_core import core_schema from typing_extensions import Annotated from web3 import HTTPProvider, Web3 -from web3.exceptions import BadFunctionCallOutput +from web3.exceptions import BadFunctionCallOutput, ContractLogicError from .parsed import ABNFParsedMessage, RegExpParsedMessage @@ -327,7 +327,7 @@ def verify( try: address = w3.eth.account.recover_message(message, signature=signature) - except ValueError: + except (ValueError, IndexError): address = None except eth_utils.exceptions.ValidationError: raise InvalidSignature from None @@ -355,7 +355,11 @@ def check_contract_wallet_signature( contract = w3.eth.contract(address=address, abi=EIP1271_CONTRACT_ABI) hash_ = _hash_eip191_message(message) try: - response = contract.caller.isValidSignature(hash_, bytes.fromhex(signature[2:])) + # For message hashes stored on-chain for Safe wallets, the signatures + # are always "0x" and should be passed in as-is. + response = contract.caller.isValidSignature( + hash_, signature if signature == "0x" else bytes.fromhex(signature[2:]) + ) return response.hex() == EIP1271_MAGICVALUE - except BadFunctionCallOutput: + except (BadFunctionCallOutput, ContractLogicError): return False diff --git a/tests/test_siwe.py b/tests/test_siwe.py index 10536f6..df3cbd4 100644 --- a/tests/test_siwe.py +++ b/tests/test_siwe.py @@ -23,10 +23,20 @@ with open(BASE_TESTS + "eip1271.json", "r") as f: verification_eip1271 = decamelize(json.load(fp=f)) +endpoint_uri = "https://cloudflare-eth.com" try: - endpoint_uri = os.environ["WEB3_PROVIDER_URI"] + uri = os.environ["WEB3_PROVIDER_URI"] + if uri != "": + endpoint_uri = uri except KeyError: - endpoint_uri = "https://cloudflare-eth.com" + pass +sepolia_endpoint_uri = "https://rpc.sepolia.org" +try: + uri = os.environ["WEB3_PROVIDER_URI_SEPOLIA"] + if uri != "": + sepolia_endpoint_uri = uri +except KeyError: + pass class TestMessageParsing: @@ -94,6 +104,15 @@ def test_eip1271_message(self, test_name, test): siwe_message = SiweMessage.from_message(message=test["message"]) siwe_message.verify(test["signature"], provider=provider) + def test_safe_wallet_message(self): + message = "localhost:3000 wants you to sign in with your Ethereum account:\n0x54D97AEa047838CAC7A9C3e452951647f12a440c\n\nPlease sign in to verify your ownership of this wallet\n\nURI: http://localhost:3000\nVersion: 1\nChain ID: 11155111\nNonce: gDj8rv7VVxN\nIssued At: 2024-10-10T08:34:03.152Z\nExpiration Time: 2024-10-13T08:34:03.249112Z" + signature = "0x" + # Use a Sepolia RPC node since the signature is generated on Sepolia testnet + # instead of mainnet like other EIP-1271 tests. + provider = HTTPProvider(endpoint_uri=sepolia_endpoint_uri) + siwe_message = SiweMessage.from_message(message=message) + siwe_message.verify(signature, provider=provider) + @pytest.mark.parametrize( "provider", [HTTPProvider(endpoint_uri=endpoint_uri), None] )