diff --git a/common.go b/common.go index 156ef80..4b0b55b 100644 --- a/common.go +++ b/common.go @@ -33,7 +33,7 @@ func (c VersionInfo) String() string { return fmt.Sprintf("%d.%d.%d", c.Major, c.Minor, c.Patch) } -// NotSupportedError the command is not supported by this app +// VersionRequiredError the command is not supported by this app type VersionRequiredError struct { Found VersionInfo Required VersionInfo @@ -72,7 +72,7 @@ func CheckVersion(ver VersionInfo, req VersionInfo) error { return NewVersionRequiredError(req, ver) } -func GetBip32bytes(bip32Path []uint32, hardenCount int) ([]byte, error) { +func GetBip32bytesv1(bip32Path []uint32, hardenCount int) ([]byte, error) { message := make([]byte, 41) if len(bip32Path) > 10 { return nil, fmt.Errorf("maximum bip32 depth = 10") @@ -88,3 +88,19 @@ func GetBip32bytes(bip32Path []uint32, hardenCount int) ([]byte, error) { } return message, nil } + +func GetBip32bytesv2(bip44Path []uint32, hardenCount int) ([]byte, error) { + message := make([]byte, 40) + if len(bip44Path) != 5 { + return nil, fmt.Errorf("path should contain 5 elements") + } + for index, element := range bip44Path { + pos := index*4 + value := element + if index < hardenCount { + value = 0x80000000 | element + } + binary.LittleEndian.PutUint32(message[pos:], value) + } + return message, nil +} diff --git a/common_test.go b/common_test.go index 7d3cb90..84f30d4 100644 --- a/common_test.go +++ b/common_test.go @@ -31,7 +31,7 @@ func Test_PrintVersion(t *testing.T) { func Test_PathGeneration0(t *testing.T) { bip32Path := []uint32{44, 100, 0, 0, 0} - pathBytes, err := GetBip32bytes(bip32Path, 0) + pathBytes, err := GetBip32bytesv1(bip32Path, 0) if err != nil { t.Fatalf( "Detected error, err: %s\n", err.Error()) @@ -55,7 +55,7 @@ func Test_PathGeneration0(t *testing.T) { func Test_PathGeneration2(t *testing.T) { bip32Path := []uint32{44, 118, 0, 0, 0} - pathBytes, err := GetBip32bytes(bip32Path, 2) + pathBytes, err := GetBip32bytesv1(bip32Path, 2) if err != nil { t.Fatalf("Detected error, err: %s\n", err.Error()) @@ -79,7 +79,7 @@ func Test_PathGeneration2(t *testing.T) { func Test_PathGeneration3(t *testing.T) { bip32Path := []uint32{44, 118, 0, 0, 0} - pathBytes, err := GetBip32bytes(bip32Path, 3) + pathBytes, err := GetBip32bytesv1(bip32Path, 3) if err != nil { t.Fatalf("Detected error, err: %s\n", err.Error()) @@ -99,3 +99,75 @@ func Test_PathGeneration3(t *testing.T) { 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) + + 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, + "2c000000640000000000000000000000000000000000000000000000000000000000000000000000", + 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) + + 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, + "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") +} diff --git a/user_app.go b/user_app.go index 40eda06..e21a73b 100644 --- a/user_app.go +++ b/user_app.go @@ -20,22 +20,15 @@ import ( "fmt" "math" - "github.com/btcsuite/btcd/btcec" "github.com/cosmos/ledger-go" ) const ( userCLA = 0x55 - userINSGetVersion = 0 - userINSPublicKeySECP256K1 = 1 - userINSSignSECP256K1 = 2 - userINSPublicKeySECP256K1ShowBech32 = 3 - userINSGetBech32PublicKey = 4 - - userINSHash = 100 - userINSPublicKeySECP256K1Test = 101 - userINSSignSECP256K1Test = 103 + userINSGetVersion = 0 + userINSSignSECP256K1 = 2 + userINSGetAddrSecp256k1 = 4 userMessageChunkSize = 250 ) @@ -46,11 +39,6 @@ type LedgerCosmos struct { version VersionInfo } -// RequiredCosmosUserAppVersion indicates the minimum required version of the Cosmos app -func RequiredCosmosUserAppVersion() VersionInfo { - return VersionInfo{Major: 1, Minor: 0,} -} - // FindLedgerCosmosUserApp finds a Cosmos user app running in a ledger device func FindLedgerCosmosUserApp() (*LedgerCosmos, error) { ledgerAPI, err := ledger_go.FindLedger() @@ -70,9 +58,8 @@ func FindLedgerCosmosUserApp() (*LedgerCosmos, error) { return nil, err } - req := RequiredCosmosUserAppVersion() - err = CheckVersion(*appVersion, req) - if err !=nil { + err = app.CheckVersion(*appVersion) + if err != nil { defer ledgerAPI.Close() return nil, err } @@ -85,6 +72,23 @@ 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 version.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 is not supported") + } +} + // GetVersion returns the current version of the Cosmos user app func (ledger *LedgerCosmos) GetVersion() (*VersionInfo, error) { message := []byte{userCLA, userINSGetVersion, 0, 0, 0} @@ -111,34 +115,21 @@ func (ledger *LedgerCosmos) GetVersion() (*VersionInfo, error) { // SignSECP256K1 signs a transaction using Cosmos user app // this command requires user confirmation in the device func (ledger *LedgerCosmos) SignSECP256K1(bip32Path []uint32, transaction []byte) ([]byte, error) { - return ledger.sign(userINSSignSECP256K1, bip32Path, transaction) + switch ledger.version.Major { + case 1: + return ledger.signv1(bip32Path, transaction) + case 2: + return ledger.signv2(bip32Path, transaction) + default: + return nil, fmt.Errorf("App version is not supported") + } } // 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) { - pathBytes, err := GetBip32bytes(bip32Path, 3) - if err != nil { - return nil, err - } - header := []byte{userCLA, userINSPublicKeySECP256K1, 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, fmt.Errorf("invalid response") - } - - cmp, err := btcec.ParsePubKey(response[:], btcec.S256()) - if err != nil { - return nil, err - } - return cmp.SerializeCompressed(), nil + pubkey, _, err := ledger.getAddressPubKeySECP256K1(bip32Path, "cosmos", false) + return pubkey, err } func validHRPByte(b byte) bool { @@ -149,138 +140,85 @@ func validHRPByte(b byte) bool { // 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) { - // Check that app is at least 1.3.1 - requiredVersion := VersionInfo{0, 1, 3, 1,} - err = CheckVersion(ledger.version, requiredVersion) - if err !=nil { - // Temporary backward compatibility - requiredVersion := VersionInfo{0, 1, 1, 1,} - err = CheckVersion(ledger.version, requiredVersion) - if err!=nil { - return nil, "", err - } - - // Call unsafe function until people can be forced to upgrade to 1.3.0 - pk, err := ledger.GetPublicKeySECP256K1(bip32Path) - if err!=nil{ - return nil, "", err - } - // comply with backwards compatible api - return pk, "", err - } - - if len(hrp) > 83 { - return nil, "", fmt.Errorf("hrp len should be <10") - } - - hrpBytes := []byte(hrp) - for _, b := range hrpBytes { - if !validHRPByte(b) { - return nil, "", fmt.Errorf("all characters in the HRP must be in the [33, 126] range") - } - } - - pathBytes, err := GetBip32bytes(bip32Path, 3) - if err != nil { - return nil, "", err - } - - // Prepare message - header := []byte{userCLA, userINSGetBech32PublicKey, 0, 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, "", fmt.Errorf("Invalid response") - } - - pubkey = response[0:33] - addr = string(response[33 : len(response)]) - - return pubkey, addr, err + return ledger.getAddressPubKeySECP256K1(bip32Path, hrp, true) } -// Hash returns the hash for the transaction (only enabled in test mode apps) -func (ledger *LedgerCosmos) Hash(transaction []byte) ([]byte, error) { - - var packetIndex = byte(1) - var packetCount = byte(math.Ceil(float64(len(transaction)) / float64(userMessageChunkSize))) +func (ledger *LedgerCosmos) GetBip32bytes(bip32Path []uint32, hardenCount int) ([]byte, error) { + var pathBytes []byte + var err error - var finalResponse []byte - for packetIndex <= packetCount { - chunk := userMessageChunkSize - if len(transaction) < userMessageChunkSize { - chunk = len(transaction) + switch ledger.version.Major { + case 1: + pathBytes, err = GetBip32bytesv1(bip32Path, 3) + if err != nil { + return nil, err } - - header := []byte{userCLA, userINSHash, packetIndex, packetCount, byte(chunk)} - message := append(header, transaction[:chunk]...) - response, err := ledger.api.Exchange(message) - + case 2: + pathBytes, err = GetBip32bytesv2(bip32Path, 3) if err != nil { return nil, err } - finalResponse = response - packetIndex++ - transaction = transaction[chunk:] - } - return finalResponse, nil -} - -// TestGetPublicKeySECP256K1 (only enabled in test mode apps) -func (ledger *LedgerCosmos) TestGetPublicKeySECP256K1() ([]byte, error) { - message := []byte{userCLA, userINSPublicKeySECP256K1Test, 0, 0, 0} - response, err := ledger.api.Exchange(message) - - if err != nil { - return nil, err + default: + return nil, fmt.Errorf("App version is not supported") } - if len(response) < 4 { - return nil, fmt.Errorf("invalid response") - } - - return response, nil + return pathBytes, nil } -// TestSignSECP256K1 (only enabled in test mode apps) -func (ledger *LedgerCosmos) TestSignSECP256K1(transaction []byte) ([]byte, error) { +func (ledger *LedgerCosmos) signv1(bip32Path []uint32, transaction []byte) ([]byte, error) { var packetIndex byte = 1 - var packetCount = byte(math.Ceil(float64(len(transaction)) / float64(userMessageChunkSize))) + var packetCount = 1 + byte(math.Ceil(float64(len(transaction))/float64(userMessageChunkSize))) var finalResponse []byte - for packetIndex <= packetCount { + var message []byte + for packetIndex <= packetCount { chunk := userMessageChunkSize - if len(transaction) < userMessageChunkSize { - chunk = len(transaction) + 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]...) } - header := []byte{userCLA, userINSSignSECP256K1Test, 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, fmt.Errorf("Not enough tokens were provided") + case "PARSER ERROR: JSMN_ERROR_INVAL": + return nil, fmt.Errorf("Unexpected character in JSON string") + case "PARSER ERROR: JSMN_ERROR_PART": + return nil, fmt.Errorf("The JSON string is not a complete.") + } + return nil, fmt.Errorf(errorMsg) + } return nil, err } finalResponse = response + if packetIndex > 1 { + transaction = transaction[chunk:] + } packetIndex++ - transaction = transaction[chunk:] + } return finalResponse, nil } -func (ledger *LedgerCosmos) sign(instruction byte, bip32Path []uint32, transaction []byte) ([]byte, error) { +func (ledger *LedgerCosmos) signv2(bip32Path []uint32, transaction []byte) ([]byte, error) { var packetIndex byte = 1 var packetCount = 1 + byte(math.Ceil(float64(len(transaction))/float64(userMessageChunkSize))) @@ -291,17 +229,23 @@ func (ledger *LedgerCosmos) sign(instruction byte, bip32Path []uint32, transacti for packetIndex <= packetCount { chunk := userMessageChunkSize if packetIndex == 1 { - pathBytes, err := GetBip32bytes(bip32Path, 3) + pathBytes, err := ledger.GetBip32bytes(bip32Path, 3) if err != nil { return nil, err } - header := []byte{userCLA, instruction, packetIndex, packetCount, byte(len(pathBytes))} + header := []byte{userCLA, userINSSignSECP256K1, 0, 0, byte(len(pathBytes))} message = append(header, pathBytes...) } else { if len(transaction) < userMessageChunkSize { chunk = len(transaction) } - header := []byte{userCLA, instruction, packetIndex, packetCount, byte(chunk)} + + payloadDesc := byte(1) + if packetIndex == packetCount { + payloadDesc = byte(2) + } + + header := []byte{userCLA, userINSSignSECP256K1, payloadDesc, 0, byte(chunk)} message = append(header, transaction[:chunk]...) } @@ -312,14 +256,18 @@ func (ledger *LedgerCosmos) sign(instruction byte, bip32Path []uint32, transacti errorMsg := string(response) switch errorMsg { case "ERROR: JSMN_ERROR_NOMEM": - return nil, fmt.Errorf("Not enough tokens were provided"); + return nil, fmt.Errorf("Not enough tokens were provided") case "PARSER ERROR: JSMN_ERROR_INVAL": - return nil, fmt.Errorf("Unexpected character in JSON string"); + return nil, fmt.Errorf("Unexpected character in JSON string") case "PARSER ERROR: JSMN_ERROR_PART": - return nil, fmt.Errorf("The JSON string is not a complete."); + return nil, fmt.Errorf("The JSON string is not a complete.") } return nil, fmt.Errorf(errorMsg) } + if err.Error() == "[APDU_CODE_DATA_INVALID] Referenced data reversibly blocked (invalidated)" { + errorMsg := string(response) + return nil, fmt.Errorf(errorMsg) + } return nil, err } @@ -332,3 +280,49 @@ func (ledger *LedgerCosmos) sign(instruction byte, bip32Path []uint32, transacti } 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, "", fmt.Errorf("hrp len should be <10") + } + + hrpBytes := []byte(hrp) + for _, b := range hrpBytes { + if !validHRPByte(b) { + return nil, "", fmt.Errorf("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, "", fmt.Errorf("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 index dd9b5d2..3fe30ce 100644 --- a/user_app_test.go +++ b/user_app_test.go @@ -53,8 +53,8 @@ func Test_UserGetVersion(t *testing.T) { fmt.Println(version) assert.Equal(t, uint8(0x0), version.AppMode, "TESTING MODE ENABLED!!") - assert.Equal(t, uint8(0x1), version.Major, "Wrong Major version") - assert.Equal(t, uint8(0x5), version.Minor, "Wrong Minor version") + 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") } @@ -74,10 +74,14 @@ func Test_UserGetPublicKey(t *testing.T) { 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, 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") + assert.Equal(t, + "03cb5a33c61595206294140c45efa8a817533e31aa05ea18343033a0732a677005", + hex.EncodeToString(pubKey), + "Unexpected pubkey") } func Test_GetAddressPubKeySECP256K1_Zero(t *testing.T) { @@ -265,5 +269,10 @@ func Test_UserSign_Fails(t *testing.T) { message = append(garbage, message...) _, err = userApp.SignSECP256K1(path, message) - assert.EqualError(t, err, "Invalid character in JSON string") + 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 index 79da04f..2f27bb1 100644 --- a/validator_app.go +++ b/validator_app.go @@ -101,7 +101,7 @@ func (ledger *LedgerTendermintValidator) GetVersion() (*VersionInfo, error) { // GetPublicKeyED25519 retrieves the public key for the corresponding bip32 derivation path func (ledger *LedgerTendermintValidator) GetPublicKeyED25519(bip32Path []uint32) ([]byte, error) { - pathBytes, err := GetBip32bytes(bip32Path, 10) + pathBytes, err := GetBip32bytesv1(bip32Path, 10) if err != nil { return nil, err } @@ -134,7 +134,7 @@ func (ledger *LedgerTendermintValidator) SignED25519(bip32Path []uint32, message for packetIndex <= packetCount { chunk := validatorMessageChunkSize if packetIndex == 1 { - pathBytes, err := GetBip32bytes(bip32Path, 10) + pathBytes, err := GetBip32bytesv1(bip32Path, 10) if err != nil { return nil, err }