使用go-ethereum验证数字签名失败,求技术支持
签名验证失败问题排查与解决
问题背景
正在从零构建带钱包认证的DApp,流程为客户端用私钥对服务器发送的消息生成数字签名,服务器通过消息解码签名提取公钥完成验证。客户端签名生成正常,但服务器验证代码运行失败。
客户端签名生成代码
package main import ( "fmt" "log" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" ) func main() { privateKey, err := crypto.HexToECDSA("ENV_PRIVATE_KEY") if err != nil { log.Fatal(err) } message := "Please sign this message for address 0xc147d85E9E038178d2F789Ae1A10635A8984D324:\n1718665517938" data := []byte(message) hash := crypto.Keccak256Hash(data) fmt.Println(hash.Hex()) // eg: 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8 signature, err := crypto.Sign(hash.Bytes(), privateKey) if err != nil { log.Fatal(err) } fmt.Println(hexutil.Encode(signature)) // eg: 0x789a80053e4927d0a898db8e065e948f5cf086e32f9ccaa54c1908e22ac430c62621578113ddbb62d509bf6049b8fb544ab06d36f916685a2eb8e57ffadde02301 }
服务器端验证代码(验证失败)
package handler import ( "bytes" "encoding/hex" "errors" "fmt" "strings" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) var ErrDecodingPubKey = errors.New("error decoding public key string") // function to verify digital signature using a unique message // uses secp256k1 with ECDSA. func verifySignature(message string, signature string, publicKeyStr string) (bool, error) { // Compute the Keccak256 hash of the message data := []byte(message) fmt.Println("Message:", data) hash := crypto.Keccak256Hash(data) fmt.Println("Hash:", hash.Hex()) // Convert the signature from its hexadecimal representation (including the 0x prefix) sigBytes := common.FromHex(signature) fmt.Println("Signature Bytes:", sigBytes) // Recover the public key from the signature (handles the 0x prefix) sigPublicKey, err := crypto.Ecrecover(hash.Bytes(), sigBytes) if err != nil { fmt.Println("Failed to recover public key:", err) return false, err } // Parse the provided public key from its hexadecimal representation to bytes publicKeyBytes, err := hex.DecodeString(strings.TrimPrefix(publicKeyStr, "0x")) if err != nil { fmt.Println("Failed to decode public key:", err) return false, ErrDecodingPubKey } fmt.Println("Public Key Bytes:", publicKeyBytes) // Check if the recovered public key matches the provided public key matches := bytes.Equal(sigPublicKey, publicKeyBytes) fmt.Println("Public Key Match:", matches) return matches, nil }
运行日志
logs - Message: [80 108 101 97 115 101 32 115 105 103 110 32 116 104 105 115 32 109 101 115 115 97 103 101 32 102 111 114 32 97 100 100 114 101 115 115 32 48 120 99 49 52 55 100 56 53 69 57 69 48 51 56 49 55 56 100 50 70 55 56 57 65 101 49 65 49 48 54 51 53 65 56 57 56 52 68 51 50 52 58 10 49 55 49 56 54 54 53 53 49 55 57 51 56] Hash: 0x257c52f4f04633e244a35543fc448b7bd8e000337c3f8feb7b6c4c3ab6041c00 Signature Bytes: [215 173 241 43 35 253 150 69 241 64 208 201 144 241 82 206 159 138 70 2 144 194 89 247 153 52 73 16 29 125 186 1 126 122 21 85 114 1 197 221 233 24 44 70 9 102 150 26 222 254 140 203 159 160 170 5 132 22 58 208 122 238 44 135 0] Failed to recover public key:
问题原因与修复方案
核心问题
go-ethereum的crypto.Sign生成的签名中,最后一个字节(v值)为0或1,但crypto.Ecrecover要求v值为27或28,这是导致公钥恢复失败的直接原因。
修复步骤
推荐使用crypto.SigToPub替代Ecrecover,该函数会自动处理v值的格式转换,无需手动调整:
修复后的完整验证代码
package handler import ( "bytes" "encoding/hex" "errors" "fmt" "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) var ErrDecodingPubKey = errors.New("error decoding public key string") func verifySignature(message string, signature string, publicKeyStr string) (bool, error) { data := []byte(message) hash := crypto.Keccak256Hash(data) sigBytes := common.FromHex(signature) if len(sigBytes) != 65 { return false, errors.New("invalid signature length") } // 使用SigToPub自动处理v值格式,直接恢复公钥对象 sigPubKey, err := crypto.SigToPub(hash.Bytes(), sigBytes) if err != nil { fmt.Println("Failed to recover public key:", err) return false, err } // 将公钥对象转换为字节数组 sigPublicKeyBytes := crypto.FromECDSAPub(sigPubKey) // 解析传入的公钥字符串 publicKeyBytes, err := hex.DecodeString(strings.TrimPrefix(publicKeyStr, "0x")) if err != nil { fmt.Println("Failed to decode public key:", err) return false, ErrDecodingPubKey } // 对比恢复的公钥与传入的公钥 matches := bytes.Equal(sigPublicKeyBytes, publicKeyBytes) return matches, nil }
额外验证要点
- 客户端与服务器必须使用完全一致的消息计算哈希,包括换行符、空格等细节,确保日志中的消息字节数组与客户端生成的完全匹配。
- 签名在传输过程中不能被截断或修改,长度必须为65字节。
- 传入的公钥必须是未压缩格式(以0x04开头,共65字节),如果是从地址反向推导,需注意地址是公钥哈希后的结果,无法直接还原公钥,必须由客户端提供公钥或通过签名恢复。
内容的提问来源于stack exchange,提问作者Atharva Jadhav
相关产品推荐
相关产品推荐

