diff --git a/common.go b/common.go index debd8f7..eb41442 100644 --- a/common.go +++ b/common.go @@ -1,5 +1,5 @@ /******************************************************************************* -* (c) 2018 - 2022 ZondaX AG +* (c) 2018 - 2023 ZondaX AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,89 +18,83 @@ package ledger_cosmos_go import ( "encoding/binary" + "errors" "fmt" + "strconv" + "strings" ) -// VersionInfo contains app version information -type VersionInfo struct { - AppMode uint8 - Major uint8 - Minor uint8 - Patch uint8 +type VersionResponse struct { + AppMode uint8 // 0: release | 0xFF: debug + Major uint8 + Minor uint8 + Patch uint8 + AppLocked uint8 + TargetId uint32 } -func (c VersionInfo) String() string { - return fmt.Sprintf("%d.%d.%d", c.Major, c.Minor, c.Patch) -} - -// VersionRequiredError the command is not supported by this app -type VersionRequiredError struct { - Found VersionInfo - Required VersionInfo +type AddressResponse struct { + pubkey []byte // 0: release | 0xFF: debug + address string } -func (e VersionRequiredError) Error() string { - return fmt.Sprintf("App Version required %s - Version found: %s", e.Required, e.Found) +type SignatureResponse struct { + signatureDER []byte } -func NewVersionRequiredError(req VersionInfo, ver VersionInfo) error { - return &VersionRequiredError{ - Found: ver, - Required: req, - } +func (c VersionResponse) String() string { + return fmt.Sprintf("%d.%d.%d", c.Major, c.Minor, c.Patch) } -// CheckVersion compares the current version with the required version -func CheckVersion(ver VersionInfo, req VersionInfo) error { - if ver.Major != req.Major { - if ver.Major > req.Major { - return nil - } - return NewVersionRequiredError(req, ver) +// Validate HRP: Max length = 83 +// All characters must be in range [33, 126], displayable chars in Ledger devices +func serializeHRP(hrp string) (hrpBytes []byte, err error) { + if len(hrp) > 83 { + return nil, errors.New("HRP len should be <= 83") } - if ver.Minor != req.Minor { - if ver.Minor > req.Minor { - return nil + hrpBytes = []byte(hrp) + for _, b := range hrpBytes { + if b < 33 || b > 126 { + return nil, errors.New("all characters in the HRP must be in the [33, 126] range") } - return NewVersionRequiredError(req, ver) } - if ver.Patch >= req.Patch { - return nil - } - return NewVersionRequiredError(req, ver) + return hrpBytes, nil } -func GetBip32bytesv1(bip32Path []uint32, hardenCount int) ([]byte, error) { - message := make([]byte, 41) - if len(bip32Path) > 10 { - return nil, fmt.Errorf("maximum bip32 depth = 10") - } - message[0] = byte(len(bip32Path)) - for index, element := range bip32Path { - pos := 1 + index*4 - value := element - if index < hardenCount { - value = 0x80000000 | element - } - binary.LittleEndian.PutUint32(message[pos:], value) +func serializePath(path string) (pathBytes []byte, err error) { + const HARDENED = 0x80000000 + + if !strings.HasPrefix(path, "m/") { + return nil, errors.New(`path should start with "m/" (e.g "m/44'/118'/0'/0/3")`) } - return message, nil -} -func GetBip32bytesv2(bip44Path []uint32, hardenCount int) ([]byte, error) { - message := make([]byte, 20) - if len(bip44Path) != 5 { - return nil, fmt.Errorf("path should contain 5 elements") + pathArray := strings.Split(path, "/") + pathArray = pathArray[1:] // remove "m" + + if len(pathArray) != 5 { + return nil, errors.New("invalid path: it must contain 5 elements") } - for index, element := range bip44Path { - pos := index * 4 - value := element - if index < hardenCount { - value = 0x80000000 | element + + // Reserve 20 bytes for serialized path + buffer := make([]byte, 4*len(pathArray)) + + for i, child := range pathArray { + value := 0 + if strings.HasSuffix(child, "'") { + value += HARDENED + child = strings.TrimSuffix(child, "'") + } + numChild, err := strconv.Atoi(child) + if err != nil { + return nil, fmt.Errorf("invalid path : %s is not a number (e.g \"m/44'/118'/0'/0/3\")", child) + } + if numChild >= HARDENED { + return nil, errors.New("incorrect child value (bigger or equal to 0x80000000)") } - binary.LittleEndian.PutUint32(message[pos:], value) + value += numChild + binary.LittleEndian.PutUint32(buffer[i*4:], uint32(value)) } - return message, nil + return buffer, nil } diff --git a/common_test.go b/common_test.go index 9ab487d..0bcf93d 100644 --- a/common_test.go +++ b/common_test.go @@ -1,5 +1,5 @@ /******************************************************************************* -* (c) 2018 - 2022 Zondax AG +* (c) 2018 - 2023 Zondax AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,20 @@ package ledger_cosmos_go import ( "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func Test_PrintVersion(t *testing.T) { - reqVersion := VersionInfo{0, 1, 2, 3} + reqVersion := VersionResponse{0, 1, 2, 3, 0, 0x12345678} s := fmt.Sprintf("%v", reqVersion) assert.Equal(t, "1.2.3", s) } -func Test_PathGeneration0(t *testing.T) { - bip32Path := []uint32{44, 100, 0, 0, 0} - - pathBytes, err := GetBip32bytesv1(bip32Path, 0) +func Test_SerializePath0(t *testing.T) { + path := "m/44'/100'/0/0/0" + pathBytes, err := serializePath(path) if err != nil { t.Fatalf("Detected error, err: %s\n", err.Error()) @@ -41,21 +41,19 @@ func Test_PathGeneration0(t *testing.T) { assert.Equal( t, - 41, + 20, len(pathBytes), - "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 41) + "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 20) assert.Equal( t, - "052c000000640000000000000000000000000000000000000000000000000000000000000000000000", + "2c00008064000080000000000000000000000000", fmt.Sprintf("%x", pathBytes), "Unexpected PathBytes\n") } - -func Test_PathGeneration2(t *testing.T) { - bip32Path := []uint32{44, 118, 0, 0, 0} - - pathBytes, err := GetBip32bytesv1(bip32Path, 2) +func Test_SerializePath1(t *testing.T) { + path := "m/44'/118'/0'/0/0" + pathBytes, err := serializePath(path) if err != nil { t.Fatalf("Detected error, err: %s\n", err.Error()) @@ -65,21 +63,20 @@ func Test_PathGeneration2(t *testing.T) { assert.Equal( t, - 41, + 20, len(pathBytes), - "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 41) + "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 20) assert.Equal( t, - "052c000080760000800000000000000000000000000000000000000000000000000000000000000000", + "2c00008076000080000000800000000000000000", fmt.Sprintf("%x", pathBytes), "Unexpected PathBytes\n") } -func Test_PathGeneration3(t *testing.T) { - bip32Path := []uint32{44, 118, 0, 0, 0} - - pathBytes, err := GetBip32bytesv1(bip32Path, 3) +func Test_SerializePath2(t *testing.T) { + path := "m/44'/60'/0'/0/0" + pathBytes, err := serializePath(path) if err != nil { t.Fatalf("Detected error, err: %s\n", err.Error()) @@ -89,85 +86,59 @@ func Test_PathGeneration3(t *testing.T) { assert.Equal( t, - 41, + 20, len(pathBytes), - "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 41) + "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 20) assert.Equal( t, - "052c000080760000800000008000000000000000000000000000000000000000000000000000000000", + "2c0000803c000080000000800000000000000000", fmt.Sprintf("%x", pathBytes), "Unexpected PathBytes\n") } -func Test_PathGeneration0v2(t *testing.T) { - bip32Path := []uint32{44, 100, 0, 0, 0} - - pathBytes, err := GetBip32bytesv2(bip32Path, 0) +func Test_SerializeHRP0(t *testing.T) { + hrp := "cosmos" + hrpBytes, err := serializeHRP(hrp) if err != nil { t.Fatalf("Detected error, err: %s\n", err.Error()) } - fmt.Printf("Path: %x\n", pathBytes) + fmt.Printf("HRP: %x\n", hrpBytes) assert.Equal( t, - 40, - len(pathBytes), - "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 40) + 6, + len(hrpBytes), + "hrpBytes has wrong length: %x, expected length: %x\n", hrpBytes, 6) assert.Equal( t, - "2c000000640000000000000000000000000000000000000000000000000000000000000000000000", - fmt.Sprintf("%x", pathBytes), - "Unexpected PathBytes\n") + "636f736d6f73", + fmt.Sprintf("%x", hrpBytes), + "Unexpected HRPBytes\n") } -func Test_PathGeneration2v2(t *testing.T) { - bip32Path := []uint32{44, 118, 0, 0, 0} - - pathBytes, err := GetBip32bytesv2(bip32Path, 2) +func Test_SerializeHRP1(t *testing.T) { + hrp := "evmos" + hrpBytes, err := serializeHRP(hrp) if err != nil { t.Fatalf("Detected error, err: %s\n", err.Error()) } - fmt.Printf("Path: %x\n", pathBytes) + fmt.Printf("HRP: %x\n", hrpBytes) assert.Equal( t, - 40, - len(pathBytes), - "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 40) + 5, + len(hrpBytes), + "hrpBytes has wrong length: %x, expected length: %x\n", hrpBytes, 5) assert.Equal( t, - "2c000080760000800000000000000000000000000000000000000000000000000000000000000000", - fmt.Sprintf("%x", pathBytes), - "Unexpected PathBytes\n") -} - -func Test_PathGeneration3v2(t *testing.T) { - bip32Path := []uint32{44, 118, 0, 0, 0} - - pathBytes, err := GetBip32bytesv2(bip32Path, 3) - - if err != nil { - t.Fatalf("Detected error, err: %s\n", err.Error()) - } - - fmt.Printf("Path: %x\n", pathBytes) - - assert.Equal( - t, - 40, - len(pathBytes), - "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 40) - - assert.Equal( - t, - "2c000080760000800000008000000000000000000000000000000000000000000000000000000000", - fmt.Sprintf("%x", pathBytes), - "Unexpected PathBytes\n") + "65766d6f73", + fmt.Sprintf("%x", hrpBytes), + "Unexpected HRPBytes\n") } diff --git a/ledger_cosmos.go b/ledger_cosmos.go new file mode 100644 index 0000000..c5e8c68 --- /dev/null +++ b/ledger_cosmos.go @@ -0,0 +1,268 @@ +/******************************************************************************* +* (c) 2018 - 2023 ZondaX AG +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ + +package ledger_cosmos_go + +import ( + "errors" + "fmt" + "math" + + ledger_go "github.com/zondax/ledger-go" +) + +type TxMode byte + +const ( + SignModeAmino TxMode = 0 + SignModeTextual TxMode = 1 + SignModeUnknown TxMode = 2 +) + +const ( + CLA = 0x55 + + INSGetVersion = 0 + INSSign = 2 + INSGetAddressAndPubKey = 4 + + CHUNKSIZE = 250 +) + +type LedgerCosmos struct { + api ledger_go.LedgerDevice + version VersionResponse +} + +// FindLedger finds a Cosmos user app running in a ledger device +func FindLedger() (_ *LedgerCosmos, rerr error) { + ledgerAdmin := ledger_go.NewLedgerAdmin() + ledgerAPI, err := ledgerAdmin.Connect(0) + if err != nil { + return nil, err + } + + defer func() { + if rerr != nil { + ledgerAPI.Close() + } + }() + + app := &LedgerCosmos{ledgerAPI, VersionResponse{}} + _, err = app.GetVersion() + if err != nil { + if err.Error() == "[APDU_CODE_CLA_NOT_SUPPORTED] Class not supported" { + err = errors.New("are you sure the Cosmos app is open?") + } + return nil, err + } + + return app, nil +} + +func (ledger *LedgerCosmos) Close() error { + return ledger.api.Close() +} + +// GetVersion returns the current version of the Ledger Cosmos app +func (ledger *LedgerCosmos) GetVersion() (*VersionResponse, error) { + message := []byte{CLA, INSGetVersion, 0, 0, 0} + response, err := ledger.api.Exchange(message) + + if err != nil { + return nil, err + } + + if len(response) != 9 { + return nil, errors.New("invalid response") + } + + ledger.version = VersionResponse{ + AppMode: response[0], + Major: response[1], + Minor: response[2], + Patch: response[3], + AppLocked: response[4], // SDK won't reply any APDU message if screensaver is active (always false) + TargetId: uint32(response[5]), + } + + return &ledger.version, nil +} + +// GetAddressAndPubKey send INSGetAddressAndPubKey APDU command +// Optional parameter to display the information on the device first + +// Response: +// | Field | Type | Content | +// | ------- | --------- | --------------------- | +// | PK | byte (33) | Compressed Public Key | +// | ADDR | byte (65) | Bech 32 addr | +// | SW1-SW2 | byte (2) | Return code | + +// Devolver Struct + err + +func (ledger *LedgerCosmos) GetAddressAndPubKey(path string, hrp string, requireConfirmation bool) (addressResponse AddressResponse, err error) { + + response := AddressResponse{ + pubkey: nil, // Compressed pubkey + address: "", + } + + // Serialize HRP + hrpBytes, err := serializeHRP(hrp) + if err != nil { + return response, err + } + + // Serialize Path + pathBytes, err := serializePath(path) + if err != nil { + return response, err + } + + p1 := byte(0) + if requireConfirmation { + p1 = byte(1) + } + + // Prepare message + // [header | hrpLen | hrp | hdpath] + header := []byte{CLA, INSGetAddressAndPubKey, p1, 0, 0} + message := append(header, byte(len(hrpBytes))) + message = append(message, hrpBytes...) + message = append(message, pathBytes...) + message[4] = byte(len(message) - len(header)) // Update payload length + + cmdResponse, err := ledger.api.Exchange(message) + + if err != nil { + return response, err + } + + // The command response must have 33 bytes from pubkey + // the HRP and the rest of the address + if 33+len(hrp) > len(cmdResponse) { + return response, errors.New("invalid response length") + } + + // Build response + response.pubkey = cmdResponse[0:33] // Compressed pubkey + response.address = string(cmdResponse[33:]) + + return response, nil +} + +func processFirstChunk(path string, hrp string, txMode TxMode) (message []byte, err error) { + // Serialize hrp + hrpBytes, err := serializeHRP(hrp) + if err != nil { + return nil, err + } + + // Serialize Path + pathBytes, err := serializePath(path) + if err != nil { + return nil, err + } + + // [header | path | hrpLen | hrp] + header := []byte{CLA, INSSign, 0, byte(txMode), 0} + message = append(header, pathBytes...) + message = append(message, byte(len(hrpBytes))) + message = append(message, hrpBytes...) + message[4] = byte(len(message) - len(header)) + + return message, nil +} + +func processErrorResponse(response []byte, responseErr error) (err error) { + // Check if we can get the error code and improve these messages + if responseErr.Error() == "[APDU_CODE_BAD_KEY_HANDLE] The parameters in the data field are incorrect" { + // In this special case, we can extract additional info + errorMsg := string(response) + switch errorMsg { + case "ERROR: JSMN_ERROR_NOMEM": + return errors.New("not enough tokens were provided") + case "PARSER ERROR: JSMN_ERROR_INVAL": + return errors.New("unexpected character in JSON string") + case "PARSER ERROR: JSMN_ERROR_PART": + return errors.New("the JSON string is not a complete") + } + return errors.New(errorMsg) + } + if responseErr.Error() == "[APDU_CODE_DATA_INVALID] Referenced data reversibly blocked (invalidated)" { + errorMsg := string(response) + return errors.New(errorMsg) + } + return responseErr +} + +func (ledger *LedgerCosmos) sign(path string, hrp string, txMode TxMode, transaction []byte) (signatureResponse SignatureResponse, err error) { + var packetCount = byte(math.Ceil(float64(len(transaction)) / float64(CHUNKSIZE))) + var message []byte + + signatureResponse = SignatureResponse{ + signatureDER: nil, + } + + if txMode >= SignModeUnknown { + return signatureResponse, errors.New("at the moment the Ledger app only works with Amino (0) and Textual(1) modes") + } + + // First chunk only contains path & HRP + message, err = processFirstChunk(path, hrp, txMode) + if err != nil { + return signatureResponse, err + } + + _, err = ledger.api.Exchange(message) + if err != nil { + return signatureResponse, err + } + + // Split the transaction in chunks + for packetIndex := byte(1); packetIndex <= packetCount; packetIndex++ { + chunk := CHUNKSIZE + if len(transaction) < CHUNKSIZE { + chunk = len(transaction) + } + + // p1 can have 3 different values: + // p1 = 0 INIT (first chunk) + // p1 = 1 ADD from chunk 1 up to packetCount - 1 + // p1 = 2 LAST indicates to the app that is the last chunk + p1 := byte(1) + if packetIndex == packetCount { + p1 = byte(2) + } + + header := []byte{CLA, INSSign, p1, byte(txMode), byte(chunk)} + message = append(header, transaction[:chunk]...) + + apduResponse, err := ledger.api.Exchange(message) + if err != nil { + fmt.Printf("ERROR ON CHUNK %d\n", packetIndex) + return signatureResponse, processErrorResponse(apduResponse, err) + } + + // Trim sent bytes + transaction = transaction[chunk:] + signatureResponse.signatureDER = apduResponse + } + + // Ledger app returns the signature in DER format + return signatureResponse, nil +} diff --git a/ledger_cosmos_test.go b/ledger_cosmos_test.go new file mode 100644 index 0000000..0683f1c --- /dev/null +++ b/ledger_cosmos_test.go @@ -0,0 +1,210 @@ +/******************************************************************************* +* (c) 2018 - 2023 ZondaX AG +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ + +package ledger_cosmos_go + +import ( + "crypto/sha256" + "fmt" + "strings" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func getDummyTx() []byte { + dummyTx := `{ + "account_number": 1, + "chain_id": "some_chain", + "fee": { + "amount": [{"amount": 10, "denom": "DEN"}], + "gas": 5 + }, + "memo": "MEMO", + "msgs": ["SOMETHING"], + "sequence": 3 + }` + dummyTx = strings.Replace(dummyTx, " ", "", -1) + dummyTx = strings.Replace(dummyTx, "\n", "", -1) + dummyTx = strings.Replace(dummyTx, "\t", "", -1) + + return []byte(dummyTx) +} + +// Ledger Test Mnemonic: equip will roof matter pink blind book anxiety banner elbow sun young + +func Test_UserFindLedger(t *testing.T) { + CosmosApp, err := FindLedger() + if err != nil { + t.Fatalf(err.Error()) + } + + assert.NotNil(t, CosmosApp) + defer CosmosApp.Close() +} + +func Test_UserGetVersion(t *testing.T) { + CosmosApp, err := FindLedger() + if err != nil { + t.Fatalf(err.Error()) + } + defer CosmosApp.Close() + + version, err := CosmosApp.GetVersion() + require.Nil(t, err, "Detected error") + fmt.Println("Current Cosmos app version: ", version) + + // Rework at v2.34.12 ---> Minimum required version + assert.GreaterOrEqual(t, uint8(2), version.Major) + if version.Major == 2 { + assert.GreaterOrEqual(t, uint8(34), version.Minor) + if version.Minor == 34 { + assert.GreaterOrEqual(t, uint8(12), version.Patch) + } + } +} + +// From v2.34.12 onwards, is possible to sign transactions using Ethereum derivation path (60) +// Verify addresses for Cosmos and Ethereum paths +// Ethereum path can be used only for a list of allowed HRP +// Check list here: +// https://github.com/cosmos/ledger-cosmos/blob/697dbd7e28cbfc8caa78d4c3bbc6febdaf6ae618/app/src/chain_config.c#L26-L30 +func Test_GetAddressAndPubkey(t *testing.T) { + CosmosApp, err := FindLedger() + if err != nil { + t.Fatalf(err.Error()) + } + defer CosmosApp.Close() + + // Test with Cosmos path + hrp := "cosmos" + path := "m/44'/118'/0'/0/3" + + addressResponse, err := CosmosApp.GetAddressAndPubKey(path, hrp, false) + if err != nil { + t.Fatalf("Detected error, err: %s\n", err.Error()) + } + + assert.Equal(t, 33, len(addressResponse.pubkey), + "Public key has wrong length: %x, expected length: %x\n", len(addressResponse.pubkey), 33) + fmt.Printf("PUBLIC KEY: %x\n", addressResponse.pubkey) + fmt.Printf("ADDRESS: %s\n", addressResponse.address) + + // assert.Equal(t, + // "03cb5a33c61595206294140c45efa8a817533e31aa05ea18343033a0732a677005", + // hex.EncodeToString(addressResponse.pubkey), + // "Unexpected pubkey") + + // Test with Ethereum path --> Enable expert mode + hrp = "inj" + path = "m/44'/60'/0'/0/1" + + addressResponse, err = CosmosApp.GetAddressAndPubKey(path, hrp, false) + if err != nil { + t.Fatalf("Detected error, err: %s\n", err.Error()) + } + + assert.Equal(t, 33, len(addressResponse.pubkey), + "Public key has wrong length: %x, expected length: %x\n", len(addressResponse.pubkey), 33) + fmt.Printf("PUBLIC KEY: %x\n", addressResponse.pubkey) + fmt.Printf("ADDRESS: %s\n", addressResponse.address) + + // assert.Equal(t, + // "03cb5a33c61595206294140c45efa8a817533e31aa05ea18343033a0732a677005", + // hex.EncodeToString(addressResponse.pubkey), + // "Unexpected pubkey") + + // // Take the compressed pubkey and verify that the expected address can be computed + // const uncompressPubKeyUint8Array = secp256k1.publicKeyConvert(resp.compressed_pk, false).subarray(1); + // const ethereumAddressBuffer = Buffer.from(keccak(Buffer.from(uncompressPubKeyUint8Array))).subarray(-20); + // const eth_address = bech32.encode(hrp, bech32.toWords(ethereumAddressBuffer)); // "cosmos15n2h0lzvfgc8x4fm6fdya89n78x6ee2fm7fxr3" + + // expect(resp.bech32_address).toEqual(eth_address) + // expect(resp.bech32_address).toEqual('inj15n2h0lzvfgc8x4fm6fdya89n78x6ee2f3h7z3f') +} + +func Test_UserSign(t *testing.T) { + CosmosApp, err := FindLedger() + if err != nil { + t.Fatalf(err.Error()) + } + defer CosmosApp.Close() + + hrp := "cosmos" + path := "m/44'/118'/0'/0/0" + + message := getDummyTx() + signatureResponse, err := CosmosApp.sign(path, hrp, SignModeAmino, message) + if err != nil { + t.Fatalf("[Sign] Error: %s\n", err.Error()) + } + + // Verify Signature + responseAddress, err := CosmosApp.GetAddressAndPubKey(path, hrp, false) + if err != nil { + t.Fatalf("Detected error, err: %s\n", err.Error()) + } + + if err != nil { + t.Fatalf("[GetPK] Error: " + err.Error()) + return + } + + pub2, err := btcec.ParsePubKey(responseAddress.pubkey) + if err != nil { + t.Fatalf("[ParsePK] Error: " + err.Error()) + return + } + + sig2, err := ecdsa.ParseDERSignature(signatureResponse.signatureDER) + if err != nil { + t.Fatalf("[ParseSig] Error: " + err.Error()) + return + } + + hash := sha256.Sum256(message) + verified := sig2.Verify(hash[:], pub2) + if !verified { + t.Fatalf("[VerifySig] Error verifying signature: " + err.Error()) + return + } +} + +func Test_UserSignFails(t *testing.T) { + CosmosApp, err := FindLedger() + if err != nil { + t.Fatalf(err.Error()) + } + defer CosmosApp.Close() + + hrp := "cosmos" + path := "m/44'/118'/0'/0/0" + + message := getDummyTx() + garbage := []byte{65} + message = append(garbage, message...) + + _, err = CosmosApp.sign(path, hrp, SignModeAmino, message) + assert.Error(t, err) + errMessage := err.Error() + + if errMessage != "Invalid character in JSON string" && errMessage != "Unexpected characters" { + assert.Fail(t, "Unexpected error message returned: "+errMessage) + } +}