how to sign a message with ecdsa privatekey using golang?

Issue

I am trying to sign a message in go generated via hd wallet’s private key using cosmos sdk. Below is the equivalent implementation in python which generates the signed message / signature as expected when submitted/verified is working properly but unable to get it working wtih Go implementation. Any inputs for equivalent golang version of the python implementation is much appreciated. Thank you.

Python version uses sha256 , ecdsa but when using the equivalent cyrpto/ecdsa doesn’t return valid signature.

Python

    def test_sign_message(self):
        """ Tests the ability of the signer to sing message """
      
        # Loading up the signer object to use for the operation
        signer: TestSigners = TestSigners.from_mnemonic("blast about old claw current first paste risk involve victory edit current")
        sample_payload_to_sign = "75628d14409a5126e6c882d05422c06f5eccaa192c082a9a5695a8e707109842'
        # print("test".encode("UTF-8").hex())
        s = signer.sign(sample_payload_to_sign)
        print(s)


from typing import List, Tuple, Dict, Union, Any
from hdwallet.hdwallet import HDWallet
from ecdsa.util import sigencode_der
from ecdsa.curves import SECP256k1
from ecdsa.keys import SigningKey
import mnemonic
import hashlib
import ecdsa


class TestSigners():

    HD_WALLET_PARAMS: Dict[str, Tuple[int, bool]] = {
        "purpose": (44, True),
        "coinType": (1022, True),
        "account": (0, True),
        "change": (0, False),
    }

    def __init__(
            self,
            seed: Union[bytes, bytearray, str]
    ) -> None:
        """ Instantiates a new signer object from the seed phrase

        Args:
            seed (Union[bytes, bytearray, str]): The seed phrase used to generate the public and
                private keys.
        """

        self.seed: Union[bytes, bytearray] = seed if isinstance(seed, (bytes, bytearray)) else bytearray.fromhex(seed)

    @classmethod
    def from_mnemonic(
            cls,
            mnemonic_phrase: Union[str, List[str], Tuple[str]]
    ) -> 'Signer':
        """
        Instantiates a new Signer object from the mnemonic phrase passed.

        Args:
            mnemonic_phrase (Union[str, :obj:`list` of :obj:`str`, :obj:`tuple` of :obj:`str`):
                A string, list, or a tuple of the mnemonic phrase. If the argument is passed as an
                iterable, then it will be joined with a space.

        Returns:
            Signer: A new signer initalized through the mnemonic phrase.
        """

        # If the supplied mnemonic phrase is a list then convert it to a string
        if isinstance(mnemonic_phrase, (list, tuple)):
            mnemonic_string: str = " ".join(mnemonic_phrase)
        else:
            mnemonic_string: str = mnemonic_phrase

        mnemonic_string: str = " ".join(mnemonic_phrase) if isinstance(mnemonic_phrase,
                                                                       (list, tuple)) else mnemonic_phrase

        return cls(mnemonic.Mnemonic.to_seed(mnemonic_string))

    def public_key(
            self,
            index: int = 0
    ) -> str:
        """
        Gets the public key for the signer for the specified account index

        Args:
            index (int): The account index to get the public keys for.

        Returns:
            str: A string of the public key for the wallet
        """

        return str(self.hdwallet(index).public_key())

    def private_key(
            self,
            index: int = 0
    ) -> str:
        """
        Gets the private key for the signer for the specified account index

        Args:
            index (int): The account index to get the private keys for.

        Returns:
            str: A string of the private key for the wallet
        """

        return str(self.hdwallet(index).private_key())

    def hdwallet(
            self,
            index: int = 0
    ) -> HDWallet:
        """
        Creates an HDWallet object suitable for the Radix blockchain with the passed account index.

        Args:
            index (int): The account index to create the HDWallet object for.

        Returns:
            HDWallet: An HD wallet object created with the Radix Parameters for a given account
                index.
        """

        hdwallet: HDWallet = HDWallet()
        hdwallet.from_seed(seed=self.seed.hex())
        for _, values_tuple in self.HD_WALLET_PARAMS.items():
            value, hardened = values_tuple
            hdwallet.from_index(value, hardened=hardened)
        hdwallet.from_index(index, True)

        return hdwallet

    def sign(
            self,
            data: str,
            index: int = 0
    ) -> str:
        """
        Signs the given data using the private keys for the account at the specified account index.

        Arguments:
            data (str): A string of the data which we wish to sign.
            index (int): The account index to get the private keys for.

        Returns:
            str: A string of the signed data
        """

        signing_key: SigningKey = ecdsa.SigningKey.from_string(  # type: ignore
            string=bytearray.fromhex(self.private_key(index)),
            curve=SECP256k1,
            hashfunc=hashlib.sha256
        )

        return signing_key.sign_digest(  # type: ignore
            digest=bytearray.fromhex(data),
            sigencode=sigencode_der
        ).hex()

GO ( Not Working )

package main

import (
    "encoding/hex"
    "fmt"
    "log"

    "github.com/cosmos/cosmos-sdk/crypto/hd"
    "github.com/cosmos/go-bip39"
    "github.com/decred/dcrd/bech32"
    "github.com/tendermint/tendermint/crypto/secp256k1"
)

func main() {

   seed := bip39.NewSeed("blast about old claw current first paste risk involve victory edit current", "")
    fmt.Println("Seed: ", hex.EncodeToString(seed)) // Seed:  dd5ffa7088c0fa4c665085bca7096a61e42ba92e7243a8ad7fbc6975a4aeea1845c6b668ebacd024fd2ca215c6cd510be7a9815528016af3a5e6f47d1cca30dd

    master, ch := hd.ComputeMastersFromSeed(seed)
    path := "m/44'/1022'/0'/0/0'"
    priv, err := hd.DerivePrivateKeyForPath(master, ch, path)
    if err != nil {
        t.Fatal(err)
    }
    fmt.Println("Derivation Path: ", path)                 // Derivation Path:  m/44'/118'/0'/0/0'
    fmt.Println("Private Key: ", hex.EncodeToString(priv)) // Private Key:  69668f2378b43009b16b5c6eb5e405d9224ca2a326a65a17919e567105fa4e5a

    var privKey = secp256k1.PrivKey(priv)
    pubKey := privKey.PubKey()
    fmt.Println("Public Key: ", hex.EncodeToString(pubKey.Bytes())) // Public Key:  03de79435cbc8a799efc24cdce7d3b180fb014d5f19949fb8d61de3f21b9f6c1f8

    //str := "test"
    str := "75628d14409a5126e6c882d05422c06f5eccaa192c082a9a5695a8e707109842"
    //hx := hex.EncodeToString([]byte(str))
    //fmt.Println(hx)
    sign, err := privKey.Sign([]byte(str))
    if err != nil {
        return
    }

    fmt.Println(hex.EncodeToString(sign))
}

Solution

Both codes return hex encoded as private key

33f34dad4bc0ce9dc320863509aed43cab33a93a29752779ae0df6dbbea33e56

and as compressed public key

026557fe37d5cab1cc8edf474f4baff67dbb2305f1764e42d31b09f83296f5de2b

Since both codes provide the same keys, the issue must be with signing!


As test message for signing the UTF8 encoding of test is used, whose SHA256 hash is hex encoded 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.

Remark 1: If a double SHA256 hashing is used as noted in the comment, the SHA256 hash of test is to be used as test message instead of test. Apart from that the further processing is the same.

The Python and Go code are currently incompatible because they differ in signing and signature format:

  • With regard to signing: In the Python code, the hashed message is passed. This is correct because sign_digest() does not hash the message (see here), and thus the hashed message is signed.
    In contrast, sign() in the Go code hashes the message (see here), so the message itself must be passed for the processing to be functionally identical to the Python code.

  • With regard to the signature format: the Python code uses the ASN.1/DER format, while the Go code uses the IEEE P1363 format.
    Therefore, a conversion from IEEE P1363 to ASN.1/DER must be performed in the Go code:

With this, the fixed Go code is:

package main

import (
    "encoding/hex"
    "fmt"

    "math/big"

    "github.com/cosmos/cosmos-sdk/crypto/hd"
    "github.com/cosmos/go-bip39"
    "github.com/tendermint/tendermint/crypto/secp256k1"

    //"github.com/btcsuite/btcd/btcec"
    "golang.org/x/crypto/cryptobyte"
    "golang.org/x/crypto/cryptobyte/asn1"
)

func main() {

    //
    // Derive private and public key (this part works)
    //
    seed := bip39.NewSeed("blast about old claw current first paste risk involve victory edit current", "")
    fmt.Println("Seed: ", hex.EncodeToString(seed)) // Seed:  dd5ffa7088c0fa4c665085bca7096a61e42ba92e7243a8ad7fbc6975a4aeea1845c6b668ebacd024fd2ca215c6cd510be7a9815528016af3a5e6f47d1cca30dd

    master, ch := hd.ComputeMastersFromSeed(seed)
    path := "m/44'/1022'/0'/0/0'"
    priv, _ := hd.DerivePrivateKeyForPath(master, ch, path)
    fmt.Println("Derivation Path: ", path)                 // Derivation Path:  m/44'/1022'/0'/0/0'
    fmt.Println("Private Key: ", hex.EncodeToString(priv)) // Private Key:  33f34dad4bc0ce9dc320863509aed43cab33a93a29752779ae0df6dbbea33e56

    var privKey = secp256k1.PrivKey(priv)
    pubKey := privKey.PubKey()
    fmt.Println("Public Key: ", hex.EncodeToString(pubKey.Bytes())) // Public Key:  026557fe37d5cab1cc8edf474f4baff67dbb2305f1764e42d31b09f83296f5de2b

    //
    // Sign (this part needs to be fixed)
    //
    data := "test"

    signature, _ := privKey.Sign([]byte(data))
    fmt.Println(hex.EncodeToString(signature))

    rVal := new(big.Int)
    rVal.SetBytes(signature[0:32])
    sVal := new(big.Int)
    sVal.SetBytes(signature[32:64])
    var b cryptobyte.Builder
    b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
        b.AddASN1BigInt(rVal)
        b.AddASN1BigInt(sVal)
    })
    signatureDER, _ := b.Bytes()
    fmt.Println("Signature, DER: ", hex.EncodeToString(signatureDER))

    /*
        hash, _ := hex.DecodeString("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")

        // Sign without hashing
        privateKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), priv)
        signature, _ := privateKey.Sign(hash[:])

        // Convert to ASN1/DER
        rVal := new(big.Int)
        rVal.SetBytes(signature.R.Bytes())
        sVal := new(big.Int)
        sVal.SetBytes(signature.S.Bytes())
        var b cryptobyte.Builder
        b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
            b.AddASN1BigInt(rVal)
            b.AddASN1BigInt(sVal)
        })
        signatureDER, _ := b.Bytes()
        fmt.Println("Signature, DER: ", hex.EncodeToString(signatureDER))
    */
}

Remark 2: If the original message is not available in the Go code, but only the hash, a function that does not hash is needed for signing.
The tendermint/crypto/secp256k1 package does not support this, but tendermint/crypto/secp256k1 uses internally btcsuite/btcd/btcec which does.
This is implemented in the commented-out code.

The output is:

Seed:  dd5ffa7088c0fa4c665085bca7096a61e42ba92e7243a8ad7fbc6975a4aeea1845c6b668ebacd024fd2ca215c6cd510be7a9815528016af3a5e6f47d1cca30dd
Derivation Path:  m/44'/1022'/0'/0/0'
Private Key:  33f34dad4bc0ce9dc320863509aed43cab33a93a29752779ae0df6dbbea33e56
Public Key:  026557fe37d5cab1cc8edf474f4baff67dbb2305f1764e42d31b09f83296f5de2b
57624717f71fae8b5917cde0f82dfe6c2e2104183ba01c6a1c9f0a8e66d3303e5035b52876d833522aace232c1d231b3aeeff303cf02d1677a240102365ce71b
Signature, DER:  3044022057624717f71fae8b5917cde0f82dfe6c2e2104183ba01c6a1c9f0a8e66d3303e02205035b52876d833522aace232c1d231b3aeeff303cf02d1677a240102365ce71b

Test:

Since the Python code generates a non-deterministic signature, verification by comparing the signatures is not possible.
Instead, a possible test is to check the signatures of both codes with the same verification code.
For this purpose, in the method sign() of the Python code the lines

return signing_key.sign_digest(  # type: ignore
    digest=bytearray.fromhex(data),
    sigencode=sigencode_der
).hex()

can be replaced by

from ecdsa.util import sigdecode_der 
signature = signing_key.sign_digest(  # from Python Code
    digest=bytearray.fromhex(data),
    sigencode=sigencode_der
)
#signature = bytes.fromhex('3044022057624717f71fae8b5917cde0f82dfe6c2e2104183ba01c6a1c9f0a8e66d3303e02205035b52876d833522aace232c1d231b3aeeff303cf02d1677a240102365ce71b') # from Go code    
verifying_key = signing_key.verifying_key
verified = verifying_key.verify_digest(signature, digest=bytearray.fromhex(data), sigdecode=sigdecode_der)
print(verified)
return signature.hex()

The test shows that both, the Python and Go code signatures are successfully verified, proving that the signature generated with the Go code is valid.


Remark 3: The Python code generates a non-deterministic signature, i.e. the signature is different even with identical input data.
In contrast, the Go code generates a deterministic signature, i.e. the signature is the same for identical input data (see here).

If the Go code should also generate a non-deterministic signature, other libraries must be used on the Go side (but this might not actually be necessary, since the non-deterministic and the deterministic variant are established algorithms and generate valid signatures in accordance with the above test).

Answered By – Topaco

Answer Checked By – Gilberto Lyons (GoLangFix Admin)

Leave a Reply

Your email address will not be published.