跳到主要内容
ecdsa.com
Go

go-ethereum crypto

go-ethereum(geth)的 crypto 包:对 32 字节哈希做 secp256k1 签名,不加消息前缀;65 字节签名中 v = 0/1;验证器拒绝 high-S,恢复则接受。
许可证
LGPL-3.0-or-later
检查的版本
1.17.8 ·
源代码
github.com/ethereum/go-ethereum
文档
pkg.go.dev/github.com/ethereum/go-ethereum/crypto

兼容性概览

ECDSA以太坊与比特币

ECDSA 曲线
secp256k1
EdDSA
none
公钥格式
sec1-uncompressedsec1-compressed
私钥格式
raw-scalar

处理以太坊签名的 Go 服务通常会导入 github.com/ethereum/go-ethereum/crypto 和 accounts。启用 cgo 时,这个包使用 go-ethereum 内置的 libsecp256k1 副本;设置 CGO_ENABLED=0 时,则退回到纯 Go 的 decred 实现——本次检查分别运行了两种构建,所有事实的结果完全一致。

crypto.Sign(hash, key) 只接受恰好 32 字节的输入(否则报错 hash is required to be exactly 32 bytes),既不计算哈希,也不添加前缀。登录消息必须先经过 accounts.TextHash,它会添加 EIP-191 前缀并用 keccak-256 计算哈希——本次检查用一个独立的 keccak 实现核对了结果。如果直接对消息的 keccak 哈希签名,得到的签名永远无法与 ethers、viem 和钱包的结果匹配。

签名为 65 字节,恢复 ID 作为 v,取值为 0 或 1,而钱包、ethers、viem 和 web3.py 在这个位置放的是 27 或 28。crypto.Ecrecover 拒绝了 v = 27/28,报错 invalid signature recovery id,因此要像示例那样,输入时减去 27,输出时再加回去。nonce 遵循 RFC 6979——签名与一个独立的参考实现一致——并且 s 始终是 low-S。

两条验证路径对 high-S 的处理不一致。crypto.VerifySignature 接收不含 v 的 64 字节,拒绝了镜像签名 (r, n − s),但 crypto.Ecrecover 和 crypto.SigToPub 能从中恢复出正确的公钥,EVM 的 ecrecover 预编译合约也是如此。因此,恢复地址的合约和服务如果把签名当作唯一标识,就必须自行拒绝 high-S。

公钥以 65 字节的 SEC1 未压缩点或 33 字节的 SEC1 压缩点传递,私钥以 32 字节的标量传递。要查看签名遵循哪种约定,可以把它粘贴到以太坊签名验证工具中;同一地址的多笔交易中重复出现的 nonce,可以在以太坊 nonce 重用检测中发现。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/go-ethereum-crypto/run.sh 于2026年10月20日在容器镜像 golang:1.27.3-trixie 中运行了 go-ethereum crypto 1.17.8;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    rsv-65

    crypto.Sign 返回 65 字节:r、s 和恢复 ID。

    检查记录
    fact ecdsa-signature-encoding = rsv-65
  • 恢复 ID v

    0-1

    最后一个字节为 0 或 1;crypto.Ecrecover 拒绝了 27/28,报错 invalid signature recovery id。

    检查记录
    fact recovery-v = 0-1
    evidence recovery-v: Ecrecover with v = 27/28: invalid signature recovery id
    evidence recovery-v: signer address recovered from the 0/1 signature matches: true
  • 消息前缀

    eip-191

    EIP-191 哈希由 accounts.TextHash 提供,其结果与对加前缀消息独立计算的 keccak-256 一致;crypto.Sign 本身不添加前缀。

    检查记录
    fact message-prefix = eip-191
    evidence message-prefix: accounts.TextHash equals keccak256 of the EIP-191 prefixed message; crypto.Sign itself adds no prefix
  • 签名调用对哪些内容做哈希

    takes-digest

    crypto.Sign 接收 32 字节的哈希,其他输入都会报错 hash is required to be exactly 32 bytes。

    检查记录
    fact ecdsa-prehash = takes-digest
    evidence ecdsa-prehash: crypto.Sign on a 31-byte input: hash is required to be exactly 32 bytes (31)
  • 默认哈希

    none

    crypto.Sign 内部不计算哈希。

    检查记录
    fact ecdsa-default-hash = none
  • ECDSA 的 nonce

    rfc6979

    RFC 6979:签名与一个独立 RFC 6979 实现(已用 RFC 的测试向量核验)在 s 规范化后的签名完全相同。

    检查记录
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: two signatures of the same digest identical: true; equal to the reference RFC 6979 signature with s normalized: true
  • 签名时的 low-S

    low-s

    64 个签名中没有 high-S 值。

    检查记录
    fact ecdsa-low-s-sign = low-s
  • 验证时的 high-S

    rejects-high-s

    对于把 s 替换为 n − s 的签名,crypto.VerifySignature 返回 false。

    检查记录
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: VerifySignature on (r, n - s): false; Ecrecover on (r, n - s, flipped v): recovers the signer true (err <nil>)
  • 从 high-S 签名恢复公钥

    accepted

    对于翻转了 v 的镜像 high-S 签名,crypto.Ecrecover 恢复出了签名者的公钥。

    检查记录
    fact ecrecover-high-s = accepted
  • ECDSA 曲线

    secp256k1

    仅支持 secp256k1。

    检查记录
    fact ecdsa-curves = secp256k1
  • 公钥格式

    sec1-uncompressedsec1-compressed

    crypto.UnmarshalPubkey 读取 65 字节的未压缩点,crypto.DecompressPubkey 读取 33 字节的压缩点。

    检查记录
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: UnmarshalPubkey(65 bytes): <nil>; DecompressPubkey(33 bytes): <nil>
  • 私钥格式

    raw-scalar

    私钥是 32 字节的标量(crypto.ToECDSA、crypto.HexToECDSA)。

    检查记录
    fact private-key-formats = raw-scalar
  • EdDSA

    none

    这个包不支持 EdDSA。

    检查记录
    fact eddsa = none
  • 后量子算法

    none

    不支持后量子算法。

    检查记录
    fact pqc = none

用法

对 EIP-191 消息签名,在 go-ethereum 的 0/1 与传输格式的 27/28 之间转换 v,并恢复出签名者。
package main

import (
	"fmt"

	"github.com/ethereum/go-ethereum/accounts"
	"github.com/ethereum/go-ethereum/crypto"
)

func main() {
	key, _ := crypto.GenerateKey()
	message := []byte("Sign in to example.com\nNonce: 8f2c1a")

	// crypto.Sign takes a 32-byte hash and adds no prefix: hash with TextHash (EIP-191).
	sig, _ := crypto.Sign(accounts.TextHash(message), key)

	// Wallets and ethers/viem send v = 27/28; go-ethereum wants 0/1.
	wire := append([]byte{}, sig...)
	wire[64] += 27

	// Coming back from the wire: undo the offset before recovering.
	in := append([]byte{}, wire...)
	if in[64] >= 27 {
		in[64] -= 27
	}
	pub, err := crypto.SigToPub(accounts.TextHash(message), in)
	fmt.Println("v on the wire:", wire[64], "signer matches:", err == nil && crypto.PubkeyToAddress(*pub) == crypto.PubkeyToAddress(key.PublicKey))
}

代码片段测试工具会在容器镜像 golang:1.27.3-trixie 中原样运行每个示例,只要输出不匹配,运行就会失败。

命令
GOFLAGS=-mod=mod go run .
输出必须匹配(正则表达式)
v on the wire: 2[78] signer matches: true

库