0

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;
}
dev
  • 73
  • 3
  • 11
  • Both codes lack a hint about which libraries are used (we can guess, but it shouldn't be like this). Please also share non-productive test data and show how these are imported. First, to verify that the data is imported correctly (e.g. using the correct encoding), and second, to compare the signatures, see [MCVE](https://stackoverflow.com/help/minimal-reproducible-example). – Topaco Aug 14 '23 at 13:24
  • @Topaco please see the edited question – dev Aug 14 '23 at 14:36
  • Your JavaScript code gives me the error message *Error: sign() legacy options not supported* for the latest version (2.0.0). Which version are you running? For 1.7.1 a 64 bytes signature in IEEE 1363 format is returned, for the Dart code the same applies. So I can't reproduce the issue! Again, post sample data: Non-productive key, message and the two signatures. Show how you use this data in your codes. – Topaco Aug 14 '23 at 15:45
  • Note that the JavaScript library additionally determines the recovery ID and uses deterministic ECDSA. The Dart library does not determine the recovery ID (as far as I can see) and applies non-deterministic ECDSA. So if you want to comply with the JavaScript library regarding these points, you probably have to use a different Dart library. – Topaco Aug 14 '23 at 15:50
  • @Topaco Could you please suggest me a library for this.I tried as above code sample but failed. – dev Aug 15 '23 at 07:36
  • As I said before: I can't reproduce the issue and thus don't know what you need, so I can't help. You would need to post 1. the @noble/secp256k1 version you use, 2. **non-productive** sample data: the signatures `signature` of **both** codes (to show what *But I am missing 2 bytes from the signature* means) plus private key `hashingPrivateKey`). – Topaco Aug 15 '23 at 07:53
  • Even after your last change you still haven't explained what you mean by *But I am missing 2 bytes from the signature?* Is the signature too short (which I can't reproduce) or do you refer to the recovery ID? Probably it would be easier to explain this with sample data, but you seem to have an allergy to sample data. Regarding the last code snippet, the libraries used are missing (one more time). – Topaco Aug 15 '23 at 13:46
  • @Topaco I updated the question with sample input data and the library used – dev Aug 16 '23 at 06:26
  • Post the signatures generated by both codes and explain what you mean by *But I am missing 2 bytes from the signature*. I cannot reproduce this! On my machine, both signatures are 64 bytes. In other words, what exactly is the problem? – Topaco Aug 16 '23 at 07:26
  • @Topaco please see my updated question. I just want to generate the same signature as generated from the javascript function – dev Aug 16 '23 at 08:53
  • From the description of the [secp256k1 package](https://pub.dev/packages/secp256k1) you used: *WARN: This lib does not provide deterministic ECDSA signature!*. So you cannot use this package. An alternative is [PointyCastle](https://pub.dev/packages/pointycastle), which like @noble\secp256k1 supports *deterministic* ECDSA. Note that with PointyCastle you can't calculate the recovery ID (at least not out-of-the-box), but you don't seem to need the recovery ID (I asked for it several times). – Topaco Aug 16 '23 at 10:23
  • @Topaco used same library..but couldn't able to succeed. please check edited question for sample – dev Aug 16 '23 at 10:48
  • No, this is not PointyCastle, but a PointyCastle wrapper which seems to require the data and not the data hash. Use the data itself or a function that accepts the hash. If this is not possible, apply PointyCastle directly, since it can handle both data and hash. – Topaco Aug 16 '23 at 13:41

2 Answers2

1

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 

Edit: As mentioned in the comment, in the Dart code the concatenated format is needed. This can be easily realized as follows:

import 'dart:convert'; // for Base64 encoding
...
Uint8List signatureRecoveryID = Uint8List.fromList(signatureIEEEP1363 + List<int>.from([recoveryId]));
print(hex.encode(signatureRecoveryID));    // 03632ddc41cdcb51c7801c2de3a3d2501246d017723ecdf6ea1a22c5debf00975a63d6b1097c044111e0b785dbd5dafd2cbaf9afe0bf7d1336e3249e8a83815800
print(base64.encode(signatureRecoveryID)); // A2Mt3EHNy1HHgBwt46PSUBJG0BdyPs326hoixd6/AJdaY9axCXwEQRHgt4Xb1dr9LLr5r+C/fRM24ySeioOBWAA=
NotARobot
  • 143
  • 6
0

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.

dev
  • 73
  • 3
  • 11