mrtnetwork / cosmos_sdk

Experience effortless interaction with a Cosmos SDK-based network in Dart, facilitating the seamless creation, signing, and transmission of transactions. (Beta version)
BSD 3-Clause "New" or "Revised" License
6 stars 5 forks source link

RFC6979 Usage for ECDSA Signing with secp256k1 #4

Open stefandric opened 1 month ago

stefandric commented 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.
mrtnetwork commented 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);
stefandric commented 1 month ago

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

mrtnetwork commented 1 month ago

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.

stefandric commented 1 month ago

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?

mrtnetwork commented 1 month ago

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.

stefandric commented 1 month ago

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.

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.

mrtnetwork commented 1 month ago

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.

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.');
stefandric commented 1 month ago

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.

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!