Skip to content

Commit 954eb64

Browse files
stackdumpclaude
andcommitted
Fix wallet signature verification for real wallets (MetaMask)
The ecRecover function failed with "point not on curve" for some wallet signatures because different wallets encode the recovery id (v) differently: 0/1, 27/28, or EIP-155 chainId*2+35+v. Fix: VerifySignature tries both recovery parities (v=0 and v=1) and accepts whichever recovers to the expected address. This handles all wallet implementations without needing to parse the v encoding. Poll create and close handlers now use VerifySignature instead of RecoverAddress + manual comparison. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dee260f commit 954eb64

2 files changed

Lines changed: 63 additions & 25 deletions

File tree

internal/server/auth.go

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,28 +162,76 @@ func RecoverAddress(message string, signature string) (string, error) {
162162
s := new(big.Int).SetBytes(sig[32:64])
163163
v := sig[64]
164164

165+
// Normalize v: wallets use 0/1 or 27/28 (EIP-155 uses chainId*2+35+v)
165166
if v >= 27 {
166167
v -= 27
167168
}
168169
if v > 1 {
169-
return "", errors.New("invalid signature recovery id")
170+
v = v % 2 // handle EIP-155 recovery ids
170171
}
171172

172-
pubX, pubY, err := ecRecover(hash, r, s, v)
173-
if err != nil {
174-
return "", fmt.Errorf("public key recovery failed: %w", err)
173+
// Try both recovery parities. Wallets encode v differently (0/1, 27/28,
174+
// EIP-155 chainId*2+35+v), so we try both and return all valid candidates.
175+
var addrs []string
176+
for _, tryV := range []byte{0, 1} {
177+
pubX, pubY, err := ecRecover(hash, r, s, tryV)
178+
if err != nil {
179+
continue
180+
}
181+
182+
pubBytes := make([]byte, 65)
183+
pubBytes[0] = 0x04
184+
xBytes := pubX.Bytes()
185+
yBytes := pubY.Bytes()
186+
copy(pubBytes[1+32-len(xBytes):33], xBytes)
187+
copy(pubBytes[33+32-len(yBytes):65], yBytes)
188+
189+
addr := "0x" + hex.EncodeToString(keccak256(pubBytes[1:])[12:])
190+
addrs = append(addrs, addr)
191+
}
192+
193+
if len(addrs) == 0 {
194+
return "", errors.New("public key recovery failed for both parities")
195+
}
196+
// Return the preferred parity's address
197+
if int(v) < len(addrs) {
198+
return addrs[v], nil
199+
}
200+
return addrs[0], nil
201+
}
202+
203+
// VerifySignature checks if a message was signed by the given address.
204+
// Tries both recovery parities to handle different wallet v-encodings.
205+
func VerifySignature(message, signature, expectedAddress string) bool {
206+
sig, err := hex.DecodeString(strings.TrimPrefix(signature, "0x"))
207+
if err != nil || len(sig) != 65 {
208+
return false
175209
}
176210

177-
// Derive Ethereum address: keccak256(uncompressed_pubkey[1:])
178-
pubBytes := make([]byte, 65)
179-
pubBytes[0] = 0x04
180-
xBytes := pubX.Bytes()
181-
yBytes := pubY.Bytes()
182-
copy(pubBytes[1+32-len(xBytes):33], xBytes)
183-
copy(pubBytes[33+32-len(yBytes):65], yBytes)
211+
prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message))
212+
hash := keccak256(append([]byte(prefix), []byte(message)...))
213+
r := new(big.Int).SetBytes(sig[:32])
214+
s := new(big.Int).SetBytes(sig[32:64])
215+
216+
for _, v := range []byte{0, 1} {
217+
pubX, pubY, err := ecRecover(hash, r, s, v)
218+
if err != nil {
219+
continue
220+
}
221+
222+
pubBytes := make([]byte, 65)
223+
pubBytes[0] = 0x04
224+
xBytes := pubX.Bytes()
225+
yBytes := pubY.Bytes()
226+
copy(pubBytes[1+32-len(xBytes):33], xBytes)
227+
copy(pubBytes[33+32-len(yBytes):65], yBytes)
184228

185-
addr := keccak256(pubBytes[1:])
186-
return "0x" + hex.EncodeToString(addr[12:]), nil
229+
addr := "0x" + hex.EncodeToString(keccak256(pubBytes[1:])[12:])
230+
if strings.EqualFold(addr, expectedAddress) {
231+
return true
232+
}
233+
}
234+
return false
187235
}
188236

189237
// ecRecover recovers the public key from an ECDSA signature on secp256k1.

internal/server/polls.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,7 @@ func (s *Server) handleCreatePoll(w http.ResponseWriter, r *http.Request) {
7777
return
7878
}
7979
sigMsg := "bitwrap-create-poll:" + req.Title
80-
recovered, err := RecoverAddress(sigMsg, req.Signature)
81-
if err != nil {
82-
http.Error(w, fmt.Sprintf("signature verification failed: %v", err), http.StatusForbidden)
83-
return
84-
}
85-
if !strings.EqualFold(recovered, req.Creator) {
80+
if !VerifySignature(sigMsg, req.Signature, req.Creator) {
8681
http.Error(w, "signature does not match creator address", http.StatusForbidden)
8782
return
8883
}
@@ -403,12 +398,7 @@ func (s *Server) handleClosePoll(w http.ResponseWriter, r *http.Request) {
403398
return
404399
}
405400
sigMsg := "bitwrap-close-poll:" + pollID
406-
recovered, err := RecoverAddress(sigMsg, req.Signature)
407-
if err != nil {
408-
http.Error(w, fmt.Sprintf("signature verification failed: %v", err), http.StatusForbidden)
409-
return
410-
}
411-
if !strings.EqualFold(recovered, poll.Creator) {
401+
if !VerifySignature(sigMsg, req.Signature, poll.Creator) {
412402
http.Error(w, "only the poll creator can close it", http.StatusForbidden)
413403
return
414404
}

0 commit comments

Comments
 (0)