Skip to main content

ECDSA

以太坊签名为什么需要加前缀"\x19Ethereum Signed Message:\n32"

原因是为了防止重放攻击。

以太坊中的交易签名采用 ECDSA 算法。该算法生成的签名是可逆的,意味着可以从签名恢复出原始消息。这使得攻击者可以拿到某笔交易的签名,将其应用到其他交易上,从而发起重放攻击。

为了防止这种攻击,在对消息签名之前,需要在消息开头添加固定的前缀 "\x19Ethereum Signed Message:\n32"。这会使原始消息发生变化,因此其产生的签名也会不同。即使攻击者拿到签名,也无法将其应用到未加前缀的原始消息上。

所以以太坊区块链中的交易签名都需要遵循这种标准,在消息签名之前添加特殊前缀,从而防止重放攻击的发生。这个前缀已经成为一个行业标准,被各种以太坊相关的签名实现广泛采用。

以太坊签名不加前缀 "\x19Ethereum Signed Message:\n32" 就直接对消息签名,会导致签名过程中产生的哈希值发生变化,从而在恢复公钥和地址时出现错误,具体原因如下:

  1. 以太坊签名采用 ECDSA 算法,该算法需要先对消息进行哈希运算才能签名。

  2. 普通哈希算法(如 SHA3)对任意长度的数据都可以产生固定长度的哈希值。但直接对消息哈希无法防止重放攻击。

  3. 为防止重放攻击,需要在消息前添加特定前缀,使同一条消息添加前缀前后产生不同的哈希值。

  4. 以太坊采用的前缀是"\x19Ethereum Signed Message:\n32",添加后再做哈希。

  5. 如果不添加这个前缀,直接对消息哈希,得到的哈希值会与标准不一样。

  6. 不同的哈希值导致签名中的签名参数 r、s 不同,从签名恢复公钥时也会不一样。

  7. 公钥不同导致最终 Derived 的地址也会不一样。

  8. 因此恢复出的地址就会错误,不匹配真实地址。

所以在以太坊生态必须添加标准前缀再签名,否则交易会失败,或恢复出错误地址。

ethers 里的provider.signMessagesigner.signMessage都默认添加了前缀

以太坊 ECDSA 恢复签名的基本原理

以太坊ECDSA恢复签名的基本原理是:

1. 对消息进行哈希运算,得到哈希值h

2. 选取一个私钥k,计算签名(r, s)

r = (h + k * G) mod n

s = (k^-1 * (h + r * privKey)) mod n

其中G是生成点,n是曲线阶

3. 恢复公钥:

R = r * G

e = hash(消息)

P = s^-1 * (e * G + R)

其中P就是恢复出的公钥

4. 从公钥P计算出地址:

a. 取公钥P的最后64字节作为字节数组

b. 对其取Keccak-256哈希

c. 取结果的最后20字节就是地址

5. 将原始消息、签名(v, r, s)、恢复出的公钥和地址进行对比,如果一致,则证明签名有效和恢复过程正确。

6. 在实际实现中,需要处理一些细节,比如v的值的处理,签名编码的规范等。

以上就是以太坊ECDSA签名的基本恢复过程,通过签名可以反向求解出地址,验证签名的正确性。

eth_signpersonal_sign的区别

eth_signpersonal_sign 都是用于对以太坊数据进行签名的方法,但有以下几点主要区别:

  1. 签名的数据格式不同
  • eth_sign: 消息的数据格式为\x19Ethereum Signed Message:\n + 消息长度 + 消息。即消息数据前需要添加 Ethereum 特定的前缀。

  • personal_sign: 消息的数据格式为\x19Ethereum Signed Message:\n + 消息。不需要消息长度。

  1. 签名所用的账户不同
  • eth_sign: 使用发送交易的账户签名,通常是解锁状态的账户。

  • personal_sign: 使用指定的任意账户签名,账号可以是锁定状态。

  1. 签名的计算不同
  • eth_sign: 对消息数据的原始字节进行签名。
  • personal_sign: 对消息数据的 UTF-8 编码进行签名。
  1. 使用场景不同
  • eth_sign: 用于对区块链交易进行签名。
  • personal_sign: 用于对数据消息进行签名,如登录签名、非交易类数据签名等。

总体来说,personal_sign 使用更灵活,适用场景更广泛,而eth_sign更专注于交易签名。但都需要遵循特定的数据格式要求。

ethers 分割 signature

// V5版本
const { r, s, v } = ethers.utils.splitSignature(
"0x003ebcbc4da71f53dc34bbbb0cbef8553eb6af4ac8ca4e214fc4d0cc122bed6915d0a7240bafd396abab1dcb315a7bc6df49d5600719800db337cb87567ff8891c"
);

// V6版本
import { splitSignature } from "@ethersproject/bytes";

const { r, s, v } = splitSignature(
"0x003ebcbc4da71f53dc34bbbb0cbef8553eb6af4ac8ca4e214fc4d0cc122bed6915d0a7240bafd396abab1dcb315a7bc6df49d5600719800db337cb87567ff8891c"
);