From 1efb3010523e02531db5cfc1d98c6a97629b0215 Mon Sep 17 00:00:00 2001 From: ftheirs Date: Wed, 20 Dec 2023 11:30:56 -0300 Subject: [PATCH 1/3] remove old files --- user_app.go | 336 ------------------------------------------ user_app_test.go | 266 --------------------------------- validator_app.go | 178 ---------------------- validator_app_test.go | 64 -------- 4 files changed, 844 deletions(-) delete mode 100644 user_app.go delete mode 100644 user_app_test.go delete mode 100644 validator_app.go delete mode 100644 validator_app_test.go diff --git a/user_app.go b/user_app.go deleted file mode 100644 index e2beec9..0000000 --- a/user_app.go +++ /dev/null @@ -1,336 +0,0 @@ -/******************************************************************************* -* (c) 2018 - 2022 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" -) - -const ( - userCLA = 0x55 - - userINSGetVersion = 0 - userINSSignSECP256K1 = 2 - userINSGetAddrSecp256k1 = 4 - - userMessageChunkSize = 250 -) - -// LedgerCosmos represents a connection to the Cosmos app in a Ledger Nano S device -type LedgerCosmos struct { - api ledger_go.LedgerDevice - version VersionInfo -} - -// FindLedgerCosmosUserApp finds a Cosmos user app running in a ledger device -func FindLedgerCosmosUserApp() (_ *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, VersionInfo{}} - appVersion, 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 - } - - if err := app.CheckVersion(*appVersion); err != nil { - return nil, err - } - - return app, nil -} - -// Close closes a connection with the Cosmos user app -func (ledger *LedgerCosmos) Close() error { - return ledger.api.Close() -} - -// VersionIsSupported returns true if the App version is supported by this library -func (ledger *LedgerCosmos) CheckVersion(ver VersionInfo) error { - version, err := ledger.GetVersion() - if err != nil { - return err - } - - switch major := version.Major; major { - case 1: - return CheckVersion(ver, VersionInfo{0, 1, 5, 1}) - case 2: - return CheckVersion(ver, VersionInfo{0, 2, 1, 0}) - default: - return fmt.Errorf("App version %d is not supported", major) - } -} - -// GetVersion returns the current version of the Cosmos user app -func (ledger *LedgerCosmos) GetVersion() (*VersionInfo, error) { - message := []byte{userCLA, userINSGetVersion, 0, 0, 0} - response, err := ledger.api.Exchange(message) - - if err != nil { - return nil, err - } - - if len(response) < 4 { - return nil, errors.New("invalid response") - } - - ledger.version = VersionInfo{ - AppMode: response[0], - Major: response[1], - Minor: response[2], - Patch: response[3], - } - - return &ledger.version, nil -} - -// SignSECP256K1 signs a transaction using Cosmos user app. It can either use -// SIGN_MODE_LEGACY_AMINO_JSON (P2=0) or SIGN_MODE_TEXTUAL (P2=1). -// this command requires user confirmation in the device -func (ledger *LedgerCosmos) SignSECP256K1(bip32Path []uint32, transaction []byte, p2 byte) ([]byte, error) { - switch major := ledger.version.Major; major { - case 1: - return ledger.signv1(bip32Path, transaction) - case 2: - return ledger.signv2(bip32Path, transaction, p2) - default: - return nil, fmt.Errorf("App version %d is not supported", major) - } -} - -// GetPublicKeySECP256K1 retrieves the public key for the corresponding bip32 derivation path (compressed) -// this command DOES NOT require user confirmation in the device -func (ledger *LedgerCosmos) GetPublicKeySECP256K1(bip32Path []uint32) ([]byte, error) { - pubkey, _, err := ledger.getAddressPubKeySECP256K1(bip32Path, "cosmos", false) - return pubkey, err -} - -func validHRPByte(b byte) bool { - // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki - return b >= 33 && b <= 126 -} - -// GetAddressPubKeySECP256K1 returns the pubkey (compressed) and address (bech( -// this command requires user confirmation in the device -func (ledger *LedgerCosmos) GetAddressPubKeySECP256K1(bip32Path []uint32, hrp string) (pubkey []byte, addr string, err error) { - return ledger.getAddressPubKeySECP256K1(bip32Path, hrp, true) -} - -func (ledger *LedgerCosmos) GetBip32bytes(bip32Path []uint32, hardenCount int) ([]byte, error) { - var pathBytes []byte - var err error - - switch major := ledger.version.Major; major { - case 1: - pathBytes, err = GetBip32bytesv1(bip32Path, 3) - if err != nil { - return nil, err - } - case 2: - pathBytes, err = GetBip32bytesv2(bip32Path, 3) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("App version %d is not supported", major) - } - - return pathBytes, nil -} - -func (ledger *LedgerCosmos) signv1(bip32Path []uint32, transaction []byte) ([]byte, error) { - var packetIndex byte = 1 - var packetCount = 1 + byte(math.Ceil(float64(len(transaction))/float64(userMessageChunkSize))) - - var finalResponse []byte - - var message []byte - - for packetIndex <= packetCount { - chunk := userMessageChunkSize - if packetIndex == 1 { - pathBytes, err := ledger.GetBip32bytes(bip32Path, 3) - if err != nil { - return nil, err - } - header := []byte{userCLA, userINSSignSECP256K1, packetIndex, packetCount, byte(len(pathBytes))} - message = append(header, pathBytes...) - } else { - if len(transaction) < userMessageChunkSize { - chunk = len(transaction) - } - header := []byte{userCLA, userINSSignSECP256K1, packetIndex, packetCount, byte(chunk)} - message = append(header, transaction[:chunk]...) - } - - response, err := ledger.api.Exchange(message) - if err != nil { - if err.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 nil, errors.New("Not enough tokens were provided") - case "PARSER ERROR: JSMN_ERROR_INVAL": - return nil, errors.New("Unexpected character in JSON string") - case "PARSER ERROR: JSMN_ERROR_PART": - return nil, errors.New("The JSON string is not a complete.") - } - return nil, errors.New(errorMsg) - } - return nil, err - } - - finalResponse = response - if packetIndex > 1 { - transaction = transaction[chunk:] - } - packetIndex++ - - } - return finalResponse, nil -} - -func (ledger *LedgerCosmos) signv2(bip32Path []uint32, transaction []byte, p2 byte) ([]byte, error) { - var packetIndex byte = 1 - var packetCount = 1 + byte(math.Ceil(float64(len(transaction))/float64(userMessageChunkSize))) - - var finalResponse []byte - - var message []byte - - if p2 > 1 { - return nil, errors.New("only values of SIGN_MODE_LEGACY_AMINO (P2=0) and SIGN_MODE_TEXTUAL (P2=1) are allowed") - } - - for packetIndex <= packetCount { - chunk := userMessageChunkSize - if packetIndex == 1 { - pathBytes, err := ledger.GetBip32bytes(bip32Path, 3) - if err != nil { - return nil, err - } - header := []byte{userCLA, userINSSignSECP256K1, 0, p2, byte(len(pathBytes))} - message = append(header, pathBytes...) - } else { - if len(transaction) < userMessageChunkSize { - chunk = len(transaction) - } - - payloadDesc := byte(1) - if packetIndex == packetCount { - payloadDesc = byte(2) - } - - header := []byte{userCLA, userINSSignSECP256K1, payloadDesc, p2, byte(chunk)} - message = append(header, transaction[:chunk]...) - } - - response, err := ledger.api.Exchange(message) - if err != nil { - if err.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 nil, errors.New("Not enough tokens were provided") - case "PARSER ERROR: JSMN_ERROR_INVAL": - return nil, errors.New("Unexpected character in JSON string") - case "PARSER ERROR: JSMN_ERROR_PART": - return nil, errors.New("The JSON string is not a complete.") - } - return nil, errors.New(errorMsg) - } - if err.Error() == "[APDU_CODE_DATA_INVALID] Referenced data reversibly blocked (invalidated)" { - errorMsg := string(response) - return nil, errors.New(errorMsg) - } - return nil, err - } - - finalResponse = response - if packetIndex > 1 { - transaction = transaction[chunk:] - } - packetIndex++ - - } - return finalResponse, nil -} - -// GetAddressPubKeySECP256K1 returns the pubkey (compressed) and address (bech( -// this command requires user confirmation in the device -func (ledger *LedgerCosmos) getAddressPubKeySECP256K1(bip32Path []uint32, hrp string, requireConfirmation bool) (pubkey []byte, addr string, err error) { - if len(hrp) > 83 { - return nil, "", errors.New("hrp len should be <10") - } - - hrpBytes := []byte(hrp) - for _, b := range hrpBytes { - if !validHRPByte(b) { - return nil, "", errors.New("all characters in the HRP must be in the [33, 126] range") - } - } - - pathBytes, err := ledger.GetBip32bytes(bip32Path, 3) - if err != nil { - return nil, "", err - } - - p1 := byte(0) - if requireConfirmation { - p1 = byte(1) - } - - // Prepare message - header := []byte{userCLA, userINSGetAddrSecp256k1, 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 length - - response, err := ledger.api.Exchange(message) - - if err != nil { - return nil, "", err - } - if len(response) < 35+len(hrp) { - return nil, "", errors.New("Invalid response") - } - - pubkey = response[0:33] - addr = string(response[33:len(response)]) - - return pubkey, addr, err -} diff --git a/user_app_test.go b/user_app_test.go deleted file mode 100644 index 0597c27..0000000 --- a/user_app_test.go +++ /dev/null @@ -1,266 +0,0 @@ -/******************************************************************************* -* (c) 2018 - 2022 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" - "encoding/hex" - "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" -) - -// Ledger Test Mnemonic: equip will roof matter pink blind book anxiety banner elbow sun young - -func Test_UserFindLedger(t *testing.T) { - userApp, err := FindLedgerCosmosUserApp() - if err != nil { - t.Fatalf(err.Error()) - } - - assert.NotNil(t, userApp) - defer userApp.Close() -} - -func Test_UserGetVersion(t *testing.T) { - userApp, err := FindLedgerCosmosUserApp() - if err != nil { - t.Fatalf(err.Error()) - } - defer userApp.Close() - - version, err := userApp.GetVersion() - require.Nil(t, err, "Detected error") - fmt.Println(version) - - assert.Equal(t, uint8(0x0), version.AppMode, "TESTING MODE ENABLED!!") - assert.Equal(t, uint8(0x2), version.Major, "Wrong Major version") - assert.Equal(t, uint8(0x1), version.Minor, "Wrong Minor version") - assert.Equal(t, uint8(0x0), version.Patch, "Wrong Patch version") -} - -func Test_UserGetPublicKey(t *testing.T) { - userApp, err := FindLedgerCosmosUserApp() - if err != nil { - t.Fatalf(err.Error()) - } - defer userApp.Close() - - path := []uint32{44, 118, 5, 0, 21} - - pubKey, err := userApp.GetPublicKeySECP256K1(path) - if err != nil { - t.Fatalf("Detected error, err: %s\n", err.Error()) - } - - assert.Equal(t, 33, len(pubKey), - "Public key has wrong length: %x, expected length: %x\n", pubKey, 65) - fmt.Printf("PUBLIC KEY: %x\n", pubKey) - - assert.Equal(t, - "03cb5a33c61595206294140c45efa8a817533e31aa05ea18343033a0732a677005", - hex.EncodeToString(pubKey), - "Unexpected pubkey") -} - -func Test_GetAddressPubKeySECP256K1_Zero(t *testing.T) { - userApp, err := FindLedgerCosmosUserApp() - if err != nil { - t.Fatalf(err.Error()) - } - defer userApp.Close() - - hrp := "cosmos" - path := []uint32{44, 118, 0, 0, 0} - - pubKey, addr, err := userApp.GetAddressPubKeySECP256K1(path, hrp) - if err != nil { - t.Fatalf("Detected error, err: %s\n", err.Error()) - } - - fmt.Printf("PUBLIC KEY : %x\n", pubKey) - fmt.Printf("BECH32 ADDR: %s\n", addr) - - assert.Equal(t, 33, len(pubKey), "Public key has wrong length: %x, expected length: %x\n", pubKey, 65) - - assert.Equal(t, "034fef9cd7c4c63588d3b03feb5281b9d232cba34d6f3d71aee59211ffbfe1fe87", hex.EncodeToString(pubKey), "Unexpected pubkey") - assert.Equal(t, "cosmos1w34k53py5v5xyluazqpq65agyajavep2rflq6h", addr, "Unexpected addr") -} - -func Test_GetAddressPubKeySECP256K1(t *testing.T) { - userApp, err := FindLedgerCosmosUserApp() - if err != nil { - t.Fatalf(err.Error()) - } - defer userApp.Close() - - hrp := "cosmos" - path := []uint32{44, 118, 5, 0, 21} - - pubKey, addr, err := userApp.GetAddressPubKeySECP256K1(path, hrp) - if err != nil { - t.Fatalf("Detected error, err: %s\n", err.Error()) - } - - fmt.Printf("PUBLIC KEY : %x\n", pubKey) - fmt.Printf("BECH32 ADDR: %s\n", addr) - - assert.Equal(t, 33, len(pubKey), "Public key has wrong length: %x, expected length: %x\n", pubKey, 65) - - assert.Equal(t, "03cb5a33c61595206294140c45efa8a817533e31aa05ea18343033a0732a677005", hex.EncodeToString(pubKey), "Unexpected pubkey") - assert.Equal(t, "cosmos162zm3k8mc685592d7vej2lxrp58mgmkcec76d6", addr, "Unexpected addr") -} - -func Test_UserPK_HDPaths(t *testing.T) { - userApp, err := FindLedgerCosmosUserApp() - if err != nil { - t.Fatalf(err.Error()) - } - defer userApp.Close() - - path := []uint32{44, 118, 0, 0, 0} - - expected := []string{ - "034fef9cd7c4c63588d3b03feb5281b9d232cba34d6f3d71aee59211ffbfe1fe87", - "0260d0487a3dfce9228eee2d0d83a40f6131f551526c8e52066fe7fe1e4a509666", - "03a2670393d02b162d0ed06a08041e80d86be36c0564335254df7462447eb69ab3", - "033222fc61795077791665544a90740e8ead638a391a3b8f9261f4a226b396c042", - "03f577473348d7b01e7af2f245e36b98d181bc935ec8b552cde5932b646dc7be04", - "0222b1a5486be0a2d5f3c5866be46e05d1bde8cda5ea1c4c77a9bc48d2fa2753bc", - "0377a1c826d3a03ca4ee94fc4dea6bccb2bac5f2ac0419a128c29f8e88f1ff295a", - "031b75c84453935ab76f8c8d0b6566c3fcc101cc5c59d7000bfc9101961e9308d9", - "038905a42433b1d677cc8afd36861430b9a8529171b0616f733659f131c3f80221", - "038be7f348902d8c20bc88d32294f4f3b819284548122229decd1adf1a7eb0848b", - } - - for i := uint32(0); i < 10; i++ { - path[4] = i - - pubKey, err := userApp.GetPublicKeySECP256K1(path) - if err != nil { - t.Fatalf("Detected error, err: %s\n", err.Error()) - } - - assert.Equal( - t, - 33, - len(pubKey), - "Public key has wrong length: %x, expected length: %x\n", pubKey, 65) - - assert.Equal( - t, - expected[i], - hex.EncodeToString(pubKey), - "Public key 44'/118'/0'/0/%d does not match\n", i) - - _, err = btcec.ParsePubKey(pubKey[:]) - require.Nil(t, err, "Error parsing public key err: %s\n", err) - - } -} - -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) -} - -func Test_UserSign(t *testing.T) { - userApp, err := FindLedgerCosmosUserApp() - if err != nil { - t.Fatalf(err.Error()) - } - defer userApp.Close() - - path := []uint32{44, 118, 0, 0, 5} - - message := getDummyTx() - signature, err := userApp.SignSECP256K1(path, message, 0) - if err != nil { - t.Fatalf("[Sign] Error: %s\n", err.Error()) - } - - // Verify Signature - pubKey, err := userApp.GetPublicKeySECP256K1(path) - 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(pubKey[:]) - if err != nil { - t.Fatalf("[ParsePK] Error: " + err.Error()) - return - } - - sig2, err := ecdsa.ParseDERSignature(signature[:]) - 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_UserSign_Fails(t *testing.T) { - userApp, err := FindLedgerCosmosUserApp() - if err != nil { - t.Fatalf(err.Error()) - } - defer userApp.Close() - - path := []uint32{44, 118, 0, 0, 5} - - message := getDummyTx() - garbage := []byte{65} - message = append(garbage, message...) - - _, err = userApp.SignSECP256K1(path, message, 0) - 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) - } -} diff --git a/validator_app.go b/validator_app.go deleted file mode 100644 index 617c536..0000000 --- a/validator_app.go +++ /dev/null @@ -1,178 +0,0 @@ -/******************************************************************************* -* (c) 2018 - 2022 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" - "math" - - "github.com/zondax/ledger-go" -) - -const ( - validatorCLA = 0x56 - - validatorINSGetVersion = 0 - validatorINSPublicKeyED25519 = 1 - validatorINSSignED25519 = 2 - - validatorMessageChunkSize = 250 -) - -// Validator app -type LedgerTendermintValidator struct { - // Add support for this app - api ledger_go.LedgerDevice -} - -// RequiredCosmosUserAppVersion indicates the minimum required version of the Tendermint app -func RequiredTendermintValidatorAppVersion() VersionInfo { - return VersionInfo{0, 0, 5, 0} -} - -// FindLedgerCosmosValidatorApp finds a Cosmos validator app running in a ledger device -func FindLedgerTendermintValidatorApp() (_ *LedgerTendermintValidator, rerr error) { - ledgerAdmin := ledger_go.NewLedgerAdmin() - ledgerAPI, err := ledgerAdmin.Connect(0) - if err != nil { - return nil, err - } - - defer func() { - if rerr != nil { - defer ledgerAPI.Close() - } - }() - - ledgerCosmosValidatorApp := &LedgerTendermintValidator{ledgerAPI} - appVersion, err := ledgerCosmosValidatorApp.GetVersion() - if err != nil { - if err.Error() == "[APDU_CODE_CLA_NOT_SUPPORTED] Class not supported" { - err = errors.New("are you sure the Tendermint Validator app is open?") - } - return nil, err - } - - req := RequiredTendermintValidatorAppVersion() - if err := CheckVersion(*appVersion, req); err != nil { - return nil, err - } - - return ledgerCosmosValidatorApp, err -} - -// Close closes a connection with the Cosmos user app -func (ledger *LedgerTendermintValidator) Close() error { - return ledger.api.Close() -} - -// GetVersion returns the current version of the Cosmos user app -func (ledger *LedgerTendermintValidator) GetVersion() (*VersionInfo, error) { - message := []byte{validatorCLA, validatorINSGetVersion, 0, 0, 0} - response, err := ledger.api.Exchange(message) - - if err != nil { - return nil, err - } - - if len(response) < 4 { - return nil, errors.New("invalid response") - } - - return &VersionInfo{ - AppMode: response[0], - Major: response[1], - Minor: response[2], - Patch: response[3], - }, nil -} - -// GetPublicKeyED25519 retrieves the public key for the corresponding bip32 derivation path -func (ledger *LedgerTendermintValidator) GetPublicKeyED25519(bip32Path []uint32) ([]byte, error) { - pathBytes, err := GetBip32bytesv1(bip32Path, 10) - if err != nil { - return nil, err - } - - header := []byte{validatorCLA, validatorINSPublicKeyED25519, 0, 0, byte(len(pathBytes))} - message := append(header, pathBytes...) - - response, err := ledger.api.Exchange(message) - - if err != nil { - return nil, err - } - - if len(response) < 4 { - return nil, errors.New("invalid response. Too short") - } - - return response, nil -} - -// SignSECP256K1 signs a message/vote using the Tendermint validator app -func (ledger *LedgerTendermintValidator) SignED25519(bip32Path []uint32, message []byte) ([]byte, error) { - var packetIndex byte = 1 - var packetCount = 1 + byte(math.Ceil(float64(len(message))/float64(validatorMessageChunkSize))) - - var finalResponse []byte - - var apduMessage []byte - - for packetIndex <= packetCount { - chunk := validatorMessageChunkSize - if packetIndex == 1 { - pathBytes, err := GetBip32bytesv1(bip32Path, 10) - if err != nil { - return nil, err - } - header := []byte{ - validatorCLA, - validatorINSSignED25519, - packetIndex, - packetCount, - byte(len(pathBytes))} - - apduMessage = append(header, pathBytes...) - } else { - if len(message) < validatorMessageChunkSize { - chunk = len(message) - } - header := []byte{ - validatorCLA, - validatorINSSignED25519, - packetIndex, - packetCount, - byte(chunk)} - - apduMessage = append(header, message[:chunk]...) - } - - response, err := ledger.api.Exchange(apduMessage) - if err != nil { - return nil, err - } - - finalResponse = response - if packetIndex > 1 { - message = message[chunk:] - } - packetIndex++ - - } - return finalResponse, nil -} diff --git a/validator_app_test.go b/validator_app_test.go deleted file mode 100644 index c61a26c..0000000 --- a/validator_app_test.go +++ /dev/null @@ -1,64 +0,0 @@ -/******************************************************************************* -* (c) 2018 - 2022 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 ( - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -func Test_ValGetVersion(t *testing.T) { - validatorApp, err := FindLedgerTendermintValidatorApp() - if err != nil { - t.Fatalf(err.Error()) - } - defer validatorApp.Close() - - version, err := validatorApp.GetVersion() - require.Nil(t, err, "Detected error") - assert.Equal(t, uint8(0x0), version.AppMode, "TESTING MODE NOT ENABLED") - assert.Equal(t, uint8(0x0), version.Major, "Wrong Major version") - assert.Equal(t, uint8(0x9), version.Minor, "Wrong Minor version") - assert.Equal(t, uint8(0x0), version.Patch, "Wrong Patch version") -} - -func Test_ValGetPublicKey(t *testing.T) { - validatorApp, err := FindLedgerTendermintValidatorApp() - if err != nil { - t.Fatalf(err.Error()) - } - defer validatorApp.Close() - - path := []uint32{44, 118, 0, 0, 0} - - for i := 1; i < 10; i++ { - pubKey, err := validatorApp.GetPublicKeyED25519(path) - require.Nil(t, err, "Detected error, err: %s\n", err) - - assert.Equal( - t, - 32, - len(pubKey), - "Public key has wrong length: %x, expected length: %x\n", pubKey, 32) - } - -} - -func Test_ValSignED25519(t *testing.T) { - t.Skip("Go support is still not available. Please refer to the Rust library") -} From 40af114e37327a8d774f327e96b067ccf3df33c9 Mon Sep 17 00:00:00 2001 From: ftheirs Date: Wed, 20 Dec 2023 11:31:55 -0300 Subject: [PATCH 2/3] new comm package --- common.go | 118 ++++++++++---------- common_test.go | 136 ++++++++++++----------- constants.go | 44 ++++++++ ledger_cosmos.go | 248 ++++++++++++++++++++++++++++++++++++++++++ ledger_cosmos_test.go | 210 +++++++++++++++++++++++++++++++++++ 5 files changed, 629 insertions(+), 127 deletions(-) create mode 100644 constants.go create mode 100644 ledger_cosmos.go create mode 100644 ledger_cosmos_test.go diff --git a/common.go b/common.go index debd8f7..429069c 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,81 @@ 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 + 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) > HRP_MAX_LENGTH { + 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 < MIN_DISPLAYABLE_CHAR || b > MAX_DISPLAYABLE_CHAR { + 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") +func serializePath(path string) (pathBytes []byte, err error) { + if !strings.HasPrefix(path, "m/") { + return nil, errors.New(`path should start with "m/" (e.g "m/44'/118'/0'/0/3")`) } - 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) - } - 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) != DEFAULT_PATH_LENGTH { + 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..7ee6593 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,24 @@ 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} + reqVersion = VersionResponse{0, 0, 0, 0, 0, 0} + s = fmt.Sprintf("%v", reqVersion) + assert.Equal(t, "0.0.0", s) +} - 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,45 +45,36 @@ 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) - - if err != nil { - t.Fatalf("Detected error, err: %s\n", err.Error()) - } +func Test_SerializePath_EmptyString(t *testing.T) { + path := "" + pathBytes, err := serializePath(path) - fmt.Printf("Path: %x\n", pathBytes) + assert.NotNil(t, err, "Expected error for empty path, got nil") + assert.Nil(t, pathBytes, "Expected nil for pathBytes, got non-nil value") +} - assert.Equal( - t, - 41, - len(pathBytes), - "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 41) +func Test_SerializePath_InvalidPath(t *testing.T) { + path := "invalid_path" + pathBytes, err := serializePath(path) - assert.Equal( - t, - "052c000080760000800000000000000000000000000000000000000000000000000000000000000000", - fmt.Sprintf("%x", pathBytes), - "Unexpected PathBytes\n") + assert.NotNil(t, err, "Expected error for invalid path, got nil") + assert.Nil(t, pathBytes, "Expected nil for pathBytes, got non-nil value") } -func Test_PathGeneration3(t *testing.T) { - bip32Path := []uint32{44, 118, 0, 0, 0} - - pathBytes, err := GetBip32bytesv1(bip32Path, 3) +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()) @@ -89,21 +84,20 @@ 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", + "2c00008076000080000000800000000000000000", 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_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()) @@ -113,61 +107,75 @@ func Test_PathGeneration0v2(t *testing.T) { assert.Equal( t, - 40, + 20, len(pathBytes), - "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 40) + "PathBytes has wrong length: %x, expected length: %x\n", pathBytes, 20) assert.Equal( t, - "2c000000640000000000000000000000000000000000000000000000000000000000000000000000", + "2c0000803c000080000000800000000000000000", fmt.Sprintf("%x", pathBytes), "Unexpected PathBytes\n") } -func Test_PathGeneration2v2(t *testing.T) { - bip32Path := []uint32{44, 118, 0, 0, 0} - - pathBytes, err := GetBip32bytesv2(bip32Path, 2) +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, - "2c000080760000800000000000000000000000000000000000000000000000000000000000000000", - fmt.Sprintf("%x", pathBytes), - "Unexpected PathBytes\n") + "636f736d6f73", + fmt.Sprintf("%x", hrpBytes), + "Unexpected HRPBytes\n") } -func Test_PathGeneration3v2(t *testing.T) { - bip32Path := []uint32{44, 118, 0, 0, 0} +func Test_SerializeHRP_EmptyString(t *testing.T) { + hrp := "" + hrpBytes, err := serializeHRP(hrp) - pathBytes, err := GetBip32bytesv2(bip32Path, 3) + assert.NotNil(t, err, "Expected error for empty hrp, got nil") + assert.Nil(t, hrpBytes, "Expected nil for hrpBytes, got non-nil value") +} + +func Test_SerializeHRP_LongString(t *testing.T) { + hrp := "a_very_long_hrp_that_exceeds_the_maximum_length" + hrpBytes, err := serializeHRP(hrp) + + assert.NotNil(t, err, "Expected error for long hrp, got nil") + assert.Nil(t, hrpBytes, "Expected nil for hrpBytes, got non-nil value") +} + +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, - "2c000080760000800000008000000000000000000000000000000000000000000000000000000000", - fmt.Sprintf("%x", pathBytes), - "Unexpected PathBytes\n") + "65766d6f73", + fmt.Sprintf("%x", hrpBytes), + "Unexpected HRPBytes\n") } diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..1699f05 --- /dev/null +++ b/constants.go @@ -0,0 +1,44 @@ +/******************************************************************************* +* (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 + +type TxMode byte + +const ( + SignModeAmino TxMode = 0 + SignModeTextual TxMode = 1 + SignModeUnknown TxMode = 2 +) + +const ( + CLA = 0x55 + + INSGetVersion = 0 + INSSign = 2 + INSGetAddressAndPubKey = 4 + + CHUNKSIZE = 250 + + DEFAULT_PATH_LENGTH = 5 +) + +const ( + HRP_MAX_LENGTH = 83 + MIN_DISPLAYABLE_CHAR = 33 + MAX_DISPLAYABLE_CHAR = 126 + HARDENED = 0x80000000 +) diff --git a/ledger_cosmos.go b/ledger_cosmos.go new file mode 100644 index 0000000..d0ca061 --- /dev/null +++ b/ledger_cosmos.go @@ -0,0 +1,248 @@ +/******************************************************************************* +* (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" + "math" + + ledger_go "github.com/zondax/ledger-go" +) + +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 { + 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) + } +} From 282f6306279539d6250d08607ae673904d72927f Mon Sep 17 00:00:00 2001 From: ftheirs Date: Wed, 20 Dec 2023 11:32:17 -0300 Subject: [PATCH 3/3] update readme and deps --- .github/workflows/test.yml | 8 +------- LICENSE | 2 +- README.md | 20 ++++++++++++++++---- docs/zondax_dark.png | Bin 0 -> 21692 bytes docs/zondax_light.png | Bin 0 -> 26737 bytes go.mod | 2 +- go.sum | 2 ++ 7 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 docs/zondax_dark.png create mode 100644 docs/zondax_light.png diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3c7bbb..750b687 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,6 @@ name: Test on: push: - branches: - - master pull_request: jobs: @@ -13,12 +11,8 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '1.18' + go-version: '1.21' - name: test & coverage report creation run: | go test common.go -mod=readonly -timeout 5m -short -race -coverprofile=coverage.txt -covermode=atomic go test common.go -mod=readonly -timeout 5m - - uses: codecov/codecov-action@v3.1.1 - with: - file: ./coverage.txt - fail_ci_if_error: true diff --git a/LICENSE b/LICENSE index 5f0220d..cde15f2 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2018 - 2022 ZondaX AG + Copyright 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. diff --git a/README.md b/README.md index 45bd224..c884f61 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,22 @@ # ledger-cosmos-go +![zondax_light](docs/zondax_light.png#gh-light-mode-only) +![zondax_dark](docs/zondax_dark.png#gh-dark-mode-only) + [![Test](https://github.com/cosmos/ledger-cosmos-go/actions/workflows/test.yml/badge.svg)](https://github.com/cosmos/ledger-cosmos-go/actions/workflows/test.yml) [![Build status](https://ci.appveyor.com/api/projects/status/ovpfx35t289n3403?svg=true)](https://ci.appveyor.com/project/cosmos/ledger-cosmos-go) -This project is work in progress. Some aspects are subject to change. +This package provides a basic client library to communicate with a Cosmos App running in a Ledger Nano S/S+/X device + +| Operation | Response | Command | +| -------------------- | --------------------------- | -------------------------------- | +| GetVersion | app version | --------------- | +| GetAddressAndPubKey | pubkey + address | HRP + HDPath + ShowInDevice | +| Sign | signature in DER format | HDPath + HRP + SignMode* + Message | + +*Available sign modes for Cosmos app: SignModeAmino (0) | SignModeTextual (1) + + +# Who we are? -# Get source -Apart from cloning, be sure you install dep dependency management tool -https://github.com/golang/dep +We are Zondax, a company pioneering blockchain services. If you want to know more about us, please visit us at [zondax.ch](https://zondax.ch) diff --git a/docs/zondax_dark.png b/docs/zondax_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c14ba36853f86af8747f8d2c880b574261b2f77b GIT binary patch literal 21692 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 YxFb&M;g{{rFQERxO|4H99&^3^f06jK0RR91 literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod index 1593c87..7e1e9d0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/cosmos/ledger-cosmos-go -go 1.18 +go 1.21 require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 diff --git a/go.sum b/go.sum index bb9c1eb..2c92d8a 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,6 +14,7 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=