skip to Main Content

I am trying to create a signature in Dart using secp256k1. But I am generating different signature as compared to javascript. JavaScript Method as follows:

import * as secp from "@noble/secp256k1";    
const signature = await secp.sign(msgHash, hashingPrivateKey, {
        der: false,
        recovered: true,
    });

I need to pass these der and recovered parameters in my Dart code. I wasn’t able to find any option to add those params in my Dart code. My code is as follows:

import 'package:secp256k1/secp256k1.dart' as secp;
    var pk = secp.PrivateKey.fromHex(hashingPrivateKey);
    var pub = pk.publicKey;
    final signature = pk.signature(msgHash);

Each time I run this function in dart , different signature.R and different signature.S are returning.Could you please explain ?

Javascript library version is "@noble/secp256k1": "^1.7.0",
input values are

msgHash : fea272bf4112f825697cfebe6f8f7dd8de1726d0c62ab45b78de4cb62f99a8dc

hashingPrivateKey : bf751b9f4cacc36548c56a88dc47aa87b93b415705b9a62aa0a669d98381f619

result signature from javascript is A2Mt3EHNy1HHgBwt46PSUBJG0BdyPs326hoixd6/AJdaY9axCXwEQRHgt4Xb1dr9LLr5r+C/fRM24ySeioOBWAA=

result signature from dart is D+ZSaLNGbFiAinyJ/B5GcLwI7dl1NqyHDkKG34iUXvlUhQ7Q6LF2gR94Nbyx2JZ3TH0fVZ4mU4MGAyWxtEDJeA==

generated from

var sig = Uint8List.fromList([
        ...NanoHelpers.bigIntToBytes(signature.R),
        ...NanoHelpers.bigIntToBytes(signature.S)
      ]);

using pointycaslte I tried the below function which also couldn’t able to generate same signature.

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;
}

2

Answers


  1. Chosen as BEST ANSWER

    finally able to resolve it by creating custom Signature with recoveryId and DeterministicSignature method .the code as follows.

    import 'package:crypto/crypto.dart';
    import 'package:elliptic/elliptic.dart';
    Signature getDeterministicSignature(PrivateKey priv, List<int> hash) {
      var k = generateSecret(priv.curve.n, priv.D, hash);
      var inv = k.modInverse(priv.curve.n);
      var hexK = k.toRadixString(16).padLeft((k.bitLength + 7) ~/ 8 * 2, '0');
      var p = priv.curve.scalarBaseMul(List<int>.generate(hexK.length ~/ 2,
          (i) => int.parse(hexK.substring(i * 2, i * 2 + 2), radix: 16)));
      var r = p.X % priv.curve.n;
      if (r.sign == 0) {
        throw Exception('calculated R is zero');
      }
    
      var e = bitsToInt(hash, priv.curve.n.bitLength);
      var s = priv.D * r + e;
      s = (s * inv) % priv.curve.n;
    
      if (s > (priv.curve.n >> 1)) {
        s = priv.curve.n - s;
      }
    
      if (s.sign == 0) {
        throw Exception('calculated S is zero');
      }
    
      var recoveryId = (p.Y.isOdd ? 1 : 0) | (s.isOdd ? 2 : 0);
      return Signature.fromRS(r, s, recoveryId);
    }
    
    BigInt generateSecret(BigInt q, BigInt x, List<int> hash) {
      var hasher = sha256;
    
      var qLen = q.bitLength;
      var hoLen =
          32; // = sha256.size, because the sha256 is fixed here so do the len
      var roLen = (qLen + 7) >> 3;
    
      var bx = intToOctets(x, roLen) + bitsToOctets(hash, q, roLen);
      var v = List<int>.filled(hoLen, 0x01);
      var k = List<int>.filled(hoLen, 0x00);
    
      k = Hmac(hasher, k).convert(v + [0x00] + bx).bytes;
      v = Hmac(hasher, k).convert(v).bytes;
      k = Hmac(hasher, k).convert(v + [0x01] + bx).bytes;
      v = Hmac(hasher, k).convert(v).bytes;
    
      while (true) {
        var t = <int>[];
        while (t.length * 8 < qLen) {
          v = Hmac(hasher, k).convert(v).bytes;
          t = t + v;
        }
    
        var secret = bitsToInt(t, qLen);
        if (secret >= BigInt.one && secret < q) {
          return secret;
        }
    
        k = Hmac(hasher, k).convert(v + [0x00]).bytes;
        v = Hmac(hasher, k).convert(v).bytes;
      }
    }
    
    ///utils
    BigInt bitsToInt(List<int> hash, int qBitLen) {
      var orderBytes = (qBitLen + 7) ~/ 8;
      if (hash.length > qBitLen) {
        hash = hash.sublist(0, orderBytes);
      }
    
      var ret = BigInt.parse(
          List<String>.generate(
              hash.length, (i) => hash[i].toRadixString(16).padLeft(2, '0')).join(),
          radix: 16);
      var excess = hash.length * 8 - qBitLen;
      if (excess > 0) {
        ret >> excess;
      }
      return ret;
    }
    
    List<int> intToOctets(BigInt v, int roLen) {
      var vLen = (v.bitLength + 7) ~/ 8;
      var vHex = v.toRadixString(16).padLeft(vLen * 2, '0');
    
      var vBytes = List<int>.generate(
          vLen, (i) => int.parse(vHex.substring(2 * i, 2 * i + 2), radix: 16));
      if (vLen < roLen) {
        vBytes = List.filled(roLen - vLen, 0) + vBytes;
      }
      if (vLen > roLen) {
        vBytes = vBytes.sublist(vLen - roLen);
      }
    
      return vBytes;
    }
    
    List<int> bitsToOctets(List<int> input, BigInt q, int roLen) {
      var z1 = bitsToInt(input, q.bitLength);
      var z2 = z1 - q;
      if (z2.sign < 0) {
        return intToOctets(z1, roLen);
      }
      return intToOctets(z2, roLen);
    }
    

    custom Signature class is given below.

    import 'package:ninja_asn1/ninja_asn1.dart';
    
    
    class Signature {
      late BigInt R;
      late BigInt S;
      late int recoveryId;
    
      Signature.fromRS(this.R, this.S, this.recoveryId);
    
      Signature.fromCompact(List<int> compactBytes) {
        R = BigInt.parse(
            List<String>.generate(
                    32, (i) => compactBytes[i].toRadixString(16).padLeft(2, '0'))
                .join(),
            radix: 16);
        S = BigInt.parse(
            List<String>.generate(32,
                    (i) => compactBytes[i + 32].toRadixString(16).padLeft(2, '0'))
                .join(),
            radix: 16);
      }
    
      Signature.fromCompactHex(String compactHex) {
        R = BigInt.parse(compactHex.substring(0, 64), radix: 16);
        S = BigInt.parse(compactHex.substring(64, 128), radix: 16);
      }
    
      /// parsing the ECDSA signatures with the more strict
      /// Distinguished Encoding Rules (DER) of ISO/IEC 8825-1
      Signature.fromASN1(List<int> asn1Bytes) {
        _parseASN1(asn1Bytes);
      }
    
      /// [fromDER] is same to [fromASN1]
      /// parsing the ECDSA signatures with the more strict
      /// Distinguished Encoding Rules (DER) of ISO/IEC 8825-1
      Signature.fromDER(List<int> asn1Bytes) {
        _parseASN1(asn1Bytes);
      }
    
      /// parsing the ECDSA signatures with the more strict
      /// Distinguished Encoding Rules (DER) of ISO/IEC 8825-1
      Signature.fromASN1Hex(String asn1Hex) {
        _parseASN1Hex(asn1Hex);
      }
    
      /// [fromDERHex] is same to [fromASN1Hex]
      /// parsing the ECDSA signatures with the more strict
      /// Distinguished Encoding Rules (DER) of ISO/IEC 8825-1
      Signature.fromDERHex(String asn1Hex) {
        _parseASN1Hex(asn1Hex);
      }
    
      List<int> toCompact() {
        var hex = toCompactHex();
        return List<int>.generate(
            64, (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16));
      }
    
      List<int> toASN1() {
        return ASN1Sequence([ASN1Integer(R), ASN1Integer(S)]).encode();
      }
    
      /// [toDER] equals to [toASN1],
      /// serializing the ECDSA signatures with the more strict
      /// Distinguished Encoding Rules (DER) of ISO/IEC 8825-1
      List<int> toDER() {
        return toASN1();
      }
    
      String toCompactHex() {
        return R.toRadixString(16).padLeft(64, '0') +
            S.toRadixString(16).padLeft(64, '0');
      }
    
      String toASN1Hex() {
        var asn1 = toASN1();
        return List<String>.generate(
            asn1.length, (i) => asn1[i].toRadixString(16).padLeft(2, '0')).join();
      }
    
      /// [toDERHex] equals to [toASN1Hex]
      String toDERHex() {
        return toASN1Hex();
      }
    
      /// [toString] equals to [toASN1Hex] or [toDERHex],
      /// because the ASN1 is recommended in paper
      @override
      String toString() {
        return toASN1Hex();
      }
    
      void _parseASN1(List<int> asn1Bytes) {
        var p = ASN1Sequence.decode(asn1Bytes);
        R = (p.children[0] as ASN1Integer).value;
        S = (p.children[1] as ASN1Integer).value;
      }
    
      void _parseASN1Hex(String asn1Hex) {
        var asn1Bytes = List<int>.generate(asn1Hex.length ~/ 2,
            (i) => int.parse(asn1Hex.substring(i * 2, i * 2 + 2), radix: 16));
        var p = ASN1Sequence.decode(asn1Bytes);
        R = (p.children[0] as ASN1Integer).value;
        S = (p.children[1] as ASN1Integer).value;
      }
    }
    

    and finally

    import 'package:elliptic/elliptic.dart' as ellep;
    var sign = getDeterministicSignature(
          ellep.PrivateKey.fromHex(ellep.getSecp256k1(), hashingPrivateKey),
          hex.decode(msgHash));
    

    It worked well.Thank you.


  2. It is more convenient to use existing libraries instead of implementing all from scratch. In addition, there is an increased risk of side-channel attacks with custom implementations.

    A deterministic ECDSA signature can be implemented with PointyCastle:

    import 'dart:typed_data';
    import 'package:convert/convert.dart';
    import 'package:pointycastle/export.dart';
    import 'package:nanodart/nanodart.dart';
    ...
    String msgHashHex = "fea272bf4112f825697cfebe6f8f7dd8de1726d0c62ab45b78de4cb62f99a8dc";
    String keyHex = "bf751b9f4cacc36548c56a88dc47aa87b93b415705b9a62aa0a669d98381f619";
    Uint8List msgHash = Uint8List.fromList(hex.decode(msgHashHex));
    Uint8List key = Uint8List.fromList(hex.decode(keyHex));
    ECPrivateKey privateKey = ECPrivateKey(NanoHelpers.byteToBigInt(key), ECDomainParameters("secp256k1"));
    ECDSASigner ecdsaSigner = ECDSASigner(/*SHA256Digest()*/null, HMac(SHA256Digest(), 64)); // 1st parameter: pass digest, if msg is passed instead of msg hash; 2nd parameter: deterministic ECDSA
    NormalizedECDSASigner necdsaSigner = NormalizedECDSASigner(ecdsaSigner);
    necdsaSigner.init(true, PrivateKeyParameter(privateKey));
    ECSignature signature = necdsaSigner.generateSignature(msgHash) as ECSignature;
    Uint8List signatureIEEEP1363 = convertToIEEE1363(signature.r, signature.s);
    print(hex.encode(signatureIEEEP1363)); // 03632ddc41cdcb51c7801c2de3a3d2501246d017723ecdf6ea1a22c5debf00975a63d6b1097c044111e0b785dbd5dafd2cbaf9afe0bf7d1336e3249e8a838158
    
    // Helper
    Uint8List convertToIEEE1363(BigInt rBI, BigInt sBI){
        return Uint8List.fromList(pad(NanoHelpers.bigIntToBytes(rBI)) + pad(NanoHelpers.bigIntToBytes(sBI)));
    }
    List<int> pad(List<int> data){
        if (data.length < 32) data = Uint8List(32 - data.length) + data;
        return data;
    }
    

    The Recovery ID can be determined with the sec package:

    import 'package:sec/sec.dart';
    ...
    EC ec = EC.secp256k1;
    Uint8List publicKey = ec.createPublicKey(privateKey.d!, false);
    BigInt publicKeyInt = NanoHelpers.byteToBigInt(publicKey.sublist(1)) ;
    int recoveryId = EC.secp256k1.calculateRecoveryId(publicKeyInt, signature, msgHash)!;
    print(recoveryId); // 0
    

    Test:

    Running the JavaScript code with the @noble/secp256k1 library and v1.7.0 results in an array with 2 elements. The first element contains the signature in IEEE P1363 format as Uint8Array, the second element contains the recovery ID as int:

    import * as secp from "@noble/secp256k1";    
    
    const hashingPrivateKey = Buffer.from('bf751b9f4cacc36548c56a88dc47aa87b93b415705b9a62aa0a669d98381f619', 'hex')
    const msgHash = 'fea272bf4112f825697cfebe6f8f7dd8de1726d0c62ab45b78de4cb62f99a8dc';
    const signature = await secp.sign(msgHash, hashingPrivateKey, {der: false, recovered: true});
    console.log(signature) // [Uint8Array(64) [3, 99...], 0]
    
    console.log(Buffer.from(signature[0]).toString('hex')) // 03632ddc41cdcb51c7801c2de3a3d2501246d017723ecdf6ea1a22c5debf00975a63d6b1097c044111e0b785dbd5dafd2cbaf9afe0bf7d1336e3249e8a838158
    console.log(signature[1]) // 0
    

    The result is the same as that of the Dart code!

    Note that the result does not follow the format you posted. You may have applied a conversion function. To get the result you posted, the signature and recovery ID must be concatenated and must be Base64 encoded (this is not a standard format):

    const bufferRec = Buffer.allocUnsafe(1)
    bufferRec.writeUInt8(signature[1], 0)
    const bufferTot = Buffer.concat([signature[0], bufferRec]) 
    console.log(bufferTot.toString('base64')) // A2Mt3EHNy1HHgBwt46PSUBJG0BdyPs326hoixd6/AJdaY9axCXwEQRHgt4Xb1dr9LLr5r+C/fRM24ySeioOBWAA=
    console.log(bufferTot.toString('hex'))    // 03632ddc41cdcb51c7801c2de3a3d2501246d017723ecdf6ea1a22c5debf00975a63d6b1097c044111e0b785dbd5dafd2cbaf9afe0bf7d1336e3249e8a83815800 
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search