eth里常用的签名算法
它能够对32byte的内容进行签名,所以要签署消息前,通常要先过一个256bit的hash,对于eth,通常就是keccak256
产生的结果包括三个数 (v, r, s),v与公钥恢复相关,r相当于一个随机数,s是签名结果
通常验签有两种方法:
在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.py 的 eth_keys.backends.native.NativeECCBackend
和 eth_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_message
或 eth_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))