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))
}
CodePudding user response:
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).