From e5c82d910ef4680f8b2b96137412088ca032b2ca Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 28 Jul 2023 11:00:23 +0200 Subject: [PATCH 1/4] [add] Python client packaging first draft --- .github/workflows/python-client.yml | 43 ++++++++++++++++++ client/.gitignore | 4 ++ client/MANIFEST.in | 1 + client/README.md | 28 ++++++++++++ client/pyproject.toml | 45 +++++++++++++++++++ .../ledger_app_clients/ethereum/__init__.py | 1 + .../ledger_app_clients/ethereum}/client.py | 12 +++-- .../ethereum}/command_builder.py | 13 ++++-- .../ethereum}/eip712/InputData.py | 18 ++++---- .../ethereum/eip712/__init__.py | 1 + .../ethereum/eip712/struct.py | 1 + .../ledger_app_clients/ethereum}/keychain.py | 3 +- .../ethereum}/keychain/cal.pem | 0 .../ethereum}/keychain/domain_name.pem | 0 .../ethereum}/response_parser.py | 0 .../ledger_app_clients/ethereum}/settings.py | 2 +- .../src/ledger_app_clients/ethereum}/tlv.py | 0 .../00-simple_mail-data.json | 0 .../00-simple_mail-filter.json | 0 .../00-simple_mail.ini | 0 .../01-addresses_array_mail-data.json | 0 .../01-addresses_array_mail.ini | 0 .../02-recipients_array_mail-data.json | 0 .../02-recipients_array_mail.ini | 0 .../03-long_string-data.json | 0 .../03-long_string.ini | 0 .../04-long_bytes-data.json | 0 .../04-long_bytes.ini | 0 .../05-signed_ints-data.json | 0 .../05-signed_ints.ini | 0 .../06-boolean-data.json | 0 .../06-boolean.ini | 0 .../07-fixed_bytes-data.json | 0 .../07-fixed_bytes.ini | 0 .../08-opensea-data.json | 0 .../08-opensea-filter.json | 0 .../08-opensea.ini | 0 .../09-rarible-data.json | 0 .../09-rarible.ini | 0 .../10-multidimensional_arrays-data.json | 0 .../10-multidimensional_arrays.ini | 0 .../11-complex_structs-data.json | 0 .../11-complex_structs-filter.json | 0 .../11-complex_structs.ini | 0 .../12-sign_in-data.json | 0 .../12-sign_in-filter.json | 0 .../12-sign_in.ini | 0 .../13-empty_arrays-data.json | 0 .../13-empty_arrays.ini | 0 tests/ragger/requirements.txt | 2 +- tests/ragger/test_domain_name.py | 17 ++++--- tests/ragger/test_eip712.py | 33 ++++++++------ 52 files changed, 179 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/python-client.yml create mode 100644 client/.gitignore create mode 100644 client/MANIFEST.in create mode 100644 client/README.md create mode 100644 client/pyproject.toml create mode 100644 client/src/ledger_app_clients/ethereum/__init__.py rename {tests/ragger/app => client/src/ledger_app_clients/ethereum}/client.py (95%) rename {tests/ragger/app => client/src/ledger_app_clients/ethereum}/command_builder.py (99%) rename {tests/ragger => client/src/ledger_app_clients/ethereum}/eip712/InputData.py (98%) create mode 100644 client/src/ledger_app_clients/ethereum/eip712/__init__.py rename tests/ragger/app/eip712.py => client/src/ledger_app_clients/ethereum/eip712/struct.py (99%) rename {tests/ragger => client/src/ledger_app_clients/ethereum}/keychain.py (99%) rename {tests/ragger => client/src/ledger_app_clients/ethereum}/keychain/cal.pem (100%) rename {tests/ragger => client/src/ledger_app_clients/ethereum}/keychain/domain_name.pem (100%) rename {tests/ragger/app => client/src/ledger_app_clients/ethereum}/response_parser.py (100%) rename {tests/ragger/app => client/src/ledger_app_clients/ethereum}/settings.py (98%) rename {tests/ragger/app => client/src/ledger_app_clients/ethereum}/tlv.py (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/00-simple_mail-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/00-simple_mail-filter.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/00-simple_mail.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/01-addresses_array_mail-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/01-addresses_array_mail.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/02-recipients_array_mail-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/02-recipients_array_mail.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/03-long_string-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/03-long_string.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/04-long_bytes-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/04-long_bytes.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/05-signed_ints-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/05-signed_ints.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/06-boolean-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/06-boolean.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/07-fixed_bytes-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/07-fixed_bytes.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/08-opensea-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/08-opensea-filter.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/08-opensea.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/09-rarible-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/09-rarible.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/10-multidimensional_arrays-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/10-multidimensional_arrays.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/11-complex_structs-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/11-complex_structs-filter.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/11-complex_structs.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/12-sign_in-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/12-sign_in-filter.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/12-sign_in.ini (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/13-empty_arrays-data.json (100%) rename tests/ragger/{eip712/input_files => eip712_input_files}/13-empty_arrays.ini (100%) diff --git a/.github/workflows/python-client.yml b/.github/workflows/python-client.yml new file mode 100644 index 0000000..658edb1 --- /dev/null +++ b/.github/workflows/python-client.yml @@ -0,0 +1,43 @@ +name: Python client checks, package build and deployment + +on: + workflow_dispatch: + push: + branches: + - develop + - master + pull_request: + +jobs: + lint: + name: Linting + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v3 + - run: pip install flake8 + - name: Flake8 lint Python code + run: find client/src/ -type f -name '*.py' -exec flake8 --max-line-length=120 '{}' '+' + + mypy: + name: Type checking + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v3 + - run: pip install mypy + - name: Mypy type checking + run: mypy client/src + + build: + name: Building the package + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v3 + - run: pip install --upgrade pip build twine + - name: Build and test the package + run: | + cd client/ + python -m build . + python -m twine check dist/* diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..3051463 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,4 @@ +*egg-info +dist +*wheel +*~ diff --git a/client/MANIFEST.in b/client/MANIFEST.in new file mode 100644 index 0000000..65272bf --- /dev/null +++ b/client/MANIFEST.in @@ -0,0 +1 @@ +include src/ledger_app_clients/ethereum/keychain/* \ No newline at end of file diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..7d8ca69 --- /dev/null +++ b/client/README.md @@ -0,0 +1,28 @@ +# Ethereum app Python client + +This package allows to communicate with the Ethereum application, either on a +real device, or emulated on Speculos. + +## Installation + +This package is deployed: + +- on `pypi.org` for the stable version. This version will work with the + application available on the `master` branch. + ```bash + pip install ledger_app_clients.ethereum` + ``` +- on `test.pypi.org` for the rolling release. This verison will work with the + application code on the `develop` branch. + ```bash + pip install --extra-index-url https://test.pypi.org/simple/ ledger_app_clients.ethereum` + ``` + +### Installation from sources + +You can install the client from this repo: + +```bash +cd client/ +pip install . +``` diff --git a/client/pyproject.toml b/client/pyproject.toml new file mode 100644 index 0000000..8993e88 --- /dev/null +++ b/client/pyproject.toml @@ -0,0 +1,45 @@ +[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 License 2.0", + "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]", + "simple-rlp", +] + +[tools.setuptools] +include-package-data = true + +[tool.setuptools.dynamic] +version = {attr = "ledger_app_clients.ethereum.__version__"} + +[project.urls] +Home = "https://github.com/LedgerHQ/app-ethereum" + +# [tool.setuptools_scm] +# write_to = "ledgerwallet/__version__.py" +# local_scheme = "no-local-version" diff --git a/client/src/ledger_app_clients/ethereum/__init__.py b/client/src/ledger_app_clients/ethereum/__init__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/client/src/ledger_app_clients/ethereum/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/tests/ragger/app/client.py b/client/src/ledger_app_clients/ethereum/client.py similarity index 95% rename from tests/ragger/app/client.py rename to client/src/ledger_app_clients/ethereum/client.py index 616670c..f556d2d 100644 --- a/tests/ragger/app/client.py +++ b/client/src/ledger_app_clients/ethereum/client.py @@ -1,16 +1,14 @@ -from enum import IntEnum, auto -from typing import Optional +import rlp +from enum import IntEnum from ragger.backend import BackendInterface from ragger.utils import RAPDU + from .command_builder import CommandBuilder from .eip712 import EIP712FieldType +from .keychain import sign_data, Key from .tlv import format_tlv -from pathlib import Path -import keychain -import rlp -ROOT_SCREENSHOT_PATH = Path(__file__).parent.parent WEI_IN_ETH = 1e+18 @@ -134,7 +132,7 @@ class EthAppClient: payload += format_tlv(DOMAIN_NAME_TAG.DOMAIN_NAME, name) payload += format_tlv(DOMAIN_NAME_TAG.ADDRESS, addr) payload += format_tlv(DOMAIN_NAME_TAG.SIGNATURE, - keychain.sign_data(keychain.Key.DOMAIN_NAME, payload)) + sign_data(Key.DOMAIN_NAME, payload)) chunks = self._cmd_builder.provide_domain_name(payload) for chunk in chunks[:-1]: diff --git a/tests/ragger/app/command_builder.py b/client/src/ledger_app_clients/ethereum/command_builder.py similarity index 99% rename from tests/ragger/app/command_builder.py rename to client/src/ledger_app_clients/ethereum/command_builder.py index ae3f730..92d895d 100644 --- a/tests/ragger/app/command_builder.py +++ b/client/src/ledger_app_clients/ethereum/command_builder.py @@ -1,8 +1,10 @@ -from enum import IntEnum, auto -from typing import Iterator, Optional -from .eip712 import EIP712FieldType -from ragger.bip import pack_derivation_path import struct +from enum import IntEnum +from ragger.bip import pack_derivation_path +from typing import Iterator + +from .eip712 import EIP712FieldType + class InsType(IntEnum): SIGN = 0x04 @@ -13,12 +15,14 @@ class InsType(IntEnum): GET_CHALLENGE = 0x20 PROVIDE_DOMAIN_NAME = 0x22 + class P1Type(IntEnum): COMPLETE_SEND = 0x00 PARTIAL_SEND = 0x01 SIGN_FIRST_CHUNK = 0x00 SIGN_SUBSQT_CHUNK = 0x80 + class P2Type(IntEnum): STRUCT_NAME = 0x00 STRUCT_FIELD = 0xff @@ -29,6 +33,7 @@ class P2Type(IntEnum): FILTERING_CONTRACT_NAME = 0x0f FILTERING_FIELD_NAME = 0xff + class CommandBuilder: _CLA: int = 0xE0 diff --git a/tests/ragger/eip712/InputData.py b/client/src/ledger_app_clients/ethereum/eip712/InputData.py similarity index 98% rename from tests/ragger/eip712/InputData.py rename to client/src/ledger_app_clients/ethereum/eip712/InputData.py index 6dd6471..68f8556 100644 --- a/tests/ragger/eip712/InputData.py +++ b/client/src/ledger_app_clients/ethereum/eip712/InputData.py @@ -1,13 +1,13 @@ -#!/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 +from typing import Callable + +from ledger_app_clients.ethereum import keychain +from ledger_app_clients.ethereum.client import EthAppClient, EIP712FieldType + # global variables app_client: EthAppClient = None @@ -18,8 +18,6 @@ sig_ctx = {} autonext_handler: Callable = None - - # From a string typename, extract the type and all the array depth # Input = "uint8[2][][4]" | "bool" # Output = ('uint8', [2, None, 4]) | ('bool', []) diff --git a/client/src/ledger_app_clients/ethereum/eip712/__init__.py b/client/src/ledger_app_clients/ethereum/eip712/__init__.py new file mode 100644 index 0000000..172091c --- /dev/null +++ b/client/src/ledger_app_clients/ethereum/eip712/__init__.py @@ -0,0 +1 @@ +from .struct import EIP712FieldType # noqa diff --git a/tests/ragger/app/eip712.py b/client/src/ledger_app_clients/ethereum/eip712/struct.py similarity index 99% rename from tests/ragger/app/eip712.py rename to client/src/ledger_app_clients/ethereum/eip712/struct.py index f719c6e..19dbacc 100644 --- a/tests/ragger/app/eip712.py +++ b/client/src/ledger_app_clients/ethereum/eip712/struct.py @@ -1,5 +1,6 @@ from enum import IntEnum, auto + class EIP712FieldType(IntEnum): CUSTOM = 0, INT = auto() diff --git a/tests/ragger/keychain.py b/client/src/ledger_app_clients/ethereum/keychain.py similarity index 99% rename from tests/ragger/keychain.py rename to client/src/ledger_app_clients/ethereum/keychain.py index 31914a4..523d1d1 100644 --- a/tests/ragger/keychain.py +++ b/client/src/ledger_app_clients/ethereum/keychain.py @@ -1,9 +1,10 @@ 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): diff --git a/tests/ragger/keychain/cal.pem b/client/src/ledger_app_clients/ethereum/keychain/cal.pem similarity index 100% rename from tests/ragger/keychain/cal.pem rename to client/src/ledger_app_clients/ethereum/keychain/cal.pem diff --git a/tests/ragger/keychain/domain_name.pem b/client/src/ledger_app_clients/ethereum/keychain/domain_name.pem similarity index 100% rename from tests/ragger/keychain/domain_name.pem rename to client/src/ledger_app_clients/ethereum/keychain/domain_name.pem diff --git a/tests/ragger/app/response_parser.py b/client/src/ledger_app_clients/ethereum/response_parser.py similarity index 100% rename from tests/ragger/app/response_parser.py rename to client/src/ledger_app_clients/ethereum/response_parser.py diff --git a/tests/ragger/app/settings.py b/client/src/ledger_app_clients/ethereum/settings.py similarity index 98% rename from tests/ragger/app/settings.py rename to client/src/ledger_app_clients/ethereum/settings.py index ec6bf78..2b44d45 100644 --- a/tests/ragger/app/settings.py +++ b/client/src/ledger_app_clients/ethereum/settings.py @@ -1,8 +1,8 @@ from enum import Enum, auto -from typing import List from ragger.firmware import Firmware from ragger.navigator import Navigator, NavInsID, NavIns + class SettingID(Enum): BLIND_SIGNING = auto() DEBUG_DATA = auto() diff --git a/tests/ragger/app/tlv.py b/client/src/ledger_app_clients/ethereum/tlv.py similarity index 100% rename from tests/ragger/app/tlv.py rename to client/src/ledger_app_clients/ethereum/tlv.py diff --git a/tests/ragger/eip712/input_files/00-simple_mail-data.json b/tests/ragger/eip712_input_files/00-simple_mail-data.json similarity index 100% rename from tests/ragger/eip712/input_files/00-simple_mail-data.json rename to tests/ragger/eip712_input_files/00-simple_mail-data.json diff --git a/tests/ragger/eip712/input_files/00-simple_mail-filter.json b/tests/ragger/eip712_input_files/00-simple_mail-filter.json similarity index 100% rename from tests/ragger/eip712/input_files/00-simple_mail-filter.json rename to tests/ragger/eip712_input_files/00-simple_mail-filter.json diff --git a/tests/ragger/eip712/input_files/00-simple_mail.ini b/tests/ragger/eip712_input_files/00-simple_mail.ini similarity index 100% rename from tests/ragger/eip712/input_files/00-simple_mail.ini rename to tests/ragger/eip712_input_files/00-simple_mail.ini diff --git a/tests/ragger/eip712/input_files/01-addresses_array_mail-data.json b/tests/ragger/eip712_input_files/01-addresses_array_mail-data.json similarity index 100% rename from tests/ragger/eip712/input_files/01-addresses_array_mail-data.json rename to tests/ragger/eip712_input_files/01-addresses_array_mail-data.json diff --git a/tests/ragger/eip712/input_files/01-addresses_array_mail.ini b/tests/ragger/eip712_input_files/01-addresses_array_mail.ini similarity index 100% rename from tests/ragger/eip712/input_files/01-addresses_array_mail.ini rename to tests/ragger/eip712_input_files/01-addresses_array_mail.ini diff --git a/tests/ragger/eip712/input_files/02-recipients_array_mail-data.json b/tests/ragger/eip712_input_files/02-recipients_array_mail-data.json similarity index 100% rename from tests/ragger/eip712/input_files/02-recipients_array_mail-data.json rename to tests/ragger/eip712_input_files/02-recipients_array_mail-data.json diff --git a/tests/ragger/eip712/input_files/02-recipients_array_mail.ini b/tests/ragger/eip712_input_files/02-recipients_array_mail.ini similarity index 100% rename from tests/ragger/eip712/input_files/02-recipients_array_mail.ini rename to tests/ragger/eip712_input_files/02-recipients_array_mail.ini diff --git a/tests/ragger/eip712/input_files/03-long_string-data.json b/tests/ragger/eip712_input_files/03-long_string-data.json similarity index 100% rename from tests/ragger/eip712/input_files/03-long_string-data.json rename to tests/ragger/eip712_input_files/03-long_string-data.json diff --git a/tests/ragger/eip712/input_files/03-long_string.ini b/tests/ragger/eip712_input_files/03-long_string.ini similarity index 100% rename from tests/ragger/eip712/input_files/03-long_string.ini rename to tests/ragger/eip712_input_files/03-long_string.ini diff --git a/tests/ragger/eip712/input_files/04-long_bytes-data.json b/tests/ragger/eip712_input_files/04-long_bytes-data.json similarity index 100% rename from tests/ragger/eip712/input_files/04-long_bytes-data.json rename to tests/ragger/eip712_input_files/04-long_bytes-data.json diff --git a/tests/ragger/eip712/input_files/04-long_bytes.ini b/tests/ragger/eip712_input_files/04-long_bytes.ini similarity index 100% rename from tests/ragger/eip712/input_files/04-long_bytes.ini rename to tests/ragger/eip712_input_files/04-long_bytes.ini diff --git a/tests/ragger/eip712/input_files/05-signed_ints-data.json b/tests/ragger/eip712_input_files/05-signed_ints-data.json similarity index 100% rename from tests/ragger/eip712/input_files/05-signed_ints-data.json rename to tests/ragger/eip712_input_files/05-signed_ints-data.json diff --git a/tests/ragger/eip712/input_files/05-signed_ints.ini b/tests/ragger/eip712_input_files/05-signed_ints.ini similarity index 100% rename from tests/ragger/eip712/input_files/05-signed_ints.ini rename to tests/ragger/eip712_input_files/05-signed_ints.ini diff --git a/tests/ragger/eip712/input_files/06-boolean-data.json b/tests/ragger/eip712_input_files/06-boolean-data.json similarity index 100% rename from tests/ragger/eip712/input_files/06-boolean-data.json rename to tests/ragger/eip712_input_files/06-boolean-data.json diff --git a/tests/ragger/eip712/input_files/06-boolean.ini b/tests/ragger/eip712_input_files/06-boolean.ini similarity index 100% rename from tests/ragger/eip712/input_files/06-boolean.ini rename to tests/ragger/eip712_input_files/06-boolean.ini diff --git a/tests/ragger/eip712/input_files/07-fixed_bytes-data.json b/tests/ragger/eip712_input_files/07-fixed_bytes-data.json similarity index 100% rename from tests/ragger/eip712/input_files/07-fixed_bytes-data.json rename to tests/ragger/eip712_input_files/07-fixed_bytes-data.json diff --git a/tests/ragger/eip712/input_files/07-fixed_bytes.ini b/tests/ragger/eip712_input_files/07-fixed_bytes.ini similarity index 100% rename from tests/ragger/eip712/input_files/07-fixed_bytes.ini rename to tests/ragger/eip712_input_files/07-fixed_bytes.ini diff --git a/tests/ragger/eip712/input_files/08-opensea-data.json b/tests/ragger/eip712_input_files/08-opensea-data.json similarity index 100% rename from tests/ragger/eip712/input_files/08-opensea-data.json rename to tests/ragger/eip712_input_files/08-opensea-data.json diff --git a/tests/ragger/eip712/input_files/08-opensea-filter.json b/tests/ragger/eip712_input_files/08-opensea-filter.json similarity index 100% rename from tests/ragger/eip712/input_files/08-opensea-filter.json rename to tests/ragger/eip712_input_files/08-opensea-filter.json diff --git a/tests/ragger/eip712/input_files/08-opensea.ini b/tests/ragger/eip712_input_files/08-opensea.ini similarity index 100% rename from tests/ragger/eip712/input_files/08-opensea.ini rename to tests/ragger/eip712_input_files/08-opensea.ini diff --git a/tests/ragger/eip712/input_files/09-rarible-data.json b/tests/ragger/eip712_input_files/09-rarible-data.json similarity index 100% rename from tests/ragger/eip712/input_files/09-rarible-data.json rename to tests/ragger/eip712_input_files/09-rarible-data.json diff --git a/tests/ragger/eip712/input_files/09-rarible.ini b/tests/ragger/eip712_input_files/09-rarible.ini similarity index 100% rename from tests/ragger/eip712/input_files/09-rarible.ini rename to tests/ragger/eip712_input_files/09-rarible.ini diff --git a/tests/ragger/eip712/input_files/10-multidimensional_arrays-data.json b/tests/ragger/eip712_input_files/10-multidimensional_arrays-data.json similarity index 100% rename from tests/ragger/eip712/input_files/10-multidimensional_arrays-data.json rename to tests/ragger/eip712_input_files/10-multidimensional_arrays-data.json diff --git a/tests/ragger/eip712/input_files/10-multidimensional_arrays.ini b/tests/ragger/eip712_input_files/10-multidimensional_arrays.ini similarity index 100% rename from tests/ragger/eip712/input_files/10-multidimensional_arrays.ini rename to tests/ragger/eip712_input_files/10-multidimensional_arrays.ini diff --git a/tests/ragger/eip712/input_files/11-complex_structs-data.json b/tests/ragger/eip712_input_files/11-complex_structs-data.json similarity index 100% rename from tests/ragger/eip712/input_files/11-complex_structs-data.json rename to tests/ragger/eip712_input_files/11-complex_structs-data.json diff --git a/tests/ragger/eip712/input_files/11-complex_structs-filter.json b/tests/ragger/eip712_input_files/11-complex_structs-filter.json similarity index 100% rename from tests/ragger/eip712/input_files/11-complex_structs-filter.json rename to tests/ragger/eip712_input_files/11-complex_structs-filter.json diff --git a/tests/ragger/eip712/input_files/11-complex_structs.ini b/tests/ragger/eip712_input_files/11-complex_structs.ini similarity index 100% rename from tests/ragger/eip712/input_files/11-complex_structs.ini rename to tests/ragger/eip712_input_files/11-complex_structs.ini diff --git a/tests/ragger/eip712/input_files/12-sign_in-data.json b/tests/ragger/eip712_input_files/12-sign_in-data.json similarity index 100% rename from tests/ragger/eip712/input_files/12-sign_in-data.json rename to tests/ragger/eip712_input_files/12-sign_in-data.json diff --git a/tests/ragger/eip712/input_files/12-sign_in-filter.json b/tests/ragger/eip712_input_files/12-sign_in-filter.json similarity index 100% rename from tests/ragger/eip712/input_files/12-sign_in-filter.json rename to tests/ragger/eip712_input_files/12-sign_in-filter.json diff --git a/tests/ragger/eip712/input_files/12-sign_in.ini b/tests/ragger/eip712_input_files/12-sign_in.ini similarity index 100% rename from tests/ragger/eip712/input_files/12-sign_in.ini rename to tests/ragger/eip712_input_files/12-sign_in.ini diff --git a/tests/ragger/eip712/input_files/13-empty_arrays-data.json b/tests/ragger/eip712_input_files/13-empty_arrays-data.json similarity index 100% rename from tests/ragger/eip712/input_files/13-empty_arrays-data.json rename to tests/ragger/eip712_input_files/13-empty_arrays-data.json diff --git a/tests/ragger/eip712/input_files/13-empty_arrays.ini b/tests/ragger/eip712_input_files/13-empty_arrays.ini similarity index 100% rename from tests/ragger/eip712/input_files/13-empty_arrays.ini rename to tests/ragger/eip712_input_files/13-empty_arrays.ini diff --git a/tests/ragger/requirements.txt b/tests/ragger/requirements.txt index 34213d6..b493e48 100644 --- a/tests/ragger/requirements.txt +++ b/tests/ragger/requirements.txt @@ -1,4 +1,4 @@ ragger[speculos] pytest ecdsa -simple-rlp +./client/ diff --git a/tests/ragger/test_domain_name.py b/tests/ragger/test_domain_name.py index f137610..05bcddf 100644 --- a/tests/ragger/test_domain_name.py +++ b/tests/ragger/test_domain_name.py @@ -1,12 +1,16 @@ import pytest -from ragger.error import ExceptionRAPDU -from ragger.firmware import Firmware +from pathlib import Path from ragger.backend import BackendInterface +from ragger.firmware import Firmware +from ragger.error import ExceptionRAPDU from ragger.navigator import Navigator, NavInsID -from app.client import EthAppClient, StatusWord, ROOT_SCREENSHOT_PATH -from app.settings import SettingID, settings_toggle -import app.response_parser as ResponseParser -import struct + +import ledger_app_clients.ethereum.response_parser as ResponseParser +from ledger_app_clients.ethereum.client import EthAppClient, StatusWord +from ledger_app_clients.ethereum.settings import SettingID, settings_toggle + + +ROOT_SCREENSHOT_PATH = Path(__file__).parent # Values used across all tests CHAIN_ID = 1 @@ -73,7 +77,6 @@ def test_send_fund_wrong_challenge(firmware: Firmware, backend: BackendInterface, navigator: Navigator): app_client = EthAppClient(backend) - caught = False challenge = common(app_client) try: diff --git a/tests/ragger/test_eip712.py b/tests/ragger/test_eip712.py index 6ff0bc7..0816958 100644 --- a/tests/ragger/test_eip712.py +++ b/tests/ragger/test_eip712.py @@ -1,37 +1,42 @@ -import pytest -import os import fnmatch -from typing import List -from ragger.firmware import Firmware -from ragger.backend import BackendInterface -from ragger.navigator import Navigator, NavInsID -from app.client import EthAppClient -from app.settings import SettingID, settings_toggle -from eip712 import InputData -from pathlib import Path -from configparser import ConfigParser -import app.response_parser as ResponseParser -from functools import partial +import os +import pytest import time +from configparser import ConfigParser +from functools import partial +from pathlib import Path +from ragger.backend import BackendInterface +from ragger.firmware import Firmware +from ragger.navigator import Navigator, NavInsID +from typing import List + +import ledger_app_clients.ethereum.response_parser as ResponseParser +from ledger_app_clients.ethereum.client import EthAppClient +from ledger_app_clients.ethereum.eip712 import InputData +from ledger_app_clients.ethereum.settings import SettingID, settings_toggle + BIP32_PATH = "m/44'/60'/0'/0/0" def input_files() -> List[str]: files = [] - for file in os.scandir("%s/eip712/input_files" % (os.path.dirname(__file__))): + for file in os.scandir("%s/eip712_input_files" % (os.path.dirname(__file__))): if fnmatch.fnmatch(file, "*-data.json"): files.append(file.path) return sorted(files) + @pytest.fixture(params=input_files()) def input_file(request) -> str: return Path(request.param) + @pytest.fixture(params=[True, False]) def verbose(request) -> bool: return request.param + @pytest.fixture(params=[False, True]) def filtering(request) -> bool: return request.param From 54b979186d220a7f9e4e82248ddf32f02670b0b4 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 28 Jul 2023 15:41:17 +0200 Subject: [PATCH 2/4] [clean] Linter / typing fixes --- .github/workflows/python-client.yml | 4 +- client/CHANGELOG.md | 12 +++ client/README.md | 6 +- client/pyproject.toml | 5 +- .../src/ledger_app_clients/ethereum/client.py | 34 +++++---- .../ethereum/command_builder.py | 18 ++--- .../ethereum/eip712/InputData.py | 73 ++++++++++++------- .../ledger_app_clients/ethereum/keychain.py | 8 +- .../ethereum/response_parser.py | 1 + .../ledger_app_clients/ethereum/settings.py | 19 +++-- client/src/ledger_app_clients/ethereum/tlv.py | 10 +-- 11 files changed, 117 insertions(+), 73 deletions(-) create mode 100644 client/CHANGELOG.md diff --git a/.github/workflows/python-client.yml b/.github/workflows/python-client.yml index 658edb1..ca8f164 100644 --- a/.github/workflows/python-client.yml +++ b/.github/workflows/python-client.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v3 - run: pip install flake8 - name: Flake8 lint Python code - run: find client/src/ -type f -name '*.py' -exec flake8 --max-line-length=120 '{}' '+' + run: (cd client && find src/ -type f -name '*.py' -exec flake8 --max-line-length=120 '{}' '+') mypy: name: Type checking @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v3 - run: pip install mypy - name: Mypy type checking - run: mypy client/src + run: (cd client && mypy src/) build: name: Building the package diff --git a/client/CHANGELOG.md b/client/CHANGELOG.md new file mode 100644 index 0000000..9cdc9ea --- /dev/null +++ b/client/CHANGELOG.md @@ -0,0 +1,12 @@ +# 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.0.1] - 2023-08-07 + +### Added + +- Initial version diff --git a/client/README.md b/client/README.md index 7d8ca69..37da79a 100644 --- a/client/README.md +++ b/client/README.md @@ -10,12 +10,12 @@ This package is deployed: - on `pypi.org` for the stable version. This version will work with the application available on the `master` branch. ```bash - pip install ledger_app_clients.ethereum` + pip install ledger_app_clients.ethereum ``` -- on `test.pypi.org` for the rolling release. This verison will work with the +- on `test.pypi.org` for the rolling release. This version will work with the application code on the `develop` branch. ```bash - pip install --extra-index-url https://test.pypi.org/simple/ ledger_app_clients.ethereum` + pip install --extra-index-url https://test.pypi.org/simple/ ledger_app_clients.ethereum ``` ### Installation from sources diff --git a/client/pyproject.toml b/client/pyproject.toml index 8993e88..bea18ff 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -40,6 +40,5 @@ version = {attr = "ledger_app_clients.ethereum.__version__"} [project.urls] Home = "https://github.com/LedgerHQ/app-ethereum" -# [tool.setuptools_scm] -# write_to = "ledgerwallet/__version__.py" -# local_scheme = "no-local-version" +[tool.mypy] +ignore_missing_imports = true diff --git a/client/src/ledger_app_clients/ethereum/client.py b/client/src/ledger_app_clients/ethereum/client.py index f556d2d..18d774f 100644 --- a/client/src/ledger_app_clients/ethereum/client.py +++ b/client/src/ledger_app_clients/ethereum/client.py @@ -2,6 +2,7 @@ import rlp from enum import IntEnum from ragger.backend import BackendInterface from ragger.utils import RAPDU +from typing import List, Optional, Union from .command_builder import CommandBuilder from .eip712 import EIP712FieldType @@ -13,14 +14,15 @@ WEI_IN_ETH = 1e+18 class StatusWord(IntEnum): - OK = 0x9000 - ERROR_NO_INFO = 0x6a00 - INVALID_DATA = 0x6a80 - INSUFFICIENT_MEMORY = 0x6a84 - INVALID_INS = 0x6d00 - INVALID_P1_P2 = 0x6b00 + 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 + REF_DATA_NOT_FOUND = 0x6a88 + class DOMAIN_NAME_TAG(IntEnum): STRUCTURE_TYPE = 0x01 @@ -39,10 +41,10 @@ class EthAppClient: self._client = client self._cmd_builder = CommandBuilder() - def _send(self, payload: bytearray): + def _send(self, payload: bytes): return self._client.exchange_async_raw(payload) - def response(self) -> RAPDU: + def response(self) -> Optional[RAPDU]: return self._client._last_async_response def eip712_send_struct_def_struct_name(self, name: str): @@ -52,7 +54,7 @@ class EthAppClient: field_type: EIP712FieldType, type_name: str, type_size: int, - array_levels: [], + array_levels: List, key_name: str): return self._send(self._cmd_builder.eip712_send_struct_def_struct_field( field_type, @@ -68,7 +70,7 @@ class EthAppClient: 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(raw_value) + chunks = self._cmd_builder.eip712_send_struct_impl_struct_field(bytearray(raw_value)) for chunk in chunks[:-1]: with self._send(chunk): pass @@ -102,7 +104,7 @@ class EthAppClient: to: bytes, amount: float, chain_id: int): - data = list() + data: List[Union[int, bytes]] = list() data.append(nonce) data.append(gas_price) data.append(gas_limit) @@ -123,12 +125,12 @@ class EthAppClient: return self._send(self._cmd_builder.get_challenge()) def provide_domain_name(self, challenge: int, name: str, addr: bytes): - payload = format_tlv(DOMAIN_NAME_TAG.STRUCTURE_TYPE, 3) # TrustedDomainName + payload = format_tlv(DOMAIN_NAME_TAG.STRUCTURE_TYPE, 3) # TrustedDomainName payload += format_tlv(DOMAIN_NAME_TAG.STRUCTURE_VERSION, 1) - payload += format_tlv(DOMAIN_NAME_TAG.SIGNER_KEY_ID, 0) # test key - payload += format_tlv(DOMAIN_NAME_TAG.SIGNER_ALGO, 1) # secp256k1 + payload += format_tlv(DOMAIN_NAME_TAG.SIGNER_KEY_ID, 0) # test key + payload += format_tlv(DOMAIN_NAME_TAG.SIGNER_ALGO, 1) # secp256k1 payload += format_tlv(DOMAIN_NAME_TAG.CHALLENGE, challenge) - payload += format_tlv(DOMAIN_NAME_TAG.COIN_TYPE, 0x3c) # ETH in slip-44 + payload += format_tlv(DOMAIN_NAME_TAG.COIN_TYPE, 0x3c) # ETH in slip-44 payload += format_tlv(DOMAIN_NAME_TAG.DOMAIN_NAME, name) payload += format_tlv(DOMAIN_NAME_TAG.ADDRESS, addr) payload += format_tlv(DOMAIN_NAME_TAG.SIGNATURE, diff --git a/client/src/ledger_app_clients/ethereum/command_builder.py b/client/src/ledger_app_clients/ethereum/command_builder.py index 92d895d..8f2dbfd 100644 --- a/client/src/ledger_app_clients/ethereum/command_builder.py +++ b/client/src/ledger_app_clients/ethereum/command_builder.py @@ -1,7 +1,7 @@ import struct from enum import IntEnum from ragger.bip import pack_derivation_path -from typing import Iterator +from typing import List from .eip712 import EIP712FieldType @@ -41,7 +41,7 @@ class CommandBuilder: ins: InsType, p1: int, p2: int, - cdata: bytearray = bytes()) -> bytes: + cdata: bytes = bytes()) -> bytes: header = bytearray() header.append(self._CLA) @@ -67,24 +67,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) @@ -107,7 +107,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() @@ -193,7 +193,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: diff --git a/client/src/ledger_app_clients/ethereum/eip712/InputData.py b/client/src/ledger_app_clients/ethereum/eip712/InputData.py index 68f8556..ac0877c 100644 --- a/client/src/ledger_app_clients/ethereum/eip712/InputData.py +++ b/client/src/ledger_app_clients/ethereum/eip712/InputData.py @@ -3,7 +3,7 @@ import json import re import signal import sys -from typing import Callable +from typing import Any, Callable, Dict, List, Optional from ledger_app_clients.ethereum import keychain from ledger_app_clients.ethereum.client import EthAppClient, EIP712FieldType @@ -11,11 +11,16 @@ from ledger_app_clients.ethereum.client import EthAppClient, EIP712FieldType # global variables app_client: EthAppClient = None -filtering_paths = None -current_path = list() -sig_ctx = {} +filtering_paths: Dict = {} +current_path: List[str] = list() +sig_ctx: Dict[str, Any] = {} -autonext_handler: Callable = None + +def default_handler(): + raise RuntimeError("Uninitialized handler") + + +autonext_handler: Callable = default_handler # From a string typename, extract the type and all the array depth @@ -55,29 +60,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 @@ -86,7 +96,6 @@ parsing_type_functions["string"] = parse_string parsing_type_functions["bytes"] = parse_bytes - def send_struct_def_field(typename, keyname): type_enum = None @@ -108,7 +117,6 @@ def send_struct_def_field(typename, keyname): return (typename, type_enum, typesize, array_lvls) - def encode_integer(value, typesize): data = bytearray() @@ -122,9 +130,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: @@ -133,42 +141,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 @@ -180,7 +197,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): @@ -188,7 +204,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(): @@ -199,8 +214,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: @@ -215,7 +229,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]), @@ -232,7 +246,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(): @@ -244,6 +257,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 @@ -262,6 +276,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 @@ -281,12 +296,14 @@ 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): + +def read_filtering_file(filtering_file_path: str): 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 @@ -295,12 +312,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 @@ -314,7 +333,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()) @@ -322,22 +341,24 @@ 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 + if app_client._client.firmware.device == 'stax': # Stax Speculos is slow interval = seconds * 3 else: interval = seconds signal.setitimer(signal.ITIMER_REAL, seconds, interval) + 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: + filtering_file_path: Optional[str] = None, + autonext: Optional[Callable] = None) -> bool: global sig_ctx global app_client global autonext_handler @@ -357,7 +378,7 @@ def process_file(aclient: EthAppClient, if filtering_file_path: init_signature_context(types, domain) - filtr = read_filtering_file(domain, message, filtering_file_path) + filtr = read_filtering_file(filtering_file_path) # send types definition for key in types.keys(): @@ -365,7 +386,7 @@ def process_file(aclient: EthAppClient, pass for f in types[key]: (f["type"], f["enum"], f["typesize"], f["array_lvls"]) = \ - send_struct_def_field(f["type"], f["name"]) + send_struct_def_field(f["type"], f["name"]) if filtering_file_path: with app_client.eip712_filtering_activate(): diff --git a/client/src/ledger_app_clients/ethereum/keychain.py b/client/src/ledger_app_clients/ethereum/keychain.py index 523d1d1..4e66b6a 100644 --- a/client/src/ledger_app_clients/ethereum/keychain.py +++ b/client/src/ledger_app_clients/ethereum/keychain.py @@ -3,6 +3,7 @@ import hashlib from ecdsa import SigningKey from ecdsa.util import sigencode_der from enum import Enum, auto +from typing import Dict # Private key PEM files have to be named the same (lowercase) as their corresponding enum entries @@ -11,14 +12,17 @@ class Key(Enum): CAL = auto() DOMAIN_NAME = auto() -_keys: dict[Key, SigningKey] = dict() + +_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: diff --git a/client/src/ledger_app_clients/ethereum/response_parser.py b/client/src/ledger_app_clients/ethereum/response_parser.py index 5e73df4..26a2638 100644 --- a/client/src/ledger_app_clients/ethereum/response_parser.py +++ b/client/src/ledger_app_clients/ethereum/response_parser.py @@ -9,6 +9,7 @@ def signature(data: bytes) -> tuple[bytes, bytes, bytes]: return v, r, s + def challenge(data: bytes) -> int: assert len(data) == 4 return int.from_bytes(data, "big") diff --git a/client/src/ledger_app_clients/ethereum/settings.py b/client/src/ledger_app_clients/ethereum/settings.py index 2b44d45..d9d3ed5 100644 --- a/client/src/ledger_app_clients/ethereum/settings.py +++ b/client/src/ledger_app_clients/ethereum/settings.py @@ -1,6 +1,7 @@ from enum import Enum, auto from ragger.firmware import Firmware from ragger.navigator import Navigator, NavInsID, NavIns +from typing import List, Union class SettingID(Enum): @@ -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] diff --git a/client/src/ledger_app_clients/ethereum/tlv.py b/client/src/ledger_app_clients/ethereum/tlv.py index 2ff4cef..fd5dc10 100644 --- a/client/src/ledger_app_clients/ethereum/tlv.py +++ b/client/src/ledger_app_clients/ethereum/tlv.py @@ -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) From 72586268fa8c93543aed661590dd2ff6279c132c Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 28 Jul 2023 19:07:55 +0200 Subject: [PATCH 3/4] [ci][add] Using the 'reusable_pypi_deployment' workflow for Pytho package deployment --- .github/workflows/python-client.yml | 30 +++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/workflows/python-client.yml b/.github/workflows/python-client.yml index ca8f164..8a063a4 100644 --- a/.github/workflows/python-client.yml +++ b/.github/workflows/python-client.yml @@ -6,7 +6,13 @@ on: branches: - develop - master + paths: + - ./client/ + - .github/workflows/python-client.yml pull_request: + paths: + - ./client/ + - .github/workflows/python-client.yml jobs: lint: @@ -29,15 +35,15 @@ jobs: - name: Mypy type checking run: (cd client && mypy src/) - build: - name: Building the package - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v3 - - run: pip install --upgrade pip build twine - - name: Build and test the package - run: | - cd client/ - python -m build . - python -m twine check dist/* + packaging: + needs: [lint, mypy] + if: github.event_name == 'push' + name: Build, test and deploy the Python package + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_pypi_deployment.yml@v1 + with: + package_directory: "client/" + stable_deployment: ${{ github.ref == 'refs/heads/master' }} + check_changelog_version: true + publish: ${{ github.event_name == 'push' }} + secrets: + pypi_token: ${{ github.ref == 'refs/heads/master' && secrets.PYPI_PUBLIC_API_TOKEN || secrets.TEST_PYPI_PUBLIC_API_TOKEN }} From 91bb889ced5a564941ae1d8ee16333d59fea350e Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Tue, 29 Aug 2023 10:55:12 +0200 Subject: [PATCH 4/4] [fix] Review corrections --- .github/workflows/python-client.yml | 1 - client/MANIFEST.in | 2 +- client/README.md | 9 +++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-client.yml b/.github/workflows/python-client.yml index 8a063a4..984d51c 100644 --- a/.github/workflows/python-client.yml +++ b/.github/workflows/python-client.yml @@ -37,7 +37,6 @@ jobs: packaging: needs: [lint, mypy] - if: github.event_name == 'push' name: Build, test and deploy the Python package uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_pypi_deployment.yml@v1 with: diff --git a/client/MANIFEST.in b/client/MANIFEST.in index 65272bf..98060d1 100644 --- a/client/MANIFEST.in +++ b/client/MANIFEST.in @@ -1 +1 @@ -include src/ledger_app_clients/ethereum/keychain/* \ No newline at end of file +include src/ledger_app_clients/ethereum/keychain/* diff --git a/client/README.md b/client/README.md index 37da79a..13c5a4c 100644 --- a/client/README.md +++ b/client/README.md @@ -1,6 +1,6 @@ -# Ethereum app Python client +# Python client for the Ledger Ethereum application -This package allows to communicate with the Ethereum application, either on a +This package allows to communicate with the Ledger Ethereum application, either on a real device, or emulated on Speculos. ## Installation @@ -9,11 +9,13 @@ This package is deployed: - on `pypi.org` for the stable version. This version will work with the application available on the `master` branch. + ```bash pip install ledger_app_clients.ethereum ``` - on `test.pypi.org` for the rolling release. This version will work with the - application code on the `develop` branch. + application available on the `develop` branch. + ```bash pip install --extra-index-url https://test.pypi.org/simple/ ledger_app_clients.ethereum ``` @@ -23,6 +25,5 @@ This package is deployed: You can install the client from this repo: ```bash -cd client/ pip install . ```