如何让Python与NodeJS使用secp256k1生成相同ECDSA签名?
解决Python与NodeJS secp256k1签名不一致的问题
你遇到的签名不一致问题主要是因为两个库在签名格式、S值归一化以及Recovery ID处理上的差异导致的。NodeJS的secp256k1库基于底层的libsecp256k1实现,默认会做这些细节处理,而Python的ecdsa库默认行为不同。下面提供两种解决方案,优先推荐第一种,因为它和NodeJS使用相同的底层依赖,行为完全对齐。
方案一:使用Python版secp256k1库(最简便)
直接使用和NodeJS同源的secp256k1 Python绑定,这样无需手动处理各种细节,签名逻辑完全一致。
步骤1:安装依赖
pip install secp256k1
步骤2:修改后的代码
import hashlib import secp256k1 # 原始消息和私钥 message = bytearray([2, 118, 145, 101, 166, 249, 149, 13, 2, 58, 65, 94, 230, 104, 184, 11, 185, 107, 92, 154, 226, 3, 93, 151, 189, 251, 68, 243, 86, 23, 90, 68, 255, 111, 3, 0, 0, 0, 0, 0, 0, 187, 226, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 84, 101, 115, 116, 105, 0, 0, 0, 0, 0, 0, 0]) private_key_buffer = bytearray([122, 241, 114, 103, 51, 227, 157, 149, 221, 126, 157, 173, 31, 111, 43, 118, 208, 71, 123, 59, 96, 68, 57, 177, 53, 59, 151, 188, 36, 167, 40, 68]) # 计算消息的SHA3-256哈希,和NodeJS的SHA3BUF行为一致 msg_hash = hashlib.sha3_256(message).digest() # 加载原始私钥 sk = secp256k1.PrivateKey(private_key_buffer, raw=True) # 生成可恢复签名(包含Recovery ID) sig_tuple = sk.sign(msg_hash, raw=True, recoverable=True) # 拼接签名:r(32字节) + s(32字节) + Recovery ID(1字节),和NodeJS输出格式完全匹配 signature = sig_tuple[0] + bytes([sig_tuple[1]]) # 转换为列表查看结果,和NodeJS的签名一致 print(list(signature))
方案二:修改ecdsa库的签名逻辑
如果你不想引入新库,可以调整ecdsa库的参数,手动处理S值归一化和Recovery ID:
import hashlib import ecdsa from ecdsa.util import sigencode_string_canonize, sigdecode_string, number_to_string message = bytearray([2, 118, 145, 101, 166, 249, 149, 13, 2, 58, 65, 94, 230, 104, 184, 11, 185, 107, 92, 154, 226, 3, 93, 151, 189, 251, 68, 243, 86, 23, 90, 68, 255, 111, 3, 0, 0, 0, 0, 0, 0, 187, 226, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 84, 101, 115, 116, 105, 0, 0, 0, 0, 0, 0, 0]) private_key_buffer = bytearray([122, 241, 114, 103, 51, 227, 157, 149, 221, 126, 157, 173, 31, 111, 43, 118, 208, 71, 123, 59, 96, 68, 57, 177, 53, 59, 151, 188, 36, 167, 40, 68]) # 手动计算SHA3-256哈希 msg_hash = hashlib.sha3_256(message).digest() # 加载私钥 sk = ecdsa.SigningKey.from_string(private_key_buffer, curve=ecdsa.SECP256k1) # 签名时使用canonize编码,确保S值归一化到曲线阶的一半以下(符合BIP-62,和NodeJS库行为一致) # hashfunc设为恒等函数,因为我们已经手动计算了哈希,避免重复哈希 sig_bytes = sk.sign_deterministic(msg_hash, hashfunc=lambda x: x, sigencode=sigencode_string_canonize) r, s = sigdecode_string(sig_bytes, sk.curve.order) # 计算Recovery ID(用于恢复公钥,NodeJS签名包含这个字节) vk = sk.get_verifying_key() recovery_id = None for i in range(4): try: recovered_vk = ecdsa.VerifyingKey.from_public_key_recovery( number_to_string(r, sk.curve.order) + number_to_string(s, sk.curve.order), msg_hash, sk.curve, hashlib.sha3_256, i ) if recovered_vk == vk: recovery_id = i break except: continue # 拼接最终签名:r + s + Recovery ID signature = number_to_string(r, sk.curve.order) + number_to_string(s, sk.curve.order) + bytes([recovery_id]) print(list(signature))
差异原因说明
- S值归一化:NodeJS的
secp256k1库默认会将签名中的S值调整到曲线阶的一半以下(符合BIP-62规范),而ecdsa库默认不做这个处理,需要通过sigencode_string_canonize参数开启。 - Recovery ID:NodeJS的签名包含1字节的Recovery ID(用于恢复公钥),而
ecdsa库默认只输出R和S的64字节,需要手动计算并添加这个字节。 - 底层实现:方案一使用的Python版
secp256k1和NodeJS库基于同一个C语言底层库,所以行为完全一致,无需手动处理这些细节。
内容的提问来源于stack exchange,提问作者37dev
相关产品推荐
相关产品推荐

