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

如何在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: fea272bf4112f825697cfebe6f8f7dd8de1726d0c62ab45b78de4cb62f99a8dc
  • hashingPrivateKey: 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库

  1. 启用RFC6979确定性签名模式,确保每次签名的R、S固定。
  2. 实现S值规范化处理,将大于曲线阶一半的S替换为n-S。
  3. 生成签名后获取恢复ID,拼接32字节R + 32字节S + 1字节恢复ID,再转base64。

使用pointycastle的修正方案

  1. 保留确定性ECDSA配置,添加S值规范化逻辑:
// 规范化S值
BigInt n = domain.n;
BigInt halfN = n >> 1;
if (signature.s > halfN) {
  signature = ECSignature(signature.r, n - signature.s);
}
  1. 实现签名恢复逻辑获取恢复ID,拼接R、S和恢复ID后再转base64。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 13:52:06