如何在Dart中生成与JavaScript一致的secp256k1签名?
Dart与JS的secp256k1签名不一致问题
问题描述
我在Dart中使用secp256k1生成签名时,结果始终和JavaScript版本不一致。同时每次运行Dart函数,返回的signature.R和signature.S都不一样,想问这是为什么?
JavaScript实现代码
使用@noble/secp256k1@^1.7.0库:
import * as secp from "@noble/secp256k1"; const signature = await secp.sign(msgHash, hashingPrivateKey, { der: false, recovered: true, });
Dart实现代码(使用secp256k1库)
当前使用的Dart secp256k1库找不到对应der和recovered的配置选项:
import 'package:secp256k1/secp256k1.dart' as secp; var pk = secp.PrivateKey.fromHex(hashingPrivateKey); var pub = pk.publicKey; final signature = pk.signature(msgHash);
输入参数
msgHash:fea272bf4112f825697cfebe6f8f7dd8de1726d0c62ab45b78de4cb62f99a8dchashingPrivateKey:bf751b9f4cacc36548c56a88dc47aa87b93b415705b9a62aa0a669d98381f619
签名结果对比
- JavaScript生成的签名:
A2Mt3EHNy1HHgBwt46PSUBJG0BdyPs326hoixd6/AJdaY9axCXwEQRHgt4Xb1dr9LLr5r+C/fRM24ySeioOBWAA= - Dart生成的签名(通过拼接R和S得到):
D+ZSaLNGbFiAinyJ/B5GcLwI7dl1NqyHDkKG34iUXvlUhQ7Q6LF2gR94Nbyx2JZ3TH0fVZ4mU4MGAyWxtEDJeA==
Dart中拼接R和S的代码:
var sig = Uint8List.fromList([ ...NanoHelpers.bigIntToBytes(signature.R), ...NanoHelpers.bigIntToBytes(signature.S) ]);
尝试使用pointycastle库(仍不一致)
ECSignature createSignature(String msgHash, String hashingPrivateKey) { var domain = ECDomainParameters('secp256k1'); ECPrivateKey ecPrivateKey = ECPrivateKey( NanoHelpers.byteToBigInt( Uint8List.fromList(hex.decode(hashingPrivateKey))), domain); ECSignature signature = CryptoUtils.ecSign( ecPrivateKey, Uint8List.fromList(hex.decode(msgHash)), algorithmName: 'SHA-256/DET-ECDSA'); var g = ecPrivateKey.parameters?.G; var ecPublicKey = ECPublicKey( g! * NanoHelpers.byteToBigInt( Uint8List.fromList(hex.decode(hashingPrivateKey))), domain, ); var verify = CryptoUtils.ecVerify( ecPublicKey, Uint8List.fromList(hex.decode(msgHash)), signature, algorithm: 'SHA-256/DET-ECDSA'); print('verify====$verify'); print('ECSignature.R===${signature.r.toRadixString(16)}'); print('ECSignature.S===${signature.s.toRadixString(16)}'); var sig = Uint8List.fromList([ ...NanoHelpers.bigIntToBytes(signature.r), ...NanoHelpers.bigIntToBytes(signature.s) ]); print('sig=createSignature==1==${base64.encode(sig)}'); print('sig==createSignature=1==${base64.encode(sig).length}'); return signature; }
解答
1. 每次Dart签名R、S不同的原因
ECDSA签名过程需要引入随机数k,标准ECDSA每次使用不同的k就会生成不同的R和S。但你用JS库时默认启用了RFC6979确定性随机数生成,所以每次签名结果固定;而Dart的secp256k1库默认可能用的是随机k,导致每次签名不同。
2. 签名不一致的核心问题
(1)签名格式差异
JS库中der: false指定输出64字节紧凑签名(32字节R + 32字节S),recovered: true会额外添加1字节恢复ID,最终返回65字节数据;而Dart代码只拼接了R和S,少了恢复ID,格式不匹配。
(2)S值规范化
@noble/secp256k1默认会将S值规范化到曲线阶的一半以下(即当S > n/2时,自动替换为n-S),部分Dart库可能未做这个处理,导致S值差异。
(3)签名生成逻辑差异
JS库默认用RFC6979生成确定性随机数,而Dart的secp256k1库可能默认未启用该逻辑,导致签名结果不固定。
解决步骤
针对Dart的secp256k1库
- 启用RFC6979确定性签名模式,确保每次签名的R、S固定。
- 实现S值规范化处理,将大于曲线阶一半的S替换为n-S。
- 生成签名后获取恢复ID,拼接32字节R + 32字节S + 1字节恢复ID,再转base64。
使用pointycastle的修正方案
- 保留确定性ECDSA配置,添加S值规范化逻辑:
// 规范化S值 BigInt n = domain.n; BigInt halfN = n >> 1; if (signature.s > halfN) { signature = ECSignature(signature.r, n - signature.s); }
- 实现签名恢复逻辑获取恢复ID,拼接R、S和恢复ID后再转base64。
内容的提问来源于stack exchange,提问作者dev
相关产品推荐
相关产品推荐

