主页 > imtoken钱包最新版本 > 使用比特币私钥签署任何消息

使用比特币私钥签署任何消息

imtoken钱包最新版本 2023-01-17 08:49:07

在双密钥系统中,签名可以用作身份验证和授权的手段。

同样在比特币中,我可以提供以下信息来证明这个地址是属于我的。

物品内容

地址

1AfxgwYJrBgriZDLryfyKuSdBsi59jeBX9

留言

你好世界

签名

H9DnqMSGQmqi0zIWQqVfPXQsq59Qt11F1rUQDxv+4/iUDrSLJ6xHZ7PUSKvVThVWQAy/lLEpE3JeMpOlwUiVGlo=

任何人都可以使用钱包工具或第三方服务进行验证。

可以证明,地址对应的私钥确实在我手里。请注意,要签署的消息可以是您和我事先协商好的任何内容。这里使用“Hello World”只是为了演示。

本文将详细介绍如何使用比特币私钥对任意消息进行签名。

发现“问题”

你没有注意到,文章开头的证明信息只包含地址、消息和签名,没有公钥。

但是,验证操作需要公钥。

可以从地址反转吗?不能。地址是公钥散列的可逆编码,由于散列是单向运算,所以无法从公钥散列中推导出公钥。所以无法从地址计算公钥。

那么问题来了,如何在没有公钥的情况下验证签名呢?只有一种可能,公钥可以从 ECDSA 签名中恢复。

说说ECDSA签名

在介绍 ECDSA 时,我们强调了在签名过程中使用的临时私钥 $k$ 非常重要比特币私钥格式,以确保它是绝对私有的,并且它是足够随机生成的。

签名和消息是公开的,即如果你知道$(r, s)$和$e$,如果你也知道$k$,就可以计算出私钥。

比特币私钥格式

$$

a = r^{-1}(sk - e) \bmod{n}

$$

进一步,如果你知道$K$,你就可以计算出公钥。

$$

A = aG = r^{-1}(sk - e) \cdot G = r^{-1}(s \cdot kG - eG) = r^{-1}(sK - eG)

$$

所以从签名中恢复公钥的关键是确定点$K$$(x_K, y_K)$的坐标。

注意对于Secp256k1的参数$n$和$p$,有$n < p$,$r = x_K \bmod{n}$。

所以从$r$中找到$x_K$的结果不一定是唯一的。当 $r + n < p$ 时,$x_K = r$ 和 $x_K = r + n$ 都是解。

另外,知道$x_K$ 不能唯一确定$y_K$。椭圆曲线上下对称。如果点 $(x, y)$ 在曲线上,那么点 $(x, -y \bmod{p})$ 也在曲线上。之前介绍比特币公钥的时候提到过,对于Secp256k1,至少要知道该点的X坐标和Y坐标的奇偶性,才能唯一确定该点。

让我们总结一下。如果要通过 ECDSA 签名 $(r, s)$ 和消息 $e$ 恢复公钥,则需要知道另外两条信息:

可恢复的 ECDSA 签名

这个“额外”的“信息”只有四种状态,可以用2个二进制位来表示。高位 0 表示 $x_K < n$,低位 0 表示 $y_K$ 的偶数。

二进制位内容十进制值

00

$x_K

01

$x_K < n$, $y_K$ 是奇数

1

10

$x_K > n$ , $y_K$ 是偶数

比特币私钥格式

2

11

$x_K > n$, $y_K$ 是奇数

3

p>

我们将此令牌称为recovery_id或recid,包含此令牌的ECDSA签名$(recid, r, s)$称为“可恢复”ECDSA签名或密集签名(compact signature)。

对应的代码不难实现,只要在计算$(r, s)$时计算recid即可。

def sign_recoverable(private_key: int, message: bytes) -> tuple:
"""Create recoverable ECDSA signature, aka compact signature, (recovery_id, r, s)"""
e = hash_to_int(message)
recovery_id, r, s = 0, 0, 0
while not r or not s:
k = random.randrange(1, curve.n)
k_x, k_y = scalar_multiply(k, curve.g)
# r
r = k_x % curve.n
recovery_id = 0 | 2 if k_x > curve.n else 0 | k_y % 2
# s
s = ((e + r * private_key) * modular_multiplicative_inverse(k, curve.n)) % curve.n
return recovery_id, r, s

计算消息摘要

用私钥签署交易实际上是签署一个私钥对。同样,在对消息进行签名时,需要先计算消息的摘要,再对消息摘要进行签名。

消息摘要具有特定格式。

物品内容

长度(H)

H字节长度,VarInt类型表示,固定值0x18

H

p>

字符串比特币签名消息:\n UTF-8 编码

长度(米)

M字节长度,VarInt类型表示

M

消息$m$的UTF-8编码

def message_bytes(message: str) -> bytes:
"""Serialize plain text message to format (LEN || message.utf-8)"""
msg_bytes = message.encode('utf-8')
return int_to_varint(len(msg_bytes)) + msg_bytes


def message_digest(message: str) -> bytes:
"""Returns the digest of plain text message"""
return message_bytes('Bitcoin Signed Message:\n') + message_bytes(message)

比特币私钥格式

调用message_digest方法获取原始消息的消息摘要。

请注意,我们在计算“消息摘要”时没有使用哈希方法,所以严格来说,这个计算的结果不能称为消息的“摘要”。对整个转换过程更准确的描述应该是“消息格式化”或“消息序列化”。之所以也使用“digest”这个词,是为了对应前面交易签名介绍中提到的“”,方便大家整体理解。

序列化签名结果

至此,所有的准备工作都完成了,需要序列化才能显示最终结果。

签名 $(recid, r, s)$ 总是序列化为 65 字节。

字节长度内容

1

前缀信息

32

整数r是序列化的字节流

32

大端模式序列化的整数s字节流

别忘了,比特币公钥方法有两种表示,一种以0x04开头,有65个字节,另一种以0x02或0x03开头,有33个字节,即同一个公钥会对应两个 P2PKH 地址。为了在签名验证时验证恢复的公钥的哈希是否与输入地址匹配,还需要在前缀信息中标记公钥的表示。

前缀值内容

27 + recid + 4

公钥用33个字节表示

27 + recid + 0

公钥用65字节表示

最后对序列化的签名数据进行Base64编码处理,方便人们复制转录。

from base64 import b64encode


def sign_message(private_key: int, plain_text: str) -> tuple:
"""Sign arbitrary message with bitcoin private key, returns (p2pkh_address, serialized_compact_signature)"""
d = message_digest(plain_text)
# recovery signature
recovery_id, r, s = sign_recoverable(private_key, d)
# p2pkh address
public_key = scalar_multiply(private_key, curve.g)
p2pkh_address = public_key_to_address(public_key, compressed=True)
# prefix = 27 + recovery_id + (4 if using compressed public key else 0)
prefix = 27 + recovery_id + 4
serialized_sig = prefix.to_bytes(1, byteorder='big') + r.to_bytes(32, byteorder='big') + s.to_bytes(32, byteorder='big')
return p2pkh_address, b64encode(serialized_sig).decode('ascii')

我们写一个简单的例子来测试。

if __name__ == '__main__':
# 私钥
priv_key = 0xf97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62
# 公钥
pub_key = scalar_multiply(priv_key, curve.g)
# 消息
plain = '使用比特币私钥对任意消息签名 aaron67'
print(plain)
# 签名
print(sign_message(priv_key, plain))

比特币私钥格式

输出是

使用比特币私钥对任意消息签名 aaron67
('1AfxgwYJrBgriZDLryfyKuSdBsi59jeBX9', 'H87QAsj1vFPZBNBCyfJtOK2HW9IpM5Bio5zoeRoMr7Yb4Qr4//4X/C/Ah5d64Lq4P/5EKImfnAIW8x0zx74HQSs=')

使用钱包工具或第三方服务验证此输出,即可成功验证签名。

签名验证

在验证消息的签名时,输入变成比特币地址、原始消息和序列化的比特币私钥格式,因此相应的签名验证过程也应该微调。

从序列化的签名中,解析出recid和$(r,s)$,并明确公钥的表示,还原公钥,用公钥和消息$(r,s)$验证签名验证恢复的公钥的哈希是否与输入的比特币地址匹配

使用recid恢复公钥时,需要从该点的X坐标中找到对应的Y坐标。 Secp256k1 的曲线方程

$$

y^2 \equiv x^3 + 7 \pmod{p}

$$

知道$x$就等于知道$y^2 \bmod{p}$,要求$y$,需要“平方”,很麻烦,因为有取模操作。我们可以利用费马小定理来改进。

对于整数 $a$ 和素数 $p$,有

$$

a^p \equiv a \pmod{p}

$$

曲线Secp256k1的$p$是素数,所以有

$$

y^{p} \equiv y \pmod {p}

$$

利用性质将上式两边放大$y$倍,有

$$

比特币私钥格式

y^{p+1} \equiv y^2 \pmod{p}

$$

$$

(y^2)^{\frac{p+1}{2}} \equiv y^2 \pmod{p}

$$

然后

$$

y = (y^2)^{\frac{p+1}{4}} \bmod{p}\ \ \ \ 或 \ \ \ \ -(y^2)^ {\frac{p+1}{4}} \bmod{p}

$$

注意 $\ frac{p+1}{4}$ 是一个整数。也就是说,$y$ 可以通过将 $y^2$ 提高到整数次方来求解。

有了这些知识,不难写出对应的签名验证码,求解point_k时请注意费马小定理的应用。

def verify_message(p2pkh_address: str, plain_text: str, signature: str) -> bool:
"""Verify serialized compact signature with p2pkh address and plain text"""
sig_bytes = b64decode(signature)
if len(sig_bytes) != 65:
return False
prefix, r, s = sig_bytes[0], int.from_bytes(sig_bytes[1:33], byteorder='big'), int.from_bytes(sig_bytes[33:], byteorder='big')
# Calculate recovery_id
compressed = False
if prefix < 27 or prefix >= 35:
return False
if prefix >= 31:
compressed = True
prefix -= 4
recovery_id = prefix - 27
# Recover point kG, k is the ephemeral private key
x = r + (curve.n if recovery_id >= 2 else 0)
y_squared = (x * x * x + curve.a * x + curve.b) % curve.p
y = pow(y_squared, (curve.p + 1) // 4, curve.p)
if (y + recovery_id) % 2 != 0:
y = -y % curve.p
point_k = (x, y)
# Calculate point aG, a is the private key
d = message_digest(plain_text)
e = hash_to_int(d)
mod_inv_r = modular_multiplicative_inverse(r, curve.n)
public_key = add(scalar_multiply(mod_inv_r * s, point_k), scalar_multiply(mod_inv_r * (-e % curve.n), curve.g))
# Verify signature
if not verify_signature(public_key, d, (r, s)):
return False
# Check public key hash
if public_key_hash(public_key, compressed) != address_to_public_key_hash(p2pkh_address):
return False
# OK
return True

我们使用其他工具生成的签名作为输入,来验证签名验证码的正确性。将私钥导入ElectrumSV钱包对消息进行签名,即可得到结果

IGdzMq98lowek10e3JFXWj909xp0oLRj71aF7jpWRxaabwH+fBia/K2JpoGQlFFbAl/Q5jo2DYSzQw6pZWhmRtk=

调用验证方法test。

if __name__ == '__main__':
# 私钥
priv_key = 0xf97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62
# 公钥
pub_key = scalar_multiply(priv_key, curve.g)
# 地址
address = public_key_to_address(pub_key, compressed=True)
# 消息
plain = '使用比特币私钥对任意消息签名 aaron67'
# 验证签名
sig_electrum = 'IGdzMq98lowek10e3JFXWj909xp0oLRj71aF7jpWRxaabwH+fBia/K2JpoGQlFFbAl/Q5jo2DYSzQw6pZWhmRtk='
print(verify_message(address, plain, sig_electrum))

运算的结果是

True

输出符合预期。

完整代码

sign_message.py

参考