Reputation: 152
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;
}
Upvotes: 1
Views: 962
Reputation:
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=
Upvotes: 1
Reputation: 152
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.
Upvotes: 0