siwe-rb 0.2.0 → 0.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +3 -78
- data/README.md +21 -3
- data/lib/siwe/adapter.rb +7 -11
- data/lib/siwe/crypto.rb +132 -0
- data/lib/siwe/message.rb +10 -4
- data/lib/siwe/smart_wallet.rb +5 -9
- data/lib/siwe/util.rb +6 -10
- data/lib/siwe/version.rb +1 -1
- data/lib/siwe.rb +1 -0
- metadata +9 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b6bc3df2351b75f3628baf83e1a49b32967538ce242b76407b0d978f7777fa2
|
|
4
|
+
data.tar.gz: ddfd9da52f3d0dd77504c3df22ec547091b7038356623891ac465d008298d68b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b318a4225a6ea4c31c142720ba981a0fd6d465f69a0e150aece84898a5144299c0b8e5f4b89db75c230fe156a9a76f0b21f8ed926da8884458a05aab078e2f77
|
|
7
|
+
data.tar.gz: d5980f23b3f96765fcdf617998cb5d0554aa326fef6c0a2bf9401943a53478bbec452b1f552d0f87d57c9c70f5cf6b99e5bdb6e2b7b36411919ab1d1b9045555
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] — 2026-09-11
|
|
4
|
+
|
|
5
|
+
- Replace the `eth` runtime dependency with `keccak` and Ruby OpenSSL for EOA signer recovery, EIP-55 checksums, EIP-191 hashing, and smart-wallet ABI encoding. This removes `rbsecp256k1` and its conflicting build-tool dependencies from installations such as Discourse.
|
|
6
|
+
- Default to `Siwe::Adapter::OpenSSL`; remove `Siwe::Adapter::EthGem`. Custom adapters should raise `Siwe::Crypto::SignatureError` for recovery failures. Message verification APIs and smart-wallet chain binding are unchanged.
|
|
7
|
+
- Signing with `eth` now requires an explicit application dependency. Package smoke tests and the regular test suite no longer depend on it.
|
|
8
|
+
- Preserve EOA verification of raw binary signatures and `0X`-prefixed hex. Malformed contract-signature hex now returns `:invalid_signature` without calling RPC; odd-length hex is rejected instead of implicitly left-padded.
|
|
9
|
+
- Propagate OpenSSL configuration errors instead of reporting them as invalid signatures.
|
|
10
|
+
|
|
3
11
|
## [0.2.0] — 2026-05-26
|
|
4
12
|
|
|
5
13
|
Spec-conformance pass. Several behaviour changes; the only one that breaks API contract is the strict chain-id requirement on the smart-wallet path (3).
|
data/Gemfile.lock
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
siwe-rb (0.
|
|
5
|
-
|
|
4
|
+
siwe-rb (0.3.0)
|
|
5
|
+
keccak (~> 1.3)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
@@ -10,70 +10,26 @@ GEM
|
|
|
10
10
|
addressable (2.9.0)
|
|
11
11
|
public_suffix (>= 2.0.2, < 8.0)
|
|
12
12
|
ast (2.4.3)
|
|
13
|
-
base64 (0.3.0)
|
|
14
13
|
bigdecimal (3.3.1)
|
|
15
|
-
bls12-381 (0.3.1)
|
|
16
|
-
bigdecimal
|
|
17
|
-
h2c (>= 0.2.1, < 0.3)
|
|
18
14
|
crack (1.0.1)
|
|
19
15
|
bigdecimal
|
|
20
16
|
rexml
|
|
21
17
|
diff-lcs (1.6.2)
|
|
22
18
|
docile (1.4.1)
|
|
23
|
-
ecdsa (1.2.0)
|
|
24
|
-
eth (0.5.17)
|
|
25
|
-
base64 (~> 0.1)
|
|
26
|
-
bigdecimal (~> 3.1)
|
|
27
|
-
bls12-381 (~> 0.3)
|
|
28
|
-
forwardable (~> 1.3)
|
|
29
|
-
httpx (~> 1.6)
|
|
30
|
-
keccak (~> 1.3)
|
|
31
|
-
konstructor (~> 1.0)
|
|
32
|
-
openssl (~> 3.3)
|
|
33
|
-
rbsecp256k1 (~> 6.0)
|
|
34
|
-
scrypt (~> 3.0)
|
|
35
|
-
ffi (1.17.4)
|
|
36
|
-
ffi (1.17.4-aarch64-linux-gnu)
|
|
37
|
-
ffi (1.17.4-aarch64-linux-musl)
|
|
38
|
-
ffi (1.17.4-arm-linux-gnu)
|
|
39
|
-
ffi (1.17.4-arm-linux-musl)
|
|
40
|
-
ffi (1.17.4-arm64-darwin)
|
|
41
|
-
ffi (1.17.4-x86-linux-gnu)
|
|
42
|
-
ffi (1.17.4-x86-linux-musl)
|
|
43
|
-
ffi (1.17.4-x86_64-darwin)
|
|
44
|
-
ffi (1.17.4-x86_64-linux-gnu)
|
|
45
|
-
ffi (1.17.4-x86_64-linux-musl)
|
|
46
|
-
ffi-compiler (1.4.2)
|
|
47
|
-
ffi (>= 1.15.5)
|
|
48
|
-
rake
|
|
49
|
-
forwardable (1.4.0)
|
|
50
|
-
h2c (0.2.1)
|
|
51
|
-
ecdsa (~> 1.2.0)
|
|
52
19
|
hashdiff (1.2.1)
|
|
53
|
-
http-2 (1.1.3)
|
|
54
|
-
httpx (1.7.6)
|
|
55
|
-
http-2 (>= 1.1.3)
|
|
56
20
|
json (2.19.5)
|
|
57
21
|
keccak (1.3.3)
|
|
58
|
-
konstructor (1.0.2)
|
|
59
22
|
language_server-protocol (3.17.0.5)
|
|
60
23
|
lint_roller (1.1.0)
|
|
61
|
-
mini_portile2 (2.8.9)
|
|
62
|
-
openssl (3.3.2)
|
|
63
24
|
parallel (2.1.0)
|
|
64
25
|
parser (3.3.11.1)
|
|
65
26
|
ast (~> 2.4.1)
|
|
66
27
|
racc
|
|
67
|
-
pkg-config (1.6.5)
|
|
68
28
|
prism (1.9.0)
|
|
69
29
|
public_suffix (7.0.5)
|
|
70
30
|
racc (1.8.1)
|
|
71
31
|
rainbow (3.1.1)
|
|
72
32
|
rake (13.4.2)
|
|
73
|
-
rbsecp256k1 (6.0.0)
|
|
74
|
-
mini_portile2 (~> 2.8)
|
|
75
|
-
pkg-config (~> 1.5)
|
|
76
|
-
rubyzip (~> 2.3)
|
|
77
33
|
regexp_parser (2.12.0)
|
|
78
34
|
rexml (3.4.4)
|
|
79
35
|
rspec (3.13.2)
|
|
@@ -104,10 +60,6 @@ GEM
|
|
|
104
60
|
parser (>= 3.3.7.2)
|
|
105
61
|
prism (~> 1.7)
|
|
106
62
|
ruby-progressbar (1.13.0)
|
|
107
|
-
rubyzip (2.4.1)
|
|
108
|
-
scrypt (3.1.0)
|
|
109
|
-
ffi-compiler (>= 1.0, < 2.0)
|
|
110
|
-
rake (~> 13)
|
|
111
63
|
simplecov (0.22.0)
|
|
112
64
|
docile (~> 1.1)
|
|
113
65
|
simplecov-html (~> 0.11)
|
|
@@ -146,47 +98,22 @@ DEPENDENCIES
|
|
|
146
98
|
CHECKSUMS
|
|
147
99
|
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
|
|
148
100
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
149
|
-
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
150
101
|
bigdecimal (3.3.1) sha256=eaa01e228be54c4f9f53bf3cc34fe3d5e845c31963e7fcc5bedb05a4e7d52218
|
|
151
|
-
bls12-381 (0.3.1) sha256=d1525cd319c53f14178d54f355dfe1b4c572fbf5625965c1afccdc425b9896dc
|
|
152
102
|
crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
|
|
153
103
|
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
154
104
|
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
|
155
|
-
ecdsa (1.2.0) sha256=b8f7c9541b6c587cbe37e705a1b76be6dc1e85336aa23ac3cf2f07b038bcec55
|
|
156
|
-
eth (0.5.17) sha256=5be9caa85b0643613010ae33c92e746a02ce376a35245e6eb5f491e80531f137
|
|
157
|
-
ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
|
|
158
|
-
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
|
|
159
|
-
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
|
|
160
|
-
ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
|
|
161
|
-
ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95
|
|
162
|
-
ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b
|
|
163
|
-
ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf
|
|
164
|
-
ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809
|
|
165
|
-
ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
|
|
166
|
-
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
|
|
167
|
-
ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
|
|
168
|
-
ffi-compiler (1.4.2) sha256=a9d69d5d9ced6f64776ddafa535867ad90e5740ad37bed7afb4de6e450da126e
|
|
169
|
-
forwardable (1.4.0) sha256=f1cd40cc9812937980e1c76f1aa053660990a7c9b6a98fc37d945468afcce838
|
|
170
|
-
h2c (0.2.1) sha256=a53b7dfeb95660c97b615235ea1b29ec8e1856edc571845685682d94b6e8e227
|
|
171
105
|
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
|
|
172
|
-
http-2 (1.1.3) sha256=1b2f379d35a11dbae94f8a1a52c053d8c161eb4a0c98b5d1605ff1b2bf171c9c
|
|
173
|
-
httpx (1.7.6) sha256=82d825abc9876a132adc3492c56a0c528478ac238dd6f74d3422ab0036c6b5c8
|
|
174
106
|
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
|
|
175
107
|
keccak (1.3.3) sha256=970dcb1e78b8c3129ba2baff8a5baa5cebf391136db1f4018e8ccc9130794557
|
|
176
|
-
konstructor (1.0.2) sha256=fd6ac9eb1dadc1e520e06042aa2ef0122d6a070e06cde86db5a54c0c7bfe2e31
|
|
177
108
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
178
109
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
179
|
-
mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
|
|
180
|
-
openssl (3.3.2) sha256=7f4e01215dc9c4be1fca71d692406be3e6340b39c1f71a47fea9c497decd0f6c
|
|
181
110
|
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
|
|
182
111
|
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
|
|
183
|
-
pkg-config (1.6.5) sha256=33f9f81c5322983d22b439b8b672f27777b406fea23bfec74ff14bbeb42ec733
|
|
184
112
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
185
113
|
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
|
|
186
114
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
187
115
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
188
116
|
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
189
|
-
rbsecp256k1 (6.0.0) sha256=d38f563bfc6bcf3ce20c336a4798cc6990427f291e74c6817360363a72f11669
|
|
190
117
|
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
|
191
118
|
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
|
192
119
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
@@ -197,12 +124,10 @@ CHECKSUMS
|
|
|
197
124
|
rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
|
|
198
125
|
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
|
|
199
126
|
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
|
200
|
-
rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615
|
|
201
|
-
scrypt (3.1.0) sha256=67fde35bc7e3b7fe906c5be9c242acf95fe4a886c89572f405f6b11df30aa6af
|
|
202
127
|
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
|
203
128
|
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
|
204
129
|
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
|
205
|
-
siwe-rb (0.
|
|
130
|
+
siwe-rb (0.3.0)
|
|
206
131
|
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
|
|
207
132
|
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
|
|
208
133
|
webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# siwe-rb
|
|
2
2
|
|
|
3
|
-
Sign
|
|
3
|
+
Sign in with Ethereum (EIP-4361) for Ruby — message construction, parsing, and signature verification, with built-in support for ERC-1271 and EIP-6492 smart contract wallets.
|
|
4
4
|
|
|
5
5
|
`siwe-rb` is the Ruby companion to the [TypeScript](https://github.com/signinwithethereum/siwe), [Python](https://github.com/signinwithethereum/siwe-py), and [Rust](https://github.com/signinwithethereum/siwe-rs) implementations under the [@signinwithethereum](https://github.com/signinwithethereum) organisation. It runs against the same shared test-vector suite.
|
|
6
6
|
|
|
@@ -8,19 +8,27 @@ Sign-In with Ethereum (EIP-4361) for Ruby — message construction, parsing, and
|
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
10
|
# Gemfile
|
|
11
|
-
gem "siwe-rb", "~> 0.
|
|
11
|
+
gem "siwe-rb", "~> 0.3.0"
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
```ruby
|
|
15
15
|
require "siwe"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Requires Ruby ≥ 3.3.
|
|
18
|
+
Requires Ruby ≥ 3.3 with OpenSSL secp256k1 support. The only external runtime
|
|
19
|
+
dependency is `keccak`; verification does not require `eth` or `rbsecp256k1`.
|
|
20
|
+
|
|
21
|
+
For hosts that install gems with `--ignore-dependencies` (including Discourse
|
|
22
|
+
plugins), install and activate `keccak` before `siwe-rb`.
|
|
19
23
|
|
|
20
24
|
## Usage
|
|
21
25
|
|
|
22
26
|
### Construct and sign a message
|
|
23
27
|
|
|
28
|
+
Signing is usually performed by the user’s wallet. This Ruby signing example
|
|
29
|
+
uses the optional `eth` gem, which you must add to your own Gemfile to run it.
|
|
30
|
+
`siwe-rb` itself only verifies signatures.
|
|
31
|
+
|
|
24
32
|
```ruby
|
|
25
33
|
require "siwe"
|
|
26
34
|
require "eth"
|
|
@@ -91,6 +99,16 @@ config = Siwe::Config.new(rpc: custom_rpc)
|
|
|
91
99
|
message.verify!(signature: sig, domain: domain, nonce: nonce, config: config)
|
|
92
100
|
```
|
|
93
101
|
|
|
102
|
+
### Crypto adapters
|
|
103
|
+
|
|
104
|
+
The default adapter is `Siwe::Adapter::OpenSSL`. Custom adapters still implement
|
|
105
|
+
`verify_message(message, signature)` and return the recovered EIP-55 address.
|
|
106
|
+
Raise `Siwe::Crypto::SignatureError` for a bad signature so verification can try
|
|
107
|
+
the smart-wallet fallback; unexpected exceptions propagate to the caller.
|
|
108
|
+
|
|
109
|
+
Version 0.3 removes `Siwe::Adapter::EthGem`. Applications that sign with `eth`
|
|
110
|
+
must declare it themselves; ordinary verification callers need no API changes.
|
|
111
|
+
|
|
94
112
|
### Error handling
|
|
95
113
|
|
|
96
114
|
All failures raise (or, for `verify`, surface as `response.error`) a single `Siwe::Error` carrying a `type` symbol from `Siwe::ErrorType`:
|
data/lib/siwe/adapter.rb
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require_relative "error"
|
|
6
|
-
require_relative "error_type"
|
|
3
|
+
require_relative "crypto"
|
|
7
4
|
|
|
8
5
|
module Siwe
|
|
9
6
|
module Adapter
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
class
|
|
7
|
+
# Recover the signer using Ruby's OpenSSL bindings and Keccak.
|
|
8
|
+
# A custom adapter implements #verify_message(message, signature), returns
|
|
9
|
+
# an EIP-55 address, and raises Crypto::SignatureError for invalid signatures.
|
|
10
|
+
class OpenSSL
|
|
14
11
|
def verify_message(message, signature)
|
|
15
|
-
|
|
16
|
-
Eth::Util.public_key_to_address(public_key).to_s
|
|
12
|
+
Crypto.recover_address(message, signature)
|
|
17
13
|
end
|
|
18
14
|
end
|
|
19
15
|
|
|
20
|
-
DEFAULT =
|
|
16
|
+
DEFAULT = OpenSSL.new
|
|
21
17
|
end
|
|
22
18
|
end
|
data/lib/siwe/crypto.rb
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/keccak"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module Siwe
|
|
7
|
+
# Ethereum cryptographic and encoding helpers. Signature recovery handles
|
|
8
|
+
# only public inputs and uses Ruby's OpenSSL bindings for secp256k1 operations.
|
|
9
|
+
module Crypto
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
class SignatureError < StandardError; end
|
|
13
|
+
|
|
14
|
+
HEX_ADDRESS = /\A0x[0-9a-fA-F]{40}\z/
|
|
15
|
+
HEX_SIGNATURE = /\A(?:0[xX])?([0-9a-fA-F]{64})([0-9a-fA-F]{64})([0-9a-fA-F]{2})\z/
|
|
16
|
+
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
17
|
+
|
|
18
|
+
def keccak256(data)
|
|
19
|
+
Digest::Keccak.new(256).digest(data)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def eip191_hash(message)
|
|
23
|
+
keccak256("\x19Ethereum Signed Message:\n#{message.bytesize}".b + message.b)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def recover_address(message, signature)
|
|
27
|
+
public_key = recover_public_key(eip191_hash(message), signature)
|
|
28
|
+
checksum_address("0x#{bin_to_hex(keccak256(public_key.byteslice(1, 64)).byteslice(-20, 20))}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def recover_public_key(digest, signature)
|
|
32
|
+
raise SignatureError, "digest must be exactly 32 bytes" unless digest&.bytesize == 32
|
|
33
|
+
|
|
34
|
+
r, s, parity = parse_signature(signature)
|
|
35
|
+
group = OpenSSL::PKey::EC::Group.new("secp256k1")
|
|
36
|
+
recovery_point = recovery_point(group, r, parity)
|
|
37
|
+
order = group.order
|
|
38
|
+
# ECDSA recovery: Q = r^-1 * (sR - zG).
|
|
39
|
+
digest_integer = digest.unpack1("H*").to_i(16)
|
|
40
|
+
signed_recovery_point = recovery_point.mul(OpenSSL::BN.new(s.to_s))
|
|
41
|
+
inverse_message_point = group.generator.mul(OpenSSL::BN.new((-digest_integer % SECP256K1_ORDER).to_s))
|
|
42
|
+
inverse_r = OpenSSL::BN.new(r.to_s).mod_inverse(order)
|
|
43
|
+
public_key = signed_recovery_point.add(inverse_message_point).mul(inverse_r)
|
|
44
|
+
raise SignatureError, "could not recover a valid secp256k1 public key" if public_key.infinity?
|
|
45
|
+
|
|
46
|
+
public_key.to_octet_string(:uncompressed)
|
|
47
|
+
rescue OpenSSL::PKey::EC::Point::Error => e
|
|
48
|
+
raise SignatureError, e.message
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_signature(signature)
|
|
52
|
+
# eth historically accepted raw 65-byte signatures as well as hex.
|
|
53
|
+
signature = bin_to_hex(signature) if signature.is_a?(String) && signature.bytesize == 65
|
|
54
|
+
match = HEX_SIGNATURE.match(signature.to_s.b)
|
|
55
|
+
raise SignatureError, "signature must be exactly 65 bytes" unless match
|
|
56
|
+
|
|
57
|
+
r, s, recovery = match.captures.map { |hex| hex.to_i(16) }
|
|
58
|
+
unless r.positive? && r < SECP256K1_ORDER && s.positive? && s < SECP256K1_ORDER
|
|
59
|
+
raise SignatureError, "signature values are outside the secp256k1 scalar field"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
[r, s, recovery_id(recovery)]
|
|
63
|
+
end
|
|
64
|
+
private_class_method :parse_signature
|
|
65
|
+
|
|
66
|
+
def recovery_point(group, scalar, recovery_id)
|
|
67
|
+
# personal_sign has one parity bit; its x coordinate is r. OpenSSL
|
|
68
|
+
# decompresses and validates the point (secp256k1 has cofactor 1).
|
|
69
|
+
encoded = [2 + recovery_id].pack("C") + [scalar.to_s(16).rjust(64, "0")].pack("H*")
|
|
70
|
+
point = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(encoded, 2))
|
|
71
|
+
raise SignatureError, "signature recovery point is not on secp256k1" unless point.on_curve?
|
|
72
|
+
|
|
73
|
+
point
|
|
74
|
+
end
|
|
75
|
+
private_class_method :recovery_point
|
|
76
|
+
|
|
77
|
+
def checksum_address(address)
|
|
78
|
+
return nil unless HEX_ADDRESS.match?(address.to_s)
|
|
79
|
+
|
|
80
|
+
hex = remove_hex_prefix(address).downcase
|
|
81
|
+
hash = bin_to_hex(keccak256(hex))
|
|
82
|
+
checksummed = hex.chars.each_with_index.map do |character, index|
|
|
83
|
+
character.match?(/[a-f]/) && hash[index].to_i(16) >= 8 ? character.upcase : character
|
|
84
|
+
end.join
|
|
85
|
+
"0x#{checksummed}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def abi_encode_validator_args(address, digest, signature)
|
|
89
|
+
raise ArgumentError, "invalid Ethereum address" unless HEX_ADDRESS.match?(address.to_s)
|
|
90
|
+
raise ArgumentError, "digest must be 32 bytes" unless digest&.bytesize == 32
|
|
91
|
+
|
|
92
|
+
signature_bytes = hex_to_bin(signature)
|
|
93
|
+
address_word = remove_hex_prefix(address).downcase.rjust(64, "0")
|
|
94
|
+
digest_word = bin_to_hex(digest)
|
|
95
|
+
offset_word = 96.to_s(16).rjust(64, "0")
|
|
96
|
+
length_word = signature_bytes.bytesize.to_s(16).rjust(64, "0")
|
|
97
|
+
padded_signature = bin_to_hex(signature_bytes).ljust(((signature_bytes.bytesize + 31) / 32) * 64, "0")
|
|
98
|
+
|
|
99
|
+
"#{address_word}#{digest_word}#{offset_word}#{length_word}#{padded_signature}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def hex_to_bin(hex)
|
|
103
|
+
normalized = remove_hex_prefix(hex.to_s)
|
|
104
|
+
unless normalized.length.even? && normalized.match?(/\A[0-9a-fA-F]*\z/)
|
|
105
|
+
raise ArgumentError, "invalid hexadecimal value"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
[normalized].pack("H*")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def bin_to_hex(binary)
|
|
112
|
+
binary.unpack1("H*")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def remove_hex_prefix(value)
|
|
116
|
+
value.start_with?("0x", "0X") ? value[2..] : value
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def recovery_id(value)
|
|
120
|
+
case value
|
|
121
|
+
when 0, 1 then value
|
|
122
|
+
when 27, 28 then value - 27
|
|
123
|
+
# Preserve eth.rb's historical default-chain behavior for EIP-155-style
|
|
124
|
+
# v values, even though personal_sign normally emits 0/1 or 27/28.
|
|
125
|
+
when 37, 38 then value - 37
|
|
126
|
+
else
|
|
127
|
+
raise SignatureError, "invalid signature recovery byte #{value}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
private_class_method :recovery_id
|
|
131
|
+
end
|
|
132
|
+
end
|
data/lib/siwe/message.rb
CHANGED
|
@@ -10,7 +10,7 @@ require_relative "response"
|
|
|
10
10
|
require_relative "util"
|
|
11
11
|
|
|
12
12
|
module Siwe
|
|
13
|
-
# An EIP-4361 Sign
|
|
13
|
+
# An EIP-4361 Sign in with Ethereum message.
|
|
14
14
|
# Construct via `Siwe::Message.new(**fields)` or parse via `Siwe::Message.parse(string)`.
|
|
15
15
|
# Render via `#prepare_message` (alias `#to_eip4361`, `#to_s`).
|
|
16
16
|
class Message
|
|
@@ -176,19 +176,25 @@ module Siwe
|
|
|
176
176
|
recovered = recover_eoa(signature, cfg)
|
|
177
177
|
return if recovered && recovered.downcase == @address.downcase
|
|
178
178
|
|
|
179
|
+
# EOA recovery also accepts binary signatures. Contract signatures use
|
|
180
|
+
# hex of arbitrary byte length, including zero. Reject malformed hex
|
|
181
|
+
# here so the ABI encoder cannot leak an ArgumentError from #verify.
|
|
182
|
+
unless signature.is_a?(String) && signature.b.match?(/\A(?:0[xX])?(?:[0-9a-fA-F]{2})*\z/)
|
|
183
|
+
raise Error.new(ErrorType::INVALID_SIGNATURE, expected: "hex-encoded signature", received: signature.inspect)
|
|
184
|
+
end
|
|
185
|
+
|
|
179
186
|
# EOA recovery failed or address mismatch — try smart-wallet path if configured.
|
|
180
187
|
return if smart_wallet_valid?(signature, cfg)
|
|
181
188
|
|
|
182
189
|
raise Error.new(ErrorType::INVALID_SIGNATURE, expected: @address, received: recovered.to_s)
|
|
183
190
|
end
|
|
184
191
|
|
|
185
|
-
# Errors specific to signature recovery failures
|
|
192
|
+
# Errors specific to signature recovery failures. Other
|
|
186
193
|
# exceptions (e.g. an adapter raising on misconfiguration, network errors)
|
|
187
194
|
# are intentionally not rescued — they signal programmer error or transport
|
|
188
195
|
# failure, not "this signature is bad".
|
|
189
196
|
EOA_RECOVERY_ERRORS = [
|
|
190
|
-
|
|
191
|
-
Eth::Chain::ReplayProtectionError,
|
|
197
|
+
Crypto::SignatureError,
|
|
192
198
|
ArgumentError,
|
|
193
199
|
TypeError
|
|
194
200
|
].freeze
|
data/lib/siwe/smart_wallet.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "crypto"
|
|
4
4
|
|
|
5
5
|
require_relative "eip6492"
|
|
6
6
|
require_relative "error"
|
|
@@ -23,14 +23,11 @@ module Siwe
|
|
|
23
23
|
# Returns true (valid) or false (validator returned non-1).
|
|
24
24
|
# Re-raises Siwe::Error from the rpc client on transport errors.
|
|
25
25
|
def verify(rpc:, address:, message:, signature:) # rubocop:disable Naming/PredicateMethod
|
|
26
|
-
|
|
26
|
+
digest = eip191_hash(message)
|
|
27
27
|
sig_hex = strip_hex_prefix(signature)
|
|
28
28
|
|
|
29
|
-
args =
|
|
30
|
-
|
|
31
|
-
[address, Eth::Util.hex_to_bin(hash_hex), Eth::Util.hex_to_bin(sig_hex)]
|
|
32
|
-
)
|
|
33
|
-
data = "0x#{Eip6492::VALIDATOR_BYTECODE}#{Eth::Util.bin_to_hex(args)}"
|
|
29
|
+
args = Crypto.abi_encode_validator_args(address, digest, sig_hex)
|
|
30
|
+
data = "0x#{Eip6492::VALIDATOR_BYTECODE}#{args}"
|
|
34
31
|
|
|
35
32
|
result = rpc.eth_call(to: nil, data: data)
|
|
36
33
|
result_indicates_valid?(result)
|
|
@@ -38,8 +35,7 @@ module Siwe
|
|
|
38
35
|
|
|
39
36
|
# Hash a message per EIP-191 (`personal_sign`). Returns binary keccak256.
|
|
40
37
|
def eip191_hash(message)
|
|
41
|
-
|
|
42
|
-
Eth::Util.keccak256(prefixed)
|
|
38
|
+
Crypto.eip191_hash(message)
|
|
43
39
|
end
|
|
44
40
|
|
|
45
41
|
# Validator returns 0x01 (possibly zero-padded to 32 bytes) for valid,
|
data/lib/siwe/util.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "ipaddr"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require "time"
|
|
6
6
|
require "date"
|
|
7
|
-
|
|
7
|
+
require_relative "crypto"
|
|
8
8
|
|
|
9
9
|
module Siwe
|
|
10
10
|
module Util
|
|
@@ -58,12 +58,10 @@ module Siwe
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def valid_address?(addr)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
rescue StandardError
|
|
66
|
-
false
|
|
61
|
+
case address_case(addr)
|
|
62
|
+
when :lower, :upper, :checksum then true
|
|
63
|
+
else false
|
|
64
|
+
end
|
|
67
65
|
end
|
|
68
66
|
|
|
69
67
|
def address_case(addr)
|
|
@@ -78,9 +76,7 @@ module Siwe
|
|
|
78
76
|
end
|
|
79
77
|
|
|
80
78
|
def checksum_address(addr)
|
|
81
|
-
|
|
82
|
-
rescue StandardError
|
|
83
|
-
nil
|
|
79
|
+
Crypto.checksum_address(addr)
|
|
84
80
|
end
|
|
85
81
|
|
|
86
82
|
# Validate an RFC 3986 authority: [userinfo "@"] host [":" port].
|
data/lib/siwe/version.rb
CHANGED
data/lib/siwe.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Siwe
|
|
|
10
10
|
autoload :Parser, "siwe/parser"
|
|
11
11
|
autoload :Message, "siwe/message"
|
|
12
12
|
autoload :Config, "siwe/config"
|
|
13
|
+
autoload :Crypto, "siwe/crypto"
|
|
13
14
|
autoload :Adapter, "siwe/adapter"
|
|
14
15
|
autoload :Rpc, "siwe/rpc"
|
|
15
16
|
autoload :SmartWallet, "siwe/smart_wallet"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: siwe-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- EthID.org
|
|
@@ -10,25 +10,19 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: keccak
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
19
|
-
- - "<"
|
|
20
|
-
- !ruby/object:Gem::Version
|
|
21
|
-
version: '1.0'
|
|
18
|
+
version: '1.3'
|
|
22
19
|
type: :runtime
|
|
23
20
|
prerelease: false
|
|
24
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
22
|
requirements:
|
|
26
|
-
- - "
|
|
27
|
-
- !ruby/object:Gem::Version
|
|
28
|
-
version: 0.5.11
|
|
29
|
-
- - "<"
|
|
23
|
+
- - "~>"
|
|
30
24
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: '1.
|
|
25
|
+
version: '1.3'
|
|
32
26
|
description: EIP-4361 message construction, parsing, and signature verification, with
|
|
33
27
|
built-in support for ERC-1271 and EIP-6492 smart contract wallets.
|
|
34
28
|
email:
|
|
@@ -46,6 +40,7 @@ files:
|
|
|
46
40
|
- lib/siwe.rb
|
|
47
41
|
- lib/siwe/adapter.rb
|
|
48
42
|
- lib/siwe/config.rb
|
|
43
|
+
- lib/siwe/crypto.rb
|
|
49
44
|
- lib/siwe/eip6492.rb
|
|
50
45
|
- lib/siwe/error.rb
|
|
51
46
|
- lib/siwe/error_type.rb
|
|
@@ -78,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
78
73
|
- !ruby/object:Gem::Version
|
|
79
74
|
version: '0'
|
|
80
75
|
requirements: []
|
|
81
|
-
rubygems_version: 4.0.
|
|
76
|
+
rubygems_version: 4.0.16
|
|
82
77
|
specification_version: 4
|
|
83
|
-
summary: Sign
|
|
78
|
+
summary: Sign in with Ethereum (EIP-4361) for Ruby
|
|
84
79
|
test_files: []
|