eth里常用的签名算法

它能够对32byte的内容进行签名,所以要签署消息前,通常要先过一个256bit的hash,对于eth,通常就是keccak256

产生的结果包括三个数 (v, r, s),v与公钥恢复相关,r相当于一个随机数,s是签名结果

通常验签有两种方法:

  1. verify:与一般签名相同,提供msg、r&s、pubkey,就能验证签名正确性
  2. recover:提供msg、r&s&v,可以恢复出pubkey,这是ECDSA的特性

在eth中,通常采用第二种方法,且evm官方提供了precompiled contract ecrecover ,solidity中可直接使用ecrecover函数来调用

go-ethereum/core/vm/contracts.go · ethereum/go-ethereum (github.com)

在这里面,v稍微有些特殊,他是正常ecdsa签出来的签名的 v + 27,27是个来自bitcoin的magic number,而且v一定要是27或28,precompiled合约里有相应的检查

python实现可以参考来自 web3.pyeth_keys.backends.native.NativeECCBackendeth_keys.backends.native.ecdsa

from eth_account.account import Account
import requests
rpc = '<http://127.0.0.1:8545>'
Account.enable_unaudited_hdwallet_features()

mnemonic = 'e'
account = Account.from_mnemonic(mnemonic)
private_key = account.key
print('address: ', account.address)
signed = Account().signHash(b'\\0' * 32, private_key)
print(signed)

同一个消息多个签名

既然签名中的r是一个随机数,那对于同样的消息,我们应该可以计算出多个不同的签名?

但在使用 web3.py 提供的 eth_account.Account.sign_messageeth_account.Account._sign_hash 时,可以发现每次产生的签名的vrs都是相同的,对此,我们可以跟进到 eth_keys.backends.native.ecdsa.ecdsa_raw_sign 函数中,发现用于计算 r 的随机参数 k 是通过 deterministic_generate_k 计算出的一个确定的数(对于相同的msg和privkey),这是 **RFC 6979** 规定的一种ECDSA的确定性签名算法。

那么recover的这种性质依赖 **RFC 6979** 吗?

否,任意签名都有这种性质

所以我们前面问题的答案就应该是:对,我们可以对于同一消息生成多个r&s不同的签名

参考 python-ecdsa/src/ecdsa/ecdsa.py · tlsfuzzer/python-ecdsa (github.com) ,我们可以将k的生成改为随机值,范围在 [1, N) ,得到如下代码,为了使用方便,ecdsa_recover返回的实际上不是公钥,而是签名地址

import hashlib
import hmac
from eth_utils import big_endian_to_int

def deterministic_generate_k(msg_hash: bytes,
                             private_key_bytes: bytes) -> int:
    v_0 = b'\\x01' * 32
    k_0 = b'\\x00' * 32

    k_1 = hmac.new(k_0, v_0 + b'\\x00' + private_key_bytes + msg_hash, hashlib.sha256).digest()
    v_1 = hmac.new(k_1, v_0, hashlib.sha256).digest()
    k_2 = hmac.new(k_1, v_1 + b'\\x01' + private_key_bytes + msg_hash, hashlib.sha256).digest()
    v_2 = hmac.new(k_2, v_1, hashlib.sha256).digest()

    kb = hmac.new(k_2, v_2, hashlib.sha256).digest()
    k = big_endian_to_int(kb)
    return k

from eth_keys.constants import (
    SECPK1_N as N,
    SECPK1_G as G
)

from eth_keys.backends.native.jacobian import (
    inv,
    fast_multiply
)

def ecdsa_sign(msg_hash: bytes,
               private_key: str | bytes,
               deterministic: bool = True) -> tuple[int, int, int]:
    assert len(msg_hash) == 32, "length of msg_hash must == 32"

    if not isinstance(private_key, bytes):
        private_key = HexBytes(private_key)

    z = big_endian_to_int(msg_hash)
    if deterministic:
        k = deterministic_generate_k(msg_hash, private_key)
        # print(f"k: {k}, G: {G}, N: {N}")
    else:
        import secrets
        k = 1 + secrets.randbelow(N-1)

    r, y = fast_multiply(G, k)
    s_raw = inv(k, N) * (z + r * big_endian_to_int(private_key)) % N

    v = 27 + ((y % 2) ^ (0 if s_raw * 2 < N else 1))
    s = s_raw if s_raw * 2 < N else N - s_raw

    return v, r, s

def ecdsa_recover(msg_hash: bytes,
                  vrs: tuple[int, int, int]):
    return Account._recover_hash(msg_hash, vrs = (vrs[0] - 27, vrs[1], vrs[2]))

# (v, r, s) = ecdsa_sign(Web3.keccak(sign_data), private_key, deterministic=False)
# ecdsa_recover(Web3.keccak(b'a'), (v, r, s))