如何使用 ECDSA secp256k1 曲线在 python 中以与在 Javascript 中签名相同的方式对消息进行签名?

How to sign message in python the same way it is signed in Javascript by using the ECDSA secp256k1 curve?

我正在尝试对 python 中的字节数组进行签名,其方式与使用 secp256k1 from NodeJS

在加密库中发生的方式相同

这是 NodeJS/Browser 上的代码:

const secp256k1 = require('secp256k1')

var message = [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];

var private_key_buffer = [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]

const signature = secp256k1.sign(SHA3BUF(message), private_key_buffer)

这是我在 python 中的实现:

import hashlib
import ecdsa

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])

signinKey = ecdsa.SigningKey.from_string(private_key_buffer, curve=ecdsa.SECP256k1)

signature = signinKey.sign_deterministic(message, hashfunc=hashlib.sha3_256)

但由于某些原因,我在 javascript 代码中获得的签名与 python 代码中的签名不同:

java script signature: [23, 54, 64, 151, 95, 33, 200, 66, 246, 166, 144, 182, 81, 179, 124, 223, 250, 50, 137, 169, 45, 181, 197, 74, 225, 207, 116, 125, 50, 241, 38, 52, 118, 215, 252, 94, 191, 154, 200, 195, 152, 73, 1, 197, 158, 24, 72, 177, 118, 39, 241, 82, 114, 107, 25, 106, 67, 205, 202, 4, 7, 57, 82, 237]

python script signature: [213, 69, 97, 237, 85, 226, 217, 201, 51, 14, 220, 92, 105, 59, 54, 92, 87, 88, 233, 147, 191, 15, 21, 86, 134, 202, 205, 223, 83, 134, 70, 39, 10, 19, 147, 20, 181, 180, 88, 103, 79, 55, 144, 98, 84, 2, 224, 127, 192, 200, 200, 250, 170, 129, 67, 99, 163, 72, 92, 253, 109, 108, 104, 206]

那么如何让python代码输出与JS代码相同的签名呢?

对于确定性 ECDSA,如RFC6979, a hash algorithm is used in two places: One algorithm (H1) is used for hashing the message, another (H2) for determining the k-value. k is a parameter within the signature algorithm, whose role is described e.g. in RFC6979, section 2.4 or also here中所述。对于非确定性变体,k 是随机确定的,对于确定性变体,如 RFC6979 中所述。

RFC6979 未指定 H1H2 必须不同,请参阅 RFC6979, section 3.6。尽管如此,一个实现提供了分别定义两种哈希算法的可能性是有道理的。

  1. Python的 ECDSA 实现通常允许应用两种不同的哈希算法。在第二种情况中显示之前,以下变体对应于发布的 Python 代码,应用 same 哈希算法 H1 = H2 = SHA3-256sign_deterministic 方法中指定的哈希算法同时定义了 H1H2:

     import hashlib
     import ecdsa
    
     message = b'Everything should be made as simple as possible, but not simpler.'
     private_key_buffer = bytearray.fromhex('0000000000000000000000000000000000000000000000000000000000000001') 
    
     sk = ecdsa.SigningKey.from_string(private_key_buffer, curve=ecdsa.SECP256k1) 
     signature = sk.sign_deterministic(message, hashfunc=hashlib.sha3_256)
    
     print(signature.hex())
    

    签名是:

     r = 88ecdbc6a2762e7ad1160f7c984cd61385ff07982280538dd7d2103be2dce720
     s = c1487df9feab7afda6e6115bdd4d9c5316e3f917a3235a5e47aee09624491304
    
  2. 下一个变体使用 H1 = SHA3-256 对消息进行哈希处理,并使用 H2 = SHA256 进行 k-确定。这可以通过将 sign_deterministic 方法替换为 sign_digest_deterministic 方法来实现,该方法允许使用 H1 单独散列消息。 sign_digest_deterministic-method 中指定的哈希算法只定义了 H2:

     import hashlib
     import ecdsa
    
     message = b'Everything should be made as simple as possible, but not simpler.'
     private_key_buffer = bytearray.fromhex('0000000000000000000000000000000000000000000000000000000000000001') 
    
     digest = hashlib.sha3_256()
     digest.update(message)
     hash = digest.digest()
    
     sk = ecdsa.SigningKey.from_string(private_key_buffer, curve=ecdsa.SECP256k1) 
     signature = sk.sign_digest_deterministic(hash, hashfunc=hashlib.sha256)
    
     print(signature.hex())
    

    签名是:

     r = 64b10395957b78d3bd3db279e5fa4ebee36b58dd1becace4bc2d7e3a04cf6259
     s = 19f1eee7495064ac679d7b64ab7213b921b650c0a3746f2938ffeede0ff1f2e8
    
  3. 以下代码在功能上与发布的 NodeJS 代码相同:

     const secp256k1 = require('secp256k1')
     const sha3 = require('js-sha3')
    
     message = 'Everything should be made as simple as possible, but not simpler.'
     private_key_buffer = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001','hex')
    
     digest = sha3.sha3_256;
     hash = Buffer.from(digest(message), 'hex')
    
     signature = secp256k1.sign(hash, private_key_buffer)
     console.log(signature.signature.toString('hex'))
    

    并生成与第二种情况相同的 签名,即显然 H2 = SHA256。我没费多少功夫就找到了将其更改为 SHA3-256 的方法。但是,根据文档,可以 replace the default generator 实现 RFC6979。这也应该改变 H2,但可能会更昂贵。

总结:最简单的解决两个代码不兼容的方法 就是按照上面第二种情况中描述的那样改变Python-代码,即使用sign_digest_deterministic-方法。然后使用 SHA3-256 对消息进行哈希处理,k 生成发生在 SHA256 中。一个更昂贵的替代方案是实现自己的生成器以在 NodeJS 代码中启用 kSHA3-256 生成。或者,当然,您尝试为 NodeJS 代码找到另一个 ECDSA 库,它允许您分别定义 H1H2,类似于 Python 代码。

更新:

规范签名:如果(r,s)是签名,那么(r, -s mod n) = (r, n - s)也是valid signature. Here n is the order of the base point. If in case s > n/2 the part -s mod n = n - s is used instead of s, then the result for the signature is unambiguous and is limited to the area below n/2. This is called canonical signature, which is particularly relevant for the Bitcoin topic and also frequently used for test vectors