diff --git a/api/firmware/bip85_test.go b/api/firmware/bip85_test.go new file mode 100644 index 0000000..033f1f4 --- /dev/null +++ b/api/firmware/bip85_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 Shift Crypto 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 firmware + +import ( + "encoding/hex" + + "github.com/stretchr/testify/require" +) + +func (s *initializedSimulatorTestSuite) TestBIP85AppBip39() { + // Can't test this yet as the simulator panics at trinary_choice (12, 18, 24 word choice). + //require.NoError(s.T(), s.device.BIP85AppBip39()) +} + +func (s *initializedSimulatorTestSuite) TestBIP85AppLN() { + entropy, err := s.device.BIP85AppLN() + require.NoError(s.T(), err) + require.Equal(s.T(), + "d05448562b8b64994b7de7eac43cdc8a", + hex.EncodeToString(entropy)) +} diff --git a/api/firmware/btc_test.go b/api/firmware/btc_test.go index 2d97f30..0dc9f69 100644 --- a/api/firmware/btc_test.go +++ b/api/firmware/btc_test.go @@ -1,4 +1,5 @@ // Copyright 2018-2019 Shift Cryptosecurity AG +// Copyright 2024 Shift Crypto AG // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,12 +21,26 @@ import ( "github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/messages" "github.com/BitBoxSwiss/bitbox02-api-go/util/semver" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) const hardenedKeyStart = 0x80000000 +func parseECDSASignature(t *testing.T, sig []byte) *ecdsa.Signature { + t.Helper() + require.Len(t, sig, 64) + r := new(btcec.ModNScalar) + r.SetByteSlice(sig[:32]) + s := new(btcec.ModNScalar) + s.SetByteSlice(sig[32:]) + return ecdsa.NewSignature(r, s) +} + func TestNewXPub(t *testing.T) { xpub, err := NewXPub( "xpub6FEZ9Bv73h1vnE4TJG4QFj2RPXJhhsPbnXgFyH3ErLvpcZrDcynY65bhWga8PazWHLSLi23PoBhGcLcYW6JRiJ12zXZ9Aop4LbAqsS3gtcy") @@ -39,6 +54,69 @@ func TestNewXPub(t *testing.T) { }, xpub) } +func (s *initializedSimulatorTestSuite) TestBTCXpub() { + xpub, err := s.device.BTCXPub(messages.BTCCoin_TBTC, []uint32{ + 49 + hardenedKeyStart, + 1 + hardenedKeyStart, + 0 + hardenedKeyStart, + }, messages.BTCPubRequest_YPUB, false) + require.NoError(s.T(), err) + require.Equal(s.T(), "ypub6WqXiL3fbDK5QNPe3hN4uSVkEvuE8wXoNCcecgggSuKVpU3Kc4fTvhuLgUhtnbAdaTb9gpz5PQdvzcsKPTLgW2CPkF5ZNRzQeKFT4NSc1xN", xpub) +} + +func (s *initializedSimulatorTestSuite) TestBTCAddress() { + address, err := s.device.BTCAddress( + messages.BTCCoin_TBTC, + []uint32{ + 84 + hardenedKeyStart, + 1 + hardenedKeyStart, + 0 + hardenedKeyStart, + 1, + 10, + }, + NewBTCScriptConfigSimple(messages.BTCScriptConfig_P2WPKH), + false, + ) + require.NoError(s.T(), err) + require.Equal(s.T(), "tb1qq064dxjgl9h9wzgsmzy6t6306qew42w9ka02u3", address) +} + +func parseXPub(t *testing.T, xpubStr string, keypath ...uint32) *hdkeychain.ExtendedKey { + t.Helper() + xpub, err := hdkeychain.NewKeyFromString(xpubStr) + require.NoError(t, err) + + for _, child := range keypath { + xpub, err = xpub.Derive(child) + require.NoError(t, err) + } + return xpub +} + +func (s *initializedSimulatorTestSuite) TestBTCSignMessage() { + coin := messages.BTCCoin_BTC + accountKeypath := []uint32{49 + hardenedKeyStart, 0 + hardenedKeyStart, 0 + hardenedKeyStart} + + xpubStr, err := s.device.BTCXPub(coin, accountKeypath, messages.BTCPubRequest_XPUB, false) + require.NoError(s.T(), err) + + xpub := parseXPub(s.T(), xpubStr, 0, 10) + pubKey, err := xpub.ECPubKey() + require.NoError(s.T(), err) + + sig, _, _, err := s.device.BTCSignMessage( + coin, + &messages.BTCScriptConfigWithKeypath{ + ScriptConfig: NewBTCScriptConfigSimple(messages.BTCScriptConfig_P2WPKH_P2SH), + Keypath: append(accountKeypath, 0, 10), + }, + []byte("message"), + ) + require.NoError(s.T(), err) + sigHash := chainhash.DoubleHashB([]byte("\x18Bitcoin Signed Message:\n\x07message")) + require.True(s.T(), parseECDSASignature(s.T(), sig).Verify(sigHash, pubKey)) +} + func TestBTCXPub(t *testing.T) { testConfigurations(t, func(t *testing.T, env *testEnv) { t.Helper() diff --git a/api/firmware/cardano_test.go b/api/firmware/cardano_test.go new file mode 100644 index 0000000..8b66389 --- /dev/null +++ b/api/firmware/cardano_test.go @@ -0,0 +1,66 @@ +// Copyright 2024 Shift Crypto 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 firmware + +import ( + "encoding/hex" + + "github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/messages" + "github.com/stretchr/testify/require" +) + +func (s *initializedSimulatorTestSuite) TestCardanoXPubs() { + xpubs, err := s.device.CardanoXPubs( + [][]uint32{ + {1852 + hardenedKeyStart, 1815 + hardenedKeyStart, hardenedKeyStart}, + {1852 + hardenedKeyStart, 1815 + hardenedKeyStart, hardenedKeyStart + 1}, + }, + ) + require.NoError(s.T(), err) + require.Len(s.T(), xpubs, 2) + require.Equal(s.T(), + "9fc9550e8379cb97c2d2557d89574207c6cf4d4ff62b37e377f2b3b3c284935b677f0fe5a4a6928c7b982c0c149f140c26c0930b73c2fe16feddfa21625e0316", + hex.EncodeToString(xpubs[0]), + ) + require.Equal(s.T(), + "7ffd0bd7d54f1648ac59a357d3eb27b878c2f7c09739d3b7c7e6662d496dea16f10ef525258833d37db047cd530bf373ebcb283495aa4c768424a2af37cee661", + hex.EncodeToString(xpubs[1]), + ) +} + +func (s *initializedSimulatorTestSuite) TestCardanoAddress() { + const account = uint32(1) + const rolePayment = uint32(0) // receive + const roleStake = uint32(2) // stake role must be 2 + const addressIdx = uint32(10) // address index + const stakeAddressIdx = uint32(0) // stake addr idx must be 0 + address, err := s.device.CardanoAddress( + messages.CardanoNetwork_CardanoMainnet, + &messages.CardanoScriptConfig{ + Config: &messages.CardanoScriptConfig_PkhSkh_{ + PkhSkh: &messages.CardanoScriptConfig_PkhSkh{ + KeypathPayment: []uint32{1852 + hardenedKeyStart, 1815 + hardenedKeyStart, account + hardenedKeyStart, rolePayment, addressIdx}, + KeypathStake: []uint32{1852 + hardenedKeyStart, 1815 + hardenedKeyStart, account + hardenedKeyStart, roleStake, stakeAddressIdx}, + }, + }, + }, + false, + ) + require.NoError(s.T(), err) + require.Equal(s.T(), + "addr1qxdq2ez52f5gtva3m77xgf5x4a7ap78mal43e5hhszyqehaaddssj2eta30yv9chr0sf4gu0jw77gag2g464yq0c70gqks5cr4", + address, + ) +} diff --git a/api/firmware/device_test.go b/api/firmware/device_test.go index 6d4d0f5..0d3af72 100644 --- a/api/firmware/device_test.go +++ b/api/firmware/device_test.go @@ -1,4 +1,5 @@ // Copyright 2018-2019 Shift Cryptosecurity AG +// Copyright 2024 Shift Crypto AG // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,17 +19,81 @@ import ( "crypto/rand" "errors" "fmt" + "net" + "os/exec" "testing" + "time" "github.com/BitBoxSwiss/bitbox02-api-go/api/common" "github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/messages" "github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/mocks" + "github.com/BitBoxSwiss/bitbox02-api-go/communication/u2fhid" "github.com/BitBoxSwiss/bitbox02-api-go/util/semver" "github.com/flynn/noise" "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" "google.golang.org/protobuf/proto" ) +// Runs tests against a simulator which is not initialzed (not seeded). +type simulatorTestSuite struct { + suite.Suite + + cmd *exec.Cmd + device *Device + conn net.Conn +} + +func (s *simulatorTestSuite) SetupSuite() { + s.cmd = exec.Command("./testdata/simulator") + require.NoError(s.T(), s.cmd.Start()) + + var err error + for i := 0; i < 200; i++ { + s.conn, err = net.Dial("tcp", "localhost:15423") + if err == nil { + break + } + time.Sleep(10 * time.Millisecond) + } + require.NoError(s.T(), err) + + const bitboxCMD = 0x80 + 0x40 + 0x01 + + communication := u2fhid.NewCommunication(s.conn, bitboxCMD) + s.device = NewDevice(nil, nil, + &mocks.Config{}, communication, &mocks.Logger{}, + ) + require.NoError(s.T(), s.device.Init()) + s.device.ChannelHashVerify(true) + + require.Equal(s.T(), common.ProductBitBox02Multi, s.device.Product()) +} + +func (s *simulatorTestSuite) TearDownSuite() { + require.NoError(s.T(), s.conn.Close()) + require.NoError(s.T(), s.cmd.Process.Kill()) +} + +func TestSimulatorSuite(t *testing.T) { + suite.Run(t, &simulatorTestSuite{}) +} + +// Runs tests againt a simulator that is seeded with this mnemonic: +// boring mistake dish oyster truth pigeon viable emerge sort crash wire portion cannon couple enact box walk height pull today solid off enable tide +type initializedSimulatorTestSuite struct { + simulatorTestSuite +} + +func (s *initializedSimulatorTestSuite) SetupSuite() { + s.simulatorTestSuite.SetupSuite() + require.NoError(s.T(), s.device.RestoreFromMnemonic()) +} + +func TestInitializedSimulatorSuite(t *testing.T) { + suite.Run(t, &initializedSimulatorTestSuite{}) +} + // newDevice creates a device to test with, with init/pairing already processed. func newDevice( t *testing.T, diff --git a/api/firmware/eth.go b/api/firmware/eth.go index 9da9a81..96b1267 100644 --- a/api/firmware/eth.go +++ b/api/firmware/eth.go @@ -253,7 +253,7 @@ func (device *Device) ETHSignEIP1559( } // ETHSignMessage signs an Ethereum message. The provided msg will be prefixed with "\x19Ethereum -// message\n" + len(msg) in the hardware, e.g. "\x19Ethereum\n5hello" (yes, the len prefix is the +// Signed Message\n" + len(msg) in the hardware, e.g. "\x19Ethereum Signed dMessage\n5hello" (yes, the len prefix is the // ascii representation with no fixed size or delimiter, WTF). // 27 is added to the recID to denote an uncompressed pubkey. func (device *Device) ETHSignMessage( diff --git a/api/firmware/eth_test.go b/api/firmware/eth_test.go index 693c338..1c187f0 100644 --- a/api/firmware/eth_test.go +++ b/api/firmware/eth_test.go @@ -19,8 +19,15 @@ import ( "github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/messages" "github.com/stretchr/testify/require" + "golang.org/x/crypto/sha3" ) +func hashKeccak(b []byte) []byte { + h := sha3.NewLegacyKeccak256() + h.Write(b) + return h.Sum(nil) +} + func parseTypeNoErr(t *testing.T, typ string, types map[string]interface{}) *messages.ETHSignTypedMessageRequest_MemberType { t.Helper() parsed, err := parseType(typ, types) @@ -238,3 +245,143 @@ func TestEncodeValue(t *testing.T) { require.NoError(t, err) require.Equal(t, []byte("\x00\x00\x03\xe8"), encoded) } + +func (s *initializedSimulatorTestSuite) TestETHPub() { + chainID := uint64(1) + xpub, err := s.device.ETHPub( + chainID, + []uint32{ + 44 + hardenedKeyStart, + 60 + hardenedKeyStart, + 0 + hardenedKeyStart, + 0, + }, + messages.ETHPubRequest_XPUB, + false, + nil, + ) + require.NoError(s.T(), err) + require.Equal(s.T(), + "xpub6F2rrkQ947NAvxGQdZPcw1fMHdnJMxXCPtGKWdmf1aaumRkaCoJF72yFYhKRmkbat27bhDy79FWndkS3skRNLgbsuuJKqBoFyUcrp5ZgmC3", + xpub, + ) + + address, err := s.device.ETHPub( + chainID, + []uint32{ + 44 + hardenedKeyStart, + 60 + hardenedKeyStart, + 0 + hardenedKeyStart, + 0, + 1, + }, + messages.ETHPubRequest_ADDRESS, + false, + nil, + ) + require.NoError(s.T(), err) + require.Equal(s.T(), + "0x6A2A567cB891DeF8eA8C215C85f93d2f0F844ceB", + address, + ) +} + +func (s *initializedSimulatorTestSuite) TestETHSignMessage() { + chainID := uint64(1) + xpubStr, err := s.device.ETHPub( + chainID, + []uint32{ + 44 + hardenedKeyStart, + 60 + hardenedKeyStart, + 0 + hardenedKeyStart, + 0, + }, + messages.ETHPubRequest_XPUB, + false, + nil, + ) + require.NoError(s.T(), err) + + xpub := parseXPub(s.T(), xpubStr, 10) + pubKey, err := xpub.ECPubKey() + require.NoError(s.T(), err) + + sig, err := s.device.ETHSignMessage( + chainID, + []uint32{ + 44 + hardenedKeyStart, + 60 + hardenedKeyStart, + 0 + hardenedKeyStart, + 0, + 10, + }, + []byte("message"), + ) + require.NoError(s.T(), err) + + sigHash := hashKeccak([]byte("\x19Ethereum Signed Message:\n7message")) + require.True(s.T(), parseECDSASignature(s.T(), sig[:64]).Verify(sigHash, pubKey)) +} + +func (s *initializedSimulatorTestSuite) TestETHSignTypedMessage() { + msg := []byte(` +{ + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Attachment": [ + { "name": "contents", "type": "string" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallet", "type": "address" }, + { "name": "age", "type": "uint8" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" }, + { "name": "attachments", "type": "Attachment[]" } + ] + }, + "primaryType": "Mail", + "domain": { + "name": "Ether Mail", + "version": "1", + "chainId": 1, + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "age": 20 + }, + "to": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "age": "0x1e" + }, + "contents": "Hello, Bob!", + "attachments": [{ "contents": "attachment1" }, { "contents": "attachment2" }] + } +}`) + + sig, err := s.device.ETHSignTypedMessage( + 1, + []uint32{ + 44 + hardenedKeyStart, + 60 + hardenedKeyStart, + 0 + hardenedKeyStart, + 0, + 10, + }, + msg, + ) + require.NoError(s.T(), err) + require.Len(s.T(), sig, 65) +} diff --git a/api/firmware/mnemonic_test.go b/api/firmware/mnemonic_test.go new file mode 100644 index 0000000..d2cfa5d --- /dev/null +++ b/api/firmware/mnemonic_test.go @@ -0,0 +1,40 @@ +// Copyright 2024 Shift Crypto 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 firmware + +import "github.com/stretchr/testify/require" + +func (s *initializedSimulatorTestSuite) TestShowMnemonic() { + require.NoError(s.T(), s.device.ShowMnemonic()) +} + +func (s *initializedSimulatorTestSuite) TestSetMnemonicPassphraseEnabled() { + info, err := s.device.DeviceInfo() + require.NoError(s.T(), err) + require.False(s.T(), info.MnemonicPassphraseEnabled) + + require.NoError(s.T(), s.device.SetMnemonicPassphraseEnabled(true)) + + info, err = s.device.DeviceInfo() + require.NoError(s.T(), err) + require.True(s.T(), info.MnemonicPassphraseEnabled) + + require.NoError(s.T(), s.device.SetMnemonicPassphraseEnabled(false)) + + info, err = s.device.DeviceInfo() + require.NoError(s.T(), err) + require.False(s.T(), info.MnemonicPassphraseEnabled) + +} diff --git a/api/firmware/sdcard_test.go b/api/firmware/sdcard_test.go new file mode 100644 index 0000000..4485881 --- /dev/null +++ b/api/firmware/sdcard_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 Shift Crypto 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 firmware + +import ( + "github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/messages" + "github.com/stretchr/testify/require" +) + +func (s *simulatorTestSuite) TestCheckSDCard() { + inserted, err := s.device.CheckSDCard() + require.NoError(s.T(), err) + // Simulator always returns true. + require.True(s.T(), inserted) +} + +func (s *simulatorTestSuite) TestInsertRemoveSDCard() { + require.NoError(s.T(), + s.device.InsertRemoveSDCard(messages.InsertRemoveSDCardRequest_INSERT_CARD)) + require.NoError(s.T(), + s.device.InsertRemoveSDCard(messages.InsertRemoveSDCardRequest_REMOVE_CARD)) +} diff --git a/api/firmware/system_test.go b/api/firmware/system_test.go new file mode 100644 index 0000000..8a5508b --- /dev/null +++ b/api/firmware/system_test.go @@ -0,0 +1,32 @@ +// Copyright 2024 Shift Crypto 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 firmware + +import "github.com/stretchr/testify/require" + +func (s *simulatorTestSuite) TestDeviceName() { + info, err := s.device.DeviceInfo() + require.NoError(s.T(), err) + require.Equal(s.T(), "My BitBox", info.Name) + + // Name too long. + require.Error(s.T(), s.device.SetDeviceName( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + + require.NoError(s.T(), s.device.SetDeviceName("new name")) + info, err = s.device.DeviceInfo() + require.NoError(s.T(), err) + require.Equal(s.T(), "new name", info.Name) +} diff --git a/api/firmware/testdata/simulator b/api/firmware/testdata/simulator new file mode 100755 index 0000000..7cb1ea7 Binary files /dev/null and b/api/firmware/testdata/simulator differ