From 6a90fcebe04329c564eb38bfd44e7d6c43a19bbe Mon Sep 17 00:00:00 2001 From: Jacob Shufro Date: Sun, 20 Jul 2025 20:50:45 -0400 Subject: [PATCH] Looser signature validation with V byte --- util/ethereum.go | 24 ++++++++++++++++++++---- util/ethereum_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/util/ethereum.go b/util/ethereum.go index 5421bae..ac9c26e 100644 --- a/util/ethereum.go +++ b/util/ethereum.go @@ -2,6 +2,7 @@ package util import ( "crypto/ecdsa" + "fmt" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" @@ -23,6 +24,9 @@ func RecoverAddressFromSignature(msg []byte, sig []byte) (*common.Address, error if len(sig) != crypto.SignatureLength { return nil, secp256k1.ErrInvalidSignatureLen } + + v := &sig[crypto.RecoveryIDOffset] + // According to the Ethereum Yellow Paper, the signature format must be // [R || S || V], and V (the Recovery ID) must be 27 or 28. This was apparently // inherited from Bitcoin. @@ -30,15 +34,27 @@ func RecoverAddressFromSignature(msg []byte, sig []byte) (*common.Address, error // References: // - https://github.com/ethereum/go-ethereum/issues/19751#issuecomment-504900739 // - https://ethereum.github.io/yellowpaper/paper.pdf, page 22, Appendix E., (213) - sig[crypto.RecoveryIDOffset] -= 27 + // + // Additionally, we've seen some wallets produce signatures with V=0 or 1. + // In those cases, we don't need to tweak the recovery ID before recovering the public key. + switch *v { + case 27, 28: + // Subtract 27 to get the actual recovery ID. + *v -= 27 + // Restore V to its original value at the end of the function. + defer func() { + *v += 27 + }() + case 0, 1: + // Do nothing. + default: + return nil, fmt.Errorf("invalid recovery ID: %d", *v) + } // Recover the public key from the signature. hash := accounts.TextHash(msg) pubKey, err := crypto.SigToPub(hash, sig) - // Restore V to its original value. - sig[crypto.RecoveryIDOffset] += 27 - if err != nil { return nil, err } diff --git a/util/ethereum_test.go b/util/ethereum_test.go index 7259d08..018cea2 100644 --- a/util/ethereum_test.go +++ b/util/ethereum_test.go @@ -38,3 +38,35 @@ func TestAccountsTextHash(t *testing.T) { }) } } + +func TestRecoverAddressFromSignature(t *testing.T) { + tests := []struct { + name string + node_id string + msg string + signature string + }{ + { + name: "Example signature", + node_id: "0xEA28d002042fd9898D0Db016be9758eeAFE35C1E", + msg: "Rescue Node 1753055877", + signature: "3b1ffab4b818e917b81a878265061660c05a5d9eb55bda623b184d1d8f0af9af58c366d487652a7017d994999ad344f72ea85f2c40ff6da7e0810f8a4a6e7b341c", + }, + { + name: "Example signature with low v", + node_id: "0xEA28d002042fd9898D0Db016be9758eeAFE35C1E", + msg: "Rescue Node 1753055877", + signature: "3b1ffab4b818e917b81a878265061660c05a5d9eb55bda623b184d1d8f0af9af58c366d487652a7017d994999ad344f72ea85f2c40ff6da7e0810f8a4a6e7b3401", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + signature, err := hex.DecodeString(tt.signature) + assert.NoError(t, err) + address, err := RecoverAddressFromSignature([]byte(tt.msg), signature) + assert.NoError(t, err) + assert.Equal(t, common.HexToAddress(tt.node_id), *address) + }) + } +}