Giter Club home page Giter Club logo

siwe-py's Introduction

Sign-In with Ethereum

This package provides a Python implementation of EIP-4361: Sign In With Ethereum.

Installation

SIWE can be easily installed in any Python project with pip:

pip install siwe

Usage

SIWE provides a SiweMessage class which implements EIP-4361.

Parsing a SIWE Message

Parsing is done by initializing a SiweMessage object with an EIP-4361 formatted string:

from siwe import SiweMessage
message = SiweMessage.from_message(message=eip_4361_string)

Or to initialize a SiweMessage as a pydantic.BaseModel right away:

message = SiweMessage(domain="login.xyz", address="0x1234...", ...)

Verifying and Authenticating a SIWE Message

Verification and authentication is performed via EIP-191, using the address field of the SiweMessage as the expected signer. The validate method checks message structural integrity, signature address validity, and time-based validity attributes.

try:
    message.verify(signature="0x...")
    # You can also specify other checks (e.g. the nonce or domain expected).
except siwe.ValidationError:
    # Invalid

Serialization of a SIWE Message

SiweMessage instances can also be serialized as their EIP-4361 string representations via the prepare_message method:

print(message.prepare_message())

Example

Parsing and verifying a SiweMessage is easy:

try:
    message: SiweMessage = SiweMessage(message=eip_4361_string)
    message.verify(signature, nonce="abcdef", domain="example.com"):
except siwe.ValueError:
    # Invalid message
    print("Authentication attempt rejected.")
except siwe.ExpiredMessage:
    print("Authentication attempt rejected.")
except siwe.DomainMismatch:
    print("Authentication attempt rejected.")
except siwe.NonceMismatch:
    print("Authentication attempt rejected.")
except siwe.MalformedSession as e:
    # e.missing_fields contains the missing information needed for validation
    print("Authentication attempt rejected.")
except siwe.InvalidSignature:
    print("Authentication attempt rejected.")

# Message has been verified. Authentication complete. Continue with authorization/other.

Testing

poetry install
git submodule update --init
poetry run pytest

See Also

Disclaimer

Our Python library for Sign-In with Ethereum has not yet undergone a formal security audit. We welcome continued feedback on the usability, architecture, and security of this implementation.

siwe-py's People

Contributors

ameyarao98 avatar fubuloubu avatar kasparpeterson avatar krhoda avatar mikeshultz avatar mishuagopian avatar obstropolos avatar payton avatar sambarnes avatar sbihel avatar theosirian avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

siwe-py's Issues

Add EIP-55 validation

EIP-55 validation should be added to parsing to avoid interop issues. We should use the new EIP-55 test cases from the siwe repository.

CustomDateTime Date Attribute Retention Issue with Multiple Values.

When multiple CustomDateTime properties are present simultaneously (e.g., expiration_time and not_before), the date attribute of the CustomDateTime class retains only the last value.

        verification_time = datetime.now(UTC) if timestamp is None else timestamp
        if (
            self.expiration_time is not None
            and verification_time >= self.expiration_time.date
        ):
            raise ExpiredMessage
        if self.not_before is not None and verification_time <= self.not_before.date:
            raise NotYetValidMessage

The self.expiration_time.date will be identical to self.not_before.date, which is incorrect.

Here is the reason why this is happening:

    @classmethod
    def validate(cls, v: str):
        """Validate the format."""
        cls.date = isoparse(v)
        return cls(v)

date should be an attribute of the object rather than the class.

Message from string doesn't work

Hey

I want to use siwe for one of my project. However i am having issue. When I create SiweMessage always when i use with string. Even tho the string version is created with SiweMessage a little snippet to reproduce the issue.

from eth_account import Account
from siwe.siwe import SiweMessage

account = Account.create()
test_message = {
        "domain": "test",
        "address": account.address,
        "uri": "test",
        "version": "1",
        "chain_id": "1",
    }
message = SiweMessage(test_message)
signature = account.sign_message(
        messages.encode_defunct(text=message.prepare_message())
    ).signature

# Fails here
message_from_string = SiweMessage(message.prepare_message())

Not able to import siwe

Hello,
I want to use siwe for one of my project in python. However i am having an issue. When i try to import siwe in file it gives me error.
I tried to import in different ways.

from siwe.siwe import SiweMessage
ModuleNotFoundError: No module named 'siwe.siwe'; 'siwe' is not a package
from siwe import siwe
ImportError: cannot import name 'siwe' from 'siwe'

and used this

siwe.SiweMessage({})

but it did not work.
At last i used this

import siwe

and it worked but how do i access SiweMessage and other classes coming from siwe library.
Please Help.

Thanks
Dhruvil Dave

Parsing/Validation and Verification

We should make sure the terms validation and verification are used consistently. We should validate a SIWE message when it is parsed or created. Validation includes schema validation and making sure the message complies with EIP-4361 spec. Verification means the EIP-191 signature is correct and is verified against a given optional domain, timestamp, nonce etc.

For this I propose, we do the following for validation:

  • SIWE message complies with the SIWE ABNF in general
  • SIWE message contains valid types such as for address (EIP-55), authority (see RFC/EIP), Resources (should be URIs) etc.
  • SIWE has all mandatory parameters (e.g., domain, address, issued at)
  • SIWE only uses accepted white spaces (LF)
  • Parameters in SIWE message are correctly ordered (this is required by the ABNF)

As a result of the validation above, it should not be possible to get a SIWE message that is invalid. Then verification includes the following:

  • verifying the EIP-191 signature
  • verifying that expiration time is < date.now() + some minimal clock skew
  • verifying that expected nonce and domain are in the message (for this, a server would need to have a configuration for domain and has to remember issued nonces which should also expire individually -> note, if a server issues a nonce it is still up to the frontend WHEN to create the SIWE message which can use a different issued at and a longer expiration time).
  • verifying not before date < date.now()

Issue in using `SiweMessage` as FastAPI body model

The SiweMessage class barfs on a very basic usage with FastAPI, here's a MWE:

app.py:

import siwe
from fastapi import FastAPI

app = FastAPI()


@app.post("/login")
def siwe_login(data: siwe.SiweMessage):
    pass

If you run it using uvicorn app:app and then access http://127.0.0.1:8000/docs you will get:

$ uvicorn app:app
INFO:     Started server process [******]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     127.0.0.1:41242 - "GET /docs HTTP/1.1" 200 OK
INFO:     127.0.0.1:41242 - "GET /openapi.json HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "site-packages/uvicorn/protocols/http/httptools_impl.py", line 411, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/uvicorn/middleware/proxy_headers.py", line 69, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "site-packages/starlette/applications.py", line 123, in __call__
    await self.middleware_stack(scope, receive, send)
  File "site-packages/starlette/middleware/errors.py", line 186, in __call__
    raise exc
  File "site-packages/starlette/middleware/errors.py", line 164, in __call__
    await self.app(scope, receive, _send)
  File "site-packages/starlette/middleware/exceptions.py", line 62, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "site-packages/starlette/routing.py", line 758, in __call__
    await self.middleware_stack(scope, receive, send)
  File "site-packages/starlette/routing.py", line 778, in app
    await route.handle(scope, receive, send)
  File "site-packages/starlette/routing.py", line 299, in handle
    await self.app(scope, receive, send)
  File "site-packages/starlette/routing.py", line 79, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "site-packages/starlette/routing.py", line 74, in app
    response = await func(request)
               ^^^^^^^^^^^^^^^^^^^
  File "site-packages/fastapi/applications.py", line 1009, in openapi
    return JSONResponse(self.openapi())
                        ^^^^^^^^^^^^^^
  File "site-packages/fastapi/applications.py", line 981, in openapi
    self.openapi_schema = get_openapi(
                          ^^^^^^^^^^^^
  File "site-packages/fastapi/openapi/utils.py", line 475, in get_openapi
    field_mapping, definitions = get_definitions(
                                 ^^^^^^^^^^^^^^^^
  File "site-packages/fastapi/_compat.py", line 227, in get_definitions
    field_mapping, definitions = schema_generator.generate_definitions(
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/json_schema.py", line 372, in generate_definitions
    self.generate_inner(schema)
  File "site-packages/pydantic/json_schema.py", line 547, in generate_inner
    json_schema = current_handler(schema)
                  ^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/_internal/_schema_generation_shared.py", line 36, in __call__
    return self.handler(__core_schema)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/json_schema.py", line 504, in handler_func
    json_schema = generate_for_schema_type(schema_or_field)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/json_schema.py", line 1164, in chain_schema
    return self.generate_inner(schema['steps'][step_index])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/json_schema.py", line 547, in generate_inner
    json_schema = current_handler(schema)
                  ^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/_internal/_schema_generation_shared.py", line 36, in __call__
    return self.handler(__core_schema)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/json_schema.py", line 504, in handler_func
    json_schema = generate_for_schema_type(schema_or_field)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/json_schema.py", line 977, in function_plain_schema
    return self._function_schema(schema)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/json_schema.py", line 942, in _function_schema
    return self.handle_invalid_for_json_schema(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/pydantic/json_schema.py", line 2093, in handle_invalid_for_json_schema
    raise PydanticInvalidForJsonSchema(f'Cannot generate a JsonSchema for {error_info}')
pydantic.errors.PydanticInvalidForJsonSchema: Cannot generate a JsonSchema for core_schema.PlainValidatorFunctionSchema ({'type': 'with-info', 'function': <bound method BaseModel.validate of <class 'siwe.siwe.SiweMessage'>>})

For further information visit https://errors.pydantic.dev/2.6/u/invalid-for-json-schema

This might be related to #39

Should lib check the dictionary message fields?

In this code:

class SiweMessage:
...
    def __init__(self, message: Union[str, dict] = None, abnf: bool = True):
...
        for k, v in message_dict.items():
            if k == "expiration_time" and v is not None:
                self.expiration_time_parsed = isoparse(v)
            elif k == "not_before" and v is not None:
                self.not_before_parsed = isoparse(v)
            setattr(self, k, v)
...

the library didn't check the fields name, so this may be a exploit here when the caller didn't verify user's input.

My question is should the lib check the fields or just leave this to the application developers?

I did it in my app code:

        FIELDS_CHECK = ['ens', 'domain', 'address', 'chainId', 'chain_id', 'uri', 'version', 'statement', 'type', 'nonce', 'issuedAt', 'issued_at', 'signature']
        for k, v in message.items():
            if k not in FIELDS_CHECK:
                app_log.warn(f'Message filed invalid: {k}!')
                return False

AttributeError: 'SiweMessage' object has no attribute 'chain_id'

  File "/Users/ant/GitHub/ethos/src/ethos/main.py", line 48, in post
    siwe_message.validate()
  File "/Users/ant/miniconda3/envs/ws/lib/python3.9/site-packages/siwe/siwe.py", line 188, in validate
    message = eth_account.messages.encode_defunct(text=self.sign_message())
  File "/Users/ant/miniconda3/envs/ws/lib/python3.9/site-packages/siwe/siwe.py", line 175, in sign_message
    message = self.to_message()
  File "/Users/ant/miniconda3/envs/ws/lib/python3.9/site-packages/siwe/siwe.py", line 130, in to_message
    chain_field = f"Chain ID: {self.chain_id or 1}"
AttributeError: 'SiweMessage' object has no attribute 'chain_id'

The message format just copy from notepad example.

while the message content is like this:

    {'domain': '127.0.0.1:4003',
     'address': '0x464eE0FF90B7aC76d3ec8D2a25E6926DeCC88f6d',
     'chainId': '1',
     'uri': 'http://127.0.0.1:4003',
     'version': '1',
     'statement': 'EthOS',
     'type': 'Personal signature',
     'nonce': 'some nonce',
     'issuedAt': '2022-01-24T02:32:10.239Z',
     'signature': 'some signature'
     }
    '''

My question is the field name seems to be not be right, according EIP-4361, the correct field name should has a '-' in it .

So, need I convert the chainId into chain-id ? or we should fix the python lib as well?

JS and Python SIWE message compatibility

Hello,

I am using the frontend code from quick start for SIWE to a Python backend. I am running into a ValueError when parsing on Python side.

Frontend:

async function createSiweMessage(address, statement) {
    const res = await fetch(`${BACKEND_ADDR}/nonce`, {
        credentials: 'include',
    });
    const message = new SiweMessage({
        domain,
        address,
        statement,
        uri: origin,
        version: '1',
        chainId: '1',
        nonce: await res.text()
    });
    console.log(message.prepareMessage());
    return message.prepareMessage();
}

Backend

@app.post("/verify")
async def verify(request: Request, response: Response): 
    siwe_message: SiweMessage = None
    data = await request.json()
    print(f"Data: {data}")
    if  not "message" in data or not "signature" in data:
        response.status_code = 422
        return { "message": 'Expected prepareMessage object as body.' }

    try:
        print(f"Data message: {data['message']}")
        siwe_message: SiweMessage = SiweMessage(message=data["message"])
        print(f"SiweMessage: {siwe_message}")
    except ValueError as ve:
        print(f"Parsing Failed {ve}")

message:

Data: {'message': 'localhost:8080 wants you to sign in with your Ethereum account:\n0xDb26......A14B\n\nSign in with Ethereum to the app.\n\nURI: http://localhost:8080\nVersion: 1\nChain ID: 1\nNonce: "WPxoHcfeqjQ"\nIssued At: 2023-03-09T22:47:49.095Z', 'signature': '0xb0b3cf5737017aa2aaf0b352cfe6454cb6565c016ecfca90068358db95d3e854090af77e93fe9f2e6938948b21ca595d638990e5805334360b0bf70629bb19a61c'}

I noticed that:

siwe/lib/clint.ts joins the statment with a newline:

toMessage(): string {
		const header = `${this.domain} 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}`;

Could this possibly be due to this field in

) # Human-readable ASCII assertion that the user will sign, and it must not contain `\n`.

    statement: Optional[str] = Field(
        None, regex="^[^\n]+$"
    )  # Human-readable ASCII assertion that the user will sign, and it must not contain `\n`.

thanks.

Verify success with invalid signature

Hi, I want to use siwe for my project. I have signed the message in frontend and passing it to python backend. When I try to verify the message with an test invalid signature. it will pass without raising error

python3.8.10

from siwe import SiweMessage
from siwe.siwe import VerificationError, InvalidSignature, MalformedSession, DomainMismatch, ExpiredMessage, MalformedSession, NonceMismatch, NotYetValidMessage

message = {
    "domain":"localhost:3000",
    "address":"0x8D873cA5De39ae2aCF371515823ab2EDb5c7c928",
    "statement":"Sign in with Ethereum to the app.",
    "uri":"http://localhost:3000",
    "version":"1",
    "chain_id": "1",
    "nonce":"5854bdb2953c9e9974c4bfce22143f7403d911d99277577cd40a85926320dc54",
    "issued_at":"2022-11-01T17:04:08.402Z"
}
message: SiweMessage = SiweMessage(message=message)
print(message.prepare_message())
try:
    # try passing an invalid signature
    message.verify(signature="test_invalid_signature")
    # You can also specify other checks (e.g. the nonce or domain expected).
    print("Authentication attempt accepted.")
except ValueError:
    # Invalid message
    print("Authentication attempt rejected. Invalid message.")
except NotYetValidMessage:
    # The message is not yet valid
    print("Authentication attempt rejected. The message is not yet valid.")
except ExpiredMessage:
    # The message has expired
    print("Authentication attempt rejected. The message has expired.")
except DomainMismatch:
    print("Authentication attempt rejected. Domain mismatch.")
except NonceMismatch:
    print("Authentication attempt rejected. The nonce is not the expected one.")
except MalformedSession as e:
    # e.missing_fields contains the missing information needed for validation
    print("Authentication attempt rejected. Missing fields")
except InvalidSignature:
    print("Authentication attempt rejected. Invalid signature.")
except VerificationError:
    # VerificationError
    print("Authentication attempt rejected. Verification Error.")

# Message has been verified. Authentication complete. Continue with authorization/other.

Both valid and invalid signature will pass the verification.
Is there anything I missed or did wrong?
Thanks

Full type annotations

Would it be possible to add full type annotations to this repo? Currently we have to skip SIWE in our mypy config during lint check.

Support EIP-1271: Standard Signature Validation Method for Contracts

Add support for EIP-1271 to validate contract signatures.

https://eips.ethereum.org/EIPS/eip-1271

siwe-py/siwe/siwe.py

Lines 223 to 233 in 58bdc99

def check_contract_wallet_signature(message: SiweMessage, provider: HTTPProvider):
"""
Calls the EIP-1271 method for Smart Contract wallets,
:param message: The EIP-4361 parsed message
:param provider: A Web3 provider able to perform a contract check.
:return: True if the signature is valid per EIP-1271.
"""
raise NotImplementedError(
"siwe does not yet support EIP-1271 method signature verification."
)

clarify intended behavior of `validate`

The validate and verify functions (unclear of the distinction, but appears to be discussed in #24) has an inconsistent implementation in nearly every language. I am curious what the intended implementation is, totally understand that different languages have different idioms to adhere to.

  • Python: the validate function does not return anything, merely raises an exception if there is an error in parsing. However, the docstring indicates an intended boolean return type (true if ok, false otherwise).
  • Javascript: the equivalent validate function returns {Promise<SiweMessage>} This object if valid., which is a parsed message with all the message's fields.
  • Rust: my read of the verify function (not validate) is that it returns the publickey, if valid, and throws an error otherwise.
  • Golang: verify function looks to be same as in Rust
  • Ruby: I think this function returns true if valid, and raises an exception otherwise.
  • Elixir: AFAICT this implementation actually does return true if valid and false otherwise, as it catches any exceptions to finally return a boolean.

In particular, Javascript looks to be the outlier with its behavior - it does validate the message but additionally returns the message. However, it's also the language with the most adoption / usage, so I assumed it was the 'canonical' approach.

Happy to help with the changes if we can confirm the desired behavior, thanks for working on the protocol!

Refactor CustomDateField

The current implementation is a bit hacky, and would not work with pydantic v2. It could be made into a datetime object, which Pydantic can parse from various formats,making it easier to create a message if not using a string. There should still however be a manual check when parsed from a string that it fits the ISO standard. Some custom logic (or someting in dateutil?) may be needed to turn the datetime into the proper format

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.