diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..96154bd --- /dev/null +++ b/.clang-format @@ -0,0 +1,17 @@ +BasedOnStyle: Google +IndentWidth: 4 +Language: Cpp +ColumnLimit: 100 +PointerAlignment: Right +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AllowAllParametersOfDeclarationOnNextLine: false +SortIncludes: false +SpaceAfterCStyleCast: true +AllowShortCaseLabelsOnASingleLine: false +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortFunctionsOnASingleLine: None +BinPackArguments: false +BinPackParameters: false diff --git a/.github/workflows/build_and_functional_tests.yml b/.github/workflows/build_and_functional_tests.yml new file mode 100644 index 0000000..9b74700 --- /dev/null +++ b/.github/workflows/build_and_functional_tests.yml @@ -0,0 +1,33 @@ +name: Build and run functional tests using ragger through reusable workflow + +# This workflow will build the app and then run functional tests using the Ragger framework upon Speculos emulation. +# It calls a reusable workflow developed by Ledger's internal developer team to build the application and upload the +# resulting binaries. +# It then calls another reusable workflow to run the Ragger tests on the compiled application binary. +# +# While this workflow is optional, having functional testing on your application is mandatory and this workflow and +# tooling environment is meant to be easy to use and adapt after forking your application + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + build_application: + name: Build application using the reusable workflow + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1 + with: + upload_app_binaries_artifact: "compiled_app_binaries" + + ragger_tests: + name: Run ragger tests using the reusable workflow + needs: build_application + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1 + with: + download_app_binaries_artifact: "compiled_app_binaries" + test_dir: tests diff --git a/.github/workflows/codeql_checks.yml b/.github/workflows/codeql_checks.yml new file mode 100644 index 0000000..2494182 --- /dev/null +++ b/.github/workflows/codeql_checks.yml @@ -0,0 +1,44 @@ +name: "CodeQL" + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + # Excluded path: add the paths you want to ignore instead of deleting the workflow + paths-ignore: + - '.github/workflows/*.yml' + - 'tests/*' + +jobs: + analyse: + name: Analyse + strategy: + matrix: + sdk: [ "$NANOS_SDK", "$NANOX_SDK", "$NANOSP_SDK" ] + #'cpp' covers C and C++ + language: [ 'cpp' ] + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + # CodeQL will create the database during the compilation + - name: Build + run: | + make BOLOS_SDK=${{ matrix.sdk }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/coding_style_checks.yml b/.github/workflows/coding_style_checks.yml new file mode 100644 index 0000000..6be2786 --- /dev/null +++ b/.github/workflows/coding_style_checks.yml @@ -0,0 +1,25 @@ +name: Run coding style check through reusable workflow + +# This workflow will run linting checks to ensure a level of uniformization among all Ledger applications. +# +# The presence of this workflow is mandatory as a minimal level of linting is required. +# You are however free to modify the content of the .clang-format file and thus the coding style of your application. +# We simply ask you to not diverge too much from the linting of the Boilerplate application. + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + check_linting: + name: Check linting using the reusable workflow + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_lint.yml@v1 + with: + source: './src' + extensions: 'h,c' + version: 11 diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml deleted file mode 100644 index 0397770..0000000 --- a/.github/workflows/coverity.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Coverity - -on: - # Triggers the workflow on push or pull request events but only for the master branch - push: - branches: [ master ] - pull_request: - branches: [ master ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -jobs: - coverity-scan: - name: Coverity Scan - runs-on: ubuntu-latest - - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-scanner:latest - - steps: - - uses: actions/checkout@v2 - - - name: Build with cov-build - run: | - make clean - cov-build --dir cov-int make default - - name: Submit the result to Coverity Scan - run: | - tar czvf cov-int.tar.gz cov-int - curl \ - --form token=$TOKEN \ - --form email=$EMAIL \ - --form file=@cov-int.tar.gz \ - --form version=$GITHUB_REF \ - --form description="$GITHUB_REPOSITORY app" \ - https://scan.coverity.com/builds?project=$GITHUB_REPOSITORY - env: - EMAIL: ${{ secrets.COVERITY_SCAN_EMAIL }} - TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }} diff --git a/.github/workflows/python_client_checks.yml b/.github/workflows/python_client_checks.yml new file mode 100644 index 0000000..8ce8ac2 --- /dev/null +++ b/.github/workflows/python_client_checks.yml @@ -0,0 +1,44 @@ +name: Checks on the Python client + +# This workflow performs some checks on the Python client used by the Boilerplate tests +# It is there to help us maintain a level of quality in our codebase and does not have to be kept on forked +# applications. + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + + lint: + name: Boilerplate client linting + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v3 + - name: Installing PIP dependencies + run: | + pip install pylint + pip install --extra-index-url https://test.pypi.org/simple/ -r tests/requirements.txt + - name: Lint Python code + run: | + pylint --rc tests/setup.cfg tests/application_client/ + + mypy: + name: Type checking + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v3 + - name: Installing PIP dependencies + run: | + pip install mypy + pip install --extra-index-url https://test.pypi.org/simple/ -r tests/requirements.txt + - name: Mypy type checking + run: | + mypy tests/application_client/ diff --git a/Makefile b/Makefile index 070f102..8569a24 100755 --- a/Makefile +++ b/Makefile @@ -1,111 +1,109 @@ -#******************************************************************************* -# Ledger App -# (c) 2017 Ledger +# **************************************************************************** +# Ledger App Boilerplate +# (c) 2023 Ledger SAS. # -# 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 +# 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 +# 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. -#******************************************************************************* +# 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. +# **************************************************************************** ifeq ($(BOLOS_SDK),) $(error Environment variable BOLOS_SDK is not set) endif + include $(BOLOS_SDK)/Makefile.defines -######### -# App # -######### +######################################## +# Mandatory configuration # +######################################## +# Application name +APPNAME = "Sia" -APPNAME = Sia -APPVERSION = 0.4.5 -ifeq ($(TARGET_NAME), TARGET_NANOX) -ICONNAME=nanox_app_sia.gif -else -ICONNAME=nanos_app_sia.gif -endif - -# The --path argument here restricts which BIP32 paths the app is allowed to derive. -APP_LOAD_PARAMS = --path "44'/93'" --curve ed25519 $(COMMON_LOAD_PARAMS) -ifeq ($(TARGET_NAME),TARGET_NANOX) -APP_LOAD_PARAMS += --appFlags 0x240 -else -APP_LOAD_PARAMS += --appFlags 0x40 -endif +# Application version +APPVERSION_M = 1 +APPVERSION_N = 0 +APPVERSION_P = 0 +APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" +# Application source files APP_SOURCE_PATH += src -SDK_SOURCE_PATH += lib_stusb lib_stusb_impl -SDK_SOURCE_PATH += lib_ux - -all: default -load: all - python -m ledgerblue.loadApp $(APP_LOAD_PARAMS) - -delete: - python -m ledgerblue.deleteApp $(COMMON_DELETE_PARAMS) - -############ -# Platform # -############ - -DEFINES += OS_IO_SEPROXYHAL -DEFINES += HAVE_SPRINTF -DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=7 IO_HID_EP_LENGTH=64 HAVE_USB_APDU +# Application icons following guidelines: +# https://developers.ledger.com/docs/embedded-app/design-requirements/#device-icon +ICON_NANOS = nanos_app_sia.gif +ICON_NANOX = nanox_app_sia.gif +ICON_NANOSP = nanos2_app_sia.gif +ICON_STAX = stax_app_sia_big.gif + +# Application allowed derivation curves. +# Possibles curves are: secp256k1, secp256r1, ed25519 and bls12381g1 +# If your app needs it, you can specify multiple curves by using: +# `CURVE_APP_LOAD_PARAMS = ` +CURVE_APP_LOAD_PARAMS = ed25519 + +# Application allowed derivation paths. +# You should request a specific path for your app. +# This serve as an isolation mechanism. +# Most application will have to request a path according to the BIP-0044 +# and SLIP-0044 standards. +# If your app needs it, you can specify multiple path by using: +# `PATH_APP_LOAD_PARAMS = "44'/1'" "45'/1'"` +PATH_APP_LOAD_PARAMS = "44'/93'" + +# Setting to allow building variant applications +# - is the name of the parameter which should be set +# to specify the variant that should be build. +# - a list of variant that can be build using this app code. +# * It must at least contains one value. +# * Values can be the app ticker or anything else but should be unique. +VARIANT_PARAM = COIN +VARIANT_VALUES = sia + +# Enabling DEBUG flag will enable PRINTF and disable optimizations +# DEBUG = 1 + +######################################## +# Application custom permissions # +######################################## +# See SDK `include/appflags.h` for the purpose of each permission +#HAVE_APPLICATION_FLAG_DERIVE_MASTER = 1 +#HAVE_APPLICATION_FLAG_GLOBAL_PIN = 1 +#HAVE_APPLICATION_FLAG_BOLOS_SETTINGS = 1 +#HAVE_APPLICATION_FLAG_LIBRARY = 1 + +######################################## +# Application communication interfaces # +######################################## +ENABLE_BLUETOOTH = 1 +#ENABLE_NFC = 1 + +######################################## +# NBGL custom features # +######################################## +#ENABLE_NBGL_QRCODE = 1 +#ENABLE_NBGL_KEYBOARD = 1 +#ENABLE_NBGL_KEYPAD = 1 + +######################################## +# Features disablers # +######################################## +# These advanced settings allow to disable some feature that are by +# default enabled in the SDK `Makefile.standard_app`. +#DISABLE_STANDARD_APP_FILES = 1 +#DISABLE_DEFAULT_IO_SEPROXY_BUFFER_SIZE = 1 # To allow custom size declaration +#DISABLE_STANDARD_APP_DEFINES = 1 # Will set all the following disablers +#DISABLE_STANDARD_SNPRINTF = 1 +#DISABLE_STANDARD_USB = 1 +#DISABLE_STANDARD_WEBUSB = 1 +#DISABLE_STANDARD_BAGL_UX_FLOW = 1 DEFINES += HAVE_LEGACY_PID -DEFINES += HAVE_BAGL BAGL_WIDTH=128 BAGL_HEIGHT=64 -DEFINES += HAVE_UX_FLOW -DEFINES += APPVERSION=\"$(APPVERSION)\" -DEFINES += UNUSED\(x\)=\(void\)x - -### Nano X -ifeq ($(TARGET_NAME),TARGET_NANOX) -# bluetooth -DEFINES += HAVE_BLE BLE_COMMAND_TIMEOUT_MS=2000 -DEFINES += HAVE_BLE_APDU -SDK_SOURCE_PATH += lib_blewbxx lib_blewbxx_impl -endif - -ifeq ($(TARGET_NAME),TARGET_NANOS) -DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128 -else -DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300 -# include fonts or ui will be empty -DEFINES += HAVE_BAGL_ELLIPSIS -DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX -DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX -DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX -endif - -############## -# Compiler # -############## - -CC := $(CLANGPATH)clang -CFLAGS += -O3 -Os - -AS := $(GCCPATH)arm-none-eabi-gcc -LD := $(GCCPATH)arm-none-eabi-gcc -LDFLAGS += -O3 -Os -LDLIBS += -lm -lgcc -lc - -################## -# Dependencies # -################## - -# import rules to compile glyphs -include $(BOLOS_SDK)/Makefile.glyphs -# import generic rules from the sdk -include $(BOLOS_SDK)/Makefile.rules - -dep/%.d: %.c Makefile -listvariants: - @echo VARIANTS COIN sia +include $(BOLOS_SDK)/Makefile.standard_app diff --git a/README.md b/README.md index 6938629..0b25917 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,51 @@ # ledger-app-sia-x -This is an unofficial Sia wallet app for the Ledger Nano S/X. +This is a Sia wallet app for the Ledger Stax, Nano S, Nano SP, and Nano X. -When installed on a Nano S/X, the app allows you to generate Sia addresses, +When installed on a Ledger device, the app allows you to generate Sia addresses, calculate transaction hashes, sign those hashes, and use those signatures to construct valid transactions. The Sia app is the most secure method currently available for performing these actions. -This code also serves as a walkthrough for writing your own Ledger Nano S/X app. +This code also serves as a walkthrough for writing your own Ledger device. The code is heavily commented and describes both the high-level architecture -and low-level implementation details of Nano S/X app development. Begin by +and low-level implementation details of Ledger app development. Begin by reading `src/main.c`, and the comments will tell you which files to read next. No binaries are provided at this time. To build and install the Sia app on -your Ledger Nano S/X, follow Ledger's [setup instructions](https://ledger.readthedocs.io/en/latest/userspace/setup.html). +your Ledger device, follow Ledger's [setup instructions](https://speculos.ledger.com/user/docker.html). After the app is installed, build the `sialedger.go` binary to interact with the device. `./sialedger --help` will print a list of commands. -## Usage +## Installation and Usage -Please refer to our [standalone guide](https://siatech.helpdocs.io/article/1tteqxvgh0) for a walkthrough that demonstrates how -to generate addresses and sign transactions using the app. +Please refer to our [standalone guide](https://docs.sia.tech/sia-integrations/using-the-sia-ledger-nano-app-sia-central) for a walkthrough that demonstrates how +to install the app, generate addresses and sign transactions. ## Security Model -The attack surface for using the Sia wallet app on a Ledger Nano S/X comprises -the Sia app itself, the system firmware running on the Nano S/X, the computer -that the Nano S/X is connected to, and posession/control of the device. For our +The attack surface for using the Sia wallet app on a Ledger device comprises +the Sia app itself, the system firmware running on the Ledger device, the computer +that the Ledger device is connected to, and possession/control of the device. For our purposes, the app only needs to ensure its own correctness and protect the -user from the computer that the Nano S/X is connected to. Other attack surfaces +user from the computer that the Ledger device is connected to. Other attack surfaces are beyond our control; we assume that the user physically controls the device, is not running malicious/buggy software on the device, and follows proper security protocols. The goal of the Sia app is to achieve perfect security given these assumptions. The main attack vector that we are concerned with, then, is a computer running -malicious sofware. This software may imitate programs like `sialedger` in such +malicious software. This software may imitate programs like `sialedger` in such a way that the user cannot tell the difference, but secretly act maliciously. Specifically, the computer can do the following: 1. Lie to the user about which actions it is performing. *Example: the user runs `./sialedger addr 1`, but the computer secretly runs `./sialedger addr 2` instead.* -2. Lie to the user about what it received from the Nano S/X. *Example: the Nano +2. Lie to the user about what it received from the Ledger device. *Example: the Nano S generates address X, but the computer displays address Y.* -3. Exfiltrate data supplied by the user or the Nano S/X. *Example: the user +3. Exfiltrate data supplied by the user or the Ledger device. *Example: the user generates addresses A and B; the computer "phones home" to report that A and B are owned by the same person.* 4. Refuse to comply with the user's requests. *Example: the user runs @@ -58,7 +58,7 @@ certain actions. Type-1 and type-2 actions, on the other hand, are much more dangerous, and can be trivially exploited to steal the user's coins. To combat these attacks, apps must make use of the embedded display on the -Nano S/X. If data sent to/from the Nano S/X is displayed on the screen, the user +Ledger device. If data sent to/from the Ledger device is displayed on the screen, the user can verify that the computer is not lying about what it sent or received. In the interest of user-friendliness, we would like to display as little information as much as possible, but each omission brings with it the risk of diff --git a/TUTORIAL.md b/TUTORIAL.md index e48673b..61840de 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,57 +1,3 @@ -## Introduction +## Tutorial -This is a guide on how to use the Sia app on a Ledger Nano device. The Sia app currently supports the Ledger Nano S and Ledger Nano X models and supports transactions involving Siacoins. - -## Requirements - - - Initialized Ledger device with the latest firmware - - Ledger device connected to computer via USB - - Ledger Live open - -## Enable Developer Mode -The Sia app for Ledger devices can only be installed in developer mode. To enable developer mode, do the following: - -1. Open the **Settings** tab in Ledger Live -2. Select the **Experimental Features** tab within settings -3. Enable the option titled **Developer Mode** - -## Install the Sia App - -1. Select the **Manager** tab in Ledger Live -2. Ensure you are on the **Apps Catalog** tab within the manager -3. If prompted, allow the Ledger Manager access to your device -4. Search for Sia within the app catalog -5. Click install and wait for processing to complete -6. Exit Ledger Live - -## Connecting to Sia Wallet - -1. Connect and unlock your Ledger device -2. Ensure that Ledger Live does not have the manager open. Leaving it open will interfere with the Sia web wallet and cause the Sia app to crash. -3. Open the Sia app on the device -4. Visit the [Sia Central Wallet website](https://wallet.siacentral.com/) -5. When we first visit the Sia Central Wallet, we are prompted to set a password to encrypt the wallet(s) with. Make sure to select a secure password. -6. After typing and confirming the password, select **Ledger Wallet** as the type of wallet. -7. Press the connect button and select your Ledger device -8. Select **Import Public Key** and accept the public key request on your device -9. Press done to confirm wallet importation - -## Using Sia Wallet - -### Viewing Balance - -The default screen of the Sia Central Wallet allows you to see your balance along with its value in USD. - -### Sending - -Press the **Send** button. Then input the recipient address, along with the amount, denominated in either SC or USD. Press **Send**, then confirm the transaction information on your Ledger device and then press accept to complete the signing. - -### Receiving - -Press the **Receive** button. The address of your wallet represented in text and as a QR code will appear. - -## Finding Support / Communities - -- [Discord](https://discord.gg/sFCT3Ar) -- [Forum](https://forum.sia.tech/) -- [Sia Docs](https://support.sia.tech/) +Please refer to https://docs.sia.tech/sia-integrations/using-the-sia-ledger-nano-app-sia-central for the latest guide on installing and using the Ledger app for Sia. diff --git a/docs/apdu.md b/docs/apdu.md new file mode 100644 index 0000000..4b32e99 --- /dev/null +++ b/docs/apdu.md @@ -0,0 +1,154 @@ +# Sia application: Technical specification + +This page details the protocol implementation of the Sia app. + +## Framework + +We use an [APDU protocol](https://gist.github.com/Wollac/49f0c4e318e42f463b8306298dfb4f4a) compatible message format. + +All commands use CLA = 0xE0. + +| CLA | INS | COMMAND_NAME | DESCRIPTION | +| ---- | ---- | -------------- | --------------------------------------- | +| 0xE0 | 0x01 | GET_VERSION | Returns version of the app | +| 0xE0 | 0x02 | GET_PUBLIC_KEY | Returns public key or addreses | +| 0xE0 | 0x04 | SIGN_HASH | Sign a 32 byte hash | +| 0xE0 | 0x08 | GET_TXN_HASH | Sign a transaction or retrieve its hash | + +### Commands requiring multiple messages + +Sending a transaction can sometimes take multiple messages if the transaction is sufficiently large. In the event that more data is required, SW_OK is returned and the app listens for additional messages, which will use P1_MORE=0x80 instead of P1_FIRST=0x00. + +### Note on encoding + +To learn more on how transactions are encoded, visit https://pkg.go.dev/go.sia.tech/core/types#Transaction. + +## Status Words + +| SW | SW Name | Description | +| ------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0x6B00 | SW_DEVELOPER_ERR | Errors that should not occur | +| 0x6B01 | SW_INVALID_PARAM | Invalid user specified parameters are supplied such as an invalid signature index when signing a transaction | +| 0x6B02 | SW_IMPROPER_INIT | If this is the first packet of a transaction signing event, the transaction context must not already be initialized. Otherwise, an attacker could fool the user by concatenating two transactions. This error is returned in that case. | +| 0x6985 | SW_USER_REJECTED | User declined this action | +| 0x6D00 | SW_INS_NOT_SUPPORTED | Unsupported command used | +| 0x9000 | SW_OK | Success | + +## Commands + +### GET_VERSION + +Returns version of the app. + +#### Encoding + +##### Command + +| CLA | INS | +| ---- | ---- | +| 0xE0 | 0x01 | + +##### Input data + +None + +##### Output data + +| Length | Description | +| ---- | ---- | +| 1 | Major version | +| 1 | Minor version | +| 1 | Maintenance version | + +### GET_PUBLIC_KEY + +Returns public key or addreses. + +#### Encoding + +##### Command + +| CLA | INS | P2 +| ---- | ---- | ---- | +| 0xE0 | 0x02 | 0x00 to display address and 0x01 to display pubkey | + +##### Input data + +| Length | Description | +| ---- | ---- | +| 4 | Little endian encoded uint32 index | + + +##### Output data + +For pubkey + +| Length | Description | +| ---- | ---- | +| 32 | Sia-encoded pubkey | + +For address + +| Length | Description | +| ---- | ---- | +| 76 | Sia-encoded address | + +### SIGN_HASH + +Sign a 32 byte hash. + +#### Encoding + +##### Command + +| CLA | INS | +| ---- | ---- | +| 0xE0 | 0x03 | + +##### Input data + +| Length | Description | +| ---- | ---- | +| 4 | Little endian encoded uint32 index | +| 32 | Binary encoded hash to sign | + +##### Output data + +| Length | Description | +| ---- | ---- | +| 64 | Binary encoded signature | + +### GET_TXN_HASH + +Sign a transaction or retrieve its hash. + +#### Encoding + +##### Command + +| CLA | INS | P1 | P2 | +| ---- | ---- | ---- | ---- | +| 0xE0 | 0x04 | 0x00 for the first message and 0x80 for any messages after | 0x00 to display transaction hash and 0x01 to sign transaction hash | + +##### Input data + +| Length | Description | +| ---- | ---- | +| 4 | (first packet) Little endian encoded uint32 key index | +| 2 | (first packet) Little endian encoded uint16 signature index | +| 4 | (first packet) Little endian encoded uint32 change index | +| At most 255-4-2-4=245 bytes for the first packet and 255 thereafter | Sia-encoded transaction | + +##### Output data + +For transaction hash + +| Length | Description | +| ---- | ---- | +| 32 | Binary encoded transaction hash | + +For transaction signature + +| Length | Description | +| ---- | ---- | +| 64 | Binary encoded transaction signature | diff --git a/glyphs/icon_back.gif b/glyphs/icon_back.gif deleted file mode 100644 index a2a7e6d..0000000 Binary files a/glyphs/icon_back.gif and /dev/null differ diff --git a/glyphs/icon_certificate.gif b/glyphs/icon_certificate.gif deleted file mode 100644 index 89b529f..0000000 Binary files a/glyphs/icon_certificate.gif and /dev/null differ diff --git a/glyphs/icon_crossmark.gif b/glyphs/icon_crossmark.gif deleted file mode 100644 index 2dcf9d9..0000000 Binary files a/glyphs/icon_crossmark.gif and /dev/null differ diff --git a/glyphs/icon_dashboard.gif b/glyphs/icon_dashboard.gif deleted file mode 100644 index 5c30551..0000000 Binary files a/glyphs/icon_dashboard.gif and /dev/null differ diff --git a/glyphs/icon_validate.gif b/glyphs/icon_validate.gif deleted file mode 100644 index ccb5cab..0000000 Binary files a/glyphs/icon_validate.gif and /dev/null differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..087a3a0 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module sialedger + +go 1.20 + +require ( + github.com/karalabe/hid v1.0.0 + gitlab.com/NebulousLabs/Sia v1.5.6 + lukechampine.com/flagg v1.1.1 +) + +require ( + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/dchest/threefish v0.0.0-20120919164726-3ecf4c494abf // indirect + gitlab.com/NebulousLabs/encoding v0.0.0-20200604091946-456c3dc907fe // indirect + gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975 // indirect + gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect + gitlab.com/NebulousLabs/merkletree v0.0.0-20200118113624-07fbf710afc4 // indirect + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..043907f --- /dev/null +++ b/go.sum @@ -0,0 +1,201 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/threefish v0.0.0-20120919164726-3ecf4c494abf h1:K5VXW9LjmJv/xhjvQcNWTdk4WOSyreil6YaubuCPeRY= +github.com/dchest/threefish v0.0.0-20120919164726-3ecf4c494abf/go.mod h1:bXVurdTuvOiJu7NHALemFe0JMvC2UmwYHW+7fcZaZ2M= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= +github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karalabe/hid v1.0.0 h1:+/CIMNXhSU/zIJgnIvBD2nKHxS/bnRHhhs9xBryLpPo= +github.com/karalabe/hid v1.0.0/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/vbauerster/mpb/v5 v5.0.3/go.mod h1:h3YxU5CSr8rZP4Q3xZPVB3jJLhWPou63lHEdr9ytH4Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xtaci/smux v1.3.3/go.mod h1:f+nYm6SpuHMy/SH0zpbvAFHT1QoMcgLOsWcFip5KfPw= +gitlab.com/NebulousLabs/Sia v1.5.6 h1:nSWon3DgGGD/8IIhPoBXfnCGQ35TwmqxD/jzGH8TZYk= +gitlab.com/NebulousLabs/Sia v1.5.6/go.mod h1:riaRk5yJmCA0jBOCXBHKeD1ePyAMPgyiqRRMxAML08A= +gitlab.com/NebulousLabs/bolt v1.4.4/go.mod h1:ZL02cwhpLNif6aruxvUMqu/Bdy0/lFY21jMFfNAA+O8= +gitlab.com/NebulousLabs/demotemutex v0.0.0-20151003192217-235395f71c40/go.mod h1:HfnnxM8isYA7FUlqS5h34XTeiBhPtcuCquVujKsn9aw= +gitlab.com/NebulousLabs/encoding v0.0.0-20200604091946-456c3dc907fe h1:vylvMCgxVPYojpQ2p536xDooW/B3znEnw58mCxrlZow= +gitlab.com/NebulousLabs/encoding v0.0.0-20200604091946-456c3dc907fe/go.mod h1:Gi3CPCauIWmGp7YrnV/mKZ8qkD/N/LrunGNc8QmsVkU= +gitlab.com/NebulousLabs/entropy-mnemonics v0.0.0-20181018051301-7532f67e3500/go.mod h1:4koft3fRXTETovKPTeX/Aggj+ajCGWCcuuBBc598Pcs= +gitlab.com/NebulousLabs/errors v0.0.0-20171229012116-7ead97ef90b8/go.mod h1:ZkMZ0dpQyWwlENaeZVBiQRjhMEZvk6VTXquzl3FOFP8= +gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975 h1:L/ENs/Ar1bFzUeKx6m3XjlmBgIUlykX9dzvp5k9NGxc= +gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975/go.mod h1:ZkMZ0dpQyWwlENaeZVBiQRjhMEZvk6VTXquzl3FOFP8= +gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 h1:dizWJqTWjwyD8KGcMOwgrkqu1JIkofYgKkmDeNE7oAs= +gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA= +gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3/go.mod h1:sleOmkovWsDEQVYXmOJhx69qheoMTmCuPYyiCFCihlg= +gitlab.com/NebulousLabs/go-upnp v0.0.0-20210414172302-67b91c9a5c03/go.mod h1:vhrHTGDh4YR7wK8Z+kRJ+x8SF/6RUM3Vb64Si5FD0L8= +gitlab.com/NebulousLabs/log v0.0.0-20200529173103-40b250c2d92c/go.mod h1:qOhJbQ7Vzw+F+RCVmpPZ7WAwBIM9PZv4tWKp6Kgd9CY= +gitlab.com/NebulousLabs/log v0.0.0-20200604091839-0ba4a941cdc2/go.mod h1:qOhJbQ7Vzw+F+RCVmpPZ7WAwBIM9PZv4tWKp6Kgd9CY= +gitlab.com/NebulousLabs/merkletree v0.0.0-20200118113624-07fbf710afc4 h1:iuNdBfBg0umjOvrEf9MxGzK+NwAyE2oCZjDqUx9zVFs= +gitlab.com/NebulousLabs/merkletree v0.0.0-20200118113624-07fbf710afc4/go.mod h1:0cjDwhA+Pv9ZQXHED7HUSS3sCvo2zgsoaMgE7MeGBWo= +gitlab.com/NebulousLabs/monitor v0.0.0-20191205095550-2b0fd3e1012a/go.mod h1:QxXtb5hIp2xQkfb+lzBDIqQIGEj22U7AkYCXO3hkhqc= +gitlab.com/NebulousLabs/persist v0.0.0-20200605115618-007e5e23d877/go.mod h1:KT2SgNX75xjMIQdDi3Rf3tcDWsX/D289R65Ss/7lKBg= +gitlab.com/NebulousLabs/ratelimit v0.0.0-20200811080431-99b8f0768b2e/go.mod h1:HVrehlTxX2hYjsrL1k0WK43OZ0NGZfGvqzPL+n0/zrM= +gitlab.com/NebulousLabs/siamux v0.0.0-20200723083235-f2c35a421446/go.mod h1:B0RyynPElUG2Y2CAVIIRriIqR9qht2I+nDisi3gfKn0= +gitlab.com/NebulousLabs/siamux v0.0.0-20210301103357-b3e1da6f3ee1/go.mod h1:Edr1el/xhHsWOYPBrm6oR1WLkU8YX1QRcY+efONUxiw= +gitlab.com/NebulousLabs/threadgroup v0.0.0-20200527092543-afa01960408c/go.mod h1:av52iTyGuPtGU+GMcqfGtZu2vxhIjPgrxvIwVYelEvs= +gitlab.com/NebulousLabs/threadgroup v0.0.0-20200608151952-38921fbef213/go.mod h1:vIutAvl7lmJqLVYTCBY5WDdJomP+V74At8LCeEYoH8w= +gitlab.com/NebulousLabs/writeaheadlog v0.0.0-20200618142844-c59a90f49130/go.mod h1:SxigdS5Q1ui+OMgGAXt1E/Fg3RB6PvKXMov2O3gvIzs= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191105034135-c7e5f84aec59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/flagg v1.1.1 h1:jB5oL4D5zSUrzm5og6dDEi5pnrTF1poKfC7KE1lLsqc= +lukechampine.com/flagg v1.1.1/go.mod h1:a9ZuZu5LSPXELWSJrabRD00ort+lDXSOQu34xWgEoDI= diff --git a/nanos2_app_sia.gif b/nanos2_app_sia.gif new file mode 100644 index 0000000..340c99a Binary files /dev/null and b/nanos2_app_sia.gif differ diff --git a/nanos_app_sia.gif b/nanos_app_sia.gif index f3cad3f..4b6c2f6 100644 Binary files a/nanos_app_sia.gif and b/nanos_app_sia.gif differ diff --git a/sialedger.go b/sialedger.go index 8761171..6d3567a 100644 --- a/sialedger.go +++ b/sialedger.go @@ -318,15 +318,18 @@ func OpenNanoHID() (*Nano, error) { ledgerVendorID = 0x2c97 ledgerNanoSProductID = 0x0001 ledgerNanoXProductID = 0x0004 + ledgerStaxProductID = 0x0006 ) // search for Nano S nanoSDevices := hid.Enumerate(ledgerVendorID, ledgerNanoSProductID) // search for Nano X nanoXDevices := hid.Enumerate(ledgerVendorID, ledgerNanoXProductID) - if len(nanoSDevices) == 0 && len(nanoXDevices) == 0 { - return nil, errors.New("Nano not detected") - } else if len(nanoSDevices) > 1 || len(nanoXDevices) > 1 { + // search for Stax + staxDevices := hid.Enumerate(ledgerVendorID, ledgerStaxProductID) + if len(nanoSDevices) == 0 && len(nanoXDevices) == 0 && len(staxDevices) == 0 { + return nil, errors.New("Device not detected") + } else if len(nanoSDevices) > 1 || len(nanoXDevices) > 1 || len(staxDevices) > 1 { return nil, errors.New("Unexpected error -- Is the Sia wallet app running?") } @@ -336,7 +339,10 @@ func OpenNanoHID() (*Nano, error) { device, err = nanoSDevices[0].Open() } else if len(nanoXDevices) > 0 { device, err = nanoXDevices[0].Open() + } else if len(staxDevices) > 0 { + device, err = staxDevices[0].Open() } + if err != nil { return nil, err } diff --git a/src/app_main.c b/src/app_main.c new file mode 100644 index 0000000..bbb3c4b --- /dev/null +++ b/src/app_main.c @@ -0,0 +1,291 @@ +/******************************************************************************* + * + * (c) 2016 Ledger + * (c) 2018 Nebulous + * + * 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. + ********************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "blake2b.h" +#include "sia.h" +#include "sia_ux.h" + +// These are global variables declared in ux.h. They can't be defined there +// because multiple files include ux.h; they need to be defined in exactly one +// place. See ux.h for their descriptions. +commandContext global; +const internalStorage_t N_storage_real; + +void ui_idle(void); +void ui_menu_about(void); + +static void update_blind_sign_ui(void); + +static void toggle_blind_sign(void) { + const bool new_value = !N_storage.blindSign; + nvm_write((void *) &N_storage.blindSign, (void *) &new_value, sizeof(bool)); + update_blind_sign_ui(); +} + +#ifdef HAVE_BAGL +static char BLIND_SIGNING_MESSAGE[22] = {0}; + +UX_STEP_NOCB(ux_menu_ready_step, nn, {"Awaiting", "commands"}); +UX_STEP_CB(ux_menu_about_step, pn, ui_menu_about(), {&C_icon_certificate, "About"}); +UX_STEP_VALID(ux_menu_exit_step, pn, os_sched_exit(0), {&C_icon_dashboard, "Quit"}); + +// flow for the main menu: +// #1 screen: ready +// #2 screen: about submenu +// #3 screen: quit +UX_FLOW(ux_menu_main_flow, &ux_menu_ready_step, &ux_menu_about_step, &ux_menu_exit_step, FLOW_LOOP); + +UX_STEP_NOCB(ux_menu_version_step, bn, {"Version", APPVERSION}); +UX_STEP_NOCB(ux_menu_developer_step, bn, {"Developer", APPDEVELOPER}); +UX_STEP_CB(ux_menu_blind_sign_step, + pn, + toggle_blind_sign(), + {&C_icon_certificate, BLIND_SIGNING_MESSAGE}); +UX_STEP_CB(ux_menu_back_step, pb, ui_idle(), {&C_icon_back, "Back"}); + +// flow for the about submenu: +// #1 screen: app version +// #2 screen: blind sign setting +// #3 screen: back button +UX_FLOW(ux_menu_about_flow, + &ux_menu_version_step, + &ux_menu_developer_step, + &ux_menu_blind_sign_step, + &ux_menu_back_step, + FLOW_LOOP); + +void ui_idle(void) { + if (G_ux.stack_count == 0) { + ux_stack_push(); + } + + ux_flow_init(0, ux_menu_main_flow, NULL); +} + +void ui_menu_about(void) { + ux_flow_init(0, ux_menu_about_flow, NULL); +} + +static void update_blind_sign_ui(void) { + if (N_storage.blindSign) { + memcpy(BLIND_SIGNING_MESSAGE, "Disable blind signing", sizeof(BLIND_SIGNING_MESSAGE)); + } else { + memcpy(BLIND_SIGNING_MESSAGE, "Enable blind signing", sizeof(BLIND_SIGNING_MESSAGE)); + } + ui_idle(); +} + +#else + +static void controls_callback(int token, uint8_t index, int page); + +#define SETTING_INFO_NB 2 +static const char *const INFO_TYPES[SETTING_INFO_NB] = {"Version", "Developer"}; +static const char *const INFO_CONTENTS[SETTING_INFO_NB] = {APPVERSION, APPDEVELOPER}; + +// settings switches definitions +enum { BLIND_SIGNING_TOKEN = FIRST_USER_TOKEN }; +enum { BLIND_SIGNING_ID = 0, SETTINGS_SWITCHES_NB }; + +static nbgl_contentSwitch_t switches[SETTINGS_SWITCHES_NB] = {0}; + +static const nbgl_contentInfoList_t infoList = { + .nbInfos = SETTING_INFO_NB, + .infoTypes = INFO_TYPES, + .infoContents = INFO_CONTENTS, +}; + +// settings menu definition +#define SETTING_CONTENTS_NB 1 +static const nbgl_content_t contents[SETTING_CONTENTS_NB] = { + {.type = SWITCHES_LIST, + .content.switchesList.nbSwitches = SETTINGS_SWITCHES_NB, + .content.switchesList.switches = switches, + .contentActionCallback = controls_callback}}; + +static const nbgl_genericContents_t settingContents = {.callbackCallNeeded = false, + .contentsList = contents, + .nbContents = SETTING_CONTENTS_NB}; + +static void controls_callback(int token, uint8_t index, int page) { + UNUSED(index); + UNUSED(page); + if (token == BLIND_SIGNING_TOKEN) { + toggle_blind_sign(); + } +} + +static void update_blind_sign_ui(void) { + switches[BLIND_SIGNING_ID].initState = N_storage.blindSign ? ON_STATE : OFF_STATE; +} + +void app_quit(void) { + // exit app here + os_sched_exit(-1); +} + +void ui_idle(void) { + switches[BLIND_SIGNING_ID].text = "Enable blind signing"; + switches[BLIND_SIGNING_ID].subText = "Recommend only for experienced users"; + switches[BLIND_SIGNING_ID].token = BLIND_SIGNING_TOKEN; + switches[BLIND_SIGNING_ID].tuneId = TUNE_TAP_CASUAL; + + nbgl_useCaseHomeAndSettings(APPNAME, + &C_stax_app_sia_big, + NULL, + INIT_HOME_PAGE, + &settingContents, + &infoList, + NULL, + app_quit); +} + +#endif + +unsigned int io_reject(void) { + io_send_sw(SW_USER_REJECTED); + // Return to the main screen. + ui_idle(); + return 0; +} + +// The APDU protocol uses a single-byte instruction code (INS) to specify +// which command should be executed. We'll use this code to dispatch on a +// table of function pointers. +#define INS_GET_VERSION 0x01 +#define INS_GET_PUBLIC_KEY 0x02 +#define INS_SIGN_HASH 0x04 +#define INS_GET_TXN_HASH 0x08 + +// This is the function signature for a command handler. +// Returns 0 on success. +typedef uint16_t handler_fn_t(uint8_t p1, uint8_t p2, uint8_t *dataBuffer, uint16_t dataLength); + +handler_fn_t handleGetVersion; +handler_fn_t handleGetPublicKey; +handler_fn_t handleSignHash; +handler_fn_t handleCalcTxnHash; + +static handler_fn_t *lookupHandler(uint8_t ins) { + switch (ins) { + case INS_GET_VERSION: + return handleGetVersion; + case INS_GET_PUBLIC_KEY: + return handleGetPublicKey; + case INS_SIGN_HASH: + return handleSignHash; + case INS_GET_TXN_HASH: + return handleCalcTxnHash; + default: + return NULL; + } +} + +// These are the offsets of various parts of a request APDU packet. INS +// identifies the requested command (see above), and P1 and P2 are parameters +// to the command. +#define CLA 0xE0 +#define OFFSET_CLA 0x00 +#define OFFSET_INS 0x01 +#define OFFSET_P1 0x02 +#define OFFSET_P2 0x03 +#define OFFSET_LC 0x04 +#define OFFSET_CDATA 0x05 + +void send_error_code(uint16_t e) { + // Convert the exception to a response code. All error codes + // start with 6, except for 0x9000, which is a special + // "success" code. Every APDU payload should end with such a + // code, even if no other data is sent. For example, when + // calcTxnHash is processing packets of txn data, it replies + // with just 0x9000 to indicate that it is ready to receive + // more data. + // + // If the first byte is not a 6, mask it with 0x6800 to + // convert it to a proper error code. I'm not totally sure why + // this is done; perhaps to handle single-byte exception + // codes? + short sw = 0; + switch (e & 0xF000) { + case 0x6000: + case 0x9000: + sw = e; + break; + default: + sw = 0x6800 | (e & 0x7FF); + break; + } + io_send_sw(sw); +} + +void app_main() { + // Mark the transaction context as uninitialized. + explicit_bzero(&global, sizeof(global)); + + // Initialize io + io_init(); + + if (!N_storage.initialized) { + internalStorage_t storage = {0}; + storage.initialized = true; + nvm_write((void *) &N_storage, (void *) &storage, sizeof(internalStorage_t)); + } + update_blind_sign_ui(); + ui_idle(); + + int input_len = 0; + command_t cmd = {0}; + for (;;) { + // Read command into G_io_apdu_buffer + if ((input_len = io_recv_command()) < 0) { + PRINTF("Failed to receive"); + io_send_sw(SW_INVALID_PARAM); + continue; + } + + // Parse command into CLA, INS, P1/P2, LC, and data + if (!apdu_parser(&cmd, G_io_apdu_buffer, input_len)) { + PRINTF("Invalid command length"); + io_send_sw(SW_INVALID_PARAM); + continue; + } + + // Lookup and call the requested command handler. + handler_fn_t *handlerFn = lookupHandler(cmd.ins); + if (!handlerFn) { + PRINTF("Instruction not supported"); + send_error_code(SW_INS_NOT_SUPPORTED); + continue; + } + + const uint16_t e = handlerFn(cmd.p1, cmd.p2, cmd.data, cmd.lc); + if (e != 0) { + send_error_code(e); + continue; + } + } +} diff --git a/src/blake2b.c b/src/blake2b.c index f845d8f..1ffa038 100644 --- a/src/blake2b.c +++ b/src/blake2b.c @@ -1,25 +1,36 @@ +#include "blake2b.h" + +#include #include -#include #include -#include + +#include "sia.h" void blake2b_init(cx_blake2b_t *S) { - cx_blake2b_init(S, 256); + LEDGER_ASSERT(CX_OK == cx_blake2b_init_no_throw(S, 256), "blake2b_init failed"); } -void blake2b_update(cx_blake2b_t *S, const uint8_t *in, uint64_t inlen) { - cx_hash((cx_hash_t *)S, 0, in, inlen, NULL, 0); +void blake2b_update(cx_blake2b_t *S, const uint8_t *in, uint16_t inlen) { + LEDGER_ASSERT(CX_OK == cx_hash_no_throw((cx_hash_t *) S, 0, in, inlen, NULL, 0), + "blake2b_update failed"); } -void blake2b_final(cx_blake2b_t *S, uint8_t *out, uint64_t outlen) { - uint8_t buf[32]; - cx_hash((cx_hash_t *)S, CX_LAST, NULL, 0, buf, sizeof(buf)); - memmove(out, buf, outlen); +void blake2b_final(cx_blake2b_t *S, uint8_t *out, uint16_t outlen) { + if (outlen < 32) { + uint8_t buf[32] = {0}; + LEDGER_ASSERT( + CX_OK == cx_hash_no_throw((cx_hash_t *) S, CX_LAST, NULL, 0, buf, sizeof(buf)), + "blake2b_final failed"); + memmove(out, buf, outlen); + } else { + LEDGER_ASSERT(CX_OK == cx_hash_no_throw((cx_hash_t *) S, CX_LAST, NULL, 0, out, outlen), + "blake2b_final failed"); + } } -void blake2b(uint8_t *out, uint64_t outlen, const uint8_t *in, uint64_t inlen) { - cx_blake2b_t S; - blake2b_init(&S); - blake2b_update(&S, in, inlen); - blake2b_final(&S, out, outlen); +void blake2b(uint8_t *out, uint16_t outlen, const uint8_t *in, uint16_t inlen) { + cx_blake2b_t S; + blake2b_init(&S); + blake2b_update(&S, in, inlen); + blake2b_final(&S, out, outlen); } diff --git a/src/blake2b.h b/src/blake2b.h index ab342d1..eafb5db 100644 --- a/src/blake2b.h +++ b/src/blake2b.h @@ -1,12 +1,17 @@ -#include "os.h" -#include "cx.h" +#ifndef BLAKE2B_H +#define BLAKE2B_H + +#include +#include // blake2b_init initializes a 256-bit unkeyed BLAKE2B hash. void blake2b_init(cx_blake2b_t *S); // blake2b_update adds data to a BLAKE2B hash. -void blake2b_update(cx_blake2b_t *S, const uint8_t *in, uint64_t inlen); +void blake2b_update(cx_blake2b_t *S, const uint8_t *in, uint16_t inlen); // blake2b_final outputs a finalized BLAKE2B hash. -void blake2b_final(cx_blake2b_t *S, uint8_t *out, uint64_t outlen); +void blake2b_final(cx_blake2b_t *S, uint8_t *out, uint16_t outlen); // blake2b is a helper function that outputs the BLAKE2B hash of in. -void blake2b(uint8_t *out, uint64_t outlen, const uint8_t *in, uint64_t inlen); +void blake2b(uint8_t *out, uint16_t outlen, const uint8_t *in, uint16_t inlen); + +#endif /* BLAKE2B_H */ \ No newline at end of file diff --git a/src/calcTxnHash.c b/src/calcTxnHash.c index 9a2b5b2..c4079bc 100644 --- a/src/calcTxnHash.c +++ b/src/calcTxnHash.c @@ -1,3 +1,4 @@ +#ifdef HAVE_BAGL // This file contains the implementation of the calcTxnHash command. It is // significantly more complicated than the other commands, mostly due to the // transaction parsing. @@ -19,11 +20,14 @@ // // Keep this description in mind as you read through the implementation. -#include +#include +#include #include +#include #include -#include #include +#include + #include "blake2b.h" #include "sia.h" #include "sia_ux.h" @@ -31,366 +35,237 @@ static calcTxnHashContext_t *ctx = &global.calcTxnHashContext; +static void fmtTxnElem(void); +static uint16_t display_index(void); static unsigned int ui_calcTxnHash_elem_button(void); static unsigned int io_seproxyhal_touch_txn_hash_ok(void); -UX_STEP_CB( - ux_compare_hash_flow_1_step, - bnnn_paging, - ui_idle(), - { - "Compare Hash:", - global.calcTxnHashContext.fullStr - } -); +UX_STEP_CB(ux_compare_hash_flow_1_step, + bnnn_paging, + ui_idle(), + {"Compare Hash:", global.calcTxnHashContext.fullStr[0]}); -UX_FLOW( - ux_compare_hash_flow, - &ux_compare_hash_flow_1_step -); +UX_FLOW(ux_compare_hash_flow, &ux_compare_hash_flow_1_step); -UX_STEP_NOCB( - ux_sign_txn_flow_1_step, - nn, - { - "Sign this txn", - global.calcTxnHashContext.fullStr - } -); +UX_STEP_NOCB(ux_sign_txn_flow_1_step, nn, {"Sign this txn", global.calcTxnHashContext.fullStr[0]}); -UX_STEP_VALID( - ux_sign_txn_flow_2_step, - pb, - io_seproxyhal_touch_txn_hash_ok(), - { - &C_icon_validate, - "Approve" - } -); +UX_STEP_VALID(ux_sign_txn_flow_2_step, + pb, + io_seproxyhal_touch_txn_hash_ok(), + {&C_icon_validate_14, "Approve"}); -UX_STEP_VALID( - ux_sign_txn_flow_3_step, - pb, - io_seproxyhal_cancel(), - { - &C_icon_crossmark, - "Reject" - } -); +UX_STEP_VALID(ux_sign_txn_flow_3_step, pb, io_reject(), {&C_icon_crossmark, "Reject"}); // Flow for the signing transaction menu: // #1 screen: "Sign this txn?" // #2 screen: approve // #3 screen: reject -UX_FLOW( - ux_sign_txn_flow, - &ux_sign_txn_flow_1_step, - &ux_sign_txn_flow_2_step, - &ux_sign_txn_flow_3_step -); +UX_FLOW(ux_sign_txn_flow, + &ux_sign_txn_flow_1_step, + &ux_sign_txn_flow_2_step, + &ux_sign_txn_flow_3_step); // We use one generic step for each element so we don't have to make // separate UX_FLOWs for SC outputs, SF outputs, miner fees, etc -UX_STEP_CB( - ux_show_txn_elem_1_step, - bnnn_paging, - ui_calcTxnHash_elem_button(), - { - global.calcTxnHashContext.labelStr, - global.calcTxnHashContext.fullStr - } -); +UX_STEP_CB(ux_show_txn_elem_1_step, + bnnn_paging, + ui_calcTxnHash_elem_button(), + {global.calcTxnHashContext.labelStr, global.calcTxnHashContext.fullStr[0]}); // For each element of the transaction (sc outputs, sf outputs, miner fees), // we show the data paginated for confirmation purposes. When the user // confirms that element, they are shown the next element until // they finish all the elements and are given the option to approve/reject. -UX_FLOW( - ux_show_txn_elem_flow, - &ux_show_txn_elem_1_step -); - +UX_FLOW(ux_show_txn_elem_flow, &ux_show_txn_elem_1_step); static unsigned int io_seproxyhal_touch_txn_hash_ok(void) { - deriveAndSign(G_io_apdu_buffer, ctx->keyIndex, ctx->txn.sigHash); - io_exchange_with_code(SW_OK, 64); - ui_idle(); - return 0; + uint8_t signature[64] = {0}; + deriveAndSign(signature, ctx->keyIndex, ctx->txn.sigHash); + io_send_response_pointer(signature, sizeof(signature), SW_OK); + ui_idle(); + return 0; } +static unsigned int ui_calcTxnHash_elem_button(void) { + if (ctx->elementIndex >= ctx->txn.elementIndex) { + // We've finished decoding the transaction, and all elements have + // been displayed. + if (ctx->sign) { + // If we're signing the transaction, prepare and display the + // approval screen. + memmove(ctx->fullStr[0], "with key #", 10); + memmove(ctx->fullStr[0] + 10 + (bin2dec(ctx->fullStr[0] + 10, ctx->keyIndex)), "?", 2); + ux_flow_init(0, ux_sign_txn_flow, NULL); + } else { + // If we're just computing the hash, send it immediately and + // display the comparison screen + io_send_response_pointer(ctx->txn.sigHash, sizeof(ctx->txn.sigHash), SW_OK); + bin2hex(ctx->fullStr[0], ctx->txn.sigHash, sizeof(ctx->txn.sigHash)); + ux_flow_init(0, ux_compare_hash_flow, NULL); + } + // Reset the initialization state. + ctx->elementIndex = 0; + ctx->initialized = false; + return 0; + } + + fmtTxnElem(); + ux_flow_init(0, ux_show_txn_elem_flow, NULL); + return 0; +} + +// Gets the current index number to be displayed in the UI +static uint16_t display_index(void) { + txn_state_t *txn = &ctx->txn; + uint16_t first_index_of_type = 0; + const txnElemType_e current_type = txn->elements[ctx->elementIndex].elemType; + for (uint16_t i = 0; i < txn->elementIndex; i++) { + if (current_type == txn->elements[i].elemType) { + first_index_of_type = i; + break; + } + } + return ctx->elementIndex - first_index_of_type + 1; +} // This is a helper function that prepares an element of the transaction for // display. It stores the type of the element in labelStr, and a human- // readable representation of the element in fullStr. As in previous screens, // partialStr holds the visible portion of fullStr. -static void fmtTxnElem(calcTxnHashContext_t *ctx) { - txn_state_t *txn = &ctx->txn; +static void fmtTxnElem(void) { + txn_state_t *txn = &ctx->txn; - switch (txn->elemType) { - case TXN_ELEM_SC_OUTPUT: - memmove(ctx->labelStr, "SC Output #", 11); - bin2dec(ctx->labelStr+11, txn->displayIndex); - // An element can have multiple screens. For each siacoin output, the - // user needs to see both the destination address and the amount. - // These are rendered in separate screens, and elemPart is used to - // identify which screen is being viewed. - if (ctx->elemPart == 0) { - memmove(ctx->fullStr, txn->outAddr, sizeof(txn->outAddr)); - ctx->elemPart++; - } else { - memmove(ctx->fullStr, txn->outVal, sizeof(txn->outVal)); - formatSC(ctx->fullStr, txn->valLen); - ctx->elemPart = 0; - } - break; + switch (txn->elements[ctx->elementIndex].elemType) { + case TXN_ELEM_SC_OUTPUT: { + memmove(ctx->labelStr, "SC Output #", 11); + bin2dec(ctx->labelStr + 11, display_index()); + // An element can have multiple screens. For each siacoin output, the + // user needs to see both the destination address and the amount. + // These are rendered in separate screens, and elemPart is used to + // identify which screen is being viewed. + if (ctx->elemPart == 0) { + format_address(ctx->fullStr[0], txn->elements[ctx->elementIndex].outAddr); + ctx->elemPart++; + } else { + const uint8_t valLen = + cur2dec(ctx->fullStr[0], txn->elements[ctx->elementIndex].outVal); + formatSC(ctx->fullStr[0], valLen); + ctx->elemPart = 0; - case TXN_ELEM_SF_OUTPUT: - memmove(ctx->labelStr, "SF Output #", 11); - bin2dec(ctx->labelStr+11, txn->displayIndex); - if (ctx->elemPart == 0) { - memmove(ctx->fullStr, txn->outAddr, sizeof(txn->outAddr)); - ctx->elemPart++; - } else { - memmove(ctx->fullStr, txn->outVal, sizeof(txn->outVal)); - memmove(ctx->fullStr+txn->valLen, " SF", 4); - ctx->elemPart = 0; - } - break; + ctx->elementIndex++; + } + break; + } + case TXN_ELEM_SF_OUTPUT: { + memmove(ctx->labelStr, "SF Output #", 11); + bin2dec(ctx->labelStr + 11, display_index()); + if (ctx->elemPart == 0) { + format_address(ctx->fullStr[0], txn->elements[ctx->elementIndex].outAddr); + ctx->elemPart++; + } else { + const uint8_t valLen = + cur2dec(ctx->fullStr[0], txn->elements[ctx->elementIndex].outVal); + memmove(ctx->fullStr[0] + valLen, " SF", 4); + ctx->elemPart = 0; - case TXN_ELEM_MINER_FEE: - // Miner fees only have one part. - memmove(ctx->labelStr, "Miner Fee #", 11); - bin2dec(ctx->labelStr+11, txn->sliceIndex); - memmove(ctx->fullStr, txn->outVal, sizeof(txn->outVal)); - formatSC(ctx->fullStr, txn->valLen); - ctx->elemPart = 0; - break; + ctx->elementIndex++; + } + break; + } + case TXN_ELEM_MINER_FEE: { + // Miner fees only have one part. + memmove(ctx->labelStr, "Miner Fee #", 11); + bin2dec(ctx->labelStr + 11, display_index()); - default: - // This should never happen. - io_exchange_with_code(SW_DEVELOPER_ERR, 0); - ui_idle(); - break; - } -} + const uint8_t valLen = + cur2dec(ctx->fullStr[0], txn->elements[ctx->elementIndex].outVal); + formatSC(ctx->fullStr[0], valLen); -static unsigned int ui_calcTxnHash_elem_button(void) { - if (ctx->elemPart > 0) { - // We're in the middle of displaying a multi-part element; display - // the next part. - fmtTxnElem(ctx); - ux_flow_init(0, ux_show_txn_elem_flow, NULL); - return 0; - } - // Attempt to decode the next element in the transaction. - switch (txn_next_elem(&ctx->txn)) { - case TXN_STATE_ERR: - // The transaction is invalid. - io_exchange_with_code(SW_INVALID_PARAM, 0); - ui_idle(); - break; - case TXN_STATE_PARTIAL: - // We don't have enough data to decode the next element; send an - // OK code to request more. - io_exchange_with_code(SW_OK, 0); - break; - case TXN_STATE_READY: - // We successively decoded one or more elements; display the first - // part of the first element. - ctx->elemPart = 0; - fmtTxnElem(ctx); - ux_flow_init(0, ux_show_txn_elem_flow, NULL); - break; - case TXN_STATE_FINISHED: - // We've finished decoding the transaction, and all elements have - // been displayed. - if (ctx->sign) { - // If we're signing the transaction, prepare and display the - // approval screen. - memmove(ctx->fullStr, "with key #", 10); - memmove(ctx->fullStr+10+(bin2dec(ctx->fullStr+10, ctx->keyIndex)), "?", 2); - ux_flow_init(0, ux_sign_txn_flow, NULL); - } else { - // If we're just computing the hash, send it immediately and - // display the comparison screen - memmove(G_io_apdu_buffer, ctx->txn.sigHash, 32); - io_exchange_with_code(SW_OK, 32); - bin2hex(ctx->fullStr, ctx->txn.sigHash, sizeof(ctx->txn.sigHash)); - ux_flow_init(0, ux_compare_hash_flow, NULL); - } - // Reset the initialization state. - ctx->initialized = false; - break; - } - return 0; + ctx->elemPart = 0; + ctx->elementIndex++; + break; + } + default: { + // This should never happen. + io_send_sw(SW_DEVELOPER_ERR); + ui_idle(); + break; + } + } } -// APDU parameters -#define P1_FIRST 0x00 // 1st packet of multi-packet transfer -#define P1_MORE 0x80 // nth packet of multi-packet transfer -#define P2_DISPLAY_HASH 0x00 // display transaction hash -#define P2_SIGN_HASH 0x01 // sign transaction hash +static void zero_ctx(void) { + explicit_bzero(ctx, sizeof(calcTxnHashContext_t)); +} // handleCalcTxnHash reads a signature index and a transaction, calculates the // SigHash of the transaction, and optionally signs the hash using a specified -// key. The transaction is processed in a streaming fashion and displayed -// piece-wise to the user. -void handleCalcTxnHash(uint8_t p1, uint8_t p2, uint8_t *dataBuffer, uint16_t dataLength, volatile unsigned int *flags, volatile unsigned int *tx __attribute__((unused))) { - if ((p1 != P1_FIRST && p1 != P1_MORE) || (p2 != P2_DISPLAY_HASH && p2 != P2_SIGN_HASH)) { - THROW(SW_INVALID_PARAM); - } +// key. The transaction is displayed piece-wise to the user. +uint16_t handleCalcTxnHash(uint8_t p1, uint8_t p2, uint8_t *dataBuffer, uint16_t dataLength) { + if ((p1 != P1_FIRST && p1 != P1_MORE) || (p2 != P2_DISPLAY_HASH && p2 != P2_SIGN_HASH)) { + return SW_INVALID_PARAM; + } + + if (p1 == P1_FIRST) { + // If this is the first packet of a transaction, the transaction + // context must not already be initialized. (Otherwise, an attacker + // could fool the user by concatenating two transactions.) + // + // NOTE: ctx->initialized is set to false when the Sia app loads. + if (ctx->initialized) { + zero_ctx(); + return SW_IMPROPER_INIT; + } + zero_ctx(); + ctx->initialized = true; - if (p1 == P1_FIRST) { - // If this is the first packet of a transaction, the transaction - // context must not already be initialized. (Otherwise, an attacker - // could fool the user by concatenating two transactions.) - // - // NOTE: ctx->initialized is set to false when the Sia app loads. - if (ctx->initialized) { - THROW(SW_IMPROPER_INIT); - } - ctx->initialized = true; + // If this is the first packet, it will include the key index, sig + // index, and change index in addition to the transaction data. Use + // these to initialize the ctx and the transaction decoder. + ctx->keyIndex = U4LE(dataBuffer, 0); // NOTE: ignored if !ctx->sign + dataBuffer += 4; + dataLength -= 4; + uint16_t sigIndex = U2LE(dataBuffer, 0); + dataBuffer += 2; + dataLength -= 2; + uint32_t changeIndex = U4LE(dataBuffer, 0); + dataBuffer += 4; + dataLength -= 4; + txn_init(&ctx->txn, sigIndex, changeIndex); - // If this is the first packet, it will include the key index, sig - // index, and change index in addition to the transaction data. Use - // these to initialize the ctx and the transaction decoder. - ctx->keyIndex = U4LE(dataBuffer, 0); // NOTE: ignored if !ctx->sign - dataBuffer += 4; dataLength -= 4; - uint16_t sigIndex = U2LE(dataBuffer, 0); - dataBuffer += 2; dataLength -= 2; - uint32_t changeIndex = U4LE(dataBuffer, 0); - dataBuffer += 4; dataLength -= 4; - txn_init(&ctx->txn, sigIndex, changeIndex); + // Set ctx->sign according to P2. + ctx->sign = (p2 & P2_SIGN_HASH); - // Set ctx->sign according to P2. - ctx->sign = (p2 & P2_SIGN_HASH); + ctx->elemPart = 0; + } else { + // If this is not P1_FIRST, the transaction must have been + // initialized previously. + if (!ctx->initialized) { + zero_ctx(); + return SW_IMPROPER_INIT; + } + } - ctx->elemPart = 0; - } else { - // If this is not P1_FIRST, the transaction must have been - // initialized previously. - if (!ctx->initialized) { - THROW(SW_IMPROPER_INIT); - } - } + // Add the new data to transaction decoder. + txn_update(&ctx->txn, dataBuffer, dataLength); - // Add the new data to transaction decoder. - txn_update(&ctx->txn, dataBuffer, dataLength); + // Attempt to decode the next element of the transaction. Note that this + // code is essentially identical to ui_calcTxnHash_elem_button. Sadly, + // there doesn't seem to be a clean way to avoid this duplication. + switch (txn_parse(&ctx->txn)) { + case TXN_STATE_ERR: + // don't leave state lingering + zero_ctx(); + return SW_INVALID_PARAM; + break; + case TXN_STATE_PARTIAL: + return SW_OK; + break; + case TXN_STATE_FINISHED: + fmtTxnElem(); + ux_flow_init(0, ux_show_txn_elem_flow, NULL); + break; + } - // Attempt to decode the next element of the transaction. Note that this - // code is essentially identical to ui_calcTxnHash_elem_button. Sadly, - // there doesn't seem to be a clean way to avoid this duplication. - switch (txn_next_elem(&ctx->txn)) { - case TXN_STATE_ERR: - THROW(SW_INVALID_PARAM); - case TXN_STATE_PARTIAL: - THROW(SW_OK); - case TXN_STATE_READY: - ctx->elemPart = 0; - fmtTxnElem(ctx); - ux_flow_init(0, ux_show_txn_elem_flow, NULL); - *flags |= IO_ASYNCH_REPLY; - break; - case TXN_STATE_FINISHED: - if (ctx->sign) { - memmove(ctx->fullStr, "with key #", 10); - bin2dec(ctx->fullStr+10, ctx->keyIndex); - memmove(ctx->fullStr+10+(bin2dec(ctx->fullStr+10, ctx->keyIndex)), "?", 2); - ux_flow_init(0, ux_sign_txn_flow, NULL); - *flags |= IO_ASYNCH_REPLY; - } else { - memmove(G_io_apdu_buffer, ctx->txn.sigHash, 32); - io_exchange_with_code(SW_OK, 32); - bin2hex(ctx->fullStr, ctx->txn.sigHash, sizeof(ctx->txn.sigHash)); - ux_flow_init(0, ux_compare_hash_flow, NULL); - // The above code does something strange: it calls io_exchange - // directly from the command handler. You might wonder: why not - // just prepare the APDU buffer and let sia_main call io_exchange? - // The answer, surprisingly, is that we also need to call - // UX_DISPLAY, and UX_DISPLAY affects io_exchange in subtle ways. - // To understand why, we'll need to dive deep into the Nano S - // firmware. I recommend that you don't skip this section, even - // though it's lengthy, because it will save you a lot of - // frustration when you go "off the beaten path" in your own app. - // - // Recall that the Nano S has two chips. Your app (and the Ledger - // OS, BOLOS) runs on the Secure Element. The SE is completely - // self-contained; it doesn't talk to the outside world at all. It - // only talks to the other chip, the MCU. The MCU is what - // processes button presses, renders things on screen, and - // exchanges APDU packets with the computer. The communication - // layer between the SE and the MCU is called SEPROXYHAL. There - // are some nice diagrams in the "Hardware Architecture" section - // of Ledger's docs that will help you visualize all this. - // - // The SEPROXYHAL protocol, like any communication protocol, - // specifies exactly when each party is allowed to talk. - // Communication happens in a loop: first the MCU sends an Event, - // then the SE replies with zero or more Commands, and finally the - // SE sends a Status to indicate that it has finished processing - // the Event, completing one iteration: - // - // Event -> Commands -> Status -> Event -> Commands -> ... - // - // For our purposes, an "Event" is a request APDU, and a "Command" - // is a response APDU. (There are other types of Events and - // Commands, such as button presses, but they aren't relevant - // here.) As for the Status, there is a "General" Status and a - // "Display" Status. A General Status tells the MCU to send the - // response APDU, and a Display Status tells it to render an - // element on the screen. Remember, it's "zero or more Commands," - // so it's legal to send just a Status without any Commands. - // - // You may have some picture of the problem now. Imagine we - // prepare the APDU buffer, then call UX_DISPLAY, and then let - // sia_main send the APDU with io_exchange. What happens at the - // SEPROXYHAL layer? First, UX_DISPLAY will send a Display Status. - // Then, io_exchange will send a Command and a General Status. But - // no Event was processed between the two Statuses! This causes - // SEPROXYHAL to freak out and crash, forcing you to reboot your - // Nano S. - // - // So why does calling io_exchange before UX_DISPLAY fix the - // problem? Won't we just end up sending two Statuses again? The - // secret is that io_exchange_with_code uses the - // IO_RETURN_AFTER_TX flag. Previously, the only thing we needed - // to know about IO_RETURN_AFTER_TX is that it sends a response - // APDU without waiting for the next request APDU. But it has one - // other important property: it tells io_exchange not to send a - // Status! So the only Status we send comes from UX_DISPLAY. This - // preserves the ordering required by SEPROXYHAL. - // - // Lastly: what if we prepare the APDU buffer in the handler, but - // with the IO_RETURN_AFTER_TX flag set? Will that work? - // Unfortunately not. io_exchange won't send a status, but it - // *will* send a Command containing the APDU, so we still end up - // breaking the correct SEPROXYHAL ordering. - // - // Here's a list of rules that will help you debug similar issues: - // - // - Always preserve the order: Event -> Commands -> Status - // - UX_DISPLAY sends a Status - // - io_exchange sends a Command and a Status - // - IO_RETURN_AFTER_TX makes io_exchange not send a Status - // - IO_ASYNCH_REPLY (or tx=0) makes io_exchange not send a Command - // - // Okay, that second rule isn't 100% accurate. UX_DISPLAY doesn't - // necessarily send a single Status: it sends a separate Status - // for each element you render! The reason this works is that the - // MCU replies to each Display Status with a Display Processed - // Event. That means you can call UX_DISPLAY many times in a row - // without disrupting SEPROXYHAL. Anyway, as far as we're - // concerned, it's simpler to think of UX_DISPLAY as sending just - // a single Status. - } - // Reset the initialization state. - ctx->initialized = false; - break; - } + return 0; } -// It is not necessary to completely understand this handler to write your own -// Nano S app; much of it is Sia-specific and will not generalize to other -// apps. The important part is knowing how to structure handlers that involve -// multiple APDU exchanges. If you would like to dive deeper into how the -// handler buffers transaction data and parses elements, proceed to txn.c. -// Otherwise, this concludes the walkthrough. Feel free to fork this app and -// modify it to suit your own needs. +#endif /* HAVE_BAGL */ \ No newline at end of file diff --git a/src/calcTxnHash_nbgl.c b/src/calcTxnHash_nbgl.c new file mode 100644 index 0000000..f513c04 --- /dev/null +++ b/src/calcTxnHash_nbgl.c @@ -0,0 +1,247 @@ +#ifndef HAVE_BAGL + +#include +#include +#include +#include +#include +#include +#include + +#include "blake2b.h" +#include "sia.h" +#include "sia_ux.h" +#include "txn.h" + +static calcTxnHashContext_t *ctx = &global.calcTxnHashContext; + +static void fmtTxnElem(void); +static uint16_t display_index(void); +static bool nav_callback(uint8_t page, nbgl_pageContent_t *content); +static void confirm_callback(bool confirm); + +// Gets the current index number to be displayed in the UI +static uint16_t display_index(void) { + txn_state_t *txn = &ctx->txn; + uint16_t first_index_of_type = 0; + const txnElemType_e current_type = txn->elements[ctx->elementIndex].elemType; + for (uint16_t i = 0; i < txn->elementIndex; i++) { + if (current_type == txn->elements[i].elemType) { + first_index_of_type = i; + break; + } + } + return ctx->elementIndex - first_index_of_type + 1; +} + +// This is a helper function that prepares an element of the transaction for +// display. It stores the type of the element in labelStr, and a human- +// readable representation of the element in fullStr. As in previous screens, +// partialStr holds the visible portion of fullStr. +static void fmtTxnElem(void) { + txn_state_t *txn = &ctx->txn; + + switch (txn->elements[ctx->elementIndex].elemType) { + case TXN_ELEM_SC_OUTPUT: { + memmove(ctx->labelStr, "SC Output #", 11); + bin2dec(ctx->labelStr + 11, display_index()); + // An element can have multiple screens. For each siacoin output, the + // user needs to see both the destination address and the amount. + // These are rendered in separate screens, and elemPart is used to + // identify which screen is being viewed. + format_address(ctx->fullStr[0], txn->elements[ctx->elementIndex].outAddr); + const uint8_t valLen = + cur2dec(ctx->fullStr[1], txn->elements[ctx->elementIndex].outVal); + formatSC(ctx->fullStr[1], valLen); + break; + } + + case TXN_ELEM_SF_OUTPUT: { + memmove(ctx->labelStr, "SF Output #", 11); + bin2dec(ctx->labelStr + 11, display_index()); + format_address(ctx->fullStr[0], txn->elements[ctx->elementIndex].outAddr); + cur2dec(ctx->fullStr[1], txn->elements[ctx->elementIndex].outVal); + break; + } + + case TXN_ELEM_MINER_FEE: { + // Miner fees only have one part. + memmove(ctx->labelStr, "Miner Fee #", 11); + bin2dec(ctx->labelStr + 11, display_index()); + + const uint8_t valLen = + cur2dec(ctx->fullStr[0], txn->elements[ctx->elementIndex].outVal); + formatSC(ctx->fullStr[0], valLen); + break; + } + + default: + // This should never happen. + io_send_sw(SW_DEVELOPER_ERR); + ui_idle(); + break; + } +} + +static void confirm_callback(bool confirm) { + ctx->finished = false; + ctx->initialized = false; + + if (confirm) { + if (ctx->sign) { + uint8_t signature[64] = {0}; + deriveAndSign(signature, ctx->keyIndex, ctx->txn.sigHash); + io_send_response_pointer(signature, sizeof(signature), SW_OK); + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_SIGNED, ui_idle); + } else { + io_send_response_pointer(ctx->txn.sigHash, sizeof(ctx->txn.sigHash), SW_OK); + nbgl_useCaseStatus("TRANSACTION HASHED", true, ui_idle); + } + } else { + io_send_sw(SW_USER_REJECTED); + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_REJECTED, ui_idle); + } +} + +static nbgl_layoutTagValue_t pairs[2]; + +static bool nav_callback(uint8_t page, nbgl_pageContent_t *content) { + ctx->elementIndex = page; + if (ctx->elementIndex >= ctx->txn.elementIndex) { + content->type = INFO_LONG_PRESS; + content->infoLongPress.icon = &C_stax_app_sia_big; + if (ctx->sign) { + content->infoLongPress.text = "Sign transaction"; + content->infoLongPress.longPressText = "Hold to sign"; + } else { + content->infoLongPress.text = "Hash transaction"; + content->infoLongPress.longPressText = "Hold to hash"; + } + return true; + } + + fmtTxnElem(); + + if (ctx->txn.elements[ctx->elementIndex].elemType == TXN_ELEM_MINER_FEE) { + pairs[0].item = "Miner Fee Amount (SC)"; + pairs[0].value = ctx->fullStr[0]; + + content->tagValueList.nbPairs = 1; + content->tagValueList.pairs = &pairs[0]; + } else { + pairs[0].item = "To"; + pairs[0].value = ctx->fullStr[0]; + if (ctx->txn.elements[ctx->elementIndex].elemType == TXN_ELEM_SC_OUTPUT) { + pairs[1].item = "Amount (SC)"; + } else { + pairs[1].item = "Amount (SF)"; + } + pairs[1].value = ctx->fullStr[1]; + + content->tagValueList.nbPairs = 2; + content->tagValueList.pairs = &pairs[0]; + } + + content->title = ctx->labelStr; + content->type = TAG_VALUE_LIST; + content->tagValueList.callback = NULL; + + content->tagValueList.startIndex = 0; + content->tagValueList.wrapping = false; + content->tagValueList.smallCaseForValue = false; + content->tagValueList.nbMaxLinesForValue = 0; + + return true; +} + +static void begin_review(void) { + nbgl_useCaseRegularReview(0, + ctx->txn.elementIndex + 1, + "Cancel", + NULL, + nav_callback, + confirm_callback); +} + +static void cancel_review(void) { + confirm_callback(false); +} + +static void zero_ctx(void) { + explicit_bzero(ctx, sizeof(calcTxnHashContext_t)); +} + +// handleCalcTxnHash reads a signature index and a transaction, calculates the +// SigHash of the transaction, and optionally signs the hash using a specified +// key. The transaction is displayed piece-wise to the user. +uint16_t handleCalcTxnHash(uint8_t p1, uint8_t p2, uint8_t *dataBuffer, uint16_t dataLength) { + if ((p1 != P1_FIRST && p1 != P1_MORE) || (p2 != P2_DISPLAY_HASH && p2 != P2_SIGN_HASH)) { + return SW_INVALID_PARAM; + } + + if (p1 == P1_FIRST) { + // If this is the first packet of a transaction, the transaction + // context must not already be initialized. (Otherwise, an attacker + // could fool the user by concatenating two transactions.) + // + // NOTE: ctx->initialized is set to false when the Sia app loads. + if (ctx->initialized) { + zero_ctx(); + return SW_IMPROPER_INIT; + } + explicit_bzero(ctx, sizeof(calcTxnHashContext_t)); + ctx->initialized = true; + + // If this is the first packet, it will include the key index, sig + // index, and change index in addition to the transaction data. Use + // these to initialize the ctx and the transaction decoder. + ctx->keyIndex = U4LE(dataBuffer, 0); // NOTE: ignored if !ctx->sign + dataBuffer += 4; + dataLength -= 4; + uint16_t sigIndex = U2LE(dataBuffer, 0); + dataBuffer += 2; + dataLength -= 2; + uint32_t changeIndex = U4LE(dataBuffer, 0); + dataBuffer += 4; + dataLength -= 4; + txn_init(&ctx->txn, sigIndex, changeIndex); + + // Set ctx->sign according to P2. + ctx->sign = (p2 & P2_SIGN_HASH); + + ctx->elemPart = 0; + } else { + // If this is not P1_FIRST, the transaction must have been + // initialized previously. + if (!ctx->initialized) { + zero_ctx(); + return SW_IMPROPER_INIT; + } + } + + // Add the new data to transaction decoder. + txn_update(&ctx->txn, dataBuffer, dataLength); + + switch (txn_parse(&ctx->txn)) { + case TXN_STATE_ERR: + // don't leave state lingering + zero_ctx(); + return SW_INVALID_PARAM; + break; + case TXN_STATE_PARTIAL: + return SW_OK; + break; + case TXN_STATE_FINISHED: + nbgl_useCaseReviewStart(&C_stax_app_sia_big, + (ctx->sign) ? "Sign Transaction" : "Hash Transaction", + NULL, + "Cancel", + begin_review, + cancel_review); + break; + } + + return 0; +} + +#endif /* HAVE_BAGL */ diff --git a/src/getPublicKey.c b/src/getPublicKey.c index 2f50933..6671560 100644 --- a/src/getPublicKey.c +++ b/src/getPublicKey.c @@ -19,132 +19,125 @@ // Keep this description in mind as you read through the implementation. #include +#include #include #include #include #include +#include #include "blake2b.h" #include "sia.h" #include "sia_ux.h" +// These are APDU parameters that control the behavior of the getPublicKey +// command. +#define P2_DISPLAY_ADDRESS 0x00 +#define P2_DISPLAY_PUBKEY 0x01 + // Get a pointer to getPublicKey's state variables. -static getPublicKeyContext_t *ctx = &global.getPublicKeyContext; +static getPublicKeyContext_t* ctx = &global.getPublicKeyContext; + +static unsigned int process_pubkey(bool send); +#ifdef HAVE_BAGL // Allows scrolling through the address/public key -UX_STEP_CB( - ux_compare_pk_flow_1_step, - bnnn_paging, - ui_idle(), - { - "Compare", - global.getPublicKeyContext.fullStr - } -); - -UX_FLOW( - ux_compare_pk_flow, - &ux_compare_pk_flow_1_step -); - -unsigned int io_seproxyhal_touch_pk_ok(void) { - cx_ecfp_public_key_t publicKey = {0}; - - // The response APDU will contain multiple objects, which means we need to - // remember our offset within G_io_apdu_buffer. By convention, the offset - // variable is named 'tx'. - uint8_t tx = 0; - - deriveSiaKeypair(ctx->keyIndex, NULL, &publicKey); - extractPubkeyBytes(G_io_apdu_buffer + tx, &publicKey); - tx += 32; - pubkeyToSiaAddress((char *) G_io_apdu_buffer + tx, &publicKey); - tx += 76; +UX_STEP_CB(ux_compare_pk_flow_1_step, + bnnn_paging, + ui_idle(), + {"Compare", global.getPublicKeyContext.fullStr}); + +UX_FLOW(ux_compare_pk_flow, &ux_compare_pk_flow_1_step); + +UX_STEP_NOCB(ux_approve_pk_flow_1_step, + bn, + {global.getPublicKeyContext.typeStr, global.getPublicKeyContext.keyStr}); + +UX_STEP_VALID(ux_approve_pk_flow_2_step, + pb, + process_pubkey(true), + {&C_icon_validate_14, "Approve"}); + +UX_STEP_VALID(ux_approve_pk_flow_3_step, pb, io_reject(), {&C_icon_crossmark, "Reject"}); + +// Flow for the public key/address menu: +// #1 screen: "generate address/public key from key #x?" +// #2 screen: approve +// #3 screen: reject +UX_FLOW(ux_approve_pk_flow, + &ux_approve_pk_flow_1_step, + &ux_approve_pk_flow_2_step, + &ux_approve_pk_flow_3_step); +#else + +static void review_choice(bool confirm) { + if (confirm) { + process_pubkey(true); + if (ctx->genAddr) { + nbgl_useCaseReviewStatus(STATUS_TYPE_ADDRESS_VERIFIED, ui_idle); + } else { + nbgl_useCaseStatus("PUBKEY VERIFIED", true, ui_idle); + } + } else { + io_send_sw(SW_USER_REJECTED); + if (ctx->genAddr) { + nbgl_useCaseReviewStatus(STATUS_TYPE_ADDRESS_REJECTED, ui_idle); + } else { + nbgl_useCaseStatus("Pubkey Verification Cancelled", false, ui_idle); + } + } +} +#endif + +static unsigned int process_pubkey(bool send) { + uint8_t publicKey[65] = {0}; + + deriveSiaPublicKey(ctx->keyIndex, publicKey); + + uint8_t pubkeyBytes[32] = {0}; + extractPubkeyBytes(pubkeyBytes, publicKey); + uint8_t siaAddress[76 + 1] = {0}; + pubkeyToSiaAddress((char*) siaAddress, publicKey); // Flush the APDU buffer, sending the response. - io_exchange_with_code(SW_OK, tx); + const buffer_t bufs[2] = { + {.ptr = pubkeyBytes, .size = 32, .offset = 0}, + {.ptr = siaAddress, .size = 76, .offset = 0}, + }; + if (send) { + io_send_response_buffers(bufs, sizeof(bufs) / sizeof(bufs[0]), SW_OK); + } - // Prepare the comparison screen, filling in the header and body text. +// Prepare the comparison screen, filling in the header and body text. +#ifdef HAVE_BAGL memmove(ctx->typeStr, "Compare:", 9); +#endif if (ctx->genAddr) { // The APDU buffer already contains the hex-encoded address, so // copy it directly. - memmove(ctx->fullStr, G_io_apdu_buffer + 32, 76); - ctx->fullStr[76] = '\0'; + memcpy(ctx->fullStr, siaAddress, sizeof(siaAddress)); } else { // The APDU buffer contains the raw bytes of the public key, so // first we need to convert to a human-readable form. - bin2hex(ctx->fullStr, G_io_apdu_buffer, 32); + bin2hex(ctx->fullStr, pubkeyBytes, 32); } +#ifdef HAVE_BAGL ux_flow_init(0, ux_compare_pk_flow, NULL); - +#endif return 0; } -UX_STEP_NOCB( - ux_approve_pk_flow_1_step, bn, - { - global.getPublicKeyContext.typeStr, - global.getPublicKeyContext.keyStr - } -); - -UX_STEP_VALID( - ux_approve_pk_flow_2_step, - pb, - io_seproxyhal_touch_pk_ok(), - { - &C_icon_validate, - "Approve" - } -); - -UX_STEP_VALID( - ux_approve_pk_flow_3_step, - pb, - io_seproxyhal_cancel(), - { - &C_icon_crossmark, - "Reject" - } -); - -// Flow for the public key/address menu: -// #1 screen: "generate address/public key from key #x?" -// #2 screen: approve -// #3 screen: reject -UX_FLOW( - ux_approve_pk_flow, - &ux_approve_pk_flow_1_step, - &ux_approve_pk_flow_2_step, - &ux_approve_pk_flow_3_step -); - -// These are APDU parameters that control the behavior of the getPublicKey -// command. -#define P2_DISPLAY_ADDRESS 0x00 -#define P2_DISPLAY_PUBKEY 0x01 - -void handleGetPublicKey(uint8_t p1, uint8_t p2, uint8_t* buffer, uint16_t len, - /* out */ volatile unsigned int* flags, - /* out */ volatile unsigned int* tx) { +uint16_t handleGetPublicKey(uint8_t p1, uint8_t p2, uint8_t* buffer, uint16_t len) { UNUSED(p1); UNUSED(len); - UNUSED(tx); if ((p2 != P2_DISPLAY_ADDRESS) && (p2 != P2_DISPLAY_PUBKEY)) { - // Although THROW is technically a general-purpose exception - // mechanism, within a command handler it is basically just a - // convenient way of bailing out early and sending an error code to - // the computer. The exception will be caught by sia_main, which - // appends the code to the response APDU and sends it, much like - // io_exchange_with_code. THROW should not be called from - // preprocessors or button handlers. - THROW(SW_INVALID_PARAM); + return SW_INVALID_PARAM; } // Read Key Index + explicit_bzero(ctx, sizeof(getPublicKeyContext_t)); ctx->keyIndex = U4LE(buffer, 0); ctx->genAddr = (p2 == P2_DISPLAY_ADDRESS); @@ -160,7 +153,17 @@ void handleGetPublicKey(uint8_t p1, uint8_t p2, uint8_t* buffer, uint16_t len, memmove(ctx->keyStr + 5 + n, "?", 2); } +#ifdef HAVE_BAGL ux_flow_init(0, ux_approve_pk_flow, NULL); +#else + process_pubkey(false); + nbgl_useCaseAddressReview(ctx->fullStr, + NULL, + &C_stax_app_sia_big, + ctx->typeStr, + ctx->keyStr, + review_choice); +#endif - *flags |= IO_ASYNCH_REPLY; + return 0; } diff --git a/src/getVersion.c b/src/getVersion.c index ade1075..3d373c8 100644 --- a/src/getVersion.c +++ b/src/getVersion.c @@ -1,23 +1,22 @@ -#include -#include +#include #include #include +#include +#include + #include "blake2b.h" #include "sia.h" #include "sia_ux.h" -#include +#include // handleGetVersion is the entry point for the getVersion command. It // unconditionally sends the app version. void handleGetVersion(uint8_t p1 __attribute__((unused)), - uint8_t p2 __attribute__((unused)), - uint8_t *dataBuffer __attribute__((unused)), - uint16_t dataLength __attribute__((unused)), - volatile unsigned int *flags __attribute__((unused)), - volatile unsigned int *tx __attribute__((unused))) { - G_io_apdu_buffer[0] = APPVERSION[0] - '0'; - G_io_apdu_buffer[1] = APPVERSION[2] - '0'; - G_io_apdu_buffer[2] = APPVERSION[4] - '0'; - io_exchange_with_code(SW_OK, 3); + uint8_t p2 __attribute__((unused)), + uint8_t *dataBuffer __attribute__((unused)), + uint16_t dataLength __attribute__((unused))) { + static const uint8_t appVersion[3] = {APPVERSION[0] - '0', + APPVERSION[2] - '0', + APPVERSION[4] - '0'}; + io_send_response_pointer(appVersion, sizeof(appVersion), SW_OK); } - diff --git a/src/main.c b/src/main.c deleted file mode 100644 index 6122fbd..0000000 --- a/src/main.c +++ /dev/null @@ -1,494 +0,0 @@ -/******************************************************************************* -* -* (c) 2016 Ledger -* (c) 2018 Nebulous -* -* 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. -********************************************************************************/ - -// This code also serves as a walkthrough for writing your own Ledger Nano S -// app. Begin by reading this file top-to-bottom, and proceed to the next file -// when directed. It is recommended that you install this app on your Nano S -// so that you can see how each section of code maps to real-world behavior. -// This also allows you to experiment by modifying the code and observing the -// effect on the app. -// -// I'll begin by describing the high-level architecture of the app. The entry -// point is this file, main.c, which initializes the app and runs the APDU -// request/response loop. The loop reads APDU packets from the computer, which -// instructs it to run various commands. The Sia app supports three commands, -// each defined in a separate file: getPublicKey, signHash, and calcTxnHash. -// These each make use of Sia-specific functions, which are defined in sia.c. -// Finally, some global variables and helper functions are declared in ux.h. -// -// Each command consists of a command handler and a set of screens. Each -// screen has an associated set of elements that can be rendered, a -// preprocessor that controls which elements are rendered, and a button -// handler that processes user input. The command handler is called whenever -// sia_main receives an APDU requesting that command, and is responsible for -// displaying the first screen of the command. Control flow then moves to the -// button handler for that screen, which selects the next screen to display -// based on which button was pressed. Button handlers are also responsible for -// sending APDU replies back to the computer. -// -// The control flow can be a little confusing to understand, because the -// button handler isn't really on the "main execution path" -- it's only -// called via interrupt, typically while execution is blocked on an -// io_exchange call. (In general, it is instructive to think of io_exchange as -// the *only* call that can block.) io_exchange exchanges APDU packets with -// the computer: first it sends a response packet, then it receives a request -// packet. This ordering may seem strange, but it makes sense when you -// consider that the Nano S has to do work in between receiving a command and -// replying to it. Thus, the packet sent by io_exchange is a *response* to the -// previous request, and the packet received is the next request. -// -// But there's a problem with this flow: in most cases, we can't respond to -// the command request until we've received some user input, e.g. approving a -// signature. If io_exchange is the only call that blocks, how can we tell it -// to wait for user input? The answer is a special flag, IO_ASYNC_REPLY. When -// io_exchange is called with this flag, it blocks, but it doesn't send a -// response; instead, it just waits for a new request. Later on, we make a -// separate call to io_exchange, this time with the IO_RETURN_AFTER_TX flag. -// This call sends the response, and then returns immediately without waiting -// for the next request. Visually, it is clear that these flags have opposite -// effects on io_exchange: -// -// ----Time---> -// io_exchange: [---Send Response---|---Wait for Request---] -// IO_ASYNC_REPLY: ^Only do this part^ -// IO_RETURN_AFTER_TX: ^Only do this part^ -// -// So a typical command flow looks something like this. We start in sia_main, -// which is an infinite loop that starts by calling io_exchange. It receives -// an APDU request from the computer and calls the associated command handler. -// The handler displays a screen, e.g. "Generate address?", and sets the -// IO_ASYNC_REPLY flag before returning. Control returns to sia_main, which -// loops around and calls io_exchange again; due to the flag, it now blocks. -// Everything is frozen until the user decides which button to press. When -// they eventually press the "Approve" button, the button handler jumps into -// action. It generates the address, constructs a response APDU containing -// that address, calls io_exchange with IO_RETURN_AFTER_TX, and redisplays the -// main menu. When a new command arrives, it will be received by the blocked -// io_exchange in sia_main. -// -// More complex commands may require multiple requests and responses. There -// are two approaches to handling this. One approach is to treat each command -// handler as a self-contained unit: that is, the main loop should only handle -// the *first* request for a given command. Subsequent requests are handled by -// additional io_exchange calls within the command handler. The other approach -// is to let the main loop handle all requests, and design the handlers so -// that they can "pick up where they left off." Both designs have tradeoffs. -// In the Sia app, the only handler that requires multiple requests is -// calcTxnHash, and it takes the latter approach. -// -// On the other end of the spectrum, there are simple commands that do not -// require any user input. Many Nano S apps have a "getVersion" command that -// replies to the computer with the app's version. In this case, it is -// sufficient for the command handler to prepare the response APDU and allow -// the main loop to send it immediately, without setting IO_ASYNC_REPLY. -// -// The important things to remember are: -// - io_exchange is the only blocking call -// - the main loop invokes command handlers, which display screens and set button handlers -// - button handlers switch between screens and reply to the computer - -#include "os.h" -#include -#include "glyphs.h" -#include "blake2b.h" -#include "sia.h" -#include "sia_ux.h" -#include -#include -#include - -// You may notice that this file includes blake2b.h despite doing no hashing. -// This is because the Sia app uses the Plan 9 convention for header files: -// header files may not #include other header files. This file needs ux.h, but -// ux.h depends on sia.h, which depends on blake2b.h; so all three must be -// included before we can include ux.h. Feel free to use the more conventional -// #ifndef guards in your own app. - -// These are global variables declared in ux.h. They can't be defined there -// because multiple files include ux.h; they need to be defined in exactly one -// place. See ux.h for their descriptions. -commandContext global; -ux_state_t G_ux; -bolos_ux_params_t G_ux_params; - -// Here we define the main menu, using the Ledger-provided menu API. This menu -// turns out to be fairly unimportant for Nano S apps, since commands are sent -// by the computer instead of being initiated by the user. It typically just -// contains an idle screen and a version screen. - -UX_STEP_NOCB( - ux_menu_ready_step, - nn, - { - "Awaiting", - "commands" - } -); -UX_STEP_CB( - ux_menu_about_step, - pn, - ui_menu_about(), - { - &C_icon_certificate, - "About" - } -); -UX_STEP_VALID( - ux_menu_exit_step, - pn, - os_sched_exit(0), - { - &C_icon_dashboard, - "Quit" - } -); - -// flow for the main menu: -// #1 screen: ready -// #2 screen: about submenu -// #3 screen: quit -UX_FLOW( - ux_menu_main_flow, - &ux_menu_ready_step, - &ux_menu_about_step, - &ux_menu_exit_step, - FLOW_LOOP -); - -void ui_idle() { - if (G_ux.stack_count == 0) { - ux_stack_push(); - } - - ux_flow_init(0, ux_menu_main_flow, NULL); -} - -UX_STEP_NOCB( - ux_menu_info_step, - bn, - { - "Version", - APPVERSION - } -); -UX_STEP_CB( - ux_menu_back_step, - pb, - ui_idle(), - { - &C_icon_back, - "Back" - } -); - -// flow for the about submenu: -// #1 screen: app version -// #2 screen: back button -UX_FLOW( - ux_menu_about_flow, - &ux_menu_info_step, - &ux_menu_back_step, - FLOW_LOOP -); - -void ui_menu_about() { - ux_flow_init(0, ux_menu_about_flow, NULL); -} - -// io_exchange_with_code is a helper function for sending response APDUs from -// button handlers. Note that the IO_RETURN_AFTER_TX flag is set. 'tx' is the -// conventional name for the size of the response APDU, i.e. the write-offset -// within G_io_apdu_buffer. -void io_exchange_with_code(uint16_t code, uint16_t tx) { - G_io_apdu_buffer[tx++] = code >> 8; - G_io_apdu_buffer[tx++] = code & 0xFF; - io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, tx); -} - -unsigned int io_seproxyhal_cancel(void) { - io_exchange_with_code(SW_USER_REJECTED, 0); - // Return to the main screen. - ui_idle(); - return 0; -} - - -// The APDU protocol uses a single-byte instruction code (INS) to specify -// which command should be executed. We'll use this code to dispatch on a -// table of function pointers. -#define INS_GET_VERSION 0x01 -#define INS_GET_PUBLIC_KEY 0x02 -#define INS_SIGN_HASH 0x04 -#define INS_GET_TXN_HASH 0x08 - -// This is the function signature for a command handler. 'flags' and 'tx' are -// out-parameters that will control the behavior of the next io_exchange call -// in sia_main. It's common to set *flags |= IO_ASYNC_REPLY, but tx is -// typically unused unless the handler is immediately sending a response APDU. -typedef void handler_fn_t(uint8_t p1, uint8_t p2, uint8_t *dataBuffer, uint16_t dataLength, volatile unsigned int *flags, volatile unsigned int *tx); - -handler_fn_t handleGetVersion; -handler_fn_t handleGetPublicKey; -handler_fn_t handleSignHash; -handler_fn_t handleCalcTxnHash; - -static handler_fn_t* lookupHandler(uint8_t ins) { - switch (ins) { - case INS_GET_VERSION: return handleGetVersion; - case INS_GET_PUBLIC_KEY: return handleGetPublicKey; - case INS_SIGN_HASH: return handleSignHash; - case INS_GET_TXN_HASH: return handleCalcTxnHash; - default: return NULL; - } -} - -// These are the offsets of various parts of a request APDU packet. INS -// identifies the requested command (see above), and P1 and P2 are parameters -// to the command. -#define CLA 0xE0 -#define OFFSET_CLA 0x00 -#define OFFSET_INS 0x01 -#define OFFSET_P1 0x02 -#define OFFSET_P2 0x03 -#define OFFSET_LC 0x04 -#define OFFSET_CDATA 0x05 - -// This is the main loop that reads and writes APDUs. It receives request -// APDUs from the computer, looks up the corresponding command handler, and -// calls it on the APDU payload. Then it loops around and calls io_exchange -// again. The handler may set the 'flags' and 'tx' variables, which affect the -// subsequent io_exchange call. The handler may also throw an exception, which -// will be caught, converted to an error code, appended to the response APDU, -// and sent in the next io_exchange call. -static void sia_main(void) { - // Mark the transaction context as uninitialized. - global.calcTxnHashContext.initialized = false; - - volatile unsigned int rx = 0; - volatile unsigned int tx = 0; - volatile unsigned int flags = 0; - - // Exchange APDUs until EXCEPTION_IO_RESET is thrown. - for (;;) { - volatile unsigned short sw = 0; - - // The Ledger SDK implements a form of exception handling. In addition - // to explicit THROWs in user code, syscalls (prefixed with os_ or - // cx_) may also throw exceptions. - // - // In sia_main, this TRY block serves to catch any thrown exceptions - // and convert them to response codes, which are then sent in APDUs. - // However, EXCEPTION_IO_RESET will be re-thrown and caught by the - // "true" main function defined at the bottom of this file. - BEGIN_TRY { - TRY { - rx = tx; - tx = 0; // ensure no race in CATCH_OTHER if io_exchange throws an error - rx = io_exchange(CHANNEL_APDU | flags, rx); - flags = 0; - - // No APDU received; trigger a reset. - if (rx == 0) { - THROW(EXCEPTION_IO_RESET); - } - // Malformed APDU. - if (G_io_apdu_buffer[OFFSET_CLA] != CLA) { - THROW(0x6E00); - } - // Lookup and call the requested command handler. - handler_fn_t *handlerFn = lookupHandler(G_io_apdu_buffer[OFFSET_INS]); - if (!handlerFn) { - THROW(0x6D00); - } - - // without calling this, pagination will always begin on the last page if a paginated menu has been scrolled through before during the session - #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) - ux_layout_bnnn_paging_reset(); - #else - ux_layout_paging_reset(); - #endif - - handlerFn(G_io_apdu_buffer[OFFSET_P1], G_io_apdu_buffer[OFFSET_P2], - G_io_apdu_buffer + OFFSET_CDATA, G_io_apdu_buffer[OFFSET_LC], &flags, &tx); - } - CATCH(EXCEPTION_IO_RESET) { - THROW(EXCEPTION_IO_RESET); - } - CATCH_OTHER(e) { - // Convert the exception to a response code. All error codes - // start with 6, except for 0x9000, which is a special - // "success" code. Every APDU payload should end with such a - // code, even if no other data is sent. For example, when - // calcTxnHash is processing packets of txn data, it replies - // with just 0x9000 to indicate that it is ready to receive - // more data. - // - // If the first byte is not a 6, mask it with 0x6800 to - // convert it to a proper error code. I'm not totally sure why - // this is done; perhaps to handle single-byte exception - // codes? - switch (e & 0xF000) { - case 0x6000: - case 0x9000: - sw = e; - break; - default: - sw = 0x6800 | (e & 0x7FF); - break; - } - G_io_apdu_buffer[tx++] = sw >> 8; - G_io_apdu_buffer[tx++] = sw & 0xFF; - } - FINALLY { - } - } - END_TRY; - } -} - - -// Everything below this point is Ledger magic. And the magic isn't well- -// documented, so if you want to understand it, you'll need to read the -// source, which you can find in the nanos-secure-sdk repo. Fortunately, you -// don't need to understand any of this in order to write an app. -// -// Next, we'll look at how the various commands are implemented. We'll start -// with the simplest command, signHash.c. - -// override point, but nothing more to do -void io_seproxyhal_display(const bagl_element_t *element) { - io_seproxyhal_display_default((bagl_element_t *)element); -} - -unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; - -unsigned char io_event(unsigned char channel __attribute__((unused))) { - // can't have more than one tag in the reply, not supported yet. - switch (G_io_seproxyhal_spi_buffer[0]) { - case SEPROXYHAL_TAG_FINGER_EVENT: - UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer); - break; - - case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: - UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer); - break; - - case SEPROXYHAL_TAG_STATUS_EVENT: - if (G_io_apdu_media == IO_APDU_MEDIA_USB_HID && - !(U4BE(G_io_seproxyhal_spi_buffer, 3) & - SEPROXYHAL_TAG_STATUS_EVENT_FLAG_USB_POWERED)) { - THROW(EXCEPTION_IO_RESET); - } - UX_DEFAULT_EVENT(); - break; - - case SEPROXYHAL_TAG_DISPLAY_PROCESSED_EVENT: - UX_DISPLAYED_EVENT({}); - break; - - case SEPROXYHAL_TAG_TICKER_EVENT: - UX_TICKER_EVENT(G_io_seproxyhal_spi_buffer, {}); - break; - - default: - UX_DEFAULT_EVENT(); - break; - } - - // close the event if not done previously (by a display or whatever) - if (!io_seproxyhal_spi_is_status_sent()) { - io_seproxyhal_general_status(); - } - - // command has been processed, DO NOT reset the current APDU transport - return 1; -} - -unsigned short io_exchange_al(unsigned char channel, unsigned short tx_len) { - switch (channel & ~(IO_FLAGS)) { - case CHANNEL_KEYBOARD: - break; - // multiplexed io exchange over a SPI channel and TLV encapsulated protocol - case CHANNEL_SPI: - if (tx_len) { - io_seproxyhal_spi_send(G_io_apdu_buffer, tx_len); - if (channel & IO_RESET_AFTER_REPLIED) { - reset(); - } - return 0; // nothing received from the master so far (it's a tx transaction) - } else { - return io_seproxyhal_spi_recv(G_io_apdu_buffer, sizeof(G_io_apdu_buffer), 0); - } - default: - THROW(INVALID_PARAMETER); - } - return 0; -} - -static void app_exit(void) { - BEGIN_TRY_L(exit) { - TRY_L(exit) { - os_sched_exit(-1); - } - FINALLY_L(exit) { - } - } - END_TRY_L(exit); -} - -__attribute__((section(".boot"))) int main(void) { - // exit critical section - __asm volatile("cpsie i"); - - for (;;) { - UX_INIT(); - os_boot(); - BEGIN_TRY { - TRY { - io_seproxyhal_init(); - -#ifdef HAVE_BLE - G_io_app.plane_mode = os_setting_get(OS_SETTING_PLANEMODE, NULL, 0); -#endif - - USB_power(0); - USB_power(1); - -#ifdef HAVE_BLE - BLE_power(0, NULL); - BLE_power(1, "Nano X"); -#endif - - ui_idle(); - sia_main(); - } - CATCH(EXCEPTION_IO_RESET) { - // reset IO and UX before continuing - continue; - } - CATCH_ALL { - break; - } - FINALLY { - } - } - END_TRY; - } - app_exit(); - return 0; -} diff --git a/src/sia.c b/src/sia.c index a9564f5..057202c 100644 --- a/src/sia.c +++ b/src/sia.c @@ -1,152 +1,173 @@ +#include "sia.h" + +#include +#include +#include +#include +#include #include #include -#include #include -#include + #include "blake2b.h" -#include "sia.h" -void deriveSiaKeypair(uint32_t index, cx_ecfp_private_key_t *privateKey, cx_ecfp_public_key_t *publicKey) { - uint8_t keySeed[32]; - cx_ecfp_private_key_t pk; - - // bip32 path for 44'/93'/n'/0'/0' - uint32_t bip32Path[] = {44 | 0x80000000, 93 | 0x80000000, index | 0x80000000, 0x80000000, 0x80000000}; - os_perso_derive_node_bip32_seed_key(HDW_ED25519_SLIP10, CX_CURVE_Ed25519, bip32Path, 5, keySeed, NULL, NULL, 0); - - cx_ecfp_init_private_key(CX_CURVE_Ed25519, keySeed, sizeof(keySeed), &pk); - if (publicKey) { - cx_ecfp_init_public_key(CX_CURVE_Ed25519, NULL, 0, publicKey); - cx_ecfp_generate_pair(CX_CURVE_Ed25519, publicKey, &pk, 1); - } - if (privateKey) { - *privateKey = pk; - } - explicit_bzero(keySeed, sizeof(keySeed)); - explicit_bzero(&pk, sizeof(pk)); +static void siaSetPath(uint32_t index, uint32_t path[static 5]) { + path[0] = 44 | 0x80000000; + path[1] = 93 | 0x80000000; + path[2] = index | 0x80000000; + path[3] = 0x80000000; + path[4] = 0x80000000; +} + +void deriveSiaPublicKey(uint32_t index, uint8_t publicKey[static 65]) { + uint32_t bip32Path[5]; + siaSetPath(index, bip32Path); + + LEDGER_ASSERT(CX_OK == bip32_derive_with_seed_get_pubkey_256(HDW_ED25519_SLIP10, + CX_CURVE_Ed25519, + bip32Path, + 5, + publicKey, + NULL, + CX_SHA512, + NULL, + 0), + "get pubkey failed"); } -void extractPubkeyBytes(unsigned char *dst, const cx_ecfp_public_key_t *publicKey) { - for (int i = 0; i < 32; i++) { - dst[i] = publicKey->W[64 - i]; - } - if (publicKey->W[32] & 1) { - dst[31] |= 0x80; - } +void extractPubkeyBytes(unsigned char *dst, const uint8_t publicKey[static 65]) { + for (int i = 0; i < 32; i++) { + dst[i] = publicKey[64 - i]; + } + if (publicKey[32] & 1) { + dst[31] |= 0x80; + } } void deriveAndSign(uint8_t *dst, uint32_t index, const uint8_t *hash) { - cx_ecfp_private_key_t privateKey; - deriveSiaKeypair(index, &privateKey, NULL); - cx_eddsa_sign(&privateKey, CX_RND_RFC6979 | CX_LAST, CX_SHA512, hash, 32, NULL, 0, dst, 64, NULL); - explicit_bzero(&privateKey, sizeof(privateKey)); + uint32_t bip32Path[5]; + siaSetPath(index, bip32Path); + + size_t signatureLength = 64; + LEDGER_ASSERT(CX_OK == bip32_derive_with_seed_eddsa_sign_hash_256(HDW_ED25519_SLIP10, + CX_CURVE_Ed25519, + bip32Path, + 5, + CX_SHA512, + hash, + 32, + dst, + &signatureLength, + NULL, + 0), + "signing txn failed"); } void bin2hex(char *dst, const uint8_t *data, uint64_t inlen) { - static uint8_t const hex[] = "0123456789abcdef"; - for (uint64_t i = 0; i < inlen; i++) { - dst[2*i+0] = hex[(data[i]>>4) & 0x0F]; - dst[2*i+1] = hex[(data[i]>>0) & 0x0F]; - } - dst[2*inlen] = '\0'; + static uint8_t const hex[] = "0123456789abcdef"; + for (uint64_t i = 0; i < inlen; i++) { + dst[2 * i + 0] = hex[(data[i] >> 4) & 0x0F]; + dst[2 * i + 1] = hex[(data[i] >> 0) & 0x0F]; + } + dst[2 * inlen] = '\0'; } -void pubkeyToSiaAddress(char *dst, const cx_ecfp_public_key_t *publicKey) { - // A Sia address is the Merkle root of a set of unlock conditions. - // For a "standard" address, the unlock conditions are: - // - // - no timelock - // - one public key - // - one signature required - // - // For now, the Ledger will only be able to generate standard addresses. - // We can add support for arbitrary unlock conditions later. - - // defined in RFC 6962 - const uint8_t leafHashPrefix = 0; - const uint8_t nodeHashPrefix = 1; - - // encode the timelock, pubkey, and sigsrequired - // TODO: can reuse buffers here to make this more efficient - uint8_t timelockData[9]; - memset(timelockData, 0, sizeof(timelockData)); - timelockData[0] = leafHashPrefix; - - uint8_t pubkeyData[57]; - memset(pubkeyData, 0, sizeof(pubkeyData)); - pubkeyData[0] = leafHashPrefix; - memmove(pubkeyData + 1, "ed25519", 7); - pubkeyData[17] = 32; - extractPubkeyBytes(pubkeyData + 25, publicKey); - - uint8_t sigsrequiredData[9]; - memset(sigsrequiredData, 0, sizeof(sigsrequiredData)); - sigsrequiredData[0] = leafHashPrefix; - sigsrequiredData[1] = 1; - - // To calculate the Merkle root, we need a buffer large enough to hold two - // hashes plus a special leading byte. - uint8_t merkleData[65]; - merkleData[0] = nodeHashPrefix; - // hash timelock into slot 1 - blake2b(merkleData+1, 32, timelockData, sizeof(timelockData)); - // hash pubkey into slot 2 - blake2b(merkleData+33, 32, pubkeyData, sizeof(pubkeyData)); - // join hashes into slot 1 - blake2b(merkleData+1, 32, merkleData, 65); - // hash sigsrequired into slot 2 - blake2b(merkleData+33, 32, sigsrequiredData, sizeof(sigsrequiredData)); - // join hashes into slot 1, finishing Merkle root (unlock hash) - blake2b(merkleData+1, 32, merkleData, 65); - - // hash the unlock hash to get a checksum - uint8_t checksum[6]; - blake2b(checksum, sizeof(checksum), merkleData+1, 32); - - // convert the hash+checksum to hex - bin2hex(dst, merkleData+1, 32); - bin2hex(dst+64, checksum, sizeof(checksum)); +void pubkeyToSiaAddress(char *dst, const uint8_t publicKey[static 65]) { + // A Sia address is the Merkle root of a set of unlock conditions. + // For a "standard" address, the unlock conditions are: + // + // - no timelock + // - one public key + // - one signature required + // + // For now, the Ledger will only be able to generate standard addresses. + // We can add support for arbitrary unlock conditions later. + + // defined in RFC 6962 + const uint8_t leafHashPrefix = 0; + const uint8_t nodeHashPrefix = 1; + + // encode the timelock, pubkey, and sigsrequired + // TODO: can reuse buffers here to make this more efficient + uint8_t timelockData[9]; + memset(timelockData, 0, sizeof(timelockData)); + timelockData[0] = leafHashPrefix; + + uint8_t pubkeyData[57]; + memset(pubkeyData, 0, sizeof(pubkeyData)); + pubkeyData[0] = leafHashPrefix; + memmove(pubkeyData + 1, "ed25519", 7); + pubkeyData[17] = 32; + extractPubkeyBytes(pubkeyData + 25, publicKey); + + uint8_t sigsrequiredData[9]; + memset(sigsrequiredData, 0, sizeof(sigsrequiredData)); + sigsrequiredData[0] = leafHashPrefix; + sigsrequiredData[1] = 1; + + // To calculate the Merkle root, we need a buffer large enough to hold two + // hashes plus a special leading byte. + uint8_t merkleData[65]; + merkleData[0] = nodeHashPrefix; + // hash timelock into slot 1 + blake2b(merkleData + 1, 32, timelockData, sizeof(timelockData)); + // hash pubkey into slot 2 + blake2b(merkleData + 33, 32, pubkeyData, sizeof(pubkeyData)); + // join hashes into slot 1 + blake2b(merkleData + 1, 32, merkleData, 65); + // hash sigsrequired into slot 2 + blake2b(merkleData + 33, 32, sigsrequiredData, sizeof(sigsrequiredData)); + // join hashes into slot 1, finishing Merkle root (unlock hash) + blake2b(merkleData + 1, 32, merkleData, 65); + + // hash the unlock hash to get a checksum + uint8_t checksum[6]; + blake2b(checksum, sizeof(checksum), merkleData + 1, 32); + + // convert the hash+checksum to hex + bin2hex(dst, merkleData + 1, 32); + bin2hex(dst + 64, checksum, sizeof(checksum)); } int bin2dec(char *dst, uint64_t n) { - if (n == 0) { - dst[0] = '0'; - dst[1] = '\0'; - return 1; - } - // determine final length - int len = 0; - for (uint64_t nn = n; nn != 0; nn /= 10) { - len++; - } - // write digits in big-endian order - for (int i = len-1; i >= 0; i--) { - dst[i] = (n % 10) + '0'; - n /= 10; - } - dst[len] = '\0'; - return len; + if (n == 0) { + dst[0] = '0'; + dst[1] = '\0'; + return 1; + } + // determine final length + int len = 0; + for (uint64_t nn = n; nn != 0; nn /= 10) { + len++; + } + // write digits in big-endian order + for (int i = len - 1; i >= 0; i--) { + dst[i] = (n % 10) + '0'; + n /= 10; + } + dst[len] = '\0'; + return len; } #define SC_ZEROS 24 int formatSC(char *buf, uint8_t decLen) { - if (decLen < SC_ZEROS+1) { - // if < 1 SC, pad with leading zeros - memmove(buf + (SC_ZEROS-decLen)+2, buf, decLen+1); - memset(buf, '0', SC_ZEROS+2-decLen); - decLen = SC_ZEROS + 1; - } else { - memmove(buf + (decLen-SC_ZEROS)+1, buf + (decLen-SC_ZEROS), SC_ZEROS+1); - } - // add decimal point, trim trailing zeros, and add units - buf[decLen-SC_ZEROS] = '.'; - while (decLen > 0 && buf[decLen] == '0') { - decLen--; - } - if (buf[decLen] == '.') { - decLen--; - } - memmove(buf + decLen + 1, " SC", 4); - return decLen + 4; + if (decLen < SC_ZEROS + 1) { + // if < 1 SC, pad with leading zeros + memmove(buf + (SC_ZEROS - decLen) + 2, buf, decLen + 1); + memset(buf, '0', SC_ZEROS + 2 - decLen); + decLen = SC_ZEROS + 1; + } else { + memmove(buf + (decLen - SC_ZEROS) + 1, buf + (decLen - SC_ZEROS), SC_ZEROS + 1); + } + // add decimal point, trim trailing zeros, and add units + buf[decLen - SC_ZEROS] = '.'; + while (decLen > 0 && buf[decLen] == '0') { + decLen--; + } + if (buf[decLen] == '.') { + decLen--; + } + memmove(buf + decLen + 1, " SC", 4); + return decLen + 4; } diff --git a/src/sia.h b/src/sia.h index a5364ae..737ea5f 100644 --- a/src/sia.h +++ b/src/sia.h @@ -1,14 +1,23 @@ -#pragma once +#ifndef SIA_H +#define SIA_H +#include #include #include // exception codes -#define SW_DEVELOPER_ERR 0x6B00 -#define SW_INVALID_PARAM 0x6B01 -#define SW_IMPROPER_INIT 0x6B02 -#define SW_USER_REJECTED 0x6985 -#define SW_OK 0x9000 +#define SW_DEVELOPER_ERR 0x6B00 +#define SW_INVALID_PARAM 0x6B01 +#define SW_IMPROPER_INIT 0x6B02 +#define SW_USER_REJECTED 0x6985 +#define SW_INS_NOT_SUPPORTED 0x6D00 +#define SW_OK 0x9000 + +// APDU parameters +#define P1_FIRST 0x00 // 1st packet of multi-packet transfer +#define P1_MORE 0x80 // nth packet of multi-packet transfer +#define P2_DISPLAY_HASH 0x00 // display transaction hash +#define P2_SIGN_HASH 0x01 // sign transaction hash // bin2hex converts binary to hex and appends a final NUL byte. void bin2hex(char *dst, const uint8_t *data, uint64_t inlen); @@ -23,16 +32,18 @@ int formatSC(char *buf, uint8_t decLen); // extractPubkeyBytes converts a Ledger-style public key to a Sia-friendly // 32-byte array. -void extractPubkeyBytes(unsigned char *dst, const cx_ecfp_public_key_t *publicKey); +void extractPubkeyBytes(unsigned char *dst, const uint8_t publicKey[static 65]); // pubkeyToSiaAddress converts a Ledger pubkey to a Sia wallet address. -void pubkeyToSiaAddress(char *dst, const cx_ecfp_public_key_t *publicKey); +void pubkeyToSiaAddress(char *dst, const uint8_t publicKey[static 65]); -// deriveSiaKeypair derives an Ed25519 key pair from an index and the Ledger -// seed. Either privateKey or publicKey may be NULL. -void deriveSiaKeypair(uint32_t index, cx_ecfp_private_key_t *privateKey, cx_ecfp_public_key_t *publicKey); +// deriveSiaPublicKey derives an Ed25519 public key from an index and the +// Ledger seed. +void deriveSiaPublicKey(uint32_t index, uint8_t publicKey[static 64]); // deriveAndSign derives an Ed25519 private key from an index and the // Ledger seed, and uses it to produce a 64-byte signature of the provided // 32-byte hash. The key is cleared from memory after signing. void deriveAndSign(uint8_t *dst, uint32_t index, const uint8_t *hash); + +#endif /* SIA_H */ \ No newline at end of file diff --git a/src/sia_ux.h b/src/sia_ux.h index ddf47c3..829efdf 100644 --- a/src/sia_ux.h +++ b/src/sia_ux.h @@ -1,62 +1,69 @@ +#ifndef SIA_UX_H +#define SIA_UX_H + #include #include "txn.h" +#ifdef HAVE_NBGL +#include +#endif + +#define APPDEVELOPER "Sia Foundation" + // Each command has some state associated with it that sticks around for the // life of the command. A separate context_t struct should be defined for each // command. typedef struct { - uint32_t keyIndex; - bool genAddr; - // NUL-terminated strings for display - char typeStr[40]; // variable-length - char keyStr[40]; // variable-length - char fullStr[77]; // variable length + uint32_t keyIndex; + bool genAddr; + // NUL-terminated strings for display + char typeStr[40]; // variable-length + char keyStr[40]; // variable-length + char fullStr[77]; // variable length } getPublicKeyContext_t; #define SIA_HASH_SIZE 32 typedef struct { - uint32_t keyIndex; - uint8_t hash[SIA_HASH_SIZE]; - char hexHash[SIA_HASH_SIZE * 2]; + uint32_t keyIndex; + uint8_t hash[SIA_HASH_SIZE]; + + char typeStr[40]; + char hexHash[SIA_HASH_SIZE * 2]; } signHashContext_t; typedef struct { - uint32_t keyIndex; - bool sign; - uint8_t elemPart; // screen index of elements - txn_state_t txn; - // NULL-terminated strings for display - char labelStr[40]; // variable length - char fullStr[128]; // variable length - bool initialized; // protects against certain attacks + uint32_t keyIndex; + bool sign; + uint8_t elemPart; // screen index of elements + + uint16_t elementIndex; + + txn_state_t txn; + // NULL-terminated strings for display + char labelStr[40]; // variable length + char fullStr[2][128]; // variable length + bool initialized; // protects against certain attacks + bool finished; // whether we have reached the end of the transaction } calcTxnHashContext_t; // To save memory, we store all the context types in a single global union, // taking advantage of the fact that only one command is executed at a time. typedef union { - getPublicKeyContext_t getPublicKeyContext; - signHashContext_t signHashContext; - calcTxnHashContext_t calcTxnHashContext; + getPublicKeyContext_t getPublicKeyContext; + signHashContext_t signHashContext; + calcTxnHashContext_t calcTxnHashContext; } commandContext; extern commandContext global; -// These are helper macros for defining UI elements. There are four basic UI -// elements: the background, which is a black rectangle that fills the whole -// screen; icons on the left and right sides of the screen, typically used for -// navigation or approval; and text, which can be anywhere. The UI_TEXT macro -// uses Open Sans Regular 11px, which I've found to be adequate for all text -// elements; if other fonts are desired, I suggest defining a separate macro -// for each of them (e.g. UI_TEXT_BOLD). -// -// In the event that you want to define your own UI elements from scratch, -// you'll want to read include/bagl.h and include/os_io_seproxyhal.h in the -// nanos-secure-sdk repo to learn what each of the fields are used for. -#define UI_BACKGROUND() {{BAGL_RECTANGLE,0,0,0,128,32,0,0,BAGL_FILL,0,0xFFFFFF,0,0},NULL,0,0,0,NULL,NULL,NULL} -#define UI_ICON_LEFT(userid, glyph) {{BAGL_ICON,userid,3,12,7,7,0,0,0,0xFFFFFF,0,0,glyph},NULL,0,0,0,NULL,NULL,NULL} -#define UI_ICON_RIGHT(userid, glyph) {{BAGL_ICON,userid,117,13,8,6,0,0,0,0xFFFFFF,0,0,glyph},NULL,0,0,0,NULL,NULL,NULL} -#define UI_TEXT(userid, x, y, w, text) {{BAGL_LABELINE,userid,x,y,w,12,0,0,0,0xFFFFFF,0,BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER,0},(char *)text,0,0,0,NULL,NULL,NULL} +typedef struct internalStorage_t { + bool blindSign; + bool initialized; +} internalStorage_t; + +extern const internalStorage_t N_storage_real; +#define N_storage (*(volatile internalStorage_t *) PIC(&N_storage_real)) // ui_idle displays the main menu screen. Command handlers should call ui_idle // when they finish. @@ -65,11 +72,7 @@ void ui_idle(void); // about submenu of the main screen void ui_menu_about(void); -// io_exchange_with_code is a helper function for sending APDUs, primarily -// from button handlers. It appends code to G_io_apdu_buffer and calls -// io_exchange with the IO_RETURN_AFTER_TX flag. tx is the current offset -// within G_io_apdu_buffer (before the code is appended). -void io_exchange_with_code(uint16_t code, uint16_t tx); - // standard "reject" function so we don't repeat code -unsigned int io_seproxyhal_cancel(void); +unsigned int io_reject(void); + +#endif /* SIA_UX_H */ \ No newline at end of file diff --git a/src/signHash.c b/src/signHash.c index 4472607..77e4060 100644 --- a/src/signHash.c +++ b/src/signHash.c @@ -17,13 +17,15 @@ // // Keep this description in mind as you read through the implementation. -#include #include +#include +#include +#include +#include + #include "blake2b.h" #include "sia.h" #include "sia_ux.h" -#include -#include // Get a pointer to signHash's state variables. This is purely for // convenience, so that we can refer to these variables concisely from any @@ -34,110 +36,100 @@ static unsigned int io_seproxyhal_touch_hash_ok(void) { // Derive the secret key and sign the hash, storing the signature in // the APDU buffer. This is the first Sia-specific function we've // encountered; it is defined in sia.c. - deriveAndSign(G_io_apdu_buffer, ctx->keyIndex, ctx->hash); - io_exchange_with_code(SW_OK, 64); + uint8_t signature[64] = {0}; + deriveAndSign(signature, ctx->keyIndex, ctx->hash); + io_send_response_pointer(signature, sizeof(signature), SW_OK); +#ifdef HAVE_BAGL ui_idle(); +#else + nbgl_useCaseStatus("HASH SIGNED", true, ui_idle); +#endif + return 0; } -UX_STEP_NOCB( - ux_approve_hash_flow_1_step, - bnnn_paging, - { - "Compare Input:", - global.signHashContext.hexHash - } -); - -UX_STEP_VALID( - ux_approve_hash_flow_2_step, - pb, - io_seproxyhal_touch_hash_ok(), - { - &C_icon_validate, - "Approve" - } -); - -UX_STEP_VALID( - ux_approve_hash_flow_3_step, - pb, - io_seproxyhal_cancel(), - { - &C_icon_crossmark, - "Reject" - } -); +#ifdef HAVE_BAGL +UX_STEP_NOCB(ux_approve_hash_flow_1_step, + bnnn_paging, + {"Compare Input:", global.signHashContext.hexHash}); + +UX_STEP_VALID(ux_approve_hash_flow_2_step, + pb, + io_seproxyhal_touch_hash_ok(), + {&C_icon_validate_14, "Approve"}); + +UX_STEP_VALID(ux_approve_hash_flow_3_step, pb, io_reject(), {&C_icon_crossmark, "Reject"}); // Flow for the signing hash menu: // #1 screen: the hash repeated for confirmation // #2 screen: approve // #3 screen: reject -UX_FLOW( - ux_approve_hash_flow, - &ux_approve_hash_flow_1_step, - &ux_approve_hash_flow_2_step, - &ux_approve_hash_flow_3_step -); - -void handleSignHash( - uint8_t p1 __attribute__((unused)), - uint8_t p2 __attribute__((unused)), - uint8_t *buffer, - uint16_t len, - /* out */ volatile unsigned int *flags, - /* out */ volatile unsigned int *tx __attribute__((unused))) { - - if (len != sizeof(uint32_t) + SIA_HASH_SIZE) { - THROW(SW_INVALID_PARAM); - } +UX_FLOW(ux_approve_hash_flow, + &ux_approve_hash_flow_1_step, + &ux_approve_hash_flow_2_step, + &ux_approve_hash_flow_3_step); +#else + +static nbgl_layoutTagValue_t pair = {0}; + +static void cancel_review(void) { + // display a status page and go back to main + io_send_sw(SW_USER_REJECTED); + nbgl_useCaseStatus("Signing Cancelled", false, ui_idle); +} + +static void confirm_callback(bool confirm) { + if (confirm) { + io_seproxyhal_touch_hash_ok(); + } else { + cancel_review(); + } +} + +#endif + +uint16_t handleSignHash(uint8_t p1 __attribute__((unused)), + uint8_t p2 __attribute__((unused)), + uint8_t *buffer, + uint16_t len) { + if (len != sizeof(uint32_t) + SIA_HASH_SIZE) { + return SW_INVALID_PARAM; + } else if (!N_storage.blindSign) { + return SW_USER_REJECTED; + } + // Read the index of the signing key. U4LE is a helper macro for // converting a 4-byte buffer to a uint32_t. + explicit_bzero(ctx, sizeof(signHashContext_t)); ctx->keyIndex = U4LE(buffer, 0); + // Read the hash. memcpy(ctx->hash, buffer + sizeof(uint32_t), SIA_HASH_SIZE); // Prepare to display the comparison screen by converting the hash to hex bin2hex(ctx->hexHash, ctx->hash, SIA_HASH_SIZE); +#ifdef HAVE_BAGL ux_flow_init(0, ux_approve_hash_flow, NULL); +#else + snprintf(ctx->typeStr, sizeof(ctx->typeStr), "Sign Hash with Key %d?", ctx->keyIndex); - *flags |= IO_ASYNCH_REPLY; -} + pair.item = "Hash"; + pair.value = ctx->hexHash; -// Now that we've seen the individual pieces, we can construct a full picture -// of what the signHash command looks like. -// -// The command begins when sia_main reads an APDU packet from the computer -// with INS == INS_SIGN_HASH. sia_main looks up the appropriate handler, -// handleSignHash, and calls it. handleSignHash reads the command data, -// prepares and displays the comparison screen, and sets the IO_ASYNC_REPLY -// flag. Control returns to sia_main, which blocks when it reaches the -// io_exchange call. -// -// UX_DISPLAY was called with the ui_prepro_signHash_compare preprocessor, so -// that preprocessor is now called each time the compare screen is rendered. -// Since we are initially displaying the beginning of the hash, the -// preprocessor hides the left arrow. The user presses and holds the right -// button, which triggers the button handler to advance the displayIndex every -// 100ms. Each advance requires redisplaying the screen via UX_REDISPLAY(), -// and thus rerunning the preprocessor. As soon as the right button is -// pressed, the preprocessor detects that text has scrolled off the left side -// of the screen, so it unhides the left arrow; when the end of the hash is -// reached, it hides the right arrow. -// -// When the user has finished comparing the hashes, they press both buttons -// together, triggering ui_signHash_compare_button to prepare the approval -// screen and call UX_DISPLAY on ui_signHash_approve. A NULL preprocessor is -// specified for this screen, since we don't need to filter out any of its -// elements. We'll assume that the user presses the 'approve' button, causing -// the button handler to place the hash in G_io_apdu_buffer and call -// io_exchange_with_code, which sends the response APDU to the computer with -// the IO_RETURN_AFTER_TX flag set. The button handler then calls ui_idle, -// thus returning to the main menu. -// -// This completes the signHash command. Back in sia_main, io_exchange is still -// blocked, waiting for the computer to send a new request APDU. For the next -// section of this walkthrough, we will assume that the next APDU requests the -// getPublicKey command, so proceed to getPublicKey.c. + nbgl_layoutTagValueList_t tagValueList = {0}; + tagValueList.nbPairs = 1; + tagValueList.pairs = &pair; + + nbgl_useCaseReview(TYPE_MESSAGE, + &tagValueList, + &C_stax_app_sia_big, + ctx->typeStr, + NULL, + "Sign hash", + confirm_callback); +#endif + + return 0; +} diff --git a/src/txn.c b/src/txn.c index 74970cb..c1a0beb 100644 --- a/src/txn.c +++ b/src/txn.c @@ -1,347 +1,380 @@ #include "txn.h" -#include "sia.h" // For SW_DEVELOPER_ERR. Should be removed. -#include "os.h" +#include #include +#include "sia.h" // For SW_DEVELOPER_ERR. Should be removed. + static void divWW10(uint64_t u1, uint64_t u0, uint64_t *q, uint64_t *r) { - const uint64_t s = 60ULL; - const uint64_t v = 11529215046068469760ULL; - const uint64_t vn1 = 2684354560ULL; - const uint64_t _B2 = 4294967296ULL; - uint64_t un32 = u1<>(64-s); - uint64_t un10 = u0 << s; - uint64_t un1 = un10 >> 32; - uint64_t un0 = un10 & (_B2-1); - uint64_t q1 = un32 / vn1; - uint64_t rhat = un32 - q1*vn1; - - while (q1 >= _B2) { - q1--; - rhat += vn1; - if (rhat >= _B2) { - break; - } - } - - uint64_t un21 = un32*_B2 + un1 - q1*v; - uint64_t q0 = un21 / vn1; - rhat = un21 - q0*vn1; - - while (q0 >= _B2) { - q0--; - rhat += vn1; - if (rhat >= _B2) { - break; - } - } - - *q = q1*_B2 + q0; - *r = (un21*_B2 + un0 - q0*v) >> s; + const uint64_t s = 60ULL; + const uint64_t v = 11529215046068469760ULL; + const uint64_t vn1 = 2684354560ULL; + const uint64_t _B2 = 4294967296ULL; + uint64_t un32 = u1 << s | u0 >> (64 - s); + uint64_t un10 = u0 << s; + uint64_t un1 = un10 >> 32; + uint64_t un0 = un10 & (_B2 - 1); + uint64_t q1 = un32 / vn1; + uint64_t rhat = un32 - q1 * vn1; + + while (q1 >= _B2) { + q1--; + rhat += vn1; + if (rhat >= _B2) { + break; + } + } + + uint64_t un21 = un32 * _B2 + un1 - q1 * v; + uint64_t q0 = un21 / vn1; + rhat = un21 - q0 * vn1; + + while (q0 >= _B2) { + q0--; + rhat += vn1; + if (rhat >= _B2) { + break; + } + } + + *q = q1 * _B2 + q0; + *r = (un21 * _B2 + un0 - q0 * v) >> s; } static uint64_t quorem10(uint64_t nat[], int len) { - uint64_t r = 0; - for (int i = len - 1; i >= 0; i--) { - divWW10(r, nat[i], &nat[i], &r); - } - return r; + uint64_t r = 0; + for (int i = len - 1; i >= 0; i--) { + divWW10(r, nat[i], &nat[i], &r); + } + return r; } // cur2dec converts a Sia-encoded currency value to a decimal string and // appends a final NUL byte. It returns the length of the string. If the value // is too large, it throws TXN_STATE_ERR. -static int cur2dec(uint8_t *out, uint8_t *cur) { - if (cur[0] == 0) { - out[0] = '\0'; - return 0; - } - - // sanity check the size of the value. The size (in bytes) is given in the - // first byte; it should never be greater than 18 (18 bytes = 144 bits, - // i.e. a value of 2^144 H, or 22 quadrillion SC). - if (cur[0] > 18) { - THROW(TXN_STATE_ERR); - } - - - // convert big-endian uint8_t[] to little-endian uint64_t[] - // - // NOTE: the Sia encoding omits any leading zeros, so the first "uint64" - // may not be a full 8 bytes. We handle this by treating the length prefix - // as part of the first uint64. This is safe as long as the length prefix - // has only 1 non-zero byte, which should be enforced elsewhere. - uint64_t nat[32]; - int len = (cur[0] / 8) + ((cur[0] % 8) != 0); - cur += 8 - (len*8 - cur[0]); - for (int i = 0; i < len; i++) { - nat[len-i-1] = U8BE(cur, i*8); - } - - // decode digits into buf, right-to-left - // - // NOTE: buf must be large enough to hold the decimal representation of - // 2^144, which has 44 digits. - uint8_t buf[64]; - int i = sizeof(buf); - buf[--i] = '\0'; - while (len > 0) { - if (i <= 0) { - THROW(TXN_STATE_ERR); - } - buf[--i] = '0' + quorem10(nat, len); - // normalize nat - while (len > 0 && nat[len-1] == 0) { - len--; - } - } - - // copy buf->out, trimming whitespace - memmove(out, buf+i, sizeof(buf)-i); - return sizeof(buf)-i-1; +int cur2dec(char *out, uint8_t *cur) { + if (cur[0] == 0) { + out[0] = '\0'; + return 0; + } + + // sanity check the size of the value. The size (in bytes) is given in the + // first byte; it should never be greater than 18 (18 bytes = 144 bits, + // i.e. a value of 2^144 H, or 22 quadrillion SC). + if (cur[0] > 18) { + THROW(TXN_STATE_ERR); + } + + // convert big-endian uint8_t[] to little-endian uint64_t[] + // + // NOTE: the Sia encoding omits any leading zeros, so the first "uint64" + // may not be a full 8 bytes. We handle this by treating the length prefix + // as part of the first uint64. This is safe as long as the length prefix + // has only 1 non-zero byte, which should be enforced elsewhere. + uint64_t nat[32]; + int len = (cur[0] / 8) + ((cur[0] % 8) != 0); + const int zeros = (len * 8 - cur[0]); + cur += 1; + + nat[len - 1] = 0; + for (int i = 0; i < 8 - zeros; i++) { + nat[len - 1] <<= 8; + nat[len - 1] |= cur[i]; + } + cur += 8 - zeros; + for (int i = 1; i < len; i++) { + nat[len - i - 1] = U8BE(cur, (i - 1) * 8); + } + + // decode digits into buf, right-to-left + // + // NOTE: buf must be large enough to hold the decimal representation of + // 2^144, which has 44 digits. + uint8_t buf[64]; + int i = sizeof(buf); + buf[--i] = '\0'; + while (len > 0) { + if (i <= 0) { + THROW(TXN_STATE_ERR); + } + buf[--i] = '0' + quorem10(nat, len); + // normalize nat + while (len > 0 && nat[len - 1] == 0) { + len--; + } + } + + // copy buf->out, trimming whitespace + memmove(out, buf + i, sizeof(buf) - i); + return sizeof(buf) - i - 1; } static void need_at_least(txn_state_t *txn, uint64_t n) { - if ((txn->buflen - txn->pos) < n) { - THROW(TXN_STATE_PARTIAL); - } + if ((txn->buflen - txn->pos) < n) { + THROW(TXN_STATE_PARTIAL); + } } static void seek(txn_state_t *txn, uint64_t n) { - need_at_least(txn, n); - txn->pos += n; + need_at_least(txn, n); + txn->pos += n; } static void advance(txn_state_t *txn) { - // if elem is covered, add it to the hash - if (txn->elemType != TXN_ELEM_TXN_SIG) { - blake2b_update(&txn->blake, txn->buf, txn->pos); - } else if (txn->sliceIndex == txn->sigIndex && txn->pos >= 48) { - // add just the ParentID, Timelock, and PublicKeyIndex - blake2b_update(&txn->blake, txn->buf, 48); - } - - txn->buflen -= txn->pos; - memmove(txn->buf, txn->buf+txn->pos, txn->buflen); - txn->pos = 0; + // if elem is covered, add it to the hash + if (txn->elements[txn->elementIndex].elemType != TXN_ELEM_TXN_SIG) { + blake2b_update(&txn->blake, txn->buf, txn->pos); + } else if (txn->sliceIndex == txn->sigIndex && txn->pos >= 48) { + // add just the ParentID, Timelock, and PublicKeyIndex + blake2b_update(&txn->blake, txn->buf, 48); + } + + txn->buflen -= txn->pos; + memmove(txn->buf, txn->buf + txn->pos, txn->buflen); + txn->pos = 0; } static uint64_t readInt(txn_state_t *txn) { - need_at_least(txn, 8); - uint64_t u = U8LE(txn->buf, txn->pos); - seek(txn, 8); - return u; + need_at_least(txn, 8); + uint64_t u = U8LE(txn->buf, txn->pos); + seek(txn, 8); + return u; } static void readCurrency(txn_state_t *txn, uint8_t *outVal) { - uint64_t valLen = readInt(txn); - need_at_least(txn, valLen); - if (outVal) { - txn->valLen = cur2dec(outVal, txn->buf+txn->pos-8); - } - seek(txn, valLen); + uint64_t valLen = readInt(txn); + need_at_least(txn, valLen); + if (outVal) { + if (valLen > 16) { + THROW(TXN_STATE_ERR); + } + outVal[0] = valLen; + memmove(outVal + 1, txn->buf + txn->pos, valLen); + } + seek(txn, valLen); } static void readHash(txn_state_t *txn, char *outAddr) { - need_at_least(txn, 32); - if (outAddr) { - bin2hex(outAddr, txn->buf+txn->pos, 32); - uint8_t checksum[6]; - blake2b(checksum, sizeof(checksum), txn->buf+txn->pos, 32); - bin2hex(outAddr+64, checksum, sizeof(checksum)); - } - seek(txn, 32); + need_at_least(txn, 32); + if (outAddr) { + memmove(outAddr, txn->buf + txn->pos, 32); + } + seek(txn, 32); } static void readPrefixedBytes(txn_state_t *txn) { - uint64_t len = readInt(txn); - seek(txn, len); + uint64_t len = readInt(txn); + seek(txn, len); } static void readUnlockConditions(txn_state_t *txn) { - readInt(txn); // Timelock - uint64_t numKeys = readInt(txn); // PublicKeys - while (numKeys --> 0) { - seek(txn, 16); // Algorithm - readPrefixedBytes(txn); // Key - } - readInt(txn); // SignaturesRequired + readInt(txn); // Timelock + uint64_t numKeys = readInt(txn); // PublicKeys + while (numKeys-- > 0) { + seek(txn, 16); // Algorithm + readPrefixedBytes(txn); // Key + } + readInt(txn); // SignaturesRequired } static void readCoveredFields(txn_state_t *txn) { - need_at_least(txn, 1); - // for now, we require WholeTransaction = true - if (txn->buf[txn->pos] != 1) { - THROW(TXN_STATE_ERR); - } - seek(txn, 1); - // all other fields must be empty - for (int i = 0; i < 10; i++) { - if (readInt(txn) != 0) { - THROW(TXN_STATE_ERR); - } - } + need_at_least(txn, 1); + // for now, we require WholeTransaction = true + if (txn->buf[txn->pos] != 1) { + THROW(TXN_STATE_ERR); + } + seek(txn, 1); + // all other fields must be empty + for (int i = 0; i < 10; i++) { + if (readInt(txn) != 0) { + THROW(TXN_STATE_ERR); + } + } } static void addReplayProtection(cx_blake2b_t *S) { - // The official Sia Nano S app only signs transactions on the - // Foundation-supported chain. To use the app on a different chain, - // recompile the app with a different replayPrefix. - static uint8_t const replayPrefix[] = {1}; - blake2b_update(S, replayPrefix, 1); + // The official Sia Nano S app only signs transactions on the + // Foundation-supported chain. To use the app on a different chain, + // recompile the app with a different replayPrefix. + static uint8_t const replayPrefix[] = {1}; + blake2b_update(S, replayPrefix, 1); } // throws txnDecoderState_e static void __txn_next_elem(txn_state_t *txn) { - // if we're on a slice boundary, read the next length prefix and bump the - // element type - while (txn->sliceIndex == txn->sliceLen) { - if (txn->elemType == TXN_ELEM_TXN_SIG) { - // store final hash - blake2b_final(&txn->blake, txn->sigHash, sizeof(txn->sigHash)); - THROW(TXN_STATE_FINISHED); - } - txn->sliceLen = readInt(txn); - txn->sliceIndex = 0; - txn->displayIndex = 0; - txn->elemType++; - advance(txn); - - // if we've reached the TransactionSignatures, check that sigIndex is - // a valid index - if ((txn->elemType == TXN_ELEM_TXN_SIG) && (txn->sigIndex >= txn->sliceLen)) { - THROW(TXN_STATE_ERR); - } - } - - switch (txn->elemType) { - // these elements should be displayed - case TXN_ELEM_SC_OUTPUT: - readCurrency(txn, txn->outVal); // Value - readHash(txn, txn->outAddr); // UnlockHash - advance(txn); - txn->sliceIndex++; - if (!memcmp(txn->outAddr, txn->changeAddr, sizeof(txn->outAddr))) { - // do not display the change address or increment displayIndex - return; - } - txn->displayIndex++; - THROW(TXN_STATE_READY); - - case TXN_ELEM_SF_OUTPUT: - readCurrency(txn, txn->outVal); // Value - readHash(txn, txn->outAddr); // UnlockHash - readCurrency(txn, NULL); // ClaimStart - advance(txn); - txn->sliceIndex++; - txn->displayIndex++; - THROW(TXN_STATE_READY); - - case TXN_ELEM_MINER_FEE: - readCurrency(txn, txn->outVal); // Value - memmove(txn->outAddr, "[Miner Fee]", 12); - advance(txn); - txn->sliceIndex++; - THROW(TXN_STATE_READY); - - // these elements should be decoded, but not displayed - case TXN_ELEM_SC_INPUT: - readHash(txn, NULL); // ParentID - readUnlockConditions(txn); // UnlockConditions - addReplayProtection(&txn->blake); - advance(txn); - txn->sliceIndex++; - return; - - case TXN_ELEM_SF_INPUT: - readHash(txn, NULL); // ParentID - readUnlockConditions(txn); // UnlockConditions - readHash(txn, NULL); // ClaimUnlockHash - addReplayProtection(&txn->blake); - advance(txn); - txn->sliceIndex++; - return; - - case TXN_ELEM_TXN_SIG: - readHash(txn, NULL); // ParentID - readInt(txn); // PublicKeyIndex - readInt(txn); // Timelock - readCoveredFields(txn); // CoveredFields - readPrefixedBytes(txn); // Signature - advance(txn); - txn->sliceIndex++; - return; - - // these elements should not be present - case TXN_ELEM_FC: - case TXN_ELEM_FCR: - case TXN_ELEM_SP: - case TXN_ELEM_ARB_DATA: - if (txn->sliceLen != 0) { - THROW(TXN_STATE_ERR); - } - return; - } + // too many elements + if (txn->elementIndex == MAX_ELEMS) { + THROW(TXN_STATE_ERR); + } + // if we're on a slice boundary, read the next length prefix and bump the + // element type + while (txn->sliceIndex == txn->sliceLen) { + if (txn->elements[txn->elementIndex].elemType == TXN_ELEM_TXN_SIG) { + // store final hash + blake2b_final(&txn->blake, txn->sigHash, sizeof(txn->sigHash)); + THROW(TXN_STATE_FINISHED); + } + // too many elements + txn->sliceLen = readInt(txn); + txn->sliceIndex = 0; + txn->elements[txn->elementIndex].elemType++; + advance(txn); + + // if we've reached the TransactionSignatures, check that sigIndex is + // a valid index + if ((txn->elements[txn->elementIndex].elemType == TXN_ELEM_TXN_SIG) && + (txn->sigIndex >= txn->sliceLen)) { + THROW(TXN_STATE_ERR); + } + } + + switch (txn->elements[txn->elementIndex].elemType) { + // these elements should be displayed + case TXN_ELEM_SC_OUTPUT: + readCurrency(txn, txn->elements[txn->elementIndex].outVal); // Value + readHash(txn, (char *) txn->elements[txn->elementIndex].outAddr); // UnlockHash + advance(txn); + if (!memcmp(txn->elements[txn->elementIndex].outAddr, + txn->changeAddr, + sizeof(txn->elements[txn->elementIndex].outAddr))) { + // do not display the change address or increment displayIndex + return; + } + + txn->sliceIndex++; + txn->elements[txn->elementIndex + 1].elemType = + txn->elements[txn->elementIndex].elemType; + txn->elementIndex++; + return; + + case TXN_ELEM_SF_OUTPUT: + readCurrency(txn, txn->elements[txn->elementIndex].outVal); // Value + readHash(txn, (char *) txn->elements[txn->elementIndex].outAddr); // UnlockHash + readCurrency(txn, NULL); // ClaimStart + advance(txn); + + txn->sliceIndex++; + txn->elements[txn->elementIndex + 1].elemType = + txn->elements[txn->elementIndex].elemType; + txn->elementIndex++; + return; + + case TXN_ELEM_MINER_FEE: + readCurrency(txn, txn->elements[txn->elementIndex].outVal); // Value + memmove(txn->elements[txn->elementIndex].outAddr, "[Miner Fee]", 12); + advance(txn); + + txn->sliceIndex++; + txn->elements[txn->elementIndex + 1].elemType = + txn->elements[txn->elementIndex].elemType; + txn->elementIndex++; + return; + + // these elements should be decoded, but not displayed + case TXN_ELEM_SC_INPUT: + readHash(txn, NULL); // ParentID + readUnlockConditions(txn); // UnlockConditions + addReplayProtection(&txn->blake); + advance(txn); + txn->sliceIndex++; + return; + + case TXN_ELEM_SF_INPUT: + readHash(txn, NULL); // ParentID + readUnlockConditions(txn); // UnlockConditions + readHash(txn, NULL); // ClaimUnlockHash + addReplayProtection(&txn->blake); + advance(txn); + txn->sliceIndex++; + return; + + case TXN_ELEM_TXN_SIG: + readHash(txn, NULL); // ParentID + readInt(txn); // PublicKeyIndex + readInt(txn); // Timelock + readCoveredFields(txn); // CoveredFields + readPrefixedBytes(txn); // Signature + advance(txn); + txn->sliceIndex++; + return; + + // these elements should not be present + case TXN_ELEM_FC: + case TXN_ELEM_FCR: + case TXN_ELEM_SP: + case TXN_ELEM_ARB_DATA: + if (txn->sliceLen != 0) { + THROW(TXN_STATE_ERR); + } + return; + } } - -txnDecoderState_e txn_next_elem(txn_state_t *txn) { - // Like many transaction decoders, we use exceptions to jump out of deep - // call stacks when we encounter an error. There are two important rules - // for Ledger exceptions: declare modified variables as volatile, and do - // not THROW(0). Presumably, 0 is the sentinel value for "no exception - // thrown." So be very careful when throwing enums, since enums start at 0 - // by default. - volatile txnDecoderState_e result; - BEGIN_TRY { - TRY { - // read until we reach a displayable element or the end of the buffer - for (;;) { - __txn_next_elem(txn); - } - } - CATCH_OTHER(e) { - result = e; - } - FINALLY { - } - } - END_TRY; - if (txn->buflen + 255 > sizeof(txn->buf)) { - // we filled the buffer to max capacity, but there still wasn't enough - // to decode a full element. This generally means that the txn is - // corrupt in some way, since elements shouldn't be very large. - return TXN_STATE_ERR; - } - return result; +txnDecoderState_e txn_parse(txn_state_t *txn) { + // Like many transaction decoders, we use exceptions to jump out of deep + // call stacks when we encounter an error. There are two important rules + // for Ledger exceptions: declare modified variables as volatile, and do + // not THROW(0). Presumably, 0 is the sentinel value for "no exception + // thrown." So be very careful when throwing enums, since enums start at 0 + // by default. + volatile txnDecoderState_e result; + BEGIN_TRY { + TRY { + // read until we reach a displayable element or the end of the buffer + for (;;) { + __txn_next_elem(txn); + } + } + CATCH_OTHER(e) { + result = e; + } + FINALLY { + } + } + END_TRY; + if (txn->buflen + 255 > sizeof(txn->buf)) { + // we filled the buffer to max capacity, but there still wasn't enough + // to decode a full element. This generally means that the txn is + // corrupt in some way, since elements shouldn't be very large. + return TXN_STATE_ERR; + } + return result; } void txn_init(txn_state_t *txn, uint16_t sigIndex, uint32_t changeIndex) { - memset(txn, 0, sizeof(txn_state_t)); - txn->buflen = txn->pos = txn->sliceIndex = txn->displayIndex = txn->sliceLen = txn->valLen = 0; - txn->elemType = -1; // first increment brings it to SC_INPUT - txn->sigIndex = sigIndex; + memset(txn, 0, sizeof(txn_state_t)); + txn->sigIndex = sigIndex; - cx_ecfp_public_key_t publicKey = {0}; - deriveSiaKeypair(changeIndex, NULL, &publicKey); - pubkeyToSiaAddress(&txn->changeAddr, &publicKey); + txn->elementIndex = 0; + txn->elements[txn->elementIndex].elemType = -1; // first increment brings it to SC_INPUT - // initialize hash state - blake2b_init(&txn->blake); + uint8_t publicKey[65] = {0}; + deriveSiaPublicKey(changeIndex, publicKey); + pubkeyToSiaAddress((char *) &txn->changeAddr, publicKey); + + // initialize hash state + blake2b_init(&txn->blake); } void txn_update(txn_state_t *txn, uint8_t *in, uint8_t inlen) { - // the buffer should never overflow; any elements should always be drained - // before the next read. - if (txn->buflen + inlen > sizeof(txn->buf)) { - THROW(SW_DEVELOPER_ERR); - } - - // append to the buffer - memmove(txn->buf + txn->buflen, in, inlen); - txn->buflen += inlen; - - // reset the seek position; if we previously threw TXN_STATE_PARTIAL, now - // we can try decoding again from the beginning. - txn->pos = 0; + // the buffer should never overflow; any elements should always be drained + // before the next read. + if (txn->buflen + inlen > sizeof(txn->buf)) { + THROW(SW_DEVELOPER_ERR); + } + + // append to the buffer + memmove(txn->buf + txn->buflen, in, inlen); + txn->buflen += inlen; + + // reset the seek position; if we previously threw TXN_STATE_PARTIAL, now + // we can try decoding again from the beginning. + txn->pos = 0; +} + +void format_address(char *dst, uint8_t *src) { + bin2hex(dst, src, 32); + uint8_t checksum[6]; + blake2b(checksum, sizeof(checksum), src, 32); + bin2hex(dst + 64, checksum, sizeof(checksum)); } diff --git a/src/txn.h b/src/txn.h index 1bee2ac..bc3f53c 100644 --- a/src/txn.h +++ b/src/txn.h @@ -1,55 +1,67 @@ -#pragma once +#ifndef TXN_H +#define TXN_H + +#include #include "blake2b.h" -#include +#ifdef TARGET_NANOS +#define MAX_ELEMS 20 +#else +#define MAX_ELEMS 128 +#endif // macros for converting raw bytes to uint64_t -#define U8BE(buf, off) (((uint64_t)(U4BE(buf, off)) << 32) | ((uint64_t)(U4BE(buf, off + 4)) & 0xFFFFFFFF)) -#define U8LE(buf, off) (((uint64_t)(U4LE(buf, off + 4)) << 32) | ((uint64_t)(U4LE(buf, off)) & 0xFFFFFFFF)) +#define U8BE(buf, off) \ + (((uint64_t)(U4BE(buf, off)) << 32) | ((uint64_t)(U4BE(buf, off + 4)) & 0xFFFFFFFF)) +#define U8LE(buf, off) \ + (((uint64_t)(U4LE(buf, off + 4)) << 32) | ((uint64_t)(U4LE(buf, off)) & 0xFFFFFFFF)) // txnDecoderState_e indicates a transaction decoder status typedef enum { - TXN_STATE_ERR = 1, // invalid transaction (NOTE: it's illegal to THROW(0)) - TXN_STATE_PARTIAL, // no elements have been fully decoded yet - TXN_STATE_READY, // at least one element is fully decoded - TXN_STATE_FINISHED, // reached end of transaction + TXN_STATE_ERR = 1, // invalid transaction (NOTE: it's illegal to THROW(0)) + TXN_STATE_PARTIAL, // no elements have been fully decoded yet + TXN_STATE_FINISHED, // reached end of transaction } txnDecoderState_e; // txnElemType_e indicates a transaction element type. typedef enum { - TXN_ELEM_SC_INPUT, - TXN_ELEM_SC_OUTPUT, - TXN_ELEM_FC, - TXN_ELEM_FCR, - TXN_ELEM_SP, - TXN_ELEM_SF_INPUT, - TXN_ELEM_SF_OUTPUT, - TXN_ELEM_MINER_FEE, - TXN_ELEM_ARB_DATA, - TXN_ELEM_TXN_SIG, + TXN_ELEM_SC_INPUT, + TXN_ELEM_SC_OUTPUT, + TXN_ELEM_FC, + TXN_ELEM_FCR, + TXN_ELEM_SP, + TXN_ELEM_SF_INPUT, + TXN_ELEM_SF_OUTPUT, + TXN_ELEM_MINER_FEE, + TXN_ELEM_ARB_DATA, + TXN_ELEM_TXN_SIG, } txnElemType_e; +typedef struct { + uint8_t elemType; // type of element (txnElemType_e) + + uint8_t outVal[1 + 16]; // currency value, Sia-encoded + uint8_t outAddr[32]; // address, Sia-encoded +} txn_elem_t; + // txn_state_t is a helper object for computing the SigHash of a streamed // transaction. typedef struct { - uint8_t buf[510]; // holds raw tx bytes; large enough for two 0xFF reads - uint16_t buflen; // number of valid bytes in buf - uint16_t pos; // mid-decode offset; reset to 0 after each elem - - txnElemType_e elemType; // type of most-recently-seen element - uint64_t sliceLen; // most-recently-seen slice length prefix - uint16_t sliceIndex; // offset within current element slice - uint16_t displayIndex; // index of element being displayed - - uint16_t sigIndex; // index of TxnSig being computed - cx_blake2b_t blake; // hash state - uint8_t sigHash[32]; // buffer to hold final hash - - uint8_t outVal[128]; // most-recently-seen currency value, in decimal - uint8_t valLen; // length of outVal - uint8_t outAddr[77]; // most-recently-seen address - uint8_t changeAddr[77]; // change address + uint8_t buf[510]; // holds raw tx bytes; large enough for two 0xFF reads + uint16_t buflen; // number of valid bytes in buf + uint16_t pos; // mid-decode offset; reset to 0 after each elem + + uint16_t elementIndex; + txn_elem_t elements[MAX_ELEMS]; // only elements that will be displayed + + uint64_t sliceLen; // most-recently-seen slice length prefix + uint16_t sliceIndex; // offset within current element slice + + uint16_t sigIndex; // index of TxnSig being computed + uint8_t changeAddr[77]; // change address + cx_blake2b_t blake; // hash state + uint8_t sigHash[32]; // buffer to hold final hash } txn_state_t; // txn_init initializes a transaction decoder, preparing it to calculate the @@ -59,9 +71,20 @@ void txn_init(txn_state_t *txn, uint16_t sigIndex, uint32_t changeIndex); // txn_update adds data to a transaction decoder. void txn_update(txn_state_t *txn, uint8_t *in, uint8_t inlen); -// txn_next_elem decodes the next element of the transaction. If the element +// txn_parse decodes the the transaction. If elements // is ready for display, txn_next_elem returns TXN_STATE_READY. If more data // is required, it returns TXN_STATE_PARTIAL. If a decoding error is // encountered, it returns TXN_STATE_ERR. If the transaction has been fully // decoded, it returns TXN_STATE_FINISHED. -txnDecoderState_e txn_next_elem(txn_state_t *txn); +txnDecoderState_e txn_parse(txn_state_t *txn); + +// txn takes the Sia-encoded address in src and converts it to a hex encoded +// readable address in dst +void format_address(char *dst, uint8_t *src); + +// cur2dec converts a Sia-encoded currency value to a decimal string and +// appends a final NUL byte. It returns the length of the string. If the value +// is too large, it throws TXN_STATE_ERR. +int cur2dec(char *out, uint8_t *cur); + +#endif /* TXN_H */ \ No newline at end of file diff --git a/stax_app_sia.gif b/stax_app_sia.gif new file mode 100644 index 0000000..6d7ea0b Binary files /dev/null and b/stax_app_sia.gif differ diff --git a/stax_app_sia.png b/stax_app_sia.png new file mode 100644 index 0000000..f51aae5 Binary files /dev/null and b/stax_app_sia.png differ diff --git a/stax_app_sia_big.gif b/stax_app_sia_big.gif new file mode 100644 index 0000000..9cd653b Binary files /dev/null and b/stax_app_sia_big.gif differ diff --git a/stax_app_sia_big.png b/stax_app_sia_big.png new file mode 100644 index 0000000..325f195 Binary files /dev/null and b/stax_app_sia_big.png differ diff --git a/tests/application_client/__init__.py b/tests/application_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/application_client/__pycache__/__init__.cpython-39.pyc b/tests/application_client/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..6fb61c4 Binary files /dev/null and b/tests/application_client/__pycache__/__init__.cpython-39.pyc differ diff --git a/tests/application_client/__pycache__/boilerplate_command_sender.cpython-39.pyc b/tests/application_client/__pycache__/boilerplate_command_sender.cpython-39.pyc new file mode 100644 index 0000000..c4c5ad5 Binary files /dev/null and b/tests/application_client/__pycache__/boilerplate_command_sender.cpython-39.pyc differ diff --git a/tests/application_client/__pycache__/boilerplate_response_unpacker.cpython-39.pyc b/tests/application_client/__pycache__/boilerplate_response_unpacker.cpython-39.pyc new file mode 100644 index 0000000..145b9d0 Binary files /dev/null and b/tests/application_client/__pycache__/boilerplate_response_unpacker.cpython-39.pyc differ diff --git a/tests/application_client/__pycache__/boilerplate_transaction.cpython-39.pyc b/tests/application_client/__pycache__/boilerplate_transaction.cpython-39.pyc new file mode 100644 index 0000000..c51c509 Binary files /dev/null and b/tests/application_client/__pycache__/boilerplate_transaction.cpython-39.pyc differ diff --git a/tests/application_client/__pycache__/boilerplate_utils.cpython-39.pyc b/tests/application_client/__pycache__/boilerplate_utils.cpython-39.pyc new file mode 100644 index 0000000..9eaed6f Binary files /dev/null and b/tests/application_client/__pycache__/boilerplate_utils.cpython-39.pyc differ diff --git a/tests/application_client/boilerplate_command_sender.py b/tests/application_client/boilerplate_command_sender.py new file mode 100644 index 0000000..1c3d5fa --- /dev/null +++ b/tests/application_client/boilerplate_command_sender.py @@ -0,0 +1,155 @@ +from enum import IntEnum +from typing import Generator, List, Optional +from contextlib import contextmanager + +from ragger.backend.interface import BackendInterface, RAPDU + + +MAX_APDU_LEN: int = 255 + +CLA: int = 0xE0 + + +class P1(IntEnum): + # Parameter 1 for first APDU number. + P1_START = 0x00 + P1_MORE = 0x80 + + +class P2(IntEnum): + # Parameter 2 for last APDU to receive. + P2_LAST = 0x00 + # Parameter 2 for more APDU to receive. + P2_MORE = 0x80 + + P2_DISPLAY_ADDRESS = 0x00 + P2_DISPLAY_PUBKEY = 0x01 + + P2_DISPLAY_HASH = 0x00 + P2_SIGN_HASH = 0x01 + + +class InsType(IntEnum): + GET_VERSION = 0x01 + GET_PUBLIC_KEY = 0x02 + SIGN_HASH = 0x04 + GET_TXN_HASH = 0x08 + + +class Errors(IntEnum): + SW_OK = 0x9000 + SW_INVALID_PARAM = 0x681 + + SW_DENY = 0x6985 + SW_WRONG_P1P2 = 0x6A86 + SW_WRONG_DATA_LENGTH = 0x6A87 + SW_INS_NOT_SUPPORTED = 0x6D00 + SW_CLA_NOT_SUPPORTED = 0x6E00 + SW_WRONG_RESPONSE_LENGTH = 0xB000 + SW_DISPLAY_BIP32_PATH_FAIL = 0xB001 + SW_DISPLAY_ADDRESS_FAIL = 0xB002 + SW_DISPLAY_AMOUNT_FAIL = 0xB003 + SW_WRONG_TX_LENGTH = 0xB004 + SW_TX_PARSING_FAIL = 0xB005 + SW_TX_HASH_FAIL = 0xB006 + SW_BAD_STATE = 0xB007 + SW_SIGNATURE_FAIL = 0xB008 + + +def split_message(message: bytes, max_size: int) -> List[bytes]: + return [message[x : x + max_size] for x in range(0, len(message), max_size)] + + +class BoilerplateCommandSender: + def __init__(self, backend: BackendInterface) -> None: + self.backend = backend + + def get_app_and_version(self) -> RAPDU: + return self.backend.exchange( + cla=0xB0, # specific CLA for BOLOS + ins=0x01, # specific INS for get_app_and_version + p1=P1.P1_START, + p2=P2.P2_LAST, + data=b"", + ) + + def get_version(self) -> RAPDU: + return self.backend.exchange( + cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START, p2=P2.P2_LAST, data=b"" + ) + + @contextmanager + def get_address_with_confirmation(self, index: int) -> Generator[None, None, None]: + with self.backend.exchange_async( + cla=CLA, + ins=InsType.GET_PUBLIC_KEY, + p1=P1.P1_START, + p2=P2.P2_DISPLAY_ADDRESS, + data=index.to_bytes(4, "little", signed=False), + ) as response: + yield response + + @contextmanager + def get_public_key_with_confirmation( + self, index: int + ) -> Generator[None, None, None]: + with self.backend.exchange_async( + cla=CLA, + ins=InsType.GET_PUBLIC_KEY, + p1=P1.P1_START, + p2=P2.P2_DISPLAY_PUBKEY, + data=index.to_bytes(4, "little", signed=False), + ) as response: + yield response + + @contextmanager + def sign_hash_with_confirmation( + self, index: int, to_sign: bytes + ) -> Generator[None, None, None]: + with self.backend.exchange_async( + cla=CLA, + ins=InsType.SIGN_HASH, + p1=P1.P1_START, + p2=P2.P2_DISPLAY_PUBKEY, + data=index.to_bytes(4, "little", signed=False) + to_sign[:32], + ) as response: + yield response + + @contextmanager + def sign_tx( + self, + key_index: int, + sig_index: int, + change_index: int, + transaction: bytes, + ) -> Generator[None, None, None]: + p1 = P1.P1_START + messages = split_message( + key_index.to_bytes(4, "little", signed=False) + + sig_index.to_bytes(2, "little", signed=False) + + change_index.to_bytes(4, "little", signed=False) + + transaction, + MAX_APDU_LEN, + ) + for i in range(len(messages) - 1): + with self.backend.exchange_async( + cla=CLA, + ins=InsType.GET_TXN_HASH, + p1=p1, + p2=P2.P2_SIGN_HASH, + data=messages[i], + ) as response: + pass + p1 = P1.P1_MORE + + with self.backend.exchange_async( + cla=CLA, + ins=InsType.GET_TXN_HASH, + p1=p1, + p2=P2.P2_SIGN_HASH, + data=messages[-1], + ) as response: + yield response + + def get_async_response(self) -> Optional[RAPDU]: + return self.backend.last_async_response diff --git a/tests/application_client/boilerplate_response_unpacker.py b/tests/application_client/boilerplate_response_unpacker.py new file mode 100644 index 0000000..d80d148 --- /dev/null +++ b/tests/application_client/boilerplate_response_unpacker.py @@ -0,0 +1,76 @@ +from typing import Tuple +from struct import unpack + +# remainder, data_len, data +def pop_sized_buf_from_buffer(buffer: bytes, size: int) -> Tuple[bytes, bytes]: + return buffer[size:], buffer[0:size] + + +# remainder, data_len, data +def pop_size_prefixed_buf_from_buf(buffer: bytes) -> Tuple[bytes, int, bytes]: + data_len = buffer[0] + return buffer[1 + data_len :], data_len, buffer[1 : data_len + 1] + + +# Unpack from response: +# response = app_name (var) +def unpack_get_app_name_response(response: bytes) -> str: + return response.decode("ascii") + + +# Unpack from response: +# response = MAJOR (1) +# MINOR (1) +# PATCH (1) +def unpack_get_version_response(response: bytes) -> Tuple[int, int, int]: + assert len(response) == 3 + major, minor, patch = unpack("BBB", response) + return (major, minor, patch) + + +# Unpack from response: +# response = format_id (1) +# app_name_raw_len (1) +# app_name_raw (var) +# version_raw_len (1) +# version_raw (var) +# unused_len (1) +# unused (var) +def unpack_get_app_and_version_response(response: bytes) -> Tuple[str, str]: + response, _ = pop_sized_buf_from_buffer(response, 1) + response, _, app_name_raw = pop_size_prefixed_buf_from_buf(response) + response, _, version_raw = pop_size_prefixed_buf_from_buf(response) + response, _, _ = pop_size_prefixed_buf_from_buf(response) + + assert len(response) == 0 + + return app_name_raw.decode("ascii"), version_raw.decode("ascii") + + +# Unpack from response: +# response = pub_key_len (1) +# pub_key (var) +# chain_code_len (1) +# chain_code (var) +def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes, int, bytes]: + response, pub_key_len, pub_key = pop_size_prefixed_buf_from_buf(response) + response, chain_code_len, chain_code = pop_size_prefixed_buf_from_buf(response) + + assert pub_key_len == 65 + assert chain_code_len == 32 + assert len(response) == 0 + + return pub_key_len, pub_key, chain_code_len, chain_code + + +# Unpack from response: +# response = der_sig_len (1) +# der_sig (var) +# v (1) +def unpack_sign_tx_response(response: bytes) -> Tuple[int, bytes, int]: + response, der_sig_len, der_sig = pop_size_prefixed_buf_from_buf(response) + response, v = pop_sized_buf_from_buffer(response, 1) + + assert len(response) == 0 + + return der_sig_len, der_sig, int.from_bytes(v, byteorder="big") diff --git a/tests/application_client/boilerplate_utils.py b/tests/application_client/boilerplate_utils.py new file mode 100644 index 0000000..6401390 --- /dev/null +++ b/tests/application_client/boilerplate_utils.py @@ -0,0 +1,60 @@ +from io import BytesIO +from typing import Optional, Literal + + +UINT64_MAX: int = 2**64 - 1 +UINT32_MAX: int = 2**32 - 1 +UINT16_MAX: int = 2**16 - 1 + + +def write_varint(n: int) -> bytes: + if n < 0xFC: + return n.to_bytes(1, byteorder="little") + + if n <= UINT16_MAX: + return b"\xFD" + n.to_bytes(2, byteorder="little") + + if n <= UINT32_MAX: + return b"\xFE" + n.to_bytes(4, byteorder="little") + + if n <= UINT64_MAX: + return b"\xFF" + n.to_bytes(8, byteorder="little") + + raise ValueError(f"Can't write to varint: '{n}'!") + + +def read_varint(buf: BytesIO, prefix: Optional[bytes] = None) -> int: + b: bytes = prefix if prefix else buf.read(1) + + if not b: + raise ValueError(f"Can't read prefix: '{b.hex()}'!") + + n: int = {b"\xfd": 2, b"\xfe": 4, b"\xff": 8}.get(b, 1) # default to 1 + + b = buf.read(n) if n > 1 else b + + if len(b) != n: + raise ValueError("Can't read varint!") + + return int.from_bytes(b, byteorder="little") + + +def read(buf: BytesIO, size: int) -> bytes: + b: bytes = buf.read(size) + + if len(b) < size: + raise ValueError(f"Can't read {size} bytes in buffer!") + + return b + + +def read_uint( + buf: BytesIO, bit_len: int, byteorder: Literal["big", "little"] = "little" +) -> int: + size: int = bit_len // 8 + b: bytes = buf.read(size) + + if len(b) < size: + raise ValueError(f"Can't read u{bit_len} in buffer!") + + return int.from_bytes(b, byteorder) diff --git a/tests/application_client/py.typed b/tests/application_client/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c959044 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +from ragger.conftest import configuration + +########################### +### CONFIGURATION START ### +########################### + +# You can configure optional parameters by overriding the value of ragger.configuration.OPTIONAL_CONFIGURATION +# Please refer to ragger/conftest/configuration.py for their descriptions and accepted values + +######################### +### CONFIGURATION END ### +######################### + +# Pull all features from the base ragger conftest using the overridden configuration +pytest_plugins = ("ragger.conftest.base_conftest",) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..bb30da6 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,7 @@ +pytest +ragger[speculos]>=1.6.0 +ragger[ledgerwallet]>=1.6.0 +Jinja2==3.1.2 +Flask==2.1.2 +ecdsa>=0.16.1,<0.17.0 +Werkzeug==2.2.2 \ No newline at end of file diff --git a/tests/setup.cfg b/tests/setup.cfg new file mode 100644 index 0000000..7d0d7e3 --- /dev/null +++ b/tests/setup.cfg @@ -0,0 +1,21 @@ +[tool:pytest] +addopts = --strict-markers + +[pylint] +disable = C0114, # missing-module-docstring + C0115, # missing-class-docstring + C0116, # missing-function-docstring + C0103, # invalid-name + R0801, # duplicate-code + R0913 # too-many-arguments +max-line-length=100 +extension-pkg-whitelist=hid + +[pycodestyle] +max-line-length = 100 + +[mypy-hid.*] +ignore_missing_imports = True + +[mypy-pytest.*] +ignore_missing_imports = True diff --git a/tests/snapshots/nanos/test_app_mainmenu/00000.png b/tests/snapshots/nanos/test_app_mainmenu/00000.png new file mode 100644 index 0000000..961c1e6 Binary files /dev/null and b/tests/snapshots/nanos/test_app_mainmenu/00000.png differ diff --git a/tests/snapshots/nanos/test_app_mainmenu/00001.png b/tests/snapshots/nanos/test_app_mainmenu/00001.png new file mode 100644 index 0000000..bed12a6 Binary files /dev/null and b/tests/snapshots/nanos/test_app_mainmenu/00001.png differ diff --git a/tests/snapshots/nanos/test_app_mainmenu/00002.png b/tests/snapshots/nanos/test_app_mainmenu/00002.png new file mode 100644 index 0000000..f79db06 Binary files /dev/null and b/tests/snapshots/nanos/test_app_mainmenu/00002.png differ diff --git a/tests/snapshots/nanos/test_app_mainmenu/00003.png b/tests/snapshots/nanos/test_app_mainmenu/00003.png new file mode 100644 index 0000000..961c1e6 Binary files /dev/null and b/tests/snapshots/nanos/test_app_mainmenu/00003.png differ diff --git a/tests/snapshots/nanos/test_get_address_confirm_accepted/00000.png b/tests/snapshots/nanos/test_get_address_confirm_accepted/00000.png new file mode 100644 index 0000000..a3fa4ba Binary files /dev/null and b/tests/snapshots/nanos/test_get_address_confirm_accepted/00000.png differ diff --git a/tests/snapshots/nanos/test_get_address_confirm_accepted/00001.png b/tests/snapshots/nanos/test_get_address_confirm_accepted/00001.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_get_address_confirm_accepted/00001.png differ diff --git a/tests/snapshots/nanos/test_get_address_confirm_accepted/00002.png b/tests/snapshots/nanos/test_get_address_confirm_accepted/00002.png new file mode 100644 index 0000000..73f2029 Binary files /dev/null and b/tests/snapshots/nanos/test_get_address_confirm_accepted/00002.png differ diff --git a/tests/snapshots/nanos/test_get_address_confirm_refused/00000.png b/tests/snapshots/nanos/test_get_address_confirm_refused/00000.png new file mode 100644 index 0000000..a3fa4ba Binary files /dev/null and b/tests/snapshots/nanos/test_get_address_confirm_refused/00000.png differ diff --git a/tests/snapshots/nanos/test_get_address_confirm_refused/00001.png b/tests/snapshots/nanos/test_get_address_confirm_refused/00001.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_get_address_confirm_refused/00001.png differ diff --git a/tests/snapshots/nanos/test_get_address_confirm_refused/00002.png b/tests/snapshots/nanos/test_get_address_confirm_refused/00002.png new file mode 100644 index 0000000..9c7e704 Binary files /dev/null and b/tests/snapshots/nanos/test_get_address_confirm_refused/00002.png differ diff --git a/tests/snapshots/nanos/test_get_address_confirm_refused/00003.png b/tests/snapshots/nanos/test_get_address_confirm_refused/00003.png new file mode 100644 index 0000000..961c1e6 Binary files /dev/null and b/tests/snapshots/nanos/test_get_address_confirm_refused/00003.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00000.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00000.png new file mode 100644 index 0000000..100ac31 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00000.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00001.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00001.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00001.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00002.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00002.png new file mode 100644 index 0000000..ee0a343 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00002.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00000.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00000.png new file mode 100644 index 0000000..100ac31 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00000.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00001.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00001.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00001.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00002.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00002.png new file mode 100644 index 0000000..9c7e704 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00002.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00003.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00003.png new file mode 100644 index 0000000..961c1e6 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_accept/00000.png b/tests/snapshots/nanos/test_sign_hash_accept/00000.png new file mode 100644 index 0000000..f64f1ae Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_accept/00000.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_accept/00001.png b/tests/snapshots/nanos/test_sign_hash_accept/00001.png new file mode 100644 index 0000000..5aa5a16 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_accept/00001.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_accept/00002.png b/tests/snapshots/nanos/test_sign_hash_accept/00002.png new file mode 100644 index 0000000..a12c83d Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_accept/00002.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_accept/00003.png b/tests/snapshots/nanos/test_sign_hash_accept/00003.png new file mode 100644 index 0000000..c0c57ec Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_accept/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_accept/00004.png b/tests/snapshots/nanos/test_sign_hash_accept/00004.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_accept/00004.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_accept/00005.png b/tests/snapshots/nanos/test_sign_hash_accept/00005.png new file mode 100644 index 0000000..961c1e6 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_accept/00005.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_reject/00000.png b/tests/snapshots/nanos/test_sign_hash_reject/00000.png new file mode 100644 index 0000000..f64f1ae Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_reject/00000.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_reject/00001.png b/tests/snapshots/nanos/test_sign_hash_reject/00001.png new file mode 100644 index 0000000..5aa5a16 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_reject/00001.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_reject/00002.png b/tests/snapshots/nanos/test_sign_hash_reject/00002.png new file mode 100644 index 0000000..a12c83d Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_reject/00002.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_reject/00003.png b/tests/snapshots/nanos/test_sign_hash_reject/00003.png new file mode 100644 index 0000000..c0c57ec Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_reject/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_reject/00004.png b/tests/snapshots/nanos/test_sign_hash_reject/00004.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_reject/00004.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_reject/00005.png b/tests/snapshots/nanos/test_sign_hash_reject/00005.png new file mode 100644 index 0000000..9c7e704 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_reject/00005.png differ diff --git a/tests/snapshots/nanos/test_sign_hash_reject/00006.png b/tests/snapshots/nanos/test_sign_hash_reject/00006.png new file mode 100644 index 0000000..961c1e6 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_hash_reject/00006.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00000.png b/tests/snapshots/nanos/test_sign_tx_accept/00000.png new file mode 100644 index 0000000..ae964a3 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00000.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00001.png b/tests/snapshots/nanos/test_sign_tx_accept/00001.png new file mode 100644 index 0000000..988b91c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00001.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00002.png b/tests/snapshots/nanos/test_sign_tx_accept/00002.png new file mode 100644 index 0000000..037a2be Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00002.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00003.png b/tests/snapshots/nanos/test_sign_tx_accept/00003.png new file mode 100644 index 0000000..8fa05a7 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00004.png b/tests/snapshots/nanos/test_sign_tx_accept/00004.png new file mode 100644 index 0000000..6f53a3d Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00004.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00005.png b/tests/snapshots/nanos/test_sign_tx_accept/00005.png new file mode 100644 index 0000000..b1df415 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00005.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00006.png b/tests/snapshots/nanos/test_sign_tx_accept/00006.png new file mode 100644 index 0000000..25032bd Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00006.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00007.png b/tests/snapshots/nanos/test_sign_tx_accept/00007.png new file mode 100644 index 0000000..065a169 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00007.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00008.png b/tests/snapshots/nanos/test_sign_tx_accept/00008.png new file mode 100644 index 0000000..3c0b5e6 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00008.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00009.png b/tests/snapshots/nanos/test_sign_tx_accept/00009.png new file mode 100644 index 0000000..640c845 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00009.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00010.png b/tests/snapshots/nanos/test_sign_tx_accept/00010.png new file mode 100644 index 0000000..4498c98 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00010.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00011.png b/tests/snapshots/nanos/test_sign_tx_accept/00011.png new file mode 100644 index 0000000..7b387da Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00011.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00012.png b/tests/snapshots/nanos/test_sign_tx_accept/00012.png new file mode 100644 index 0000000..f87534b Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00012.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00013.png b/tests/snapshots/nanos/test_sign_tx_accept/00013.png new file mode 100644 index 0000000..7bbc4dc Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00013.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00014.png b/tests/snapshots/nanos/test_sign_tx_accept/00014.png new file mode 100644 index 0000000..3e92d46 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00014.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00015.png b/tests/snapshots/nanos/test_sign_tx_accept/00015.png new file mode 100644 index 0000000..7669f95 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00015.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00016.png b/tests/snapshots/nanos/test_sign_tx_accept/00016.png new file mode 100644 index 0000000..bd70734 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00016.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00017.png b/tests/snapshots/nanos/test_sign_tx_accept/00017.png new file mode 100644 index 0000000..d913366 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00017.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00018.png b/tests/snapshots/nanos/test_sign_tx_accept/00018.png new file mode 100644 index 0000000..c6f2da2 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00018.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00019.png b/tests/snapshots/nanos/test_sign_tx_accept/00019.png new file mode 100644 index 0000000..6a1ac72 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00019.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00020.png b/tests/snapshots/nanos/test_sign_tx_accept/00020.png new file mode 100644 index 0000000..11c4fc2 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00020.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00021.png b/tests/snapshots/nanos/test_sign_tx_accept/00021.png new file mode 100644 index 0000000..7139b49 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00021.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00022.png b/tests/snapshots/nanos/test_sign_tx_accept/00022.png new file mode 100644 index 0000000..541da48 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00022.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00023.png b/tests/snapshots/nanos/test_sign_tx_accept/00023.png new file mode 100644 index 0000000..8c5d86d Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00023.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00024.png b/tests/snapshots/nanos/test_sign_tx_accept/00024.png new file mode 100644 index 0000000..1cbad7f Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00024.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00025.png b/tests/snapshots/nanos/test_sign_tx_accept/00025.png new file mode 100644 index 0000000..8846d33 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00025.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00026.png b/tests/snapshots/nanos/test_sign_tx_accept/00026.png new file mode 100644 index 0000000..1c46447 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00026.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00027.png b/tests/snapshots/nanos/test_sign_tx_accept/00027.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00027.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_accept/00028.png b/tests/snapshots/nanos/test_sign_tx_accept/00028.png new file mode 100644 index 0000000..961c1e6 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_accept/00028.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00000.png b/tests/snapshots/nanos/test_sign_tx_refused/00000.png new file mode 100644 index 0000000..ae964a3 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00000.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00001.png b/tests/snapshots/nanos/test_sign_tx_refused/00001.png new file mode 100644 index 0000000..988b91c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00001.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00002.png b/tests/snapshots/nanos/test_sign_tx_refused/00002.png new file mode 100644 index 0000000..037a2be Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00002.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00003.png b/tests/snapshots/nanos/test_sign_tx_refused/00003.png new file mode 100644 index 0000000..8fa05a7 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00004.png b/tests/snapshots/nanos/test_sign_tx_refused/00004.png new file mode 100644 index 0000000..6f53a3d Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00004.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00005.png b/tests/snapshots/nanos/test_sign_tx_refused/00005.png new file mode 100644 index 0000000..b1df415 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00005.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00006.png b/tests/snapshots/nanos/test_sign_tx_refused/00006.png new file mode 100644 index 0000000..25032bd Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00006.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00007.png b/tests/snapshots/nanos/test_sign_tx_refused/00007.png new file mode 100644 index 0000000..065a169 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00007.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00008.png b/tests/snapshots/nanos/test_sign_tx_refused/00008.png new file mode 100644 index 0000000..3c0b5e6 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00008.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00009.png b/tests/snapshots/nanos/test_sign_tx_refused/00009.png new file mode 100644 index 0000000..640c845 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00009.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00010.png b/tests/snapshots/nanos/test_sign_tx_refused/00010.png new file mode 100644 index 0000000..4498c98 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00010.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00011.png b/tests/snapshots/nanos/test_sign_tx_refused/00011.png new file mode 100644 index 0000000..7b387da Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00011.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00012.png b/tests/snapshots/nanos/test_sign_tx_refused/00012.png new file mode 100644 index 0000000..f87534b Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00012.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00013.png b/tests/snapshots/nanos/test_sign_tx_refused/00013.png new file mode 100644 index 0000000..7bbc4dc Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00013.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00014.png b/tests/snapshots/nanos/test_sign_tx_refused/00014.png new file mode 100644 index 0000000..3e92d46 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00014.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00015.png b/tests/snapshots/nanos/test_sign_tx_refused/00015.png new file mode 100644 index 0000000..7669f95 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00015.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00016.png b/tests/snapshots/nanos/test_sign_tx_refused/00016.png new file mode 100644 index 0000000..bd70734 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00016.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00017.png b/tests/snapshots/nanos/test_sign_tx_refused/00017.png new file mode 100644 index 0000000..d913366 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00017.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00018.png b/tests/snapshots/nanos/test_sign_tx_refused/00018.png new file mode 100644 index 0000000..c6f2da2 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00018.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00019.png b/tests/snapshots/nanos/test_sign_tx_refused/00019.png new file mode 100644 index 0000000..6a1ac72 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00019.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00020.png b/tests/snapshots/nanos/test_sign_tx_refused/00020.png new file mode 100644 index 0000000..11c4fc2 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00020.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00021.png b/tests/snapshots/nanos/test_sign_tx_refused/00021.png new file mode 100644 index 0000000..7139b49 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00021.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00022.png b/tests/snapshots/nanos/test_sign_tx_refused/00022.png new file mode 100644 index 0000000..541da48 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00022.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00023.png b/tests/snapshots/nanos/test_sign_tx_refused/00023.png new file mode 100644 index 0000000..8c5d86d Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00023.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00024.png b/tests/snapshots/nanos/test_sign_tx_refused/00024.png new file mode 100644 index 0000000..1cbad7f Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00024.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00025.png b/tests/snapshots/nanos/test_sign_tx_refused/00025.png new file mode 100644 index 0000000..8846d33 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00025.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00026.png b/tests/snapshots/nanos/test_sign_tx_refused/00026.png new file mode 100644 index 0000000..1c46447 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00026.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00027.png b/tests/snapshots/nanos/test_sign_tx_refused/00027.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00027.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00028.png b/tests/snapshots/nanos/test_sign_tx_refused/00028.png new file mode 100644 index 0000000..9c7e704 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00028.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00029.png b/tests/snapshots/nanos/test_sign_tx_refused/00029.png new file mode 100644 index 0000000..961c1e6 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00029.png differ diff --git a/tests/snapshots/nanosp/test_app_mainmenu/00000.png b/tests/snapshots/nanosp/test_app_mainmenu/00000.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanosp/test_app_mainmenu/00000.png differ diff --git a/tests/snapshots/nanosp/test_app_mainmenu/00001.png b/tests/snapshots/nanosp/test_app_mainmenu/00001.png new file mode 100644 index 0000000..7aa1937 Binary files /dev/null and b/tests/snapshots/nanosp/test_app_mainmenu/00001.png differ diff --git a/tests/snapshots/nanosp/test_app_mainmenu/00002.png b/tests/snapshots/nanosp/test_app_mainmenu/00002.png new file mode 100644 index 0000000..bad4cea Binary files /dev/null and b/tests/snapshots/nanosp/test_app_mainmenu/00002.png differ diff --git a/tests/snapshots/nanosp/test_app_mainmenu/00003.png b/tests/snapshots/nanosp/test_app_mainmenu/00003.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanosp/test_app_mainmenu/00003.png differ diff --git a/tests/snapshots/nanosp/test_get_address_confirm_accepted/00000.png b/tests/snapshots/nanosp/test_get_address_confirm_accepted/00000.png new file mode 100644 index 0000000..c56938b Binary files /dev/null and b/tests/snapshots/nanosp/test_get_address_confirm_accepted/00000.png differ diff --git a/tests/snapshots/nanosp/test_get_address_confirm_accepted/00001.png b/tests/snapshots/nanosp/test_get_address_confirm_accepted/00001.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_address_confirm_accepted/00001.png differ diff --git a/tests/snapshots/nanosp/test_get_address_confirm_accepted/00002.png b/tests/snapshots/nanosp/test_get_address_confirm_accepted/00002.png new file mode 100644 index 0000000..6f4d8ab Binary files /dev/null and b/tests/snapshots/nanosp/test_get_address_confirm_accepted/00002.png differ diff --git a/tests/snapshots/nanosp/test_get_address_confirm_refused/00000.png b/tests/snapshots/nanosp/test_get_address_confirm_refused/00000.png new file mode 100644 index 0000000..c56938b Binary files /dev/null and b/tests/snapshots/nanosp/test_get_address_confirm_refused/00000.png differ diff --git a/tests/snapshots/nanosp/test_get_address_confirm_refused/00001.png b/tests/snapshots/nanosp/test_get_address_confirm_refused/00001.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_address_confirm_refused/00001.png differ diff --git a/tests/snapshots/nanosp/test_get_address_confirm_refused/00002.png b/tests/snapshots/nanosp/test_get_address_confirm_refused/00002.png new file mode 100644 index 0000000..e90cd9d Binary files /dev/null and b/tests/snapshots/nanosp/test_get_address_confirm_refused/00002.png differ diff --git a/tests/snapshots/nanosp/test_get_address_confirm_refused/00003.png b/tests/snapshots/nanosp/test_get_address_confirm_refused/00003.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_address_confirm_refused/00003.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00000.png b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00000.png new file mode 100644 index 0000000..9c80384 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00000.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00001.png b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00001.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00001.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00002.png b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00002.png new file mode 100644 index 0000000..de29aa8 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00002.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00000.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00000.png new file mode 100644 index 0000000..9c80384 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00000.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00001.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00001.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00001.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00002.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00002.png new file mode 100644 index 0000000..e90cd9d Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00002.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00003.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00003.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_hash_accept/00000.png b/tests/snapshots/nanosp/test_sign_hash_accept/00000.png new file mode 100644 index 0000000..aa1c990 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_hash_accept/00000.png differ diff --git a/tests/snapshots/nanosp/test_sign_hash_accept/00001.png b/tests/snapshots/nanosp/test_sign_hash_accept/00001.png new file mode 100644 index 0000000..276e191 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_hash_accept/00001.png differ diff --git a/tests/snapshots/nanosp/test_sign_hash_accept/00002.png b/tests/snapshots/nanosp/test_sign_hash_accept/00002.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_hash_accept/00002.png differ diff --git a/tests/snapshots/nanosp/test_sign_hash_accept/00003.png b/tests/snapshots/nanosp/test_sign_hash_accept/00003.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_hash_accept/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_hash_reject/00000.png b/tests/snapshots/nanosp/test_sign_hash_reject/00000.png new file mode 100644 index 0000000..aa1c990 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_hash_reject/00000.png differ diff --git a/tests/snapshots/nanosp/test_sign_hash_reject/00001.png b/tests/snapshots/nanosp/test_sign_hash_reject/00001.png new file mode 100644 index 0000000..276e191 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_hash_reject/00001.png differ diff --git a/tests/snapshots/nanosp/test_sign_hash_reject/00002.png b/tests/snapshots/nanosp/test_sign_hash_reject/00002.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_hash_reject/00002.png differ diff --git a/tests/snapshots/nanosp/test_sign_hash_reject/00003.png b/tests/snapshots/nanosp/test_sign_hash_reject/00003.png new file mode 100644 index 0000000..e90cd9d Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_hash_reject/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_hash_reject/00004.png b/tests/snapshots/nanosp/test_sign_hash_reject/00004.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_hash_reject/00004.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00000.png b/tests/snapshots/nanosp/test_sign_tx_accept/00000.png new file mode 100644 index 0000000..72c5e8e Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00000.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00001.png b/tests/snapshots/nanosp/test_sign_tx_accept/00001.png new file mode 100644 index 0000000..3a8465b Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00001.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00002.png b/tests/snapshots/nanosp/test_sign_tx_accept/00002.png new file mode 100644 index 0000000..fd0b1d6 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00002.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00003.png b/tests/snapshots/nanosp/test_sign_tx_accept/00003.png new file mode 100644 index 0000000..5f0f44c Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00004.png b/tests/snapshots/nanosp/test_sign_tx_accept/00004.png new file mode 100644 index 0000000..5875f0b Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00004.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00005.png b/tests/snapshots/nanosp/test_sign_tx_accept/00005.png new file mode 100644 index 0000000..a661596 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00005.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00006.png b/tests/snapshots/nanosp/test_sign_tx_accept/00006.png new file mode 100644 index 0000000..dcef220 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00006.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00007.png b/tests/snapshots/nanosp/test_sign_tx_accept/00007.png new file mode 100644 index 0000000..c48c0af Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00007.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00008.png b/tests/snapshots/nanosp/test_sign_tx_accept/00008.png new file mode 100644 index 0000000..b79601d Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00008.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00009.png b/tests/snapshots/nanosp/test_sign_tx_accept/00009.png new file mode 100644 index 0000000..e661f58 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00009.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00010.png b/tests/snapshots/nanosp/test_sign_tx_accept/00010.png new file mode 100644 index 0000000..73460f9 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00010.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00011.png b/tests/snapshots/nanosp/test_sign_tx_accept/00011.png new file mode 100644 index 0000000..b7d3d38 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00011.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00012.png b/tests/snapshots/nanosp/test_sign_tx_accept/00012.png new file mode 100644 index 0000000..a54c460 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00012.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00013.png b/tests/snapshots/nanosp/test_sign_tx_accept/00013.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00013.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_accept/00014.png b/tests/snapshots/nanosp/test_sign_tx_accept/00014.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_accept/00014.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00000.png b/tests/snapshots/nanosp/test_sign_tx_refused/00000.png new file mode 100644 index 0000000..72c5e8e Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00000.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00001.png b/tests/snapshots/nanosp/test_sign_tx_refused/00001.png new file mode 100644 index 0000000..3a8465b Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00001.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00002.png b/tests/snapshots/nanosp/test_sign_tx_refused/00002.png new file mode 100644 index 0000000..fd0b1d6 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00002.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00003.png b/tests/snapshots/nanosp/test_sign_tx_refused/00003.png new file mode 100644 index 0000000..5f0f44c Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00004.png b/tests/snapshots/nanosp/test_sign_tx_refused/00004.png new file mode 100644 index 0000000..5875f0b Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00004.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00005.png b/tests/snapshots/nanosp/test_sign_tx_refused/00005.png new file mode 100644 index 0000000..a661596 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00005.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00006.png b/tests/snapshots/nanosp/test_sign_tx_refused/00006.png new file mode 100644 index 0000000..dcef220 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00006.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00007.png b/tests/snapshots/nanosp/test_sign_tx_refused/00007.png new file mode 100644 index 0000000..c48c0af Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00007.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00008.png b/tests/snapshots/nanosp/test_sign_tx_refused/00008.png new file mode 100644 index 0000000..b79601d Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00008.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00009.png b/tests/snapshots/nanosp/test_sign_tx_refused/00009.png new file mode 100644 index 0000000..e661f58 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00009.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00010.png b/tests/snapshots/nanosp/test_sign_tx_refused/00010.png new file mode 100644 index 0000000..73460f9 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00010.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00011.png b/tests/snapshots/nanosp/test_sign_tx_refused/00011.png new file mode 100644 index 0000000..b7d3d38 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00011.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00012.png b/tests/snapshots/nanosp/test_sign_tx_refused/00012.png new file mode 100644 index 0000000..a54c460 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00012.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00013.png b/tests/snapshots/nanosp/test_sign_tx_refused/00013.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00013.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00014.png b/tests/snapshots/nanosp/test_sign_tx_refused/00014.png new file mode 100644 index 0000000..e90cd9d Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00014.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00015.png b/tests/snapshots/nanosp/test_sign_tx_refused/00015.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00015.png differ diff --git a/tests/snapshots/nanox/test_app_mainmenu/00000.png b/tests/snapshots/nanox/test_app_mainmenu/00000.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanox/test_app_mainmenu/00000.png differ diff --git a/tests/snapshots/nanox/test_app_mainmenu/00001.png b/tests/snapshots/nanox/test_app_mainmenu/00001.png new file mode 100644 index 0000000..7aa1937 Binary files /dev/null and b/tests/snapshots/nanox/test_app_mainmenu/00001.png differ diff --git a/tests/snapshots/nanox/test_app_mainmenu/00002.png b/tests/snapshots/nanox/test_app_mainmenu/00002.png new file mode 100644 index 0000000..bad4cea Binary files /dev/null and b/tests/snapshots/nanox/test_app_mainmenu/00002.png differ diff --git a/tests/snapshots/nanox/test_app_mainmenu/00003.png b/tests/snapshots/nanox/test_app_mainmenu/00003.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanox/test_app_mainmenu/00003.png differ diff --git a/tests/snapshots/nanox/test_get_address_confirm_accepted/00000.png b/tests/snapshots/nanox/test_get_address_confirm_accepted/00000.png new file mode 100644 index 0000000..c56938b Binary files /dev/null and b/tests/snapshots/nanox/test_get_address_confirm_accepted/00000.png differ diff --git a/tests/snapshots/nanox/test_get_address_confirm_accepted/00001.png b/tests/snapshots/nanox/test_get_address_confirm_accepted/00001.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_get_address_confirm_accepted/00001.png differ diff --git a/tests/snapshots/nanox/test_get_address_confirm_accepted/00002.png b/tests/snapshots/nanox/test_get_address_confirm_accepted/00002.png new file mode 100644 index 0000000..6f4d8ab Binary files /dev/null and b/tests/snapshots/nanox/test_get_address_confirm_accepted/00002.png differ diff --git a/tests/snapshots/nanox/test_get_address_confirm_refused/00000.png b/tests/snapshots/nanox/test_get_address_confirm_refused/00000.png new file mode 100644 index 0000000..c56938b Binary files /dev/null and b/tests/snapshots/nanox/test_get_address_confirm_refused/00000.png differ diff --git a/tests/snapshots/nanox/test_get_address_confirm_refused/00001.png b/tests/snapshots/nanox/test_get_address_confirm_refused/00001.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_get_address_confirm_refused/00001.png differ diff --git a/tests/snapshots/nanox/test_get_address_confirm_refused/00002.png b/tests/snapshots/nanox/test_get_address_confirm_refused/00002.png new file mode 100644 index 0000000..e90cd9d Binary files /dev/null and b/tests/snapshots/nanox/test_get_address_confirm_refused/00002.png differ diff --git a/tests/snapshots/nanox/test_get_address_confirm_refused/00003.png b/tests/snapshots/nanox/test_get_address_confirm_refused/00003.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanox/test_get_address_confirm_refused/00003.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00000.png b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00000.png new file mode 100644 index 0000000..9c80384 Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00000.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00001.png b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00001.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00001.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00002.png b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00002.png new file mode 100644 index 0000000..de29aa8 Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00002.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00000.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00000.png new file mode 100644 index 0000000..9c80384 Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00000.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00001.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00001.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00001.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00002.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00002.png new file mode 100644 index 0000000..e90cd9d Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00002.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00003.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00003.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_hash_accept/00000.png b/tests/snapshots/nanox/test_sign_hash_accept/00000.png new file mode 100644 index 0000000..aa1c990 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_hash_accept/00000.png differ diff --git a/tests/snapshots/nanox/test_sign_hash_accept/00001.png b/tests/snapshots/nanox/test_sign_hash_accept/00001.png new file mode 100644 index 0000000..276e191 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_hash_accept/00001.png differ diff --git a/tests/snapshots/nanox/test_sign_hash_accept/00002.png b/tests/snapshots/nanox/test_sign_hash_accept/00002.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_hash_accept/00002.png differ diff --git a/tests/snapshots/nanox/test_sign_hash_accept/00003.png b/tests/snapshots/nanox/test_sign_hash_accept/00003.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_hash_accept/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_hash_reject/00000.png b/tests/snapshots/nanox/test_sign_hash_reject/00000.png new file mode 100644 index 0000000..aa1c990 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_hash_reject/00000.png differ diff --git a/tests/snapshots/nanox/test_sign_hash_reject/00001.png b/tests/snapshots/nanox/test_sign_hash_reject/00001.png new file mode 100644 index 0000000..276e191 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_hash_reject/00001.png differ diff --git a/tests/snapshots/nanox/test_sign_hash_reject/00002.png b/tests/snapshots/nanox/test_sign_hash_reject/00002.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_hash_reject/00002.png differ diff --git a/tests/snapshots/nanox/test_sign_hash_reject/00003.png b/tests/snapshots/nanox/test_sign_hash_reject/00003.png new file mode 100644 index 0000000..e90cd9d Binary files /dev/null and b/tests/snapshots/nanox/test_sign_hash_reject/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_hash_reject/00004.png b/tests/snapshots/nanox/test_sign_hash_reject/00004.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_hash_reject/00004.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00000.png b/tests/snapshots/nanox/test_sign_tx_accept/00000.png new file mode 100644 index 0000000..72c5e8e Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00000.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00001.png b/tests/snapshots/nanox/test_sign_tx_accept/00001.png new file mode 100644 index 0000000..3a8465b Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00001.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00002.png b/tests/snapshots/nanox/test_sign_tx_accept/00002.png new file mode 100644 index 0000000..fd0b1d6 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00002.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00003.png b/tests/snapshots/nanox/test_sign_tx_accept/00003.png new file mode 100644 index 0000000..5f0f44c Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00004.png b/tests/snapshots/nanox/test_sign_tx_accept/00004.png new file mode 100644 index 0000000..5875f0b Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00004.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00005.png b/tests/snapshots/nanox/test_sign_tx_accept/00005.png new file mode 100644 index 0000000..a661596 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00005.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00006.png b/tests/snapshots/nanox/test_sign_tx_accept/00006.png new file mode 100644 index 0000000..dcef220 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00006.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00007.png b/tests/snapshots/nanox/test_sign_tx_accept/00007.png new file mode 100644 index 0000000..c48c0af Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00007.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00008.png b/tests/snapshots/nanox/test_sign_tx_accept/00008.png new file mode 100644 index 0000000..b79601d Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00008.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00009.png b/tests/snapshots/nanox/test_sign_tx_accept/00009.png new file mode 100644 index 0000000..e661f58 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00009.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00010.png b/tests/snapshots/nanox/test_sign_tx_accept/00010.png new file mode 100644 index 0000000..73460f9 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00010.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00011.png b/tests/snapshots/nanox/test_sign_tx_accept/00011.png new file mode 100644 index 0000000..b7d3d38 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00011.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00012.png b/tests/snapshots/nanox/test_sign_tx_accept/00012.png new file mode 100644 index 0000000..a54c460 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00012.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00013.png b/tests/snapshots/nanox/test_sign_tx_accept/00013.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00013.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_accept/00014.png b/tests/snapshots/nanox/test_sign_tx_accept/00014.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_accept/00014.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00000.png b/tests/snapshots/nanox/test_sign_tx_refused/00000.png new file mode 100644 index 0000000..72c5e8e Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00000.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00001.png b/tests/snapshots/nanox/test_sign_tx_refused/00001.png new file mode 100644 index 0000000..3a8465b Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00001.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00002.png b/tests/snapshots/nanox/test_sign_tx_refused/00002.png new file mode 100644 index 0000000..fd0b1d6 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00002.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00003.png b/tests/snapshots/nanox/test_sign_tx_refused/00003.png new file mode 100644 index 0000000..5f0f44c Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00004.png b/tests/snapshots/nanox/test_sign_tx_refused/00004.png new file mode 100644 index 0000000..5875f0b Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00004.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00005.png b/tests/snapshots/nanox/test_sign_tx_refused/00005.png new file mode 100644 index 0000000..a661596 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00005.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00006.png b/tests/snapshots/nanox/test_sign_tx_refused/00006.png new file mode 100644 index 0000000..dcef220 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00006.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00007.png b/tests/snapshots/nanox/test_sign_tx_refused/00007.png new file mode 100644 index 0000000..c48c0af Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00007.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00008.png b/tests/snapshots/nanox/test_sign_tx_refused/00008.png new file mode 100644 index 0000000..b79601d Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00008.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00009.png b/tests/snapshots/nanox/test_sign_tx_refused/00009.png new file mode 100644 index 0000000..e661f58 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00009.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00010.png b/tests/snapshots/nanox/test_sign_tx_refused/00010.png new file mode 100644 index 0000000..73460f9 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00010.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00011.png b/tests/snapshots/nanox/test_sign_tx_refused/00011.png new file mode 100644 index 0000000..b7d3d38 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00011.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00012.png b/tests/snapshots/nanox/test_sign_tx_refused/00012.png new file mode 100644 index 0000000..a54c460 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00012.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00013.png b/tests/snapshots/nanox/test_sign_tx_refused/00013.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00013.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00014.png b/tests/snapshots/nanox/test_sign_tx_refused/00014.png new file mode 100644 index 0000000..e90cd9d Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00014.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00015.png b/tests/snapshots/nanox/test_sign_tx_refused/00015.png new file mode 100644 index 0000000..f88fa82 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00015.png differ diff --git a/tests/snapshots/stax/test_app_mainmenu/00000.png b/tests/snapshots/stax/test_app_mainmenu/00000.png new file mode 100644 index 0000000..7287939 Binary files /dev/null and b/tests/snapshots/stax/test_app_mainmenu/00000.png differ diff --git a/tests/snapshots/stax/test_app_mainmenu/00001.png b/tests/snapshots/stax/test_app_mainmenu/00001.png new file mode 100644 index 0000000..520cf9a Binary files /dev/null and b/tests/snapshots/stax/test_app_mainmenu/00001.png differ diff --git a/tests/snapshots/stax/test_app_mainmenu/00002.png b/tests/snapshots/stax/test_app_mainmenu/00002.png new file mode 100644 index 0000000..9c6d16f Binary files /dev/null and b/tests/snapshots/stax/test_app_mainmenu/00002.png differ diff --git a/tests/snapshots/stax/test_app_mainmenu/00003.png b/tests/snapshots/stax/test_app_mainmenu/00003.png new file mode 100644 index 0000000..7287939 Binary files /dev/null and b/tests/snapshots/stax/test_app_mainmenu/00003.png differ diff --git a/tests/snapshots/stax/test_get_address_confirm_accepted/00000.png b/tests/snapshots/stax/test_get_address_confirm_accepted/00000.png new file mode 100644 index 0000000..600a56e Binary files /dev/null and b/tests/snapshots/stax/test_get_address_confirm_accepted/00000.png differ diff --git a/tests/snapshots/stax/test_get_address_confirm_accepted/00001.png b/tests/snapshots/stax/test_get_address_confirm_accepted/00001.png new file mode 100644 index 0000000..710512d Binary files /dev/null and b/tests/snapshots/stax/test_get_address_confirm_accepted/00001.png differ diff --git a/tests/snapshots/stax/test_get_address_confirm_accepted/00002.png b/tests/snapshots/stax/test_get_address_confirm_accepted/00002.png new file mode 100644 index 0000000..3f906b2 Binary files /dev/null and b/tests/snapshots/stax/test_get_address_confirm_accepted/00002.png differ diff --git a/tests/snapshots/stax/test_get_address_confirm_refused/00000.png b/tests/snapshots/stax/test_get_address_confirm_refused/00000.png new file mode 100644 index 0000000..600a56e Binary files /dev/null and b/tests/snapshots/stax/test_get_address_confirm_refused/00000.png differ diff --git a/tests/snapshots/stax/test_get_address_confirm_refused/00001.png b/tests/snapshots/stax/test_get_address_confirm_refused/00001.png new file mode 100644 index 0000000..b0eba3f Binary files /dev/null and b/tests/snapshots/stax/test_get_address_confirm_refused/00001.png differ diff --git a/tests/snapshots/stax/test_get_public_key_confirm_accepted/00000.png b/tests/snapshots/stax/test_get_public_key_confirm_accepted/00000.png new file mode 100644 index 0000000..54b604d Binary files /dev/null and b/tests/snapshots/stax/test_get_public_key_confirm_accepted/00000.png differ diff --git a/tests/snapshots/stax/test_get_public_key_confirm_accepted/00001.png b/tests/snapshots/stax/test_get_public_key_confirm_accepted/00001.png new file mode 100644 index 0000000..d547da0 Binary files /dev/null and b/tests/snapshots/stax/test_get_public_key_confirm_accepted/00001.png differ diff --git a/tests/snapshots/stax/test_get_public_key_confirm_accepted/00002.png b/tests/snapshots/stax/test_get_public_key_confirm_accepted/00002.png new file mode 100644 index 0000000..5fd0549 Binary files /dev/null and b/tests/snapshots/stax/test_get_public_key_confirm_accepted/00002.png differ diff --git a/tests/snapshots/stax/test_get_public_key_confirm_refused/00000.png b/tests/snapshots/stax/test_get_public_key_confirm_refused/00000.png new file mode 100644 index 0000000..54b604d Binary files /dev/null and b/tests/snapshots/stax/test_get_public_key_confirm_refused/00000.png differ diff --git a/tests/snapshots/stax/test_get_public_key_confirm_refused/00001.png b/tests/snapshots/stax/test_get_public_key_confirm_refused/00001.png new file mode 100644 index 0000000..18e6db5 Binary files /dev/null and b/tests/snapshots/stax/test_get_public_key_confirm_refused/00001.png differ diff --git a/tests/snapshots/stax/test_sign_hash_accept/00000.png b/tests/snapshots/stax/test_sign_hash_accept/00000.png new file mode 100644 index 0000000..832d9e0 Binary files /dev/null and b/tests/snapshots/stax/test_sign_hash_accept/00000.png differ diff --git a/tests/snapshots/stax/test_sign_hash_accept/00001.png b/tests/snapshots/stax/test_sign_hash_accept/00001.png new file mode 100644 index 0000000..5f98802 Binary files /dev/null and b/tests/snapshots/stax/test_sign_hash_accept/00001.png differ diff --git a/tests/snapshots/stax/test_sign_hash_accept/00002.png b/tests/snapshots/stax/test_sign_hash_accept/00002.png new file mode 100644 index 0000000..b8c7d1e Binary files /dev/null and b/tests/snapshots/stax/test_sign_hash_accept/00002.png differ diff --git a/tests/snapshots/stax/test_sign_hash_accept/00003.png b/tests/snapshots/stax/test_sign_hash_accept/00003.png new file mode 100644 index 0000000..c35e7a7 Binary files /dev/null and b/tests/snapshots/stax/test_sign_hash_accept/00003.png differ diff --git a/tests/snapshots/stax/test_sign_hash_reject/00000.png b/tests/snapshots/stax/test_sign_hash_reject/00000.png new file mode 100644 index 0000000..832d9e0 Binary files /dev/null and b/tests/snapshots/stax/test_sign_hash_reject/00000.png differ diff --git a/tests/snapshots/stax/test_sign_hash_reject/00001.png b/tests/snapshots/stax/test_sign_hash_reject/00001.png new file mode 100644 index 0000000..56afa4b Binary files /dev/null and b/tests/snapshots/stax/test_sign_hash_reject/00001.png differ diff --git a/tests/snapshots/stax/test_sign_tx_accept/00000.png b/tests/snapshots/stax/test_sign_tx_accept/00000.png new file mode 100644 index 0000000..63ad1f9 Binary files /dev/null and b/tests/snapshots/stax/test_sign_tx_accept/00000.png differ diff --git a/tests/snapshots/stax/test_sign_tx_accept/00001.png b/tests/snapshots/stax/test_sign_tx_accept/00001.png new file mode 100644 index 0000000..75da96b Binary files /dev/null and b/tests/snapshots/stax/test_sign_tx_accept/00001.png differ diff --git a/tests/snapshots/stax/test_sign_tx_accept/00002.png b/tests/snapshots/stax/test_sign_tx_accept/00002.png new file mode 100644 index 0000000..4136279 Binary files /dev/null and b/tests/snapshots/stax/test_sign_tx_accept/00002.png differ diff --git a/tests/snapshots/stax/test_sign_tx_accept/00003.png b/tests/snapshots/stax/test_sign_tx_accept/00003.png new file mode 100644 index 0000000..063ad08 Binary files /dev/null and b/tests/snapshots/stax/test_sign_tx_accept/00003.png differ diff --git a/tests/snapshots/stax/test_sign_tx_accept/00004.png b/tests/snapshots/stax/test_sign_tx_accept/00004.png new file mode 100644 index 0000000..3ce7b6c Binary files /dev/null and b/tests/snapshots/stax/test_sign_tx_accept/00004.png differ diff --git a/tests/snapshots/stax/test_sign_tx_accept/00005.png b/tests/snapshots/stax/test_sign_tx_accept/00005.png new file mode 100644 index 0000000..f24411e Binary files /dev/null and b/tests/snapshots/stax/test_sign_tx_accept/00005.png differ diff --git a/tests/snapshots/stax/test_sign_tx_accept/00006.png b/tests/snapshots/stax/test_sign_tx_accept/00006.png new file mode 100644 index 0000000..2ba6d27 Binary files /dev/null and b/tests/snapshots/stax/test_sign_tx_accept/00006.png differ diff --git a/tests/snapshots/stax/test_sign_tx_refused/00000.png b/tests/snapshots/stax/test_sign_tx_refused/00000.png new file mode 100644 index 0000000..63ad1f9 Binary files /dev/null and b/tests/snapshots/stax/test_sign_tx_refused/00000.png differ diff --git a/tests/snapshots/stax/test_sign_tx_refused/00001.png b/tests/snapshots/stax/test_sign_tx_refused/00001.png new file mode 100644 index 0000000..cebc8be Binary files /dev/null and b/tests/snapshots/stax/test_sign_tx_refused/00001.png differ diff --git a/tests/test_app_mainmenu.py b/tests/test_app_mainmenu.py new file mode 100644 index 0000000..ca082b2 --- /dev/null +++ b/tests/test_app_mainmenu.py @@ -0,0 +1,26 @@ +from ragger.navigator import NavInsID + +from utils import ROOT_SCREENSHOT_PATH + + +# In this test we check the behavior of the device main menu +def test_app_mainmenu(firmware, navigator, test_name): + # Navigate in the main menu + if firmware.device.startswith("nano"): + instructions = [ + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK, + ] + else: + instructions = [ + NavInsID.USE_CASE_HOME_INFO, + NavInsID.USE_CASE_SETTINGS_NEXT, + NavInsID.USE_CASE_SETTINGS_SINGLE_PAGE_EXIT, + ] + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, + test_name, + instructions, + screen_change_before_first_instruction=False, + ) diff --git a/tests/test_error_cmd.py b/tests/test_error_cmd.py new file mode 100644 index 0000000..5bdd479 --- /dev/null +++ b/tests/test_error_cmd.py @@ -0,0 +1,19 @@ +from ragger.backend import RaisePolicy +from application_client.boilerplate_command_sender import CLA, InsType, P1, P2, Errors + + +# Ensure the app returns an error when a bad CLA is used +# Standard io library does not raise error in this situation +# def test_bad_cla(backend): +# # Disable raising when trying to unpack an error APDU +# backend.raise_policy = RaisePolicy.RAISE_NOTHING +# rapdu = backend.exchange(cla=CLA + 1, ins=InsType.GET_VERSION) +# assert rapdu.status == Errors.SW_INVALID_PARAM + + +# Ensure the app returns an error when a bad INS is used +def test_bad_ins(backend): + # Disable raising when trying to unpack an error APDU + backend.raise_policy = RaisePolicy.RAISE_NOTHING + rapdu = backend.exchange(cla=CLA, ins=0xFF) + assert rapdu.status == Errors.SW_INS_NOT_SUPPORTED diff --git a/tests/test_name_version.py b/tests/test_name_version.py new file mode 100644 index 0000000..b1252dd --- /dev/null +++ b/tests/test_name_version.py @@ -0,0 +1,17 @@ +from application_client.boilerplate_command_sender import BoilerplateCommandSender +from application_client.boilerplate_response_unpacker import ( + unpack_get_app_and_version_response, +) + + +# Test a specific APDU asking BOLOS (and not the app) the name and version of the current app +def test_get_app_and_version(backend, backend_name): + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # Send the special instruction to BOLOS + response = client.get_app_and_version() + # Use an helper to parse the response, assert the values + app_name, version = unpack_get_app_and_version_response(response.data) + + assert app_name == "Sia" + assert version == "1.0.0" diff --git a/tests/test_pubkey_cmd.py b/tests/test_pubkey_cmd.py new file mode 100644 index 0000000..be16a4c --- /dev/null +++ b/tests/test_pubkey_cmd.py @@ -0,0 +1,126 @@ +from application_client.boilerplate_command_sender import ( + BoilerplateCommandSender, + Errors, +) +from application_client.boilerplate_response_unpacker import ( + unpack_get_public_key_response, +) +from ragger.bip import calculate_public_key_and_chaincode, CurveChoice +from ragger.backend import RaisePolicy +from ragger.navigator import NavInsID, NavIns +from utils import ROOT_SCREENSHOT_PATH + +# Test will ask to generate a public key that will be accepted on screen +def test_get_public_key_confirm_accepted(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + index = 5 + + with client.get_public_key_with_confirmation(index=index): + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) + else: + instructions = [ + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_ADDRESS_CONFIRMATION_CONFIRM, + ] + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, test_name, instructions + ) + + response = client.get_async_response() + ref_public_key, _ = calculate_public_key_and_chaincode( + CurveChoice.Ed25519Slip, path="44'/93'/%d'/0'/0'" % (index) + ) + assert response.status == Errors.SW_OK + assert response.data[:32].hex() == ref_public_key[2:] + + +# Test will ask to generate a public key that will be rejected on screen +def test_get_public_key_confirm_refused(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + index = 5 + + with client.get_public_key_with_confirmation(index=index): + # Disable raising when trying to unpack an error APDU + backend.raise_policy = RaisePolicy.RAISE_NOTHING + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Reject", + ROOT_SCREENSHOT_PATH, + test_name, + ) + else: + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, test_name, [NavInsID.USE_CASE_REVIEW_REJECT] + ) + + response = client.get_async_response() + # Assert that we have received a refusal + assert response.status == Errors.SW_DENY + assert len(response.data) == 0 + + +# Test will ask to generate an address that will be accepted on screen +def test_get_address_confirm_accepted(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + index = 5 + + with client.get_address_with_confirmation(index=index): + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) + else: + instructions = [ + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_ADDRESS_CONFIRMATION_CONFIRM, + ] + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, test_name, instructions + ) + + response = client.get_async_response() + ref_public_key, _ = calculate_public_key_and_chaincode( + CurveChoice.Ed25519Slip, path="44'/93'/%d'/0'/0'" % (index) + ) + assert response.status == Errors.SW_OK + assert response.data[:32].hex() == ref_public_key[2:] + + +# Test will ask to generate an address that will be rejected on screen +def test_get_address_confirm_refused(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + index = 5 + + with client.get_address_with_confirmation(index=index): + # Disable raising when trying to unpack an error APDU + backend.raise_policy = RaisePolicy.RAISE_NOTHING + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Reject", + ROOT_SCREENSHOT_PATH, + test_name, + ) + else: + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, test_name, [NavInsID.USE_CASE_REVIEW_REJECT] + ) + + response = client.get_async_response() + # Assert that we have received a refusal + assert response.status == Errors.SW_DENY + assert len(response.data) == 0 diff --git a/tests/test_sign_hash_cmd.py b/tests/test_sign_hash_cmd.py new file mode 100644 index 0000000..66a37cf --- /dev/null +++ b/tests/test_sign_hash_cmd.py @@ -0,0 +1,110 @@ +from application_client.boilerplate_command_sender import ( + BoilerplateCommandSender, + Errors, +) +from application_client.boilerplate_response_unpacker import ( + unpack_get_public_key_response, + unpack_sign_tx_response, +) +from ragger.backend import RaisePolicy +from ragger.navigator import NavIns, NavInsID +from ragger.bip import calculate_public_key_and_chaincode, CurveChoice +from utils import ROOT_SCREENSHOT_PATH + +test_to_sign = bytes.fromhex( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +) + +# Test will ask to sign a hash that will be accepted on screen +def test_sign_hash_accept(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + index = 5 + + if firmware.device.startswith("nano"): + navigator.navigate([ + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + ], screen_change_before_first_instruction=False) + else: + navigator.navigate([ + NavInsID.USE_CASE_HOME_SETTINGS, + NavIns(NavInsID.TOUCH, (350,115)), + NavInsID.USE_CASE_SETTINGS_MULTI_PAGE_EXIT, + ], screen_change_before_first_instruction=False) + + with client.sign_hash_with_confirmation(index=index, to_sign=test_to_sign): + # Disable raising when trying to unpack an error APDU + backend.raise_policy = RaisePolicy.RAISE_NOTHING + if firmware.device.startswith("nano"): + # enable blind signing + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name, + ) + else: + instructions = [ + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_REVIEW_CONFIRM, + ] + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, test_name, instructions, + ) + + response = client.get_async_response() + assert response.status == Errors.SW_OK + assert response.data == bytes.fromhex( + "abd9187ca30200709137fa76dee32d58700f05c2debef62fb9b36af663498657384772ea437c886e07be20ddc60aaf04bb54736ab5dbaed4c00a6bdffcf7750f" + ) + + +# Test will ask to sign a hash that will be rejected on screen +def test_sign_hash_reject(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + index = 5 + + if firmware.device.startswith("nano"): + navigator.navigate([ + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + ], screen_change_before_first_instruction=False) + else: + navigator.navigate([ + NavInsID.USE_CASE_HOME_SETTINGS, + NavIns(NavInsID.TOUCH, (350,115)), + NavInsID.USE_CASE_SETTINGS_MULTI_PAGE_EXIT, + ], screen_change_before_first_instruction=False) + + with client.sign_hash_with_confirmation(index=index, to_sign=test_to_sign): + # Disable raising when trying to unpack an error APDU + backend.raise_policy = RaisePolicy.RAISE_NOTHING + + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare( + NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Reject", + ROOT_SCREENSHOT_PATH, + test_name, + ) + else: + navigator.navigate_and_compare( + ROOT_SCREENSHOT_PATH, test_name, [ + NavInsID.USE_CASE_REVIEW_REJECT, + NavInsID.USE_CASE_CHOICE_CONFIRM, + ] + ) + + # Assert that we have received a refusal + response = client.get_async_response() + assert response.status == Errors.SW_DENY + assert len(response.data) == 0 diff --git a/tests/test_sign_txn_cmd.py b/tests/test_sign_txn_cmd.py new file mode 100644 index 0000000..25aed7e --- /dev/null +++ b/tests/test_sign_txn_cmd.py @@ -0,0 +1,144 @@ +import base64 +from application_client.boilerplate_command_sender import ( + BoilerplateCommandSender, + Errors, +) +from application_client.boilerplate_response_unpacker import ( + unpack_get_public_key_response, + unpack_sign_tx_response, +) +from ragger.backend import RaisePolicy +from ragger.navigator import NavInsID +from utils import ROOT_SCREENSHOT_PATH + +# In this tests we check the behavior of the device when asked to sign a transaction + + +# Encoded version of {"siacoininputs":[{"unlockhash":"bc9d0e935b4a5d6511c353cfd226990a31b47a409b069ca819280ec8440a0ba97f8b1081dc8c","unlockconditions":{"timelock":0,"requiredsignatures":1,"publickeys":["ed25519:4dd481abf56b5f96d82b13823ce81f8d8f0d0eb3ac2d656366ca2a822e526f49"]}}],"siacoinoutputs":[{"unlockhash":"7813b59b2da28959e13466b8701f40133ceda7677edfc7c17829c3b5c58d624596ea749b9d7c","value":"83117000000000000000000000000"},{"unlockhash":"6f4710e9acbc9a20987222d4e79f56baf3b5642059e2f3922ac8e6b1f4812df04fa00dff468f","value":"51405720000000000000000000000"}],"siafundinputs":null,"siafundoutputs":[{"unlockhash":"7813b59b2da28959e13466b8701f40133ceda7677edfc7c17829c3b5c58d624596ea749b9d7c","value":"83117000000000000000000000000"},{"unlockhash":"6f4710e9acbc9a20987222d4e79f56baf3b5642059e2f3922ac8e6b1f4812df04fa00dff468f","value":"51405720000000000000000000000"}],"storagecontracts":null,"contractrevisions":null,"storageproofs":null,"minerfees":null,"arbitrarydata":[],"transactionsignatures":[{"parentid":"784a77549f25083a69a388a1661e0a6b2ac8c7fc98e2b69edde6bd45d155ad03","signature":"9611b663d7f24e354b5eb9a82c26cf5855d958ad12617bee89c90ac38219adb76c74a97103a0ab4ec991b0120145d23aaa6471209d3542094377a4e96227f70b","publickeyindex":0,"coveredfields":{"wholetransaction":true}}]} +test_transaction = bytes.fromhex( + "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000006564323535313900000000000000000020000000000000004dd481abf56b5f96d82b13823ce81f8d8f0d0eb3ac2d656366ca2a822e526f49000000000000000002000000000000000d00000000000000010c90c55e861d3c59cd0000007813b59b2da28959e13466b8701f40133ceda7677edfc7c17829c3b5c58d62450c00000000000000a619d0a11bec7c940f0000006f4710e9acbc9a20987222d4e79f56baf3b5642059e2f3922ac8e6b1f4812df0000000000000000000000000000000000000000000000000000000000000000002000000000000000d00000000000000010c90c55e861d3c59cd0000007813b59b2da28959e13466b8701f40133ceda7677edfc7c17829c3b5c58d624500000000000000000c00000000000000a619d0a11bec7c940f0000006f4710e9acbc9a20987222d4e79f56baf3b5642059e2f3922ac8e6b1f4812df00000000000000000000000000000000000000000000000000100000000000000784a77549f25083a69a388a1661e0a6b2ac8c7fc98e2b69edde6bd45d155ad03000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000f7ad756faeb777b7f6e1edf9e1be5e6fd6bcd9cdba71fe7ce7977de7c69dd76eb5edb79ef3d73dd1a737f36d7d69d6fbe9cef86bdef5d376b469be1e73df756f4d76d35e39776dda69aeb8ef5db4f5ddf9e36d3de37efb6b87bdeb6dbb7fbd1b" +) + +# Transaction signature refused test +# The test will ask for a transaction signature that will be refused on screen +def test_sign_tx_refused(firmware, backend, navigator, test_name): + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # Disable raising when trying to unpack an error APDU + backend.raise_policy = RaisePolicy.RAISE_NOTHING + + with client.sign_tx( + key_index=0, + sig_index=0, + change_index=4294967295, + transaction=test_transaction, + ): + if firmware.device.startswith("nano"): + instructions = [] + if firmware.device == "nanos": + for i in range(2): + instructions.extend(4 * [NavInsID.RIGHT_CLICK]) + instructions.extend([ + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + ]) + for i in range(2): + instructions.extend(4 * [NavInsID.RIGHT_CLICK]) + instructions.extend([ + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + ]) + else: + instructions.extend([ + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + ]) + + instructions.extend([ + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + ]) + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, test_name, instructions) + else: + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, test_name, [NavInsID.USE_CASE_REVIEW_REJECT]) + + response = client.get_async_response() + assert response.status == Errors.SW_DENY + assert len(response.data) == 0 + + +# Transaction signature accepted test +# The test will ask for a transaction signature that will be accepted on screen +def test_sign_tx_accept(firmware, backend, navigator, test_name): + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # Disable raising when trying to unpack an error APDU + backend.raise_policy = RaisePolicy.RAISE_NOTHING + + with client.sign_tx(key_index=0, sig_index=0, change_index=4294967295, transaction=test_transaction): + if firmware.device.startswith("nano"): + instructions = [] + if firmware.device == "nanos": + for i in range(2): + instructions.extend(4 * [NavInsID.RIGHT_CLICK]) + instructions.extend([ + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + ]) + for i in range(2): + instructions.extend(4 * [NavInsID.RIGHT_CLICK]) + instructions.extend([ + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + ]) + else: + instructions.extend([ + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + NavInsID.BOTH_CLICK, + ]) + + instructions.extend([ + NavInsID.RIGHT_CLICK, + NavInsID.BOTH_CLICK, + ]) + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, test_name, instructions) + else: + instructions = [ + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_REVIEW_CONFIRM, + ] + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, test_name, instructions) + + + response = client.get_async_response() + assert response.status == Errors.SW_OK + assert response.data == base64.b64decode( + "mr7i3aLQDoyHIM1ZXV+OTd34EM3bSpemN3tmV2ilH3x/yEVUtoZTVBMpuW8BMQ9vV21QIgxUGpzBfZccEY0bAg==" + ) diff --git a/tests/test_version_cmd.py b/tests/test_version_cmd.py new file mode 100644 index 0000000..700d049 --- /dev/null +++ b/tests/test_version_cmd.py @@ -0,0 +1,16 @@ +from application_client.boilerplate_command_sender import BoilerplateCommandSender +from application_client.boilerplate_response_unpacker import unpack_get_version_response + +# Taken from the Makefile, to update every time the Makefile version is bumped +MAJOR = 1 +MINOR = 0 +PATCH = 0 + +# In this test we check the behavior of the device when asked to provide the app version +def test_version(backend): + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # Send the GET_VERSION instruction + rapdu = client.get_version() + # Use an helper to parse the response, assert the values + assert unpack_get_version_response(rapdu.data) == (MAJOR, MINOR, PATCH) diff --git a/tests/usage.md b/tests/usage.md new file mode 100644 index 0000000..be8890f --- /dev/null +++ b/tests/usage.md @@ -0,0 +1,74 @@ +# How to use the Ragger test framework + +This framework allows testing the application on the Speculos emulator or on a real device using LedgerComm or LedgerWallet + + +## Quickly get started with Ragger and Speculos + +### Install ragger and dependencies + +``` +pip install --extra-index-url https://test.pypi.org/simple/ -r requirements.txt +sudo apt-get update && sudo apt-get install qemu-user-static +``` + +### Compile the application + +The application to test must be compiled for all required devices. +You can use for this the container `ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite`: +``` +docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest +cd # replace with the name of your app, (eg boilerplate) +docker run --user "$(id -u)":"$(id -g)" --rm -ti -v "$(realpath .):/app" --privileged -v "/dev/bus/usb:/dev/bus/usb" ledger-app-builder-lite:latest +make clean && make BOLOS_SDK=$_SDK # replace with one of [NANOS, NANOX, NANOSP, STAX] +exit +``` + +### Run a simple test using the Speculos emulator + +You can use the following command to get your first experience with Ragger and Speculos +``` +pytest -v --tb=short --device nanox --display +``` +Or you can refer to the section `Available pytest options` to configure the options you want to use + + +### Run a simple test using a real device + +The application to test must be loaded and started on a Ledger device plugged in USB. +You can use for this the container `ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite`: +``` +docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest +cd app-/ # replace with the name of your app, (eg boilerplate) +docker run --user "$(id -u)":"$(id -g)" --rm -ti -v "$(realpath .):/app" --privileged -v "/dev/bus/usb:/dev/bus/usb" ledger-app-builder-lite:latest +make clean && make BOLOS_SDK=$_SDK load # replace with one of [NANOS, NANOX, NANOSP, STAX] +exit +``` + +You can use the following command to get your first experience with Ragger and Ledgerwallet on a NANOX. +Make sure that the device is plugged, unlocked, and that the tested application is open. +``` +pytest -v --tb=short --device nanox --backend ledgerwallet +``` +Or you can refer to the section `Available pytest options` to configure the options you want to use + + +## Available pytest options + +Standard useful pytest options +``` + -v formats the test summary in a readable way + -s enable logs for successful tests, on Speculos it will enable app logs if compiled with DEBUG=1 + -k only run the tests that contain in their names + --tb=short in case of errors, formats the test traceback in a readable way +``` + +Custom pytest options +``` + --device run the test on the specified device [nanos,nanox,nanosp,stax,all]. This parameter is mandatory + --backend run the tests against the backend [speculos, ledgercomm, ledgerwallet]. Speculos is the default + --display on Speculos, enables the display of the app screen using QT + --golden_run on Speculos, screen comparison functions will save the current screen instead of comparing + --log_apdu_file log all apdu exchanges to the file in parameter. The previous file content is erased +``` + diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..afffedb --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,21 @@ +from pathlib import Path +from hashlib import sha256 + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + + +ROOT_SCREENSHOT_PATH = Path(__file__).parent.resolve() + + +# Check if a signature of a given message is valid +# def check_signature_validity( +# public_key: bytes, signature: bytes, message: bytes +# ) -> bool: +# pk: VerifyingKey = VerifyingKey.from_string( +# public_key, curve=SECP256k1, hashfunc=sha256 +# ) +# return pk.verify( +# signature=signature, data=message, hashfunc=keccak_256, sigdecode=sigdecode_der +# )