深入理解 ECDSA、r s v 与 Ethereum Sign Message 实战差异

·

本文不涉及晦涩的数学推导,只从“格式”与“使用场景”两个维度,提炼出开发者最该掌握的 核心知识点,让你在任何语言环境里,都能一眼看出 标准 ECDSAEthereum Sign Message 的差别。

为什么会有两份签章?

在我们产品的实际业务里,经常遇到两大类“签名”场景:

  1. 链上身份认证
    把签完名的消息发进智能合约,再用 ecrecover 恢复原地址,完成“谁签名、谁操作”的校验。
  2. 链下数据确权
    仅对一条 JSON 数据或一段文本做签名,证明“这是我本人签的”,常用于登录、投票、KYC 等流程。

由于我们代码栈横跨 iOS / Android / 后端 C# / Java / Node.js,每个生态的 ECDSA 实现又不尽相同,早期大家还分不清“标准 signature”和“Web3 provider 的 sign message”到底差在哪,踩坑无数。于是,本文应运而生。


core 关键词:ECDSA、r s v、public key recovery、Ethereum 签名、JSON RPC sign

1. r、s、v 三兄弟的故事

不少人在文档里第一次看到 “3045……” 开头的一大串 HEX,总会抓瞎。记住这三步即可:

  1. r、s 各自 32 字节
  2. v 只占 1 字节
  3. 这三个玩意儿拼在一起就是 Ethereum 多数 API 里的 65 字节签名字节串
如果你看到 71、72、73 字节的签名,那是标准 ECDSA 输出的 DER 格式。它把长度标记、标签字节全算了进去,不是 Ethereum 业务想要的数据格式,要先做 ASN.1 → r||s 的转换。

2. Ethereum 为什么要“魔改” v 值?

如果你把 eth_sign 结果打到控制台,往往看到 v 值是 27 / 28,或者 37 / 38——看上去跟 ECDSA 完全不是一回事。实际逻辑是:

一句话总结:v 在标准 ECDSA 里没有,在 Ethereum 里就是 recovery-id + 修正常量


数据格式差异速查

场景是否包含 v起始字节用途
标准 ECDSA (DER)30通用签名,需手动还原公钥
Ethereum Sign1b / 1c直接用 web3.eth.ecrecover
Ledger / Trezor同上硬件钱包返回也是 65 字节串

👉 遇到格式错乱?先确认 DER 与 r||s||v 别搞串,再用这条快速验证工具。


3. 代码层面的坑

3.1 iOS / Android 原生库

iOS 的 SecKeyRawSign 默认输出 71~73 字节 DER,想接 Ethereum 需利用 ASN.1 Parser 手动拆分 r、s,再对接 v 的计算逻辑。
Android 的 Signature 类同理,差别在于 BouncyCastle 有封装好的 ECDSASigner 帮忙把 DER 拆成 r、s。

3.2 后端 C# / Java

👉 调试签名始终 0x 开头零填充?查看这份零填充快速修复清单,5 分钟排查。


案例小课堂

假设我们要在 Hardhat 做单元测试,对一条文本 "login_at_1689913157" 做签名,然后调用合约 ecrecover 验证。

  1. 格式统一
    在 dApp 侧用 eth_sign,拿到 65 字节:

    0x8e7e52…(64 字节 r+s)+ 1b
  2. 链上验证
    solidity 合约:ecrecover(hash, v, r, s),直接得回地址,顺势校验。

FAQ:3 分钟解惑合集

Q1:为什么有了 r、s 还要 v?
A:ECDSA 恢复算法一次给你 1~4 个候选公钥,必须用 v 指定“是其中哪一个”。

Q2:我的 Java 端签名发到合约总是失败?
A:多半忘记 +27/+35。参考 Sign.signPrefixedMessage 源码,只有在 chainId ≠ NONE 时才走 EIP-155。

Q3:DER 格式能否直接在 Ethereum 转 Ethernet?
A:不行。需先拆 ASN.1,拿到 64 字节 r||s,再补 1 字节 v。

Q4:前端钱包返回 personal_sign,跟 eth_sign 有区别吗?
A:前缀字符串不同。personal_sign 会多加 "\x19Ethereum Signed Message:\n32",防止交易重放。后端计算 hash 时要保持完全一致。

Q5:为什么 r、s 有时会溢出 32 bytes?
A:算法里 r、s 定义为 0 < r, s < n。非常罕见接近上限时出现首位为 1(0x80 以上),为防止被解读为负数的补码,编码器会给 r 或 s 前缀 0x00,导致变成 33 bytes。后端要先去掉前导零再恢复。


小结

掌握了这些格式差异,无论 iOS、Android 还是 Node.js,你都能“指哪儿打哪儿”,把一道签名难题拆成三条公式,既安全又优雅。