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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94f4cdbde4fcf4c3f8e2e30c26390a0ed6a56600297f9a843d651e48009a64ea
4
- data.tar.gz: 7b065dbbb37260fae1520c4a8f3d89e9917ebcd8b187e6aa0467104591b9b7cd
3
+ metadata.gz: 4b6bc3df2351b75f3628baf83e1a49b32967538ce242b76407b0d978f7777fa2
4
+ data.tar.gz: ddfd9da52f3d0dd77504c3df22ec547091b7038356623891ac465d008298d68b
5
5
  SHA512:
6
- metadata.gz: 2241b00fc2ab6ccd140d3fab0983b9a112e6812fda40485c75320ce680a62feb461d6d0197cb5166dce43b0f6b8e2223d0b60d605f735e91dcb7e7076d2ec101
7
- data.tar.gz: 68d02a5ba04da51c6c74ff934ccb2e1f7d47ca62a948df4b35dd1b9134075651753345902d5c4b5f47e6a5bcbd5c14dd1005274c981401de2c8fcb6b0164c1b8
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.2.0)
5
- eth (>= 0.5.11, < 1.0)
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.2.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-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.
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.1"
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
- require "eth"
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
- # Default crypto adapter using the eth gem. The verification path only needs
11
- # signer recovery any object implementing `#verify_message(message, signature)`
12
- # returning the recovered EIP-55 address can be plugged in via Config.new(adapter:).
13
- class EthGem
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
- public_key = Eth::Signature.personal_recover(message, signature)
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 = EthGem.new
16
+ DEFAULT = OpenSSL.new
21
17
  end
22
18
  end
@@ -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-In with Ethereum message.
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 from the `eth` gem. Other
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
- Eth::Signature::SignatureError,
191
- Eth::Chain::ReplayProtectionError,
197
+ Crypto::SignatureError,
192
198
  ArgumentError,
193
199
  TypeError
194
200
  ].freeze
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "eth"
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
- hash_hex = bin_to_hex(eip191_hash(message))
26
+ digest = eip191_hash(message)
27
27
  sig_hex = strip_hex_prefix(signature)
28
28
 
29
- args = Eth::Abi.encode(
30
- %w[address bytes32 bytes],
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
- prefixed = Eth::Signature.prefix_message(message)
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
- require "eth"
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
- return false if addr.nil? || addr.empty?
62
-
63
- Eth::Address.new(addr)
64
- true
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
- Eth::Address.new(addr).to_s
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Siwe
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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.2.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: eth
13
+ name: keccak
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ">="
16
+ - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.5.11
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.0'
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.3
76
+ rubygems_version: 4.0.16
82
77
  specification_version: 4
83
- summary: Sign-In with Ethereum (EIP-4361) for Ruby
78
+ summary: Sign in with Ethereum (EIP-4361) for Ruby
84
79
  test_files: []