diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml index a19fb99..acd55ab 100644 --- a/.github/workflows/auto-author-assign.yml +++ b/.github/workflows/auto-author-assign.yml @@ -11,4 +11,4 @@ jobs: assign-author: runs-on: ubuntu-latest steps: - - uses: toshimaru/auto-author-assign@v1.4.0 \ No newline at end of file + - uses: toshimaru/auto-author-assign@v1.4.0 diff --git a/.github/workflows/build-workflow.yml b/.github/workflows/build-workflow.yml index 0c9ef5f..46f714b 100644 --- a/.github/workflows/build-workflow.yml +++ b/.github/workflows/build-workflow.yml @@ -1,7 +1,6 @@ name: Compilation on: - workflow_dispatch: push: branches: - master @@ -9,48 +8,27 @@ on: branches: - master - develop + workflow_dispatch: jobs: - nano_debug_build: - name: Build debug application for NanoS, X and S+ + nano_release_build: + name: Build release application for NanoS, X and S+ strategy: matrix: - include: - - SDK: "$NANOS_SDK" - artifact: nanos - - SDK: "$NANOX_SDK" - artifact: nanox - - SDK: "$NANOSP_SDK" - artifact: nanosp + sdk: ["$NANOS_SDK", "$NANOX_SDK", "$NANOSP_SDK"] runs-on: ubuntu-latest container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest steps: - name: Clone uses: actions/checkout@v2 - with: - submodules: recursive - - - name: Build an altcoin - run: | - make BOLOS_SDK=${{ matrix.SDK }} DEBUG=1 ALLOW_DATA=1 CHAIN=ethereum_classic - mv bin/app.elf ethereum_classic_${{ matrix.artifact }}.elf - - - name: Upload altcoin binary - uses: actions/upload-artifact@v2 - with: - name: ethereum_classic_${{ matrix.artifact }} - path: ./ethereum_classic_${{ matrix.artifact }}.elf - name: Build Ethereum run: | - make clean - make BOLOS_SDK=${{ matrix.SDK }} DEBUG=1 ALLOW_DATA=1 - mv bin/app.elf ethereum_${{ matrix.artifact }}.elf + make -j BOLOS_SDK=${{ matrix.sdk }} - - name: Upload app binary - uses: actions/upload-artifact@v2 - with: - name: ethereum_${{ matrix.artifact }} - path: ./ethereum_${{ matrix.artifact }}.elf \ No newline at end of file + - name: Build an altcoin + run: | + make clean + make -j BOLOS_SDK=${{ matrix.sdk }} CHAIN=polygon diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 85efce8..428de38 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -1,7 +1,6 @@ name: Tests on: - workflow_dispatch: push: branches: - master @@ -9,14 +8,14 @@ on: branches: - master - develop + workflow_dispatch: jobs: scan-build: name: Clang Static Analyzer runs-on: ubuntu-latest - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest steps: - uses: actions/checkout@v2 @@ -25,6 +24,7 @@ jobs: run: | make clean scan-build --use-cc=clang -analyze-headers -enable-checker security -enable-checker unix -enable-checker valist -o scan-build --status-bugs make default + - uses: actions/upload-artifact@v2 if: failure() with: @@ -39,7 +39,7 @@ jobs: name: Building binaries for E2E Zemu tests runs-on: ubuntu-latest container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest steps: - uses: actions/checkout@v2 @@ -52,19 +52,21 @@ jobs: - name: Upload app binaries uses: actions/upload-artifact@v2 with: - name: e2e_elfs + name: e2e_zemu_elfs path: ./tests/zemu/elfs/ jobs-e2e-zemu-tests: name: E2E Zemu tests needs: [building_for_e2e_zemu_tests] runs-on: ubuntu-latest + steps: - name: Test run: | id echo $HOME echo $DISPLAY + - name: Checkout uses: actions/checkout@v2 @@ -90,7 +92,7 @@ jobs: path: tmp/ - name: Gather elfs - run: cp `find tmp/e2e_elfs/ -name "*.elf"` tests/zemu/elfs/ + run: cp `find tmp/e2e_zemu_elfs/ -name "*.elf"` tests/zemu/elfs/ - name: Run zemu tests run: cd tests/zemu/ && yarn test @@ -104,7 +106,7 @@ jobs: name: Building binaries for E2E Speculos tests runs-on: ubuntu-latest container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest steps: - uses: actions/checkout@v2 @@ -112,14 +114,14 @@ jobs: - name: Build testing binaries run: | mkdir tests/speculos/elfs - make clean && make DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOS_SDK && mv bin/app.elf tests/speculos/elfs/nanos.elf - make clean && make DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOX_SDK && mv bin/app.elf tests/speculos/elfs/nanox.elf - make clean && make DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOSP_SDK && mv bin/app.elf tests/speculos/elfs/nanosp.elf + make clean && make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOS_SDK && mv bin/app.elf tests/speculos/elfs/nanos.elf + make clean && make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOX_SDK && mv bin/app.elf tests/speculos/elfs/nanox.elf + make clean && make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK=$NANOSP_SDK && mv bin/app.elf tests/speculos/elfs/nanosp.elf - name: Upload app binaries uses: actions/upload-artifact@v2 with: - name: e2e_elfs + name: e2e_speculos_elfs path: ./tests/speculos/elfs @@ -127,8 +129,8 @@ jobs: name: Speculos tests strategy: matrix: - model: ["nanosp", "nanos", "nanox"] - + model: ["nanosp", "nanos", "nanox"] + needs: [building_for_e2e_speculos_tests] runs-on: ubuntu-latest @@ -145,7 +147,7 @@ jobs: path: tmp/ - name: Gather elfs - run: cp `find tmp/e2e_elfs/ -name "*.elf"` tests/speculos/elfs/ + run: cp `find tmp/e2e_speculos_elfs/ -name "*.elf"` tests/speculos/elfs/ - name: Install dependencies run: | @@ -154,6 +156,123 @@ jobs: pip install --extra-index-url https://test.pypi.org/simple/ -r requirements.txt - name: Run speculos tests - run: | + run: | cd tests/speculos - pytest --model ${{ matrix.model }} --path ./elfs/${{ matrix.model }}.elf --display headless \ No newline at end of file + pytest --model ${{ matrix.model }} --path ./elfs/${{ matrix.model }}.elf --display headless + + +# ===================================================== +# RAGGER TESTS +# ===================================================== + + build_ragger_elfs: + name: Building binaries for Ragger tests + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Build test binaries + run: | + make -j BOLOS_SDK=$NANOS_SDK CAL_TESTING_KEY=1 + mv bin/app.elf app-nanos.elf + make clean + make -j BOLOS_SDK=$NANOX_SDK CAL_TESTING_KEY=1 + mv bin/app.elf app-nanox.elf + make clean + make -j BOLOS_SDK=$NANOSP_SDK CAL_TESTING_KEY=1 + mv bin/app.elf app-nanosp.elf + + - name: Upload app binaries + uses: actions/upload-artifact@v2 + with: + name: ragger_elfs + path: ./app-*.elf + + create_ragger_env: + name: Cache Ragger environment + runs-on: ubuntu-latest + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: APT update + run: | + sudo apt update + + - name: Create virtual env with dependencies + run: | + cd tests/ragger + python3 -m venv ./venv + . ./venv/bin/activate + pip3 install --extra-index-url https://test.pypi.org/simple/ -r requirements.txt speculos + # Used for the cache key + echo "py_deps=$(pip freeze | md5sum | cut -d' ' -f1)" >> $GITHUB_ENV + + - name: Download QEMU + run: | + sudo apt install --download-only -y qemu-user-static + mkdir -p tests/ragger/packages + cp /var/cache/apt/archives/*.deb tests/ragger/packages/ + # Used for the cache key + echo "deb_deps=$(find /var/cache/apt/archives/ -maxdepth 0 -type f -name '*.deb' | md5sum | cut -d' ' -f 1)" >> $GITHUB_ENV + + - name: Set up cache + uses: actions/cache@v3 + with: + key: ${{ runner.os }}-raggenv-${{ env.py_deps }}-${{ env.deb_deps }} + path: | + tests/ragger/venv/ + tests/ragger/packages/ + outputs: + py_deps: ${{ env.py_deps }} + deb_deps: ${{ env.deb_deps }} + + + jobs-ragger-tests: + name: Ragger tests + strategy: + matrix: + model: ["nanos", "nanox", "nanosp"] + needs: [build_ragger_elfs, create_ragger_env] + runs-on: ubuntu-latest + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Download previously built artifacts + uses: actions/download-artifact@v2 + with: + name: ragger_elfs + path: tmp/ + + - name: Put them where they belong + run: | + mkdir -p tests/ragger/elfs + find tmp/ -type f -name '*.elf' -exec cp {} tests/ragger/elfs/ \; + + - name: Get cached environment + uses: actions/cache@v3 + with: + key: ${{ runner.os }}-raggenv-${{ needs.create_ragger_env.outputs.py_deps }}-${{ needs.create_ragger_env.outputs.deb_deps }} + path: | + tests/ragger/venv/ + tests/ragger/packages/ + + - name: Install QEMU + run: | + sudo mv tests/ragger/packages/*.deb /var/cache/apt/archives/ + sudo apt install -y qemu-user-static + + - name: Run tests + env: + CAL_SIGNATURE_TEST_KEY: ${{ secrets.CAL_SIGNATURE_TEST_KEY }} + run: | + cd tests/ragger + . ./venv/bin/activate + pytest --path ./elfs --model ${{ matrix.model }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d9d0b..c678402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.10.0](https://github.com/ledgerhq/app-ethereum/compare/1.9.20...1.10.0) - 2022-10-26 + +### Changed + +- EIP-712 signatures are now computed on-device and display their content (clear-signing) (LNX & LNS+) + ## [1.9.20](https://github.com/ledgerhq/app-ethereum/compare/1.9.19...1.9.20) - 2022-10-10 ### Added diff --git a/Makefile b/Makefile index eedf848..40b9020 100644 --- a/Makefile +++ b/Makefile @@ -33,8 +33,8 @@ APP_LOAD_PARAMS += --path "1517992542'/1101353413'" ################## APPVERSION_M=1 -APPVERSION_N=9 -APPVERSION_P=20 +APPVERSION_N=10 +APPVERSION_P=0 APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P) APP_LOAD_FLAGS= --appFlags 0xa40 --dep Ethereum:$(APPVERSION) @@ -137,6 +137,22 @@ DEFINES += HAVE_NFT_TESTING_KEY endif endif +# Dynamic memory allocator +ifneq ($(TARGET_NAME),TARGET_NANOS) +DEFINES += HAVE_DYN_MEM_ALLOC +endif + +# EIP-712 +ifneq ($(TARGET_NAME),TARGET_NANOS) +DEFINES += HAVE_EIP712_FULL_SUPPORT +endif + +# CryptoAssetsList testing key +CAL_TESTING_KEY:=0 +ifneq ($(CAL_TESTING_KEY),0) +DEFINES += HAVE_CAL_TESTING_KEY +endif + # Enabling debug PRINTF DEBUG:=0 ifneq ($(DEBUG),0) diff --git a/doc/eth_contract_support_embedded.asc b/doc/.adoc similarity index 100% rename from doc/eth_contract_support_embedded.asc rename to doc/.adoc diff --git a/doc/eth_starkware_extensions.asc b/doc/eth_starkware_extensions.adoc similarity index 100% rename from doc/eth_starkware_extensions.asc rename to doc/eth_starkware_extensions.adoc diff --git a/doc/ethapp.asc b/doc/ethapp.adoc similarity index 74% rename from doc/ethapp.asc rename to doc/ethapp.adoc index 839e70b..bc57b3b 100644 --- a/doc/ethapp.asc +++ b/doc/ethapp.adoc @@ -1,679 +1,978 @@ -Ethereum application : Common Technical Specifications -======================================================= -Ledger Firmware Team -Application version 1.9.18 - 29th of January 2022 - -## 1.0 - - Initial release - -## 1.1 - - Add GET APP CONFIGURATION - - Add an option to return the chain code in GET ETH PUBLIC ADDRESS - -## 1.2 - - Add SIGN ETH PERSONAL MESSAGE - -## 1.1.10 - - Add PROVIDE ERC 20 TOKEN INFORMATION - -## 1.5.0 - - Add SIGN ETH EIP 712 - - Add GET ETH2 PUBLIC KEY - -## 1.7.6 - - Add SET EXTERNAL PLUGIN - -## 1.9.13 - - Add SET PLUGIN - -## 1.9.17 - - Add PERFORM PRIVACY OPERATION - -## About - -This application describes the APDU messages interface to communicate with the Ethereum application. - -The application covers the following functionalities : - - - Retrieve a public Ethereum address given a BIP 32 path - - Sign a basic Ethereum transaction given a BIP 32 path - - Provide callbacks to validate the data associated to an Ethereum transaction - -The application interface can be accessed over HID or BLE - -## General purpose APDUs - -### GET ETH PUBLIC ADDRESS - -#### Description - -This command returns the public key and Ethereum address for the given BIP 32 path. - -The address can be optionally checked on the device before being returned. - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 02 | 00 : return address - - 01 : display address and confirm before returning - | 00 : do not return the chain code - - 01 : return the chain code | variable | variable -|============================================================================================================================== - -'Input data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Number of BIP 32 derivations to perform (max 10) | 1 -| First derivation index (big endian) | 4 -| ... | 4 -| Last derivation index (big endian) | 4 -|============================================================================================================================== - -'Output data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Public Key length | 1 -| Uncompressed Public Key | var -| Ethereum address length | 1 -| Ethereum address | var -| Chain code if requested | 32 -|============================================================================================================================== - - -### SIGN ETH TRANSACTION - -#### Description - -https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md - -This command signs an Ethereum transaction after having the user validate the following parameters - - - Gas price - - Gas limit - - Recipient address - - Value - -The input data is the RLP encoded transaction (as per https://github.com/ethereum/pyethereum/blob/develop/ethereum/transactions.py#L22), without v/r/s present, streamed to the device in 255 bytes maximum data chunks. - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 04 | 00 : first transaction data block - - 80 : subsequent transaction data block - | 00 | variable | variable -|============================================================================================================================== - -'Input data (first transaction data block)' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Number of BIP 32 derivations to perform (max 10) | 1 -| First derivation index (big endian) | 4 -| ... | 4 -| Last derivation index (big endian) | 4 -| RLP transaction chunk | variable -|============================================================================================================================== - -'Input data (other transaction data block)' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| RLP transaction chunk | variable -|============================================================================================================================== - - -'Output data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| v | 1 -| r | 32 -| s | 32 -|============================================================================================================================== - - - -### SIGN ETH PERSONAL MESSAGE - -#### Description - -This command signs an Ethereum message following the personal_sign specification (https://github.com/ethereum/go-ethereum/pull/2940) after having the user validate the SHA-256 hash of the message being signed. - -This command has been supported since firmware version 1.0.8 - -The input data is the message to sign, streamed to the device in 255 bytes maximum data chunks - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 08 | 00 : first message data block - - 80 : subsequent message data block - | 00 | variable | variable -|============================================================================================================================== - -'Input data (first message data block)' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Number of BIP 32 derivations to perform (max 10) | 1 -| First derivation index (big endian) | 4 -| ... | 4 -| Last derivation index (big endian) | 4 -| Message length | 4 -| Message chunk | variable -|============================================================================================================================== - -'Input data (other transaction data block)' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Message chunk | variable -|============================================================================================================================== - - -'Output data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| v | 1 -| r | 32 -| s | 32 -|============================================================================================================================== - - -### PROVIDE ERC 20 TOKEN INFORMATION - -#### Description - -This commands provides a trusted description of an ERC 20 token to associate a contract address with a ticker and number of decimals. - -It shall be run immediately before performing a transaction involving a contract calling this contract address to display the proper token information to the user if necessary, as marked in GET APP CONFIGURATION flags. - -The signature is computed on - -ticker || address || number of decimals (uint4be) || chainId (uint4be) - -signed by the following secp256k1 public key 0482bbf2f34f367b2e5bc21847b6566f21f0976b22d3388a9a5e446ac62d25cf725b62a2555b2dd464a4da0ab2f4d506820543af1d242470b1b1a969a27578f353 - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 0A | 00 | 00 | variable | 00 -|============================================================================================================================== - -'Input data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Length of ERC 20 ticker | 1 -| ERC 20 ticker | variable -| ERC 20 contract address | 20 -| Number of decimals (big endian encoded) | 4 -| Chain ID (big endian encoded) | 4 -| Token information signature | variable -|============================================================================================================================== - -'Output data' - -None - -### PROVIDE NFT INFORMATION - -#### Description - -This commands provides a trusted description of an NFT to associate a contract address with a collectionName. - -It shall be run immediately before performing a transaction involving a contract calling this contract address to display the proper nft information to the user if necessary, as marked in GET APP CONFIGURATION flags. - -The signature is computed on: - -type || version || len(collectionName) || collectionName || address || chainId || keyId || algorithmId - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 14 | 00 | 00 | variable | 00 -|============================================================================================================================== - -'Input data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Type | 1 -| Version | 1 -| Collection Name Length | 1 -| Collection Name | variable -| Address | 20 -| Chain ID | 8 -| KeyID | 1 -| Algorithm ID | 1 -| Signature Length | 1 -| Signature | variable -|============================================================================================================================== - -'Output data' - -None - - -### SET EXTERNAL PLUGIN - -#### Description - -This commands provides the name of a trusted binding of a plugin with a contract address and a supported method selector. This plugin will be called to interpret contract data in the following transaction signing command. - -It shall be run immediately before performing a transaction involving a contract supported by this plugin to display the proper information to the user if necessary. - -The function returns an error sw (0x6984) if the plugin requested is not installed on the device, 0x9000 otherwise. - -The signature is computed on - -len(pluginName) || pluginName || contractAddress || methodSelector - -signed by the following secp256k1 public key 0482bbf2f34f367b2e5bc21847b6566f21f0976b22d3388a9a5e446ac62d25cf725b62a2555b2dd464a4da0ab2f4d506820543af1d242470b1b1a969a27578f353 - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 12 | 00 | 00 | variable | 00 -|============================================================================================================================== - -'Input data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Length of plugin name | 1 -| plugin name | variable -| contract address | 20 -| method selector | 4 -| signature | variable -|============================================================================================================================== - -'Output data' - -None - -### SET PLUGIN - -#### Description - -This commands provides the name of a trusted binding of a plugin with a contract address and a supported method selector. This plugin will be called to interpret contract data in the following transaction signing command. - -It can be used to set both internal and external plugins. - -It shall be run immediately before performing a transaction involving a contract supported by this plugin to display the proper information to the user if necessary. - -The function returns an error sw (0x6984) if the plugin requested is not installed on the device, 0x9000 otherwise. - -The plugin names `ERC20`, `ERC721` and `ERC1155` are reserved. Additional plugin names might be added to this list in the future. - -The signature is computed on - -type || version || len(pluginName) || pluginName || address || selector || chainId || keyId || algorithmId || len(signature) || signature - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 16 | 00 | 00 | variable | 00 -|============================================================================================================================== - -'Input data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Type | 1 -| Version | 1 -| Plugin Name Length | 1 -| Plugin Name | variable -| Address | 20 -| Selector | 4 -| Chain ID | 8 -| KeyID | 1 -| Algorithm ID | 1 -| Signature Length | 1 -| Signature | variable -|============================================================================================================================== - -'Output data' - -None - -### PERFORM PRIVACY OPERATION - -#### Description - -This command performs privacy operations as defined in EIP 1024 (https://ethereum-magicians.org/t/eip-1024-cross-client-encrypt-decrypt/505) - -It can return the public encryption key on Curve25519 for a given Ethereum account or the shared secret (generated by the scalar multiplication of the remote public key by the account private key on Curve25519) used to decrypt private data encrypted for a given Ethereum account - -All data can be optionally checked on the device before being returned. - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 18 | 00 : return data - - 01 : display data and confirm before returning - | 00 : return the public encryption key - - 01 : return the shared secret | variable | variable -|============================================================================================================================== - -'Input data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Number of BIP 32 derivations to perform (max 10) | 1 -| First derivation index (big endian) | 4 -| ... | 4 -| Last derivation index (big endian) | 4 -| Third party public key on Curve25519, if returning the shared secret | 32 -|============================================================================================================================== - -'Output data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Public encryption key or shared secret | 32 -|============================================================================================================================== - - -### GET APP CONFIGURATION - -#### Description - -This command returns specific application configuration - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 06 | 00 | 00 | 00 | 04 -|============================================================================================================================== - -'Input data' - -None - -'Output data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Flags - 0x01 : arbitrary data signature enabled by user - - 0x02 : ERC 20 Token information needs to be provided externally - | 01 -| Application major version | 01 -| Application minor version | 01 -| Application patch version | 01 -|============================================================================================================================== - -### SIGN ETH EIP 712 - -#### Description - -This command signs an Ethereum message following the EIP 712 specification (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) - -For implementation version 0, the domain hash and message hash are provided to the device, which displays them and returns the signature - -This command has been supported since firmware version 1.5.0 - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 0C | 00 - | implementation version : 00 | variable | variable -|============================================================================================================================== - -'Input data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Number of BIP 32 derivations to perform (max 10) | 1 -| First derivation index (big endian) | 4 -| ... | 4 -| Last derivation index (big endian) | 4 -| Domain hash | 32 -| Message hash | 32 -|============================================================================================================================== - -'Output data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| v | 1 -| r | 32 -| s | 32 -|============================================================================================================================== - -### GET ETH2 PUBLIC KEY - -#### Description - -This command returns an Ethereum 2 BLS12-381 public key derived following EIP 2333 specification (https://eips.ethereum.org/EIPS/eip-2333) - -This command has been supported since firmware version 1.6.0 - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 0E | 00 : return public key - - 01 : display public key and confirm before returning - | 00 | variable | variable -|============================================================================================================================== - -'Input data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Number of BIP 32 derivations to perform (max 10) | 1 -| First derivation index (big endian) | 4 -| ... | 4 -| Last derivation index (big endian) | 4 -|============================================================================================================================== - -'Output data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Public key | 48 -|============================================================================================================================== - -### SET ETH2 WITHDRAWAL INDEX - -#### Description - -This command sets the index of the Withdrawal key used as withdrawal credentials in an ETH2 deposit contract call signature. The path of the Withdrawal key is defined as m/12381/3600/index/0 according to EIP 2334 (https://eips.ethereum.org/EIPS/eip-2334) - -The default index used is 0 if this method isn't called before the deposit contract transaction is sent to the device to be signed - -This command has been supported since firmware version 1.5.0 - -#### Coding - -'Command' - -[width="80%"] -|============================================================================================================================== -| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* -| E0 | 10 | 00 - | 00 | variable | variable -|============================================================================================================================== - -'Input data' - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Withdrawal key index (big endian) | 4 -|============================================================================================================================== - -'Output data' - -None - -## Transport protocol - -### General transport description - -Ledger APDUs requests and responses are encapsulated using a flexible protocol allowing to fragment large payloads over different underlying transport mechanisms. - -The common transport header is defined as follows : - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| Communication channel ID (big endian) | 2 -| Command tag | 1 -| Packet sequence index (big endian) | 2 -| Payload | var -|============================================================================================================================== - -The Communication channel ID allows commands multiplexing over the same physical link. It is not used for the time being, and should be set to 0101 to avoid compatibility issues with implementations ignoring a leading 00 byte. - -The Command tag describes the message content. Use TAG_APDU (0x05) for standard APDU payloads, or TAG_PING (0x02) for a simple link test. - -The Packet sequence index describes the current sequence for fragmented payloads. The first fragment index is 0x00. - -### APDU Command payload encoding - -APDU Command payloads are encoded as follows : - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| APDU length (big endian) | 2 -| APDU CLA | 1 -| APDU INS | 1 -| APDU P1 | 1 -| APDU P2 | 1 -| APDU length | 1 -| Optional APDU data | var -|============================================================================================================================== - -APDU payload is encoded according to the APDU case - -[width="80%"] -|======================================================================================= -| Case Number | *Lc* | *Le* | Case description -| 1 | 0 | 0 | No data in either direction - L is set to 00 -| 2 | 0 | !0 | Input Data present, no Output Data - L is set to Lc -| 3 | !0 | 0 | Output Data present, no Input Data - L is set to Le -| 4 | !0 | !0 | Both Input and Output Data are present - L is set to Lc -|======================================================================================= - -### APDU Response payload encoding - -APDU Response payloads are encoded as follows : - -[width="80%"] -|============================================================================================================================== -| *Description* | *Length* -| APDU response length (big endian) | 2 -| APDU response data and Status Word | var -|============================================================================================================================== - -### USB mapping - -Messages are exchanged with the dongle over HID endpoints over interrupt transfers, with each chunk being 64 bytes long. The HID Report ID is ignored. - -### BLE mapping - -A similar encoding is used over BLE, without the Communication channel ID. - -The application acts as a GATT server defining service UUID D973F2E0-B19E-11E2-9E96-0800200C9A66 - -When using this service, the client sends requests to the characteristic D973F2E2-B19E-11E2-9E96-0800200C9A66, and gets notified on the characteristic D973F2E1-B19E-11E2-9E96-0800200C9A66 after registering for it. - -Requests are encoded using the standard BLE 20 bytes MTU size - -## Status Words - -The following standard Status Words are returned for all APDUs - some specific Status Words can be used for specific commands and are mentioned in the command description. - -'Status Words' - -[width="80%"] -|=============================================================================================== -| *SW* | *Description* -| 6501 | TransactionType not supported -| 6502 | Output buffer too small for chainId conversion -| 6503 | Plugin error -| 6504 | Failed to convert from int256 -| 6700 | Incorrect length -| 6982 | Security status not satisfied (Canceled by user) -| 6A80 | Invalid data -| 6B00 | Incorrect parameter P1 or P2 -| 6Fxx | Technical problem (Internal error, please report) -| 9000 | Normal ending of the command -|=============================================================================================== +Ethereum application : Common Technical Specifications +======================================================= +Ledger Firmware Team +Application version 1.9.19 - 2022-05-17 + +## Version history + +### 1.0 + - Initial release + +### 1.1 + - Add GET APP CONFIGURATION + - Add an option to return the chain code in GET ETH PUBLIC ADDRESS + +### 1.2 + - Add SIGN ETH PERSONAL MESSAGE + +### 1.1.10 + - Add PROVIDE ERC 20 TOKEN INFORMATION + +### 1.5.0 + - Add SIGN ETH EIP 712 + - Add GET ETH2 PUBLIC KEY + +### 1.7.6 + - Add SET EXTERNAL PLUGIN + +### 1.9.13 + - Add SET PLUGIN + +### 1.9.17 + - Add PROVIDE NFT INFORMATION + +### 1.9.18 + - Add PERFORM PRIVACY OPERATION + +### 1.10.0 + - Add EIP712 STRUCT DEFINITION & EIP712 STRUCT IMPLEMENTATION + - Update to SIGN ETH EIP712 + +## About + +This application describes the APDU messages interface to communicate with the Ethereum application. + +The application covers the following functionalities : + + - Retrieve a public Ethereum address given a BIP 32 path + - Sign a basic Ethereum transaction given a BIP 32 path + - Provide callbacks to validate the data associated to an Ethereum transaction + +The application interface can be accessed over HID or BLE + +## General purpose APDUs + +### GET ETH PUBLIC ADDRESS + +#### Description + +This command returns the public key and Ethereum address for the given BIP 32 path. + +The address can be optionally checked on the device before being returned. + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 02 | 00 : return address + + 01 : display address and confirm before returning + | 00 : do not return the chain code + + 01 : return the chain code | variable | variable +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Number of BIP 32 derivations to perform (max 10) | 1 +| First derivation index (big endian) | 4 +| ... | 4 +| Last derivation index (big endian) | 4 +|============================================================================================================================== + +'Output data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Public Key length | 1 +| Uncompressed Public Key | var +| Ethereum address length | 1 +| Ethereum address | var +| Chain code if requested | 32 +|============================================================================================================================== + + +### SIGN ETH TRANSACTION + +#### Description + +https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md + +This command signs an Ethereum transaction after having the user validate the following parameters + + - Gas price + - Gas limit + - Recipient address + - Value + +The input data is the RLP encoded transaction (as per https://github.com/ethereum/pyethereum/blob/develop/ethereum/transactions.py#L22), without v/r/s present, streamed to the device in 255 bytes maximum data chunks. + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 04 | 00 : first transaction data block + + 80 : subsequent transaction data block + | 00 | variable | variable +|============================================================================================================================== + +'Input data (first transaction data block)' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Number of BIP 32 derivations to perform (max 10) | 1 +| First derivation index (big endian) | 4 +| ... | 4 +| Last derivation index (big endian) | 4 +| RLP transaction chunk | variable +|============================================================================================================================== + +'Input data (other transaction data block)' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| RLP transaction chunk | variable +|============================================================================================================================== + + +'Output data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| v | 1 +| r | 32 +| s | 32 +|============================================================================================================================== + + +### GET APP CONFIGURATION + +#### Description + +This command returns specific application configuration + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 06 | 00 | 00 | 00 | 04 +|============================================================================================================================== + +'Input data' + +None + +'Output data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Flags + 0x01 : arbitrary data signature enabled by user + + 0x02 : ERC 20 Token information needs to be provided externally + | 01 +| Application major version | 01 +| Application minor version | 01 +| Application patch version | 01 +|============================================================================================================================== + + +### SIGN ETH PERSONAL MESSAGE + +#### Description + +This command signs an Ethereum message following the personal_sign specification (https://github.com/ethereum/go-ethereum/pull/2940) after having the user validate the SHA-256 hash of the message being signed. + +This command has been supported since firmware version 1.0.8 + +The input data is the message to sign, streamed to the device in 255 bytes maximum data chunks + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 08 | 00 : first message data block + + 80 : subsequent message data block + | 00 | variable | variable +|============================================================================================================================== + +'Input data (first message data block)' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Number of BIP 32 derivations to perform (max 10) | 1 +| First derivation index (big endian) | 4 +| ... | 4 +| Last derivation index (big endian) | 4 +| Message length | 4 +| Message chunk | variable +|============================================================================================================================== + +'Input data (other transaction data block)' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Message chunk | variable +|============================================================================================================================== + + +'Output data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| v | 1 +| r | 32 +| s | 32 +|============================================================================================================================== + + +### PROVIDE ERC 20 TOKEN INFORMATION + +#### Description + +This command provides a trusted description of an ERC 20 token to associate a contract address with a ticker and number of decimals. + +It shall be run immediately before performing a transaction involving a contract calling this contract address to display the proper token information to the user if necessary, as marked in GET APP CONFIGURATION flags. + +The signature is computed on + +ticker || address || number of decimals (uint4be) || chainId (uint4be) + +signed by the following secp256k1 public key 0482bbf2f34f367b2e5bc21847b6566f21f0976b22d3388a9a5e446ac62d25cf725b62a2555b2dd464a4da0ab2f4d506820543af1d242470b1b1a969a27578f353 + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 0A | 00 | 00 | variable | 00 +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Length of ERC 20 ticker | 1 +| ERC 20 ticker | variable +| ERC 20 contract address | 20 +| Number of decimals (big endian encoded) | 4 +| Chain ID (big endian encoded) | 4 +| Token information signature | variable +|============================================================================================================================== + +'Output data' + +None + + +### SIGN ETH EIP 712 + +#### Description + +This command signs an Ethereum message following the EIP 712 specification (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) + +For implementation version 0, the domain hash and message hash are provided to the device, which displays them and returns the signature + +This command has been supported since app version 1.5.0 + +The full implementation uses all the JSON data and does all the hashing on the +device, it has been supported since app version 1.9.19. This command should come +last, after all the EIP712 SEND STRUCT DEFINITION & SEND STRUCT IMPLEMENTATION. + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 0C | 00 + | 00: v0 implementation + + 01: full implementation + | variable + | variable +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Number of BIP 32 derivations to perform (max 10) | 1 +| First derivation index (big endian) | 4 +| ... | 4 +| Last derivation index (big endian) | 4 +| Domain hash *(only for v0)* | 32 +| Message hash *(only for v0)* | 32 +|============================================================================================================================== + +'Output data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| v | 1 +| r | 32 +| s | 32 +|============================================================================================================================== + + +### GET ETH2 PUBLIC KEY + +#### Description + +This command returns an Ethereum 2 BLS12-381 public key derived following EIP 2333 specification (https://eips.ethereum.org/EIPS/eip-2333) + +This command has been supported since firmware version 1.6.0 + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 0E | 00 : return public key + + 01 : display public key and confirm before returning + | 00 | variable | variable +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Number of BIP 32 derivations to perform (max 10) | 1 +| First derivation index (big endian) | 4 +| ... | 4 +| Last derivation index (big endian) | 4 +|============================================================================================================================== + +'Output data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Public key | 48 +|============================================================================================================================== + + +### SET ETH2 WITHDRAWAL INDEX + +#### Description + +This command sets the index of the Withdrawal key used as withdrawal credentials in an ETH2 deposit contract call signature. The path of the Withdrawal key is defined as m/12381/3600/index/0 according to EIP 2334 (https://eips.ethereum.org/EIPS/eip-2334) + +The default index used is 0 if this method isn't called before the deposit contract transaction is sent to the device to be signed + +This command has been supported since firmware version 1.5.0 + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 10 | 00 + | 00 | variable | variable +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Withdrawal key index (big endian) | 4 +|============================================================================================================================== + +'Output data' + +None + + +### SET EXTERNAL PLUGIN + +#### Description + +This command provides the name of a trusted binding of a plugin with a contract address and a supported method selector. This plugin will be called to interpret contract data in the following transaction signing command. + +It shall be run immediately before performing a transaction involving a contract supported by this plugin to display the proper information to the user if necessary. + +The function returns an error sw (0x6984) if the plugin requested is not installed on the device, 0x9000 otherwise. + +The signature is computed on + +len(pluginName) || pluginName || contractAddress || methodSelector + +signed by the following secp256k1 public key 0482bbf2f34f367b2e5bc21847b6566f21f0976b22d3388a9a5e446ac62d25cf725b62a2555b2dd464a4da0ab2f4d506820543af1d242470b1b1a969a27578f353 + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 12 | 00 | 00 | variable | 00 +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Length of plugin name | 1 +| plugin name | variable +| contract address | 20 +| method selector | 4 +| signature | variable +|============================================================================================================================== + +'Output data' + +None + + +### PROVIDE NFT INFORMATION + +#### Description + +This command provides a trusted description of an NFT to associate a contract address with a collectionName. + +It shall be run immediately before performing a transaction involving a contract calling this contract address to display the proper nft information to the user if necessary, as marked in GET APP CONFIGURATION flags. + +The signature is computed on: + +type || version || len(collectionName) || collectionName || address || chainId || keyId || algorithmId + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 14 | 00 | 00 | variable | 00 +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Type | 1 +| Version | 1 +| Collection Name Length | 1 +| Collection Name | variable +| Address | 20 +| Chain ID | 8 +| KeyID | 1 +| Algorithm ID | 1 +| Signature Length | 1 +| Signature | variable +|============================================================================================================================== + +'Output data' + +None + + +### SET PLUGIN + +#### Description + +This command provides the name of a trusted binding of a plugin with a contract address and a supported method selector. This plugin will be called to interpret contract data in the following transaction signing command. + +It can be used to set both internal and external plugins. + +It shall be run immediately before performing a transaction involving a contract supported by this plugin to display the proper information to the user if necessary. + +The function returns an error sw (0x6984) if the plugin requested is not installed on the device, 0x9000 otherwise. + +The plugin names `ERC20`, `ERC721` and `ERC1155` are reserved. Additional plugin names might be added to this list in the future. + +The signature is computed on + +type || version || len(pluginName) || pluginName || address || selector || chainId || keyId || algorithmId || len(signature) || signature + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 16 | 00 | 00 | variable | 00 +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Type | 1 +| Version | 1 +| Plugin Name Length | 1 +| Plugin Name | variable +| Address | 20 +| Selector | 4 +| Chain ID | 8 +| KeyID | 1 +| Algorithm ID | 1 +| Signature Length | 1 +| Signature | variable +|============================================================================================================================== + +'Output data' + +None + +### PERFORM PRIVACY OPERATION + +#### Description + +This command performs privacy operations as defined in EIP 1024 (https://ethereum-magicians.org/t/eip-1024-cross-client-encrypt-decrypt/505) + +It can return the public encryption key on Curve25519 for a given Ethereum account or the shared secret (generated by the scalar multiplication of the remote public key by the account private key on Curve25519) used to decrypt private data encrypted for a given Ethereum account + +All data can be optionally checked on the device before being returned. + +#### Coding + +'Command' + +[width="80%"] +|============================================================================================================================== +| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le* +| E0 | 18 | 00 : return data + + 01 : display data and confirm before returning + | 00 : return the public encryption key + + 01 : return the shared secret | variable | variable +|============================================================================================================================== + +'Input data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Number of BIP 32 derivations to perform (max 10) | 1 +| First derivation index (big endian) | 4 +| ... | 4 +| Last derivation index (big endian) | 4 +| Third party public key on Curve25519, if returning the shared secret | 32 +|============================================================================================================================== + +'Output data' + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Public encryption key or shared secret | 32 +|============================================================================================================================== + + +### EIP712 SEND STRUCT DEFINITION + +#### Description + +This command sends the message definition with all its types. + +These commands should come before the EIP712 SEND STRUCT IMPLEMENTATION ones. + +#### Coding + +_Command_ + +[width="80%"] +|========================================================================= +| *CLA* | *INS* | *P1* | *P2* | *LC* | *Le* +| E0 | 1A | 00 + | 00 : struct name + + FF : struct field + | variable + | variable +|========================================================================= + +_Input data_ + +##### If P2 == struct name + +[width="80%"] +|========================================== +| *Description* | *Length (byte)* +| Name | LC +|========================================== + +##### If P2 == struct field + +:check_y: ✅ +:check_n: ❌ + +[width="80%"] +|====================================================================== +| *Description* | *Length (byte)* | *Mandatory* +| TypeDesc (type description) | 1 | {check_y} +| TypeNameLength | 1 | {check_n} +| TypeName | variable | {check_n} +| TypeSize | 1 | {check_n} +| ArrayLevelCount | 1 | {check_n} +| ArrayLevels | variable | {check_n} +| KeyNameLength | 1 | {check_y} +| KeyName | variable | {check_y} +|====================================================================== + +###### TypeDesc + +From MSB to LSB: + +[width="80%"] +|============================================================= +| *Description* | *Length (bit)* +| TypeArray (is it an array?) | 1 +| TypeSize (is a type size specified?) | 1 +| Unused | 2 +| Type | 4 +|============================================================= + +How to interpret Type from its value : + +[width="40%"] +|=========================================== +| *Value* | *Type* +| 0 | custom (struct type) +| 1 | int +| 2 | uint +| 3 | address +| 4 | bool +| 5 | string +| 6 | fixed-sized bytes +| 7 | dynamic-sized bytes +|=========================================== + +###### TypeName + +_Only present if the Type is set to custom._ + +Indicates the name of the struct that will be the type of the field. + + +###### TypeSize + +_Only present if the TypeSize bit is set in TypeDesc._ + +Indicates the byte size of the field. (Ex: 8 for an int64) + + +###### ArrayLevelCount + +_Only present if the TypeArray bit is set in TypeDesc._ + +Indicates how many array levels that field has (Ex: 3 for int16[2][][4]). + +###### ArrayLevels + +_Only present if the TypeArray bit is set in TypeDesc._ + +Types of array level: + +[width="40%"] +|================================ +| *Byte value* | *Type* +| 0 | Dynamic sized (type[]) +| 1 | Fixed size (type[N]) +|================================ + +Each fixed-sized array level is followed by a byte indicating its size (number of elements). + + +_Output data_ + +None + + +### EIP712 SEND STRUCT IMPLEMENTATION + +#### Description + +This command sends the message implementation with all its values. + +These commands should come after the EIP712 SEND STRUCT DEFINITION ones. + +#### Coding + +_Command_ + +[width="80%"] +|========================================================================= +| *CLA* | *INS* | *P1* | *P2* | *LC* | *Le* +| E0 | 1C | 00 : complete send + + 01 : partial send, more to come + | 00 : root struct + + 0F : array + + FF : struct field + | variable + | variable +|========================================================================= + +_Input data_ + +##### If P2 == root struct + +[width="80%"] +|========================================== +| *Description* | *Length (byte)* +| Name | LC +|========================================== + +Sets the name of the upcoming root structure all the following fields will be apart +of until we set another root structure. + +##### If P2 == array + +[width="80%"] +|========================================== +| *Description* | *Length (byte)* +| Array size | 1 +|========================================== + +Sets the size of the upcoming array the following N fields will be apart of. + +##### If P2 == struct field + +[width="80%"] +|========================================== +| *Description* | *Length (byte)* +| Value length | 2 (BE) +| Value | variable +|========================================== + +Sets the raw value of the next field in order in the current root structure. +Raw as in, an integer in the JSON file represented as "128" would only be 1 byte long (0x80) +instead of 3 as an array of ASCII characters, same for addresses and so on. + + +_Output data_ + +None + + +### EIP712 FILTERING + +#### Description + +This command provides a trusted way of deciding what information from the JSON data to show and replace some values by more meaningful ones. + +This mode can be overriden by the in-app setting to fully clear-sign EIP-712 messages. + +For the signatures : + +* The chain ID used for the signature must be 8 bytes wide. +* The schema hash = sha224sum of the value of _types_ at the root of the JSON data (stripped of all spaces and newlines) + +##### Activation + +Full filtering is disabled by default and has to be changed with this APDU (default behaviour is basic filtering handled by the app itself). + +Field substitution will be ignored if the full filtering is not activated. + +This command should come before the domain & message implementations. If activated, fields will be by default hidden unless they receive a field name substitution. + +##### Message info + +This command should come right after the implementation of the domain has been sent with *SEND STRUCT IMPLEMENTATION*, just before sending the message implementation. +The first byte is used so that a signature of one type cannot be valid as another type. + +The signature is computed on : + +183 || chain ID (BE) || contract address || schema hash || filters count || display name + + +##### Show field + +These commands should come before the corresponding *SEND STRUCT IMPLEMENTATION* and are only usable for message fields (and not domain ones). +The first byte is used so that a signature of one type cannot be valid as another type. + +The signature is computed on : + +72 || chain ID (BE) || contract address || schema hash || field path || display name + +#### Coding + +_Command_ + +[width="80%"] +|========================================================================= +| *CLA* | *INS* | *P1* | *P2* | *LC* | *Le* +| E0 | 1E | 00 + | 00 : activate + + 0F : message info + + FF : show field + | variable | variable +|========================================================================= + +_Input data_ + +##### If P2 == activate + +None + +##### If P2 == message info + +[width="80%"] +|========================================== +| *Description* | *Length (byte)* +| Display name length | 1 +| Display name | variable +| Filters count | 1 +| Signature length | 1 +| Signature | variable +|========================================== + +##### If P2 == show field + +[width="80%"] +|========================================== +| *Description* | *Length (byte)* +| Display name length | 1 +| Display name | variable +| Signature length | 1 +| Signature | variable +|========================================== + +_Output data_ + +None + + +## Transport protocol + +### General transport description + +Ledger APDUs requests and responses are encapsulated using a flexible protocol allowing to fragment large payloads over different underlying transport mechanisms. + +The common transport header is defined as follows : + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| Communication channel ID (big endian) | 2 +| Command tag | 1 +| Packet sequence index (big endian) | 2 +| Payload | var +|============================================================================================================================== + +The Communication channel ID allows commands multiplexing over the same physical link. It is not used for the time being, and should be set to 0101 to avoid compatibility issues with implementations ignoring a leading 00 byte. + +The Command tag describes the message content. Use TAG_APDU (0x05) for standard APDU payloads, or TAG_PING (0x02) for a simple link test. + +The Packet sequence index describes the current sequence for fragmented payloads. The first fragment index is 0x00. + +### APDU Command payload encoding + +APDU Command payloads are encoded as follows : + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| APDU length (big endian) | 2 +| APDU CLA | 1 +| APDU INS | 1 +| APDU P1 | 1 +| APDU P2 | 1 +| APDU length | 1 +| Optional APDU data | var +|============================================================================================================================== + +APDU payload is encoded according to the APDU case + +[width="80%"] +|======================================================================================= +| Case Number | *Lc* | *Le* | Case description +| 1 | 0 | 0 | No data in either direction - L is set to 00 +| 2 | 0 | !0 | Input Data present, no Output Data - L is set to Lc +| 3 | !0 | 0 | Output Data present, no Input Data - L is set to Le +| 4 | !0 | !0 | Both Input and Output Data are present - L is set to Lc +|======================================================================================= + +### APDU Response payload encoding + +APDU Response payloads are encoded as follows : + +[width="80%"] +|============================================================================================================================== +| *Description* | *Length* +| APDU response length (big endian) | 2 +| APDU response data and Status Word | var +|============================================================================================================================== + +### USB mapping + +Messages are exchanged with the dongle over HID endpoints over interrupt transfers, with each chunk being 64 bytes long. The HID Report ID is ignored. + +### BLE mapping + +A similar encoding is used over BLE, without the Communication channel ID. + +The application acts as a GATT server defining service UUID D973F2E0-B19E-11E2-9E96-0800200C9A66 + +When using this service, the client sends requests to the characteristic D973F2E2-B19E-11E2-9E96-0800200C9A66, and gets notified on the characteristic D973F2E1-B19E-11E2-9E96-0800200C9A66 after registering for it. + +Requests are encoded using the standard BLE 20 bytes MTU size + +## Status Words + +The following standard Status Words are returned for all APDUs - some specific Status Words can be used for specific commands and are mentioned in the command description. + +'Status Words' + +[width="80%"] +|=============================================================================================== +| *SW* | *Description* +| 6501 | TransactionType not supported +| 6502 | Output buffer too small for chainId conversion +| 6503 | Plugin error +| 6504 | Failed to convert from int256 +| 6700 | Incorrect length +| 6982 | Security status not satisfied (Canceled by user) +| 6A80 | Invalid data +| 6B00 | Incorrect parameter P1 or P2 +| 6Fxx | Technical problem (Internal error, please report) +| 9000 | Normal ending of the command +|=============================================================================================== diff --git a/doc/ethapp_plugins.asc b/doc/ethapp_plugins.adoc similarity index 100% rename from doc/ethapp_plugins.asc rename to doc/ethapp_plugins.adoc diff --git a/src/apdu_constants.h b/src/apdu_constants.h index 219b7d0..cab0688 100644 --- a/src/apdu_constants.h +++ b/src/apdu_constants.h @@ -21,16 +21,30 @@ #define INS_PROVIDE_NFT_INFORMATION 0x14 #define INS_SET_PLUGIN 0x16 #define INS_PERFORM_PRIVACY_OPERATION 0x18 +#define INS_EIP712_STRUCT_DEF 0x1A +#define INS_EIP712_STRUCT_IMPL 0x1C +#define INS_EIP712_FILTERING 0x1E #define P1_CONFIRM 0x01 #define P1_NON_CONFIRM 0x00 #define P2_NO_CHAINCODE 0x00 #define P2_CHAINCODE 0x01 #define P1_FIRST 0x00 #define P1_MORE 0x80 +#define P2_EIP712_LEGACY_IMPLEM 0x00 +#define P2_EIP712_FULL_IMPLEM 0x01 #define COMMON_CLA 0xB0 #define COMMON_INS_GET_WALLET_ID 0x04 +#define APDU_RESPONSE_OK 0x9000 +#define APDU_RESPONSE_ERROR_NO_INFO 0x6a00 +#define APDU_RESPONSE_INVALID_DATA 0x6a80 +#define APDU_RESPONSE_INSUFFICIENT_MEMORY 0x6a84 +#define APDU_RESPONSE_INVALID_INS 0x6d00 +#define APDU_RESPONSE_INVALID_P1_P2 0x6b00 +#define APDU_RESPONSE_CONDITION_NOT_SATISFIED 0x6985 +#define APDU_RESPONSE_REF_DATA_NOT_FOUND 0x6a88 + #ifdef HAVE_STARKWARE #define STARKWARE_CLA 0xF0 @@ -51,12 +65,7 @@ #endif -#define OFFSET_CLA 0 -#define OFFSET_INS 1 -#define OFFSET_P1 2 -#define OFFSET_P2 3 -#define OFFSET_LC 4 -#define OFFSET_CDATA 5 +enum { OFFSET_CLA = 0, OFFSET_INS, OFFSET_P1, OFFSET_P2, OFFSET_LC, OFFSET_CDATA }; #define ERR_APDU_EMPTY 0x6982 #define ERR_APDU_SIZE_MISMATCH 0x6983 @@ -64,62 +73,62 @@ void handleGetPublicKey(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handleProvideErc20TokenInformation(uint8_t p1, uint8_t p2, - const uint8_t *dataBuffer, - uint16_t dataLength, + const uint8_t *workBuffer, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handleProvideNFTInformation(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handleSign(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handleGetAppConfiguration(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); bool handleSignPersonalMessage(uint8_t p1, uint8_t p2, const uint8_t *const payload, uint8_t length); -void handleSignEIP712Message(uint8_t p1, - uint8_t p2, - const uint8_t *dataBuffer, - uint16_t dataLength, - unsigned int *flags, - unsigned int *tx); +void handleSignEIP712Message_v0(uint8_t p1, + uint8_t p2, + const uint8_t *dataBuffer, + uint8_t dataLength, + unsigned int *flags, + unsigned int *tx); void handleSetExternalPlugin(uint8_t p1, uint8_t p2, const uint8_t *workBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handleSetPlugin(uint8_t p1, uint8_t p2, const uint8_t *workBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handlePerformPrivacyOperation(uint8_t p1, uint8_t p2, const uint8_t *workBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); @@ -128,13 +137,13 @@ void handlePerformPrivacyOperation(uint8_t p1, void handleGetEth2PublicKey(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handleSetEth2WinthdrawalIndex(uint8_t p1, uint8_t p2, uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); @@ -145,28 +154,30 @@ void handleSetEth2WinthdrawalIndex(uint8_t p1, void handleStarkwareGetPublicKey(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handleStarkwareSignMessage(uint8_t p1, uint8_t p2, uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handleStarkwareProvideQuantum(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); void handleStarkwareUnsafeSign(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx); #endif +extern uint16_t apdu_response_code; + #endif // _APDU_CONSTANTS_H_ diff --git a/src/common_ui.h b/src/common_ui.h index 3bca6b9..9b2b0dc 100644 --- a/src/common_ui.h +++ b/src/common_ui.h @@ -25,6 +25,11 @@ void ui_191_switch_to_message_end(void); void ui_191_switch_to_sign(void); void ui_191_switch_to_question(void); +// EIP-712 +void ui_712_start(void); +void ui_712_switch_to_message(void); +void ui_712_switch_to_sign(void); + #include "ui_callbacks.h" #include diff --git a/src/main.c b/src/main.c index 2a32e22..1828416 100644 --- a/src/main.c +++ b/src/main.c @@ -28,6 +28,7 @@ #include "handle_swap_sign_transaction.h" #include "handle_get_printable_amount.h" #include "handle_check_address.h" +#include "commands_712.h" #ifdef HAVE_STARKWARE #include "stark_crypto.h" @@ -48,6 +49,7 @@ strings_t strings; cx_sha3_t global_sha3; uint8_t appState; +uint16_t apdu_response_code; bool called_from_swap; pluginType_t pluginType; #ifdef HAVE_STARKWARE @@ -491,7 +493,7 @@ void handleGetWalletId(volatile unsigned int *tx) { #endif // HAVE_WALLET_ID_SDK -const uint8_t *parseBip32(const uint8_t *dataBuffer, uint16_t *dataLength, bip32_path_t *bip32) { +const uint8_t *parseBip32(const uint8_t *dataBuffer, uint8_t *dataLength, bip32_path_t *bip32) { if (*dataLength < 1) { PRINTF("Invalid data\n"); return NULL; @@ -672,13 +674,25 @@ void handleApdu(unsigned int *flags, unsigned int *tx) { break; case INS_SIGN_EIP_712_MESSAGE: - memset(tmpCtx.transactionContext.tokenSet, 0, MAX_ITEMS); - handleSignEIP712Message(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); + switch (G_io_apdu_buffer[OFFSET_P2]) { + case P2_EIP712_LEGACY_IMPLEM: + memset(tmpCtx.transactionContext.tokenSet, 0, MAX_ITEMS); + handleSignEIP712Message_v0(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); + break; +#ifdef HAVE_EIP712_FULL_SUPPORT + case P2_EIP712_FULL_IMPLEM: + *flags |= IO_ASYNCH_REPLY; + handle_eip712_sign(G_io_apdu_buffer); + break; +#endif // HAVE_EIP712_FULL_SUPPORT + default: + THROW(APDU_RESPONSE_INVALID_P1_P2); + } break; #ifdef HAVE_ETH2 @@ -704,6 +718,23 @@ void handleApdu(unsigned int *flags, unsigned int *tx) { #endif +#ifdef HAVE_EIP712_FULL_SUPPORT + case INS_EIP712_STRUCT_DEF: + *flags |= IO_ASYNCH_REPLY; + handle_eip712_struct_def(G_io_apdu_buffer); + break; + + case INS_EIP712_STRUCT_IMPL: + *flags |= IO_ASYNCH_REPLY; + handle_eip712_struct_impl(G_io_apdu_buffer); + break; + + case INS_EIP712_FILTERING: + *flags |= IO_ASYNCH_REPLY; + handle_eip712_filtering(G_io_apdu_buffer); + break; +#endif // HAVE_EIP712_FULL_SUPPORT + #if 0 case 0xFF: // return to dashboard goto return_to_dashboard; diff --git a/src/shared_context.h b/src/shared_context.h index 424c0fa..3b3b98b 100644 --- a/src/shared_context.h +++ b/src/shared_context.h @@ -27,6 +27,9 @@ typedef struct internalStorage_t { unsigned char dataAllowed; unsigned char contractDetails; unsigned char displayNonce; +#ifdef HAVE_EIP712_FULL_SUPPORT + bool verbose_eip712; +#endif // HAVE_EIP712_FULL_SUPPORT uint8_t initialized; } internalStorage_t; @@ -219,6 +222,6 @@ extern uint32_t eth2WithdrawalIndex; #endif void reset_app_context(void); -const uint8_t *parseBip32(const uint8_t *, uint16_t *, bip32_path_t *); +const uint8_t *parseBip32(const uint8_t *dataBuffer, uint8_t *dataLength, bip32_path_t *bip32); #endif // _SHARED_CONTEXT_H_ diff --git a/src/stark_utils.c b/src/stark_utils.c index 4df86e9..a585e03 100644 --- a/src/stark_utils.c +++ b/src/stark_utils.c @@ -4,6 +4,7 @@ #include "shared_context.h" #include "ethUtils.h" #include "uint256.h" +#include "uint_common.h" #include "os_io_seproxyhal.h" diff --git a/src/tokens.c b/src/tokens.c index ed5ce27..20ae6f8 100644 --- a/src/tokens.c +++ b/src/tokens.c @@ -19,7 +19,7 @@ #include "tokens.h" -const tokenDefinition_t const TOKENS_EXTRA[NUM_TOKENS_EXTRA] = { +const tokenDefinition_t TOKENS_EXTRA[NUM_TOKENS_EXTRA] = { // Ropsten DeversiFi tokens {{0x4c, 0x5f, 0x66, 0x59, 0x61, 0x97, 0xa8, 0x6f, 0xb3, 0x0a, diff --git a/src/tokens.h b/src/tokens.h index 9da9a22..7aa38f6 100644 --- a/src/tokens.h +++ b/src/tokens.h @@ -44,7 +44,7 @@ extern tokenDefinition_t const TOKENS_EXTRA[NUM_TOKENS_EXTRA]; #ifndef HAVE_TOKENS_LIST static const uint8_t LEDGER_SIGNATURE_PUBLIC_KEY[] = { -#ifndef LEDGER_TEST_PUBLIC_KEY +#ifndef HAVE_CAL_TESTING_KEY // production key 2019-01-11 03:07PM (erc20signer) 0x04, 0x5e, 0x6c, 0x10, 0x20, 0xc1, 0x4d, 0xc4, 0x64, 0x42, 0xfe, 0x89, 0xf9, 0x7c, 0x0b, 0x68, 0xcd, 0xb1, 0x59, 0x76, 0xdc, 0x24, 0xf2, 0x4c, 0x31, 0x6e, 0x7b, 0x30, diff --git a/src/ui_callbacks.h b/src/ui_callbacks.h index 1616e3e..2035c27 100644 --- a/src/ui_callbacks.h +++ b/src/ui_callbacks.h @@ -14,8 +14,6 @@ unsigned int io_seproxyhal_touch_signMessage_ok(void); unsigned int io_seproxyhal_touch_signMessage_cancel(void); unsigned int io_seproxyhal_touch_data_ok(const bagl_element_t *e); unsigned int io_seproxyhal_touch_data_cancel(const bagl_element_t *e); -unsigned int io_seproxyhal_touch_signMessage712_v0_ok(const bagl_element_t *e); -unsigned int io_seproxyhal_touch_signMessage712_v0_cancel(const bagl_element_t *e); unsigned int io_seproxyhal_touch_eth2_address_ok(const bagl_element_t *e); unsigned int io_seproxyhal_touch_privacy_ok(const bagl_element_t *e); unsigned int io_seproxyhal_touch_privacy_cancel(const bagl_element_t *e); diff --git a/src/utils.c b/src/utils.c index 7a5627f..be03f10 100644 --- a/src/utils.c +++ b/src/utils.c @@ -20,38 +20,44 @@ #include "ethUstream.h" #include "ethUtils.h" +#include "uint128.h" #include "uint256.h" #include "tokens.h" #include "utils.h" -static const unsigned char hex_digits[] = - {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; - void array_hexstr(char *strbuf, const void *bin, unsigned int len) { while (len--) { - *strbuf++ = hex_digits[((*((char *) bin)) >> 4) & 0xF]; - *strbuf++ = hex_digits[(*((char *) bin)) & 0xF]; + *strbuf++ = HEXDIGITS[((*((char *) bin)) >> 4) & 0xF]; + *strbuf++ = HEXDIGITS[(*((char *) bin)) & 0xF]; bin = (const void *) ((unsigned int) bin + 1); } *strbuf = 0; // EOS } -void convertUint256BE(uint8_t *data, uint32_t length, uint256_t *target) { - uint8_t tmp[INT256_LENGTH]; - memset(tmp, 0, 32); - memmove(tmp + 32 - length, data, length); - readu256BE(tmp, target); +void convertUint64BEto128(const uint8_t *const data, uint32_t length, uint128_t *const target) { + uint8_t tmp[INT128_LENGTH]; + int64_t value; + + value = u64_from_BE(data, length); + memset(tmp, ((value < 0) ? 0xff : 0), sizeof(tmp) - length); + memmove(tmp + sizeof(tmp) - length, data, length); + readu128BE(tmp, target); } -int local_strchr(char *string, char ch) { - unsigned int length = strlen(string); - unsigned int i; - for (i = 0; i < length; i++) { - if (string[i] == ch) { - return i; - } - } - return -1; +void convertUint128BE(const uint8_t *const data, uint32_t length, uint128_t *const target) { + uint8_t tmp[INT128_LENGTH]; + + memset(tmp, 0, sizeof(tmp) - length); + memmove(tmp + sizeof(tmp) - length, data, length); + readu128BE(tmp, target); +} + +void convertUint256BE(const uint8_t *const data, uint32_t length, uint256_t *const target) { + uint8_t tmp[INT256_LENGTH]; + + memset(tmp, 0, sizeof(tmp) - length); + memmove(tmp + sizeof(tmp) - length, data, length); + readu256BE(tmp, target); } uint64_t u64_from_BE(const uint8_t *in, uint8_t size) { diff --git a/src/utils.h b/src/utils.h index bd52398..504c1b1 100644 --- a/src/utils.h +++ b/src/utils.h @@ -22,11 +22,13 @@ #include "uint256.h" +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) + void array_hexstr(char* strbuf, const void* bin, unsigned int len); -void convertUint256BE(uint8_t* data, uint32_t length, uint256_t* target); - -int local_strchr(char* string, char ch); +void convertUint128BE(const uint8_t* const data, uint32_t length, uint128_t* const target); +void convertUint256BE(const uint8_t* const data, uint32_t length, uint256_t* const target); +void convertUint64BEto128(const uint8_t* const data, uint32_t length, uint128_t* const target); uint64_t u64_from_BE(const uint8_t* in, uint8_t size); diff --git a/src_bagl/ui_flow.c b/src_bagl/ui_flow.c index 79cedf4..7cac427 100644 --- a/src_bagl/ui_flow.c +++ b/src_bagl/ui_flow.c @@ -1,10 +1,19 @@ #include "shared_context.h" +#include "ui_callbacks.h" #include "common_ui.h" +#include "utils.h" + +#define ENABLED_STR "Enabled" +#define DISABLED_STR "Disabled" +#define BUF_INCREMENT (MAX(strlen(ENABLED_STR), strlen(DISABLED_STR)) + 1) void display_settings(const ux_flow_step_t* const start_step); void switch_settings_blind_signing(void); void switch_settings_display_data(void); void switch_settings_display_nonce(void); +#ifdef HAVE_EIP712_FULL_SUPPORT +void switch_settings_verbose_eip712(void); +#endif // HAVE_EIP712_FULL_SUPPORT ////////////////////////////////////////////////////////////////////// // clang-format off @@ -48,75 +57,83 @@ UX_FLOW(ux_idle_flow, &ux_idle_flow_4_step, FLOW_LOOP); -#if defined(TARGET_NANOS) - // clang-format off UX_STEP_CB( - ux_settings_flow_1_step, + ux_settings_flow_blind_signing_step, +#ifdef TARGET_NANOS bnnn_paging, - switch_settings_blind_signing(), - { - .title = "Blind signing", - .text = strings.common.fullAddress, - }); - -UX_STEP_CB( - ux_settings_flow_2_step, - bnnn_paging, - switch_settings_display_data(), - { - .title = "Debug data", - .text = strings.common.fullAddress + 12 - }); - -UX_STEP_CB( - ux_settings_flow_3_step, - bnnn_paging, - switch_settings_display_nonce(), - { - .title = "Account nonce", - .text = strings.common.fullAddress + 26 - }); - #else - -UX_STEP_CB( - ux_settings_flow_1_step, bnnn, +#endif switch_settings_blind_signing(), { +#ifdef TARGET_NANOS + .title = "Blind signing", + .text = +#else "Blind signing", - "Enable transaction", + "Transaction", "blind signing", - strings.common.fullAddress, +#endif + strings.common.fullAddress }); UX_STEP_CB( - ux_settings_flow_2_step, + ux_settings_flow_display_data_step, +#ifdef TARGET_NANOS + bnnn_paging, +#else bnnn, +#endif switch_settings_display_data(), { +#ifdef TARGET_NANOS + .title = "Debug data", + .text = +#else "Debug data", - "Display contract data", + "Show contract data", "details", - strings.common.fullAddress + 12 - }); - - UX_STEP_CB( - ux_settings_flow_3_step, - bnnn, - switch_settings_display_nonce(), - { - "Nonce", - "Display account nonce", - "in transactions", - strings.common.fullAddress + 26 - }); - #endif + strings.common.fullAddress + BUF_INCREMENT + }); UX_STEP_CB( - ux_settings_flow_4_step, + ux_settings_flow_display_nonce_step, +#ifdef TARGET_NANOS + bnnn_paging, +#else + bnnn, +#endif + switch_settings_display_nonce(), + { +#ifdef TARGET_NANOS + .title = "Account nonce", + .text = +#else + "Nonce", + "Show account nonce", + "in transactions", +#endif + strings.common.fullAddress + (BUF_INCREMENT * 2) + }); + +#ifdef HAVE_EIP712_FULL_SUPPORT +UX_STEP_CB( + ux_settings_flow_verbose_eip712_step, + bnnn, + switch_settings_verbose_eip712(), + { + "Verbose EIP-712", + "Ignore filtering &", + "display raw content", + strings.common.fullAddress + (BUF_INCREMENT * 3) + }); +#endif // HAVE_EIP712_FULL_SUPPORT + + +UX_STEP_CB( + ux_settings_flow_back_step, pb, ui_idle(), { @@ -126,43 +143,63 @@ UX_STEP_CB( // clang-format on UX_FLOW(ux_settings_flow, - &ux_settings_flow_1_step, - &ux_settings_flow_2_step, - &ux_settings_flow_3_step, - &ux_settings_flow_4_step); + &ux_settings_flow_blind_signing_step, + &ux_settings_flow_display_data_step, + &ux_settings_flow_display_nonce_step, +#ifdef HAVE_EIP712_FULL_SUPPORT + &ux_settings_flow_verbose_eip712_step, +#endif // HAVE_EIP712_FULL_SUPPORT + &ux_settings_flow_back_step); void display_settings(const ux_flow_step_t* const start_step) { - strlcpy(strings.common.fullAddress, (N_storage.dataAllowed ? "Enabled" : "NOT Enabled"), 12); - strlcpy(strings.common.fullAddress + 12, - (N_storage.contractDetails ? "Displayed" : "NOT Displayed"), - 26 - 12); - strlcpy(strings.common.fullAddress + 26, - (N_storage.displayNonce ? "Displayed" : "NOT Displayed"), - sizeof(strings.common.fullAddress) - 26); + bool settings[] = {N_storage.dataAllowed, + N_storage.contractDetails, + N_storage.displayNonce, +#ifdef HAVE_EIP712_FULL_SUPPORT + N_storage.verbose_eip712 +#endif // HAVE_EIP712_FULL_SUPPORT + }; + uint8_t offset = 0; + + for (unsigned int i = 0; i < ARRAY_SIZE(settings); ++i) { + strlcpy(strings.common.fullAddress + offset, + (settings[i] ? ENABLED_STR : DISABLED_STR), + sizeof(strings.common.fullAddress) - offset); + offset += BUF_INCREMENT; + } + ux_flow_init(0, ux_settings_flow, start_step); } -void switch_settings_blind_signing() { +void switch_settings_blind_signing(void) { uint8_t value = (N_storage.dataAllowed ? 0 : 1); nvm_write((void*) &N_storage.dataAllowed, (void*) &value, sizeof(uint8_t)); - display_settings(&ux_settings_flow_1_step); + display_settings(&ux_settings_flow_blind_signing_step); } -void switch_settings_display_data() { +void switch_settings_display_data(void) { uint8_t value = (N_storage.contractDetails ? 0 : 1); nvm_write((void*) &N_storage.contractDetails, (void*) &value, sizeof(uint8_t)); - display_settings(&ux_settings_flow_2_step); + display_settings(&ux_settings_flow_display_data_step); } -void switch_settings_display_nonce() { +void switch_settings_display_nonce(void) { uint8_t value = (N_storage.displayNonce ? 0 : 1); nvm_write((void*) &N_storage.displayNonce, (void*) &value, sizeof(uint8_t)); - display_settings(&ux_settings_flow_3_step); + display_settings(&ux_settings_flow_display_nonce_step); } +#ifdef HAVE_EIP712_FULL_SUPPORT +void switch_settings_verbose_eip712(void) { + bool value = !N_storage.verbose_eip712; + nvm_write((void*) &N_storage.verbose_eip712, (void*) &value, sizeof(value)); + display_settings(&ux_settings_flow_verbose_eip712_step); +} +#endif // HAVE_EIP712_FULL_SUPPORT + ////////////////////////////////////////////////////////////////////// // clang-format off -#if defined(TARGET_NANOS) +#ifdef TARGET_NANOS UX_STEP_CB( ux_warning_contract_data_step, bnnn_paging, @@ -171,7 +208,7 @@ UX_STEP_CB( "Error", "Blind signing must be enabled in Settings", }); -#elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) +#else UX_STEP_CB( ux_warning_contract_data_step, pnn, diff --git a/src_bagl/ui_flow_signMessage712.c b/src_bagl/ui_flow_signMessage712.c index ac239d7..8e0ee75 100644 --- a/src_bagl/ui_flow_signMessage712.c +++ b/src_bagl/ui_flow_signMessage712.c @@ -1,62 +1,87 @@ -#include "shared_context.h" -#include "ui_callbacks.h" +#ifdef HAVE_EIP712_FULL_SUPPORT -void prepare_domain_hash_v0() { - snprintf(strings.tmp.tmp, 70, "0x%.*H", 32, tmpCtx.messageSigningContext712.domainHash); -} +#include "ui_logic.h" +#include "shared_context.h" // strings -void prepare_message_hash_v0() { - snprintf(strings.tmp.tmp, 70, "0x%.*H", 32, tmpCtx.messageSigningContext712.messageHash); +enum { UI_712_POS_REVIEW, UI_712_POS_END }; +static uint8_t ui_pos; + +static void dummy_cb(void) { + if (!ui_712_next_field()) { + if (ui_pos == UI_712_POS_REVIEW) { + ux_flow_next(); + ui_pos = UI_712_POS_END; + } else // UI_712_POS_END + { + ux_flow_prev(); + ui_pos = UI_712_POS_REVIEW; + } + } } // clang-format off UX_STEP_NOCB( - ux_sign_712_v0_flow_1_step, + ux_712_step_review, pnn, { - &C_icon_certificate, - "Sign", + &C_icon_eye, + "Review", "typed message", }); -UX_STEP_NOCB_INIT( - ux_sign_712_v0_flow_2_step, - bnnn_paging, - prepare_domain_hash_v0(), - { - .title = "Domain hash", +UX_STEP_NOCB( + ux_712_step_dynamic, + bnnn_paging, + { + .title = strings.tmp.tmp2, .text = strings.tmp.tmp, - }); -UX_STEP_NOCB_INIT( - ux_sign_712_v0_flow_3_step, - bnnn_paging, - prepare_message_hash_v0(), - { - .title = "Message hash", - .text = strings.tmp.tmp, - }); + } +); +UX_STEP_INIT( + ux_712_step_dummy, + NULL, + NULL, + { + dummy_cb(); + } +); UX_STEP_CB( - ux_sign_712_v0_flow_4_step, - pbb, - io_seproxyhal_touch_signMessage712_v0_ok(NULL), + ux_712_step_approve, + pb, + ui_712_approve(NULL), { &C_icon_validate_14, - "Sign", - "message", + "Approve", }); UX_STEP_CB( - ux_sign_712_v0_flow_5_step, - pbb, - io_seproxyhal_touch_signMessage712_v0_cancel(NULL), + ux_712_step_reject, + pb, + ui_712_reject(NULL), { &C_icon_crossmark, - "Cancel", - "signature", + "Reject", }); // clang-format on -UX_FLOW(ux_sign_712_v0_flow, - &ux_sign_712_v0_flow_1_step, - &ux_sign_712_v0_flow_2_step, - &ux_sign_712_v0_flow_3_step, - &ux_sign_712_v0_flow_4_step, - &ux_sign_712_v0_flow_5_step); +UX_FLOW(ux_712_flow, + &ux_712_step_review, + &ux_712_step_dynamic, + &ux_712_step_dummy, + &ux_712_step_approve, + &ux_712_step_reject); + +void ui_712_start(void) { + ux_flow_init(0, ux_712_flow, NULL); + ui_pos = UI_712_POS_REVIEW; +} + +void ui_712_switch_to_message(void) { + ux_flow_init(0, ux_712_flow, &ux_712_step_dynamic); + ui_pos = UI_712_POS_REVIEW; +} + +void ui_712_switch_to_sign(void) { + ux_flow_init(0, ux_712_flow, &ux_712_step_approve); + ui_pos = UI_712_POS_END; +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_bagl/ui_flow_signMessage712_v0.c b/src_bagl/ui_flow_signMessage712_v0.c new file mode 100644 index 0000000..230852e --- /dev/null +++ b/src_bagl/ui_flow_signMessage712_v0.c @@ -0,0 +1,72 @@ +#include "shared_context.h" +#include "ui_callbacks.h" +#include "common_712.h" +#include "ethUtils.h" + +void prepare_domain_hash_v0() { + snprintf(strings.tmp.tmp, + sizeof(strings.tmp.tmp), + "0x%.*H", + KECCAK256_HASH_BYTESIZE, + tmpCtx.messageSigningContext712.domainHash); +} + +void prepare_message_hash_v0() { + snprintf(strings.tmp.tmp, + sizeof(strings.tmp.tmp), + "0x%.*H", + KECCAK256_HASH_BYTESIZE, + tmpCtx.messageSigningContext712.messageHash); +} + +// clang-format off +UX_STEP_NOCB( + ux_sign_712_v0_flow_1_step, + pnn, + { + &C_icon_certificate, + "Sign", + "typed message", + }); +UX_STEP_NOCB_INIT( + ux_sign_712_v0_flow_2_step, + bnnn_paging, + prepare_domain_hash_v0(), + { + .title = "Domain hash", + .text = strings.tmp.tmp, + }); +UX_STEP_NOCB_INIT( + ux_sign_712_v0_flow_3_step, + bnnn_paging, + prepare_message_hash_v0(), + { + .title = "Message hash", + .text = strings.tmp.tmp, + }); +UX_STEP_CB( + ux_sign_712_v0_flow_4_step, + pbb, + ui_712_approve_cb(NULL), + { + &C_icon_validate_14, + "Sign", + "message", + }); +UX_STEP_CB( + ux_sign_712_v0_flow_5_step, + pbb, + ui_712_reject_cb(NULL), + { + &C_icon_crossmark, + "Cancel", + "signature", + }); +// clang-format on + +UX_FLOW(ux_sign_712_v0_flow, + &ux_sign_712_v0_flow_1_step, + &ux_sign_712_v0_flow_2_step, + &ux_sign_712_v0_flow_3_step, + &ux_sign_712_v0_flow_4_step, + &ux_sign_712_v0_flow_5_step); diff --git a/src_bagl/ui_flow_signTx.c b/src_bagl/ui_flow_signTx.c index 6d05d6d..574dd9c 100644 --- a/src_bagl/ui_flow_signTx.c +++ b/src_bagl/ui_flow_signTx.c @@ -7,6 +7,7 @@ #include "eth_plugin_handler.h" #include "ui_plugin.h" #include "common_ui.h" +#include "plugins.h" // clang-format off UX_STEP_NOCB( diff --git a/src_bagl/ui_flow_stark_sign.c b/src_bagl/ui_flow_stark_sign.c index df3c6a1..548e880 100644 --- a/src_bagl/ui_flow_stark_sign.c +++ b/src_bagl/ui_flow_stark_sign.c @@ -3,18 +3,7 @@ #include "shared_context.h" #include "ui_callbacks.h" #include "ethUtils.h" - -void stark_sign_display_master_account() { - snprintf(strings.tmp.tmp, - sizeof(strings.tmp.tmp), - "0x%.*H", - 32, - dataContext.starkContext.transferDestination); -} - -void stark_sign_display_condition_fact() { - snprintf(strings.tmp.tmp, sizeof(strings.tmp.tmp), "0x%.*H", 32, dataContext.starkContext.fact); -} +#include "starkDisplayUtils.h" // clang-format off UX_STEP_NOCB(ux_stark_limit_order_1_step, diff --git a/src_bagl/ui_plugin.c b/src_bagl/ui_plugin.c index ed3f52f..c3743fa 100644 --- a/src_bagl/ui_plugin.c +++ b/src_bagl/ui_plugin.c @@ -3,38 +3,11 @@ #include "eth_plugin_handler.h" #include "ui_callbacks.h" #include "ui_plugin.h" +#include "plugins.h" // This function is not exported by the SDK void ux_layout_paging_redisplay_by_addr(unsigned int stack_slot); -void plugin_ui_get_id() { - ethQueryContractID_t pluginQueryContractID; - eth_plugin_prepare_query_contract_ID(&pluginQueryContractID, - strings.common.fullAddress, - sizeof(strings.common.fullAddress), - strings.common.fullAmount, - sizeof(strings.common.fullAmount)); - // Query the original contract for ID if it's not an internal alias - if (!eth_plugin_call(ETH_PLUGIN_QUERY_CONTRACT_ID, (void *) &pluginQueryContractID)) { - PRINTF("Plugin query contract ID call failed\n"); - io_seproxyhal_touch_tx_cancel(NULL); - } -} - -void plugin_ui_get_item() { - ethQueryContractUI_t pluginQueryContractUI; - eth_plugin_prepare_query_contract_UI(&pluginQueryContractUI, - dataContext.tokenContext.pluginUiCurrentItem, - strings.common.fullAddress, - sizeof(strings.common.fullAddress), - strings.common.fullAmount, - sizeof(strings.common.fullAmount)); - if (!eth_plugin_call(ETH_PLUGIN_QUERY_CONTRACT_UI, (void *) &pluginQueryContractUI)) { - PRINTF("Plugin query contract UI call failed\n"); - io_seproxyhal_touch_tx_cancel(NULL); - } -} - void display_next_plugin_item(bool entering) { if (entering) { if (dataContext.tokenContext.pluginUiState == PLUGIN_UI_OUTSIDE) { diff --git a/src_bagl/ui_plugin.h b/src_bagl/ui_plugin.h index ae86b6a..2f9fe60 100644 --- a/src_bagl/ui_plugin.h +++ b/src_bagl/ui_plugin.h @@ -1,8 +1,6 @@ #ifndef _UI_PLUGIN_H_ #define _UI_PLUGIN_H_ -void plugin_ui_get_id(); -void plugin_ui_get_item(); void display_next_plugin_item(bool entering); #endif // _UI_PLUGIN_H_ diff --git a/src_common/ethUstream.h b/src_common/ethUstream.h index 4229d40..ed5aea3 100644 --- a/src_common/ethUstream.h +++ b/src_common/ethUstream.h @@ -37,6 +37,7 @@ typedef customStatus_e (*ustreamProcess_t)(struct txContext_t *context); #define TX_FLAG_TYPE 0x01 #define ADDRESS_LENGTH 20 +#define INT128_LENGTH 16 #define INT256_LENGTH 32 // First variant of every Tx enum. diff --git a/src_common/ethUtils.c b/src_common/ethUtils.c index 804837c..4dd0e6a 100644 --- a/src_common/ethUtils.c +++ b/src_common/ethUtils.c @@ -27,8 +27,6 @@ #include #include -#include "os.h" -#include "cx.h" #include "ethUtils.h" #include "chainConfig.h" #include "ethUstream.h" diff --git a/src_common/ethUtils.h b/src_common/ethUtils.h index 3c147cc..0755b13 100644 --- a/src_common/ethUtils.h +++ b/src_common/ethUtils.h @@ -23,6 +23,8 @@ #include "cx.h" #include "chainConfig.h" +#define KECCAK256_HASH_BYTESIZE 32 + /** * @brief Decode an RLP encoded field - see * https://github.com/ethereum/wiki/wiki/RLP diff --git a/src_common/mem.c b/src_common/mem.c new file mode 100644 index 0000000..2234cf4 --- /dev/null +++ b/src_common/mem.c @@ -0,0 +1,65 @@ +/** + * Dynamic allocator that uses a fixed-length buffer that is hopefully big enough + * + * The two functions alloc & dealloc use the buffer as a simple stack. + * Especially useful when an unpredictable amount of data will be received and have to be stored + * during the transaction but discarded right after. + */ + +#ifdef HAVE_DYN_MEM_ALLOC + +#include +#include "mem.h" + +#define SIZE_MEM_BUFFER 8192 + +static uint8_t mem_buffer[SIZE_MEM_BUFFER]; +static size_t mem_idx; + +/** + * Initializes the memory buffer index + */ +void mem_init(void) { + mem_idx = 0; +} + +/** + * Resets the memory buffer index + */ +void mem_reset(void) { + mem_init(); +} + +/** + * Allocates (push) a chunk of the memory buffer of a given size. + * + * Checks to see if there are enough space left in the memory buffer, returns + * the current location in the memory buffer and moves the index accordingly. + * + * @param[in] size Requested allocation size in bytes + * @return Allocated memory pointer; \ref NULL if not enough space left. + */ +void *mem_alloc(size_t size) { + if ((mem_idx + size) > SIZE_MEM_BUFFER) // Buffer exceeded + { + return NULL; + } + mem_idx += size; + return &mem_buffer[mem_idx - size]; +} + +/** + * De-allocates (pop) a chunk of memory buffer by a given size. + * + * @param[in] size Requested deallocation size in bytes + */ +void mem_dealloc(size_t size) { + if (size > mem_idx) // More than is already allocated + { + mem_idx = 0; + } else { + mem_idx -= size; + } +} + +#endif // HAVE_DYN_MEM_ALLOC diff --git a/src_common/mem.h b/src_common/mem.h new file mode 100644 index 0000000..eafa797 --- /dev/null +++ b/src_common/mem.h @@ -0,0 +1,15 @@ +#ifndef MEM_H_ +#define MEM_H_ + +#ifdef HAVE_DYN_MEM_ALLOC + +#include + +void mem_init(void); +void mem_reset(void); +void *mem_alloc(size_t size); +void mem_dealloc(size_t size); + +#endif // HAVE_DYN_MEM_ALLOC + +#endif // MEM_H_ diff --git a/src_common/mem_utils.c b/src_common/mem_utils.c new file mode 100644 index 0000000..b7f58ad --- /dev/null +++ b/src_common/mem_utils.c @@ -0,0 +1,60 @@ +#ifdef HAVE_DYN_MEM_ALLOC + +#include +#include +#include +#include "mem.h" +#include "mem_utils.h" + +/** + * Format an unsigned number up to 32-bit into memory into an ASCII string. + * + * @param[in] value Value to write in memory + * @param[out] length number of characters written to memory + * + * @return pointer to memory area or \ref NULL if the allocation failed + */ +char *mem_alloc_and_format_uint(uint32_t value, uint8_t *const length) { + char *mem_ptr; + uint32_t value_copy; + uint8_t size; + + size = 1; // minimum size, even if 0 + value_copy = value; + while (value_copy >= 10) { + value_copy /= 10; + size += 1; + } + // +1 for the null character + if ((mem_ptr = mem_alloc(sizeof(char) * (size + 1)))) { + snprintf(mem_ptr, (size + 1), "%u", value); + mem_dealloc(sizeof(char)); // to skip the null character + if (length != NULL) { + *length = size; + } + } + return mem_ptr; +} + +/** + * Allocate and align, required when dealing with pointers of multi-bytes data + * like structures that will be dereferenced at runtime. + * + * @param[in] size the size of the data we want to allocate in memory + * @param[in] alignment the byte alignment needed + * + * @return pointer to the memory area, \ref NULL if the allocation failed + */ +void *mem_alloc_and_align(size_t size, size_t alignment) { + uint8_t align_diff = (uintptr_t) mem_alloc(0) % alignment; + + if (align_diff > 0) // alignment needed + { + if (mem_alloc(alignment - align_diff) == NULL) { + return NULL; + } + } + return mem_alloc(size); +} + +#endif // HAVE_DYN_MEM_ALLOC diff --git a/src_common/mem_utils.h b/src_common/mem_utils.h new file mode 100644 index 0000000..c6440e1 --- /dev/null +++ b/src_common/mem_utils.h @@ -0,0 +1,16 @@ +#ifndef MEM_UTILS_H_ +#define MEM_UTILS_H_ + +#ifdef HAVE_DYN_MEM_ALLOC + +#include +#include + +#define MEM_ALLOC_AND_ALIGN_TYPE(type) mem_alloc_and_align(sizeof(type), __alignof__(type)) + +char *mem_alloc_and_format_uint(uint32_t value, uint8_t *const written_chars); +void *mem_alloc_and_align(size_t size, size_t alignment); + +#endif // HAVE_DYN_MEM_ALLOC + +#endif // MEM_UTILS_H_ diff --git a/src_common/plugins.c b/src_common/plugins.c new file mode 100644 index 0000000..42c17a9 --- /dev/null +++ b/src_common/plugins.c @@ -0,0 +1,40 @@ +#include "eth_plugin_handler.h" +#include "ui_callbacks.h" + +void plugin_ui_get_id(void) { + ethQueryContractID_t pluginQueryContractID; + eth_plugin_prepare_query_contract_ID(&pluginQueryContractID, + strings.common.fullAddress, + sizeof(strings.common.fullAddress), + strings.common.fullAmount, + sizeof(strings.common.fullAmount)); + // Query the original contract for ID if it's not an internal alias + if (!eth_plugin_call(ETH_PLUGIN_QUERY_CONTRACT_ID, (void *) &pluginQueryContractID)) { + PRINTF("Plugin query contract ID call failed\n"); + io_seproxyhal_touch_tx_cancel(NULL); + } +} + +void plugin_ui_get_item_internal(char *title_buffer, + size_t title_buffer_size, + char *msg_buffer, + size_t msg_buffer_size) { + ethQueryContractUI_t pluginQueryContractUI; + eth_plugin_prepare_query_contract_UI(&pluginQueryContractUI, + dataContext.tokenContext.pluginUiCurrentItem, + title_buffer, + title_buffer_size, + msg_buffer, + msg_buffer_size); + if (!eth_plugin_call(ETH_PLUGIN_QUERY_CONTRACT_UI, (void *) &pluginQueryContractUI)) { + PRINTF("Plugin query contract UI call failed\n"); + io_seproxyhal_touch_tx_cancel(NULL); + } +} + +void plugin_ui_get_item(void) { + plugin_ui_get_item_internal(strings.common.fullAddress, + sizeof(strings.common.fullAddress), + strings.common.fullAmount, + sizeof(strings.common.fullAmount)); +} \ No newline at end of file diff --git a/src_common/plugins.h b/src_common/plugins.h new file mode 100644 index 0000000..1ccc123 --- /dev/null +++ b/src_common/plugins.h @@ -0,0 +1,11 @@ +#ifndef _PLUGIN_H_ +#define _PLUGIN_H_ + +void plugin_ui_get_id(); +void plugin_ui_get_item(); +void plugin_ui_get_item_internal(uint8_t *title_buffer, + size_t title_buffer_size, + uint8_t *msg_buffer, + size_t msg_buffer_size); + +#endif // _PLUGIN_H_ \ No newline at end of file diff --git a/src_common/starkDisplayUtils.c b/src_common/starkDisplayUtils.c new file mode 100644 index 0000000..a59ae99 --- /dev/null +++ b/src_common/starkDisplayUtils.c @@ -0,0 +1,17 @@ +#ifdef HAVE_STARKWARE + +#include "shared_context.h" + +void stark_sign_display_master_account() { + snprintf(strings.tmp.tmp, + sizeof(strings.tmp.tmp), + "0x%.*H", + 32, + dataContext.starkContext.transferDestination); +} + +void stark_sign_display_condition_fact() { + snprintf(strings.tmp.tmp, sizeof(strings.tmp.tmp), "0x%.*H", 32, dataContext.starkContext.fact); +} + +#endif \ No newline at end of file diff --git a/src_common/starkDisplayUtils.h b/src_common/starkDisplayUtils.h new file mode 100644 index 0000000..48399b8 --- /dev/null +++ b/src_common/starkDisplayUtils.h @@ -0,0 +1,6 @@ +#ifdef HAVE_STARKWARE + +void stark_sign_display_master_account(); +void stark_sign_display_condition_fact(); + +#endif \ No newline at end of file diff --git a/src_common/uint128.c b/src_common/uint128.c new file mode 100644 index 0000000..0663a24 --- /dev/null +++ b/src_common/uint128.c @@ -0,0 +1,292 @@ +/******************************************************************************* + * Ledger Ethereum App + * (c) 2016-2019 Ledger + * + * 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. + ********************************************************************************/ + +// Adapted from https://github.com/calccrypto/uint256_t + +#include +#include +#include "uint128.h" +#include "uint_common.h" +#include "ethUtils.h" // HEXDIGITS + +void readu128BE(const uint8_t *const buffer, uint128_t *const target) { + UPPER_P(target) = readUint64BE(buffer); + LOWER_P(target) = readUint64BE(buffer + 8); +} + +bool zero128(const uint128_t *const number) { + return ((LOWER_P(number) == 0) && (UPPER_P(number) == 0)); +} + +void copy128(uint128_t *const target, const uint128_t *const number) { + UPPER_P(target) = UPPER_P(number); + LOWER_P(target) = LOWER_P(number); +} + +void clear128(uint128_t *const target) { + UPPER_P(target) = 0; + LOWER_P(target) = 0; +} + +void shiftl128(const uint128_t *const number, uint32_t value, uint128_t *const target) { + if (value >= 128) { + clear128(target); + } else if (value == 64) { + UPPER_P(target) = LOWER_P(number); + LOWER_P(target) = 0; + } else if (value == 0) { + copy128(target, number); + } else if (value < 64) { + UPPER_P(target) = (UPPER_P(number) << value) + (LOWER_P(number) >> (64 - value)); + LOWER_P(target) = (LOWER_P(number) << value); + } else if ((128 > value) && (value > 64)) { + UPPER_P(target) = LOWER_P(number) << (value - 64); + LOWER_P(target) = 0; + } else { + clear128(target); + } +} + +void shiftr128(const uint128_t *const number, uint32_t value, uint128_t *const target) { + if (value >= 128) { + clear128(target); + } else if (value == 64) { + UPPER_P(target) = 0; + LOWER_P(target) = UPPER_P(number); + } else if (value == 0) { + copy128(target, number); + } else if (value < 64) { + uint128_t result; + UPPER(result) = UPPER_P(number) >> value; + LOWER(result) = (UPPER_P(number) << (64 - value)) + (LOWER_P(number) >> value); + copy128(target, &result); + } else if ((128 > value) && (value > 64)) { + LOWER_P(target) = UPPER_P(number) >> (value - 64); + UPPER_P(target) = 0; + } else { + clear128(target); + } +} + +uint32_t bits128(const uint128_t *const number) { + uint32_t result = 0; + if (UPPER_P(number)) { + result = 64; + uint64_t up = UPPER_P(number); + while (up) { + up >>= 1; + result++; + } + } else { + uint64_t low = LOWER_P(number); + while (low) { + low >>= 1; + result++; + } + } + return result; +} + +bool equal128(const uint128_t *const number1, const uint128_t *const number2) { + return (UPPER_P(number1) == UPPER_P(number2)) && (LOWER_P(number1) == LOWER_P(number2)); +} + +bool gt128(const uint128_t *const number1, const uint128_t *const number2) { + if (UPPER_P(number1) == UPPER_P(number2)) { + return (LOWER_P(number1) > LOWER_P(number2)); + } + return (UPPER_P(number1) > UPPER_P(number2)); +} + +bool gte128(const uint128_t *const number1, const uint128_t *const number2) { + return gt128(number1, number2) || equal128(number1, number2); +} + +void add128(const uint128_t *const number1, + const uint128_t *const number2, + uint128_t *const target) { + UPPER_P(target) = UPPER_P(number1) + UPPER_P(number2) + + ((LOWER_P(number1) + LOWER_P(number2)) < LOWER_P(number1)); + LOWER_P(target) = LOWER_P(number1) + LOWER_P(number2); +} + +void sub128(const uint128_t *const number1, + const uint128_t *const number2, + uint128_t *const target) { + UPPER_P(target) = UPPER_P(number1) - UPPER_P(number2) - + ((LOWER_P(number1) - LOWER_P(number2)) > LOWER_P(number1)); + LOWER_P(target) = LOWER_P(number1) - LOWER_P(number2); +} + +void or128(const uint128_t *const number1, + const uint128_t *const number2, + uint128_t *const target) { + UPPER_P(target) = UPPER_P(number1) | UPPER_P(number2); + LOWER_P(target) = LOWER_P(number1) | LOWER_P(number2); +} + +void mul128(const uint128_t *const number1, + const uint128_t *const number2, + uint128_t *const target) { + uint64_t top[4] = {UPPER_P(number1) >> 32, + UPPER_P(number1) & 0xffffffff, + LOWER_P(number1) >> 32, + LOWER_P(number1) & 0xffffffff}; + uint64_t bottom[4] = {UPPER_P(number2) >> 32, + UPPER_P(number2) & 0xffffffff, + LOWER_P(number2) >> 32, + LOWER_P(number2) & 0xffffffff}; + uint64_t products[4][4]; + uint128_t tmp, tmp2; + + for (int y = 3; y > -1; y--) { + for (int x = 3; x > -1; x--) { + products[3 - x][y] = top[x] * bottom[y]; + } + } + + uint64_t fourth32 = products[0][3] & 0xffffffff; + uint64_t third32 = (products[0][2] & 0xffffffff) + (products[0][3] >> 32); + uint64_t second32 = (products[0][1] & 0xffffffff) + (products[0][2] >> 32); + uint64_t first32 = (products[0][0] & 0xffffffff) + (products[0][1] >> 32); + + third32 += products[1][3] & 0xffffffff; + second32 += (products[1][2] & 0xffffffff) + (products[1][3] >> 32); + first32 += (products[1][1] & 0xffffffff) + (products[1][2] >> 32); + + second32 += products[2][3] & 0xffffffff; + first32 += (products[2][2] & 0xffffffff) + (products[2][3] >> 32); + + first32 += products[3][3] & 0xffffffff; + + UPPER(tmp) = first32 << 32; + LOWER(tmp) = 0; + UPPER(tmp2) = third32 >> 32; + LOWER(tmp2) = third32 << 32; + add128(&tmp, &tmp2, target); + UPPER(tmp) = second32; + LOWER(tmp) = 0; + add128(&tmp, target, &tmp2); + UPPER(tmp) = 0; + LOWER(tmp) = fourth32; + add128(&tmp, &tmp2, target); +} + +void divmod128(const uint128_t *const l, + const uint128_t *const r, + uint128_t *const retDiv, + uint128_t *const retMod) { + uint128_t copyd, adder, resDiv, resMod; + uint128_t one; + UPPER(one) = 0; + LOWER(one) = 1; + uint32_t diffBits = bits128(l) - bits128(r); + clear128(&resDiv); + copy128(&resMod, l); + if (gt128(r, l)) { + copy128(retMod, l); + clear128(retDiv); + } else { + shiftl128(r, diffBits, ©d); + shiftl128(&one, diffBits, &adder); + if (gt128(©d, &resMod)) { + shiftr128(©d, 1, ©d); + shiftr128(&adder, 1, &adder); + } + while (gte128(&resMod, r)) { + if (gte128(&resMod, ©d)) { + sub128(&resMod, ©d, &resMod); + or128(&resDiv, &adder, &resDiv); + } + shiftr128(©d, 1, ©d); + shiftr128(&adder, 1, &adder); + } + copy128(retDiv, &resDiv); + copy128(retMod, &resMod); + } +} + +bool tostring128(const uint128_t *const number, + uint32_t baseParam, + char *const out, + uint32_t outLength) { + uint128_t rDiv; + uint128_t rMod; + uint128_t base; + copy128(&rDiv, number); + clear128(&rMod); + clear128(&base); + LOWER(base) = baseParam; + uint32_t offset = 0; + if ((baseParam < 2) || (baseParam > 16)) { + return false; + } + do { + if (offset > (outLength - 1)) { + return false; + } + divmod128(&rDiv, &base, &rDiv, &rMod); + out[offset++] = HEXDIGITS[(uint8_t) LOWER(rMod)]; + } while (!zero128(&rDiv)); + + if (offset > (outLength - 1)) { + return false; + } + + out[offset] = '\0'; + reverseString(out, offset); + return true; +} + +/** + * Format a uint128_t into a string as a signed integer + * + * @param[in] number the number to format + * @param[in] base the radix used in formatting + * @param[out] out the output buffer + * @param[in] out_length the length of the output buffer + * @return whether the formatting was successful or not + */ +bool tostring128_signed(const uint128_t *const number, + uint32_t base, + char *const out, + uint32_t out_length) { + uint128_t max_unsigned_val; + uint128_t max_signed_val; + uint128_t one_val; + uint128_t two_val; + uint128_t tmp; + + // showing negative numbers only really makes sense in base 10 + if (base == 10) { + explicit_bzero(&one_val, sizeof(one_val)); + LOWER(one_val) = 1; + explicit_bzero(&two_val, sizeof(two_val)); + LOWER(two_val) = 2; + + memset(&max_unsigned_val, 0xFF, sizeof(max_unsigned_val)); + divmod128(&max_unsigned_val, &two_val, &max_signed_val, &tmp); + if (gt128(number, &max_signed_val)) // negative value + { + sub128(&max_unsigned_val, number, &tmp); + add128(&tmp, &one_val, &tmp); + out[0] = '-'; + return tostring128(&tmp, base, out + 1, out_length - 1); + } + } + return tostring128(number, base, out, out_length); // positive value +} diff --git a/src_common/uint128.h b/src_common/uint128.h new file mode 100644 index 0000000..e1166c2 --- /dev/null +++ b/src_common/uint128.h @@ -0,0 +1,60 @@ +/******************************************************************************* + * Ledger Ethereum App + * (c) 2016-2019 Ledger + * + * 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. + ********************************************************************************/ + +// Adapted from https://github.com/calccrypto/uint256_t + +#ifndef _UINT128_H_ +#define _UINT128_H_ + +#include +#include + +typedef struct uint128_t { + uint64_t elements[2]; +} uint128_t; + +void readu128BE(const uint8_t *const buffer, uint128_t *const target); +bool zero128(const uint128_t *const number); +void copy128(uint128_t *const target, const uint128_t *const number); +void clear128(uint128_t *const target); +void shiftl128(const uint128_t *const number, uint32_t value, uint128_t *const target); +void shiftr128(const uint128_t *const number, uint32_t value, uint128_t *const target); +uint32_t bits128(const uint128_t *const number); +bool equal128(const uint128_t *const number1, const uint128_t *const number2); +bool gt128(const uint128_t *const number1, const uint128_t *const number2); +bool gte128(const uint128_t *const number1, const uint128_t *const number2); +void add128(const uint128_t *const number1, + const uint128_t *const number2, + uint128_t *const target); +void sub128(const uint128_t *const number1, + const uint128_t *const number2, + uint128_t *const target); +void or128(const uint128_t *const number1, const uint128_t *const number2, uint128_t *const target); +void mul128(const uint128_t *const number1, + const uint128_t *const number2, + uint128_t *const target); +void divmod128(const uint128_t *const l, + const uint128_t *const r, + uint128_t *const div, + uint128_t *const mod); +bool tostring128(const uint128_t *const number, uint32_t base, char *const out, uint32_t outLength); +bool tostring128_signed(const uint128_t *const number, + uint32_t base, + char *const out, + uint32_t out_length); + +#endif // _UINT128_H_ diff --git a/src_common/uint256.c b/src_common/uint256.c index 22b738e..9da483d 100644 --- a/src_common/uint256.c +++ b/src_common/uint256.c @@ -19,76 +19,31 @@ #include #include - #include "uint256.h" +#include "uint_common.h" +#include "ethUstream.h" // INT256_LENGTH +#include "ethUtils.h" // HEXDIGITS -static const char HEXDIGITS[] = "0123456789abcdef"; - -static uint64_t readUint64BE(uint8_t *buffer) { - return (((uint64_t) buffer[0]) << 56) | (((uint64_t) buffer[1]) << 48) | - (((uint64_t) buffer[2]) << 40) | (((uint64_t) buffer[3]) << 32) | - (((uint64_t) buffer[4]) << 24) | (((uint64_t) buffer[5]) << 16) | - (((uint64_t) buffer[6]) << 8) | (((uint64_t) buffer[7])); -} - -void readu128BE(uint8_t *buffer, uint128_t *target) { - UPPER_P(target) = readUint64BE(buffer); - LOWER_P(target) = readUint64BE(buffer + 8); -} - -void readu256BE(uint8_t *buffer, uint256_t *target) { +void readu256BE(const uint8_t *const buffer, uint256_t *const target) { readu128BE(buffer, &UPPER_P(target)); readu128BE(buffer + 16, &LOWER_P(target)); } -bool zero128(uint128_t *number) { - return ((LOWER_P(number) == 0) && (UPPER_P(number) == 0)); -} - -bool zero256(uint256_t *number) { +bool zero256(const uint256_t *const number) { return (zero128(&LOWER_P(number)) && zero128(&UPPER_P(number))); } -void copy128(uint128_t *target, uint128_t *number) { - UPPER_P(target) = UPPER_P(number); - LOWER_P(target) = LOWER_P(number); -} - -void copy256(uint256_t *target, uint256_t *number) { +void copy256(uint256_t *const target, const uint256_t *const number) { copy128(&UPPER_P(target), &UPPER_P(number)); copy128(&LOWER_P(target), &LOWER_P(number)); } -void clear128(uint128_t *target) { - UPPER_P(target) = 0; - LOWER_P(target) = 0; -} - -void clear256(uint256_t *target) { +void clear256(uint256_t *const target) { clear128(&UPPER_P(target)); clear128(&LOWER_P(target)); } -void shiftl128(uint128_t *number, uint32_t value, uint128_t *target) { - if (value >= 128) { - clear128(target); - } else if (value == 64) { - UPPER_P(target) = LOWER_P(number); - LOWER_P(target) = 0; - } else if (value == 0) { - copy128(target, number); - } else if (value < 64) { - UPPER_P(target) = (UPPER_P(number) << value) + (LOWER_P(number) >> (64 - value)); - LOWER_P(target) = (LOWER_P(number) << value); - } else if ((128 > value) && (value > 64)) { - UPPER_P(target) = LOWER_P(number) << (value - 64); - LOWER_P(target) = 0; - } else { - clear128(target); - } -} - -void shiftl256(uint256_t *number, uint32_t value, uint256_t *target) { +void shiftl256(const uint256_t *const number, uint32_t value, uint256_t *const target) { if (value >= 256) { clear256(target); } else if (value == 128) { @@ -113,28 +68,7 @@ void shiftl256(uint256_t *number, uint32_t value, uint256_t *target) { } } -void shiftr128(uint128_t *number, uint32_t value, uint128_t *target) { - if (value >= 128) { - clear128(target); - } else if (value == 64) { - UPPER_P(target) = 0; - LOWER_P(target) = UPPER_P(number); - } else if (value == 0) { - copy128(target, number); - } else if (value < 64) { - uint128_t result; - UPPER(result) = UPPER_P(number) >> value; - LOWER(result) = (UPPER_P(number) << (64 - value)) + (LOWER_P(number) >> value); - copy128(target, &result); - } else if ((128 > value) && (value > 64)) { - LOWER_P(target) = UPPER_P(number) >> (value - 64); - UPPER_P(target) = 0; - } else { - clear128(target); - } -} - -void shiftr256(uint256_t *number, uint32_t value, uint256_t *target) { +void shiftr256(const uint256_t *const number, uint32_t value, uint256_t *const target) { if (value >= 256) { clear256(target); } else if (value == 128) { @@ -159,26 +93,7 @@ void shiftr256(uint256_t *number, uint32_t value, uint256_t *target) { } } -uint32_t bits128(uint128_t *number) { - uint32_t result = 0; - if (UPPER_P(number)) { - result = 64; - uint64_t up = UPPER_P(number); - while (up) { - up >>= 1; - result++; - } - } else { - uint64_t low = LOWER_P(number); - while (low) { - low >>= 1; - result++; - } - } - return result; -} - -uint32_t bits256(uint256_t *number) { +uint32_t bits256(const uint256_t *const number) { uint32_t result = 0; if (!zero128(&UPPER_P(number))) { result = 128; @@ -199,44 +114,25 @@ uint32_t bits256(uint256_t *number) { return result; } -bool equal128(uint128_t *number1, uint128_t *number2) { - return (UPPER_P(number1) == UPPER_P(number2)) && (LOWER_P(number1) == LOWER_P(number2)); -} - -bool equal256(uint256_t *number1, uint256_t *number2) { +bool equal256(const uint256_t *const number1, const uint256_t *const number2) { return (equal128(&UPPER_P(number1), &UPPER_P(number2)) && equal128(&LOWER_P(number1), &LOWER_P(number2))); } -bool gt128(uint128_t *number1, uint128_t *number2) { - if (UPPER_P(number1) == UPPER_P(number2)) { - return (LOWER_P(number1) > LOWER_P(number2)); - } - return (UPPER_P(number1) > UPPER_P(number2)); -} - -bool gt256(uint256_t *number1, uint256_t *number2) { +bool gt256(const uint256_t *const number1, const uint256_t *const number2) { if (equal128(&UPPER_P(number1), &UPPER_P(number2))) { return gt128(&LOWER_P(number1), &LOWER_P(number2)); } return gt128(&UPPER_P(number1), &UPPER_P(number2)); } -bool gte128(uint128_t *number1, uint128_t *number2) { - return gt128(number1, number2) || equal128(number1, number2); -} - -bool gte256(uint256_t *number1, uint256_t *number2) { +bool gte256(const uint256_t *const number1, const uint256_t *const number2) { return gt256(number1, number2) || equal256(number1, number2); } -void add128(uint128_t *number1, uint128_t *number2, uint128_t *target) { - UPPER_P(target) = UPPER_P(number1) + UPPER_P(number2) + - ((LOWER_P(number1) + LOWER_P(number2)) < LOWER_P(number1)); - LOWER_P(target) = LOWER_P(number1) + LOWER_P(number2); -} - -void add256(uint256_t *number1, uint256_t *number2, uint256_t *target) { +void add256(const uint256_t *const number1, + const uint256_t *const number2, + uint256_t *const target) { uint128_t tmp; add128(&UPPER_P(number1), &UPPER_P(number2), &UPPER_P(target)); add128(&LOWER_P(number1), &LOWER_P(number2), &tmp); @@ -249,104 +145,31 @@ void add256(uint256_t *number1, uint256_t *number2, uint256_t *target) { add128(&LOWER_P(number1), &LOWER_P(number2), &LOWER_P(target)); } -void minus128(uint128_t *number1, uint128_t *number2, uint128_t *target) { - UPPER_P(target) = UPPER_P(number1) - UPPER_P(number2) - - ((LOWER_P(number1) - LOWER_P(number2)) > LOWER_P(number1)); - LOWER_P(target) = LOWER_P(number1) - LOWER_P(number2); -} - -void minus256(uint256_t *number1, uint256_t *number2, uint256_t *target) { +void sub256(const uint256_t *const number1, + const uint256_t *const number2, + uint256_t *const target) { uint128_t tmp; - minus128(&UPPER_P(number1), &UPPER_P(number2), &UPPER_P(target)); - minus128(&LOWER_P(number1), &LOWER_P(number2), &tmp); + sub128(&UPPER_P(number1), &UPPER_P(number2), &UPPER_P(target)); + sub128(&LOWER_P(number1), &LOWER_P(number2), &tmp); if (gt128(&tmp, &LOWER_P(number1))) { uint128_t one; UPPER(one) = 0; LOWER(one) = 1; - minus128(&UPPER_P(target), &one, &UPPER_P(target)); + sub128(&UPPER_P(target), &one, &UPPER_P(target)); } - minus128(&LOWER_P(number1), &LOWER_P(number2), &LOWER_P(target)); + sub128(&LOWER_P(number1), &LOWER_P(number2), &LOWER_P(target)); } -void or128(uint128_t *number1, uint128_t *number2, uint128_t *target) { - UPPER_P(target) = UPPER_P(number1) | UPPER_P(number2); - LOWER_P(target) = LOWER_P(number1) | LOWER_P(number2); -} - -void or256(uint256_t *number1, uint256_t *number2, uint256_t *target) { +void or256(const uint256_t *const number1, + const uint256_t *const number2, + uint256_t *const target) { or128(&UPPER_P(number1), &UPPER_P(number2), &UPPER_P(target)); or128(&LOWER_P(number1), &LOWER_P(number2), &LOWER_P(target)); } -void mul128(uint128_t *number1, uint128_t *number2, uint128_t *target) { - uint64_t top[4] = {UPPER_P(number1) >> 32, - UPPER_P(number1) & 0xffffffff, - LOWER_P(number1) >> 32, - LOWER_P(number1) & 0xffffffff}; - uint64_t bottom[4] = {UPPER_P(number2) >> 32, - UPPER_P(number2) & 0xffffffff, - LOWER_P(number2) >> 32, - LOWER_P(number2) & 0xffffffff}; - uint64_t products[4][4]; - uint128_t tmp, tmp2; - - for (int y = 3; y > -1; y--) { - for (int x = 3; x > -1; x--) { - products[3 - x][y] = top[x] * bottom[y]; - } - } - - uint64_t fourth32 = products[0][3] & 0xffffffff; - uint64_t third32 = (products[0][2] & 0xffffffff) + (products[0][3] >> 32); - uint64_t second32 = (products[0][1] & 0xffffffff) + (products[0][2] >> 32); - uint64_t first32 = (products[0][0] & 0xffffffff) + (products[0][1] >> 32); - - third32 += products[1][3] & 0xffffffff; - second32 += (products[1][2] & 0xffffffff) + (products[1][3] >> 32); - first32 += (products[1][1] & 0xffffffff) + (products[1][2] >> 32); - - second32 += products[2][3] & 0xffffffff; - first32 += (products[2][2] & 0xffffffff) + (products[2][3] >> 32); - - first32 += products[3][3] & 0xffffffff; - - UPPER(tmp) = first32 << 32; - LOWER(tmp) = 0; - UPPER(tmp2) = third32 >> 32; - LOWER(tmp2) = third32 << 32; - add128(&tmp, &tmp2, target); - UPPER(tmp) = second32; - LOWER(tmp) = 0; - add128(&tmp, target, &tmp2); - UPPER(tmp) = 0; - LOWER(tmp) = fourth32; - add128(&tmp, &tmp2, target); -} - -void write_u64_be(uint8_t *buffer, uint64_t value) { - buffer[0] = ((value >> 56) & 0xff); - buffer[1] = ((value >> 48) & 0xff); - buffer[2] = ((value >> 40) & 0xff); - buffer[3] = ((value >> 32) & 0xff); - buffer[4] = ((value >> 24) & 0xff); - buffer[5] = ((value >> 16) & 0xff); - buffer[6] = ((value >> 8) & 0xff); - buffer[7] = (value & 0xff); -} - -void read_u64_be(uint8_t *in, uint64_t *out) { - uint8_t *out_ptr = (uint8_t *) out; - *out_ptr++ = in[7]; - *out_ptr++ = in[6]; - *out_ptr++ = in[5]; - *out_ptr++ = in[4]; - *out_ptr++ = in[3]; - *out_ptr++ = in[2]; - *out_ptr++ = in[1]; - *out_ptr = in[0]; -} - -void mul256(uint256_t *number1, uint256_t *number2, uint256_t *target) { +void mul256(const uint256_t *const number1, + const uint256_t *const number2, + uint256_t *const target) { uint8_t num1[INT256_LENGTH], num2[INT256_LENGTH], result[INT256_LENGTH * 2]; memset(&result, 0, sizeof(result)); for (uint8_t i = 0; i < 4; i++) { @@ -359,38 +182,10 @@ void mul256(uint256_t *number1, uint256_t *number2, uint256_t *target) { } } -void divmod128(uint128_t *l, uint128_t *r, uint128_t *retDiv, uint128_t *retMod) { - uint128_t copyd, adder, resDiv, resMod; - uint128_t one; - UPPER(one) = 0; - LOWER(one) = 1; - uint32_t diffBits = bits128(l) - bits128(r); - clear128(&resDiv); - copy128(&resMod, l); - if (gt128(r, l)) { - copy128(retMod, l); - clear128(retDiv); - } else { - shiftl128(r, diffBits, ©d); - shiftl128(&one, diffBits, &adder); - if (gt128(©d, &resMod)) { - shiftr128(©d, 1, ©d); - shiftr128(&adder, 1, &adder); - } - while (gte128(&resMod, r)) { - if (gte128(&resMod, ©d)) { - minus128(&resMod, ©d, &resMod); - or128(&resDiv, &adder, &resDiv); - } - shiftr128(©d, 1, ©d); - shiftr128(&adder, 1, &adder); - } - copy128(retDiv, &resDiv); - copy128(retMod, &resMod); - } -} - -void divmod256(uint256_t *l, uint256_t *r, uint256_t *retDiv, uint256_t *retMod) { +void divmod256(const uint256_t *const l, + const uint256_t *const r, + uint256_t *const retDiv, + uint256_t *const retMod) { uint256_t copyd, adder, resDiv, resMod; uint256_t one; clear256(&one); @@ -411,7 +206,7 @@ void divmod256(uint256_t *l, uint256_t *r, uint256_t *retDiv, uint256_t *retMod) } while (gte256(&resMod, r)) { if (gte256(&resMod, ©d)) { - minus256(&resMod, ©d, &resMod); + sub256(&resMod, ©d, &resMod); or256(&resDiv, &adder, &resDiv); } shiftr256(©d, 1, ©d); @@ -422,41 +217,10 @@ void divmod256(uint256_t *l, uint256_t *r, uint256_t *retDiv, uint256_t *retMod) } } -static void reverseString(char *str, uint32_t length) { - uint32_t i, j; - for (i = 0, j = length - 1; i < j; i++, j--) { - uint8_t c; - c = str[i]; - str[i] = str[j]; - str[j] = c; - } -} - -bool tostring128(uint128_t *number, uint32_t baseParam, char *out, uint32_t outLength) { - uint128_t rDiv; - uint128_t rMod; - uint128_t base; - copy128(&rDiv, number); - clear128(&rMod); - clear128(&base); - LOWER(base) = baseParam; - uint32_t offset = 0; - if ((baseParam < 2) || (baseParam > 16)) { - return false; - } - do { - if (offset > (outLength - 1)) { - return false; - } - divmod128(&rDiv, &base, &rDiv, &rMod); - out[offset++] = HEXDIGITS[(uint8_t) LOWER(rMod)]; - } while (!zero128(&rDiv)); - out[offset] = '\0'; - reverseString(out, offset); - return true; -} - -bool tostring256(uint256_t *number, uint32_t baseParam, char *out, uint32_t outLength) { +bool tostring256(const uint256_t *const number, + uint32_t baseParam, + char *const out, + uint32_t outLength) { uint256_t rDiv; uint256_t rMod; uint256_t base; @@ -485,3 +249,42 @@ bool tostring256(uint256_t *number, uint32_t baseParam, char *out, uint32_t outL reverseString(out, offset); return true; } + +/** + * Format a uint256_t into a string as a signed integer + * + * @param[in] number the number to format + * @param[in] base the radix used in formatting + * @param[out] out the output buffer + * @param[in] out_length the length of the output buffer + * @return whether the formatting was successful or not + */ +bool tostring256_signed(const uint256_t *const number, + uint32_t base, + char *const out, + uint32_t out_length) { + uint256_t max_unsigned_val; + uint256_t max_signed_val; + uint256_t one_val; + uint256_t two_val; + uint256_t tmp; + + // showing negative numbers only really makes sense in base 10 + if (base == 10) { + explicit_bzero(&one_val, sizeof(one_val)); + LOWER(LOWER(one_val)) = 1; + explicit_bzero(&two_val, sizeof(two_val)); + LOWER(LOWER(two_val)) = 2; + + memset(&max_unsigned_val, 0xFF, sizeof(max_unsigned_val)); + divmod256(&max_unsigned_val, &two_val, &max_signed_val, &tmp); + if (gt256(number, &max_signed_val)) // negative value + { + sub256(&max_unsigned_val, number, &tmp); + add256(&tmp, &one_val, &tmp); + out[0] = '-'; + return tostring256(&tmp, base, out + 1, out_length - 1); + } + } + return tostring256(number, base, out, out_length); // positive value +} diff --git a/src_common/uint256.h b/src_common/uint256.h index 5683c3e..97b8cfb 100644 --- a/src_common/uint256.h +++ b/src_common/uint256.h @@ -22,55 +22,40 @@ #include #include -#include "os.h" -#include "cx.h" -#include "ethUstream.h" - -typedef struct uint128_t { - uint64_t elements[2]; -} uint128_t; +#include "uint128.h" typedef struct uint256_t { uint128_t elements[2]; } uint256_t; -#define UPPER_P(x) x->elements[0] -#define LOWER_P(x) x->elements[1] -#define UPPER(x) x.elements[0] -#define LOWER(x) x.elements[1] - -void readu128BE(uint8_t *buffer, uint128_t *target); -void readu256BE(uint8_t *buffer, uint256_t *target); -void write_u64_be(uint8_t *buffer, uint64_t value); -bool zero128(uint128_t *number); -bool zero256(uint256_t *number); -void copy128(uint128_t *target, uint128_t *number); -void copy256(uint256_t *target, uint256_t *number); -void clear128(uint128_t *target); -void clear256(uint256_t *target); -void shiftl128(uint128_t *number, uint32_t value, uint128_t *target); -void shiftr128(uint128_t *number, uint32_t value, uint128_t *target); -void shiftl256(uint256_t *number, uint32_t value, uint256_t *target); -void shiftr256(uint256_t *number, uint32_t value, uint256_t *target); -uint32_t bits128(uint128_t *number); -uint32_t bits256(uint256_t *number); -bool equal128(uint128_t *number1, uint128_t *number2); -bool equal256(uint256_t *number1, uint256_t *number2); -bool gt128(uint128_t *number1, uint128_t *number2); -bool gt256(uint256_t *number1, uint256_t *number2); -bool gte128(uint128_t *number1, uint128_t *number2); -bool gte256(uint256_t *number1, uint256_t *number2); -void add128(uint128_t *number1, uint128_t *number2, uint128_t *target); -void add256(uint256_t *number1, uint256_t *number2, uint256_t *target); -void minus128(uint128_t *number1, uint128_t *number2, uint128_t *target); -void minus256(uint256_t *number1, uint256_t *number2, uint256_t *target); -void or128(uint128_t *number1, uint128_t *number2, uint128_t *target); -void or256(uint256_t *number1, uint256_t *number2, uint256_t *target); -void mul128(uint128_t *number1, uint128_t *number2, uint128_t *target); -void mul256(uint256_t *number1, uint256_t *number2, uint256_t *target); -void divmod128(uint128_t *l, uint128_t *r, uint128_t *div, uint128_t *mod); -void divmod256(uint256_t *l, uint256_t *r, uint256_t *div, uint256_t *mod); -bool tostring128(uint128_t *number, uint32_t base, char *out, uint32_t outLength); -bool tostring256(uint256_t *number, uint32_t base, char *out, uint32_t outLength); +void readu256BE(const uint8_t *const buffer, uint256_t *const target); +bool zero256(const uint256_t *const number); +void copy256(uint256_t *const target, const uint256_t *const number); +void clear256(uint256_t *const target); +void shiftl256(const uint256_t *const number, uint32_t value, uint256_t *const target); +void shiftr256(const uint256_t *const number, uint32_t value, uint256_t *const target); +uint32_t bits256(const uint256_t *const number); +bool equal256(const uint256_t *const number1, const uint256_t *const number2); +bool gt256(const uint256_t *const number1, const uint256_t *const number2); +bool gte256(const uint256_t *const number1, const uint256_t *const number2); +void add256(const uint256_t *const number1, + const uint256_t *const number2, + uint256_t *const target); +void sub256(const uint256_t *const number1, + const uint256_t *const number2, + uint256_t *const target); +void or256(const uint256_t *const number1, const uint256_t *const number2, uint256_t *const target); +void mul256(const uint256_t *const number1, + const uint256_t *const number2, + uint256_t *const target); +void divmod256(const uint256_t *const l, + const uint256_t *const r, + uint256_t *const div, + uint256_t *const mod); +bool tostring256(const uint256_t *const number, uint32_t base, char *const out, uint32_t outLength); +bool tostring256_signed(const uint256_t *const number, + uint32_t base, + char *const out, + uint32_t out_length); #endif // _UINT256_H_ diff --git a/src_common/uint_common.c b/src_common/uint_common.c new file mode 100644 index 0000000..5fe06a7 --- /dev/null +++ b/src_common/uint_common.c @@ -0,0 +1,60 @@ +/******************************************************************************* + * Ledger Ethereum App + * (c) 2016-2019 Ledger + * + * 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. + ********************************************************************************/ + +// Adapted from https://github.com/calccrypto/uint256_t + +#include "uint_common.h" + +void write_u64_be(uint8_t *const buffer, uint64_t value) { + buffer[0] = ((value >> 56) & 0xff); + buffer[1] = ((value >> 48) & 0xff); + buffer[2] = ((value >> 40) & 0xff); + buffer[3] = ((value >> 32) & 0xff); + buffer[4] = ((value >> 24) & 0xff); + buffer[5] = ((value >> 16) & 0xff); + buffer[6] = ((value >> 8) & 0xff); + buffer[7] = (value & 0xff); +} + +void read_u64_be(const uint8_t *const in, uint64_t *const out) { + uint8_t *out_ptr = (uint8_t *) out; + *out_ptr++ = in[7]; + *out_ptr++ = in[6]; + *out_ptr++ = in[5]; + *out_ptr++ = in[4]; + *out_ptr++ = in[3]; + *out_ptr++ = in[2]; + *out_ptr++ = in[1]; + *out_ptr = in[0]; +} + +uint64_t readUint64BE(const uint8_t *const buffer) { + return (((uint64_t) buffer[0]) << 56) | (((uint64_t) buffer[1]) << 48) | + (((uint64_t) buffer[2]) << 40) | (((uint64_t) buffer[3]) << 32) | + (((uint64_t) buffer[4]) << 24) | (((uint64_t) buffer[5]) << 16) | + (((uint64_t) buffer[6]) << 8) | (((uint64_t) buffer[7])); +} + +void reverseString(char *const str, uint32_t length) { + uint32_t i, j; + for (i = 0, j = length - 1; i < j; i++, j--) { + char c; + c = str[i]; + str[i] = str[j]; + str[j] = c; + } +} diff --git a/src_common/uint_common.h b/src_common/uint_common.h new file mode 100644 index 0000000..c12d9e8 --- /dev/null +++ b/src_common/uint_common.h @@ -0,0 +1,35 @@ +/******************************************************************************* + * Ledger Ethereum App + * (c) 2016-2019 Ledger + * + * 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. + ********************************************************************************/ + +// Adapted from https://github.com/calccrypto/uint256_t + +#ifndef _UINT_COMMON_H_ +#define _UINT_COMMON_H_ + +#include + +#define UPPER_P(x) x->elements[0] +#define LOWER_P(x) x->elements[1] +#define UPPER(x) x.elements[0] +#define LOWER(x) x.elements[1] + +void write_u64_be(uint8_t *const buffer, uint64_t value); +void read_u64_be(const uint8_t *const in, uint64_t *const out); +uint64_t readUint64BE(const uint8_t *const buffer); +void reverseString(char *const str, uint32_t length); + +#endif //_UINT_COMMON_H_ diff --git a/src_features/getAppConfiguration/cmd_getAppConfiguration.c b/src_features/getAppConfiguration/cmd_getAppConfiguration.c index cac5544..39489b7 100644 --- a/src_features/getAppConfiguration/cmd_getAppConfiguration.c +++ b/src_features/getAppConfiguration/cmd_getAppConfiguration.c @@ -4,7 +4,7 @@ void handleGetAppConfiguration(uint8_t p1, uint8_t p2, const uint8_t *workBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { UNUSED(p1); diff --git a/src_features/getEth2PublicKey/cmd_getEth2PublicKey.c b/src_features/getEth2PublicKey/cmd_getEth2PublicKey.c index b88dada..853d834 100644 --- a/src_features/getEth2PublicKey/cmd_getEth2PublicKey.c +++ b/src_features/getEth2PublicKey/cmd_getEth2PublicKey.c @@ -44,7 +44,7 @@ void getEth2PublicKey(uint32_t *bip32Path, uint8_t bip32PathLength, uint8_t *out void handleGetEth2PublicKey(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { bip32_path_t bip32; diff --git a/src_features/getPublicKey/cmd_getPublicKey.c b/src_features/getPublicKey/cmd_getPublicKey.c index 3c25640..07bab1d 100644 --- a/src_features/getPublicKey/cmd_getPublicKey.c +++ b/src_features/getPublicKey/cmd_getPublicKey.c @@ -9,7 +9,7 @@ void handleGetPublicKey(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { uint8_t privateKeyData[INT256_LENGTH]; diff --git a/src_features/performPrivacyOperation/cmd_performPrivacyOperation.c b/src_features/performPrivacyOperation/cmd_performPrivacyOperation.c index da9d986..5cb9552 100644 --- a/src_features/performPrivacyOperation/cmd_performPrivacyOperation.c +++ b/src_features/performPrivacyOperation/cmd_performPrivacyOperation.c @@ -26,7 +26,7 @@ void decodeScalar(const uint8_t *scalarIn, uint8_t *scalarOut) { void handlePerformPrivacyOperation(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { uint8_t privateKeyData[INT256_LENGTH]; diff --git a/src_features/provideErc20TokenInformation/cmd_provideTokenInfo.c b/src_features/provideErc20TokenInformation/cmd_provideTokenInfo.c index dc23117..1340966 100644 --- a/src_features/provideErc20TokenInformation/cmd_provideTokenInfo.c +++ b/src_features/provideErc20TokenInformation/cmd_provideTokenInfo.c @@ -8,8 +8,8 @@ void handleProvideErc20TokenInformation(uint8_t p1, uint8_t p2, - uint8_t *workBuffer, - uint16_t dataLength, + const uint8_t *workBuffer, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { UNUSED(p1); @@ -103,12 +103,13 @@ void handleProvideErc20TokenInformation(uint8_t p1, void handleProvideErc20TokenInformation(uint8_t p1, uint8_t p2, const uint8_t *workBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, - __attribute__((unused)) unsigned int *tx) { + unsigned int *tx) { UNUSED(p1); UNUSED(p2); UNUSED(flags); + UNUSED(tx); uint32_t offset = 0; uint8_t tickerLength; uint32_t chainId; diff --git a/src_features/provideNFTInformation/cmd_provideNFTInfo.c b/src_features/provideNFTInformation/cmd_provideNFTInfo.c index 4469061..3030095 100644 --- a/src_features/provideNFTInformation/cmd_provideNFTInfo.c +++ b/src_features/provideNFTInformation/cmd_provideNFTInfo.c @@ -55,7 +55,7 @@ typedef bool verificationAlgo(const cx_ecfp_public_key_t *, void handleProvideNFTInformation(uint8_t p1, uint8_t p2, const uint8_t *workBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { UNUSED(p1); diff --git a/src_features/setExternalPlugin/cmd_setExternalPlugin.c b/src_features/setExternalPlugin/cmd_setExternalPlugin.c index 20506f9..4a94356 100644 --- a/src_features/setExternalPlugin/cmd_setExternalPlugin.c +++ b/src_features/setExternalPlugin/cmd_setExternalPlugin.c @@ -9,7 +9,7 @@ void handleSetExternalPlugin(uint8_t p1, uint8_t p2, const uint8_t *workBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { UNUSED(p1); @@ -91,4 +91,4 @@ void handleSetExternalPlugin(uint8_t p1, G_io_apdu_buffer[(*tx)++] = 0x90; G_io_apdu_buffer[(*tx)++] = 0x00; -} \ No newline at end of file +} diff --git a/src_features/setPlugin/cmd_setPlugin.c b/src_features/setPlugin/cmd_setPlugin.c index 3cc0c49..8cdb806 100644 --- a/src_features/setPlugin/cmd_setPlugin.c +++ b/src_features/setPlugin/cmd_setPlugin.c @@ -88,7 +88,7 @@ static pluginType_t getPluginType(char *pluginName, uint8_t pluginNameLength) { void handleSetPlugin(uint8_t p1, uint8_t p2, const uint8_t *workBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { UNUSED(p1); diff --git a/src_features/signMessage/cmd_signMessage.c b/src_features/signMessage/cmd_signMessage.c index 9cfd4d6..1779142 100644 --- a/src_features/signMessage/cmd_signMessage.c +++ b/src_features/signMessage/cmd_signMessage.c @@ -88,7 +88,7 @@ static void reset_ui_buffer(void) { * @param[in] length the payload size * @return pointer to the start of the start of the message; \ref NULL if it failed */ -static const uint8_t *first_apdu_data(const uint8_t *data, uint16_t *length) { +static const uint8_t *first_apdu_data(const uint8_t *data, uint8_t *length) { if (appState != APP_STATE_IDLE) { reset_app_context(); } @@ -212,12 +212,11 @@ bool handleSignPersonalMessage(uint8_t p1, const uint8_t *const payload, uint8_t length) { const uint8_t *data = payload; - uint16_t u16_length = length; (void) p2; processed_size = 0; if (p1 == P1_FIRST) { - if ((data = first_apdu_data(data, &u16_length)) == NULL) { + if ((data = first_apdu_data(data, &length)) == NULL) { return false; } processed_size = data - payload; @@ -227,7 +226,7 @@ bool handleSignPersonalMessage(uint8_t p1, return false; } - if (!feed_hash(data, u16_length)) { + if (!feed_hash(data, length)) { return false; } diff --git a/src_features/signMessageEIP712/cmd_signMessage712.c b/src_features/signMessageEIP712/cmd_signMessage712.c deleted file mode 100644 index 847c40a..0000000 --- a/src_features/signMessageEIP712/cmd_signMessage712.c +++ /dev/null @@ -1,36 +0,0 @@ -#include "shared_context.h" -#include "apdu_constants.h" -#include "utils.h" -#include "common_ui.h" - -void handleSignEIP712Message(uint8_t p1, - uint8_t p2, - const uint8_t *workBuffer, - uint16_t dataLength, - unsigned int *flags, - unsigned int *tx) { - UNUSED(tx); - if ((p1 != 00) || (p2 != 00)) { - THROW(0x6B00); - } - if (appState != APP_STATE_IDLE) { - reset_app_context(); - } - - workBuffer = parseBip32(workBuffer, &dataLength, &tmpCtx.messageSigningContext.bip32); - - if (workBuffer == NULL || dataLength < 32 + 32) { - THROW(0x6a80); - } - - memmove(tmpCtx.messageSigningContext712.domainHash, workBuffer, 32); - memmove(tmpCtx.messageSigningContext712.messageHash, workBuffer + 32, 32); - -#ifdef NO_CONSENT - io_seproxyhal_touch_signMessage_ok(); -#else // NO_CONSENT - ui_sign_712_v0(); -#endif // NO_CONSENT - - *flags |= IO_ASYNCH_REPLY; -} diff --git a/src_features/signMessageEIP712/commands_712.c b/src_features/signMessageEIP712/commands_712.c new file mode 100644 index 0000000..7810b93 --- /dev/null +++ b/src_features/signMessageEIP712/commands_712.c @@ -0,0 +1,216 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include +#include +#include "commands_712.h" +#include "apdu_constants.h" // APDU response codes +#include "context_712.h" +#include "field_hash.h" +#include "path.h" +#include "ui_logic.h" +#include "typed_data.h" +#include "schema_hash.h" +#include "filtering.h" +#include "common_712.h" +#include "ethUtils.h" // allzeroes + +/** + * Send the response to the previous APDU command + * + * In case of an error it uses the global variable to retrieve the error code and resets + * the app context + * + * @param[in] success whether the command was successful + */ +void handle_eip712_return_code(bool success) { + if (success) { + apdu_response_code = APDU_RESPONSE_OK; + } else if (apdu_response_code == APDU_RESPONSE_OK) { // somehow not set + apdu_response_code = APDU_RESPONSE_ERROR_NO_INFO; + } + + G_io_apdu_buffer[0] = (apdu_response_code >> 8) & 0xff; + G_io_apdu_buffer[1] = apdu_response_code & 0xff; + + // Send back the response, do not restart the event loop + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 2); + + if (!success) { + eip712_context_deinit(); + } +} + +/** + * Process the EIP712 struct definition command + * + * @param[in] apdu_buf the APDU payload + * @return whether the command was successful or not + */ +bool handle_eip712_struct_def(const uint8_t *const apdu_buf) { + bool ret = true; + + if (eip712_context == NULL) { + ret = eip712_context_init(); + } + + if (struct_state == DEFINED) { + ret = false; + } + + if (ret) { + switch (apdu_buf[OFFSET_P2]) { + case P2_DEF_NAME: + ret = set_struct_name(apdu_buf[OFFSET_LC], &apdu_buf[OFFSET_CDATA]); + break; + case P2_DEF_FIELD: + ret = set_struct_field(apdu_buf[OFFSET_LC], &apdu_buf[OFFSET_CDATA]); + break; + default: + PRINTF("Unknown P2 0x%x for APDU 0x%x\n", + apdu_buf[OFFSET_P2], + apdu_buf[OFFSET_INS]); + apdu_response_code = APDU_RESPONSE_INVALID_P1_P2; + ret = false; + } + } + handle_eip712_return_code(ret); + return ret; +} + +/** + * Process the EIP712 struct implementation command + * + * @param[in] apdu_buf the APDU payload + * @return whether the command was successful or not + */ +bool handle_eip712_struct_impl(const uint8_t *const apdu_buf) { + bool ret = false; + bool reply_apdu = true; + + if (eip712_context == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + } else { + switch (apdu_buf[OFFSET_P2]) { + case P2_IMPL_NAME: + // set root type + ret = path_set_root((char *) &apdu_buf[OFFSET_CDATA], apdu_buf[OFFSET_LC]); + if (ret) { + if (N_storage.verbose_eip712) { + ui_712_review_struct(path_get_root()); + reply_apdu = false; + } + ui_712_field_flags_reset(); + } + break; + case P2_IMPL_FIELD: + if ((ret = field_hash(&apdu_buf[OFFSET_CDATA], + apdu_buf[OFFSET_LC], + apdu_buf[OFFSET_P1] != P1_COMPLETE))) { + reply_apdu = false; + } + break; + case P2_IMPL_ARRAY: + ret = path_new_array_depth(&apdu_buf[OFFSET_CDATA], apdu_buf[OFFSET_LC]); + break; + default: + PRINTF("Unknown P2 0x%x for APDU 0x%x\n", + apdu_buf[OFFSET_P2], + apdu_buf[OFFSET_INS]); + apdu_response_code = APDU_RESPONSE_INVALID_P1_P2; + } + } + if (reply_apdu) { + handle_eip712_return_code(ret); + } + return ret; +} + +/** + * Process the EIP712 filtering command + * + * @param[in] apdu_buf the APDU payload + * @return whether the command was successful or not + */ +bool handle_eip712_filtering(const uint8_t *const apdu_buf) { + bool ret = true; + bool reply_apdu = true; + e_filtering_type type; + + if (eip712_context == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + ret = false; + } else { + switch (apdu_buf[OFFSET_P2]) { + case P2_FILT_ACTIVATE: + if (!N_storage.verbose_eip712) { + ui_712_set_filtering_mode(EIP712_FILTERING_FULL); + ret = compute_schema_hash(); + } + break; + case P2_FILT_MESSAGE_INFO: + case P2_FILT_SHOW_FIELD: + type = (apdu_buf[OFFSET_P2] == P2_FILT_MESSAGE_INFO) + ? FILTERING_PROVIDE_MESSAGE_INFO + : FILTERING_SHOW_FIELD; + if (ui_712_get_filtering_mode() == EIP712_FILTERING_FULL) { + ret = + provide_filtering_info(&apdu_buf[OFFSET_CDATA], apdu_buf[OFFSET_LC], type); + if ((apdu_buf[OFFSET_P2] == P2_FILT_MESSAGE_INFO) && ret) { + reply_apdu = false; + } + } + break; + default: + PRINTF("Unknown P2 0x%x for APDU 0x%x\n", + apdu_buf[OFFSET_P2], + apdu_buf[OFFSET_INS]); + apdu_response_code = APDU_RESPONSE_INVALID_P1_P2; + ret = false; + } + } + if (reply_apdu) { + handle_eip712_return_code(ret); + } + return ret; +} + +/** + * Process the EIP712 sign command + * + * @param[in] apdu_buf the APDU payload + * @return whether the command was successful or not + */ +bool handle_eip712_sign(const uint8_t *const apdu_buf) { + bool ret = false; + uint8_t length = apdu_buf[OFFSET_LC]; + + if (eip712_context == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + } + // if the final hashes are still zero or if there are some unimplemented fields + else if (allzeroes(tmpCtx.messageSigningContext712.domainHash, + sizeof(tmpCtx.messageSigningContext712.domainHash)) || + allzeroes(tmpCtx.messageSigningContext712.messageHash, + sizeof(tmpCtx.messageSigningContext712.messageHash)) || + (path_get_field() != NULL)) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + } else if ((ui_712_get_filtering_mode() == EIP712_FILTERING_FULL) && + (ui_712_remaining_filters() != 0)) { + PRINTF("%d EIP712 filters are missing\n", ui_712_remaining_filters()); + apdu_response_code = APDU_RESPONSE_REF_DATA_NOT_FOUND; + } else if (parseBip32(&apdu_buf[OFFSET_CDATA], &length, &tmpCtx.messageSigningContext.bip32) != + NULL) { + if (!N_storage.verbose_eip712 && (ui_712_get_filtering_mode() == EIP712_FILTERING_BASIC)) { + ui_712_message_hash(); + } + ret = true; + ui_712_end_sign(); + } + if (!ret) { + handle_eip712_return_code(ret); + } + return ret; +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/commands_712.h b/src_features/signMessageEIP712/commands_712.h new file mode 100644 index 0000000..e97d77b --- /dev/null +++ b/src_features/signMessageEIP712/commands_712.h @@ -0,0 +1,33 @@ +#ifndef EIP712_H_ +#define EIP712_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include + +// APDUs P1 +#define P1_COMPLETE 0x00 +#define P1_PARTIAL 0xFF + +// APDUs P2 +#define P2_DEF_NAME 0x00 +#define P2_DEF_FIELD 0xFF +#define P2_IMPL_NAME P2_DEF_NAME +#define P2_IMPL_ARRAY 0x0F +#define P2_IMPL_FIELD P2_DEF_FIELD +#define P2_FILT_ACTIVATE 0x00 +#define P2_FILT_MESSAGE_INFO 0x0F +#define P2_FILT_SHOW_FIELD 0xFF + +#define DOMAIN_STRUCT_NAME "EIP712Domain" + +bool handle_eip712_struct_def(const uint8_t *const apdu_buf); +bool handle_eip712_struct_impl(const uint8_t *const apdu_buf); +bool handle_eip712_sign(const uint8_t *const apdu_buf); +bool handle_eip712_filtering(const uint8_t *const apdu_buf); +void handle_eip712_return_code(bool success); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // EIP712_H_ diff --git a/src_features/signMessageEIP712/context_712.c b/src_features/signMessageEIP712/context_712.c new file mode 100644 index 0000000..82abd09 --- /dev/null +++ b/src_features/signMessageEIP712/context_712.c @@ -0,0 +1,78 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include +#include "context_712.h" +#include "mem.h" +#include "mem_utils.h" +#include "sol_typenames.h" +#include "path.h" +#include "field_hash.h" +#include "ui_logic.h" +#include "typed_data.h" +#include "apdu_constants.h" // APDU response codes +#include "shared_context.h" // reset_app_context +#include "common_ui.h" // ui_idle + +e_struct_init struct_state = NOT_INITIALIZED; +s_eip712_context *eip712_context = NULL; + +/** + * Initialize the EIP712 context + * + * @return a boolean indicating if the initialization was successful or not + */ +bool eip712_context_init(void) { + // init global variables + mem_init(); + + if ((eip712_context = MEM_ALLOC_AND_ALIGN_TYPE(*eip712_context)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + + if (sol_typenames_init() == false) { + return false; + } + + if (path_init() == false) { + return false; + } + + if (field_hash_init() == false) { + return false; + } + + if (ui_712_init() == false) { + return false; + } + + if (typed_data_init() == false) // this needs to be initialized last ! + { + return false; + } + + // Since they are optional, they might not be provided by the JSON data + explicit_bzero(eip712_context->contract_addr, sizeof(eip712_context->contract_addr)); + eip712_context->chain_id = 0; + + struct_state = NOT_INITIALIZED; + + return true; +} + +/** + * De-initialize the EIP712 context + */ +void eip712_context_deinit(void) { + typed_data_deinit(); + path_deinit(); + field_hash_deinit(); + ui_712_deinit(); + mem_reset(); + eip712_context = NULL; + reset_app_context(); + ui_idle(); +} + +#endif diff --git a/src_features/signMessageEIP712/context_712.h b/src_features/signMessageEIP712/context_712.h new file mode 100644 index 0000000..2ae5c8d --- /dev/null +++ b/src_features/signMessageEIP712/context_712.h @@ -0,0 +1,25 @@ +#ifndef EIP712_CTX_H_ +#define EIP712_CTX_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include "ethUstream.h" // ADDRESS_LENGTH + +typedef struct { + uint8_t contract_addr[ADDRESS_LENGTH]; + uint64_t chain_id; + uint8_t schema_hash[224 / 8]; +} s_eip712_context; + +extern s_eip712_context *eip712_context; + +bool eip712_context_init(void); +void eip712_context_deinit(void); + +typedef enum { NOT_INITIALIZED, INITIALIZED, DEFINED } e_struct_init; +extern e_struct_init struct_state; + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // EIP712_CTX_H_ diff --git a/src_features/signMessageEIP712/encode_field.c b/src_features/signMessageEIP712/encode_field.c new file mode 100644 index 0000000..2c3339d --- /dev/null +++ b/src_features/signMessageEIP712/encode_field.c @@ -0,0 +1,136 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include +#include "encode_field.h" +#include "mem.h" +#include "shared_context.h" +#include "apdu_constants.h" // APDU response codes + +typedef enum { MSB, LSB } e_padding_type; + +/** + * Encode a field value to 32 bytes (padded) + * + * @param[in] value field value to encode + * @param[in] length field length before encoding + * @param[in] ptype padding direction (LSB vs MSB) + * @param[in] pval value used for padding + * @return encoded field value + */ +static void *field_encode(const uint8_t *const value, + uint8_t length, + e_padding_type ptype, + uint8_t pval) { + uint8_t *padded_value; + uint8_t start_idx; + + if (length > EIP_712_ENCODED_FIELD_LENGTH) // sanity check + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return NULL; + } + if ((padded_value = mem_alloc(EIP_712_ENCODED_FIELD_LENGTH)) != NULL) { + switch (ptype) { + case MSB: + memset(padded_value, pval, EIP_712_ENCODED_FIELD_LENGTH - length); + start_idx = EIP_712_ENCODED_FIELD_LENGTH - length; + break; + case LSB: + explicit_bzero(padded_value + length, EIP_712_ENCODED_FIELD_LENGTH - length); + start_idx = 0; + break; + default: + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; // should not be here + } + memcpy(&padded_value[start_idx], value, length); + } else { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + } + return padded_value; +} + +/** + * Encode an unsigned integer + * + * @param[in] value pointer to the "packed" integer received + * @param[in] length its byte-length + * @return the encoded value + */ +void *encode_uint(const uint8_t *const value, uint8_t length) { + // no length check here since it will be checked by field_encode + return field_encode(value, length, MSB, 0x00); +} + +/** + * Encode a signed integer + * + * @param[in] value pointer to the "packed" integer received + * @param[in] length its byte-length + * @param[in] typesize the type size in bytes + * @return the encoded value + */ +void *encode_int(const uint8_t *const value, uint8_t length, uint8_t typesize) { + uint8_t padding_value; + + if (length < 1) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return NULL; + } + + if ((length == typesize) && (value[0] & (1 << 7))) // negative number + { + padding_value = 0xFF; + } else { + padding_value = 0x00; + } + // no length check here since it will be checked by field_encode + return field_encode(value, length, MSB, padding_value); +} + +/** + * Encode a fixed-size byte array + * + * @param[in] value pointer to the "packed" bytes array + * @param[in] length its byte-length + * @return the encoded value + */ +void *encode_bytes(const uint8_t *const value, uint8_t length) { + // no length check here since it will be checked by field_encode + return field_encode(value, length, LSB, 0x00); +} + +/** + * Encode a boolean + * + * @param[in] value pointer to the boolean received + * @param[in] length its byte-length + * @return the encoded value + */ +void *encode_boolean(const bool *const value, uint8_t length) { + if (length != 1) // sanity check + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return NULL; + } + return encode_uint((uint8_t *) value, length); +} + +/** + * Encode an address + * + * @param[in] value pointer to the address received + * @param[in] length its byte-length + * @return the encoded value + */ +void *encode_address(const uint8_t *const value, uint8_t length) { + if (length != ADDRESS_LENGTH) // sanity check + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return NULL; + } + return encode_uint(value, length); +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/encode_field.h b/src_features/signMessageEIP712/encode_field.h new file mode 100644 index 0000000..f3a684a --- /dev/null +++ b/src_features/signMessageEIP712/encode_field.h @@ -0,0 +1,19 @@ +#ifndef ENCODE_FIELD_H_ +#define ENCODE_FIELD_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include + +#define EIP_712_ENCODED_FIELD_LENGTH 32 + +void *encode_uint(const uint8_t *const value, uint8_t length); +void *encode_int(const uint8_t *const value, uint8_t length, uint8_t typesize); +void *encode_boolean(const bool *const value, uint8_t length); +void *encode_address(const uint8_t *const value, uint8_t length); +void *encode_bytes(const uint8_t *const value, uint8_t length); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // ENCODE_FIELD_H_ diff --git a/src_features/signMessageEIP712/field_hash.c b/src_features/signMessageEIP712/field_hash.c new file mode 100644 index 0000000..cc57dae --- /dev/null +++ b/src_features/signMessageEIP712/field_hash.c @@ -0,0 +1,284 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include "field_hash.h" +#include "encode_field.h" +#include "path.h" +#include "mem.h" +#include "mem_utils.h" +#include "shared_context.h" +#include "ui_logic.h" +#include "ethUtils.h" // KECCAK256_HASH_BYTESIZE +#include "context_712.h" // contract_addr +#include "utils.h" // u64_from_BE +#include "apdu_constants.h" // APDU response codes +#include "typed_data.h" +#include "commands_712.h" +#include "hash_bytes.h" + +static s_field_hashing *fh = NULL; + +/** + * Initialize the field hash context + * + * @return whether the initialization was successful or not + */ +bool field_hash_init(void) { + if (fh == NULL) { + if ((fh = MEM_ALLOC_AND_ALIGN_TYPE(*fh)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + fh->state = FHS_IDLE; + } + return true; +} + +/** + * Deinitialize the field hash context + */ +void field_hash_deinit(void) { + fh = NULL; +} + +/** + * Special handling of the first chunk received from a field value + * + * @param[in] field_ptr pointer to the struct field definition + * @param[in] data the field value + * @param[in,out] data_length the value length + * @return the data pointer + */ +static const uint8_t *field_hash_prepare(const void *const field_ptr, + const uint8_t *data, + uint8_t *data_length) { + e_type field_type; + + field_type = struct_field_type(field_ptr); + fh->remaining_size = __builtin_bswap16(*(uint16_t *) &data[0]); // network byte order + data += sizeof(uint16_t); + *data_length -= sizeof(uint16_t); + fh->state = FHS_WAITING_FOR_MORE; + if (IS_DYN(field_type)) { + cx_keccak_init(&global_sha3, 256); // init hash + ui_712_new_field(field_ptr, data, *data_length); + } + return data; +} + +/** + * Finalize static field hash + * + * Encode the field data depending on its type + * + * @param[in] field_ptr pointer to the struct field definition + * @param[in] data the field value + * @param[in] data_length the value length + * @return pointer to the encoded value + */ +static const uint8_t *field_hash_finalize_static(const void *const field_ptr, + const uint8_t *const data, + uint8_t data_length) { + uint8_t *value = NULL; + e_type field_type; + + field_type = struct_field_type(field_ptr); + switch (field_type) { + case TYPE_SOL_INT: + value = encode_int(data, data_length, get_struct_field_typesize(field_ptr)); + break; + case TYPE_SOL_UINT: + value = encode_uint(data, data_length); + break; + case TYPE_SOL_BYTES_FIX: + value = encode_bytes(data, data_length); + break; + case TYPE_SOL_ADDRESS: + value = encode_address(data, data_length); + break; + case TYPE_SOL_BOOL: + value = encode_boolean((bool *) data, data_length); + break; + case TYPE_CUSTOM: + default: + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + PRINTF("Unknown solidity type!\n"); + } + + if (value == NULL) { + return NULL; + } + ui_712_new_field(field_ptr, data, data_length); + return value; +} + +/** + * Finalize dynamic field hash + * + * Allocate and hash the data + * + * @return pointer to the hash, \ref NULL if it failed + */ +static uint8_t *field_hash_finalize_dynamic(void) { + uint8_t *value; + + if ((value = mem_alloc(KECCAK256_HASH_BYTESIZE)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return NULL; + } + // copy hash into memory + cx_hash((cx_hash_t *) &global_sha3, CX_LAST, NULL, 0, value, KECCAK256_HASH_BYTESIZE); + return value; +} + +/** + * Feed the newly created field hash into the parent struct's progressive hash + * + * @param[in] field_type the struct field's type + * @param[in] hash the field hash + */ +static void field_hash_feed_parent(e_type field_type, const uint8_t *const hash) { + uint8_t len; + + if (IS_DYN(field_type)) { + len = KECCAK256_HASH_BYTESIZE; + } else { + len = EIP_712_ENCODED_FIELD_LENGTH; + } + + // last thing in mem is the hash of the previous field + // and just before it is the current hash context + cx_sha3_t *hash_ctx = (cx_sha3_t *) (hash - sizeof(cx_sha3_t)); + // continue the progressive hash on it + hash_nbytes(hash, len, (cx_hash_t *) hash_ctx); + // deallocate it + mem_dealloc(len); +} + +/** + * Special domain fields handling + * + * Do something special for certain EIP712Domain fields + * + * @param[in] field_ptr pointer to the struct field definition + * @param[in] data the field value + * @param[in] data_length the value length + * @return whether an error occured or not + */ +static bool field_hash_domain_special_fields(const void *const field_ptr, + const uint8_t *const data, + uint8_t data_length) { + const char *key; + uint8_t keylen; + + key = get_struct_field_keyname(field_ptr, &keylen); + // copy contract address into context + if (strncmp(key, "verifyingContract", keylen) == 0) { + if (data_length != sizeof(eip712_context->contract_addr)) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + PRINTF("Unexpected verifyingContract length!\n"); + return false; + } + memcpy(eip712_context->contract_addr, data, data_length); + } else if (strncmp(key, "chainId", keylen) == 0) { + eip712_context->chain_id = u64_from_BE(data, data_length); + } + return true; +} + +/** + * Finalize the data hashing + * + * @param[in] field_ptr pointer to the struct field definition + * @param[in] data the field value + * @param[in] data_length the value length + * @return whether an error occured or not + */ +static bool field_hash_finalize(const void *const field_ptr, + const uint8_t *const data, + uint8_t data_length) { + const uint8_t *value = NULL; + e_type field_type; + + field_type = struct_field_type(field_ptr); + if (!IS_DYN(field_type)) { + if ((value = field_hash_finalize_static(field_ptr, data, data_length)) == NULL) { + return false; + } + } else { + if ((value = field_hash_finalize_dynamic()) == NULL) { + return false; + } + } + + field_hash_feed_parent(field_type, value); + + if (path_get_root_type() == ROOT_DOMAIN) { + if (field_hash_domain_special_fields(field_ptr, data, data_length) == false) { + return false; + } + } + path_advance(); + fh->state = FHS_IDLE; + ui_712_finalize_field(); + return true; +} + +/** + * Hash a field value + * + * @param[in] data the field value + * @param[in] data_length the value length + * @param[in] partial whether there is more of that data coming later or not + * @return whether the data hashing was successful or not + */ +bool field_hash(const uint8_t *data, uint8_t data_length, bool partial) { + const void *field_ptr; + e_type field_type; + + if ((fh == NULL) || ((field_ptr = path_get_field()) == NULL)) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + field_type = struct_field_type(field_ptr); + if (fh->state == FHS_IDLE) // first packet for this frame + { + if (data_length < 2) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + + data = field_hash_prepare(field_ptr, data, &data_length); + } + if (data_length > fh->remaining_size) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + fh->remaining_size -= data_length; + // if a dynamic type -> continue progressive hash + if (IS_DYN(field_type)) { + hash_nbytes(data, data_length, (cx_hash_t *) &global_sha3); + } + if (fh->remaining_size == 0) { + if (partial) // only makes sense if marked as complete + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if (field_hash_finalize(field_ptr, data, data_length) == false) { + return false; + } + } else { + if (!partial || !IS_DYN(field_type)) // only makes sense if marked as partial + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + handle_eip712_return_code(true); + } + + return true; +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/field_hash.h b/src_features/signMessageEIP712/field_hash.h new file mode 100644 index 0000000..448dbd8 --- /dev/null +++ b/src_features/signMessageEIP712/field_hash.h @@ -0,0 +1,24 @@ +#ifndef FIELD_HASH_H_ +#define FIELD_HASH_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include + +#define IS_DYN(type) (((type) == TYPE_SOL_STRING) || ((type) == TYPE_SOL_BYTES_DYN)) + +typedef enum { FHS_IDLE, FHS_WAITING_FOR_MORE } e_field_hashing_state; + +typedef struct { + uint16_t remaining_size; + uint8_t state; // e_field_hashing_state +} s_field_hashing; + +bool field_hash_init(void); +void field_hash_deinit(void); +bool field_hash(const uint8_t *data, uint8_t data_length, bool partial); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // FIELD_HASH_H_ diff --git a/src_features/signMessageEIP712/filtering.c b/src_features/signMessageEIP712/filtering.c new file mode 100644 index 0000000..1ba40dc --- /dev/null +++ b/src_features/signMessageEIP712/filtering.c @@ -0,0 +1,185 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include "filtering.h" +#include "hash_bytes.h" +#include "ethUstream.h" // INT256_LENGTH +#include "apdu_constants.h" // APDU return codes +#include "context_712.h" +#include "commands_712.h" +#include "typed_data.h" +#include "path.h" +#include "ui_logic.h" + +/** + * Reconstruct the field path and hash it + * + * @param[in] hash_ctx the hashing context + */ +static void hash_filtering_path(cx_hash_t *const hash_ctx) { + const void *field_ptr; + const char *key; + uint8_t key_len; + + for (uint8_t i = 0; i < path_get_depth_count(); ++i) { + if (i > 0) { + hash_byte('.', hash_ctx); + } + if ((field_ptr = path_get_nth_field(i + 1)) != NULL) { + if ((key = get_struct_field_keyname(field_ptr, &key_len)) != NULL) { + // field name + hash_nbytes((uint8_t *) key, key_len, hash_ctx); + + // array levels + if (struct_field_is_array(field_ptr)) { + uint8_t lvl_count; + + get_struct_field_array_lvls_array(field_ptr, &lvl_count); + for (int j = 0; j < lvl_count; ++j) { + hash_nbytes((uint8_t *) ".[]", 3, hash_ctx); + } + } + } + } + } +} + +/** + * Verify the provided signature + * + * @param[in] dname_length length of provided substitution name + * @param[in] dname provided substitution name + * @param[in] sig_length provided signature length + * @param[in] sig pointer to the provided signature + * @param[in] type the type of filtering + * @return whether the signature verification worked or not + */ +static bool verify_filtering_signature(uint8_t dname_length, + const char *const dname, + uint8_t sig_length, + const uint8_t *const sig, + e_filtering_type type) { + uint8_t hash[INT256_LENGTH]; + cx_ecfp_public_key_t verifying_key; + cx_sha256_t hash_ctx; + uint64_t chain_id; + + cx_sha256_init(&hash_ctx); + + // Magic number, makes it so a signature of one type can't be used as another + switch (type) { + case FILTERING_SHOW_FIELD: + hash_byte(FILTERING_MAGIC_STRUCT_FIELD, (cx_hash_t *) &hash_ctx); + break; + case FILTERING_PROVIDE_MESSAGE_INFO: + hash_byte(FILTERING_MAGIC_CONTRACT_NAME, (cx_hash_t *) &hash_ctx); + break; + default: + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + PRINTF("Invalid filtering type when verifying signature!\n"); + return false; + } + + // Chain ID + chain_id = __builtin_bswap64(eip712_context->chain_id); + hash_nbytes((uint8_t *) &chain_id, sizeof(chain_id), (cx_hash_t *) &hash_ctx); + + // Contract address + hash_nbytes(eip712_context->contract_addr, + sizeof(eip712_context->contract_addr), + (cx_hash_t *) &hash_ctx); + + // Schema hash + hash_nbytes(eip712_context->schema_hash, + sizeof(eip712_context->schema_hash), + (cx_hash_t *) &hash_ctx); + + if (type == FILTERING_SHOW_FIELD) { + hash_filtering_path((cx_hash_t *) &hash_ctx); + } else // FILTERING_PROVIDE_MESSAGE_INFO + { + hash_byte(ui_712_remaining_filters(), (cx_hash_t *) &hash_ctx); + } + + // Display name + hash_nbytes((uint8_t *) dname, sizeof(char) * dname_length, (cx_hash_t *) &hash_ctx); + + // Finalize hash + cx_hash((cx_hash_t *) &hash_ctx, CX_LAST, NULL, 0, hash, INT256_LENGTH); + + cx_ecfp_init_public_key(CX_CURVE_256K1, + LEDGER_SIGNATURE_PUBLIC_KEY, + sizeof(LEDGER_SIGNATURE_PUBLIC_KEY), + &verifying_key); + if (!cx_ecdsa_verify(&verifying_key, CX_LAST, CX_SHA256, hash, sizeof(hash), sig, sig_length)) { +#ifndef HAVE_BYPASS_SIGNATURES + PRINTF("Invalid EIP-712 filtering signature\n"); + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; +#endif + } + return true; +} + +/** + * Provide filtering information about upcoming struct field + * + * @param[in] payload the raw data received + * @param[in] length payload length + * @param[in] type the type of filtering + * @return if everything went well or not + */ +bool provide_filtering_info(const uint8_t *const payload, uint8_t length, e_filtering_type type) { + bool ret = false; + uint8_t dname_len; + const char *dname; + uint8_t sig_len; + const uint8_t *sig; + uint8_t offset = 0; + + if (type == FILTERING_PROVIDE_MESSAGE_INFO) { + if (path_get_root_type() != ROOT_DOMAIN) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + } else // FILTERING_SHOW_FIELD + { + if (path_get_root_type() != ROOT_MESSAGE) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + } + if (length > 0) { + dname_len = payload[offset++]; + if ((1 + dname_len) < length) { + dname = (char *) &payload[offset]; + offset += dname_len; + if (type == FILTERING_PROVIDE_MESSAGE_INFO) { + ui_712_set_filters_count(payload[offset++]); + } + sig_len = payload[offset++]; + sig = &payload[offset]; + offset += sig_len; + if ((sig_len > 0) && (offset == length)) { + if ((ret = verify_filtering_signature(dname_len, dname, sig_len, sig, type))) { + if (type == FILTERING_PROVIDE_MESSAGE_INFO) { + if (!N_storage.verbose_eip712) { + ui_712_set_title("Contract", 8); + ui_712_set_value(dname, dname_len); + ui_712_redraw_generic_step(); + } + } else // FILTERING_SHOW_FIELD + { + if (dname_len > 0) // don't substitute for an empty name + { + ui_712_set_title(dname, dname_len); + } + ui_712_flag_field(true, dname_len > 0); + } + } + } + } + } + return ret; +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/filtering.h b/src_features/signMessageEIP712/filtering.h new file mode 100644 index 0000000..3e33b9a --- /dev/null +++ b/src_features/signMessageEIP712/filtering.h @@ -0,0 +1,18 @@ +#ifndef FILTERING_H_ +#define FILTERING_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include + +#define FILTERING_MAGIC_CONTRACT_NAME 0b10110111 // 183 +#define FILTERING_MAGIC_STRUCT_FIELD 0b01001000 // ~183 = 72 + +typedef enum { FILTERING_PROVIDE_MESSAGE_INFO, FILTERING_SHOW_FIELD } e_filtering_type; + +bool provide_filtering_info(const uint8_t *const payload, uint8_t length, e_filtering_type type); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // FILTERING_H_ diff --git a/src_features/signMessageEIP712/format_hash_field_type.c b/src_features/signMessageEIP712/format_hash_field_type.c new file mode 100644 index 0000000..1760c7d --- /dev/null +++ b/src_features/signMessageEIP712/format_hash_field_type.c @@ -0,0 +1,117 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include "format_hash_field_type.h" +#include "mem.h" +#include "mem_utils.h" +#include "commands_712.h" +#include "hash_bytes.h" +#include "apdu_constants.h" // APDU response codes +#include "typed_data.h" + +/** + * Format & hash a struct field typesize + * + * @param[in] field_ptr pointer to the struct field + * @param[in] hash_ctx pointer to the hashing context + * @return whether the formatting & hashing were successful or not + */ +static bool format_hash_field_type_size(const void *const field_ptr, cx_hash_t *hash_ctx) { + uint16_t field_size; + char *uint_str_ptr; + uint8_t uint_str_len; + + field_size = get_struct_field_typesize(field_ptr); + switch (struct_field_type(field_ptr)) { + case TYPE_SOL_INT: + case TYPE_SOL_UINT: + field_size *= 8; // bytes -> bits + break; + case TYPE_SOL_BYTES_FIX: + break; + default: + // should not be in here :^) + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + uint_str_ptr = mem_alloc_and_format_uint(field_size, &uint_str_len); + if (uint_str_ptr == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + hash_nbytes((uint8_t *) uint_str_ptr, uint_str_len, hash_ctx); + mem_dealloc(uint_str_len); + return true; +} + +/** + * Format & hash a struct field array levels + * + * @param[in] field_ptr pointer to the struct field + * @param[in] hash_ctx pointer to the hashing context + * @return whether the formatting & hashing were successful or not + */ +static bool format_hash_field_type_array_levels(const void *const field_ptr, cx_hash_t *hash_ctx) { + uint8_t array_size; + char *uint_str_ptr; + uint8_t uint_str_len; + const void *lvl_ptr; + uint8_t lvls_count; + + lvl_ptr = get_struct_field_array_lvls_array(field_ptr, &lvls_count); + while (lvls_count-- > 0) { + hash_byte('[', hash_ctx); + + switch (struct_field_array_depth(lvl_ptr, &array_size)) { + case ARRAY_DYNAMIC: + break; + case ARRAY_FIXED_SIZE: + if ((uint_str_ptr = mem_alloc_and_format_uint(array_size, &uint_str_len)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + hash_nbytes((uint8_t *) uint_str_ptr, uint_str_len, hash_ctx); + mem_dealloc(uint_str_len); + break; + default: + // should not be in here :^) + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + hash_byte(']', hash_ctx); + lvl_ptr = get_next_struct_field_array_lvl(lvl_ptr); + } + return true; +} + +/** + * Format & hash a struct field type + * + * @param[in] field_ptr pointer to the struct field + * @param[in] hash_ctx pointer to the hashing context + * @return whether the formatting & hashing were successful or not + */ +bool format_hash_field_type(const void *const field_ptr, cx_hash_t *hash_ctx) { + const char *name; + uint8_t length; + + // field type name + name = get_struct_field_typename(field_ptr, &length); + hash_nbytes((uint8_t *) name, length, hash_ctx); + + // field type size + if (struct_field_has_typesize(field_ptr)) { + if (!format_hash_field_type_size(field_ptr, hash_ctx)) { + return false; + } + } + + // field type array levels + if (struct_field_is_array(field_ptr)) { + if (!format_hash_field_type_array_levels(field_ptr, hash_ctx)) { + return false; + } + } + return true; +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/format_hash_field_type.h b/src_features/signMessageEIP712/format_hash_field_type.h new file mode 100644 index 0000000..b6d635f --- /dev/null +++ b/src_features/signMessageEIP712/format_hash_field_type.h @@ -0,0 +1,12 @@ +#ifndef FORMAT_HASH_FIELD_TYPE_H_ +#define FORMAT_HASH_FIELD_TYPE_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include "cx.h" + +bool format_hash_field_type(const void *const field_ptr, cx_hash_t *hash_ctx); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // FORMAT_HASH_FIELD_TYPE_H_ diff --git a/src_features/signMessageEIP712/hash_bytes.c b/src_features/signMessageEIP712/hash_bytes.c new file mode 100644 index 0000000..6f702a4 --- /dev/null +++ b/src_features/signMessageEIP712/hash_bytes.c @@ -0,0 +1,26 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include "hash_bytes.h" + +/** + * Continue given progressive hash on given bytes + * + * @param[in] bytes_ptr pointer to bytes + * @param[in] n number of bytes to hash + * @param[in] hash_ctx pointer to the hashing context + */ +void hash_nbytes(const uint8_t *const bytes_ptr, uint8_t n, cx_hash_t *const hash_ctx) { + cx_hash(hash_ctx, 0, bytes_ptr, n, NULL, 0); +} + +/** + * Continue given progressive hash on given byte + * + * @param[in] byte byte to hash + * @param[in] hash_ctx pointer to the hashing context + */ +void hash_byte(uint8_t byte, cx_hash_t *const hash_ctx) { + hash_nbytes(&byte, 1, hash_ctx); +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/hash_bytes.h b/src_features/signMessageEIP712/hash_bytes.h new file mode 100644 index 0000000..6c1d329 --- /dev/null +++ b/src_features/signMessageEIP712/hash_bytes.h @@ -0,0 +1,13 @@ +#ifndef HASH_BYTES_H_ +#define HASH_BYTES_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include "cx.h" + +void hash_nbytes(const uint8_t *const bytes_ptr, uint8_t n, cx_hash_t *hash_ctx); +void hash_byte(uint8_t byte, cx_hash_t *hash_ctx); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // HASH_BYTES_H_ diff --git a/src_features/signMessageEIP712/path.c b/src_features/signMessageEIP712/path.c new file mode 100644 index 0000000..ec95242 --- /dev/null +++ b/src_features/signMessageEIP712/path.c @@ -0,0 +1,608 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include +#include "path.h" +#include "mem.h" +#include "context_712.h" +#include "commands_712.h" +#include "type_hash.h" +#include "shared_context.h" +#include "ethUtils.h" +#include "mem_utils.h" +#include "ui_logic.h" +#include "apdu_constants.h" // APDU response codes +#include "typed_data.h" + +static s_path *path_struct = NULL; + +/** + * Get the field pointer to by the first N depths of the path. + * + * @param[out] fields_count_ptr the number of fields in the last evaluated depth + * @param[in] n the number of depths to evaluate + * @return the field which the first Nth depths points to + */ +static const void *get_nth_field(uint8_t *const fields_count_ptr, uint8_t n) { + const void *struct_ptr = NULL; + const void *field_ptr = NULL; + const char *typename; + uint8_t length; + uint8_t fields_count; + + if (path_struct == NULL) { + return NULL; + } + + struct_ptr = path_struct->root_struct; + + if (n > path_struct->depth_count) // sanity check + { + return NULL; + } + for (uint8_t depth = 0; depth < n; ++depth) { + field_ptr = get_struct_fields_array(struct_ptr, &fields_count); + + if (fields_count_ptr != NULL) { + *fields_count_ptr = fields_count; + } + // check if the index at this depth makes sense + if (path_struct->depths[depth] > fields_count) { + return NULL; + } + + for (uint8_t index = 0; index < path_struct->depths[depth]; ++index) { + field_ptr = get_next_struct_field(field_ptr); + } + if (struct_field_type(field_ptr) == TYPE_CUSTOM) { + typename = get_struct_field_typename(field_ptr, &length); + if ((struct_ptr = get_structn(typename, length)) == NULL) { + return NULL; + } + } + } + return field_ptr; +} + +/** + * Get the element the path is pointing to. + * + * @param[out] the number of fields in the depth of the returned field + * @return the field which the path points to + */ +static inline const void *get_field(uint8_t *const fields_count) { + return get_nth_field(fields_count, path_struct->depth_count); +} + +/** + * Get Nth struct field from path + * + * @param[in] n nth depth requested + * @return pointer to the matching field, \ref NULL otherwise + */ +const void *path_get_nth_field(uint8_t n) { + return get_nth_field(NULL, n); +} + +/** + * Get Nth to last struct field from path + * + * @param[in] n nth to last depth requested + * @return pointer to the matching field, \ref NULL otherwise + */ +const void *path_get_nth_field_to_last(uint8_t n) { + const char *typename; + uint8_t typename_len; + const void *field_ptr; + const void *struct_ptr = NULL; + + field_ptr = get_nth_field(NULL, path_struct->depth_count - n); + if (field_ptr != NULL) { + typename = get_struct_field_typename(field_ptr, &typename_len); + struct_ptr = get_structn(typename, typename_len); + } + return struct_ptr; +} + +/** + * Get the element the path is pointing to + * + * @return the field which the path points to + */ +const void *path_get_field(void) { + return get_field(NULL); +} + +/** + * Go down (add) a depth level. + * + * @return whether the push was succesful + */ +static bool path_depth_list_push(void) { + if (path_struct == NULL) { + return false; + } + if (path_struct->depth_count == MAX_PATH_DEPTH) { + return false; + } + path_struct->depths[path_struct->depth_count] = 0; + path_struct->depth_count += 1; + return true; +} + +/** + * Get the last hashing context (corresponding to the current path depth) + * + * @return pointer to the hashing context + */ +static cx_sha3_t *get_last_hash_ctx(void) { + return ((cx_sha3_t *) mem_alloc(0)) - 1; +} + +/** + * Finalize the last hashing context + * + * @param[out] hash pointer to buffer where the hash will be stored + */ +static void finalize_hash_depth(uint8_t *hash) { + const cx_sha3_t *hash_ctx; + + hash_ctx = get_last_hash_ctx(); + // finalize hash + cx_hash((cx_hash_t *) hash_ctx, CX_LAST, NULL, 0, hash, KECCAK256_HASH_BYTESIZE); + mem_dealloc(sizeof(*hash_ctx)); // remove hash context +} + +/** + * Continue last progressive hashing context with given hash + * + * @param[in] hash pointer to given hash + */ +static void feed_last_hash_depth(const uint8_t *const hash) { + const cx_sha3_t *hash_ctx; + + hash_ctx = get_last_hash_ctx(); + // continue progressive hash with the array hash + cx_hash((cx_hash_t *) hash_ctx, 0, hash, KECCAK256_HASH_BYTESIZE, NULL, 0); +} + +/** + * Create a new hashing context depth in memory + * + * @param[in] init if the hashing context should be initialized + * @return whether the memory allocation of the hashing context was successful + */ +static bool push_new_hash_depth(bool init) { + cx_sha3_t *hash_ctx; + + // allocate new hash context + if ((hash_ctx = MEM_ALLOC_AND_ALIGN_TYPE(*hash_ctx)) == NULL) { + return false; + } + if (init) { + cx_keccak_init(hash_ctx, 256); // initialize it + } + return true; +} + +/** + * Go up (remove) a depth level. + * + * @return whether the pop was successful + */ +static bool path_depth_list_pop(void) { + uint8_t hash[KECCAK256_HASH_BYTESIZE]; + + if (path_struct == NULL) { + return false; + } + if (path_struct->depth_count == 0) { + return false; + } + path_struct->depth_count -= 1; + + finalize_hash_depth(hash); + if (path_struct->depth_count > 0) { + feed_last_hash_depth(hash); + } else { + switch (path_struct->root_type) { + case ROOT_DOMAIN: + memcpy(tmpCtx.messageSigningContext712.domainHash, hash, KECCAK256_HASH_BYTESIZE); + break; + case ROOT_MESSAGE: + memcpy(tmpCtx.messageSigningContext712.messageHash, hash, KECCAK256_HASH_BYTESIZE); + break; + default: + break; + } + } + + return true; +} + +/** + * Go down (add) an array depth level. + * + * @param[in] path_idx the index in the path list + * @param[in] the number of elements contained in that depth + * @return whether the push was successful + */ +static bool array_depth_list_push(uint8_t path_idx, uint8_t size) { + s_array_depth *arr; + + if (path_struct == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + if (path_struct->array_depth_count == MAX_ARRAY_DEPTH) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + arr = &path_struct->array_depths[path_struct->array_depth_count]; + arr->path_index = path_idx; + arr->size = size; + path_struct->array_depth_count += 1; + return true; +} + +/** + * Go up (remove) an array depth level. + * + * @return whether the pop was successful + */ +static bool array_depth_list_pop(void) { + uint8_t hash[KECCAK256_HASH_BYTESIZE]; + + if (path_struct == NULL) { + return false; + } + if (path_struct->array_depth_count == 0) { + return false; + } + + finalize_hash_depth(hash); + feed_last_hash_depth(hash); + + path_struct->array_depth_count -= 1; + return true; +} + +/** + * Updates the path so that it doesn't point to a struct-type field, but rather + * only to actual fields. + * + * @return whether the path update worked or not + */ +static bool path_update(void) { + uint8_t fields_count; + const void *struct_ptr; + const void *field_ptr; + const char *typename; + uint8_t typename_len; + uint8_t hash[KECCAK256_HASH_BYTESIZE]; + + if (path_struct == NULL) { + return false; + } + if ((field_ptr = get_field(NULL)) == NULL) { + return false; + } + struct_ptr = path_struct->root_struct; + while (struct_field_type(field_ptr) == TYPE_CUSTOM) { + typename = get_struct_field_typename(field_ptr, &typename_len); + if ((struct_ptr = get_structn(typename, typename_len)) == NULL) { + return false; + } + if ((field_ptr = get_struct_fields_array(struct_ptr, &fields_count)) == NULL) { + return false; + } + + if (push_new_hash_depth(true) == false) { + return false; + } + // get the struct typehash + if (type_hash(typename, typename_len, hash) == false) { + return false; + } + feed_last_hash_depth(hash); + + ui_712_queue_struct_to_review(); + path_depth_list_push(); + } + return true; +} + +/** + * Set a new struct as the path root type + * + * @param[in] struct_name the root struct name + * @param[in] name_length the root struct name length + * @return boolean indicating if it was successful or not + */ +bool path_set_root(const char *const struct_name, uint8_t name_length) { + uint8_t hash[KECCAK256_HASH_BYTESIZE]; + + if (path_struct == NULL) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + + path_struct->root_struct = get_structn(struct_name, name_length); + + if (path_struct->root_struct == NULL) { + PRINTF("Struct name not found ("); + for (int i = 0; i < name_length; ++i) { + PRINTF("%c", struct_name[i]); + } + PRINTF(")!\n"); + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + + if (push_new_hash_depth(true) == false) { + return false; + } + if (type_hash(struct_name, name_length, hash) == false) { + return false; + } + feed_last_hash_depth(hash); + // + + // init depth, at 0 : empty path + path_struct->depth_count = 0; + path_depth_list_push(); + + // init array levels at 0 + path_struct->array_depth_count = 0; + + if ((name_length == strlen(DOMAIN_STRUCT_NAME)) && + (strncmp(struct_name, DOMAIN_STRUCT_NAME, name_length) == 0)) { + path_struct->root_type = ROOT_DOMAIN; + } else { + path_struct->root_type = ROOT_MESSAGE; + } + + struct_state = DEFINED; + + // because the first field could be a struct type + path_update(); + return true; +} + +/** + * Checks the new array depth and adds it to the list + * + * @param[in] depth pointer to the array depth definition + * @param[in] total_count number of array depth contained down to this array depth + * @param[in] pidx path index + * @param[in] size requested array depth size + * @return whether the checks and add were successful or not + */ +static bool check_and_add_array_depth(const void *depth, + uint8_t total_count, + uint8_t pidx, + uint8_t size) { + uint8_t expected_size; + uint8_t arr_idx; + e_array_type expected_type; + + arr_idx = (total_count - path_struct->array_depth_count) - 1; + // we skip index 0, since we already have it + for (uint8_t idx = 1; idx < (arr_idx + 1); ++idx) { + if ((depth = get_next_struct_field_array_lvl(depth)) == NULL) { + return false; + } + } + expected_type = struct_field_array_depth(depth, &expected_size); + if ((expected_type == ARRAY_FIXED_SIZE) && (expected_size != size)) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + PRINTF("Unexpected array depth size. (expected %d, got %d)\n", expected_size, size); + return false; + } + // add it + if (!array_depth_list_push(pidx, size)) { + return false; + } + return true; +} + +/** + * Add a new array depth with a given size (number of elements). + * + * @param[in] data pointer to the number of elements + * @param[in] length length of data + * @return whether the add was successful or not + */ +bool path_new_array_depth(const uint8_t *const data, uint8_t length) { + const void *field_ptr = NULL; + const void *depth = NULL; + uint8_t depth_count; + uint8_t total_count = 0; + uint8_t pidx; + bool is_custom; + + if (path_struct == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } else if (length != 1) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + + for (pidx = 0; pidx < path_struct->depth_count; ++pidx) { + if ((field_ptr = get_nth_field(NULL, pidx + 1)) == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + if (struct_field_is_array(field_ptr)) { + if ((depth = get_struct_field_array_lvls_array(field_ptr, &depth_count)) == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + total_count += depth_count; + if (total_count > path_struct->array_depth_count) { + if (!check_and_add_array_depth(depth, total_count, pidx, *data)) { + return false; + } + break; + } + } + } + + if (pidx == path_struct->depth_count) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + PRINTF("Did not find a matching array type.\n"); + return false; + } + is_custom = struct_field_type(field_ptr) == TYPE_CUSTOM; + if (push_new_hash_depth(!is_custom) == false) { + return false; + } + if (is_custom) { + cx_sha3_t *hash_ctx = get_last_hash_ctx(); + cx_sha3_t *old_ctx = hash_ctx - 1; + + memcpy(hash_ctx, old_ctx, sizeof(*old_ctx)); + cx_keccak_init(old_ctx, 256); // init hash + } + + return true; +} + +/** + * Advance within the struct that contains the field the path points to. + * + * @return whether the end of the struct has been reached. + */ +static bool path_advance_in_struct(void) { + bool end_reached = true; + uint8_t *depth = &path_struct->depths[path_struct->depth_count - 1]; + uint8_t fields_count; + + if (path_struct == NULL) { + return false; + } + if ((get_field(&fields_count)) == NULL) { + return false; + } + if (path_struct->depth_count > 0) { + *depth += 1; + ui_712_notify_filter_change(); + end_reached = (*depth == fields_count); + } + if (end_reached) { + path_depth_list_pop(); + } + return end_reached; +} + +/** + * Advance within the array levels of the current field the path points to. + * + * @return whether the end of the array levels has been reached. + */ +static bool path_advance_in_array(void) { + bool end_reached; + s_array_depth *arr_depth; + + if (path_struct == NULL) { + return false; + } + do { + end_reached = false; + arr_depth = &path_struct->array_depths[path_struct->array_depth_count - 1]; + + if ((path_struct->array_depth_count > 0) && + (arr_depth->path_index == (path_struct->depth_count - 1))) { + arr_depth->size -= 1; + if (arr_depth->size == 0) { + array_depth_list_pop(); + end_reached = true; + } else { + return false; + } + } + } while (end_reached); + return true; +} + +/** + * Updates the path to point to the next field in order (DFS). + * + * @return whether the advancement was successful or not + */ +bool path_advance(void) { + bool end_reached; + + do { + if (path_advance_in_array()) { + end_reached = path_advance_in_struct(); + } else { + end_reached = false; + } + } while (end_reached); + path_update(); + return true; +} + +/** + * Get root structure type from path (domain or message) + * + * @return enum representing root type + */ +e_root_type path_get_root_type(void) { + if (path_struct == NULL) { + return ROOT_DOMAIN; + } + return path_struct->root_type; +} + +/** + * Get root structure from path + * + * @return pointer to the root structure definition + */ +const void *path_get_root(void) { + if (path_struct == NULL) { + return NULL; + } + return path_struct->root_struct; +} + +/** + * Get the current amount of depth + * + * @return depth count + */ +uint8_t path_get_depth_count(void) { + if (path_struct == NULL) { + return 0; + } + return path_struct->depth_count; +} + +/** + * Initialize the path context with its indexes in memory and sets it with a depth of 0. + * + * @return whether the memory allocation were successful. + */ +bool path_init(void) { + if (path_struct == NULL) { + if ((path_struct = MEM_ALLOC_AND_ALIGN_TYPE(*path_struct)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + } else { + path_struct->depth_count = 0; + } + } + return path_struct != NULL; +} + +/** + * De-initialize the path context + */ +void path_deinit(void) { + path_struct = NULL; +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/path.h b/src_features/signMessageEIP712/path.h new file mode 100644 index 0000000..d03b4c5 --- /dev/null +++ b/src_features/signMessageEIP712/path.h @@ -0,0 +1,42 @@ +#ifndef PATH_H_ +#define PATH_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include + +#define MAX_PATH_DEPTH 16 +#define MAX_ARRAY_DEPTH 8 + +typedef struct { + uint8_t path_index; + uint8_t size; +} s_array_depth; + +typedef enum { ROOT_DOMAIN, ROOT_MESSAGE } e_root_type; + +typedef struct { + uint8_t depth_count; + uint8_t depths[MAX_PATH_DEPTH]; + uint8_t array_depth_count; + s_array_depth array_depths[MAX_ARRAY_DEPTH]; + const void *root_struct; + e_root_type root_type; +} s_path; + +bool path_set_root(const char *const struct_name, uint8_t length); +const void *path_get_field(void); +bool path_advance(void); +bool path_init(void); +void path_deinit(void); +bool path_new_array_depth(const uint8_t *const data, uint8_t length); +e_root_type path_get_root_type(void); +const void *path_get_root(void); +const void *path_get_nth_field(uint8_t n); +const void *path_get_nth_field_to_last(uint8_t n); +uint8_t path_get_depth_count(void); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // PATH_H_ diff --git a/src_features/signMessageEIP712/schema_hash.c b/src_features/signMessageEIP712/schema_hash.c new file mode 100644 index 0000000..e47c2ee --- /dev/null +++ b/src_features/signMessageEIP712/schema_hash.c @@ -0,0 +1,73 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include "schema_hash.h" +#include "hash_bytes.h" +#include "typed_data.h" +#include "format_hash_field_type.h" +#include "context_712.h" + +// the SDK does not define a SHA-224 type, define it here so it's easier +// to understand in the code +typedef cx_sha256_t cx_sha224_t; + +/** + * Compute the schema hash + * + * The schema hash is the value of the root field "types" in the JSON data, + * stripped of all its spaces and newlines. This function reconstructs the JSON syntax + * from the stored typed data. + * + * @return whether the schema hash was successful or not + */ +bool compute_schema_hash(void) { + const void *struct_ptr; + uint8_t structs_count; + const void *field_ptr; + uint8_t fields_count; + const char *name; + uint8_t name_length; + cx_sha224_t hash_ctx; + + cx_sha224_init(&hash_ctx); + + struct_ptr = get_structs_array(&structs_count); + hash_byte('{', (cx_hash_t *) &hash_ctx); + while (structs_count-- > 0) { + name = get_struct_name(struct_ptr, &name_length); + hash_byte('"', (cx_hash_t *) &hash_ctx); + hash_nbytes((uint8_t *) name, name_length, (cx_hash_t *) &hash_ctx); + hash_nbytes((uint8_t *) "\":[", 3, (cx_hash_t *) &hash_ctx); + field_ptr = get_struct_fields_array(struct_ptr, &fields_count); + while (fields_count-- > 0) { + hash_nbytes((uint8_t *) "{\"name\":\"", 9, (cx_hash_t *) &hash_ctx); + name = get_struct_field_keyname(field_ptr, &name_length); + hash_nbytes((uint8_t *) name, name_length, (cx_hash_t *) &hash_ctx); + hash_nbytes((uint8_t *) "\",\"type\":\"", 10, (cx_hash_t *) &hash_ctx); + if (!format_hash_field_type(field_ptr, (cx_hash_t *) &hash_ctx)) { + return false; + } + hash_nbytes((uint8_t *) "\"}", 2, (cx_hash_t *) &hash_ctx); + if (fields_count > 0) { + hash_byte(',', (cx_hash_t *) &hash_ctx); + } + field_ptr = get_next_struct_field(field_ptr); + } + hash_byte(']', (cx_hash_t *) &hash_ctx); + if (structs_count > 0) { + hash_byte(',', (cx_hash_t *) &hash_ctx); + } + struct_ptr = get_next_struct(struct_ptr); + } + hash_byte('}', (cx_hash_t *) &hash_ctx); + + // copy hash into context struct + cx_hash((cx_hash_t *) &hash_ctx, + CX_LAST, + NULL, + 0, + eip712_context->schema_hash, + sizeof(eip712_context->schema_hash)); + return true; +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/schema_hash.h b/src_features/signMessageEIP712/schema_hash.h new file mode 100644 index 0000000..562d445 --- /dev/null +++ b/src_features/signMessageEIP712/schema_hash.h @@ -0,0 +1,12 @@ +#ifndef SCHEMA_HASH_H_ +#define SCHEMA_HASH_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include + +bool compute_schema_hash(void); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // SCHEMA_HASH_H_ diff --git a/src_features/signMessageEIP712/sol_typenames.c b/src_features/signMessageEIP712/sol_typenames.c new file mode 100644 index 0000000..bf78f26 --- /dev/null +++ b/src_features/signMessageEIP712/sol_typenames.c @@ -0,0 +1,136 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include +#include "sol_typenames.h" +#include "mem.h" +#include "os_pic.h" +#include "apdu_constants.h" // APDU response codes +#include "typed_data.h" +#include "utils.h" // ARRAY_SIZE + +// Bit indicating they are more types associated to this typename +#define TYPENAME_MORE_TYPE (1 << 7) + +static uint8_t *sol_typenames = NULL; + +enum { IDX_ENUM = 0, IDX_STR_IDX, IDX_COUNT }; + +/** + * Find a match between a typename index and all the type enums associated to it + * + * @param[in] enum_to_idx the type enum to typename index table + * @param[in] t_idx typename index + * @return whether at least one match was found + */ +static bool find_enum_matches(const uint8_t enum_to_idx[TYPES_COUNT - 1][IDX_COUNT], + uint8_t t_idx) { + uint8_t *enum_match = NULL; + + // loop over enum/typename pairs + for (uint8_t e_idx = 0; e_idx < (TYPES_COUNT - 1); ++e_idx) { + if (t_idx == enum_to_idx[e_idx][IDX_STR_IDX]) // match + { + if (enum_match != NULL) // in case of a previous match, mark it + { + *enum_match |= TYPENAME_MORE_TYPE; + } + if ((enum_match = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + *enum_match = enum_to_idx[e_idx][IDX_ENUM]; + } + } + return (enum_match != NULL); +} + +/** + * Initialize solidity typenames in memory + * + * @return whether the initialization went well or not + */ +bool sol_typenames_init(void) { + const char *const typenames[] = { + "int", // 0 + "uint", // 1 + "address", // 2 + "bool", // 3 + "string", // 4 + "bytes" // 5 + }; + // \ref TYPES_COUNT - 1 since we don't include \ref TYPE_CUSTOM + const uint8_t enum_to_idx[TYPES_COUNT - 1][IDX_COUNT] = {{TYPE_SOL_INT, 0}, + {TYPE_SOL_UINT, 1}, + {TYPE_SOL_ADDRESS, 2}, + {TYPE_SOL_BOOL, 3}, + {TYPE_SOL_STRING, 4}, + {TYPE_SOL_BYTES_FIX, 5}, + {TYPE_SOL_BYTES_DYN, 5}}; + uint8_t *typename_len_ptr; + char *typename_ptr; + + if ((sol_typenames = mem_alloc(sizeof(uint8_t))) == NULL) { + return false; + } + *(sol_typenames) = 0; + // loop over typenames + for (uint8_t t_idx = 0; t_idx < ARRAY_SIZE(typenames); ++t_idx) { + // if at least one match was found + if (find_enum_matches(enum_to_idx, t_idx)) { + if ((typename_len_ptr = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + // get pointer to the allocated space just above + *typename_len_ptr = strlen(PIC(typenames[t_idx])); + + if ((typename_ptr = mem_alloc(sizeof(char) * *typename_len_ptr)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + // copy typename + memcpy(typename_ptr, PIC(typenames[t_idx]), *typename_len_ptr); + } + // increment array size + *(sol_typenames) += 1; + } + return true; +} + +/** + * Get typename from a given field + * + * @param[in] field_ptr pointer to a struct field + * @param[out] length length of the returned typename + * @return typename or \ref NULL in case it wasn't found + */ +const char *get_struct_field_sol_typename(const uint8_t *field_ptr, uint8_t *const length) { + e_type field_type; + const uint8_t *typename_ptr; + uint8_t typenames_count; + bool more_type; + bool typename_found; + + field_type = struct_field_type(field_ptr); + typename_ptr = get_array_in_mem(sol_typenames, &typenames_count); + typename_found = false; + while (typenames_count-- > 0) { + more_type = true; + while (more_type) { + more_type = *typename_ptr & TYPENAME_MORE_TYPE; + e_type type_enum = *typename_ptr & TYPENAME_ENUM; + if (type_enum == field_type) { + typename_found = true; + } + typename_ptr += 1; + } + typename_ptr = (uint8_t *) get_string_in_mem(typename_ptr, length); + if (typename_found) return (char *) typename_ptr; + typename_ptr += *length; + } + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return NULL; // Not found +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/sol_typenames.h b/src_features/signMessageEIP712/sol_typenames.h new file mode 100644 index 0000000..c2229b7 --- /dev/null +++ b/src_features/signMessageEIP712/sol_typenames.h @@ -0,0 +1,15 @@ +#ifndef SOL_TYPENAMES_H_ +#define SOL_TYPENAMES_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include + +bool sol_typenames_init(void); + +const char *get_struct_field_sol_typename(const uint8_t *ptr, uint8_t *const length); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // SOL_TYPENAMES_H_ diff --git a/src_features/signMessageEIP712/type_hash.c b/src_features/signMessageEIP712/type_hash.c new file mode 100644 index 0000000..1e8da4b --- /dev/null +++ b/src_features/signMessageEIP712/type_hash.c @@ -0,0 +1,199 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include +#include +#include "mem.h" +#include "mem_utils.h" +#include "type_hash.h" +#include "shared_context.h" +#include "ethUtils.h" // KECCAK256_HASH_BYTESIZE +#include "format_hash_field_type.h" +#include "hash_bytes.h" +#include "apdu_constants.h" // APDU response codes +#include "typed_data.h" + +/** + * Encode & hash the given structure field + * + * @param[in] field_ptr pointer to the struct field + * @return \ref true it finished correctly, \ref false if it didn't (memory allocation) + */ +static bool encode_and_hash_field(const void *const field_ptr) { + const char *name; + uint8_t length; + + if (!format_hash_field_type(field_ptr, (cx_hash_t *) &global_sha3)) { + return false; + } + // space between field type name and field name + hash_byte(' ', (cx_hash_t *) &global_sha3); + + // field name + name = get_struct_field_keyname(field_ptr, &length); + hash_nbytes((uint8_t *) name, length, (cx_hash_t *) &global_sha3); + return true; +} + +/** + * Encode & hash the a given structure type + * + * @param[in] struct_ptr pointer to the structure we want the typestring of + * @param[in] str_length length of the formatted string in memory + * @return pointer of the string in memory, \ref NULL in case of an error + */ +static bool encode_and_hash_type(const void *const struct_ptr) { + const char *struct_name; + uint8_t struct_name_length; + const uint8_t *field_ptr; + uint8_t fields_count; + + // struct name + struct_name = get_struct_name(struct_ptr, &struct_name_length); + hash_nbytes((uint8_t *) struct_name, struct_name_length, (cx_hash_t *) &global_sha3); + + // opening struct parenthese + hash_byte('(', (cx_hash_t *) &global_sha3); + + field_ptr = get_struct_fields_array(struct_ptr, &fields_count); + for (uint8_t idx = 0; idx < fields_count; ++idx) { + // comma separating struct fields + if (idx > 0) { + hash_byte(',', (cx_hash_t *) &global_sha3); + } + + if (encode_and_hash_field(field_ptr) == false) { + return NULL; + } + + field_ptr = get_next_struct_field(field_ptr); + } + // closing struct parenthese + hash_byte(')', (cx_hash_t *) &global_sha3); + + return true; +} + +/** + * Sort the given structs based by alphabetical order + * + * @param[in] deps_count count of how many struct dependencies pointers + * @param[in,out] deps pointer to the first dependency pointer + */ +static void sort_dependencies(uint8_t deps_count, const void **deps) { + bool changed; + const void *tmp_ptr; + const char *name1, *name2; + uint8_t namelen1, namelen2; + int str_cmp_result; + + do { + changed = false; + for (size_t idx = 0; (idx + 1) < deps_count; ++idx) { + name1 = get_struct_name(*(deps + idx), &namelen1); + name2 = get_struct_name(*(deps + idx + 1), &namelen2); + + str_cmp_result = strncmp(name1, name2, MIN(namelen1, namelen2)); + if ((str_cmp_result > 0) || ((str_cmp_result == 0) && (namelen1 > namelen2))) { + tmp_ptr = *(deps + idx); + *(deps + idx) = *(deps + idx + 1); + *(deps + idx + 1) = tmp_ptr; + + changed = true; + } + } + } while (changed); +} + +/** + * Find all the dependencies from a given structure + * + * @param[out] deps_count count of how many struct dependencie pointers + * @param[in] first_dep pointer to the first dependency pointer + * @param[in] struct_ptr pointer to the struct we are getting the dependencies of + * @return pointer to the first found dependency, \ref NULL otherwise + */ +static const void **get_struct_dependencies(uint8_t *const deps_count, + const void **first_dep, + const void *const struct_ptr) { + uint8_t fields_count; + const void *field_ptr; + const char *arg_structname; + uint8_t arg_structname_length; + const void *arg_struct_ptr; + size_t dep_idx; + const void **new_dep; + + field_ptr = get_struct_fields_array(struct_ptr, &fields_count); + for (uint8_t idx = 0; idx < fields_count; ++idx) { + if (struct_field_type(field_ptr) == TYPE_CUSTOM) { + // get struct name + arg_structname = get_struct_field_typename(field_ptr, &arg_structname_length); + // from its name, get the pointer to its definition + arg_struct_ptr = get_structn(arg_structname, arg_structname_length); + + // check if it is not already present in the dependencies array + for (dep_idx = 0; dep_idx < *deps_count; ++dep_idx) { + // it's a match! + if (*(first_dep + dep_idx) == arg_struct_ptr) { + break; + } + } + // if it's not present in the array, add it and recurse into it + if (dep_idx == *deps_count) { + *deps_count += 1; + if ((new_dep = MEM_ALLOC_AND_ALIGN_TYPE(void *)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return NULL; + } + if (*deps_count == 1) { + first_dep = new_dep; + } + *new_dep = arg_struct_ptr; + // TODO: Move away from recursive calls + get_struct_dependencies(deps_count, first_dep, arg_struct_ptr); + } + } + field_ptr = get_next_struct_field(field_ptr); + } + return first_dep; +} + +/** + * Encode the structure's type and hash it + * + * @param[in] struct_name name of the given struct + * @param[in] struct_name_length length of the name of the given struct + * @param[out] hash_buf buffer containing the resulting type_hash + * @return whether the type_hash was successful or not + */ +bool type_hash(const char *const struct_name, const uint8_t struct_name_length, uint8_t *hash_buf) { + const void *const struct_ptr = get_structn(struct_name, struct_name_length); + uint8_t deps_count = 0; + const void **deps; + void *mem_loc_bak = mem_alloc(0); + + cx_keccak_init(&global_sha3, 256); // init hash + deps = get_struct_dependencies(&deps_count, NULL, struct_ptr); + if ((deps_count > 0) && (deps == NULL)) { + return false; + } + sort_dependencies(deps_count, deps); + if (encode_and_hash_type(struct_ptr) == false) { + return false; + } + // loop over each struct and generate string + for (int idx = 0; idx < deps_count; ++idx) { + if (encode_and_hash_type(*deps) == false) { + return false; + } + deps += 1; + } + mem_dealloc(mem_alloc(0) - mem_loc_bak); + + // copy hash into memory + cx_hash((cx_hash_t *) &global_sha3, CX_LAST, NULL, 0, hash_buf, KECCAK256_HASH_BYTESIZE); + return true; +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/type_hash.h b/src_features/signMessageEIP712/type_hash.h new file mode 100644 index 0000000..ad6f070 --- /dev/null +++ b/src_features/signMessageEIP712/type_hash.h @@ -0,0 +1,13 @@ +#ifndef TYPE_HASH_H_ +#define TYPE_HASH_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include + +bool type_hash(const char *const struct_name, const uint8_t struct_name_length, uint8_t *hash_buf); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // TYPE_HASH_H_ diff --git a/src_features/signMessageEIP712/typed_data.c b/src_features/signMessageEIP712/typed_data.c new file mode 100644 index 0000000..a410ccf --- /dev/null +++ b/src_features/signMessageEIP712/typed_data.c @@ -0,0 +1,749 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include +#include "typed_data.h" +#include "sol_typenames.h" +#include "apdu_constants.h" // APDU response codes +#include "context_712.h" +#include "mem.h" +#include "mem_utils.h" + +static s_typed_data *typed_data = NULL; + +/** + * Initialize the typed data context + * + * @return whether the memory allocation was successful + */ +bool typed_data_init(void) { + if (typed_data == NULL) { + if ((typed_data = MEM_ALLOC_AND_ALIGN_TYPE(*typed_data)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + // set types pointer + if ((typed_data->structs_array = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + + // create len(types) + *(typed_data->structs_array) = 0; + } + return true; +} + +void typed_data_deinit(void) { + typed_data = NULL; +} + +/** + * Skip TypeDesc from a structure field + * + * @param[in] field_ptr pointer to the beginning of the struct field + * @param[in] ptr pointer to the current location within the struct field + * @return pointer to the data right after + */ +static const uint8_t *field_skip_typedesc(const uint8_t *field_ptr, const uint8_t *ptr) { + (void) ptr; + return field_ptr + sizeof(typedesc_t); +} + +/** + * Skip the type name from a structure field + * + * @param[in] field_ptr pointer to the beginning of the struct field + * @param[in] ptr pointer to the current location within the struct field + * @return pointer to the data right after + */ +static const uint8_t *field_skip_typename(const uint8_t *field_ptr, const uint8_t *ptr) { + uint8_t size; + + if (struct_field_type(field_ptr) == TYPE_CUSTOM) { + get_string_in_mem(ptr, &size); + ptr += (sizeof(size) + size); + } + return ptr; +} + +/** + * Skip the type size from a structure field + * + * @param[in] field_ptr pointer to the beginning of the struct field + * @param[in] ptr pointer to the current location within the struct field + * @return pointer to the data right after + */ +static const uint8_t *field_skip_typesize(const uint8_t *field_ptr, const uint8_t *ptr) { + if (struct_field_has_typesize(field_ptr)) { + ptr += sizeof(typesize_t); + } + return ptr; +} + +/** + * Skip the array levels from a structure field + * + * @param[in] field_ptr pointer to the beginning of the struct field + * @param[in] ptr pointer to the current location within the struct field + * @return pointer to the data right after + */ +static const uint8_t *field_skip_array_levels(const uint8_t *field_ptr, const uint8_t *ptr) { + uint8_t size; + + if (struct_field_is_array(field_ptr)) { + ptr = get_array_in_mem(ptr, &size); + while (size-- > 0) { + ptr = get_next_struct_field_array_lvl(ptr); + } + } + return ptr; +} + +/** + * Skip the key name from a structure field + * + * @param[in] field_ptr pointer to the beginning of the struct field + * @param[in] ptr pointer to the current location within the struct field + * @return pointer to the data right after + */ +static const uint8_t *field_skip_keyname(const uint8_t *field_ptr, const uint8_t *ptr) { + uint8_t size; + + (void) field_ptr; + ptr = get_array_in_mem(ptr, &size); + return ptr + size; +} + +/** + * Get data pointer & array size from a given pointer + * + * @param[in] ptr given pointer + * @param[out] array_size pointer to array size + * @return pointer to data + */ +const void *get_array_in_mem(const void *ptr, uint8_t *const array_size) { + if (ptr == NULL) { + return NULL; + } + if (array_size) { + *array_size = *(uint8_t *) ptr; + } + return (ptr + sizeof(*array_size)); +} + +/** + * Get pointer to beginning of string & its length from a given pointer + * + * @param[in] ptr given pointer + * @param[out] string_length pointer to string length + * @return pointer to beginning of the string + */ +const char *get_string_in_mem(const uint8_t *ptr, uint8_t *const string_length) { + return (char *) get_array_in_mem(ptr, string_length); +} + +/** + * Get the TypeDesc from a given struct field pointer + * + * @param[in] field_ptr struct field pointer + * @return TypeDesc + */ +static inline typedesc_t get_struct_field_typedesc(const uint8_t *const field_ptr) { + if (field_ptr == NULL) { + return 0; + } + return *field_ptr; +} + +/** + * Check whether a struct field is an array + * + * @param[in] field_ptr struct field pointer + * @return bool whether it is the case + */ +bool struct_field_is_array(const uint8_t *const field_ptr) { + return (get_struct_field_typedesc(field_ptr) & ARRAY_MASK); +} + +/** + * Check whether a struct field has a type size associated to it + * + * @param[in] field_ptr struct field pointer + * @return bool whether it is the case + */ +bool struct_field_has_typesize(const uint8_t *const field_ptr) { + return (get_struct_field_typedesc(field_ptr) & TYPESIZE_MASK); +} + +/** + * Get type from a struct field + * + * @param[in] field_ptr struct field pointer + * @return its type enum + */ +e_type struct_field_type(const uint8_t *const field_ptr) { + return (get_struct_field_typedesc(field_ptr) & TYPE_MASK); +} + +/** + * Get type size from a struct field + * + * @param[in] field_ptr struct field pointer + * @return its type size + */ +uint8_t get_struct_field_typesize(const uint8_t *const field_ptr) { + if (field_ptr == NULL) { + return 0; + } + return *field_skip_typedesc(field_ptr, NULL); +} + +/** + * Get custom type name from a struct field + * + * @param[in] field_ptr struct field pointer + * @param[out] length the type name length + * @return type name pointer + */ +const char *get_struct_field_custom_typename(const uint8_t *field_ptr, uint8_t *const length) { + const uint8_t *ptr; + + if (field_ptr == NULL) { + return NULL; + } + ptr = field_skip_typedesc(field_ptr, NULL); + return get_string_in_mem(ptr, length); +} + +/** + * Get type name from a struct field + * + * @param[in] field_ptr struct field pointer + * @param[out] length the type name length + * @return type name pointer + */ +const char *get_struct_field_typename(const uint8_t *field_ptr, uint8_t *const length) { + if (field_ptr == NULL) { + return NULL; + } + if (struct_field_type(field_ptr) == TYPE_CUSTOM) { + return get_struct_field_custom_typename(field_ptr, length); + } + return get_struct_field_sol_typename(field_ptr, length); +} + +/** + * Get array type of a given struct field's array depth + * + * @param[in] array_depth_ptr given array depth + * @param[out] array_size pointer to array size + * @return array type of that depth + */ +e_array_type struct_field_array_depth(const uint8_t *array_depth_ptr, uint8_t *const array_size) { + if (array_depth_ptr == NULL) { + return 0; + } + if (*array_depth_ptr == ARRAY_FIXED_SIZE) { + if (array_size != NULL) { + *array_size = *(array_depth_ptr + sizeof(uint8_t)); + } + } + return *array_depth_ptr; +} + +/** + * Get next array depth form a given struct field's array depth + * + * @param[in] array_depth_ptr given array depth + * @return next array depth + */ +const uint8_t *get_next_struct_field_array_lvl(const uint8_t *const array_depth_ptr) { + const uint8_t *ptr; + + if (array_depth_ptr == NULL) { + return NULL; + } + switch (*array_depth_ptr) { + case ARRAY_DYNAMIC: + ptr = array_depth_ptr; + break; + case ARRAY_FIXED_SIZE: + ptr = array_depth_ptr + 1; + break; + default: + // should not be in here :^) + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; + } + return ptr + 1; +} + +/** + * Get the array levels from a given struct field + * + * @param[in] field_ptr given struct field + * @param[out] length number of array levels + * @return pointer to the first array level + */ +const uint8_t *get_struct_field_array_lvls_array(const uint8_t *const field_ptr, + uint8_t *const length) { + const uint8_t *ptr; + + if (field_ptr == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; + } + ptr = field_skip_typedesc(field_ptr, NULL); + ptr = field_skip_typename(field_ptr, ptr); + ptr = field_skip_typesize(field_ptr, ptr); + return get_array_in_mem(ptr, length); +} + +/** + * Get key name from a given struct field + * + * @param[in] field_ptr given struct field + * @param[out] length name length + * @return key name + */ +const char *get_struct_field_keyname(const uint8_t *field_ptr, uint8_t *const length) { + const uint8_t *ptr; + + if (field_ptr == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; + } + ptr = field_skip_typedesc(field_ptr, NULL); + ptr = field_skip_typename(field_ptr, ptr); + ptr = field_skip_typesize(field_ptr, ptr); + ptr = field_skip_array_levels(field_ptr, ptr); + return get_string_in_mem(ptr, length); +} + +/** + * Get next struct field from a given field + * + * @param[in] field_ptr given struct field + * @return pointer to the next field + */ +const uint8_t *get_next_struct_field(const void *const field_ptr) { + const void *ptr; + + if (field_ptr == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; + } + ptr = field_skip_typedesc(field_ptr, NULL); + ptr = field_skip_typename(field_ptr, ptr); + ptr = field_skip_typesize(field_ptr, ptr); + ptr = field_skip_array_levels(field_ptr, ptr); + return field_skip_keyname(field_ptr, ptr); +} + +/** + * Get name from a given struct + * + * @param[in] struct_ptr given struct + * @param[out] length name length + * @return struct name + */ +const char *get_struct_name(const uint8_t *const struct_ptr, uint8_t *const length) { + if (struct_ptr == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; + } + return (char *) get_string_in_mem(struct_ptr, length); +} + +/** + * Get struct fields from a given struct + * + * @param[in] struct_ptr given struct + * @param[out] length number of fields + * @return struct name + */ +const uint8_t *get_struct_fields_array(const uint8_t *const struct_ptr, uint8_t *const length) { + const void *ptr; + uint8_t name_length; + + if (struct_ptr == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; + } + ptr = struct_ptr; + get_struct_name(struct_ptr, &name_length); + ptr += (sizeof(name_length) + name_length); // skip length + return get_array_in_mem(ptr, length); +} + +/** + * Get next struct from a given struct + * + * @param[in] struct_ptr given struct + * @return pointer to next struct + */ +const uint8_t *get_next_struct(const uint8_t *const struct_ptr) { + uint8_t fields_count; + const void *ptr; + + if (struct_ptr == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; + } + ptr = get_struct_fields_array(struct_ptr, &fields_count); + while (fields_count-- > 0) { + ptr = get_next_struct_field(ptr); + } + return ptr; +} + +/** + * Get structs array + * + * @param[out] length number of structs + * @return pointer to the first struct + */ +const uint8_t *get_structs_array(uint8_t *const length) { + return get_array_in_mem(typed_data->structs_array, length); +} + +/** + * Find struct with a given name + * + * @param[in] name struct name + * @param[in] length name length + * @return pointer to struct + */ +const uint8_t *get_structn(const char *const name, const uint8_t length) { + uint8_t structs_count; + const uint8_t *struct_ptr; + const char *struct_name; + uint8_t name_length; + + if (name == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; + } + struct_ptr = get_structs_array(&structs_count); + while (structs_count-- > 0) { + struct_name = get_struct_name(struct_ptr, &name_length); + if ((length == name_length) && (memcmp(name, struct_name, length) == 0)) { + return struct_ptr; + } + struct_ptr = get_next_struct(struct_ptr); + } + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return NULL; +} + +/** + * Set struct name + * + * @param[in] length name length + * @param[in] name name + * @return whether it was successful + */ +bool set_struct_name(uint8_t length, const uint8_t *const name) { + uint8_t *length_ptr; + char *name_ptr; + + if ((name == NULL) || (typed_data == NULL)) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + // increment number of structs + if ((*(typed_data->structs_array) += 1) == 0) { + PRINTF("EIP712 Structs count overflow!\n"); + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + // copy length + if ((length_ptr = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + *length_ptr = length; + + // copy name + if ((name_ptr = mem_alloc(sizeof(char) * length)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + memmove(name_ptr, name, length); + + // initialize number of fields + if ((typed_data->current_struct_fields_array = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + *(typed_data->current_struct_fields_array) = 0; + + struct_state = INITIALIZED; + return true; +} + +/** + * Set struct field TypeDesc + * + * @param[in] data the field data + * @param[in] data_idx the data index + * @return pointer to the TypeDesc in memory + */ +static const typedesc_t *set_struct_field_typedesc(const uint8_t *const data, + uint8_t *data_idx, + uint8_t length) { + typedesc_t *typedesc_ptr; + + // copy TypeDesc + if ((*data_idx + sizeof(*typedesc_ptr)) > length) // check buffer bound + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return NULL; + } + if ((typedesc_ptr = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return NULL; + } + *typedesc_ptr = data[(*data_idx)++]; + return typedesc_ptr; +} + +/** + * Set struct field custom typename + * + * @param[in] data the field data + * @param[in] data_idx the data index + * @return whether it was successful + */ +static bool set_struct_field_custom_typename(const uint8_t *const data, + uint8_t *data_idx, + uint8_t length) { + uint8_t *typename_len_ptr; + char *typename; + + // copy custom struct name length + if ((*data_idx + sizeof(*typename_len_ptr)) > length) // check buffer bound + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if ((typename_len_ptr = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + *typename_len_ptr = data[(*data_idx)++]; + + // copy name + if ((*data_idx + *typename_len_ptr) > length) // check buffer bound + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if ((typename = mem_alloc(sizeof(char) * *typename_len_ptr)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + memmove(typename, &data[*data_idx], *typename_len_ptr); + *data_idx += *typename_len_ptr; + return true; +} + +/** + * Set struct field's array levels + * + * @param[in] data the field data + * @param[in] data_idx the data index + * @return whether it was successful + */ +static bool set_struct_field_array(const uint8_t *const data, uint8_t *data_idx, uint8_t length) { + uint8_t *array_levels_count; + uint8_t *array_level; + uint8_t *array_level_size; + + if ((*data_idx + sizeof(*array_levels_count)) > length) // check buffer bound + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if ((array_levels_count = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + *array_levels_count = data[(*data_idx)++]; + for (int idx = 0; idx < *array_levels_count; ++idx) { + if ((*data_idx + sizeof(*array_level)) > length) // check buffer bound + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if ((array_level = mem_alloc(sizeof(*array_level))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + *array_level = data[(*data_idx)++]; + if (*array_level >= ARRAY_TYPES_COUNT) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + switch (*array_level) { + case ARRAY_DYNAMIC: // nothing to do + break; + case ARRAY_FIXED_SIZE: + if ((*data_idx + sizeof(*array_level_size)) > length) // check buffer bound + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if ((array_level_size = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + *array_level_size = data[(*data_idx)++]; + break; + default: + // should not be in here :^) + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + } + return true; +} + +/** + * Set struct field's type size + * + * @param[in] data the field data + * @param[in,out] data_idx the data index + * @return whether it was successful + */ +static bool set_struct_field_typesize(const uint8_t *const data, + uint8_t *data_idx, + uint8_t length) { + uint8_t *typesize_ptr; + + // copy TypeSize + if ((*data_idx + sizeof(*typesize_ptr)) > length) // check buffer bound + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if ((typesize_ptr = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + *typesize_ptr = data[(*data_idx)++]; + return true; +} + +/** + * Set struct field's key name + * + * @param[in] data the field data + * @param[in,out] data_idx the data index + * @return whether it was successful + */ +static bool set_struct_field_keyname(const uint8_t *const data, uint8_t *data_idx, uint8_t length) { + uint8_t *keyname_len_ptr; + char *keyname_ptr; + + // copy length + if ((*data_idx + sizeof(*keyname_len_ptr)) > length) // check buffer bound + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if ((keyname_len_ptr = mem_alloc(sizeof(uint8_t))) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + *keyname_len_ptr = data[(*data_idx)++]; + + // copy name + if ((*data_idx + *keyname_len_ptr) > length) // check buffer bound + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if ((keyname_ptr = mem_alloc(sizeof(char) * *keyname_len_ptr)) == NULL) { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + return false; + } + memmove(keyname_ptr, &data[*data_idx], *keyname_len_ptr); + *data_idx += *keyname_len_ptr; + return true; +} + +/** + * Set struct field + * + * @param[in] length data length + * @param[in] data the field data + * @return whether it was successful + */ +bool set_struct_field(uint8_t length, const uint8_t *const data) { + const typedesc_t *typedesc_ptr; + uint8_t data_idx = 0; + + if ((data == NULL) || (length == 0)) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } else if (typed_data == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + if (struct_state == NOT_INITIALIZED) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + // increment number of struct fields + if ((*(typed_data->current_struct_fields_array) += 1) == 0) { + PRINTF("EIP712 Struct fields count overflow!\n"); + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + if ((typedesc_ptr = set_struct_field_typedesc(data, &data_idx, length)) == NULL) { + return false; + } + + // check TypeSize flag in TypeDesc + if (*typedesc_ptr & TYPESIZE_MASK) { + // TYPESIZE and TYPE_CUSTOM are mutually exclusive + if ((*typedesc_ptr & TYPE_MASK) == TYPE_CUSTOM) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + if (set_struct_field_typesize(data, &data_idx, length) == false) { + return false; + } + + } else if ((*typedesc_ptr & TYPE_MASK) == TYPE_CUSTOM) { + if (set_struct_field_custom_typename(data, &data_idx, length) == false) { + return false; + } + } + if (*typedesc_ptr & ARRAY_MASK) { + if (set_struct_field_array(data, &data_idx, length) == false) { + return false; + } + } + + if (set_struct_field_keyname(data, &data_idx, length) == false) { + return false; + } + + if (data_idx != length) // check that there is no more + { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + return true; +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/typed_data.h b/src_features/signMessageEIP712/typed_data.h new file mode 100644 index 0000000..9cfb68e --- /dev/null +++ b/src_features/signMessageEIP712/typed_data.h @@ -0,0 +1,65 @@ +#ifndef TYPED_DATA_H_ +#define TYPED_DATA_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include + +// TypeDesc masks +#define TYPE_MASK (0xF) +#define ARRAY_MASK (1 << 7) +#define TYPESIZE_MASK (1 << 6) +#define TYPENAME_ENUM (0xF) + +typedef enum { ARRAY_DYNAMIC = 0, ARRAY_FIXED_SIZE, ARRAY_TYPES_COUNT } e_array_type; + +typedef enum { + // contract defined struct + TYPE_CUSTOM = 0, + // native types + TYPE_SOL_INT, + TYPE_SOL_UINT, + TYPE_SOL_ADDRESS, + TYPE_SOL_BOOL, + TYPE_SOL_STRING, + TYPE_SOL_BYTES_FIX, + TYPE_SOL_BYTES_DYN, + TYPES_COUNT +} e_type; + +typedef struct { + uint8_t *structs_array; + uint8_t *current_struct_fields_array; +} s_typed_data; + +typedef uint8_t typedesc_t; +typedef uint8_t typesize_t; + +const void *get_array_in_mem(const void *ptr, uint8_t *const array_size); +const char *get_string_in_mem(const uint8_t *ptr, uint8_t *const string_length); +bool struct_field_is_array(const uint8_t *ptr); +bool struct_field_has_typesize(const uint8_t *ptr); +e_type struct_field_type(const uint8_t *ptr); +uint8_t get_struct_field_typesize(const uint8_t *ptr); +const char *get_struct_field_custom_typename(const uint8_t *ptr, uint8_t *const length); +const char *get_struct_field_typename(const uint8_t *ptr, uint8_t *const length); +e_array_type struct_field_array_depth(const uint8_t *ptr, uint8_t *const array_size); +const uint8_t *get_next_struct_field_array_lvl(const uint8_t *const ptr); +const uint8_t *struct_field_half_skip(const uint8_t *ptr); +const uint8_t *get_struct_field_array_lvls_array(const uint8_t *const ptr, uint8_t *const length); +const char *get_struct_field_keyname(const uint8_t *ptr, uint8_t *const length); +const uint8_t *get_next_struct_field(const void *ptr); +const char *get_struct_name(const uint8_t *ptr, uint8_t *const length); +const uint8_t *get_struct_fields_array(const uint8_t *ptr, uint8_t *const length); +const uint8_t *get_next_struct(const uint8_t *ptr); +const uint8_t *get_structs_array(uint8_t *const length); +const uint8_t *get_structn(const char *const name_ptr, const uint8_t name_length); +bool set_struct_name(uint8_t length, const uint8_t *const name); +bool set_struct_field(uint8_t length, const uint8_t *const data); +bool typed_data_init(void); +void typed_data_deinit(void); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // TYPED_DATA_H_ diff --git a/src_features/signMessageEIP712/ui_logic.c b/src_features/signMessageEIP712/ui_logic.c new file mode 100644 index 0000000..698e5e1 --- /dev/null +++ b/src_features/signMessageEIP712/ui_logic.c @@ -0,0 +1,548 @@ +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include +#include "ui_logic.h" +#include "mem.h" +#include "mem_utils.h" +#include "os_io.h" +#include "shared_context.h" +#include "ethUtils.h" // getEthDisplayableAddress +#include "utils.h" // uint256_to_decimal +#include "common_712.h" +#include "context_712.h" // eip712_context_deinit +#include "uint256.h" // tostring256 && tostring256_signed +#include "path.h" // path_get_root_type +#include "apdu_constants.h" // APDU response codes +#include "typed_data.h" +#include "commands_712.h" +#include "common_ui.h" + +static t_ui_context *ui_ctx = NULL; + +/** + * Checks on the UI context to determine if the next EIP 712 field should be shown + * + * @return whether the next field should be shown + */ +static bool ui_712_field_shown(void) { + bool ret = false; + + if (ui_ctx->filtering_mode == EIP712_FILTERING_BASIC) { + if (N_storage.verbose_eip712 || (path_get_root_type() == ROOT_DOMAIN)) { + ret = true; + } + } else // EIP712_FILTERING_FULL + { + if (ui_ctx->field_flags & UI_712_FIELD_SHOWN) { + ret = true; + } + } + return ret; +} + +/** + * Set UI buffer + * + * @param[in] src source buffer + * @param[in] src_length source buffer size + * @param[in] dst destination buffer + * @param[in] dst_length destination buffer length + * @param[in] explicit_trunc if truncation should be explicitely shown + */ +static void ui_712_set_buf(const char *const src, + size_t src_length, + char *const dst, + size_t dst_length, + bool explicit_trunc) { + uint8_t cpy_length; + + if (src_length < dst_length) { + cpy_length = src_length; + } else { + cpy_length = dst_length - 1; + } + memcpy(dst, src, cpy_length); + dst[cpy_length] = '\0'; + if (explicit_trunc && (cpy_length < src_length)) { + memcpy(dst + cpy_length - 3, "...", 3); + } +} + +/** + * Skip the field if needed and reset its UI flags + */ +void ui_712_finalize_field(void) { + if (!ui_712_field_shown()) { + ui_712_next_field(); + } + ui_712_field_flags_reset(); +} + +/** + * Set a new title for the EIP-712 generic UX_STEP + * + * @param[in] str the new title + * @param[in] length its length + */ +void ui_712_set_title(const char *const str, uint8_t length) { + ui_712_set_buf(str, length, strings.tmp.tmp2, sizeof(strings.tmp.tmp2), false); +} + +/** + * Set a new value for the EIP-712 generic UX_STEP + * + * @param[in] str the new value + * @param[in] length its length + */ +void ui_712_set_value(const char *const str, uint8_t length) { + ui_712_set_buf(str, length, strings.tmp.tmp, sizeof(strings.tmp.tmp), true); +} + +/** + * Redraw the dynamic UI step that shows EIP712 information + */ +void ui_712_redraw_generic_step(void) { + if (!ui_ctx->shown) // Initialize if it is not already + { + ui_712_start(); + ui_ctx->shown = true; + } else { + ui_712_switch_to_message(); + } +} + +/** + * Called to fetch the next field if they have not all been processed yet + * + * @return whether there will be a next field + */ +bool ui_712_next_field(void) { + bool next = false; + + if (ui_ctx == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + } else { + if (ui_ctx->structs_to_review > 0) { + ui_712_review_struct(path_get_nth_field_to_last(ui_ctx->structs_to_review)); + ui_ctx->structs_to_review -= 1; + } + if (!ui_ctx->end_reached) { + handle_eip712_return_code(true); + next = true; + } + } + return next; +} + +/** + * Used to notify of a new struct to review + * + * @param[in] struct_ptr pointer to the structure to be shown + */ +void ui_712_review_struct(const void *const struct_ptr) { + const char *struct_name; + uint8_t struct_name_length; + const char *const title = "Review struct"; + + if (ui_ctx == NULL) { + return; + } + + ui_712_set_title(title, strlen(title)); + if ((struct_name = get_struct_name(struct_ptr, &struct_name_length)) != NULL) { + ui_712_set_value(struct_name, struct_name_length); + } + ui_712_redraw_generic_step(); +} + +/** + * Show the hash of the message on the generic UI step + */ +void ui_712_message_hash(void) { + const char *const title = "Message hash"; + + ui_712_set_title(title, strlen(title)); + snprintf(strings.tmp.tmp, + sizeof(strings.tmp.tmp), + "0x%.*H", + KECCAK256_HASH_BYTESIZE, + tmpCtx.messageSigningContext712.messageHash); + ui_712_redraw_generic_step(); +} + +/** + * Format a given data as a string + * + * @param[in] data the data that needs formatting + * @param[in] length its length + */ +static void ui_712_format_str(const uint8_t *const data, uint8_t length) { + if (ui_712_field_shown()) { + ui_712_set_value((char *) data, length); + } +} + +/** + * Format a given data as a string representation of an address + * + * @param[in] data the data that needs formatting + * @param[in] length its length + * @return if the formatting was successful + */ +static bool ui_712_format_addr(const uint8_t *const data, uint8_t length) { + if (length != ADDRESS_LENGTH) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + if (ui_712_field_shown()) { + getEthDisplayableAddress((uint8_t *) data, + strings.tmp.tmp, + sizeof(strings.tmp.tmp), + &global_sha3, + chainConfig->chainId); + } + return true; +} + +/** + * Format given data as a string representation of a boolean + * + * @param[in] data the data that needs formatting + * @param[in] length its length + * @return if the formatting was successful + */ +static bool ui_712_format_bool(const uint8_t *const data, uint8_t length) { + const char *const true_str = "true"; + const char *const false_str = "false"; + const char *str; + + if (length != 1) { + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + str = *data ? true_str : false_str; + if (ui_712_field_shown()) { + ui_712_set_value(str, strlen(str)); + } + return true; +} + +/** + * Format given data as a string representation of bytes + * + * @param[in] data the data that needs formatting + * @param[in] length its length + */ +static void ui_712_format_bytes(const uint8_t *const data, uint8_t length) { + if (ui_712_field_shown()) { + snprintf(strings.tmp.tmp, sizeof(strings.tmp.tmp), "0x%.*H", length, data); + // +2 for the "0x" + // x2 for each byte value is represented by 2 ASCII characters + if ((2 + (length * 2)) > (sizeof(strings.tmp.tmp) - 1)) { + strings.tmp.tmp[sizeof(strings.tmp.tmp) - 1 - 3] = '\0'; + strcat(strings.tmp.tmp, "..."); + } + } +} + +/** + * Format given data as a string representation of an integer + * + * @param[in] data the data that needs formatting + * @param[in] length its length + * @return if the formatting was successful + */ +static bool ui_712_format_int(const uint8_t *const data, + uint8_t length, + const void *const field_ptr) { + uint256_t value256; + uint128_t value128; + int32_t value32; + int16_t value16; + + switch (get_struct_field_typesize(field_ptr) * 8) { + case 256: + convertUint256BE(data, length, &value256); + if (ui_712_field_shown()) { + tostring256_signed(&value256, 10, strings.tmp.tmp, sizeof(strings.tmp.tmp)); + } + break; + case 128: + convertUint128BE(data, length, &value128); + if (ui_712_field_shown()) { + tostring128_signed(&value128, 10, strings.tmp.tmp, sizeof(strings.tmp.tmp)); + } + break; + case 64: + convertUint64BEto128(data, length, &value128); + if (ui_712_field_shown()) { + tostring128_signed(&value128, 10, strings.tmp.tmp, sizeof(strings.tmp.tmp)); + } + break; + case 32: + value32 = 0; + for (int i = 0; i < length; ++i) { + ((uint8_t *) &value32)[length - 1 - i] = data[i]; + } + if (ui_712_field_shown()) { + snprintf(strings.tmp.tmp, sizeof(strings.tmp.tmp), "%d", value32); + } + break; + case 16: + value16 = 0; + for (int i = 0; i < length; ++i) { + ((uint8_t *) &value16)[length - 1 - i] = data[i]; + } + if (ui_712_field_shown()) { + snprintf(strings.tmp.tmp, + sizeof(strings.tmp.tmp), + "%d", + value16); // expanded to 32 bits + } + break; + case 8: + if (ui_712_field_shown()) { + snprintf(strings.tmp.tmp, + sizeof(strings.tmp.tmp), + "%d", + ((int8_t *) data)[0]); // expanded to 32 bits + } + break; + default: + PRINTF("Unhandled field typesize\n"); + apdu_response_code = APDU_RESPONSE_INVALID_DATA; + return false; + } + return true; +} + +/** + * Format given data as a string representation of an unsigned integer + * + * @param[in] data the data that needs formatting + * @param[in] length its length + */ +static void ui_712_format_uint(const uint8_t *const data, uint8_t length) { + uint256_t value256; + + convertUint256BE(data, length, &value256); + if (ui_712_field_shown()) { + tostring256(&value256, 10, strings.tmp.tmp, sizeof(strings.tmp.tmp)); + } +} + +/** + * Used to notify of a new field to review in the current struct (key + value) + * + * @param[in] field_ptr pointer to the new struct field + * @param[in] data pointer to the field's raw value + * @param[in] length field's raw value byte-length + */ +bool ui_712_new_field(const void *const field_ptr, const uint8_t *const data, uint8_t length) { + const char *key; + uint8_t key_len; + + if (ui_ctx == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + // Key + if ((key = get_struct_field_keyname(field_ptr, &key_len)) == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return false; + } + + if (ui_712_field_shown() && !(ui_ctx->field_flags & UI_712_FIELD_NAME_PROVIDED)) { + ui_712_set_title(key, key_len); + } + + // Value + switch (struct_field_type(field_ptr)) { + case TYPE_SOL_STRING: + ui_712_format_str(data, length); + break; + case TYPE_SOL_ADDRESS: + if (ui_712_format_addr(data, length) == false) { + return false; + } + break; + case TYPE_SOL_BOOL: + if (ui_712_format_bool(data, length) == false) { + return false; + } + break; + case TYPE_SOL_BYTES_FIX: + case TYPE_SOL_BYTES_DYN: + ui_712_format_bytes(data, length); + break; + case TYPE_SOL_INT: + if (ui_712_format_int(data, length, field_ptr) == false) { + return false; + } + break; + case TYPE_SOL_UINT: + ui_712_format_uint(data, length); + break; + default: + PRINTF("Unhandled type\n"); + return false; + } + + // Check if this field is supposed to be displayed + if (ui_712_field_shown()) { + ui_712_redraw_generic_step(); + } + return true; +} + +/** + * Used to signal that we are done with reviewing the structs and we can now have + * the option to approve or reject the signature + */ +void ui_712_end_sign(void) { + if (ui_ctx == NULL) { + apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED; + return; + } + ui_ctx->end_reached = true; + + if (N_storage.verbose_eip712 || (ui_ctx->filtering_mode == EIP712_FILTERING_FULL)) { + ui_712_switch_to_sign(); + } +} + +/** + * Initializes the UI context structure in memory + */ +bool ui_712_init(void) { + if ((ui_ctx = MEM_ALLOC_AND_ALIGN_TYPE(*ui_ctx))) { + ui_ctx->shown = false; + ui_ctx->end_reached = false; + ui_ctx->filtering_mode = EIP712_FILTERING_BASIC; + } else { + apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY; + } + return ui_ctx != NULL; +} + +/** + * Deinit function that simply unsets the struct pointer to NULL + */ +void ui_712_deinit(void) { + ui_ctx = NULL; +} + +/** + * Approve button handling, calls the common handler function then + * deinitializes the EIP712 context altogether. + * @param[in] e unused here, just needed to match the UI function signature + * @return unused here, just needed to match the UI function signature + */ +unsigned int ui_712_approve() { + ui_712_approve_cb(); + eip712_context_deinit(); + return 0; +} + +/** + * Reject button handling, calls the common handler function then + * deinitializes the EIP712 context altogether. + + * @param[in] e unused here, just needed to match the UI function signature + * @return unused here, just needed to match the UI function signature + */ +unsigned int ui_712_reject() { + ui_712_reject_cb(); + eip712_context_deinit(); + return 0; +} + +/** + * Set a structure field's UI flags + * + * @param[in] show if this field should be shown on the device + * @param[in] name_provided if a substitution name has been provided + */ +void ui_712_flag_field(bool show, bool name_provided) { + if (show) { + ui_ctx->field_flags |= UI_712_FIELD_SHOWN; + } + if (name_provided) { + ui_ctx->field_flags |= UI_712_FIELD_NAME_PROVIDED; + } +} + +/** + * Set the UI filtering mode + * + * @param[in] the new filtering mode + */ +void ui_712_set_filtering_mode(e_eip712_filtering_mode mode) { + ui_ctx->filtering_mode = mode; +} + +/** + * Get the UI filtering mode + * + * @return current filtering mode + */ +e_eip712_filtering_mode ui_712_get_filtering_mode(void) { + return ui_ctx->filtering_mode; +} + +/** + * Set the number of filters this message should process + * + * @param[in] count number of filters + */ +void ui_712_set_filters_count(uint8_t count) { + ui_ctx->filters_to_process = count; +} + +/** + * Get the number of filters left to process + * + * @return number of filters + */ +uint8_t ui_712_remaining_filters(void) { + return ui_ctx->filters_to_process; +} + +/** + * Reset all the UI struct field flags + */ +void ui_712_field_flags_reset(void) { + ui_ctx->field_flags = 0; +} + +/** + * Add a struct to the UI review queue + * + * Makes it so the user will have to go through a "Review struct" screen + */ +void ui_712_queue_struct_to_review(void) { + if (N_storage.verbose_eip712) { + ui_ctx->structs_to_review += 1; + } +} + +/** + * Notify of a filter change from a path advance + * + * This function figures out by itself if there is anything to do + */ +void ui_712_notify_filter_change(void) { + if (path_get_root_type() == ROOT_MESSAGE) { + if (ui_ctx->filtering_mode == EIP712_FILTERING_FULL) { + if (ui_ctx->filters_to_process > 0) { + if (ui_ctx->field_flags & UI_712_FIELD_SHOWN) { + ui_ctx->filters_to_process -= 1; + } + } + } + } +} + +#endif // HAVE_EIP712_FULL_SUPPORT diff --git a/src_features/signMessageEIP712/ui_logic.h b/src_features/signMessageEIP712/ui_logic.h new file mode 100644 index 0000000..7ebb4b6 --- /dev/null +++ b/src_features/signMessageEIP712/ui_logic.h @@ -0,0 +1,47 @@ +#ifndef UI_LOGIC_712_H_ +#define UI_LOGIC_712_H_ + +#ifdef HAVE_EIP712_FULL_SUPPORT + +#include +#include "ux.h" + +#define UI_712_FIELD_SHOWN (1 << 0) +#define UI_712_FIELD_NAME_PROVIDED (1 << 1) + +typedef enum { EIP712_FILTERING_BASIC, EIP712_FILTERING_FULL } e_eip712_filtering_mode; + +typedef struct { + bool shown; + bool end_reached; + uint8_t filtering_mode; + uint8_t filters_to_process; + uint8_t field_flags; + uint8_t structs_to_review; +} t_ui_context; + +bool ui_712_init(void); +void ui_712_deinit(void); +bool ui_712_next_field(void); +void ui_712_review_struct(const void *const struct_ptr); +bool ui_712_new_field(const void *const field_ptr, const uint8_t *const data, uint8_t length); +void ui_712_end_sign(void); +unsigned int ui_712_approve(); +unsigned int ui_712_reject(); +void ui_712_set_title(const char *const str, uint8_t length); +void ui_712_set_value(const char *const str, uint8_t length); +void ui_712_message_hash(void); +void ui_712_redraw_generic_step(void); +void ui_712_flag_field(bool show, bool name_provided); +void ui_712_field_flags_reset(void); +void ui_712_finalize_field(void); +void ui_712_set_filtering_mode(e_eip712_filtering_mode mode); +e_eip712_filtering_mode ui_712_get_filtering_mode(void); +void ui_712_set_filters_count(uint8_t count); +uint8_t ui_712_remaining_filters(void); +void ui_712_queue_struct_to_review(void); +void ui_712_notify_filter_change(void); + +#endif // HAVE_EIP712_FULL_SUPPORT + +#endif // UI_LOGIC_712_H_ diff --git a/src_features/signMessageEIP712/ui_common_signMessage712.c b/src_features/signMessageEIP712_common/common_712.c similarity index 87% rename from src_features/signMessageEIP712/ui_common_signMessage712.c rename to src_features/signMessageEIP712_common/common_712.c index 4d5e266..abc1fd5 100644 --- a/src_features/signMessageEIP712/ui_common_signMessage712.c +++ b/src_features/signMessageEIP712_common/common_712.c @@ -1,16 +1,19 @@ -#include "os_io_seproxyhal.h" #include "shared_context.h" +#include "os_io_seproxyhal.h" +#include "ui_callbacks.h" +#include "common_712.h" +#include "ui_callbacks.h" #include "common_ui.h" static const uint8_t EIP_712_MAGIC[] = {0x19, 0x01}; -unsigned int io_seproxyhal_touch_signMessage712_v0_ok(__attribute__((unused)) - const bagl_element_t *e) { +unsigned int ui_712_approve_cb() { uint8_t privateKeyData[INT256_LENGTH]; uint8_t hash[INT256_LENGTH]; uint8_t signature[100]; cx_ecfp_private_key_t privateKey; uint32_t tx = 0; + io_seproxyhal_io_heartbeat(); cx_keccak_init(&global_sha3, 256); cx_hash((cx_hash_t *) &global_sha3, @@ -31,7 +34,8 @@ unsigned int io_seproxyhal_touch_signMessage712_v0_ok(__attribute__((unused)) sizeof(tmpCtx.messageSigningContext712.messageHash), hash, sizeof(hash)); - PRINTF("EIP712 hash to sign %.*H\n", 32, hash); + PRINTF("EIP712 Domain hash 0x%.*h\n", 32, tmpCtx.messageSigningContext712.domainHash); + PRINTF("EIP712 Message hash 0x%.*h\n", 32, tmpCtx.messageSigningContext712.messageHash); io_seproxyhal_io_heartbeat(); os_perso_derive_node_bip32(CX_CURVE_256K1, tmpCtx.messageSigningContext712.bip32.path, @@ -71,8 +75,7 @@ unsigned int io_seproxyhal_touch_signMessage712_v0_ok(__attribute__((unused)) return 0; // do not redraw the widget } -unsigned int io_seproxyhal_touch_signMessage712_v0_cancel(__attribute__((unused)) - const bagl_element_t *e) { +unsigned int ui_712_reject_cb() { reset_app_context(); G_io_apdu_buffer[0] = 0x69; G_io_apdu_buffer[1] = 0x85; diff --git a/src_features/signMessageEIP712_common/common_712.h b/src_features/signMessageEIP712_common/common_712.h new file mode 100644 index 0000000..1283a61 --- /dev/null +++ b/src_features/signMessageEIP712_common/common_712.h @@ -0,0 +1,10 @@ +#ifndef COMMON_EIP712_H_ +#define COMMON_EIP712_H_ + +#include +#include "ux.h" + +unsigned int ui_712_approve_cb(); +unsigned int ui_712_reject_cb(); + +#endif // COMMON_EIP712_H_ diff --git a/src_features/signMessageEIP712_v0/cmd_signMessage712.c b/src_features/signMessageEIP712_v0/cmd_signMessage712.c new file mode 100644 index 0000000..539e8f0 --- /dev/null +++ b/src_features/signMessageEIP712_v0/cmd_signMessage712.c @@ -0,0 +1,40 @@ +#include "shared_context.h" +#include "apdu_constants.h" +#include "utils.h" +#include "ui_flow.h" +#include "common_712.h" +#include "ethUtils.h" + +void handleSignEIP712Message_v0(uint8_t p1, + uint8_t p2, + const uint8_t *workBuffer, + uint8_t dataLength, + unsigned int *flags, + unsigned int *tx) { + (void) tx; + (void) p2; + if (p1 != 00) { + THROW(APDU_RESPONSE_INVALID_P1_P2); + } + if (appState != APP_STATE_IDLE) { + reset_app_context(); + } + + workBuffer = parseBip32(workBuffer, &dataLength, &tmpCtx.messageSigningContext.bip32); + + if ((workBuffer == NULL) || (dataLength < (KECCAK256_HASH_BYTESIZE * 2))) { + THROW(APDU_RESPONSE_INVALID_DATA); + } + memmove(tmpCtx.messageSigningContext712.domainHash, workBuffer, KECCAK256_HASH_BYTESIZE); + memmove(tmpCtx.messageSigningContext712.messageHash, + workBuffer + KECCAK256_HASH_BYTESIZE, + KECCAK256_HASH_BYTESIZE); + +#ifdef NO_CONSENT + io_seproxyhal_touch_signMessage_ok(NULL); +#else // NO_CONSENT + ux_flow_init(0, ux_sign_712_v0_flow, NULL); +#endif // NO_CONSENT + + *flags |= IO_ASYNCH_REPLY; +} diff --git a/src_features/signTx/cmd_signTx.c b/src_features/signTx/cmd_signTx.c index 7a12527..d42631b 100644 --- a/src_features/signTx/cmd_signTx.c +++ b/src_features/signTx/cmd_signTx.c @@ -6,7 +6,7 @@ void handleSign(uint8_t p1, uint8_t p2, const uint8_t *workBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { UNUSED(tx); diff --git a/src_features/stark_getPublicKey/cmd_stark_getPublicKey.c b/src_features/stark_getPublicKey/cmd_stark_getPublicKey.c index 24cfa19..7603188 100644 --- a/src_features/stark_getPublicKey/cmd_stark_getPublicKey.c +++ b/src_features/stark_getPublicKey/cmd_stark_getPublicKey.c @@ -10,7 +10,7 @@ void handleStarkwareGetPublicKey(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, unsigned int *tx) { bip32_path_t bip32; diff --git a/src_features/stark_provideQuantum/cmd_stark_provideQuantum.c b/src_features/stark_provideQuantum/cmd_stark_provideQuantum.c index 900221d..1755c93 100644 --- a/src_features/stark_provideQuantum/cmd_stark_provideQuantum.c +++ b/src_features/stark_provideQuantum/cmd_stark_provideQuantum.c @@ -8,7 +8,7 @@ void handleStarkwareProvideQuantum(uint8_t p1, __attribute__((unused)) uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, __attribute__((unused)) unsigned int *flags, __attribute__((unused)) unsigned int *tx) { size_t i = 0; diff --git a/src_features/stark_sign/cmd_stark_sign.c b/src_features/stark_sign/cmd_stark_sign.c index e27c2a0..311d9d7 100644 --- a/src_features/stark_sign/cmd_stark_sign.c +++ b/src_features/stark_sign/cmd_stark_sign.c @@ -15,7 +15,7 @@ void handleStarkwareSignMessage(uint8_t p1, uint8_t p2, uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, __attribute__((unused)) unsigned int *tx) { uint8_t privateKeyData[INT256_LENGTH]; diff --git a/src_features/stark_unsafe_sign/cmd_stark_unsafe_sign.c b/src_features/stark_unsafe_sign/cmd_stark_unsafe_sign.c index d636654..314fb22 100644 --- a/src_features/stark_unsafe_sign/cmd_stark_unsafe_sign.c +++ b/src_features/stark_unsafe_sign/cmd_stark_unsafe_sign.c @@ -9,7 +9,7 @@ void handleStarkwareUnsafeSign(uint8_t p1, uint8_t p2, const uint8_t *dataBuffer, - uint16_t dataLength, + uint8_t dataLength, unsigned int *flags, __attribute__((unused)) unsigned int *tx) { uint8_t privateKeyData[INT256_LENGTH]; diff --git a/tests/ragger/.gitignore b/tests/ragger/.gitignore new file mode 100644 index 0000000..93526df --- /dev/null +++ b/tests/ragger/.gitignore @@ -0,0 +1,2 @@ +venv/ +__pycache__/ diff --git a/tests/ragger/conftest.py b/tests/ragger/conftest.py new file mode 100644 index 0000000..de165db --- /dev/null +++ b/tests/ragger/conftest.py @@ -0,0 +1,68 @@ +import pytest +from pathlib import Path +from ragger import Firmware +from ragger.backend import SpeculosBackend, LedgerCommBackend, LedgerWalletBackend, BackendInterface +from ethereum_client.client import EthereumClient + +FWS = [ + Firmware("nanos", "2.1"), + Firmware("nanox", "2.0.2"), + Firmware("nanosp", "1.0.3") +] + +def pytest_addoption(parser): + parser.addoption("--backend", action="store", default="speculos") + parser.addoption("--path", action="store", default="./elfs") + parser.addoption("--model", action="store", required=True) + +# accessing the value of the "--backend" option as a fixture +@pytest.fixture +def arg_backend(pytestconfig) -> str: + return pytestconfig.getoption("backend") + +@pytest.fixture +def arg_path(pytestconfig) -> str: + return pytestconfig.getoption("path") + +@pytest.fixture +def arg_model(pytestconfig) -> str: + return pytestconfig.getoption("model") + +# Providing the firmware as a fixture +@pytest.fixture +def firmware(arg_model: str) -> Firmware: + for fw in FWS: + if fw.device == arg_model: + return fw + raise ValueError("Unknown device model \"%s\"" % (arg_model)) + +def get_elf_path(arg_path: str, firmware: Firmware) -> Path: + elf_dir = Path(arg_path).resolve() + assert elf_dir.is_dir(), ("%s is not a directory" % (arg_path)) + app = elf_dir / ("app-%s.elf" % firmware.device) + assert app.is_file(), ("Firmware %s does not exist !" % (app)) + return app + +# Depending on the "--backend" option value, a different backend is +# instantiated, and the tests will either run on Speculos or on a physical +# device depending on the backend +def create_backend(backend: str, arg_path: str, firmware: Firmware) -> BackendInterface: + if backend.lower() == "ledgercomm": + return LedgerCommBackend(firmware, interface="hid") + elif backend.lower() == "ledgerwallet": + return LedgerWalletBackend(firmware) + elif backend.lower() == "speculos": + return SpeculosBackend(get_elf_path(arg_path, firmware), firmware) + else: + raise ValueError(f"Backend '{backend}' is unknown. Valid backends are: {BACKENDS}") + +# This fixture will create and return the backend client +@pytest.fixture +def backend_client(arg_backend: str, arg_path: str, firmware: Firmware) -> BackendInterface: + with create_backend(arg_backend, arg_path, firmware) as b: + yield b + +# This final fixture will return the properly configured app client, to be used in tests +@pytest.fixture +def app_client(backend_client: BackendInterface) -> EthereumClient: + return EthereumClient(backend_client) diff --git a/tests/ragger/eip712/InputData.py b/tests/ragger/eip712/InputData.py new file mode 100644 index 0000000..89e877c --- /dev/null +++ b/tests/ragger/eip712/InputData.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 + +import os +import json +import sys +import re +import hashlib +from ecdsa import SigningKey +from ecdsa.util import sigencode_der +from ethereum_client.client import EthereumClient, EIP712FieldType +import base64 + +# global variables +app_client: EthereumClient = None +filtering_paths = None +current_path = list() +sig_ctx = {} + + + + +# From a string typename, extract the type and all the array depth +# Input = "uint8[2][][4]" | "bool" +# Output = ('uint8', [2, None, 4]) | ('bool', []) +def get_array_levels(typename): + array_lvls = list() + regex = re.compile("(.*)\[([0-9]*)\]$") + + while True: + result = regex.search(typename) + if not result: + break + typename = result.group(1) + + level_size = result.group(2) + if len(level_size) == 0: + level_size = None + else: + level_size = int(level_size) + array_lvls.insert(0, level_size) + return (typename, array_lvls) + + +# From a string typename, extract the type and its size +# Input = "uint64" | "string" +# Output = ('uint', 64) | ('string', None) +def get_typesize(typename): + regex = re.compile("^(\w+?)(\d*)$") + result = regex.search(typename) + typename = result.group(1) + typesize = result.group(2) + if len(typesize) == 0: + typesize = None + else: + typesize = int(typesize) + return (typename, typesize) + + + +def parse_int(typesize): + return (EIP712FieldType.INT, int(typesize / 8)) + +def parse_uint(typesize): + return (EIP712FieldType.UINT, int(typesize / 8)) + +def parse_address(typesize): + return (EIP712FieldType.ADDRESS, None) + +def parse_bool(typesize): + return (EIP712FieldType.BOOL, None) + +def parse_string(typesize): + return (EIP712FieldType.STRING, None) + +def parse_bytes(typesize): + if typesize != None: + return (EIP712FieldType.FIX_BYTES, typesize) + return (EIP712FieldType.DYN_BYTES, None) + +# set functions for each type +parsing_type_functions = {}; +parsing_type_functions["int"] = parse_int +parsing_type_functions["uint"] = parse_uint +parsing_type_functions["address"] = parse_address +parsing_type_functions["bool"] = parse_bool +parsing_type_functions["string"] = parse_string +parsing_type_functions["bytes"] = parse_bytes + + + +def send_struct_def_field(typename, keyname): + type_enum = None + + (typename, array_lvls) = get_array_levels(typename) + (typename, typesize) = get_typesize(typename) + + if typename in parsing_type_functions.keys(): + (type_enum, typesize) = parsing_type_functions[typename](typesize) + else: + type_enum = EIP712FieldType.CUSTOM + typesize = None + + app_client.eip712_send_struct_def_struct_field(type_enum, + typename, + typesize, + array_lvls, + keyname) + return (typename, type_enum, typesize, array_lvls) + + + +def encode_integer(value, typesize): + data = bytearray() + + # Some are already represented as integers in the JSON, but most as strings + if isinstance(value, str): + base = 10 + if value.startswith("0x"): + base = 16 + value = int(value, base) + + if value == 0: + data.append(0) + else: + if value < 0: # negative number, send it as unsigned + mask = 0 + for i in range(typesize): # make a mask as big as the typesize + mask = (mask << 8) | 0xff + value &= mask + while value > 0: + data.append(value & 0xff) + value >>= 8 + data.reverse() + return data + +def encode_int(value, typesize): + return encode_integer(value, typesize) + +def encode_uint(value, typesize): + return encode_integer(value, typesize) + +def encode_hex_string(value, size): + data = bytearray() + value = value[2:] # skip 0x + byte_idx = 0 + while byte_idx < size: + data.append(int(value[(byte_idx * 2):(byte_idx * 2 + 2)], 16)) + byte_idx += 1 + return data + +def encode_address(value, typesize): + return encode_hex_string(value, 20) + +def encode_bool(value, typesize): + return encode_integer(value, typesize) + +def encode_string(value, typesize): + data = bytearray() + for char in value: + data.append(ord(char)) + return data + +def encode_bytes_fix(value, typesize): + return encode_hex_string(value, typesize) + +def encode_bytes_dyn(value, typesize): + # length of the value string + # - the length of 0x (2) + # / by the length of one byte in a hex string (2) + return encode_hex_string(value, int((len(value) - 2) / 2)) + +# set functions for each type +encoding_functions = {} +encoding_functions[EIP712FieldType.INT] = encode_int +encoding_functions[EIP712FieldType.UINT] = encode_uint +encoding_functions[EIP712FieldType.ADDRESS] = encode_address +encoding_functions[EIP712FieldType.BOOL] = encode_bool +encoding_functions[EIP712FieldType.STRING] = encode_string +encoding_functions[EIP712FieldType.FIX_BYTES] = encode_bytes_fix +encoding_functions[EIP712FieldType.DYN_BYTES] = encode_bytes_dyn + + + +def send_struct_impl_field(value, field): + # Something wrong happened if this triggers + if isinstance(value, list) or (field["enum"] == EIP712FieldType.CUSTOM): + breakpoint() + + data = encoding_functions[field["enum"]](value, field["typesize"]) + + + if filtering_paths: + path = ".".join(current_path) + if path in filtering_paths.keys(): + send_filtering_show_field(filtering_paths[path]) + + app_client.eip712_send_struct_impl_struct_field(data) + + + +def evaluate_field(structs, data, field, lvls_left, new_level = True): + array_lvls = field["array_lvls"] + + if new_level: + current_path.append(field["name"]) + if len(array_lvls) > 0 and lvls_left > 0: + app_client.eip712_send_struct_impl_array(len(data)) + idx = 0 + for subdata in data: + current_path.append("[]") + if not evaluate_field(structs, subdata, field, lvls_left - 1, False): + return False + current_path.pop() + idx += 1 + if array_lvls[lvls_left - 1] != None: + if array_lvls[lvls_left - 1] != idx: + print("Mismatch in array size! Got %d, expected %d\n" % + (idx, array_lvls[lvls_left - 1]), + file=sys.stderr) + return False + else: + if field["enum"] == EIP712FieldType.CUSTOM: + if not send_struct_impl(structs, data, field["type"]): + return False + else: + send_struct_impl_field(data, field) + if new_level: + current_path.pop() + return True + + + +def send_struct_impl(structs, data, structname): + # Check if it is a struct we don't known + if structname not in structs.keys(): + return False + + struct = structs[structname] + for f in struct: + if not evaluate_field(structs, data[f["name"]], f, len(f["array_lvls"])): + return False + return True + +# ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures +def send_filtering_message_info(display_name: str, filters_count: int): + global sig_ctx + + to_sign = bytearray() + to_sign.append(183) + to_sign += sig_ctx["chainid"] + to_sign += sig_ctx["caddr"] + to_sign += sig_ctx["schema_hash"] + to_sign.append(filters_count) + for char in display_name: + to_sign.append(ord(char)) + + sig = sig_ctx["key"].sign_deterministic(to_sign, sigencode=sigencode_der) + app_client.eip712_filtering_message_info(display_name, filters_count, sig) + +# ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures +def send_filtering_show_field(display_name): + global sig_ctx + + path_str = ".".join(current_path) + + to_sign = bytearray() + to_sign.append(72) + to_sign += sig_ctx["chainid"] + to_sign += sig_ctx["caddr"] + to_sign += sig_ctx["schema_hash"] + for char in path_str: + to_sign.append(ord(char)) + for char in display_name: + to_sign.append(ord(char)) + sig = sig_ctx["key"].sign_deterministic(to_sign, sigencode=sigencode_der) + app_client.eip712_filtering_show_field(display_name, sig) + +def read_filtering_file(domain, message, filtering_file_path): + data_json = None + with open(filtering_file_path) as data: + data_json = json.load(data) + return data_json + +def prepare_filtering(filtr_data, message): + global filtering_paths + + if "fields" in filtr_data: + filtering_paths = filtr_data["fields"] + else: + filtering_paths = {} + +def handle_optional_domain_values(domain): + if "chainId" not in domain.keys(): + domain["chainId"] = 0 + if "verifyingContract" not in domain.keys(): + domain["verifyingContract"] = "0x0000000000000000000000000000000000000000" + +def init_signature_context(types, domain): + global sig_ctx + + handle_optional_domain_values(domain) + env_key = os.environ["CAL_SIGNATURE_TEST_KEY"] + key = base64.b64decode(env_key).decode() # base 64 string -> decode bytes -> string + sig_ctx["key"] = SigningKey.from_pem(key, hashlib.sha256) + caddr = domain["verifyingContract"] + if caddr.startswith("0x"): + caddr = caddr[2:] + sig_ctx["caddr"] = bytearray.fromhex(caddr) + chainid = domain["chainId"] + sig_ctx["chainid"] = bytearray() + for i in range(8): + sig_ctx["chainid"].append(chainid & (0xff << (i * 8))) + sig_ctx["chainid"].reverse() + schema_str = json.dumps(types).replace(" ","") + schema_hash = hashlib.sha224(schema_str.encode()) + sig_ctx["schema_hash"] = bytearray.fromhex(schema_hash.hexdigest()) + +def process_file(aclient: EthereumClient, input_file_path: str, filtering_file_path = None) -> bool: + global sig_ctx + global app_client + + app_client = aclient + with open(input_file_path, "r") as data: + data_json = json.load(data) + domain_typename = "EIP712Domain" + message_typename = data_json["primaryType"] + types = data_json["types"] + domain = data_json["domain"] + message = data_json["message"] + + if filtering_file_path: + init_signature_context(types, domain) + filtr = read_filtering_file(domain, message, filtering_file_path) + + # send types definition + for key in types.keys(): + app_client.eip712_send_struct_def_struct_name(key) + for f in types[key]: + (f["type"], f["enum"], f["typesize"], f["array_lvls"]) = \ + send_struct_def_field(f["type"], f["name"]) + + if filtering_file_path: + app_client.eip712_filtering_activate() + prepare_filtering(filtr, message) + + # send domain implementation + app_client.eip712_send_struct_impl_root_struct(domain_typename) + if not send_struct_impl(types, domain, domain_typename): + return False + + if filtering_file_path: + if filtr and "name" in filtr: + send_filtering_message_info(filtr["name"], len(filtering_paths)) + else: + send_filtering_message_info(domain["name"], len(filtering_paths)) + + # send message implementation + app_client.eip712_send_struct_impl_root_struct(message_typename) + if not send_struct_impl(types, message, message_typename): + return False + + return True diff --git a/tests/ragger/eip712/__init__.py b/tests/ragger/eip712/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ragger/eip712/input_files/00-simple_mail-data.json b/tests/ragger/eip712/input_files/00-simple_mail-data.json new file mode 100644 index 0000000..0b4fa66 --- /dev/null +++ b/tests/ragger/eip712/input_files/00-simple_mail-data.json @@ -0,0 +1,44 @@ +{ + "domain": { + "chainId": 1, + "name": "Simple Mail", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "contents": "Hello, Bob!", + "from": { + "name": "Cow", + "wallets": [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" + ] + }, + "to": { + "name": "Bob", + "wallets": [ + "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", + "0xB0B0b0b0b0b0B000000000000000000000000000" + ] + } + }, + "primaryType": "Mail", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallets", "type": "address[]" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/00-simple_mail-filter.json b/tests/ragger/eip712/input_files/00-simple_mail-filter.json new file mode 100644 index 0000000..33399d1 --- /dev/null +++ b/tests/ragger/eip712/input_files/00-simple_mail-filter.json @@ -0,0 +1,7 @@ +{ + "name": "Test JSON", + "fields": { + "from.name": "From", + "to.name" : "To" + } +} diff --git a/tests/ragger/eip712/input_files/00-simple_mail.ini b/tests/ragger/eip712/input_files/00-simple_mail.ini new file mode 100644 index 0000000..62d03bd --- /dev/null +++ b/tests/ragger/eip712/input_files/00-simple_mail.ini @@ -0,0 +1,4 @@ +[signature] +v = 1b +r = 23599abd6c4b631e42770c112b5955907fe91339f1ea1e102f7682262ca178b9 +s = 29fc94518588165114b4c4acb4d73e6d028dfb051d90e517b3b4746e04eb0f5f diff --git a/tests/ragger/eip712/input_files/01-addresses_array_mail-data.json b/tests/ragger/eip712/input_files/01-addresses_array_mail-data.json new file mode 100644 index 0000000..cc54d8c --- /dev/null +++ b/tests/ragger/eip712/input_files/01-addresses_array_mail-data.json @@ -0,0 +1,31 @@ +{ + "domain": { + "chainId": 1, + "name": "Addresses Array Mail", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "contents": "Hello, Bob!", + "from": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "to": [ + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", + "0xb1a22cc48f6784f629a994917cd6474923630c48" + ], + "id": 7 + }, + "primaryType": "Mail", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Mail": [ + { "name": "from", "type": "address" }, + { "name": "to", "type": "address[]" }, + { "name": "contents", "type": "string" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/01-addresses_array_mail.ini b/tests/ragger/eip712/input_files/01-addresses_array_mail.ini new file mode 100644 index 0000000..d972658 --- /dev/null +++ b/tests/ragger/eip712/input_files/01-addresses_array_mail.ini @@ -0,0 +1,4 @@ +[signature] +v = 1c +r = 3f084a471e6158bce792287500d62d40061acc1864180ed2da7a704bf3aced0f +s = 3b799ced9e48cda152b4b9a4b7f45e3119dc7acdf16710a73800b4e336fa1b40 diff --git a/tests/ragger/eip712/input_files/02-recipients_array_mail-data.json b/tests/ragger/eip712/input_files/02-recipients_array_mail-data.json new file mode 100644 index 0000000..1d27ff0 --- /dev/null +++ b/tests/ragger/eip712/input_files/02-recipients_array_mail-data.json @@ -0,0 +1,55 @@ +{ + "domain": { + "chainId": 1, + "name": "Recipients Array Mail", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "contents": "Hello, Bob!", + "from": { + "name": "Cow", + "wallets": [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" + ] + }, + "to": [ + { + "name": "Alice", + "wallets": [ + "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "0xB0B0b0b0b0b0B000000000000000000000000000" + ] + }, + { + "name": "Bob", + "wallets": [ + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57" + ] + } + ] + }, + "primaryType": "Mail", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Group": [ + { "name": "name", "type": "string" }, + { "name": "members", "type": "Person[]" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person[]" }, + { "name": "contents", "type": "string" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallets", "type": "address[]" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/02-recipients_array_mail.ini b/tests/ragger/eip712/input_files/02-recipients_array_mail.ini new file mode 100644 index 0000000..8e8dcff --- /dev/null +++ b/tests/ragger/eip712/input_files/02-recipients_array_mail.ini @@ -0,0 +1,4 @@ +[signature] +v = 1b +r = 49dd2aa96d7494e0cd9111f19f87ac50194e4bbc61ea9f4bb86d674da0ae7721 +s = 7a12ddd9083b4caaabd2fb80df6de1d5d926c0e8a73bf371a45e231d409d79d6 diff --git a/tests/ragger/eip712/input_files/03-long_string-data.json b/tests/ragger/eip712/input_files/03-long_string-data.json new file mode 100644 index 0000000..d286934 --- /dev/null +++ b/tests/ragger/eip712/input_files/03-long_string-data.json @@ -0,0 +1,50 @@ +{ + "domain": { + "chainId": 1, + "name": "Long String Mail", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "contents": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam fermentum interdum tortor, nec elementum enim dignissim ac. Proin at leo sit amet nisl ultrices mollis quis a nunc. Aliquam lobortis a libero non lobortis. Morbi elementum eleifend ante et malesuada. Proin eget fermentum risus. Vestibulum cursus dignissim mollis. In viverra, mi ac accumsan elementum, tellus metus dictum nisi, vel tincidunt odio erat in odio. Aenean nec lorem auctor, tempor ante eget, aliquet purus. Donec fringilla felis iaculis, venenatis ligula egestas, dignissim orci. Aliquam id rhoncus ante, cursus consectetur mauris. Nunc porttitor urna urna, et tristique eros maximus vestibulum. Curabitur pretium a est non porttitor. Cras mollis efficitur sem ut porta. Proin commodo volutpat iaculis. Maecenas ut nulla mi. Aenean ultrices sollicitudin enim, non luctus magna efficitur et. Sed varius sem odio, in sodales erat porta in. Nunc blandit finibus maximus. Mauris nunc tellus, interdum non laoreet sed, aliquet non.", + "from": { + "name": "Cow", + "wallets": [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" + ] + }, + "to": [ + { + "name": "Bob", + "wallets": [ + "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", + "0xB0B0b0b0b0b0B000000000000000000000000000" + ] + } + ] + }, + "primaryType": "Mail", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Group": [ + { "name": "name", "type": "string" }, + { "name": "members", "type": "Person[]" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person[]" }, + { "name": "contents", "type": "string" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallets", "type": "address[]" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/03-long_string.ini b/tests/ragger/eip712/input_files/03-long_string.ini new file mode 100644 index 0000000..9b2740a --- /dev/null +++ b/tests/ragger/eip712/input_files/03-long_string.ini @@ -0,0 +1,4 @@ +[signature] +v = 1c +r = b23ffac2cb350fd6e7d06ec4b981fe016d33426d753c870e7e753797cc43bb1f +s = 37948a656fa3403e21956ef10c8d3152f7ce22cc252d958c9f9249435090f426 diff --git a/tests/ragger/eip712/input_files/04-long_bytes-data.json b/tests/ragger/eip712/input_files/04-long_bytes-data.json new file mode 100644 index 0000000..5bcfe55 --- /dev/null +++ b/tests/ragger/eip712/input_files/04-long_bytes-data.json @@ -0,0 +1,50 @@ +{ + "domain": { + "chainId": 1, + "name": "Long Bytes Mail", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "contents": "0x11223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900112233445566778899001122334455667788990011223344556677889900", + "from": { + "name": "Cow", + "wallets": [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" + ] + }, + "to": [ + { + "name": "Bob", + "wallets": [ + "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", + "0xB0B0b0b0b0b0B000000000000000000000000000" + ] + } + ] + }, + "primaryType": "Mail", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Group": [ + { "name": "name", "type": "string" }, + { "name": "members", "type": "Person[]" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person[]" }, + { "name": "contents", "type": "bytes" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallets", "type": "address[]" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/04-long_bytes.ini b/tests/ragger/eip712/input_files/04-long_bytes.ini new file mode 100644 index 0000000..14018b7 --- /dev/null +++ b/tests/ragger/eip712/input_files/04-long_bytes.ini @@ -0,0 +1,4 @@ +[signature] +v = 1b +r = db18ea1b9757773385138d0802fb2f8107c3e45882962b8e0c6789eccdbfab05 +s = 3d66d4dee47916fb7fec39a538ad8d5e94fbc92f99327410716180ab07591218 diff --git a/tests/ragger/eip712/input_files/05-signed_ints-data.json b/tests/ragger/eip712/input_files/05-signed_ints-data.json new file mode 100644 index 0000000..a6279d6 --- /dev/null +++ b/tests/ragger/eip712/input_files/05-signed_ints-data.json @@ -0,0 +1,45 @@ +{ + "domain": { + "chainId": 1, + "name": "Signed Ints test", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "neg256" : "-256", + "pos256" : "256", + "neg128" : "-128", + "pos128" : "128", + "neg64" : "-64", + "pos64" : "64", + "neg32" : "-32", + "pos32" : "32", + "neg16" : "-16", + "pos16" : "16", + "neg8" : "-8", + "pos8" : "8" + }, + "primaryType": "Test", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Test": [ + { "name": "neg256", "type": "int256" }, + { "name": "pos256", "type": "int256" }, + { "name": "neg128", "type": "int128" }, + { "name": "pos128", "type": "int128" }, + { "name": "neg64", "type": "int64" }, + { "name": "pos64", "type": "int64" }, + { "name": "neg32", "type": "int32" }, + { "name": "pos32", "type": "int32" }, + { "name": "neg16", "type": "int16" }, + { "name": "pos16", "type": "int16" }, + { "name": "neg8", "type": "int8" }, + { "name": "pos8", "type": "int8" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/05-signed_ints.ini b/tests/ragger/eip712/input_files/05-signed_ints.ini new file mode 100644 index 0000000..754b2b3 --- /dev/null +++ b/tests/ragger/eip712/input_files/05-signed_ints.ini @@ -0,0 +1,4 @@ +[signature] +v = 1c +r = 50fb2861367daf3b5b73cac277e698b27bf7627a462fade1acb5a2ef285ba8ae +s = 131a62515a0a5c4b35c5cb672b46f562151c45508d8efcdf78c4608bc14c5f30 diff --git a/tests/ragger/eip712/input_files/06-boolean-data.json b/tests/ragger/eip712/input_files/06-boolean-data.json new file mode 100644 index 0000000..6b29025 --- /dev/null +++ b/tests/ragger/eip712/input_files/06-boolean-data.json @@ -0,0 +1,25 @@ +{ + "domain": { + "chainId": 1, + "name": "Boolean test", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "Bueno" : true, + "NoBueno": false + }, + "primaryType": "Test", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Test": [ + { "name": "Bueno", "type": "bool" }, + { "name": "NoBueno", "type": "bool" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/06-boolean.ini b/tests/ragger/eip712/input_files/06-boolean.ini new file mode 100644 index 0000000..e7de2b3 --- /dev/null +++ b/tests/ragger/eip712/input_files/06-boolean.ini @@ -0,0 +1,4 @@ +[signature] +v = 1c +r = 929681d77ed88cd1adef57185a0cd7b3a268aca5d4122b8c0acfd2ce4c0afb18 +s = 5afe8e3004c182b6b02fe7559c26f20f4133ad9b17223658ccd9061b33b021cf diff --git a/tests/ragger/eip712/input_files/07-fixed_bytes-data.json b/tests/ragger/eip712/input_files/07-fixed_bytes-data.json new file mode 100644 index 0000000..bf2db40 --- /dev/null +++ b/tests/ragger/eip712/input_files/07-fixed_bytes-data.json @@ -0,0 +1,31 @@ +{ + "domain": { + "chainId": 1, + "name": "Fixed bytes test", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "val1": "0xae", + "val4": "0x973bb640", + "val8": "0xac3608fa074a22a0", + "val16": "0x24e62129cc3ed3df6f8f3cd1e95b812a", + "val32": "0xb5d679d10bf948280080e802ce9fde218b0f8c442c47bf4ab05657d8da04d1da" + }, + "primaryType": "Test", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Test": [ + { "name": "val1", "type": "bytes1" }, + { "name": "val4", "type": "bytes4" }, + { "name": "val8", "type": "bytes8" }, + { "name": "val16", "type": "bytes16" }, + { "name": "val32", "type": "bytes32" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/07-fixed_bytes.ini b/tests/ragger/eip712/input_files/07-fixed_bytes.ini new file mode 100644 index 0000000..e2f661e --- /dev/null +++ b/tests/ragger/eip712/input_files/07-fixed_bytes.ini @@ -0,0 +1,4 @@ +[signature] +v = 1b +r = e021d88afc50079b0341b01193c4687c47b85bcd6749fe69e0b87521d65a1847 +s = 5b7670d2a67c781a11164920403db0f7707161e81d88226cdbf91298390dfeda diff --git a/tests/ragger/eip712/input_files/08-opensea-data.json b/tests/ragger/eip712/input_files/08-opensea-data.json new file mode 100644 index 0000000..35bd77a --- /dev/null +++ b/tests/ragger/eip712/input_files/08-opensea-data.json @@ -0,0 +1,153 @@ +{ + "domain" : { + "chainId" : 1, + "name" : "Wyvern Exchange Contract", + "verifyingContract" : "0x7f268357a8c2552623316e2562d90e642bb538e5", + "version" : "2.3" + }, + "message" : { + "basePrice" : "2000000000000000000", + "calldata" : "0x96809f90000000000000000000000000112f0732e59e7600768dfc35ba744b89f2356cd80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000495f947276749ce646f68ac8c248420045cb7b5ebdf2657ffc1fadfd73cf0a8cde95d50b62d3df8c0000000000000700000000320000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000", + "exchange" : "0x7f268357a8c2552623316e2562d90e642bb538e5", + "expirationTime" : "1646089435", + "extra" : "0", + "feeMethod" : 1, + "feeRecipient" : "0x5b3256965e7c3cf26e11fcaf296dfc8807c01073", + "howToCall" : 1, + "listingTime" : "1645484541", + "maker" : "0x112f0732e59e7600768dfc35ba744b89f2356cd8", + "makerProtocolFee" : "0", + "makerRelayerFee" : "1250", + "nonce" : 0, + "paymentToken" : "0x0000000000000000000000000000000000000000", + "replacementPattern" : "0x000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "saleKind" : 0, + "salt" : "21014297276898013168171430966355369260039074692095359200549020767078729356431", + "side" : 1, + "staticExtradata" : "0x", + "staticTarget" : "0x0000000000000000000000000000000000000000", + "taker" : "0x0000000000000000000000000000000000000000", + "takerProtocolFee" : "0", + "takerRelayerFee" : "0", + "target" : "0xbaf2127b49fc93cbca6269fade0f7f31df4c88a7" + }, + "primaryType" : "Order", + "types" : { + "EIP712Domain" : [ + { + "name" : "name", + "type" : "string" + }, + { + "name" : "version", + "type" : "string" + }, + { + "name" : "chainId", + "type" : "uint256" + }, + { + "name" : "verifyingContract", + "type" : "address" + } + ], + "Order" : [ + { + "name" : "exchange", + "type" : "address" + }, + { + "name" : "maker", + "type" : "address" + }, + { + "name" : "taker", + "type" : "address" + }, + { + "name" : "makerRelayerFee", + "type" : "uint256" + }, + { + "name" : "takerRelayerFee", + "type" : "uint256" + }, + { + "name" : "makerProtocolFee", + "type" : "uint256" + }, + { + "name" : "takerProtocolFee", + "type" : "uint256" + }, + { + "name" : "feeRecipient", + "type" : "address" + }, + { + "name" : "feeMethod", + "type" : "uint8" + }, + { + "name" : "side", + "type" : "uint8" + }, + { + "name" : "saleKind", + "type" : "uint8" + }, + { + "name" : "target", + "type" : "address" + }, + { + "name" : "howToCall", + "type" : "uint8" + }, + { + "name" : "calldata", + "type" : "bytes" + }, + { + "name" : "replacementPattern", + "type" : "bytes" + }, + { + "name" : "staticTarget", + "type" : "address" + }, + { + "name" : "staticExtradata", + "type" : "bytes" + }, + { + "name" : "paymentToken", + "type" : "address" + }, + { + "name" : "basePrice", + "type" : "uint256" + }, + { + "name" : "extra", + "type" : "uint256" + }, + { + "name" : "listingTime", + "type" : "uint256" + }, + { + "name" : "expirationTime", + "type" : "uint256" + }, + { + "name" : "salt", + "type" : "uint256" + }, + { + "name" : "nonce", + "type" : "uint256" + } + ] + } +} diff --git a/tests/ragger/eip712/input_files/08-opensea-filter.json b/tests/ragger/eip712/input_files/08-opensea-filter.json new file mode 100644 index 0000000..6f1d9f5 --- /dev/null +++ b/tests/ragger/eip712/input_files/08-opensea-filter.json @@ -0,0 +1,9 @@ +{ + "name": "OpenSea", + "fields": { + "maker": "Maker", + "taker": "Taker", + "basePrice": "Base Price", + "expirationTime": "Expiration Time" + } +} diff --git a/tests/ragger/eip712/input_files/08-opensea.ini b/tests/ragger/eip712/input_files/08-opensea.ini new file mode 100644 index 0000000..dfef6bb --- /dev/null +++ b/tests/ragger/eip712/input_files/08-opensea.ini @@ -0,0 +1,4 @@ +[signature] +v = 1b +r = 1539547ae7cf8ebcd3eabfb57cd2b1fb7775ce757c3f4a307c7425d35b7bfff7 +s = 47248cb61e554c1f90af6331d9c9e51cbb8655667514194f509abe097a032319 diff --git a/tests/ragger/eip712/input_files/09-rarible-data.json b/tests/ragger/eip712/input_files/09-rarible-data.json new file mode 100644 index 0000000..f5284a6 --- /dev/null +++ b/tests/ragger/eip712/input_files/09-rarible-data.json @@ -0,0 +1,110 @@ +{ + "domain" : { + "chainId" : 1, + "name" : "Exchange", + "verifyingContract" : "0x9757f2d2b135150bbeb65308d4a91804107cd8d6", + "version" : "2" + }, + "message" : { + "data" : "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000001cf0df2a5a20cd61d68d4489eebbf85b8d39e18a00000000000000000000000000000000000000000000000000000000000000fa", + "dataType" : "0x23d235ef", + "end" : 0, + "makeAsset" : { + "assetType" : { + "assetClass" : "0x973bb640", + "data" : "0x000000000000000000000000495f947276749ce646f68ac8c248420045cb7b5ebdf2657ffc1fadfd73cf0a8cde95d50b62d3df8c000000000000070000000032" + }, + "value" : "1" + }, + "maker" : "0x112f0732e59e7600768dfc35ba744b89f2356cd8", + "salt" : "0xdbf0f98bc1746711579dcce549a4cc4e866fb71bf2e185bfefbb7d32f325972e", + "start" : 0, + "takeAsset" : { + "assetType" : { + "assetClass" : "0xaaaebeba", + "data" : "0x" + }, + "value" : "2000000000000000000" + }, + "taker" : "0x0000000000000000000000000000000000000000" + }, + "primaryType" : "Order", + "types" : { + "Asset" : [ + { + "name" : "assetType", + "type" : "AssetType" + }, + { + "name" : "value", + "type" : "uint256" + } + ], + "AssetType" : [ + { + "name" : "assetClass", + "type" : "bytes4" + }, + { + "name" : "data", + "type" : "bytes" + } + ], + "EIP712Domain" : [ + { + "name" : "name", + "type" : "string" + }, + { + "name" : "version", + "type" : "string" + }, + { + "name" : "chainId", + "type" : "uint256" + }, + { + "name" : "verifyingContract", + "type" : "address" + } + ], + "Order" : [ + { + "name" : "maker", + "type" : "address" + }, + { + "name" : "makeAsset", + "type" : "Asset" + }, + { + "name" : "taker", + "type" : "address" + }, + { + "name" : "takeAsset", + "type" : "Asset" + }, + { + "name" : "salt", + "type" : "uint256" + }, + { + "name" : "start", + "type" : "uint256" + }, + { + "name" : "end", + "type" : "uint256" + }, + { + "name" : "dataType", + "type" : "bytes4" + }, + { + "name" : "data", + "type" : "bytes" + } + ] + } +} diff --git a/tests/ragger/eip712/input_files/09-rarible.ini b/tests/ragger/eip712/input_files/09-rarible.ini new file mode 100644 index 0000000..d880f0c --- /dev/null +++ b/tests/ragger/eip712/input_files/09-rarible.ini @@ -0,0 +1,4 @@ +[signature] +v = 1c +r = 341bca1c0dfd805d4befc21500084424dbe559c7aafd78d8fb461c0c76dfea1d +s = 33ebb7b6fe0691961cd8b263faac20ecbbdcaef3febb57eb76614cad629080ea diff --git a/tests/ragger/eip712/input_files/10-multidimensional_arrays-data.json b/tests/ragger/eip712/input_files/10-multidimensional_arrays-data.json new file mode 100644 index 0000000..716193b --- /dev/null +++ b/tests/ragger/eip712/input_files/10-multidimensional_arrays-data.json @@ -0,0 +1,42 @@ +{ + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Test": [ + { "type": "uint8[][][][]", "name": "depthy" } + ] + }, + "primaryType": "Test", + "domain": { + "chainId": 1, + "name": "Depth Test", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "depthy": [ + [ + [ + [ + "1", + "2" + ], + [ + "3" + ] + ], + [ + [ + "4", + "5", + "6" + ] + ] + ] + ] + } +} diff --git a/tests/ragger/eip712/input_files/10-multidimensional_arrays.ini b/tests/ragger/eip712/input_files/10-multidimensional_arrays.ini new file mode 100644 index 0000000..63113be --- /dev/null +++ b/tests/ragger/eip712/input_files/10-multidimensional_arrays.ini @@ -0,0 +1,4 @@ +[signature] +v = 1b +r = d11a91bdf7836288818875d046452061d565cc6dc1bf3dd6216ab27ef9a2844f +s = 4f6bda8ac4c39721aff7ae08989897ede9d573085a192d03ab0eb7735d2ef403 diff --git a/tests/ragger/eip712/input_files/11-complex_structs-data.json b/tests/ragger/eip712/input_files/11-complex_structs-data.json new file mode 100644 index 0000000..a3ad755 --- /dev/null +++ b/tests/ragger/eip712/input_files/11-complex_structs-data.json @@ -0,0 +1,78 @@ +{ + "domain": { + "chainId": 1, + "name": "Complex Structs Mail", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + "version": "1" + }, + "message": { + "contents": "Hello, Bob!", + "from": { + "name": "Cow", + "wallets": [ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" + ] + }, + "to": { + "name": "test list", + "members": [ + { + "name": "Bob", + "wallets": [ + "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "0xB0B0b0b0b0b0B000000000000000000000000000" + ] + }, + { + "name": "Alice", + "wallets": [ + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57" + ] + } + ] + }, + "attach": { + "list": [ + { + "name": "first", + "size": "100" + }, + { + "name": "second", + "size": "3400" + } + ] + } + }, + "primaryType": "Mail", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "Attachment": [ + { "name": "name", "type": "string" }, + { "name": "size", "type": "uint16" } + ], + "Attachments": [ + { "name": "list", "type": "Attachment[]" } + ], + "MailingList": [ + { "name": "name", "type": "string" }, + { "name": "members", "type": "Person[]" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "MailingList" }, + { "name": "contents", "type": "string" }, + { "name": "attach", "type": "Attachments" } + ], + "Person": [ + { "name": "name", "type": "string" }, + { "name": "wallets", "type": "address[]" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/11-complex_structs-filter.json b/tests/ragger/eip712/input_files/11-complex_structs-filter.json new file mode 100644 index 0000000..a38240d --- /dev/null +++ b/tests/ragger/eip712/input_files/11-complex_structs-filter.json @@ -0,0 +1,9 @@ +{ + "name": "Depthy Test", + "fields": { + "contents": "Message", + "from.name": "Sender", + "to.members.[].name": "Recipient", + "attach.list.[].name": "Attachment" + } +} diff --git a/tests/ragger/eip712/input_files/11-complex_structs.ini b/tests/ragger/eip712/input_files/11-complex_structs.ini new file mode 100644 index 0000000..ec35ea3 --- /dev/null +++ b/tests/ragger/eip712/input_files/11-complex_structs.ini @@ -0,0 +1,4 @@ +[signature] +v = 1c +r = cce2e63aaac6a5f9a74684d8fdddcbc7f3b27aa17235bfab89226821ead933b6 +s = 3f3c93977abcc3f8cc9a3dc1ecc02dbca14aca1a6ecb2fb6ca3d7c713ace1ec4 diff --git a/tests/ragger/eip712/input_files/12-sign_in-data.json b/tests/ragger/eip712/input_files/12-sign_in-data.json new file mode 100644 index 0000000..1792e88 --- /dev/null +++ b/tests/ragger/eip712/input_files/12-sign_in-data.json @@ -0,0 +1,23 @@ +{ + "domain": { + "name": "Who are You?", + "version": "1" + }, + "message": { + "banner": "Please sign this message with your wallet", + "curDate": 1660659773, + "id": 38 + }, + "primaryType": "Auth", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" } + ], + "Auth": [ + { "name": "banner", "type": "string" }, + { "name": "curDate", "type": "uint32" }, + { "name": "id", "type": "uint8" } + ] + } +} diff --git a/tests/ragger/eip712/input_files/12-sign_in-filter.json b/tests/ragger/eip712/input_files/12-sign_in-filter.json new file mode 100644 index 0000000..69da4f1 --- /dev/null +++ b/tests/ragger/eip712/input_files/12-sign_in-filter.json @@ -0,0 +1,7 @@ +{ + "name": "Ethereum sign-in", + "fields": { + "curDate": "Timestamp", + "id": "Identifier" + } +} diff --git a/tests/ragger/eip712/input_files/12-sign_in.ini b/tests/ragger/eip712/input_files/12-sign_in.ini new file mode 100644 index 0000000..83047ab --- /dev/null +++ b/tests/ragger/eip712/input_files/12-sign_in.ini @@ -0,0 +1,4 @@ +[signature] +v = 1b +r = 7be1671577753c13bfd1da8b234b6df8484daf47351c2366637fd291dd4aa4d9 +s = 1a7ffbb01dc8a64e9ee97d19b8f154e9eecbe0b1bfb9dcfa781a65e474573963 diff --git a/tests/ragger/ethereum_client/__init__.py b/tests/ragger/ethereum_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ragger/ethereum_client/client.py b/tests/ragger/ethereum_client/client.py new file mode 100644 index 0000000..61b2012 --- /dev/null +++ b/tests/ragger/ethereum_client/client.py @@ -0,0 +1,156 @@ +from enum import IntEnum, auto +from typing import Iterator, Dict, List +from ragger.backend import BackendInterface +from ragger.utils import RAPDU +from ethereum_client.command_builder import EthereumCmdBuilder +from ethereum_client.setting import SettingType, SettingImpl +from ethereum_client.eip712 import EIP712FieldType +from ethereum_client.response_parser import EthereumRespParser +import signal + + +class EthereumClient: + _settings: Dict[SettingType, SettingImpl] = { + SettingType.BLIND_SIGNING: SettingImpl( + [ "nanos", "nanox", "nanosp" ] + ), + SettingType.DEBUG_DATA: SettingImpl( + [ "nanos", "nanox", "nanosp" ] + ), + SettingType.NONCE: SettingImpl( + [ "nanos", "nanox", "nanosp" ] + ), + SettingType.VERBOSE_EIP712: SettingImpl( + [ "nanox", "nanosp" ] + ) + } + _click_delay = 1/4 + _eip712_filtering = False + + def __init__(self, client: BackendInterface): + self._client = client + self._cmd_builder = EthereumCmdBuilder() + self._resp_parser = EthereumRespParser() + signal.signal(signal.SIGALRM, self._click_signal_timeout) + for setting in self._settings.values(): + setting.value = False + + def _send(self, payload: bytearray): + return self._client.exchange_async_raw(payload) + + def _recv(self) -> RAPDU: + return self._client._last_async_response + + def _click_signal_timeout(self, _signum: int, _frame): + self._client.right_click() + + def _enable_click_until_response(self): + signal.setitimer(signal.ITIMER_REAL, + self._click_delay, + self._click_delay) + + def _disable_click_until_response(self): + signal.setitimer(signal.ITIMER_REAL, 0, 0) + + def eip712_send_struct_def_struct_name(self, name: str): + with self._send(self._cmd_builder.eip712_send_struct_def_struct_name(name)): + pass + return self._recv().status == 0x9000 + + def eip712_send_struct_def_struct_field(self, + field_type: EIP712FieldType, + type_name: str, + type_size: int, + array_levels: [], + key_name: str): + with self._send(self._cmd_builder.eip712_send_struct_def_struct_field( + field_type, + type_name, + type_size, + array_levels, + key_name)): + pass + return self._recv() + + def eip712_send_struct_impl_root_struct(self, name: str): + with self._send(self._cmd_builder.eip712_send_struct_impl_root_struct(name)): + self._enable_click_until_response() + self._disable_click_until_response() + return self._recv() + + def eip712_send_struct_impl_array(self, size: int): + with self._send(self._cmd_builder.eip712_send_struct_impl_array(size)): + pass + return self._recv() + + def eip712_send_struct_impl_struct_field(self, raw_value: bytes): + for apdu in self._cmd_builder.eip712_send_struct_impl_struct_field(raw_value): + with self._send(apdu): + self._enable_click_until_response() + self._disable_click_until_response() + assert self._recv().status == 0x9000 + + def eip712_sign_new(self, bip32): + with self._send(self._cmd_builder.eip712_sign_new(bip32)): + if not self._settings[SettingType.VERBOSE_EIP712].value and \ + not self._eip712_filtering: # need to skip the message hash + self._client.right_click() + self._client.right_click() + self._client.both_click() # approve signature + resp = self._recv() + assert resp.status == 0x9000 + return self._resp_parser.sign(resp.data) + + def eip712_sign_legacy(self, + bip32, + domain_hash: bytes, + message_hash: bytes): + with self._send(self._cmd_builder.eip712_sign_legacy(bip32, + domain_hash, + message_hash)): + self._client.right_click() # sign typed message screen + for _ in range(2): # two hashes (domain + message) + if self._client.firmware.device == "nanos": + screens_per_hash = 4 + else: + screens_per_hash = 2 + for _ in range(screens_per_hash): + self._client.right_click() + self._client.both_click() # approve signature + + resp = self._recv() + + assert resp.status == 0x9000 + return self._resp_parser.sign(resp.data) + + def settings_set(self, new_values: Dict[SettingType, bool]): + # Go to settings + for _ in range(2): + self._client.right_click() + self._client.both_click() + + for enum in self._settings.keys(): + if self._client.firmware.device in self._settings[enum].devices: + if enum in new_values.keys(): + if new_values[enum] != self._settings[enum].value: + self._client.both_click() + self._settings[enum].value = new_values[enum] + self._client.right_click() + self._client.both_click() + + def eip712_filtering_activate(self): + with self._send(self._cmd_builder.eip712_filtering_activate()): + pass + self._eip712_filtering = True + assert self._recv().status == 0x9000 + + def eip712_filtering_message_info(self, name: str, filters_count: int, sig: bytes): + with self._send(self._cmd_builder.eip712_filtering_message_info(name, filters_count, sig)): + self._enable_click_until_response() + self._disable_click_until_response() + assert self._recv().status == 0x9000 + + def eip712_filtering_show_field(self, name: str, sig: bytes): + with self._send(self._cmd_builder.eip712_filtering_show_field(name, sig)): + pass + assert self._recv().status == 0x9000 diff --git a/tests/ragger/ethereum_client/command_builder.py b/tests/ragger/ethereum_client/command_builder.py new file mode 100644 index 0000000..134405f --- /dev/null +++ b/tests/ragger/ethereum_client/command_builder.py @@ -0,0 +1,170 @@ +from enum import IntEnum, auto +from typing import Iterator +from ethereum_client.eip712 import EIP712FieldType + +class InsType(IntEnum): + EIP712_SEND_STRUCT_DEF = 0x1a + EIP712_SEND_STRUCT_IMPL = 0x1c + EIP712_SEND_FILTERING = 0x1e + EIP712_SIGN = 0x0c + +class P1Type(IntEnum): + COMPLETE_SEND = 0x00 + PARTIAL_SEND = 0x01 + +class P2Type(IntEnum): + STRUCT_NAME = 0x00 + STRUCT_FIELD = 0xff + ARRAY = 0x0f + LEGACY_IMPLEM = 0x00 + NEW_IMPLEM = 0x01 + FILTERING_ACTIVATE = 0x00 + FILTERING_CONTRACT_NAME = 0x0f + FILTERING_FIELD_NAME = 0xff + +class EthereumCmdBuilder: + _CLA: int = 0xE0 + + def _serialize(self, + ins: InsType, + p1: int, + p2: int, + cdata: bytearray = bytearray()) -> bytes: + + header = bytearray() + header.append(self._CLA) + header.append(ins) + header.append(p1) + header.append(p2) + header.append(len(cdata)) + return header + cdata + + def _string_to_bytes(self, string: str) -> bytes: + data = bytearray() + for char in string: + data.append(ord(char)) + return data + + def eip712_send_struct_def_struct_name(self, name: str) -> bytes: + return self._serialize(InsType.EIP712_SEND_STRUCT_DEF, + P1Type.COMPLETE_SEND, + P2Type.STRUCT_NAME, + self._string_to_bytes(name)) + + def eip712_send_struct_def_struct_field(self, + field_type: EIP712FieldType, + type_name: str, + type_size: int, + array_levels: [], + key_name: str) -> bytes: + data = bytearray() + typedesc = 0 + typedesc |= (len(array_levels) > 0) << 7 + typedesc |= (type_size != None) << 6 + typedesc |= field_type + data.append(typedesc) + if field_type == EIP712FieldType.CUSTOM: + data.append(len(type_name)) + data += self._string_to_bytes(type_name) + if type_size != None: + data.append(type_size) + if len(array_levels) > 0: + data.append(len(array_levels)) + for level in array_levels: + data.append(0 if level == None else 1) + if level != None: + data.append(level) + data.append(len(key_name)) + data += self._string_to_bytes(key_name) + return self._serialize(InsType.EIP712_SEND_STRUCT_DEF, + P1Type.COMPLETE_SEND, + P2Type.STRUCT_FIELD, + data) + + def eip712_send_struct_impl_root_struct(self, name: str) -> bytes: + return self._serialize(InsType.EIP712_SEND_STRUCT_IMPL, + P1Type.COMPLETE_SEND, + P2Type.STRUCT_NAME, + self._string_to_bytes(name)) + + def eip712_send_struct_impl_array(self, size: int) -> bytes: + data = bytearray() + data.append(size) + return self._serialize(InsType.EIP712_SEND_STRUCT_IMPL, + P1Type.COMPLETE_SEND, + P2Type.ARRAY, + data) + + def eip712_send_struct_impl_struct_field(self, data: bytearray) -> Iterator[bytes]: + # Add a 16-bit integer with the data's byte length (network byte order) + data_w_length = bytearray() + data_w_length.append((len(data) & 0xff00) >> 8) + data_w_length.append(len(data) & 0x00ff) + data_w_length += data + while len(data_w_length) > 0: + p1 = P1Type.PARTIAL_SEND if len(data_w_length) > 0xff else P1Type.COMPLETE_SEND + yield self._serialize(InsType.EIP712_SEND_STRUCT_IMPL, + p1, + P2Type.STRUCT_FIELD, + data_w_length[:0xff]) + data_w_length = data_w_length[0xff:] + + def _format_bip32(self, bip32, data: bytearray) -> bytearray: + data.append(len(bip32)) + for idx in bip32: + data.append((idx & 0xff000000) >> 24) + data.append((idx & 0x00ff0000) >> 16) + data.append((idx & 0x0000ff00) >> 8) + data.append((idx & 0x000000ff)) + return data + + def eip712_sign_new(self, bip32) -> bytes: + data = self._format_bip32(bip32, bytearray()) + return self._serialize(InsType.EIP712_SIGN, + P1Type.COMPLETE_SEND, + P2Type.NEW_IMPLEM, + data) + + def eip712_sign_legacy(self, + bip32, + domain_hash: bytes, + message_hash: bytes) -> bytes: + data = self._format_bip32(bip32, bytearray()) + data += domain_hash + data += message_hash + return self._serialize(InsType.EIP712_SIGN, + P1Type.COMPLETE_SEND, + P2Type.LEGACY_IMPLEM, + data) + + def eip712_filtering_activate(self): + return self._serialize(InsType.EIP712_SEND_FILTERING, + P1Type.COMPLETE_SEND, + P2Type.FILTERING_ACTIVATE, + bytearray()) + + def _eip712_filtering_send_name(self, name: str, sig: bytes) -> bytes: + data = bytearray() + data.append(len(name)) + data += self._string_to_bytes(name) + data.append(len(sig)) + data += sig + return data + + def eip712_filtering_message_info(self, name: str, filters_count: int, sig: bytes) -> bytes: + data = bytearray() + data.append(len(name)) + data += self._string_to_bytes(name) + data.append(filters_count) + data.append(len(sig)) + data += sig + return self._serialize(InsType.EIP712_SEND_FILTERING, + P1Type.COMPLETE_SEND, + P2Type.FILTERING_CONTRACT_NAME, + data) + + def eip712_filtering_show_field(self, name: str, sig: bytes) -> bytes: + return self._serialize(InsType.EIP712_SEND_FILTERING, + P1Type.COMPLETE_SEND, + P2Type.FILTERING_FIELD_NAME, + self._eip712_filtering_send_name(name, sig)) diff --git a/tests/ragger/ethereum_client/eip712.py b/tests/ragger/ethereum_client/eip712.py new file mode 100644 index 0000000..3438a1c --- /dev/null +++ b/tests/ragger/ethereum_client/eip712.py @@ -0,0 +1,11 @@ +from enum import IntEnum, auto + +class EIP712FieldType(IntEnum): + CUSTOM = 0, + INT = auto() + UINT = auto() + ADDRESS = auto() + BOOL = auto() + STRING = auto() + FIX_BYTES = auto() + DYN_BYTES = auto() diff --git a/tests/ragger/ethereum_client/response_parser.py b/tests/ragger/ethereum_client/response_parser.py new file mode 100644 index 0000000..681c18d --- /dev/null +++ b/tests/ragger/ethereum_client/response_parser.py @@ -0,0 +1,14 @@ +class EthereumRespParser: + def sign(self, data: bytes): + assert len(data) == (1 + 32 + 32) + + v = data[0:1] + data = data[1:] + + r = data[0:32] + data = data[32:] + + s = data[0:32] + data = data[32:] + + return v, r, s diff --git a/tests/ragger/ethereum_client/setting.py b/tests/ragger/ethereum_client/setting.py new file mode 100644 index 0000000..a965fe3 --- /dev/null +++ b/tests/ragger/ethereum_client/setting.py @@ -0,0 +1,15 @@ +from enum import IntEnum, auto +from typing import List + +class SettingType(IntEnum): + BLIND_SIGNING = 0, + DEBUG_DATA = auto() + NONCE = auto() + VERBOSE_EIP712 = auto() + +class SettingImpl: + devices: List[str] + value: bool + + def __init__(self, devs: List[str]): + self.devices = devs diff --git a/tests/ragger/requirements.txt b/tests/ragger/requirements.txt new file mode 100644 index 0000000..8836582 --- /dev/null +++ b/tests/ragger/requirements.txt @@ -0,0 +1,6 @@ +requests>=2.28,<3.0 +click>=8.0,<9.0 # needed by the CI as it installs an older version and breaks dependencies +protobuf==3.20.1 # To fix the protobuf dependency bug +ragger[speculos] +pytest +ecdsa diff --git a/tests/ragger/test_eip712.py b/tests/ragger/test_eip712.py new file mode 100644 index 0000000..bb53ba8 --- /dev/null +++ b/tests/ragger/test_eip712.py @@ -0,0 +1,89 @@ +import pytest +import os +import fnmatch +from typing import List +from ethereum_client.client import EthereumClient, SettingType +from eip712 import InputData +from pathlib import Path +from configparser import ConfigParser + +bip32 = [ + 0x8000002c, + 0x8000003c, + 0x80000000, + 0, + 0 +] + + +def input_files() -> List[str]: + files = [] + for file in os.scandir("./eip712/input_files"): + if fnmatch.fnmatch(file, "*-data.json"): + files.append(file.path) + return sorted(files) + +@pytest.fixture(params=input_files()) +def input_file(request) -> str: + return Path(request.param) + +@pytest.fixture(params=[True, False]) +def verbose(request) -> bool: + return request.param + +@pytest.fixture(params=[False, True]) +def filtering(request) -> bool: + return request.param + + +def test_eip712_legacy(app_client: EthereumClient): + v, r, s = app_client.eip712_sign_legacy( + bip32, + bytes.fromhex('6137beb405d9ff777172aa879e33edb34a1460e701802746c5ef96e741710e59'), + bytes.fromhex('eb4221181ff3f1a83ea7313993ca9218496e424604ba9492bb4052c03d5c3df8') + ) + + assert v == bytes.fromhex("1c") + assert r == bytes.fromhex("ea66f747173762715751c889fea8722acac3fc35db2c226d37a2e58815398f64") + assert s == bytes.fromhex("52d8ba9153de9255da220ffd36762c0b027701a3b5110f0a765f94b16a9dfb55") + + +def test_eip712_new(app_client: EthereumClient, input_file: Path, verbose: bool, filtering: bool): + print("=====> %s" % (input_file)) + if app_client._client.firmware.device != "nanos": + test_path = "%s/%s" % (input_file.parent, "-".join(input_file.stem.split("-")[:-1])) + conf_file = "%s.ini" % (test_path) + filter_file = None + + if filtering: + filter_file = "%s-filter.json" % (test_path) + + config = ConfigParser() + config.read(conf_file) + + # sanity check + assert "signature" in config.sections() + assert "v" in config["signature"] + assert "r" in config["signature"] + assert "s" in config["signature"] + + if not filtering or Path(filter_file).is_file(): + if verbose: + app_client.settings_set({ + SettingType.VERBOSE_EIP712: True + }) + + assert InputData.process_file(app_client, input_file, filter_file) == True + v, r, s = app_client.eip712_sign_new(bip32) + #print("[signature]") + #print("v = %s" % (v.hex())) + #print("r = %s" % (r.hex())) + #print("s = %s" % (s.hex())) + + assert v == bytes.fromhex(config["signature"]["v"]) + assert r == bytes.fromhex(config["signature"]["r"]) + assert s == bytes.fromhex(config["signature"]["s"]) + else: + print("No filter file found, skipping...") + else: + print("Not supported by LNS, skipping...") diff --git a/tests/speculos/test_configuration_cmd.py b/tests/speculos/test_configuration_cmd.py index 6f9e5e0..de41bde 100644 --- a/tests/speculos/test_configuration_cmd.py +++ b/tests/speculos/test_configuration_cmd.py @@ -1,10 +1,10 @@ def test_configuration(cmd): if cmd.model == "nanos": - assert cmd.get_configuration() == (14, 1, 9, 20) - + assert cmd.get_configuration() == (14, 1, 10, 0) + if cmd.model == "nanox": - assert cmd.get_configuration() == (14, 1, 9, 20) + assert cmd.get_configuration() == (14, 1, 10, 0) if cmd.model == "nanosp": - assert cmd.get_configuration() == (14, 1, 9, 20) \ No newline at end of file + assert cmd.get_configuration() == (14, 1, 10, 0) diff --git a/tests/zemu/build_local_test_elfs.sh b/tests/zemu/build_local_test_elfs.sh index b040891..c6b0d0e 100755 --- a/tests/zemu/build_local_test_elfs.sh +++ b/tests/zemu/build_local_test_elfs.sh @@ -2,7 +2,7 @@ set -e -TESTS_FULL_PATH=$(dirname "$(realpath "$0")") +TESTS_PATH=$(dirname "$(realpath "$0")") # FILL THESE WITH YOUR OWN SDKs PATHS # NANOS_SDK= @@ -17,11 +17,10 @@ NANO_SDKS=("$NANOS_SDK" "$NANOX_SDK") FILE_SUFFIXES=("nanos" "nanox") # move to the tests directory -cd "$TESTS_FULL_PATH" || exit 1 +cd "$TESTS_PATH" || exit 1 # Do it only now since before the cd command, we might not have been inside the repository GIT_REPO_ROOT=$(git rev-parse --show-toplevel) -TESTS_REL_PATH=$(realpath --relative-to="$GIT_REPO_ROOT" "$TESTS_FULL_PATH") # create elfs directory if it doesn't exist mkdir -p elfs @@ -39,7 +38,7 @@ do echo "** Building app $appname..." make clean BOLOS_SDK="$nano_sdk" make -j DEBUG=1 NFT_TESTING_KEY=1 BOLOS_SDK="$nano_sdk" CHAIN="$appname" - cp bin/app.elf "$TESTS_REL_PATH/elfs/${appname}_${elf_suffix}.elf" + cp bin/app.elf "$TESTS_PATH/elfs/${appname}_${elf_suffix}.elf" done done diff --git a/tests/zemu/snapshots/nanos_enable_blind_signing/00003.png b/tests/zemu/snapshots/nanos_enable_blind_signing/00003.png index c72ea3d..c828b7c 100644 Binary files a/tests/zemu/snapshots/nanos_enable_blind_signing/00003.png and b/tests/zemu/snapshots/nanos_enable_blind_signing/00003.png differ diff --git a/tests/zemu/snapshots/nanos_enable_blind_signing/00005.png b/tests/zemu/snapshots/nanos_enable_blind_signing/00005.png index 4c435a4..de843af 100644 Binary files a/tests/zemu/snapshots/nanos_enable_blind_signing/00005.png and b/tests/zemu/snapshots/nanos_enable_blind_signing/00005.png differ diff --git a/tests/zemu/snapshots/nanos_enable_blind_signing/00006.png b/tests/zemu/snapshots/nanos_enable_blind_signing/00006.png index 10dde6b..e615d31 100644 Binary files a/tests/zemu/snapshots/nanos_enable_blind_signing/00006.png and b/tests/zemu/snapshots/nanos_enable_blind_signing/00006.png differ diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00003.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00003.png index 6cef7c9..9e21d4a 100644 Binary files a/tests/zemu/snapshots/nanox_enable_blind_signing/00003.png and b/tests/zemu/snapshots/nanox_enable_blind_signing/00003.png differ diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00004.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00004.png index 8f75e90..fb2843a 100644 Binary files a/tests/zemu/snapshots/nanox_enable_blind_signing/00004.png and b/tests/zemu/snapshots/nanox_enable_blind_signing/00004.png differ diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00005.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00005.png index a6c3f94..1c5f8a2 100644 Binary files a/tests/zemu/snapshots/nanox_enable_blind_signing/00005.png and b/tests/zemu/snapshots/nanox_enable_blind_signing/00005.png differ diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00006.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00006.png index 123a7ac..0b4f46e 100644 Binary files a/tests/zemu/snapshots/nanox_enable_blind_signing/00006.png and b/tests/zemu/snapshots/nanox_enable_blind_signing/00006.png differ diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00007.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00007.png index 61861f2..5b3eed9 100644 Binary files a/tests/zemu/snapshots/nanox_enable_blind_signing/00007.png and b/tests/zemu/snapshots/nanox_enable_blind_signing/00007.png differ diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00008.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00008.png index a58590b..61861f2 100644 Binary files a/tests/zemu/snapshots/nanox_enable_blind_signing/00008.png and b/tests/zemu/snapshots/nanox_enable_blind_signing/00008.png differ diff --git a/tests/zemu/snapshots/nanox_enable_blind_signing/00009.png b/tests/zemu/snapshots/nanox_enable_blind_signing/00009.png new file mode 100644 index 0000000..a58590b Binary files /dev/null and b/tests/zemu/snapshots/nanox_enable_blind_signing/00009.png differ diff --git a/tests/zemu/src/blind_compound_deposit.test.js b/tests/zemu/src/blind_compound_deposit.test.js index d94f6f8..2f999ac 100644 --- a/tests/zemu/src/blind_compound_deposit.test.js +++ b/tests/zemu/src/blind_compound_deposit.test.js @@ -4,8 +4,12 @@ import { waitForAppScreen, zemu, nano_models } from './test.fixture'; nano_models.forEach(function(model) { test('[Nano ' + model.letter + '] Deposit ETH on compound, blind sign', zemu(model, async (sim, eth) => { + let clicks; + // LNS does not have an EIP712 setting + if (model.letter === 'S') clicks = 3; + else clicks = 4; // Enable blind-signing - await sim.navigateAndCompareSnapshots('.', model.name + '_enable_blind_signing', [-2, 0, 0, 3, 0]); + await sim.navigateAndCompareSnapshots('.', model.name + '_enable_blind_signing', [-2, 0, 0, clicks, 0]); const tx = eth.signTransaction( "44'/60'/1'/0/0", @@ -13,7 +17,6 @@ nano_models.forEach(function(model) { ); await waitForAppScreen(sim); - let clicks; if (model.letter === 'S') clicks = 8; else clicks = 6; await sim.navigateAndCompareSnapshots('.', model.name + '_deposit_eth_compound_blind', [clicks, -1, 0]);