ECDSA:签名/验证示例

详细解释 ECDSA 签名算法的工作原理后,下面通过代码示例演示其实际用法。

本例将使用 Python 软件包 pycoin 。它实现了采用 secp256k1 曲线(用于比特币密码学)的 ECDSA 签名算法,以及许多与比特币区块链有关的其他功能:

pip install pycoin

使用 secp256k1 曲线和 SHA3-256 进行 ECDSA 签名/验证

首先,定义用于哈希、ECDSA 签名和 ECDSA 签名验证的函数:

from pycoin.ecdsa import generator_secp256k1, sign, verify
import hashlib, secrets

def sha3_256Hash(msg):
    hashBytes = hashlib.sha3_256(msg.encode("utf8")).digest()
    return int.from_bytes(hashBytes, byteorder="big")

def signECDSAsecp256k1(msg, privKey):
    msgHash = sha3_256Hash(msg)
    signature = sign(generator_secp256k1, privKey, msgHash)
    return signature

def verifyECDSAsecp256k1(msg, signature, pubKey):
    msgHash = sha3_256Hash(msg)
    valid = verify(generator_secp256k1, pubKey, msgHash, signature)
    return valid

哈希函数 sha3_256Hash(msg) 计算并返回以 256 位整数表示的 SHA3-256 哈希值。后面的签名/验证过程会用到它。

函数 signECDSAsecp256k1(msg, privKey) 接收文本消息和 256 位 secp256k1 私钥,计算 ECDSA 签名 {r, s},并以一对 256 位整数的形式返回。pycoin 库默认生成的 ECDSA 签名是RFC 6979 所述的确定性签名。

函数 verifyECDSAsecp256k1(msg, signature, pubKey) 接收文本消息、ECDSA 签名 {r, s} 和 2*256 位 ECDSA 公钥(未压缩),并返回签名是否有效

下面演示如何使用上述函数对消息进行签名验证其签名:

# ECDSA sign message (using the curve secp256k1 + SHA3-256)
msg = "Message for ECDSA signing"
privKey = secrets.randbelow(generator_secp256k1.order())
signature = signECDSAsecp256k1(msg, privKey)
print("Message:", msg)
print("Private key:", hex(privKey))
print("Signature: r=" + hex(signature[0]) + ", s=" + hex(signature[1]))

# ECDSA verify signature (using the curve secp256k1 + SHA3-256)
pubKey = (generator_secp256k1 * privKey).pair()
valid = verifyECDSAsecp256k1(msg, signature, pubKey)
print("\nMessage:", msg)
print("Public key: (" + hex(pubKey[0]) + ", " + hex(pubKey[1]) + ")")
print("Signature valid?", valid)

# ECDSA verify tampered signature (using the curve secp256k1 + SHA3-256)
msg = "Tampered message"
valid = verifyECDSAsecp256k1(msg, signature, pubKey)
print("\nMessage:", msg)
print("Signature (tampered msg) valid?", valid)

运行上述代码示例:https://repl.it/@nakov/ECDSA-sign-verify-in-Python

上述代码的输出如下:

Message: Message for ECDSA signing
Private key: 0x79afbf7147841fca72b45a1978dd7669470ba67abbe5c220062924380c9c364b
Signature: r=0xb83380f6e1d09411ebf49afd1a95c738686bfb2b0fe2391134f4ae3d6d77b78a, s=0x6c305afcac930a3ea1721c04d8a1a979016baae011319746323a756fbaee1811

Message: Message for ECDSA signing
Public key: (0x3804a19f2437f7bba4fcfbc194379e43e514aa98073db3528ccdbdb642e240, 0x6b22d833b9a502b0e10e58aac485aa357bccd1df6ec0fa4d398908c1ac1920bc)
Signature valid? True

Message: Tampered message
Signature (tampered msg) valid? False

从上述输出可以看出,随机生成的 secp256k1 私钥包含 64 个十六进制数字(256 位)。签名后得到的签名 {r, s} 由 2 个 256 位整数构成。将私钥乘以曲线生成元点得到的公钥由 2 * 256 位组成(未压缩)。生成 ECDSA 数字签名后可以正确通过验证;如果消息遭到篡改,签名验证就会失败。

从 ECDSA 签名恢复公钥

我们已经知道,在 ECDSA 中可以从签名恢复公钥。在上一个示例末尾添加以下代码来演示这一点:

from pycoin.ecdsa import possible_public_pairs_for_signature

def recoverPubKeyFromSignature(msg, signature):
    msgHash = sha3_256Hash(msg)
    recoveredPubKeys = possible_public_pairs_for_signature(
        generator_secp256k1, msgHash, signature)
    return recoveredPubKeys

msg = "Message for ECDSA signing"
recoveredPubKeys = recoverPubKeyFromSignature(msg, signature)
print("\nMessage:", msg)
print("Signature: r=" + hex(signature[0]) + ", s=" + hex(signature[1]))
for pk in recoveredPubKeys:
    print("Recovered public key from signature: (" +
          hex(pk[0]) + ", " + hex(pk[1]) + ")")

运行上述代码示例:https://repl.it/@nakov/ECDSA-public-key-recovery-in-Python

上述代码使用 http://www.secg.org/sec1-v2.pdf 中描述的算法,从 ECDSA 签名和已签名消息中恢复所有可能的 EC 公钥。请注意,可能有多个 EC 公钥(0、1 或 2 个)与消息和签名匹配。上述代码(连同前面的代码)的预期输出如下:

Message: Message for ECDSA signing
Private key: 0xc374556584db050001c2c9265b546e66d3dbbe8239d17427c176d834a19638dc
Signature: r=0xd034c98af3274ad93f3c8ce944bbc17b11b6aa170c5f097ed98687fa0d93347c, s=0xa2318ceea2002caba38efbba3bf8ef8d43236a6edc33c040734d8eb2ed77f608

Message: Message for ECDSA signing
Public key: (0x10b5d9028ec828a0f9111e36f046afa5a0c677357351093426bcec10c663db7d, 0x271763c56fcd87b72d59ceaa5b9c3fd2122788fe344751a9bde373f903e5bb20)
Signature valid? True

Message: Tampered message
Signature (tampered msg) valid? False

Message: Message for ECDSA signing
Signature: r=0xd034c98af3274ad93f3c8ce944bbc17b11b6aa170c5f097ed98687fa0d93347c, s=0xa2318ceea2002caba38efbba3bf8ef8d43236a6edc33c040734d8eb2ed77f608
Recovered public key from signature: (0x1353fd26a6cb6110980cfd2bb5eca3b3cc3e08c930ad5991395dd826a250c79, 0xba6825142e230ee1fa2b406f3f9158a47ee49daca8ac47898c5fd92d805a101e)
Recovered public key from signature: (0x10b5d9028ec828a0f9111e36f046afa5a0c677357351093426bcec10c663db7d, 0x271763c56fcd87b72d59ceaa5b9c3fd2122788fe344751a9bde373f903e5bb20)

可以明显看出,恢复出的候选公钥有 2 个:一个等于与签名者私钥相匹配的公钥,另一个则不是(它符合公钥恢复背后的数学关系,但不是正确公钥)。为避免这种歧义,可以扩展签名以容纳 {r, s, v},其中 v 保存 ECDSA 签名算法中随机点 Ry 坐标奇偶性。之所以需要这一信息,是因为 ECDSA 签名只保留 Rx 坐标,丢失了该坐标。

从扩展 ECDSA 签名恢复公钥

要从 ECDSA 签名和消息中可靠地恢复公钥,需要一个既能生成扩展 ECDSA 签名 {r, s, v},又在内部支持公钥恢复的库。下面试用 Python 库 eth_keys

pip install eth_keys

eth_keys 是以太坊项目的一部分,实现了基于 secp256k1 的 ECC 密码学、私钥和公钥、ECDSA 扩展签名 {r, s, v},以及以太坊区块链地址。以下示例演示私钥生成、消息签名、从签名和消息中恢复公钥,以及签名验证:

import eth_keys, os

# Generate the private + public key pair (using the secp256k1 curve)
signerPrivKey = eth_keys.keys.PrivateKey(os.urandom(32))
signerPubKey = signerPrivKey.public_key
print('Private key (64 hex digits):', signerPrivKey)
print('Public key (uncompressed, 128 hex digits):', signerPubKey)

# ECDSA sign message (using the curve secp256k1 + Keccak-256)
msg = b'Message for signing'
signature = signerPrivKey.sign_msg(msg)
print('Message:', msg)
print('Signature: [r = {0}, s = {1}, v = {2}]'.format(
    hex(signature.r), hex(signature.s), hex(signature.v)))

# ECDSA public key recovery from signature + verify signature
# (using the curve secp256k1 + Keccak-256 hash)
msg = b'Message for signing'
recoveredPubKey = signature.recover_public_key_from_msg(msg)
print('Recovered public key (128 hex digits):', recoveredPubKey)
print('Public key correct?', recoveredPubKey == signerPubKey)
valid = signerPubKey.verify_msg(msg, signature)
print("Signature valid?", valid)

运行上述代码示例:https://repl.it/@nakov/ECDSA-public-key-recovery-extended-in-Python

上述代码的输出如下:

Private key (64 hex digits): 0x68abc765746a33272e47b0a96a0b4184048f70354221e04219fbc223bfe79794
Public key (uncompressed, 128 hex digits): 0x30a6dc572da312587144e7ccda1e9abd901323adebe7091bb4985e1202c2a10bc25f681b3d2e1a671438f0b125287b473c09ca345c5583cd627232b536b9ca0a
Message: b'Message for signing'
Signature: [r = 0x4cddf146c578d20a31fa6128e5d9afe6ac666e5ef5899f2767cacb56a42703cc, s = 0x3847036857aa3f077a2e142eee707e5af2653baa99b9d10764a0be3d61595dbb, v = 0x0]
Recovered public key (128 hex digits): 0x30a6dc572da312587144e7ccda1e9abd901323adebe7091bb4985e1202c2a10bc25f681b3d2e1a671438f0b125287b473c09ca345c5583cd627232b536b9ca0a
Public key correct? True
Signature valid? True

由于扩展 ECDSA 签名不存在歧义,公钥恢复总会成功。除非消息、公钥或签名遭到篡改,否则签名验证也会成功。你可以自由尝试和修改上述代码,也可以篡改已签名消息并观察结果。祝你探索愉快!

results matching ""

    No results matching ""