Merge pull request #548 from LedgerHQ/release/1.10.4

App release 1.10.4
This commit is contained in:
apaillier-ledger
2024-03-14 13:59:31 +01:00
committed by GitHub
1409 changed files with 5860 additions and 6706 deletions

View File

@@ -1,3 +1,4 @@
---
name: 'Auto Author Assign'
on:

View File

@@ -1,34 +0,0 @@
name: Compilation
on:
push:
branches:
- master
pull_request:
branches:
- master
- develop
workflow_dispatch:
jobs:
nano_release_build:
name: Build release application for NanoS, X and S+
strategy:
matrix:
sdk: ["$NANOS_SDK", "$NANOX_SDK", "$NANOSP_SDK"]
runs-on: ubuntu-latest
container:
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest
steps:
- name: Clone
uses: actions/checkout@v3
- name: Build Ethereum
run: |
make -j BOLOS_SDK=${{ matrix.sdk }}
- name: Build an altcoin
run: |
make clean
make -j BOLOS_SDK=${{ matrix.sdk }} CHAIN=polygon

View File

@@ -1,39 +1,19 @@
---
name: Tests
on:
workflow_dispatch:
push:
branches:
- master
pull_request:
branches:
- master
- develop
workflow_dispatch:
pull_request:
jobs:
scan-build:
name: Clang Static Analyzer
runs-on: ubuntu-latest
container:
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest
steps:
- uses: actions/checkout@v3
- name: Build with Clang Static Analyzer
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@v3
if: failure()
with:
name: scan-build
path: scan-build
# =====================================================
# ZEMU TESTS
# =====================================================
# =====================================================
# ZEMU TESTS
# =====================================================
building_for_e2e_zemu_tests:
name: Building binaries for E2E Zemu tests
@@ -97,9 +77,9 @@ jobs:
- name: Run zemu tests
run: cd tests/zemu/ && yarn test
# =====================================================
# SPECULOS TESTS
# =====================================================
# =====================================================
# SPECULOS TESTS
# =====================================================
building_for_e2e_speculos_tests:
@@ -114,9 +94,9 @@ jobs:
- name: Build testing binaries
run: |
mkdir tests/speculos/elfs
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
make clean && make -j DEBUG=1 NFT_STAGING_KEY=1 BOLOS_SDK=$NANOS_SDK && mv bin/app.elf tests/speculos/elfs/nanos.elf
make clean && make -j DEBUG=1 NFT_STAGING_KEY=1 BOLOS_SDK=$NANOX_SDK && mv bin/app.elf tests/speculos/elfs/nanox.elf
make clean && make -j DEBUG=1 NFT_STAGING_KEY=1 BOLOS_SDK=$NANOSP_SDK && mv bin/app.elf tests/speculos/elfs/nanosp.elf
- name: Upload app binaries
uses: actions/upload-artifact@v3
@@ -162,16 +142,16 @@ jobs:
pytest --model ${{ matrix.model }} --path ./elfs/${{ matrix.model }}.elf --display headless
# =====================================================
# RAGGER TESTS
# =====================================================
# =====================================================
# RAGGER TESTS
# =====================================================
build_ragger_elfs:
name: Build app for Ragger tests
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1
with:
upload_app_binaries_artifact: "ragger_elfs"
flags: "DEBUG=1 CAL_CI_KEY=1 DOMAIN_NAME_TEST_KEY=1"
flags: "DEBUG=1 CAL_TEST_KEY=1 DOMAIN_NAME_TEST_KEY=1 SET_PLUGIN_TEST_KEY=1 NFT_TEST_KEY=1"
jobs-ragger-tests:
name: Run Ragger tests
@@ -179,4 +159,43 @@ jobs:
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1
with:
download_app_binaries_artifact: "ragger_elfs"
test_dir: tests/ragger
# =====================================================
# STATIC ANALYSIS
# =====================================================
# Static analysis on the main ETH chain is covered by the guidelines enforcer
scan-build:
name: Clang Static Analyzer on altcoin
runs-on: ubuntu-latest
container:
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest
strategy:
fail-fast: false
matrix:
device: ["nanos", "nanos2", "nanox", "stax"]
steps:
- name: Clone
uses: actions/checkout@v3
with:
submodules: recursive
- name: Build with Clang Static Analyzer
run: |
eval "BOLOS_SDK=\$$(echo ${{ matrix.device }} | tr [:lower:] [:upper:])_SDK" && \
echo "BOLOS_SDK value will be: ${BOLOS_SDK}" && \
make -j ENABLE_SDK_WERROR=1 BOLOS_SDK=${BOLOS_SDK} CHAIN=polygon scan-build
- uses: actions/upload-artifact@v3
if: failure()
with:
name: scan-build
path: scan-build
- name: Upload scan result
if: failure()
uses: actions/upload-artifact@v3
with:
name: scan-build
path: scan-build

View File

@@ -1,26 +1,26 @@
---
name: Code style check
# This workflow will run linting checks to ensure a level of uniformization among all Ledger applications.
#
# The presence of this workflow is mandatory as a minimal level of linting is required.
# You are however free to modify the content of the .clang-format file and thus the coding style of your application.
# We simply ask you to not diverge too much from the linting of the Boilerplate application.
on:
workflow_dispatch:
push:
branches:
- master
- master
- main
- develop
pull_request:
branches:
- master
- develop
jobs:
job_lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v3
- name: Lint
uses: DoozyX/clang-format-lint-action@v0.14
with:
source: "./"
extensions: "h,c"
clangFormatVersion: 12.0.1
check_linting:
name: Check linting using the reusable workflow
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_lint.yml@v1
with:
source: './'
extensions: 'h,c'
version: 12

202
.github/workflows/pr_on_all_plugins.yml vendored Normal file
View File

@@ -0,0 +1,202 @@
---
name: Open PRs in Ethereum plugins to update SDK
on:
workflow_dispatch:
# TODO add auto trigger once finalized
jobs:
build-repositories-matrix:
name: Build repo matrix
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
steps:
- name: List plugin repositories
id: list-repos
run: |
# Retrieve the list of repositories from LedgerHQ organization
raw_repo_list=$(gh repo list LedgerHQ -L 2000 \
--json name \
--json isPrivate \
--json isArchived \
--jq '.[] | select(.isPrivate == false and .isArchived == false and select(.name | startswith("app-plugin-"))) | .name')
# Format the repository list as a JSON array
formatted_repo_list="["
while IFS= read -r repo; do
formatted_repo_list+="\"$repo\", "
done < <(echo "$raw_repo_list")
formatted_repo_list=${formatted_repo_list%%, }"]"
echo "Formatted Repository List: $formatted_repo_list"
# Set output
echo "repo_list=$formatted_repo_list" >> $GITHUB_OUTPUT
outputs:
repo-matrix: ${{ steps.list-repos.outputs.repo_list }}
get-sdk-ref:
name: Get latest SDK reference
runs-on: ubuntu-latest
steps:
- name: Checkout SDK repository
uses: actions/checkout@v3
with:
repository: LedgerHQ/ethereum-plugin-sdk
ref: develop
- name: Retrieve SDK reference
id: get-hash
run: |
# Get the short SHA reference of the latest commit in the SDK repository
sdk_ref=$(git rev-parse --short HEAD)
echo "SDK Reference: $sdk_ref"
# Set output
echo "sdk_ref=$sdk_ref" >> $GITHUB_OUTPUT
outputs:
sdk_ref: ${{ steps.get-hash.outputs.sdk_ref }}
open-issue:
name: Open SDK update summary issue
runs-on: ubuntu-latest
needs: get-sdk-ref
env:
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
steps:
- name: Create 'auto' label if missing
run: |
if [[ -z $(gh label list --repo ${{ github.repository }} --search auto) ]]; then
gh label create 'auto' --repo ${{ github.repository }} --color 'b4a8d1' --description 'Automatically created'
fi
- name: Open SDK Update Issue
id: open-issue
run: |
# Create a new issue with the SDK reference in the SDK repository
issue_url=$(gh issue create \
--repo ${{ github.repository }} \
--label auto \
--title "Bumping Eth plugin SDK to latest version ${{ needs.get-sdk-ref.outputs.sdk_ref }}" \
--body "Placeholder")
echo "Issue URL: $issue_url"
# Set output
echo "issue_url=$issue_url" >> $GITHUB_OUTPUT
outputs:
issue_url: ${{ steps.open-issue.outputs.issue_url }}
open-prs:
name: Open pull requests
runs-on: ubuntu-latest
needs: [build-repositories-matrix, open-issue, get-sdk-ref]
strategy:
fail-fast: false
matrix:
repo: ${{ fromJSON(needs.build-repositories-matrix.outputs.repo-matrix) }}
env:
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
steps:
- name: Checkout plugin repository
uses: actions/checkout@v3
with:
repository: LedgerHQ/${{ matrix.repo }}
submodules: recursive
ref: develop
# by default the action uses fetch-depth = 1, which creates
# shallow repositories from which we can't push
fetch-depth: 0
# needed, else the push inside the action will use default credentials
# instead of provided ones
persist-credentials: false
- name: Update submodule
run: |
cd ethereum-plugin-sdk/
git checkout ${{ needs.get-sdk-ref.outputs.sdk_ref }}
- name: Commit changes
id: commit-changes
run: |
# Set credentials for commit creation
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
# Branch name and title will be unique by design
branch_name="auto/bump_sdk_to_${{ needs.get-sdk-ref.outputs.sdk_ref }}"
title="[auto-update] Bump SDK to latest develop version ${{ needs.get-sdk-ref.outputs.sdk_ref }}"
echo "Branch Name: $branch_name"
echo "Title: $title"
git status
git commit -am "$title"
# Set output
echo "title=$title" >> $GITHUB_OUTPUT
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
- name: Push commit
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.CI_BOT_TOKEN }}
branch: ${{ steps.commit-changes.outputs.branch_name }}
repository: LedgerHQ/${{ matrix.repo }}
force: true
- name: Create 'auto' label if missing
run: |
if [[ -z $(gh label list --search auto) ]]; then
gh label create 'auto' --color 'b4a8d1' --description 'Automatically created'
fi
- name: Create pull request and commment on SDK issue
run: |
# Github limits the number of possible PR being opened in a given time window.
# The limits are 20 creation per minute and 150 per hour.
# As suggested in the Github documentation, put a sleep between each POST call
# 3 seconds should be sufficient but let's sleep 4 seconds just in case.
sleep $((4 * ${{ strategy.job-index }}))
# Create the PR with a placeholder body. Will be consolidated at a later step
pr_url=$(gh pr create \
--base 'develop' \
--head '${{ steps.commit-changes.outputs.branch_name }}' \
--label 'auto' \
--title '${{ steps.commit-changes.outputs.title }}' \
--body 'Created by a Github workflow')
echo "Pull request URL: $pr_url"
# Log the url of the PR in the issue on SDK side. We'll collect them from the issue later
gh issue comment "${{ needs.open-issue.outputs.issue_url }}" --body "OPENED $pr_url"
clean-issue:
name: Clean SDK update summary issue
runs-on: ubuntu-latest
needs: [get-sdk-ref, open-issue, open-prs]
env:
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
steps:
- name: Collect all comments on the SDK issue
run: |
# Get the full text of the issue: metadata + title + rich comments
content="$(gh issue view --comments "${{ needs.open-issue.outputs.issue_url }}")"
# New header of the issue body
header="Bumping Ethereum plugin SDK to latest version ${{ needs.get-sdk-ref.outputs.sdk_ref }} for all Ethereum plugins:"
# Filter the full text of the issue to collect only the PR urls
lines=""
while IFS= read -r line; do
if [[ "$line" =~ "OPENED" ]]; then
lines+=$(echo "$line" | cut -d ' ' -f2)$'\n'
fi
done < <(echo "$content")
# Use print to resolve the '\n' chars
new_body="$(printf "$header\n$lines")"
echo "New issue body: $new_body"
# Set the consolidated body of the issue
gh issue edit "${{ needs.open-issue.outputs.issue_url }}" --body "$new_body"
- name: Clean comments on the SDK issue
run: |
# gh api uses id instead of url
issue_id="$(basename "${{ needs.open-issue.outputs.issue_url }}")"
# Get url of all comments on the issue
comment_urls="$(gh api "repos/${{ github.repository }}/issues/${issue_id}/comments" --jq '.[].url')"
# Delete each comment using the Github REST api
while IFS= read -r url; do
echo "Deleting comment: $comment_urls"
gh api -X DELETE "$url"
done < <(echo "$comment_urls")

80
.github/workflows/python-client.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
---
name: Python client checks, package build and deployment
on:
workflow_dispatch:
push:
tags:
- 'client-*'
branches:
- develop
- master
paths:
- client/**
- .github/workflows/python-client.yml
pull_request:
paths:
- client/**
- .github/workflows/python-client.yml
jobs:
lint:
name: Linting
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v3
- run: pip install flake8 flake8-pyproject
- name: Flake8 lint Python code
run: (cd client && flake8 src/)
mypy:
name: Type checking
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v3
- run: pip install mypy
- name: Mypy type checking
run: (cd client && mypy src/)
package_and_deploy:
name: Build and deploy Ethereum Client Python package
runs-on: ubuntu-latest
needs: [lint, mypy]
steps:
- name: Clone
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Build Python package
run: |
pip install --upgrade pip build twine
cd client/
python -m build;
python -m twine check dist/*
pip install .;
echo "TAG_VERSION=$(python -c 'from ledger_app_clients.ethereum import __version__; print(__version__)')" >> "$GITHUB_ENV"
- name: Check version against CHANGELOG
if: startsWith(github.ref, 'refs/tags/')
run: |
CHANGELOG_VERSION=$(grep -Po '(?<=## \[)(\d+\.)+[^\]]' client/CHANGELOG.md | head -n 1)
if [ "${{ env.TAG_VERSION }}" == "${CHANGELOG_VERSION}" ];
then
echo 'Package and CHANGELOG versions match!';
exit 0;
else
echo "Tag '${{ env.TAG_VERSION }}' and CHANGELOG '${CHANGELOG_VERSION}' versions mismatch!";
exit 1;
fi
- name: Publish Python package on pypi.org
if: success() && github.event_name == 'push'
run: (cd client && python -m twine upload --verbose dist/*)
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_PUBLIC_API_TOKEN }}
TWINE_NON_INTERACTIVE: 1

View File

@@ -1,3 +1,4 @@
---
name: Updating the SDK
on:
@@ -25,7 +26,7 @@ jobs:
persist-credentials: false
- name: Build new SDK
run: python tools/build_sdk.py
run: ./tools/build_sdk.sh
- name: Extract branch name
shell: bash

View File

@@ -1,11 +1,12 @@
---
name: Swap functional tests
on:
workflow_dispatch:
push:
branches:
- master
- develop
- master
- develop
pull_request:
jobs:

2
.gitignore vendored
View File

@@ -9,7 +9,7 @@ build/
# Python
*.pyc
__version__.py
# JS
tests/node_modules

View File

@@ -5,6 +5,39 @@ 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.4](https://github.com/ledgerhq/app-ethereum/compare/1.10.3...1.10.4) - 2023-03-08
### Added
- Addresses in EIP-712 messages can now be displayed as a token ticker or a trusted domain name if a match is found
- Stax app now has icons of the other supported EVM chains
- (network) Bitcichain
- (network) Core
- (network) Bitrock Mainnet
- (network) Numbers Protocol
- (network) Linea
- (network) Holesky
### Removed
- Starkware support
- (clone) kUSD
- (clone) Tobalaba
### Changed
- Can now clear-sign NFT operations on other EVM chains without a clone app
- Can now swap on other EVM chains without a clone app
- Improved RAM usage
- Now shows an explicit ??? ticker when it is unknown instead of falling back to the native chain ticker
### Fixed
- Refusal of transactions with very large chain IDs even within specs
- Refusal of 10 character-long token tickers
- (network) Wanchain chain ID
- (network) Sepolia chain ID
## [1.10.3](https://github.com/ledgerhq/app-ethereum/compare/1.10.2...1.10.3) - 2023-07-27
### Added

267
Makefile
View File

@@ -16,13 +16,13 @@
#*******************************************************************************
ifeq ($(BOLOS_SDK),)
$(error Environment variable BOLOS_SDK is not set)
$(error Environment variable BOLOS_SDK is not set)
endif
include $(BOLOS_SDK)/Makefile.defines
DEFINES_LIB = USE_LIB_ETHEREUM
APP_LOAD_PARAMS= --curve secp256k1 $(COMMON_LOAD_PARAMS)
APP_LOAD_PARAMS = --curve secp256k1 $(COMMON_LOAD_PARAMS)
# Allow the app to use path 45 for multi-sig (see BIP45).
APP_LOAD_PARAMS += --path "45'"
# Samsung temporary implementation for wallet ID on 0xda7aba5e/0xc1a551c5
@@ -32,29 +32,30 @@ APP_LOAD_PARAMS += --path "1517992542'/1101353413'"
# Define Version #
##################
APPVERSION_M=1
APPVERSION_N=10
APPVERSION_P=3
APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)
APP_LOAD_FLAGS= --appFlags 0xa40 --dep Ethereum:$(APPVERSION)
APPVERSION_M = 1
APPVERSION_N = 10
APPVERSION_P = 4
APPVERSION = $(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)
APP_LOAD_FLAGS = --appFlags 0xa40 --dep Ethereum:$(APPVERSION)
###########################
# Set Chain environnement #
###########################
ifeq ($(CHAIN),)
CHAIN=ethereum
CHAIN = ethereum
endif
SUPPORTED_CHAINS=$(shell find makefile_conf/chain/ -type f -name '*.mk'| sed 's/.*\/\(.*\).mk/\1/g' | sort)
SUPPORTED_CHAINS = $(shell find makefile_conf/chain/ -type f -name '*.mk'| sed 's/.*\/\(.*\).mk/\1/g' | sort)
# Check if chain is available
ifeq ($(shell test -s ./makefile_conf/chain/$(CHAIN).mk && echo -n yes), yes)
include ./makefile_conf/chain/$(CHAIN).mk
include ./makefile_conf/chain/$(CHAIN).mk
else
$(error Unsupported CHAIN - use $(SUPPORTED_CHAINS))
$(error Unsupported CHAIN - use $(SUPPORTED_CHAINS))
endif
CFLAGS += -DAPPNAME=\"$(APPNAME)\"
DEFINES += CHAINID_COINNAME=\"$(TICKER)\" CHAIN_ID=$(CHAIN_ID)
#########
# Other #
@@ -65,15 +66,15 @@ DEFINES += $(DEFINES_LIB)
#prepare hsm generation
ifeq ($(TARGET_NAME),TARGET_NANOS)
ICONNAME=icons/nanos_app_$(CHAIN).gif
ICONNAME = icons/nanos_app_chain_$(CHAIN_ID).gif
else ifeq ($(TARGET_NAME),TARGET_STAX)
ICONNAME=icons/stax_app_$(CHAIN).gif
DEFINES += ICONGLYPH=C_stax_$(CHAIN)_64px
DEFINES += ICONBITMAP=C_stax_$(CHAIN)_64px_bitmap
DEFINES += ICONGLYPH_SMALL=C_stax_$(CHAIN)
GLYPH_FILES += $(ICONNAME)
ICONNAME = icons/stax_app_chain_$(CHAIN_ID).gif
DEFINES += ICONGLYPH=C_stax_chain_$(CHAIN_ID)_64px
DEFINES += ICONBITMAP=C_stax_chain_$(CHAIN_ID)_64px_bitmap
DEFINES += ICONGLYPH_SMALL=C_stax_chain_$(CHAIN_ID)
GLYPH_FILES += $(ICONNAME)
else
ICONNAME=icons/nanox_app_$(CHAIN).gif
ICONNAME = icons/nanox_app_chain_$(CHAIN_ID).gif
endif
################
@@ -85,181 +86,207 @@ all: default
# Platform #
############
DEFINES += OS_IO_SEPROXYHAL
DEFINES += HAVE_SPRINTF HAVE_SNPRINTF_FORMAT_U
DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 HAVE_USB_APDU
DEFINES += LEDGER_MAJOR_VERSION=$(APPVERSION_M) LEDGER_MINOR_VERSION=$(APPVERSION_N) LEDGER_PATCH_VERSION=$(APPVERSION_P)
DEFINES += BUILD_YEAR=\"$(shell date +%Y)\"
DEFINES += OS_IO_SEPROXYHAL
DEFINES += HAVE_SPRINTF HAVE_SNPRINTF_FORMAT_U
DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 HAVE_USB_APDU
DEFINES += LEDGER_MAJOR_VERSION=$(APPVERSION_M) LEDGER_MINOR_VERSION=$(APPVERSION_N) LEDGER_PATCH_VERSION=$(APPVERSION_P)
DEFINES += BUILD_YEAR=\"$(shell date +%Y)\"
# U2F
DEFINES += HAVE_U2F HAVE_IO_U2F
DEFINES += U2F_PROXY_MAGIC=\"w0w\"
DEFINES += USB_SEGMENT_SIZE=64
DEFINES += BLE_SEGMENT_SIZE=32 #max MTU, min 20
DEFINES += UNUSED\(x\)=\(void\)x
DEFINES += APPVERSION=\"$(APPVERSION)\"
DEFINES += HAVE_U2F HAVE_IO_U2F
DEFINES += U2F_PROXY_MAGIC=\"w0w\"
DEFINES += USB_SEGMENT_SIZE=64
DEFINES += BLE_SEGMENT_SIZE=32 #max MTU, min 20
DEFINES += APPVERSION=\"$(APPVERSION)\"
#WEBUSB_URL = www.ledgerwallet.com
#DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=$(shell echo -n $(WEBUSB_URL) | wc -c) WEBUSB_URL=$(shell echo -n $(WEBUSB_URL) | sed -e "s/./\\\'\0\\\',/g")
#WEBUSB_URL = www.ledgerwallet.com
#DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=$(shell echo -n $(WEBUSB_URL) | wc -c) WEBUSB_URL=$(shell echo -n $(WEBUSB_URL) | sed -e "s/./\\\'\0\\\',/g")
DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL=""
DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL=""
ifneq (,$(filter $(TARGET_NAME),TARGET_NANOX TARGET_STAX))
DEFINES += HAVE_BLE BLE_COMMAND_TIMEOUT_MS=2000
DEFINES += HAVE_BLE_APDU # basic ledger apdu transport over BLE
SDK_SOURCE_PATH += lib_blewbxx lib_blewbxx_impl
DEFINES += HAVE_BLE BLE_COMMAND_TIMEOUT_MS=2000
DEFINES += HAVE_BLE_APDU # basic ledger apdu transport over BLE
SDK_SOURCE_PATH += lib_blewbxx lib_blewbxx_impl
endif
ifeq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128
else
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300
endif
ifeq ($(TARGET_NAME),TARGET_STAX)
DEFINES += NBGL_QRCODE
DEFINES += NBGL_QRCODE
SDK_SOURCE_PATH += qrcode
else
DEFINES += HAVE_BAGL
DEFINES += HAVE_UX_FLOW
ifeq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_WALLET_ID_SDK
DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=32
else
DEFINES += HAVE_GLO096
DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=64
DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX
endif
DEFINES += HAVE_BAGL
DEFINES += HAVE_UX_FLOW
ifeq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_WALLET_ID_SDK
DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=32
else
DEFINES += HAVE_GLO096
DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=64
DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX
endif
endif
####################
# Enabled Features #
####################
# Enables direct data signing without having to specify it in the settings. Useful when testing with speculos.
ALLOW_DATA:=0
ALLOW_DATA ?= 0
ifneq ($(ALLOW_DATA),0)
DEFINES += HAVE_ALLOW_DATA
DEFINES += HAVE_ALLOW_DATA
endif
# Bypass the signature verification for setExternalPlugin, setPlugin, provideERC20TokenInfo and provideNFTInfo calls
BYPASS_SIGNATURES:=0
BYPASS_SIGNATURES ?= 0
ifneq ($(BYPASS_SIGNATURES),0)
DEFINES += HAVE_BYPASS_SIGNATURES
DEFINES += HAVE_BYPASS_SIGNATURES
endif
# Enable the SET_PLUGIN test key
SET_PLUGIN_TEST_KEY ?= 0
ifneq ($(SET_PLUGIN_TEST_KEY),0)
DEFINES += HAVE_SET_PLUGIN_TEST_KEY
endif
# NFTs
ifneq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_NFT_SUPPORT
# Enable the NFT testing key
NFT_TESTING_KEY:=0
ifneq ($(NFT_TESTING_KEY),0)
DEFINES += HAVE_NFT_TESTING_KEY
DEFINES += HAVE_NFT_SUPPORT
NFT_TEST_KEY ?= 0
ifneq ($(NFT_TEST_KEY),0)
DEFINES += HAVE_NFT_TEST_KEY
endif
NFT_STAGING_KEY ?= 0
ifneq ($(NFT_STAGING_KEY),0)
# Key used by the staging backend
DEFINES += HAVE_NFT_STAGING_KEY
endif
endif
ifneq (,$(filter $(DEFINES),HAVE_NFT_TEST_KEY))
ifneq (, $(filter $(DEFINES),HAVE_NFT_STAGING_KEY))
$(error Multiple alternative NFT keys set at once)
endif
endif
# Dynamic memory allocator
ifneq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_DYN_MEM_ALLOC
DEFINES += HAVE_DYN_MEM_ALLOC
endif
# EIP-712
ifneq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_EIP712_FULL_SUPPORT
DEFINES += HAVE_EIP712_FULL_SUPPORT
endif
# CryptoAssetsList key
CAL_TEST_KEY:=0
CAL_CI_KEY:=0
CAL_TEST_KEY ?= 0
ifneq ($(CAL_TEST_KEY),0)
DEFINES += HAVE_CAL_TEST_KEY
# Key used in our test framework
DEFINES += HAVE_CAL_TEST_KEY
endif
ifneq ($(CAL_CI_KEY),0)
DEFINES += HAVE_CAL_CI_KEY
CAL_STAGING_KEY ?= 0
ifneq ($(CAL_STAGING_KEY),0)
# Key used by the staging CAL
DEFINES += HAVE_CAL_STAGING_KEY
endif
ifneq (,$(filter $(DEFINES),HAVE_CAL_TEST_KEY))
ifneq (, $(filter $(DEFINES),HAVE_CAL_STAGING_KEY))
# Can't use both the staging and testing keys
$(error Multiple alternative CAL keys set at once)
endif
endif
# ENS
ifneq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_DOMAIN_NAME
DOMAIN_NAME_TEST_KEY:=0
ifneq ($(DOMAIN_NAME_TEST_KEY),0)
DEFINES += HAVE_DOMAIN_NAME_TEST_KEY
endif
DEFINES += HAVE_DOMAIN_NAME
DOMAIN_NAME_TEST_KEY ?= 0
ifneq ($(DOMAIN_NAME_TEST_KEY),0)
DEFINES += HAVE_DOMAIN_NAME_TEST_KEY
endif
endif
# Enabling debug PRINTF
ifneq ($(DEBUG),0)
DEFINES += HAVE_STACK_OVERFLOW_CHECK
ifeq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_PRINTF PRINTF=screen_printf
DEFINES += HAVE_STACK_OVERFLOW_CHECK
ifeq ($(TARGET_NAME),TARGET_NANOS)
DEFINES += HAVE_PRINTF PRINTF=screen_printf
else
DEFINES += HAVE_PRINTF PRINTF=mcu_usb_printf
endif
else
DEFINES += HAVE_PRINTF PRINTF=mcu_usb_printf
endif
else
DEFINES += PRINTF\(...\)=
DEFINES += PRINTF\(...\)=
endif
ifneq ($(NOCONSENT),)
DEFINES += NO_CONSENT
DEFINES += NO_CONSENT
endif
#DEFINES += HAVE_TOKENS_LIST # Do not activate external ERC-20 support yet
##############
# Compiler #
##############
ifneq ($(BOLOS_ENV),)
$(info BOLOS_ENV=$(BOLOS_ENV))
CLANGPATH := $(BOLOS_ENV)/clang-arm-fropi/bin/
GCCPATH := $(BOLOS_ENV)/gcc-arm-none-eabi-5_3-2016q1/bin/
$(info BOLOS_ENV=$(BOLOS_ENV))
CLANGPATH := $(BOLOS_ENV)/clang-arm-fropi/bin/
GCCPATH := $(BOLOS_ENV)/gcc-arm-none-eabi-5_3-2016q1/bin/
else
$(info BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH)
$(info BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH)
endif
ifeq ($(CLANGPATH),)
$(info CLANGPATH is not set: clang will be used from PATH)
$(info CLANGPATH is not set: clang will be used from PATH)
endif
ifeq ($(GCCPATH),)
$(info GCCPATH is not set: arm-none-eabi-* will be used from PATH)
$(info GCCPATH is not set: arm-none-eabi-* will be used from PATH)
endif
CC := $(CLANGPATH)clang
CFLAGS += -Wno-format-invalid-specifier -Wno-format-extra-args
AS := $(GCCPATH)arm-none-eabi-gcc
LD := $(GCCPATH)arm-none-eabi-gcc
LDLIBS += -lm -lgcc -lc
CC := $(CLANGPATH)clang
CFLAGS += -Wno-format-invalid-specifier -Wno-format-extra-args
AS := $(GCCPATH)arm-none-eabi-gcc
LD := $(GCCPATH)arm-none-eabi-gcc
LDLIBS += -lm -lgcc -lc
# import rules to compile glyphs(/pone)
include $(BOLOS_SDK)/Makefile.glyphs
### variables processed by the common makefile.rules of the SDK to grab source files and include dirs
APP_SOURCE_PATH += src_common src src_features src_plugins
SDK_SOURCE_PATH += lib_stusb lib_stusb_impl lib_u2f
APP_SOURCE_PATH += src_common src src_features src_plugins
SDK_SOURCE_PATH += lib_stusb lib_stusb_impl lib_u2f
ifeq ($(TARGET_NAME),TARGET_STAX)
APP_SOURCE_PATH += src_nbgl
APP_SOURCE_PATH += src_nbgl
else
SDK_SOURCE_PATH += lib_ux
APP_SOURCE_PATH += src_bagl
SDK_SOURCE_PATH += lib_ux
APP_SOURCE_PATH += src_bagl
endif
# Allow usage of function from lib_standard_app/crypto_helpers.c
APP_SOURCE_FILES += ${BOLOS_SDK}/lib_standard_app/crypto_helpers.c
### initialize plugin SDK submodule if needed, rebuild it, and warn if a difference is noticed
ifeq ($(CHAIN),ethereum)
ifneq ($(shell git submodule status | grep '^[-+]'),)
$(info INFO: Need to reinitialize git submodules)
$(shell git submodule update --init)
endif
ifneq ($(shell git submodule status | grep '^[-+]'),)
$(info INFO: Need to reinitialize git submodules)
$(shell git submodule update --init)
endif
# rebuild SDK
$(shell python3 tools/build_sdk.py)
# rebuild SDK
$(shell ./tools/build_sdk.sh)
# check if a difference is noticed (fail if it happens in CI build)
ifneq ($(shell git status | grep 'ethereum-plugin-sdk'),)
ifneq ($(JENKINS_URL),)
$(error ERROR: please update ethereum-plugin-sdk submodule first)
else
$(warning WARNING: please update ethereum-plugin-sdk submodule first)
endif
endif
# check if a difference is noticed (fail if it happens in CI build)
ifneq ($(shell git status | grep 'ethereum-plugin-sdk'),)
ifneq ($(JENKINS_URL),)
$(error ERROR: please update ethereum-plugin-sdk submodule first)
else
$(warning WARNING: please update ethereum-plugin-sdk submodule first)
endif
endif
endif
load: all
@@ -279,6 +306,16 @@ test: install_tests run_tests
unit-test:
make -C tests/unit
ifeq ($(TARGET_NAME),TARGET_STAX)
NETWORK_ICONS_FILE = $(GEN_SRC_DIR)/net_icons.gen.c
NETWORK_ICONS_DIR = $(shell dirname "$(NETWORK_ICONS_FILE)")
$(NETWORK_ICONS_FILE):
$(shell python3 tools/gen_networks.py "$(NETWORK_ICONS_DIR)")
APP_SOURCE_FILES += $(NETWORK_ICONS_FILE)
endif
# import generic rules from the sdk
include $(BOLOS_SDK)/Makefile.rules

View File

@@ -49,7 +49,7 @@ Ledger Blue is not maintained anymore, but the app can still be compiled for thi
This app follows the specification available in the `doc/` folder.
To compile it and load it on a device, please check out our [developer portal](https://developers.ledger.com/docs/nano-app/introduction/).
To compile it and load it on a device, please check out our [developer portal](https://developers.ledger.com/docs/device-app/introduction).
### Plugins

4
client/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*egg-info
dist
*wheel
*~

53
client/CHANGELOG.md Normal file
View File

@@ -0,0 +1,53 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.0] - 2024-02-13
### Added
- New `provide_token_metadata` function
### Fixed
- Increased the delay between `autonext` callback calls for EIP-712 on Stax
- `recover_transaction` util function for non-legacy transactions
## [0.2.1] - 2023-12-01
### Fixed
- v0.2.0 version already published on pypi.org
## [0.2.0] - 2023-12-01
### Added
- New generic `sign` function that uses the Web3.py library
### Removed
- `sign_legacy` & `sign_1559` functions
### Fixed
- Now uses the proper signing key for the `SET_EXTERNAL_PLUGIN` APDU
### Changed
- `get_public_addr` now returns address as `bytes` instead of `str`
## [0.1.0] - 2023-10-30
### Added
- Update the ragger app client to support "set external plugin" APDU
## [0.0.1] - 2023-07-08
### Added
- Initial version

1
client/MANIFEST.in Normal file
View File

@@ -0,0 +1 @@
include src/ledger_app_clients/ethereum/keychain/*

20
client/README.md Normal file
View File

@@ -0,0 +1,20 @@
# Python client for the Ledger Ethereum application
This package allows to communicate with the Ledger Ethereum application, either on a
real device, or emulated on Speculos.
## Installation from pypi.org
You can install the client from `pypi.org`:
```bash
pip install ledger_app_clients.ethereum
```
## Installation from sources
You can also install the client from this repo:
```bash
pip install .
```

51
client/pyproject.toml Normal file
View File

@@ -0,0 +1,51 @@
[build-system]
requires = [
"setuptools>=45",
"setuptools_scm[toml]>=6.2",
"wheel"
]
build-backend = "setuptools.build_meta"
[project]
name = "ledger_app_clients.ethereum"
authors = [
{ name = "Ledger", email = "hello@ledger.fr" }
]
description = "Ledger Ethereum Python client"
readme = { file = "README.md", content-type = "text/markdown" }
# license = { file = "LICENSE" }
classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS :: MacOS X",
]
dynamic = [ "version" ]
requires-python = ">=3.7"
dependencies = [
"ragger[speculos]",
"web3~=6.0",
]
[tools.setuptools]
include-package-data = true
[tool.setuptools_scm]
version_file = "src/ledger_app_clients/ethereum/__version__.py"
local_scheme = "no-local-version"
root = "../"
git_describe_command = "git describe --dirty --tags --long --match client-*[0-9]*"
fallback_version = "0.0.0"
[project.urls]
Home = "https://github.com/LedgerHQ/app-ethereum"
[tool.mypy]
ignore_missing_imports = true
[tool.flake8]
max-line-length = 120

View File

@@ -0,0 +1,4 @@
try:
from ledger_app_clients.ethereum.__version__ import __version__ # noqa
except ImportError:
__version__ = "unknown version" # noqa

View File

@@ -0,0 +1,255 @@
import rlp
from enum import IntEnum
from ragger.backend import BackendInterface
from ragger.utils import RAPDU
from typing import Optional
from .command_builder import CommandBuilder
from .eip712 import EIP712FieldType
from .keychain import sign_data, Key
from .tlv import format_tlv
from web3 import Web3
class StatusWord(IntEnum):
OK = 0x9000
ERROR_NO_INFO = 0x6a00
INVALID_DATA = 0x6a80
INSUFFICIENT_MEMORY = 0x6a84
INVALID_INS = 0x6d00
INVALID_P1_P2 = 0x6b00
CONDITION_NOT_SATISFIED = 0x6985
REF_DATA_NOT_FOUND = 0x6a88
class DomainNameTag(IntEnum):
STRUCTURE_TYPE = 0x01
STRUCTURE_VERSION = 0x02
CHALLENGE = 0x12
SIGNER_KEY_ID = 0x13
SIGNER_ALGO = 0x14
SIGNATURE = 0x15
DOMAIN_NAME = 0x20
COIN_TYPE = 0x21
ADDRESS = 0x22
class EthAppClient:
def __init__(self, client: BackendInterface):
self._client = client
self._cmd_builder = CommandBuilder()
def _send(self, payload: bytes):
return self._client.exchange_async_raw(payload)
def response(self) -> Optional[RAPDU]:
return self._client.last_async_response
def eip712_send_struct_def_struct_name(self, name: str):
return self._send(self._cmd_builder.eip712_send_struct_def_struct_name(name))
def eip712_send_struct_def_struct_field(self,
field_type: EIP712FieldType,
type_name: str,
type_size: int,
array_levels: list,
key_name: str):
return self._send(self._cmd_builder.eip712_send_struct_def_struct_field(
field_type,
type_name,
type_size,
array_levels,
key_name))
def eip712_send_struct_impl_root_struct(self, name: str):
return self._send(self._cmd_builder.eip712_send_struct_impl_root_struct(name))
def eip712_send_struct_impl_array(self, size: int):
return self._send(self._cmd_builder.eip712_send_struct_impl_array(size))
def eip712_send_struct_impl_struct_field(self, raw_value: bytes):
chunks = self._cmd_builder.eip712_send_struct_impl_struct_field(bytearray(raw_value))
for chunk in chunks[:-1]:
with self._send(chunk):
pass
return self._send(chunks[-1])
def eip712_sign_new(self, bip32_path: str):
return self._send(self._cmd_builder.eip712_sign_new(bip32_path))
def eip712_sign_legacy(self,
bip32_path: str,
domain_hash: bytes,
message_hash: bytes):
return self._send(self._cmd_builder.eip712_sign_legacy(bip32_path,
domain_hash,
message_hash))
def eip712_filtering_activate(self):
return self._send(self._cmd_builder.eip712_filtering_activate())
def eip712_filtering_message_info(self, name: str, filters_count: int, sig: bytes):
return self._send(self._cmd_builder.eip712_filtering_message_info(name, filters_count, sig))
def eip712_filtering_show_field(self, name: str, sig: bytes):
return self._send(self._cmd_builder.eip712_filtering_show_field(name, sig))
def sign(self,
bip32_path: str,
tx_params: dict):
tx = Web3().eth.account.create().sign_transaction(tx_params).rawTransaction
prefix = bytes()
suffix = []
if tx[0] in [0x01, 0x02]:
prefix = tx[:1]
tx = tx[len(prefix):]
else: # legacy
if "chainId" in tx_params:
suffix = [int(tx_params["chainId"]), bytes(), bytes()]
decoded = rlp.decode(tx)[:-3] # remove already computed signature
tx = prefix + rlp.encode(decoded + suffix)
chunks = self._cmd_builder.sign(bip32_path, tx, suffix)
for chunk in chunks[:-1]:
with self._send(chunk):
pass
return self._send(chunks[-1])
def get_challenge(self):
return self._send(self._cmd_builder.get_challenge())
def get_public_addr(self,
display: bool = True,
chaincode: bool = False,
bip32_path: str = "m/44'/60'/0'/0/0",
chain_id: Optional[int] = None):
return self._send(self._cmd_builder.get_public_addr(display,
chaincode,
bip32_path,
chain_id))
def provide_domain_name(self, challenge: int, name: str, addr: bytes):
payload = format_tlv(DomainNameTag.STRUCTURE_TYPE, 3) # TrustedDomainName
payload += format_tlv(DomainNameTag.STRUCTURE_VERSION, 1)
payload += format_tlv(DomainNameTag.SIGNER_KEY_ID, 0) # test key
payload += format_tlv(DomainNameTag.SIGNER_ALGO, 1) # secp256k1
payload += format_tlv(DomainNameTag.CHALLENGE, challenge)
payload += format_tlv(DomainNameTag.COIN_TYPE, 0x3c) # ETH in slip-44
payload += format_tlv(DomainNameTag.DOMAIN_NAME, name)
payload += format_tlv(DomainNameTag.ADDRESS, addr)
payload += format_tlv(DomainNameTag.SIGNATURE,
sign_data(Key.DOMAIN_NAME, payload))
chunks = self._cmd_builder.provide_domain_name(payload)
for chunk in chunks[:-1]:
with self._send(chunk):
pass
return self._send(chunks[-1])
def set_plugin(self,
plugin_name: str,
contract_addr: bytes,
selector: bytes,
chain_id: int,
type_: int = 1,
version: int = 1,
key_id: int = 2,
algo_id: int = 1,
sig: Optional[bytes] = None):
if sig is None:
# Temporarily get a command with an empty signature to extract the payload and
# compute the signature on it
tmp = self._cmd_builder.set_plugin(type_,
version,
plugin_name,
contract_addr,
selector,
chain_id,
key_id,
algo_id,
bytes())
# skip APDU header & empty sig
sig = sign_data(Key.SET_PLUGIN, tmp[5:-1])
return self._send(self._cmd_builder.set_plugin(type_,
version,
plugin_name,
contract_addr,
selector,
chain_id,
key_id,
algo_id,
sig))
def provide_nft_metadata(self,
collection: str,
addr: bytes,
chain_id: int,
type_: int = 1,
version: int = 1,
key_id: int = 1,
algo_id: int = 1,
sig: Optional[bytes] = None):
if sig is None:
# Temporarily get a command with an empty signature to extract the payload and
# compute the signature on it
tmp = self._cmd_builder.provide_nft_information(type_,
version,
collection,
addr,
chain_id,
key_id,
algo_id,
bytes())
# skip APDU header & empty sig
sig = sign_data(Key.NFT, tmp[5:-1])
return self._send(self._cmd_builder.provide_nft_information(type_,
version,
collection,
addr,
chain_id,
key_id,
algo_id,
sig))
def set_external_plugin(self,
plugin_name: str,
contract_address: bytes,
method_selelector: bytes,
sig: Optional[bytes] = None):
if sig is None:
# Temporarily get a command with an empty signature to extract the payload and
# compute the signature on it
tmp = self._cmd_builder.set_external_plugin(plugin_name, contract_address, method_selelector, bytes())
# skip APDU header & empty sig
sig = sign_data(Key.CAL, tmp[5:])
return self._send(self._cmd_builder.set_external_plugin(plugin_name, contract_address, method_selelector, sig))
def personal_sign(self, path: str, msg: bytes):
chunks = self._cmd_builder.personal_sign(path, msg)
for chunk in chunks[:-1]:
with self._send(chunk):
pass
return self._send(chunks[-1])
def provide_token_metadata(self,
ticker: str,
addr: bytes,
decimals: int,
chain_id: int,
sig: Optional[bytes] = None):
if sig is None:
# Temporarily get a command with an empty signature to extract the payload and
# compute the signature on it
tmp = self._cmd_builder.provide_erc20_token_information(ticker,
addr,
decimals,
chain_id,
bytes())
# skip APDU header & empty sig
sig = sign_data(Key.CAL, tmp[6:])
return self._send(self._cmd_builder.provide_erc20_token_information(ticker,
addr,
decimals,
chain_id,
sig))

View File

@@ -1,17 +1,29 @@
from enum import IntEnum, auto
from typing import Iterator, Optional
from .eip712 import EIP712FieldType
from ragger.bip import pack_derivation_path
# documentation about APDU format is available here:
# https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc
import struct
from enum import IntEnum
from typing import Optional
from ragger.bip import pack_derivation_path
from .eip712 import EIP712FieldType
class InsType(IntEnum):
GET_PUBLIC_ADDR = 0x02
SIGN = 0x04
PERSONAL_SIGN = 0x08
PROVIDE_ERC20_TOKEN_INFORMATION = 0x0a
PROVIDE_NFT_INFORMATION = 0x14
SET_PLUGIN = 0x16
EIP712_SEND_STRUCT_DEF = 0x1a
EIP712_SEND_STRUCT_IMPL = 0x1c
EIP712_SEND_FILTERING = 0x1e
EIP712_SIGN = 0x0c
GET_CHALLENGE = 0x20
PROVIDE_DOMAIN_NAME = 0x22
EXTERNAL_PLUGIN_SETUP = 0x12
class P1Type(IntEnum):
COMPLETE_SEND = 0x00
@@ -19,6 +31,7 @@ class P1Type(IntEnum):
SIGN_FIRST_CHUNK = 0x00
SIGN_SUBSQT_CHUNK = 0x80
class P2Type(IntEnum):
STRUCT_NAME = 0x00
STRUCT_FIELD = 0xff
@@ -29,6 +42,7 @@ class P2Type(IntEnum):
FILTERING_CONTRACT_NAME = 0x0f
FILTERING_FIELD_NAME = 0xff
class CommandBuilder:
_CLA: int = 0xE0
@@ -36,7 +50,7 @@ class CommandBuilder:
ins: InsType,
p1: int,
p2: int,
cdata: bytearray = bytes()) -> bytes:
cdata: bytes = bytes()) -> bytes:
header = bytearray()
header.append(self._CLA)
@@ -62,24 +76,24 @@ class CommandBuilder:
field_type: EIP712FieldType,
type_name: str,
type_size: int,
array_levels: [],
array_levels: list,
key_name: str) -> bytes:
data = bytearray()
typedesc = 0
typedesc |= (len(array_levels) > 0) << 7
typedesc |= (type_size != None) << 6
typedesc |= (type_size is not 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:
if type_size is not 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(0 if level is None else 1)
if level is not None:
data.append(level)
data.append(len(key_name))
data += self._string_to_bytes(key_name)
@@ -102,7 +116,7 @@ class CommandBuilder:
P2Type.ARRAY,
data)
def eip712_send_struct_impl_struct_field(self, data: bytearray) -> Iterator[bytes]:
def eip712_send_struct_impl_struct_field(self, data: bytearray) -> list[bytes]:
chunks = list()
# Add a 16-bit integer with the data's byte length (network byte order)
data_w_length = bytearray()
@@ -169,17 +183,40 @@ class CommandBuilder:
P2Type.FILTERING_FIELD_NAME,
self._eip712_filtering_send_name(name, sig))
def sign(self, bip32_path: str, rlp_data: bytes) -> list[bytes]:
def set_external_plugin(self, plugin_name: str, contract_address: bytes, selector: bytes, sig: bytes) -> bytes:
data = bytearray()
data.append(len(plugin_name))
data += self._string_to_bytes(plugin_name)
data += contract_address
data += selector
data += sig
return self._serialize(InsType.EXTERNAL_PLUGIN_SETUP,
P1Type.COMPLETE_SEND,
0x00,
data)
def sign(self, bip32_path: str, rlp_data: bytes, vrs: list) -> list[bytes]:
apdus = list()
payload = pack_derivation_path(bip32_path)
payload += rlp_data
p1 = P1Type.SIGN_FIRST_CHUNK
while len(payload) > 0:
chunk_size = 0xff
# TODO: Fix the app & remove this, issue #409
if len(vrs) == 3:
if len(payload) > chunk_size:
import rlp
diff = len(rlp.encode(vrs)) - (len(payload) - chunk_size)
if diff > 0:
chunk_size -= diff
apdus.append(self._serialize(InsType.SIGN,
p1,
0x00,
payload[:0xff]))
payload = payload[0xff:]
payload[:chunk_size]))
payload = payload[chunk_size:]
p1 = P1Type.SIGN_SUBSQT_CHUNK
return apdus
@@ -188,7 +225,7 @@ class CommandBuilder:
def provide_domain_name(self, tlv_payload: bytes) -> list[bytes]:
chunks = list()
payload = struct.pack(">H", len(tlv_payload))
payload = struct.pack(">H", len(tlv_payload))
payload += tlv_payload
p1 = 1
while len(payload) > 0:
@@ -199,3 +236,96 @@ class CommandBuilder:
payload = payload[0xff:]
p1 = 0
return chunks
def get_public_addr(self,
display: bool,
chaincode: bool,
bip32_path: str,
chain_id: Optional[int]) -> bytes:
payload = pack_derivation_path(bip32_path)
if chain_id is not None:
payload += struct.pack(">Q", chain_id)
return self._serialize(InsType.GET_PUBLIC_ADDR,
int(display),
int(chaincode),
payload)
def set_plugin(self,
type_: int,
version: int,
plugin_name: str,
contract_addr: bytes,
selector: bytes,
chain_id: int,
key_id: int,
algo_id: int,
sig: bytes) -> bytes:
payload = bytearray()
payload.append(type_)
payload.append(version)
payload.append(len(plugin_name))
payload += plugin_name.encode()
payload += contract_addr
payload += selector
payload += struct.pack(">Q", chain_id)
payload.append(key_id)
payload.append(algo_id)
payload.append(len(sig))
payload += sig
return self._serialize(InsType.SET_PLUGIN, 0x00, 0x00, payload)
def provide_nft_information(self,
type_: int,
version: int,
collection_name: str,
addr: bytes,
chain_id: int,
key_id: int,
algo_id: int,
sig: bytes):
payload = bytearray()
payload.append(type_)
payload.append(version)
payload.append(len(collection_name))
payload += collection_name.encode()
payload += addr
payload += struct.pack(">Q", chain_id)
payload.append(key_id)
payload.append(algo_id)
payload.append(len(sig))
payload += sig
return self._serialize(InsType.PROVIDE_NFT_INFORMATION, 0x00, 0x00, payload)
def personal_sign(self, path: str, msg: bytes):
payload = pack_derivation_path(path)
payload += struct.pack(">I", len(msg))
payload += msg
chunks = list()
p1 = P1Type.SIGN_FIRST_CHUNK
while len(payload) > 0:
chunk_size = 0xff
chunks.append(self._serialize(InsType.PERSONAL_SIGN,
p1,
0x00,
payload[:chunk_size]))
payload = payload[chunk_size:]
p1 = P1Type.SIGN_SUBSQT_CHUNK
return chunks
def provide_erc20_token_information(self,
ticker: str,
addr: bytes,
decimals: int,
chain_id: int,
sig: bytes) -> bytes:
payload = bytearray()
payload.append(len(ticker))
payload += ticker.encode()
payload += addr
payload += struct.pack(">I", decimals)
payload += struct.pack(">I", chain_id)
payload += sig
return self._serialize(InsType.PROVIDE_ERC20_TOKEN_INFORMATION,
0x00,
0x00,
payload)

View File

@@ -1,23 +1,27 @@
#!/usr/bin/env python3
import json
import sys
import re
import hashlib
from app.client import EthAppClient, EIP712FieldType
import keychain
from typing import Callable
import json
import re
import signal
import sys
import copy
from typing import Any, Callable, Optional
from ledger_app_clients.ethereum import keychain
from ledger_app_clients.ethereum.client import EthAppClient, EIP712FieldType
# global variables
app_client: EthAppClient = None
filtering_paths = None
current_path = list()
sig_ctx = {}
autonext_handler: Callable = None
filtering_paths: dict = {}
current_path: list[str] = list()
sig_ctx: dict[str, Any] = {}
def default_handler():
raise RuntimeError("Uninitialized handler")
autonext_handler: Callable = default_handler
# From a string typename, extract the type and all the array depth
@@ -57,29 +61,34 @@ def get_typesize(typename):
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:
if typesize is not None:
return (EIP712FieldType.FIX_BYTES, typesize)
return (EIP712FieldType.DYN_BYTES, None)
# set functions for each type
parsing_type_functions = {};
parsing_type_functions = {}
parsing_type_functions["int"] = parse_int
parsing_type_functions["uint"] = parse_uint
parsing_type_functions["address"] = parse_address
@@ -88,7 +97,6 @@ parsing_type_functions["string"] = parse_string
parsing_type_functions["bytes"] = parse_bytes
def send_struct_def_field(typename, keyname):
type_enum = None
@@ -110,7 +118,6 @@ def send_struct_def_field(typename, keyname):
return (typename, type_enum, typesize, array_lvls)
def encode_integer(value, typesize):
data = bytearray()
@@ -124,9 +131,9 @@ def encode_integer(value, typesize):
if value == 0:
data.append(0)
else:
if value < 0: # negative number, send it as unsigned
if value < 0: # negative number, send it as unsigned
mask = 0
for i in range(typesize): # make a mask as big as the typesize
for i in range(typesize): # make a mask as big as the typesize
mask = (mask << 8) | 0xff
value &= mask
while value > 0:
@@ -135,42 +142,51 @@ def encode_integer(value, typesize):
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
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
@@ -182,7 +198,6 @@ 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):
@@ -190,7 +205,6 @@ def send_struct_impl_field(value, field):
data = encoding_functions[field["enum"]](value, field["typesize"])
if filtering_paths:
path = ".".join(current_path)
if path in filtering_paths.keys():
@@ -201,8 +215,7 @@ def send_struct_impl_field(value, field):
disable_autonext()
def evaluate_field(structs, data, field, lvls_left, new_level = True):
def evaluate_field(structs, data, field, lvls_left, new_level=True):
array_lvls = field["array_lvls"]
if new_level:
@@ -217,7 +230,7 @@ def evaluate_field(structs, data, field, lvls_left, new_level = True):
return False
current_path.pop()
idx += 1
if array_lvls[lvls_left - 1] != None:
if array_lvls[lvls_left - 1] is not None:
if array_lvls[lvls_left - 1] != idx:
print("Mismatch in array size! Got %d, expected %d\n" %
(idx, array_lvls[lvls_left - 1]),
@@ -234,7 +247,6 @@ def evaluate_field(structs, data, field, lvls_left, new_level = True):
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():
@@ -246,6 +258,7 @@ def send_struct_impl(structs, data, structname):
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
@@ -264,6 +277,7 @@ def send_filtering_message_info(display_name: str, filters_count: int):
enable_autonext()
disable_autonext()
# ledgerjs doesn't actually sign anything, and instead uses already pre-computed signatures
def send_filtering_show_field(display_name):
global sig_ctx
@@ -283,11 +297,6 @@ def send_filtering_show_field(display_name):
with app_client.eip712_filtering_show_field(display_name, sig):
pass
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
@@ -297,12 +306,14 @@ def prepare_filtering(filtr_data, message):
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
@@ -316,7 +327,7 @@ def init_signature_context(types, domain):
for i in range(8):
sig_ctx["chainid"].append(chainid & (0xff << (i * 8)))
sig_ctx["chainid"].reverse()
schema_str = json.dumps(types).replace(" ","")
schema_str = json.dumps(types).replace(" ", "")
schema_hash = hashlib.sha224(schema_str.encode())
sig_ctx["schema_hash"] = bytearray.fromhex(schema_hash.hexdigest())
@@ -324,74 +335,74 @@ def init_signature_context(types, domain):
def next_timeout(_signum: int, _frame):
autonext_handler()
def enable_autonext():
seconds = 1/4
if app_client._client.firmware.device == 'stax': # Stax Speculos is slow
interval = seconds * 3
if app_client._client.firmware.device == 'stax': # Stax Speculos is slow
delay = 1.5
else:
interval = seconds
signal.setitimer(signal.ITIMER_REAL, seconds, interval)
delay = 1/4
signal.setitimer(signal.ITIMER_REAL, delay, delay)
def disable_autonext():
signal.setitimer(signal.ITIMER_REAL, 0, 0)
def process_file(aclient: EthAppClient,
input_file_path: str,
filtering_file_path = None,
autonext: Callable = None) -> bool:
def process_data(aclient: EthAppClient,
data_json: dict,
filters: Optional[dict] = None,
autonext: Optional[Callable] = None) -> bool:
global sig_ctx
global app_client
global autonext_handler
# deepcopy because this function modifies the dict
data_json = copy.deepcopy(data_json)
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"]
domain_typename = "EIP712Domain"
message_typename = data_json["primaryType"]
types = data_json["types"]
domain = data_json["domain"]
message = data_json["message"]
if autonext:
autonext_handler = autonext
signal.signal(signal.SIGALRM, next_timeout)
if autonext:
autonext_handler = autonext
signal.signal(signal.SIGALRM, next_timeout)
if filtering_file_path:
init_signature_context(types, domain)
filtr = read_filtering_file(domain, message, filtering_file_path)
if filters:
init_signature_context(types, domain)
# send types definition
for key in types.keys():
with app_client.eip712_send_struct_def_struct_name(key):
pass
for f in types[key]:
(f["type"], f["enum"], f["typesize"], f["array_lvls"]) = \
send_struct_def_field(f["type"], f["name"])
# send types definition
for key in types.keys():
with app_client.eip712_send_struct_def_struct_name(key):
pass
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:
with app_client.eip712_filtering_activate():
pass
prepare_filtering(filtr, message)
if filters:
with app_client.eip712_filtering_activate():
pass
prepare_filtering(filters, message)
# send domain implementation
with app_client.eip712_send_struct_impl_root_struct(domain_typename):
enable_autonext()
disable_autonext()
if not send_struct_impl(types, domain, domain_typename):
return False
# send domain implementation
with app_client.eip712_send_struct_impl_root_struct(domain_typename):
enable_autonext()
disable_autonext()
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))
if filters:
if filters and "name" in filters:
send_filtering_message_info(filters["name"], len(filtering_paths))
else:
send_filtering_message_info(domain["name"], len(filtering_paths))
# send message implementation
with app_client.eip712_send_struct_impl_root_struct(message_typename):
enable_autonext()
disable_autonext()
if not send_struct_impl(types, message, message_typename):
return False
# send message implementation
with app_client.eip712_send_struct_impl_root_struct(message_typename):
enable_autonext()
disable_autonext()
if not send_struct_impl(types, message, message_typename):
return False
return True

View File

@@ -0,0 +1 @@
from .struct import EIP712FieldType # noqa

View File

@@ -1,5 +1,6 @@
from enum import IntEnum, auto
class EIP712FieldType(IntEnum):
CUSTOM = 0,
INT = auto()

View File

@@ -1,23 +1,29 @@
import os
import hashlib
from ecdsa.util import sigencode_der
from ecdsa import SigningKey
from ecdsa.util import sigencode_der
from enum import Enum, auto
# Private key PEM files have to be named the same (lowercase) as their corresponding enum entries
# Example: for an entry in the Enum named DEV, its PEM file must be at keychain/dev.pem
class Key(Enum):
CAL = auto()
DOMAIN_NAME = auto()
SET_PLUGIN = auto()
NFT = auto()
_keys: dict[Key, SigningKey] = dict()
# Open the corresponding PEM file and load its key in the global dict
def _init_key(key: Key):
global _keys
with open("%s/keychain/%s.pem" % (os.path.dirname(__file__), key.name.lower())) as pem_file:
_keys[key] = SigningKey.from_pem(pem_file.read(), hashlib.sha256)
assert (key in _keys) and (_keys[key] != None)
assert (key in _keys) and (_keys[key] is not None)
# Generate a SECP256K1 signature of the given data with the given key
def sign_data(key: Key, data: bytes) -> bytes:

View File

@@ -0,0 +1,8 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIK69Gt4o0bzkOaEwUE5X2tI+Ks80FQi785Co+6woU9hioAcGBSuBBAAK
oUQDQgAEPPtfsxkF9L052dU1pAwmqrUcXX0yGbKKyUK5gPsgbPswtRzC3iEZrAOO
uw191lQXcCBKPO06eeKLMvu2cmRowA==
-----END EC PRIVATE KEY-----

View File

@@ -0,0 +1,8 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIBErwcYvqeKSOlmQ/j3xPkVcwFf+j1aiMsA+RabczvN7oAcGBSuBBAAK
oUQDQgAEwFW8Ts8FXi2FCF01Eno95nBcf4hQVc1wceh2cb8ZH+M8yPAavC8ofIGa
FIq+G1gd8bSUCvXU3DpOa2AZF3ErNw==
-----END EC PRIVATE KEY-----

View File

@@ -0,0 +1,52 @@
def signature(data: bytes) -> tuple[bytes, bytes, 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]
return v, r, s
def challenge(data: bytes) -> int:
assert len(data) == 4
return int.from_bytes(data, "big")
def pk_addr(data: bytes, has_chaincode: bool = False):
idx = 0
if len(data) < (idx + 1):
return None
pk_len = data[idx]
idx += 1
if len(data) < (idx + pk_len):
return None
pk = data[idx:idx + pk_len]
idx += pk_len
if len(data) < (idx + 1):
return None
addr_len = data[idx]
idx += 1
if len(data) < (idx + addr_len):
return None
addr = data[idx:idx + addr_len]
idx += addr_len
if has_chaincode:
if len(data) < (idx + 32):
return None
chaincode = data[idx:idx + 32]
idx += 32
else:
chaincode = None
if idx != len(data):
return None
return pk, bytes.fromhex(addr.decode()), chaincode

View File

@@ -1,7 +1,8 @@
from enum import Enum, auto
from typing import List
from ragger.firmware import Firmware
from ragger.navigator import Navigator, NavInsID, NavIns
from typing import Union
class SettingID(Enum):
BLIND_SIGNING = auto()
@@ -10,6 +11,7 @@ class SettingID(Enum):
VERBOSE_EIP712 = auto()
VERBOSE_ENS = auto()
def get_device_settings(device: str) -> list[SettingID]:
if device == "nanos":
return [
@@ -27,19 +29,22 @@ def get_device_settings(device: str) -> list[SettingID]:
]
return []
settings_per_page = 3
def get_setting_position(device: str, setting: NavInsID) -> tuple[int, int]:
screen_height = 672 # px
header_height = 85 # px
footer_height = 124 # px
def get_setting_position(device: str, setting: Union[NavInsID, SettingID]) -> tuple[int, int]:
screen_height = 672 # px
header_height = 85 # px
footer_height = 124 # px
usable_height = screen_height - (header_height + footer_height)
setting_height = usable_height // settings_per_page
index_in_page = get_device_settings(device).index(setting) % settings_per_page
index_in_page = get_device_settings(device).index(SettingID(setting)) % settings_per_page
return 350, header_height + (setting_height * index_in_page) + (setting_height // 2)
def settings_toggle(fw: Firmware, nav: Navigator, to_toggle: list[SettingID]):
moves = list()
moves: list[Union[NavIns, NavInsID]] = list()
settings = get_device_settings(fw.device)
# Assume the app is on the home page
if fw.device.startswith("nano"):
@@ -49,7 +54,7 @@ def settings_toggle(fw: Firmware, nav: Navigator, to_toggle: list[SettingID]):
if setting in to_toggle:
moves += [NavInsID.BOTH_CLICK]
moves += [NavInsID.RIGHT_CLICK]
moves += [NavInsID.BOTH_CLICK] # Back
moves += [NavInsID.BOTH_CLICK] # Back
else:
moves += [NavInsID.USE_CASE_HOME_SETTINGS]
moves += [NavInsID.USE_CASE_SETTINGS_NEXT]

View File

@@ -1,4 +1,5 @@
from typing import Any
from typing import Union
def der_encode(value: int) -> bytes:
# max() to have minimum length of 1
@@ -7,16 +8,15 @@ def der_encode(value: int) -> bytes:
value_bytes = (0x80 | len(value_bytes)).to_bytes(1, 'big') + value_bytes
return value_bytes
def format_tlv(tag: int, value: Any) -> bytes:
def format_tlv(tag: int, value: Union[int, str, bytes]) -> bytes:
if isinstance(value, int):
# max() to have minimum length of 1
value = value.to_bytes(max(1, (value.bit_length() + 7) // 8), 'big')
elif isinstance(value, str):
value = value.encode()
if not isinstance(value, bytes):
print("Unhandled TLV formatting for type : %s" % (type(value)))
return None
assert isinstance(value, bytes), f"Unhandled TLV formatting for type : {type(value)}"
tlv = bytearray()
tlv += der_encode(tag)

View File

@@ -0,0 +1,39 @@
from eth_account import Account
from eth_account.messages import encode_defunct, encode_typed_data
import rlp
def get_selector_from_data(data: str) -> bytes:
raw_data = bytes.fromhex(data[2:])
return raw_data[:4]
def recover_message(msg, vrs: tuple) -> bytes:
if isinstance(msg, dict): # EIP-712
smsg = encode_typed_data(full_message=msg)
else: # EIP-191
smsg = encode_defunct(primitive=msg)
addr = Account.recover_message(smsg, vrs)
return bytes.fromhex(addr[2:])
def recover_transaction(tx_params, vrs: tuple) -> bytes:
raw_tx = Account.create().sign_transaction(tx_params).rawTransaction
prefix = bytes()
if raw_tx[0] in [0x01, 0x02]:
prefix = raw_tx[:1]
raw_tx = raw_tx[len(prefix):]
# v is returned on one byte only so it might have overflowed
# in that case, we will reconstruct it to its full value
if "chainId" in tx_params:
trunc_chain_id = tx_params["chainId"]
while trunc_chain_id.bit_length() > 32:
trunc_chain_id >>= 8
target = tx_params["chainId"] * 2 + 35
trunc_target = trunc_chain_id * 2 + 35
diff = vrs[0][0] - (trunc_target & 0xff)
vrs = (target + diff, vrs[1], vrs[2])
decoded = rlp.decode(raw_tx)
reencoded = rlp.encode(decoded[:-3] + list(vrs))
addr = Account.recover_transaction(prefix + reencoded)
return bytes.fromhex(addr[2:])

View File

@@ -6,24 +6,24 @@ Application version 1.3.0 - 05th of July 2020
## 1.3.0
- Initial release
## About
## About
This document described how a specific device UI for a smart contract can be added in the current version of the Ethereum application, before plugins are added
## Standard support
The applications already includes dedicated UI support for those specific contract calls :
The applications already includes dedicated UI support for those specific contract calls :
* ERC 20 approve(address, uint256) - implementation in *src_features/erc20_approval*
* ERC 20 transfer(address, uint256) - implementation in *src_features/signTx*
## Requirements
The following data is necessary for a specific contract support
The following data is necessary for a specific contract support
* Smart contract ABI (at least for calls that are to be supported by the application)
The following data is optional for a specific contract support
The following data is optional for a specific contract support
* Contract address (can be optional if supported by multiple instances)
@@ -38,60 +38,11 @@ The first four bytes of the call data for a function call specifies the function
The following online tool can be used to compute selectors https://emn178.github.io/online-tools/keccak_256.html
## Limitations
* The total number of parameters of a contract call is 4 (as defined in *src/shared_context.h* for tokenContext_t.data, each parameter being encoded as an uint256). Additional parameters can be supported if not required to handle the displaying logic (see "Also handle exception that only need to process the beginning of data" in *src_features/signTx/logic_signTx.c* for CONTRACT_STARKWARE_VERIFY_ESCAPE and CONTRACT_STARKWARE_REGISTER)
* Non fixed size types (dynamic sized byte sequences, dynamic sized strings, variable length arrays) are not directly supported
## Sample implementation of a function call
This example describes how to implement a specific UI for a contract call. This exemple is using Starkex token deposit - *deposit(uint256 tokenId, uint256 vaultId, uint256 quantizedAmount)*
* Implement the specific UI logic in a new *src_features* subdirectory - note that strings.common.maxFee will be provisioned with the maximum fee to pay for this call
```
See src_features/stark_contract_deposit for this example
```
* Compute the selector
```
keccak-256("deposit(uint256,uint256,uint256)") = 00aeef8a...
```
* Add an entry to the contract_call_t enum of *src/shared_context.h* for this function call
```
CONTRACT_STARKWARE_DEPOSIT_TOKEN
```
* Check for the selector being called and the arguments size in *src_features/signTx/logic_signTx.c* customProcessor function - additional logic can be applied (quantumSet in this example)
```C
if ((context->currentFieldLength == STARKWARE_DEPOSIT_TOKEN_DATA_SIZE) &&
(memcmp(context->workBuffer, STARKWARE_DEPOSIT_TOKEN_ID, 4) == 0) &&
quantumSet) {
contractProvisioned = CONTRACT_STARKWARE_DEPOSIT_TOKEN;
}
```
* Call the dedicated UI at the end of the transaction parsing in *src_features/signTx/logic_signTx.c* finalizeParsing function
```C
if (contractProvisioned == CONTRACT_STARKWARE_DEPOSIT_TOKEN) {
ux_flow_init(0, ux_approval_starkware_deposit_flow, NULL);
return;
}
```
## Using ERC 20 token tickers
A UI implementation might want to convert an ERC 20 token contract address to a ticker for easier validation
2 tickers can be temporarily provisioned to the application by using the PROVIDE ERC 20 TOKEN INFORMATION APDU, described in *src_features/provideErc20TokenInformation* - the UI can then iterate on the provisioned tickers to display relevant information to the user
2 tickers can be temporarily provisioned to the application by using the PROVIDE ERC 20 TOKEN INFORMATION APDU, described in *src_features/provideErc20TokenInformation* - the UI can then iterate on the provisioned tickers to display relevant information to the user
The same mechanism will be extended to support well known contract addresses in the future

View File

@@ -1,348 +0,0 @@
Ethereum application : Starkware extensions
============================================
Ledger Firmware Team <hello@ledger.fr>
Application version 1.5.0 - 4th of October 2020
## 1.3.0
- Initial release
## 1.5.0
- Update with Starkex v2 APIs
## About
This specification describes the APDU messages interface implementing the Starkware extensions for the Ethereum appilcation
## Modified general purpose APDUs
### GET APP CONFIGURATION
#### Description
This command returns specific application configuration
It is modified to notify Stark extensions support on flag 0x04
#### 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
0x04 : Stark extensions are supported
0x08 : Stark protocol v2 is supported
| 01
| Application major version | 01
| Application minor version | 01
| Application patch version | 01
|==============================================================================================================================
## Additional APDUs
Additional APDUs use the APDU CLA F0
### GET STARK PUBLIC KEY
#### Description
This command returns the public Stark key (X and Y coordinates) for the given BIP 32 path.
The key can be optionally checked on the device before being returned - in that case, only the X coordinate is displayed, as this is what is used in the contract
#### Coding
'Command'
[width="80%"]
|==============================================================================================================================
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
| F0 | 02 | 00 : return address
01 : display address 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*
| Stark key | 65
|==============================================================================================================================
### SIGN STARK MESSAGE
#### Description
This command signs an order or a transfer on the Starkware curve.
The contract addressed associated to the token shall have be provisioned previously with the PROVIDE ERC 20 TOKEN INFORMATION command or this command will fail.
The quantum type for v2 messages is encoded as p1 for the PROVIDE QUANTUM command
#### Coding
'Command'
[width="80%"]
|==============================================================================================================================
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
| F0 | 04 |
01 : sign a Stark Order (protocol v1, handles ETH and regular ERC 20)
02 : sign a Stark Transfer (protocol v1, handles ETH and regular ERC 20)
03 : sign a Stark Order (since protocol v2)
04 : sign a Stark Transfer (since protocol v2)
05 : sign a Stark Conditional Transfer (since protocol v2)
| 00 | variable | variable
|==============================================================================================================================
'Input data for a Stark Order (v1)'
[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
| Contract address of the token to be sold (or 00..00 for ETH) | 20
| Quantization of the token to be sold (big endian) | 32
| Contract address of the token to be bought (or 00..00 for ETH) | 20
| Quantization of the token to be bought (big endian) | 32
| ID of the source vault (big endian encoded) | 4
| ID of the destination vault (big endian encoded) | 4
| Amount to be sold (big endian encoded) | 8
| Amount to buy (big endian encoded) | 8
| Transaction nonce (big endian encoded) | 4
| Transaction timestamp (big endian encoded) | 4
|==============================================================================================================================
'Input data for a Stark Transfer (v1)'
[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
| Contract address of the token to be transferred (or 00..00 for ETH) | 20
| Quantization of the token to be transferred (big endian) | 32
| Token target public key | 32
| ID of the source vault (big endian encoded) | 4
| ID of the destination vault (big endian encoded) | 4
| Amount to be transferred (big endian encoded) | 8
| Transaction nonce (big endian encoded) | 4
| Transaction timestamp (big endian encoded) | 4
|==============================================================================================================================
'Input data for a Stark Order (v2)'
[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
| Quantization type of the token to be sold | 1
| Contract address of the token to be sold (or 00..00 for ETH) | 20
| Quantization or Token ID of the token to be sold (big endian) | 32
| Minting blob of the token to be sold (ignored if non mintable) | 32
| Quantization type of the token to be bought | 1
| Contract address of the token to be bought (or 00..00 for ETH) | 20
| Quantization or Token ID of the token to be bought (big endian) | 32
| Minting blob of the token to be bought (ignored if non mintable) | 32
| ID of the source vault (big endian encoded) | 4
| ID of the destination vault (big endian encoded) | 4
| Amount to be sold (big endian encoded) | 8
| Amount to buy (big endian encoded) | 8
| Transaction nonce (big endian encoded) | 4
| Transaction timestamp (big endian encoded) | 4
|==============================================================================================================================
'Input data for a Stark Transfer (v2)'
[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
| Quantization type of the token to be transferred | 1
| Contract address of the token to be transferred (or 00..00 for ETH) | 20
| Quantization or Token ID of the token to be transferred (big endian) | 32
| Minting blob of the token to be transferred (ignored if non mintable) | 32
| Token target public key | 32
| ID of the source vault (big endian encoded) | 4
| ID of the destination vault (big endian encoded) | 4
| Amount to be transferred (big endian encoded) | 8
| Transaction nonce (big endian encoded) | 4
| Transaction timestamp (big endian encoded) | 4
|==============================================================================================================================
'Input data for a Stark Conditional Transfer'
[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
| Quantization type of the token to be transferred | 1
| Contract address of the token to be transferred (or 00..00 for ETH) | 20
| Quantization or Token ID of the token to be transferred (big endian) | 32
| Minting blob of the token to be transferred (ignored if non mintable) | 32
| Token target public key | 32
| ID of the source vault (big endian encoded) | 4
| ID of the destination vault (big endian encoded) | 4
| Amount to be transferred (big endian encoded) | 8
| Transaction nonce (big endian encoded) | 4
| Transaction timestamp (big endian encoded) | 4
| Conditional transfer fact (big endian) | 32
| Conditional transfer L1 condition logic address | 20
|==============================================================================================================================
'Output data'
[width="80%"]
|==============================================================================================================================
| *Description* | *Length*
| RFU (00) | 1
| r | 32
| s | 32
|==============================================================================================================================
### PROVIDES QUANTUM
#### Description
This command provides quantization data used to compute a tokenId and provide additional information to the user before signing a transaction performing a deposit or withdrawal call on a Stark powered smart contract.
It shall be called following a PROVIDE ERC 20 TOKEN INFORMATION command called for the associated contract
#### Coding
'Command'
[width="80%"]
|==============================================================================================================================
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
| F0 | 08 |
00 : legacy (protocol v1, handles ETH and regular ERC 20)
01 : quantum encoded for ETH (since protocol v2)
02 : quantum encoded for a regular ERC 20 (since protocol v2)
03 : quantum encoded for a regular ERC 721 (since protocol v2)
04 : quantum encoded for a mintable ERC 20 (since protocol v2)
05 : quantum encoded for a mintable ERC 721 (since protocol v2)
| 00 | variable | variable
|==============================================================================================================================
'Legacy Input data'
[width="80%"]
|==============================================================================================================================
| *Description* | *Length*
| Contract address used in the next transaction | 20
| Quantization to be used in the next transaction | 32
|==============================================================================================================================
'v2 Input data'
[width="80%"]
|==============================================================================================================================
| *Description* | *Length*
| Contract address used in the next transaction (ignored for ETH) | 20
| Quantization to be used in the next transaction (ignored for ERC 721s) | 32
| Minting blob to be used in the next transaction (ignored for non mintable) | 32
|==============================================================================================================================
'Output data'
None
### UNSAFE SIGN
#### Description
This command signs an arbitrary hash on the Starkware Curve after presenting the hash to the user. It is intended for speed of execution in case an unknown Stark model is pushed and should be avoided as much as possible.
#### Coding
'Command'
[width="80%"]
|==============================================================================================================================
| *CLA* | *INS* | *P1* | *P2* | *Lc* | *Le*
| F0 | 0A |
00
| 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
| Hash to sign | 32
|==============================================================================================================================
'Output data'
[width="80%"]
|==============================================================================================================================
| *Description* | *Length*
| RFU (00) | 1
| r | 32
| s | 32
|==============================================================================================================================

View File

@@ -87,6 +87,7 @@ The address can be optionally checked on the device before being returned.
| First derivation index (big endian) | 4
| ... | 4
| Last derivation index (big endian) | 4
| Chain ID (big endian) (optional) | 8
|==============================================================================================================================
'Output data'
@@ -527,7 +528,7 @@ The plugin names `ERC20`, `ERC721` and `ERC1155` are reserved. Additional plugin
The signature is computed on
type || version || len(pluginName) || pluginName || address || selector || chainId || keyId || algorithmId || len(signature) || signature
type || version || len(pluginName) || pluginName || address || selector || chainId || keyId || algorithmId
#### Coding

View File

@@ -1,315 +1,313 @@
Ethereum application Plugins : Technical Specifications
=======================================================
Ledger Firmware Team <hello@ledger.fr>
Specification version 1.0 - 24th of September 2020
## 1.0
- Initial release
## About
This specification describes the plugin interface used to display a specific UI on device for Ethereum smart contracts.
Feel free to checkout the ParaSwap plugin to see an actual implementation. Link: https://github.com/LedgerHQ/app-ethereum/blob/named-external-plugins/doc/ethapp_plugins.asc .
## Flow overview
When signing an Ethereum transaction containing data, the Ethereum application looks for a plugin using .either a selector list or the contract address.
If a plugin is found, each network serialized data field (32 bytes) is passed to the plugin along with the field offset. The plugin can decide to stop the signature process if a data field isn't expected
After all fields have been received, the plugin can report to the Ethereum application whether the full data is accepted, and the user interface model that'll be used to display the data
### Amount/Address user interface
In this model, the generic (without data) transaction display is used, with the amount and destination address replaced by data provided by the plugin
### Generic user interface
In this model, the plugin first reports a number of screens (2 lines of text, the second line being scrollable) to be displayed
The Ethereum application will request each screen to be displayed to the plugin and let the user browse through them.
The first screen being displayed is always a description of the plugin being used (name and version reported by the plugin), and the last screens include the transaction fees in ETH and a confirmation prompt
### Code flow
The plugin interfacing logic is described in _src/eth_plugin_interface.h_
The plugin common dispatcher is found in _src/eth_plugin_handler.c_
The plugin generic UI dispatcher is found in _src/eth_plugin_ui.c_
Sample internal plugins are provided in _src_plugins/_
## Creating a plugin
### Creating an internal plugin
Internal plugins are triggered on specific selectors. You can modify _src/eth_plugin_internal.c_ to add your mapping.
Other specific mappings can be also added by modifying the common dispatcher
### Creating an external plugin
An external plugin is a library application named after the base64 encoding of the 20 bytes smart contract address
## Detailed flow messages
### Generic fields
The following generic fields are present in all messages :
* pluginSharedRW : scratch objects and utilities available to the plugin (can be read and written)
* pluginSharedRO : transaction data available to the plugin (can only be read)
* pluginContext : arbitrary data blob holding the plugin context, to be set and used by the plugin
* result : return code set by the plugin following the message processing
### ETH_PLUGIN_INIT_CONTRACT
[source,C]
----
typedef struct ethPluginInitContract_t {
// in
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
uint32_t pluginContextLength;
uint8_t *selector; // 4 bytes selector
uint32_t dataSize;
char *alias; // 29 bytes alias if ETH_PLUGIN_RESULT_OK_ALIAS set
uint8_t result;
} ethPluginInitContract_t;
----
This message is sent when the selector of the data has been parsed. The following specific fields are filled when the plugin is called :
* pluginContextLength : length of the data field available to store the plugin context
* selector : 4 bytes selector of the data field
* dataSize : size in bytes of the data field
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
* ETH_PLUGIN_RESULT_OK_ALIAS : if a base64 encoded alias of another plugin to call is copied to the _alias_ field. In this case, the dispatcher will follow the alias chain, and the original plugin will only be called to retrieve its name when using a generic user interface
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
### ETH_PLUGIN_PROVIDE_PARAMETER
[source,C]
----
typedef struct ethPluginProvideParameter_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
uint8_t *parameter; // 32 bytes parameter
uint32_t parameterOffset;
uint8_t result;
} ethPluginProvideParameter_t;
----
This message is sent when a new 32 bytes component of the data field is available. The following specific fields are filled when the plugin is called :
* parameter : pointer to the 32 bytes parameter being parsed
* parameterOffset : offset to this parameter from the beginning of the data field (starts at 4, following the selector)
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
There are already defined functions to extract data from a parameter:
[source,C]
----
void copy_address(uint8_t* dst, const uint8_t* parameter, uint8_t dst_size);
void copy_parameter(uint8_t* dst, const uint8_t* parameter, uint8_t dst_size);
// Get the value from the beginning of the parameter (right to left) and check if the rest of it is zero
bool U2BE_from_parameter(const uint8_t* parameter, uint16_t* value);
bool U4BE_from_parameter(const uint8_t* parameter, uint32_t* value);
----
### ETH_PLUGIN_FINALIZE
[source,C]
----
typedef struct ethPluginFinalize_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
uint8_t *itemLookup1; // set by the plugin if a token or an nft should be looked up
uint8_t *itemLookup2;
uint8_t *amount; // set an uint256 pointer if uiType is UI_AMOUNT_ADDRESS
uint8_t *address; // set to the destination address if uiType is UI_AMOUNT_ADDRESS. Set to the user's address if uiType is UI_TYPE_GENERIC
uint8_t uiType;
uint8_t numScreens; // ignored if uiType is UI_AMOUNT_ADDRESS
uint8_t result;
} ethPluginFinalize_t;
----
This message is sent when the data field has been fully parsed. The following specific fields can be filled by the plugin :
* itemLookup1 : the pointer shall be set to a 20 bytes address to look up an ERC20 token or NFT if needed by the plugin
* itemLookup2 : the pointer shall be set to a 20 bytes address to look up an ERC20 token or NFT if needed by the plugin
* uiType : set to either ETH_UI_TYPE_AMOUNT_ADDRESS for an amount/address UI or ETH_UI_TYPE_GENERIC for a generic UI
The following specific fields are filled by the plugin when returning an amount/address UI :
* amount : set to a pointer to a 256 bits number
* address : set to a pointer to a 20 bytes address
The following specific fields are filled by the plugin when returning a generic UI :
* numScreens : number of screens handled by the plugin
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
### ETH_PLUGIN_PROVIDE_INFO
[source,C]
----
typedef struct ethPluginProvideInfo_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
union extraInfo *item1; // set by the ETH application, to be saved by the plugin
union extraInfo *item2;
uint8_t additionalScreens; // Used by the plugin if it needs to display additional screens based on the information received.
uint8_t result;
} ethPluginProvideInfo_t;
----
This message is sent if an information lookup was required by the plugin when parsing a finalize message. The following specific fields are filled when the plugin is called :
* item1 : pointer to an union matching itemLookup1, or NULL if not found
* item2 : pointer to an union matching itemLookup2, or NULL if not found
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
### ETH_PLUGIN_QUERY_CONTRACT_ID
[source,C]
----
typedef struct ethQueryContractID_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
char *name;
uint32_t nameLength;
char *version;
uint32_t versionLength;
uint8_t result;
} ethQueryContractID_t;
----
This message is sent after the parsing finalization and information lookups if requested if a generic UI is used. The following specific fields are provided when the plugin is called :
* name : pointer to the name of the plugin, to be filled by the plugin
* nameLength : maximum name length
* version : pointer to the version of the plugin, to be filled by the plugin
* versionLength : maximum version length
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
### ETH_PLUGIN_QUERY_CONTRACT_UI
[source,C]
----
typedef struct ethQueryContractUI_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
union extraInfo_t *item1;
union extraInfo_t *item2;
char network_ticker[MAX_TICKER_LEN];
uint8_t *pluginContext;
uint8_t screenIndex;
char *title;
uint32_t titleLength;
char *msg;
uint32_t msgLength;
uint8_t result;
} ethQueryContractUI_t;
----
This message is sent when a plugin screen shall be displayed if a generic UI is used. The following specific fields are provided when the plugin is called :
* item1 : pointer to token / nft information
* item2 : pointer to token / nft information
* network_ticker : string that holds the network ticker
* screenIndex : index of the screen to display, starting from 0
* title : pointer to the first line of the screen, to be filled by the plugin
* titleLength : maximum title length
* msg : pointer to the second line of the screen, to be filled by the plugin
* msgLength : maximum msg length
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
## Caveats
When setting a pointer from the plugin space, make sure to use an address that will be accessible from the Ethereum application (typically in the plugin RAM context, *not* on the plugin stack)
Do not use data types that need to be aligned (such as uint32_t) in the plugin context.
## TODOs
Provide a sample callback mechanism for common plugin actions (amount to string, 256 bits number multiplication ...) to avoid duplicating code in the plugin space
Provide external plugins samples
Fully support Starkware as an independant application (APDU logic added)
Support extra flags for the generic UI (fast confirmation on first screen, ...)
Support extra plugin provisioning (signed list of associated smart contract addresses, ...)
Ethereum application Plugins : Technical Specifications
=======================================================
Ledger Firmware Team <hello@ledger.fr>
Specification version 1.0 - 24th of September 2020
## 1.0
- Initial release
## About
This specification describes the plugin interface used to display a specific UI on device for Ethereum smart contracts.
Feel free to checkout the ParaSwap plugin to see an actual implementation. Link: https://github.com/LedgerHQ/app-ethereum/blob/named-external-plugins/doc/ethapp_plugins.asc .
## Flow overview
When signing an Ethereum transaction containing data, the Ethereum application looks for a plugin using .either a selector list or the contract address.
If a plugin is found, each network serialized data field (32 bytes) is passed to the plugin along with the field offset. The plugin can decide to stop the signature process if a data field isn't expected
After all fields have been received, the plugin can report to the Ethereum application whether the full data is accepted, and the user interface model that'll be used to display the data
### Amount/Address user interface
In this model, the generic (without data) transaction display is used, with the amount and destination address replaced by data provided by the plugin
### Generic user interface
In this model, the plugin first reports a number of screens (2 lines of text, the second line being scrollable) to be displayed
The Ethereum application will request each screen to be displayed to the plugin and let the user browse through them.
The first screen being displayed is always a description of the plugin being used (name and version reported by the plugin), and the last screens include the transaction fees in ETH and a confirmation prompt
### Code flow
The plugin interfacing logic is described in _src/eth_plugin_interface.h_
The plugin common dispatcher is found in _src/eth_plugin_handler.c_
The plugin generic UI dispatcher is found in _src/eth_plugin_ui.c_
Sample internal plugins are provided in _src_plugins/_
## Creating a plugin
### Creating an internal plugin
Internal plugins are triggered on specific selectors. You can modify _src/eth_plugin_internal.c_ to add your mapping.
Other specific mappings can be also added by modifying the common dispatcher
### Creating an external plugin
An external plugin is a library application named after the base64 encoding of the 20 bytes smart contract address
## Detailed flow messages
### Generic fields
The following generic fields are present in all messages :
* pluginSharedRW : scratch objects and utilities available to the plugin (can be read and written)
* pluginSharedRO : transaction data available to the plugin (can only be read)
* pluginContext : arbitrary data blob holding the plugin context, to be set and used by the plugin
* result : return code set by the plugin following the message processing
### ETH_PLUGIN_INIT_CONTRACT
[source,C]
----
typedef struct ethPluginInitContract_t {
// in
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
uint32_t pluginContextLength;
uint8_t *selector; // 4 bytes selector
uint32_t dataSize;
char *alias; // 29 bytes alias if ETH_PLUGIN_RESULT_OK_ALIAS set
uint8_t result;
} ethPluginInitContract_t;
----
This message is sent when the selector of the data has been parsed. The following specific fields are filled when the plugin is called :
* pluginContextLength : length of the data field available to store the plugin context
* selector : 4 bytes selector of the data field
* dataSize : size in bytes of the data field
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
* ETH_PLUGIN_RESULT_OK_ALIAS : if a base64 encoded alias of another plugin to call is copied to the _alias_ field. In this case, the dispatcher will follow the alias chain, and the original plugin will only be called to retrieve its name when using a generic user interface
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
### ETH_PLUGIN_PROVIDE_PARAMETER
[source,C]
----
typedef struct ethPluginProvideParameter_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
uint8_t *parameter; // 32 bytes parameter
uint32_t parameterOffset;
uint8_t result;
} ethPluginProvideParameter_t;
----
This message is sent when a new 32 bytes component of the data field is available. The following specific fields are filled when the plugin is called :
* parameter : pointer to the 32 bytes parameter being parsed
* parameterOffset : offset to this parameter from the beginning of the data field (starts at 4, following the selector)
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
There are already defined functions to extract data from a parameter:
[source,C]
----
void copy_address(uint8_t* dst, const uint8_t* parameter, uint8_t dst_size);
void copy_parameter(uint8_t* dst, const uint8_t* parameter, uint8_t dst_size);
// Get the value from the beginning of the parameter (right to left) and check if the rest of it is zero
bool U2BE_from_parameter(const uint8_t* parameter, uint16_t* value);
bool U4BE_from_parameter(const uint8_t* parameter, uint32_t* value);
----
### ETH_PLUGIN_FINALIZE
[source,C]
----
typedef struct ethPluginFinalize_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
uint8_t *itemLookup1; // set by the plugin if a token or an nft should be looked up
uint8_t *itemLookup2;
uint8_t *amount; // set an uint256 pointer if uiType is UI_AMOUNT_ADDRESS
uint8_t *address; // set to the destination address if uiType is UI_AMOUNT_ADDRESS. Set to the user's address if uiType is UI_TYPE_GENERIC
uint8_t uiType;
uint8_t numScreens; // ignored if uiType is UI_AMOUNT_ADDRESS
uint8_t result;
} ethPluginFinalize_t;
----
This message is sent when the data field has been fully parsed. The following specific fields can be filled by the plugin :
* itemLookup1 : the pointer shall be set to a 20 bytes address to look up an ERC20 token or NFT if needed by the plugin
* itemLookup2 : the pointer shall be set to a 20 bytes address to look up an ERC20 token or NFT if needed by the plugin
* uiType : set to either ETH_UI_TYPE_AMOUNT_ADDRESS for an amount/address UI or ETH_UI_TYPE_GENERIC for a generic UI
The following specific fields are filled by the plugin when returning an amount/address UI :
* amount : set to a pointer to a 256 bits number
* address : set to a pointer to a 20 bytes address
The following specific fields are filled by the plugin when returning a generic UI :
* numScreens : number of screens handled by the plugin
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
### ETH_PLUGIN_PROVIDE_INFO
[source,C]
----
typedef struct ethPluginProvideInfo_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
union extraInfo *item1; // set by the ETH application, to be saved by the plugin
union extraInfo *item2;
uint8_t additionalScreens; // Used by the plugin if it needs to display additional screens based on the information received.
uint8_t result;
} ethPluginProvideInfo_t;
----
This message is sent if an information lookup was required by the plugin when parsing a finalize message. The following specific fields are filled when the plugin is called :
* item1 : pointer to an union matching itemLookup1, or NULL if not found
* item2 : pointer to an union matching itemLookup2, or NULL if not found
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
* ETH_PLUGIN_RESULT_FALLBACK : if the signing logic should fallback to the generic one
### ETH_PLUGIN_QUERY_CONTRACT_ID
[source,C]
----
typedef struct ethQueryContractID_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
uint8_t *pluginContext;
char *name;
uint32_t nameLength;
char *version;
uint32_t versionLength;
uint8_t result;
} ethQueryContractID_t;
----
This message is sent after the parsing finalization and information lookups if requested if a generic UI is used. The following specific fields are provided when the plugin is called :
* name : pointer to the name of the plugin, to be filled by the plugin
* nameLength : maximum name length
* version : pointer to the version of the plugin, to be filled by the plugin
* versionLength : maximum version length
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
### ETH_PLUGIN_QUERY_CONTRACT_UI
[source,C]
----
typedef struct ethQueryContractUI_t {
ethPluginSharedRW_t *pluginSharedRW;
ethPluginSharedRO_t *pluginSharedRO;
union extraInfo_t *item1;
union extraInfo_t *item2;
char network_ticker[MAX_TICKER_LEN];
uint8_t *pluginContext;
uint8_t screenIndex;
char *title;
uint32_t titleLength;
char *msg;
uint32_t msgLength;
uint8_t result;
} ethQueryContractUI_t;
----
This message is sent when a plugin screen shall be displayed if a generic UI is used. The following specific fields are provided when the plugin is called :
* item1 : pointer to token / nft information
* item2 : pointer to token / nft information
* network_ticker : string that holds the network ticker
* screenIndex : index of the screen to display, starting from 0
* title : pointer to the first line of the screen, to be filled by the plugin
* titleLength : maximum title length
* msg : pointer to the second line of the screen, to be filled by the plugin
* msgLength : maximum msg length
The following return codes are expected, any other will abort the signing process :
* ETH_PLUGIN_RESULT_OK : if the plugin can be successfully initialized
## Caveats
When setting a pointer from the plugin space, make sure to use an address that will be accessible from the Ethereum application (typically in the plugin RAM context, *not* on the plugin stack)
Do not use data types that need to be aligned (such as uint32_t) in the plugin context.
## TODOs
Provide a sample callback mechanism for common plugin actions (amount to string, 256 bits number multiplication ...) to avoid duplicating code in the plugin space
Provide external plugins samples
Support extra flags for the generic UI (fast confirmation on first screen, ...)
Support extra plugin provisioning (signed list of associated smart contract addresses, ...)

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

View File

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

View File

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

View File

@@ -0,0 +1 @@
stax_chain_1_64px.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

View File

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 212 B

View File

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

View File

Before

Width:  |  Height:  |  Size: 622 B

After

Width:  |  Height:  |  Size: 622 B

View File

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 681 B

View File

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 199 B

View File

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 584 B

View File

Before

Width:  |  Height:  |  Size: 210 B

After

Width:  |  Height:  |  Size: 210 B

View File

@@ -0,0 +1 @@
stax_chain_1_64px.gif

View File

Before

Width:  |  Height:  |  Size: 966 B

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

View File

Before

Width:  |  Height:  |  Size: 649 B

After

Width:  |  Height:  |  Size: 649 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 251 B

View File

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 569 B

View File

Before

Width:  |  Height:  |  Size: 886 B

After

Width:  |  Height:  |  Size: 886 B

View File

Before

Width:  |  Height:  |  Size: 389 B

After

Width:  |  Height:  |  Size: 389 B

View File

Before

Width:  |  Height:  |  Size: 389 B

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

View File

Before

Width:  |  Height:  |  Size: 914 B

After

Width:  |  Height:  |  Size: 914 B

View File

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

View File

Before

Width:  |  Height:  |  Size: 997 B

After

Width:  |  Height:  |  Size: 997 B

View File

Before

Width:  |  Height:  |  Size: 985 B

After

Width:  |  Height:  |  Size: 985 B

View File

Before

Width:  |  Height:  |  Size: 836 B

After

Width:  |  Height:  |  Size: 836 B

View File

Before

Width:  |  Height:  |  Size: 340 B

After

Width:  |  Height:  |  Size: 340 B

View File

Before

Width:  |  Height:  |  Size: 310 B

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

View File

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

View File

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 314 B

View File

Before

Width:  |  Height:  |  Size: 384 B

After

Width:  |  Height:  |  Size: 384 B

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 265 B

After

Width:  |  Height:  |  Size: 265 B

View File

Before

Width:  |  Height:  |  Size: 293 B

After

Width:  |  Height:  |  Size: 293 B

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

View File

Before

Width:  |  Height:  |  Size: 342 B

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

View File

Before

Width:  |  Height:  |  Size: 248 B

After

Width:  |  Height:  |  Size: 248 B

View File

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Some files were not shown because too many files have changed in this diff Show More