You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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))

差异原因说明

  1. S值归一化:NodeJS的secp256k1库默认会将签名中的S值调整到曲线阶的一半以下(符合BIP-62规范),而ecdsa库默认不做这个处理,需要通过sigencode_string_canonize参数开启。
  2. Recovery ID:NodeJS的签名包含1字节的Recovery ID(用于恢复公钥),而ecdsa库默认只输出R和S的64字节,需要手动计算并添加这个字节。
  3. 底层实现:方案一使用的Python版secp256k1和NodeJS库基于同一个C语言底层库,所以行为完全一致,无需手动处理这些细节。

内容的提问来源于stack exchange,提问作者37dev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:16:04