signinwithethereum 5.0.0__tar.gz → 5.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/PKG-INFO +60 -9
- signinwithethereum-5.0.2/README.md +151 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/pyproject.toml +1 -1
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/siwe/grammars/eip4361.py +1 -8
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/siwe/grammars/rfc3339.py +1 -8
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/siwe/siwe.py +8 -3
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/tests/test_siwe.py +55 -1
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/uv.lock +5 -5
- signinwithethereum-5.0.0/README.md +0 -100
- signinwithethereum-5.0.0/siwe/grammars/rfc5234.py +0 -20
- signinwithethereum-5.0.0/test-vectors/.git +0 -1
- signinwithethereum-5.0.0/test-vectors/LICENSE-APACHE +0 -202
- signinwithethereum-5.0.0/test-vectors/LICENSE-MIT +0 -26
- signinwithethereum-5.0.0/test-vectors/README.md +0 -33
- signinwithethereum-5.0.0/test-vectors/vectors/grammar/invalid_chars.json +0 -202
- signinwithethereum-5.0.0/test-vectors/vectors/grammar/invalid_resources.json +0 -18
- signinwithethereum-5.0.0/test-vectors/vectors/grammar/invalid_uris.json +0 -19
- signinwithethereum-5.0.0/test-vectors/vectors/grammar/valid_chars.json +0 -42
- signinwithethereum-5.0.0/test-vectors/vectors/grammar/valid_resources.json +0 -34
- signinwithethereum-5.0.0/test-vectors/vectors/grammar/valid_specification.json +0 -74
- signinwithethereum-5.0.0/test-vectors/vectors/grammar/valid_uris.json +0 -308
- signinwithethereum-5.0.0/test-vectors/vectors/objects/message_objects.json +0 -59
- signinwithethereum-5.0.0/test-vectors/vectors/objects/parsing_negative_objects.json +0 -349
- signinwithethereum-5.0.0/test-vectors/vectors/parsing/parsing_negative.json +0 -39
- signinwithethereum-5.0.0/test-vectors/vectors/parsing/parsing_positive.json +0 -269
- signinwithethereum-5.0.0/test-vectors/vectors/parsing/parsing_warnings.json +0 -38
- signinwithethereum-5.0.0/test-vectors/vectors/verification/eip1271.json +0 -10
- signinwithethereum-5.0.0/test-vectors/vectors/verification/verification_negative.json +0 -125
- signinwithethereum-5.0.0/test-vectors/vectors/verification/verification_positive.json +0 -51
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/.codex +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/.github/workflows/publish.yml +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/.gitignore +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/.gitmodules +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/LICENSE-APACHE +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/LICENSE-MIT +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/siwe/__init__.py +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/siwe/defs.py +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/siwe/grammars/__init__.py +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/siwe/parsed.py +0 -0
- {signinwithethereum-5.0.0 → signinwithethereum-5.0.2}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: signinwithethereum
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.2
|
|
4
4
|
Summary: A Python implementation of Sign in with Ethereum (EIP-4361).
|
|
5
5
|
Project-URL: Homepage, https://siwe.xyz
|
|
6
6
|
Project-URL: Repository, https://github.com/signinwithethereum/siwe-py
|
|
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
|
|
23
23
|
# Sign in with Ethereum
|
|
24
24
|
|
|
25
|
-
This package provides a Python implementation of EIP-4361: Sign in
|
|
25
|
+
This package provides a Python implementation of EIP-4361: Sign in with Ethereum.
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ SIWE can be easily installed in any Python project with pip:
|
|
|
32
32
|
pip install signinwithethereum
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
The distribution is published as `signinwithethereum
|
|
35
|
+
The distribution is published as `signinwithethereum`; the import name is `siwe`:
|
|
36
36
|
|
|
37
37
|
```python
|
|
38
38
|
from siwe import SiweMessage
|
|
@@ -59,16 +59,60 @@ message = SiweMessage(domain="login.xyz", address="0x1234...", ...)
|
|
|
59
59
|
|
|
60
60
|
### Verifying and Authenticating a SIWE Message
|
|
61
61
|
|
|
62
|
-
Verification and authentication is performed via EIP-191, using the `address` field of the `SiweMessage` as the expected signer. The
|
|
62
|
+
Verification and authentication is performed via EIP-191, using the `address` field of the `SiweMessage` as the expected signer. The `verify` method checks message structural integrity, signature address validity, and time-based validity attributes.
|
|
63
|
+
|
|
64
|
+
Replay protection relies on a **single-use nonce** that your server issues, stores alongside the pending session, passes to `verify`, and consumes on success. Always pass the `nonce` you issued — a signature verified without a nonce check can be replayed.
|
|
63
65
|
|
|
64
66
|
```python
|
|
65
67
|
try:
|
|
66
|
-
message.verify(
|
|
67
|
-
|
|
68
|
+
message.verify(
|
|
69
|
+
signature="0x...",
|
|
70
|
+
domain="example.com",
|
|
71
|
+
nonce=expected_nonce, # the nonce your server issued for this session
|
|
72
|
+
uri="https://example.com/login",
|
|
73
|
+
chain_id=1,
|
|
74
|
+
strict=True,
|
|
75
|
+
)
|
|
76
|
+
# Consume the nonce now so it cannot be replayed.
|
|
68
77
|
except siwe.VerificationError:
|
|
69
78
|
# Invalid
|
|
70
79
|
```
|
|
71
80
|
|
|
81
|
+
Passing `strict=True` enforces that `domain`, `uri`, `chain_id`, and `nonce` are all supplied. Prefer it for authentication flows.
|
|
82
|
+
|
|
83
|
+
### Smart-contract wallet signatures (EIP-1271 / EIP-6492)
|
|
84
|
+
|
|
85
|
+
For signatures produced by contract wallets (Safe, Argent, etc.) rather than
|
|
86
|
+
externally owned accounts, pass a `web3` provider to `verify`. The
|
|
87
|
+
authentication arguments (`strict`, `domain`, `nonce`, `uri`, `chain_id`) still
|
|
88
|
+
apply — the provider only changes how the signature itself is verified:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from web3 import HTTPProvider
|
|
92
|
+
|
|
93
|
+
message.verify(
|
|
94
|
+
signature="0x...",
|
|
95
|
+
domain="example.com",
|
|
96
|
+
nonce=expected_nonce,
|
|
97
|
+
uri="https://example.com/login",
|
|
98
|
+
chain_id=1,
|
|
99
|
+
strict=True,
|
|
100
|
+
provider=HTTPProvider("https://mainnet.infura.io/v3/..."),
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
EOA recovery is tried first; if it fails, the signature is checked on-chain via
|
|
105
|
+
`isValidSignature(bytes32,bytes)` per EIP-1271. Signatures carrying the EIP-6492
|
|
106
|
+
magic suffix are handed to the universal off-chain validator bytecode via
|
|
107
|
+
`eth_call`, which covers counterfactual (undeployed) wallets as well as
|
|
108
|
+
already-deployed ones. The provider's chain id is checked against the message's
|
|
109
|
+
`chain_id` before any on-chain call.
|
|
110
|
+
|
|
111
|
+
Note: this is verification only. EIP-6492 allows a verifier to optionally
|
|
112
|
+
submit the factory transaction after a successful check to finalize on-chain
|
|
113
|
+
deployment ("side-effectful" verification). This library does not do that — if
|
|
114
|
+
you need the wallet actually deployed, submit the factory call yourself.
|
|
115
|
+
|
|
72
116
|
### Serialization of a SIWE Message
|
|
73
117
|
|
|
74
118
|
`SiweMessage` instances can also be serialized as their EIP-4361 string representations via the `prepare_message` method:
|
|
@@ -84,7 +128,14 @@ Parsing and verifying a `SiweMessage` is easy:
|
|
|
84
128
|
```python
|
|
85
129
|
try:
|
|
86
130
|
message = SiweMessage.from_message(eip_4361_string)
|
|
87
|
-
message.verify(
|
|
131
|
+
message.verify(
|
|
132
|
+
signature,
|
|
133
|
+
domain="example.com",
|
|
134
|
+
nonce=expected_nonce,
|
|
135
|
+
uri="https://example.com/login",
|
|
136
|
+
chain_id=1,
|
|
137
|
+
strict=True,
|
|
138
|
+
)
|
|
88
139
|
except ValueError:
|
|
89
140
|
# Invalid message format
|
|
90
141
|
print("Authentication attempt rejected.")
|
|
@@ -97,7 +148,7 @@ except siwe.NonceMismatch:
|
|
|
97
148
|
except siwe.InvalidSignature:
|
|
98
149
|
print("Authentication attempt rejected.")
|
|
99
150
|
|
|
100
|
-
# Message has been verified.
|
|
151
|
+
# Message has been verified. Invalidate the stored nonce, then continue with authorization/other.
|
|
101
152
|
```
|
|
102
153
|
|
|
103
154
|
## Testing
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Sign in with Ethereum
|
|
2
|
+
|
|
3
|
+
This package provides a Python implementation of EIP-4361: Sign in with Ethereum.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
SIWE can be easily installed in any Python project with pip:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install signinwithethereum
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The distribution is published as `signinwithethereum`; the import name is `siwe`:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from siwe import SiweMessage
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
SIWE provides a `SiweMessage` class which implements EIP-4361.
|
|
22
|
+
|
|
23
|
+
### Parsing a SIWE Message
|
|
24
|
+
|
|
25
|
+
Parsing is done by initializing a `SiweMessage` object with an EIP-4361 formatted string:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from siwe import SiweMessage
|
|
29
|
+
message = SiweMessage.from_message(message=eip_4361_string)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or to initialize a `SiweMessage` as a `pydantic.BaseModel` right away:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
message = SiweMessage(domain="login.xyz", address="0x1234...", ...)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Verifying and Authenticating a SIWE Message
|
|
39
|
+
|
|
40
|
+
Verification and authentication is performed via EIP-191, using the `address` field of the `SiweMessage` as the expected signer. The `verify` method checks message structural integrity, signature address validity, and time-based validity attributes.
|
|
41
|
+
|
|
42
|
+
Replay protection relies on a **single-use nonce** that your server issues, stores alongside the pending session, passes to `verify`, and consumes on success. Always pass the `nonce` you issued — a signature verified without a nonce check can be replayed.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
try:
|
|
46
|
+
message.verify(
|
|
47
|
+
signature="0x...",
|
|
48
|
+
domain="example.com",
|
|
49
|
+
nonce=expected_nonce, # the nonce your server issued for this session
|
|
50
|
+
uri="https://example.com/login",
|
|
51
|
+
chain_id=1,
|
|
52
|
+
strict=True,
|
|
53
|
+
)
|
|
54
|
+
# Consume the nonce now so it cannot be replayed.
|
|
55
|
+
except siwe.VerificationError:
|
|
56
|
+
# Invalid
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Passing `strict=True` enforces that `domain`, `uri`, `chain_id`, and `nonce` are all supplied. Prefer it for authentication flows.
|
|
60
|
+
|
|
61
|
+
### Smart-contract wallet signatures (EIP-1271 / EIP-6492)
|
|
62
|
+
|
|
63
|
+
For signatures produced by contract wallets (Safe, Argent, etc.) rather than
|
|
64
|
+
externally owned accounts, pass a `web3` provider to `verify`. The
|
|
65
|
+
authentication arguments (`strict`, `domain`, `nonce`, `uri`, `chain_id`) still
|
|
66
|
+
apply — the provider only changes how the signature itself is verified:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from web3 import HTTPProvider
|
|
70
|
+
|
|
71
|
+
message.verify(
|
|
72
|
+
signature="0x...",
|
|
73
|
+
domain="example.com",
|
|
74
|
+
nonce=expected_nonce,
|
|
75
|
+
uri="https://example.com/login",
|
|
76
|
+
chain_id=1,
|
|
77
|
+
strict=True,
|
|
78
|
+
provider=HTTPProvider("https://mainnet.infura.io/v3/..."),
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
EOA recovery is tried first; if it fails, the signature is checked on-chain via
|
|
83
|
+
`isValidSignature(bytes32,bytes)` per EIP-1271. Signatures carrying the EIP-6492
|
|
84
|
+
magic suffix are handed to the universal off-chain validator bytecode via
|
|
85
|
+
`eth_call`, which covers counterfactual (undeployed) wallets as well as
|
|
86
|
+
already-deployed ones. The provider's chain id is checked against the message's
|
|
87
|
+
`chain_id` before any on-chain call.
|
|
88
|
+
|
|
89
|
+
Note: this is verification only. EIP-6492 allows a verifier to optionally
|
|
90
|
+
submit the factory transaction after a successful check to finalize on-chain
|
|
91
|
+
deployment ("side-effectful" verification). This library does not do that — if
|
|
92
|
+
you need the wallet actually deployed, submit the factory call yourself.
|
|
93
|
+
|
|
94
|
+
### Serialization of a SIWE Message
|
|
95
|
+
|
|
96
|
+
`SiweMessage` instances can also be serialized as their EIP-4361 string representations via the `prepare_message` method:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
print(message.prepare_message())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Example
|
|
103
|
+
|
|
104
|
+
Parsing and verifying a `SiweMessage` is easy:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
try:
|
|
108
|
+
message = SiweMessage.from_message(eip_4361_string)
|
|
109
|
+
message.verify(
|
|
110
|
+
signature,
|
|
111
|
+
domain="example.com",
|
|
112
|
+
nonce=expected_nonce,
|
|
113
|
+
uri="https://example.com/login",
|
|
114
|
+
chain_id=1,
|
|
115
|
+
strict=True,
|
|
116
|
+
)
|
|
117
|
+
except ValueError:
|
|
118
|
+
# Invalid message format
|
|
119
|
+
print("Authentication attempt rejected.")
|
|
120
|
+
except siwe.ExpiredMessage:
|
|
121
|
+
print("Authentication attempt rejected.")
|
|
122
|
+
except siwe.DomainMismatch:
|
|
123
|
+
print("Authentication attempt rejected.")
|
|
124
|
+
except siwe.NonceMismatch:
|
|
125
|
+
print("Authentication attempt rejected.")
|
|
126
|
+
except siwe.InvalidSignature:
|
|
127
|
+
print("Authentication attempt rejected.")
|
|
128
|
+
|
|
129
|
+
# Message has been verified. Invalidate the stored nonce, then continue with authorization/other.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Testing
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git submodule update --init
|
|
136
|
+
uv sync
|
|
137
|
+
uv run pytest
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## See Also
|
|
141
|
+
|
|
142
|
+
- [Sign in with Ethereum: TypeScript](https://github.com/signinwithethereum/siwe)
|
|
143
|
+
- [SIWE website: siwe.xyz](https://siwe.xyz)
|
|
144
|
+
- [EIP-4361 Specification Draft](https://eips.ethereum.org/EIPS/eip-4361)
|
|
145
|
+
- [EIP-191 Specification](https://eips.ethereum.org/EIPS/eip-191)
|
|
146
|
+
|
|
147
|
+
## Disclaimer
|
|
148
|
+
|
|
149
|
+
Our Python library for Sign in with Ethereum has not yet undergone a formal
|
|
150
|
+
security audit. We welcome continued feedback on the usability, architecture,
|
|
151
|
+
and security of this implementation.
|
|
@@ -6,7 +6,7 @@ from abnf.grammars import rfc3986
|
|
|
6
6
|
from abnf.grammars.misc import load_grammar_rules
|
|
7
7
|
from abnf.parser import Rule as _Rule
|
|
8
8
|
|
|
9
|
-
from . import rfc3339
|
|
9
|
+
from . import rfc3339
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@load_grammar_rules(
|
|
@@ -17,13 +17,7 @@ from . import rfc3339, rfc5234
|
|
|
17
17
|
("scheme", rfc3986.Rule("scheme")),
|
|
18
18
|
("reserved", rfc3986.Rule("reserved")),
|
|
19
19
|
("unreserved", rfc3986.Rule("unreserved")),
|
|
20
|
-
("reserved", rfc3986.Rule("reserved")),
|
|
21
20
|
("pchar", rfc3986.Rule("pchar")),
|
|
22
|
-
# RFC 5234
|
|
23
|
-
("LF", rfc5234.Rule("LF")),
|
|
24
|
-
("HEXDIG", rfc5234.Rule("HEXDIG")),
|
|
25
|
-
("ALPHA", rfc5234.Rule("ALPHA")),
|
|
26
|
-
("DIGIT", rfc5234.Rule("DIGIT")),
|
|
27
21
|
# RFC 3339
|
|
28
22
|
("date-time", rfc3339.Rule("date-time")),
|
|
29
23
|
]
|
|
@@ -41,7 +35,6 @@ class Rule(_Rule):
|
|
|
41
35
|
"domain = authority",
|
|
42
36
|
'address = "0x" 40HEXDIG',
|
|
43
37
|
'statement = *( reserved / unreserved / " " )',
|
|
44
|
-
"uri = URI",
|
|
45
38
|
'version = "1"',
|
|
46
39
|
"nonce = 8*( ALPHA / DIGIT )",
|
|
47
40
|
"issued-at = date-time",
|
|
@@ -5,15 +5,8 @@ from typing import ClassVar, List
|
|
|
5
5
|
from abnf.grammars.misc import load_grammar_rules
|
|
6
6
|
from abnf.parser import Rule as _Rule
|
|
7
7
|
|
|
8
|
-
from . import rfc5234
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
@load_grammar_rules(
|
|
12
|
-
[
|
|
13
|
-
# RFC 5234
|
|
14
|
-
("DIGIT", rfc5234.Rule("DIGIT")),
|
|
15
|
-
]
|
|
16
|
-
)
|
|
9
|
+
@load_grammar_rules()
|
|
17
10
|
class Rule(_Rule):
|
|
18
11
|
"""Rules from RFC 3339."""
|
|
19
12
|
|
|
@@ -402,16 +402,21 @@ class SiweMessage(BaseModel):
|
|
|
402
402
|
:param provider: A Web3 provider able to perform a contract check. This is
|
|
403
403
|
required if support for Smart Contract Wallets that implement EIP-1271 or
|
|
404
404
|
EIP-6492 is needed.
|
|
405
|
-
:param strict: When True, requires uri
|
|
405
|
+
:param strict: When True, requires domain, uri, chain_id, and nonce
|
|
406
|
+
parameters to be provided. The nonce must be a single-use value issued by
|
|
407
|
+
the relying party and consumed on successful verification — reusing the
|
|
408
|
+
same nonce across sessions defeats replay protection.
|
|
406
409
|
:return: None if the message is valid and raises an exception otherwise
|
|
407
410
|
"""
|
|
408
411
|
if strict:
|
|
412
|
+
if domain is None:
|
|
413
|
+
raise VerificationError("Strict mode requires domain parameter")
|
|
409
414
|
if uri is None:
|
|
410
415
|
raise VerificationError("Strict mode requires uri parameter")
|
|
411
416
|
if chain_id is None:
|
|
412
417
|
raise VerificationError("Strict mode requires chain_id parameter")
|
|
413
|
-
if
|
|
414
|
-
raise VerificationError("Strict mode requires
|
|
418
|
+
if nonce is None:
|
|
419
|
+
raise VerificationError("Strict mode requires nonce parameter")
|
|
415
420
|
|
|
416
421
|
message = encode_defunct(text=self.prepare_message())
|
|
417
422
|
w3 = Web3(provider=provider)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
3
5
|
|
|
4
6
|
import abnf
|
|
5
7
|
import pytest
|
|
@@ -367,7 +369,46 @@ class TestFieldValidation:
|
|
|
367
369
|
def test_strict_mode_requires_domain(self):
|
|
368
370
|
m = SiweMessage(**self.base)
|
|
369
371
|
with pytest.raises(VerificationError):
|
|
370
|
-
m.verify(
|
|
372
|
+
m.verify(
|
|
373
|
+
"0x" + "00" * 65,
|
|
374
|
+
uri="https://ex.com",
|
|
375
|
+
chain_id=1,
|
|
376
|
+
nonce="12345678",
|
|
377
|
+
strict=True,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def test_strict_mode_requires_uri(self):
|
|
381
|
+
m = SiweMessage(**self.base)
|
|
382
|
+
with pytest.raises(VerificationError):
|
|
383
|
+
m.verify(
|
|
384
|
+
"0x" + "00" * 65,
|
|
385
|
+
domain="ex.com",
|
|
386
|
+
chain_id=1,
|
|
387
|
+
nonce="12345678",
|
|
388
|
+
strict=True,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def test_strict_mode_requires_chain_id(self):
|
|
392
|
+
m = SiweMessage(**self.base)
|
|
393
|
+
with pytest.raises(VerificationError):
|
|
394
|
+
m.verify(
|
|
395
|
+
"0x" + "00" * 65,
|
|
396
|
+
domain="ex.com",
|
|
397
|
+
uri="https://ex.com",
|
|
398
|
+
nonce="12345678",
|
|
399
|
+
strict=True,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def test_strict_mode_requires_nonce(self):
|
|
403
|
+
m = SiweMessage(**self.base)
|
|
404
|
+
with pytest.raises(VerificationError):
|
|
405
|
+
m.verify(
|
|
406
|
+
"0x" + "00" * 65,
|
|
407
|
+
domain="ex.com",
|
|
408
|
+
uri="https://ex.com",
|
|
409
|
+
chain_id=1,
|
|
410
|
+
strict=True,
|
|
411
|
+
)
|
|
371
412
|
|
|
372
413
|
|
|
373
414
|
def _get_abnf_rule(rule_name):
|
|
@@ -502,3 +543,16 @@ class TestValidSpecification:
|
|
|
502
543
|
siwe_message = SiweMessage.from_message(message=test["msg"], abnf=abnf_mode)
|
|
503
544
|
for key, value in test["items"].items():
|
|
504
545
|
assert getattr(siwe_message, key) == value
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def test_grammar_loads_without_grammar_warnings():
|
|
549
|
+
# Grammars load once per process at import, so check in a fresh interpreter.
|
|
550
|
+
code = (
|
|
551
|
+
"import warnings, abnf\n"
|
|
552
|
+
"warnings.simplefilter('error', getattr(abnf, 'GrammarWarning', UserWarning))\n"
|
|
553
|
+
"import siwe.grammars.eip4361\n"
|
|
554
|
+
)
|
|
555
|
+
result = subprocess.run(
|
|
556
|
+
[sys.executable, "-c", code], capture_output=True, text=True
|
|
557
|
+
)
|
|
558
|
+
assert result.returncode == 0, result.stderr
|
|
@@ -4,11 +4,11 @@ requires-python = ">=3.10, <4.0"
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "abnf"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.9.0"
|
|
8
8
|
source = { registry = "https://pypi.org/simple" }
|
|
9
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/78/55/5514e564539e23a4948795695d5f25c184f0e02ac18695db1c8d5e6f3d05/abnf-2.9.0.tar.gz", hash = "sha256:45af3643da2281e34b03fba467bdeb09797f5552d140907a578c2d8ec10fcdd5", size = 1088572, upload-time = "2026-08-27T11:12:45.963Z" }
|
|
10
10
|
wheels = [
|
|
11
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/43/4c/cb77419f4dd5e6086550a05f4bce9eb521c80a2b877215065331c2b476b7/abnf-2.9.0-py3-none-any.whl", hash = "sha256:8592e174c05cc6010f5250f2292b832993f92e2ae136d7618368ee89d649435e", size = 78100, upload-time = "2026-08-27T11:12:44.185Z" },
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
[[package]]
|
|
@@ -774,7 +774,7 @@ name = "exceptiongroup"
|
|
|
774
774
|
version = "1.3.1"
|
|
775
775
|
source = { registry = "https://pypi.org/simple" }
|
|
776
776
|
dependencies = [
|
|
777
|
-
{ name = "typing-extensions"
|
|
777
|
+
{ name = "typing-extensions" },
|
|
778
778
|
]
|
|
779
779
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
780
780
|
wheels = [
|
|
@@ -1633,7 +1633,7 @@ wheels = [
|
|
|
1633
1633
|
|
|
1634
1634
|
[[package]]
|
|
1635
1635
|
name = "signinwithethereum"
|
|
1636
|
-
version = "5.0.
|
|
1636
|
+
version = "5.0.1"
|
|
1637
1637
|
source = { editable = "." }
|
|
1638
1638
|
dependencies = [
|
|
1639
1639
|
{ name = "abnf" },
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# Sign in with Ethereum
|
|
2
|
-
|
|
3
|
-
This package provides a Python implementation of EIP-4361: Sign in With Ethereum.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
SIWE can be easily installed in any Python project with pip:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pip install signinwithethereum
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
The distribution is published as `signinwithethereum`, but the import name remains `siwe`:
|
|
14
|
-
|
|
15
|
-
```python
|
|
16
|
-
from siwe import SiweMessage
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
SIWE provides a `SiweMessage` class which implements EIP-4361.
|
|
22
|
-
|
|
23
|
-
### Parsing a SIWE Message
|
|
24
|
-
|
|
25
|
-
Parsing is done by initializing a `SiweMessage` object with an EIP-4361 formatted string:
|
|
26
|
-
|
|
27
|
-
```python
|
|
28
|
-
from siwe import SiweMessage
|
|
29
|
-
message = SiweMessage.from_message(message=eip_4361_string)
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Or to initialize a `SiweMessage` as a `pydantic.BaseModel` right away:
|
|
33
|
-
|
|
34
|
-
```python
|
|
35
|
-
message = SiweMessage(domain="login.xyz", address="0x1234...", ...)
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Verifying and Authenticating a SIWE Message
|
|
39
|
-
|
|
40
|
-
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.
|
|
41
|
-
|
|
42
|
-
```python
|
|
43
|
-
try:
|
|
44
|
-
message.verify(signature="0x...")
|
|
45
|
-
# You can also specify other checks (e.g. the nonce or domain expected).
|
|
46
|
-
except siwe.VerificationError:
|
|
47
|
-
# Invalid
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Serialization of a SIWE Message
|
|
51
|
-
|
|
52
|
-
`SiweMessage` instances can also be serialized as their EIP-4361 string representations via the `prepare_message` method:
|
|
53
|
-
|
|
54
|
-
```python
|
|
55
|
-
print(message.prepare_message())
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Example
|
|
59
|
-
|
|
60
|
-
Parsing and verifying a `SiweMessage` is easy:
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
try:
|
|
64
|
-
message = SiweMessage.from_message(eip_4361_string)
|
|
65
|
-
message.verify(signature, nonce="abcdef", domain="example.com")
|
|
66
|
-
except ValueError:
|
|
67
|
-
# Invalid message format
|
|
68
|
-
print("Authentication attempt rejected.")
|
|
69
|
-
except siwe.ExpiredMessage:
|
|
70
|
-
print("Authentication attempt rejected.")
|
|
71
|
-
except siwe.DomainMismatch:
|
|
72
|
-
print("Authentication attempt rejected.")
|
|
73
|
-
except siwe.NonceMismatch:
|
|
74
|
-
print("Authentication attempt rejected.")
|
|
75
|
-
except siwe.InvalidSignature:
|
|
76
|
-
print("Authentication attempt rejected.")
|
|
77
|
-
|
|
78
|
-
# Message has been verified. Authentication complete. Continue with authorization/other.
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Testing
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
git submodule update --init
|
|
85
|
-
uv sync
|
|
86
|
-
uv run pytest
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## See Also
|
|
90
|
-
|
|
91
|
-
- [Sign in with Ethereum: TypeScript](https://github.com/signinwithethereum/siwe)
|
|
92
|
-
- [SIWE website: siwe.xyz](https://siwe.xyz)
|
|
93
|
-
- [EIP-4361 Specification Draft](https://eips.ethereum.org/EIPS/eip-4361)
|
|
94
|
-
- [EIP-191 Specification](https://eips.ethereum.org/EIPS/eip-191)
|
|
95
|
-
|
|
96
|
-
## Disclaimer
|
|
97
|
-
|
|
98
|
-
Our Python library for Sign in with Ethereum has not yet undergone a formal
|
|
99
|
-
security audit. We welcome continued feedback on the usability, architecture,
|
|
100
|
-
and security of this implementation.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"""Primitive ABNF definition."""
|
|
2
|
-
|
|
3
|
-
from typing import ClassVar, List
|
|
4
|
-
|
|
5
|
-
from abnf.grammars.misc import load_grammar_rules
|
|
6
|
-
from abnf.parser import Rule as _Rule
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@load_grammar_rules()
|
|
10
|
-
class Rule(_Rule):
|
|
11
|
-
"""Rules from RFC 5234."""
|
|
12
|
-
|
|
13
|
-
grammar: ClassVar[List] = [
|
|
14
|
-
"ALPHA = %x41-5A / %x61-7A ; A-Z / a-z",
|
|
15
|
-
"LF = %x0A \
|
|
16
|
-
; linefeed",
|
|
17
|
-
"DIGIT = %x30-39 \
|
|
18
|
-
; 0-9",
|
|
19
|
-
'HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"',
|
|
20
|
-
]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
gitdir: ../.git/modules/test-vectors
|