Open stefandric opened 1 month ago
Context:
I’m working on a Cosmos SDK project in Flutter/Dart, and I’m implementing RFC6979 deterministic ECDSA signing with the secp256k1 curve. While researching, I came across Decred’s implementation in signature.go, specifically the signRFC6979 method.
func signRFC6979(privKey *secp256k1.PrivateKey, hash []byte)
Is used to sign an iddoc 256 hash with private key. I am using your sign method from secp256k1 private key but it produces totally different output. What I can confirm is that private key and hash do match 100% before signing.
Goal:
I want to replicate the functionality of RFC6979 deterministic nonce generation and ECDSA signing in Dart, and I have a few specific questions around how Decred’s implementation works.
Questions:
1. Nonce Generation: How exactly does RFC6979 nonce generation integrate with secp256k1 in Decred’s implementation? Specifically, I’m trying to understand how HMAC-SHA256 is used to derive the deterministic nonce in this context. 2. Public Key Recovery Code: How is the public key recovery code calculated in Decred’s implementation alongside the generation of the (r, s) signature values? Is there a specific step I should pay attention to in terms of handling public key recovery? 3. Translation to Dart: Are there any key considerations when translating this Go-based implementation to Dart for the Cosmos SDK? I’m particularly interested in ensuring correctness in the nonce generation process and the handling of (r, s) values.
Hi, which method are you using to sign in Dart?
If you're aiming to get the same result as in signature.go, I noticed that the code there doesn’t seem to check whether the s value is in the lower half of the curve order. I'm not sure, but I think this might lead to inconsistencies.
To ensure consistent results and handle this properly, I recommend using:
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
final signer = EcdsaSigningKey(ECDSAPrivateKey.fromBytes(keybytes, Curves.generatorSecp256k1));
final ecdsaSign = signer.signDigestDeterminstic(digest: digestHash, hashFunc: () => SHA256());
final sigBytes = ecdsaSign.toBytes(ETHSignerConst.secp256.curve.baselen);
Hey @mrtnetwork thank you for your prompt response!
I am using sign from private key:
final sign = privateKey.sign(signDoc.toBuffer());
The method you provided produces almost same bytes sequence at the beginning, but afterwards it is really diverse. If you want I can share a working solution I wrote and if you like it we can modify it so it can be used in your library? it seems like s differentiates from the code you sent me.
EDIT: I got it work by normalizing s:
if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
Hey @mrtnetwork thank you for your prompt response!
I am using sign from private key:
final sign = privateKey.sign(signDoc.toBuffer());
The method you provided produces almost same bytes sequence at the beginning, but afterwards it is really diverse. If you want I can share a working solution I wrote and if you like it we can modify it so it can be used in your library? it seems like s differentiates from the code you sent me.
EDIT: I got it work by normalizing s:
if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
Which private key are you using? The cosmos_sdk
package supports three types of private keys:
/// EDSA
CosmosED25519PrivateKey
/// ECDSA (secp256k1)
CosmosSecp256K1PrivateKey
/// ECDSA (secp256r1)
CosmosSecp256R1PrivateKey
Each key uses a different elliptic curve. For your case, use CosmosSecp256K1PrivateKey
, which is the correct class, and its key is converted to a normalized s value.
Hey @mrtnetwork thank you for your prompt response! I am using sign from private key:
final sign = privateKey.sign(signDoc.toBuffer());
The method you provided produces almost same bytes sequence at the beginning, but afterwards it is really diverse. If you want I can share a working solution I wrote and if you like it we can modify it so it can be used in your library? it seems like s differentiates from the code you sent me. EDIT: I got it work by normalizing s:if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
Which private key are you using? The
cosmos_sdk
package supports three types of private keys:/// EDSA CosmosED25519PrivateKey /// ECDSA (secp256k1) CosmosSecp256K1PrivateKey /// ECDSA (secp256r1) CosmosSecp256R1PrivateKey
Each key uses a different elliptic curve. For your case, use
CosmosSecp256K1PrivateKey
, which is the correct class, and its key is converted to a normalized s value.
CosmosSecp256K1PrivateKey
and then sign method which gives me totally different result.
I was implementing your proposed way from blockchain_utils and I needed to normalize the signature.
Or you say that I can combine signing through CosmosSecp256K1PrivateKey
and the logic I just mentioned so it can be 'normalized' under the hood?
Hey @mrtnetwork thank you for your prompt response! I am using sign from private key:
final sign = privateKey.sign(signDoc.toBuffer());
The method you provided produces almost same bytes sequence at the beginning, but afterwards it is really diverse. If you want I can share a working solution I wrote and if you like it we can modify it so it can be used in your library? it seems like s differentiates from the code you sent me. EDIT: I got it work by normalizing s:if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
Which private key are you using? The
cosmos_sdk
package supports three types of private keys:/// EDSA CosmosED25519PrivateKey /// ECDSA (secp256k1) CosmosSecp256K1PrivateKey /// ECDSA (secp256r1) CosmosSecp256R1PrivateKey
Each key uses a different elliptic curve. For your case, use
CosmosSecp256K1PrivateKey
, which is the correct class, and its key is converted to a normalized s value.
CosmosSecp256K1PrivateKey
and then sign method which gives me totally different result. I was implementing your proposed way from blockchain_utils and I needed to normalize the signature.Or you say that I can combine signing through
CosmosSecp256K1PrivateKey
and the logic I just mentioned so it can be 'normalized' under the hood?
I'm a bit confused :) Could you provide the complete working code? I’m trying to implement this in the Cosmos SDK. In this section, you mentioned:
EDIT: I got it to work by normalizing s:
if (ecdsaSign.s > ETHSignerConst.orderHalf) {
ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s);
}
If this works for you, it should also work with CosmosSecp256K1PrivateKey
.
In ECDSA, there are two approaches. One doesn't check the s
value (used inpersonalSign
), but for most cryptocurrency transactions, the network requires that s
be lower than half of the order. If s
is greater thanorderHalf
, it must be adjusted. In scenarios wheres
is already lower, both transactionsigning
andpersonalSign
will give the same result.
Hey @mrtnetwork thank you for your prompt response! I am using sign from private key:
final sign = privateKey.sign(signDoc.toBuffer());
The method you provided produces almost same bytes sequence at the beginning, but afterwards it is really diverse. If you want I can share a working solution I wrote and if you like it we can modify it so it can be used in your library? it seems like s differentiates from the code you sent me. EDIT: I got it work by normalizing s:if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
Which private key are you using? The
cosmos_sdk
package supports three types of private keys:/// EDSA CosmosED25519PrivateKey /// ECDSA (secp256k1) CosmosSecp256K1PrivateKey /// ECDSA (secp256r1) CosmosSecp256R1PrivateKey
Each key uses a different elliptic curve. For your case, use
CosmosSecp256K1PrivateKey
, which is the correct class, and its key is converted to a normalized s value.
CosmosSecp256K1PrivateKey
and then sign method which gives me totally different result. I was implementing your proposed way from blockchain_utils and I needed to normalize the signature. Or you say that I can combine signing throughCosmosSecp256K1PrivateKey
and the logic I just mentioned so it can be 'normalized' under the hood?I'm a bit confused :) Could you provide the complete working code? I’m trying to implement this in the Cosmos SDK. In this section, you mentioned:
EDIT: I got it to work by normalizing s:
if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
If this works for you, it should also work with
CosmosSecp256K1PrivateKey
.In ECDSA, there are two approaches. One doesn't check the
s
value (used inpersonalSign
), but for most cryptocurrency transactions, the network requires thats
be lower than half of the order. Ifs
is greater thanorderHalf
, it must be adjusted. In scenarios wheres
is already lower, both transactionsigning
andpersonalSign
will give the same result.
Sorry for confusing you :)
final signer = EcdsaSigningKey(
ECDSAPrivateKey.fromBytes(privKeyBytes, Curves.generatorSecp256k1));
var ecdsaSign = signer.signDigestDeterminstic(
digest: _sha256Hash(idDoc), hashFunc: () => SHA256());
if (ecdsaSign.s > ETHSignerConst.orderHalf) {
ecdsaSign =
ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s);
}
final sigBytes = ecdsaSign.toBytes(ETHSignerConst.secp256.curve.baselen);
So what I wanted to say is that I needed to apply normalize logic from CosmosSigner to this code so I normalize s parameter to give me the correct output.
Hey @mrtnetwork thank you for your prompt response! I am using sign from private key:
final sign = privateKey.sign(signDoc.toBuffer());
The method you provided produces almost same bytes sequence at the beginning, but afterwards it is really diverse. If you want I can share a working solution I wrote and if you like it we can modify it so it can be used in your library? it seems like s differentiates from the code you sent me. EDIT: I got it work by normalizing s:if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
Which private key are you using? The
cosmos_sdk
package supports three types of private keys:/// EDSA CosmosED25519PrivateKey /// ECDSA (secp256k1) CosmosSecp256K1PrivateKey /// ECDSA (secp256r1) CosmosSecp256R1PrivateKey
Each key uses a different elliptic curve. For your case, use
CosmosSecp256K1PrivateKey
, which is the correct class, and its key is converted to a normalized s value.
CosmosSecp256K1PrivateKey
and then sign method which gives me totally different result. I was implementing your proposed way from blockchain_utils and I needed to normalize the signature. Or you say that I can combine signing throughCosmosSecp256K1PrivateKey
and the logic I just mentioned so it can be 'normalized' under the hood?I'm a bit confused :) Could you provide the complete working code? I’m trying to implement this in the Cosmos SDK. In this section, you mentioned: EDIT: I got it to work by normalizing s:
if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
If this works for you, it should also work with
CosmosSecp256K1PrivateKey
. In ECDSA, there are two approaches. One doesn't check thes
value (used inpersonalSign
), but for most cryptocurrency transactions, the network requires thats
be lower than half of the order. Ifs
is greater thanorderHalf
, it must be adjusted. In scenarios wheres
is already lower, both transactionsigning
andpersonalSign
will give the same result.Sorry for confusing you :)
final signer = EcdsaSigningKey( ECDSAPrivateKey.fromBytes(privKeyBytes, Curves.generatorSecp256k1)); var ecdsaSign = signer.signDigestDeterminstic( digest: _sha256Hash(idDoc), hashFunc: () => SHA256()); if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); } final sigBytes = ecdsaSign.toBytes(ETHSignerConst.secp256.curve.baselen);
So what I wanted to say is that I needed to apply normalize logic from CosmosSigner to this code so I normalize s parameter to give me the correct output.
ok CosmosSecp256K1PrivateKey.sign
It does exactly that!
Are you sure that CosmosSecp256K1PrivateKey.sign gives you a different result? Please double-check.
this the code of CosmosSecp256K1PrivateKey.sign
final hash = hashMessage ? QuickCrypto.sha256Hash(digest) : digest;
if (hash.length != ETHSignerConst.digestLength) {
throw ArgumentException(
"invalid digest. digest length must be ${ETHSignerConst.digestLength} got ${digest.length}");
}
ECDSASignature ecdsaSign = _ecdsaSigningKey.signDigestDeterminstic(
digest: hash, hashFunc: () => SHA256());
if (ecdsaSign.s > ETHSignerConst.orderHalf) {
ecdsaSign =
ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s);
}
final sigBytes = ecdsaSign.toBytes(ETHSignerConst.secp256.curve.baselen);
final verifyKey = toVerifyKey();
if (verifyKey.verify(hash, sigBytes)) {
return ecdsaSign.toBytes(ETHSignerConst.digestLength);
}
throw const MessageException(
'The created signature does not pass verification.');
Hey @mrtnetwork thank you for your prompt response! I am using sign from private key:
final sign = privateKey.sign(signDoc.toBuffer());
The method you provided produces almost same bytes sequence at the beginning, but afterwards it is really diverse. If you want I can share a working solution I wrote and if you like it we can modify it so it can be used in your library? it seems like s differentiates from the code you sent me. EDIT: I got it work by normalizing s:if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
Which private key are you using? The
cosmos_sdk
package supports three types of private keys:/// EDSA CosmosED25519PrivateKey /// ECDSA (secp256k1) CosmosSecp256K1PrivateKey /// ECDSA (secp256r1) CosmosSecp256R1PrivateKey
Each key uses a different elliptic curve. For your case, use
CosmosSecp256K1PrivateKey
, which is the correct class, and its key is converted to a normalized s value.
CosmosSecp256K1PrivateKey
and then sign method which gives me totally different result. I was implementing your proposed way from blockchain_utils and I needed to normalize the signature. Or you say that I can combine signing throughCosmosSecp256K1PrivateKey
and the logic I just mentioned so it can be 'normalized' under the hood?I'm a bit confused :) Could you provide the complete working code? I’m trying to implement this in the Cosmos SDK. In this section, you mentioned: EDIT: I got it to work by normalizing s:
if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); }
If this works for you, it should also work with
CosmosSecp256K1PrivateKey
. In ECDSA, there are two approaches. One doesn't check thes
value (used inpersonalSign
), but for most cryptocurrency transactions, the network requires thats
be lower than half of the order. Ifs
is greater thanorderHalf
, it must be adjusted. In scenarios wheres
is already lower, both transactionsigning
andpersonalSign
will give the same result.Sorry for confusing you :)
final signer = EcdsaSigningKey( ECDSAPrivateKey.fromBytes(privKeyBytes, Curves.generatorSecp256k1)); var ecdsaSign = signer.signDigestDeterminstic( digest: _sha256Hash(idDoc), hashFunc: () => SHA256()); if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); } final sigBytes = ecdsaSign.toBytes(ETHSignerConst.secp256.curve.baselen);
So what I wanted to say is that I needed to apply normalize logic from CosmosSigner to this code so I normalize s parameter to give me the correct output.
ok
CosmosSecp256K1PrivateKey.sign
It does exactly that! Are you sure that CosmosSecp256K1PrivateKey.sign gives you a different result? Please double-check.this the code of
CosmosSecp256K1PrivateKey.sign
final hash = hashMessage ? QuickCrypto.sha256Hash(digest) : digest; if (hash.length != ETHSignerConst.digestLength) { throw ArgumentException( "invalid digest. digest length must be ${ETHSignerConst.digestLength} got ${digest.length}"); } ECDSASignature ecdsaSign = _ecdsaSigningKey.signDigestDeterminstic( digest: hash, hashFunc: () => SHA256()); if (ecdsaSign.s > ETHSignerConst.orderHalf) { ecdsaSign = ECDSASignature(ecdsaSign.r, ETHSignerConst.curveOrder - ecdsaSign.s); } final sigBytes = ecdsaSign.toBytes(ETHSignerConst.secp256.curve.baselen); final verifyKey = toVerifyKey(); if (verifyKey.verify(hash, sigBytes)) { return ecdsaSign.toBytes(ETHSignerConst.digestLength); } throw const MessageException( 'The created signature does not pass verification.');
Just did a test again, CosmosSecp256K1PrivateKey.sign
gives me totally different result!
EDIT: I thought that I need to forward sha256 digest but it seems it is done under the hood! All good. Thanks!
Context:
I’m working on a Cosmos SDK project in Flutter/Dart, and I’m implementing RFC6979 deterministic ECDSA signing with the secp256k1 curve. While researching, I came across Decred’s implementation in signature.go, specifically the signRFC6979 method.
func signRFC6979(privKey *secp256k1.PrivateKey, hash []byte)
Is used to sign an iddoc 256 hash with private key. I am using your sign method from secp256k1 private key but it produces totally different output. What I can confirm is that private key and hash do match 100% before signing.
Goal:
I want to replicate the functionality of RFC6979 deterministic nonce generation and ECDSA signing in Dart, and I have a few specific questions around how Decred’s implementation works.
Questions: