diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 67b1a51..5a98566 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -13,26 +13,28 @@ env: CARGO_TERM_COLOR: always jobs: - build_clippy_fmt: + build: + name : Build runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest + strategy: + matrix: + target: ["nanos", "nanox", "nanosplus"] steps: - - name: arm-none-eabi-gcc - uses: fiam/arm-none-eabi-gcc@v1.0.3 - with: - release: '9-2019-q4' - - name: Checkout + - name: Clone + uses: actions/checkout@v3 + - name: Build app + run: cargo ledger build ${{ matrix.target }} + + clippy_fmt: + name: Run static analysis and formatting check + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest + steps: + - name: Clone uses: actions/checkout@v3 - - name: Install clang - run: sudo apt-get update && sudo apt install -y clang - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - override: true - components: rust-src, rustfmt, clippy - - name: Install cargo-ledger - run: cargo install --git=https://github.com/LedgerHQ/cargo-ledger - - name: Setup cargo-ledger - run: cargo ledger setup - name: Cargo clippy uses: actions-rs/cargo@v1 with: @@ -42,6 +44,3 @@ jobs: with: command: fmt args: --all -- --check - - name: Build app - run: cargo ledger build nanosplus - diff --git a/.gitignore b/.gitignore index 9833ef3..3735321 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,22 @@ target app.json +app_nanos.json +app_nanosplus.json +app_nanox.json +# Temporary directory with snapshots taken during test runs +tests/snapshots-tmp/ +# Python +*.pyc[cod] +*.egg +__pycache__/ +*.egg-info/ +.eggs/ +.python-version + +# Related to the Ledger VSCode extension +# Virtual env for sideload (macOS and Windows) +ledger/ +# Build directory +build/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1205c81..aaa22e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,17 +2,101 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "app-boilerplate-rust" +version = "1.0.0" +dependencies = [ + "include_gif", + "ledger_device_sdk", + "ledger_device_ui_sdk", + "ledger_secure_sdk_sys", + "numtoa", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.39", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] [[package]] name = "color_quant" @@ -20,6 +104,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "errno" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "gif" version = "0.11.4" @@ -30,57 +130,178 @@ dependencies = [ "weezl", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "include_gif" -version = "0.1.0" -source = "git+https://github.com/LedgerHQ/sdk_include_gif#699d28c6157518c4493899e2eeaa8af08346e5e7" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b17d2390c93f5d739a6df5584cdb20f45ff9f1bd3c2d5644eadd8a7dfd576e1" dependencies = [ "gif", - "syn", + "syn 1.0.109", ] [[package]] -name = "nanos_sdk" -version = "0.2.1" -source = "git+https://github.com/LedgerHQ/ledger-nanos-sdk.git#4d9bfc6183d94cee6edb239c39286be3825cc179" +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "ledger_device_sdk" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc0bff405870d65947aff3d60f7c3ea908aaeb953c0577de7528424c51baa79" dependencies = [ - "cc", + "ledger_secure_sdk_sys", "num-traits", "rand_core", + "zeroize", ] [[package]] -name = "nanos_ui" -version = "0.2.0" -source = "git+https://github.com/LedgerHQ/ledger-nanos-ui.git#6977cef7770c8c52738defe898fed6e274047498" +name = "ledger_device_ui_sdk" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "541fedd3af199deb197aa7153aade1f2c2024ebcb6c99b5beff0a317dc725310" dependencies = [ "include_gif", - "nanos_sdk", + "ledger_device_sdk", + "ledger_secure_sdk_sys", + "numtoa", +] + +[[package]] +name = "ledger_secure_sdk_sys" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd205aff8bb68ea83fbc55e69af8b4867e473142a747d85731a4a497c4f39045" +dependencies = [ + "bindgen", + "cc", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", ] [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] +[[package]] +name = "numtoa" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.39", +] + [[package]] name = "proc-macro2" -version = "1.0.60" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.28" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -92,13 +313,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] -name = "rust-app" -version = "0.2.1" +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ - "nanos_sdk", - "nanos_ui", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + [[package]] name = "syn" version = "1.0.109" @@ -110,14 +377,131 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "weezl" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zeroize" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12a3946ecfc929b583800f4629b6c25b88ac6e92a40ea5670f77112a85d40a8b" diff --git a/Cargo.toml b/Cargo.toml index 468411d..2916ed2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,22 @@ [package] -name = "rust-app" -version = "0.2.1" -authors = ["yhql"] +name = "app-boilerplate-rust" +version = "1.0.0" +authors = ["yhql", "agrojean-ledger"] edition = "2021" [dependencies] -nanos_sdk = { git = "https://github.com/LedgerHQ/ledger-nanos-sdk.git" } -nanos_ui = { git = "https://github.com/LedgerHQ/ledger-nanos-ui.git" } +ledger_device_sdk = "1.0.0" +ledger_device_ui_sdk = "1.1.0" +ledger_secure_sdk_sys = "1.0.0" +include_gif = "1.0.0" +numtoa = "0.2.4" [profile.release] opt-level = 'z' lto = true [features] -pending_review_screen = ["nanos_sdk/pending_review_screen"] +pending_review_screen = ["ledger_device_sdk/pending_review_screen"] [package.metadata.ledger] curve = ["secp256k1"] @@ -27,4 +30,4 @@ icon = "crab.gif" icon = "crab_14x14.gif" [package.metadata.ledger.nanosplus] -icon = "crab_14x14.gif" \ No newline at end of file +icon = "crab_14x14.gif" diff --git a/src/app_ui/address.rs b/src/app_ui/address.rs new file mode 100644 index 0000000..55713f5 --- /dev/null +++ b/src/app_ui/address.rs @@ -0,0 +1,54 @@ +/***************************************************************************** + * Ledger App Boilerplate Rust. + * (c) 2023 Ledger SAS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +use crate::utils::{concatenate, to_hex_all_caps}; +use crate::AppSW; +use core::str::from_utf8; +use ledger_device_ui_sdk::bitmaps::{CROSSMARK, EYE, VALIDATE_14}; +use ledger_device_ui_sdk::ui::{Field, MultiFieldReview}; + +// Display only the last 20 bytes of the address +const DISPLAY_ADDR_BYTES_LEN: usize = 20; + +pub fn ui_display_pk(addr: &[u8]) -> Result { + let addr_hex_str_buf = to_hex_all_caps(&addr[addr.len() - DISPLAY_ADDR_BYTES_LEN as usize..]) + .map_err(|_| AppSW::AddrDisplayFail)?; + let addr_hex_str = from_utf8(&addr_hex_str_buf[..DISPLAY_ADDR_BYTES_LEN * 2]) + .map_err(|_| AppSW::AddrDisplayFail)?; + + let mut addr_hex_str_with_prefix_buf = [0u8; DISPLAY_ADDR_BYTES_LEN * 2 + 2]; + concatenate(&["0x", &addr_hex_str], &mut addr_hex_str_with_prefix_buf); + let addr_hex_str_with_prefix = + from_utf8(&addr_hex_str_with_prefix_buf).map_err(|_| AppSW::AddrDisplayFail)?; + + let my_field = [Field { + name: "Address", + value: addr_hex_str_with_prefix, + }]; + + let my_review = MultiFieldReview::new( + &my_field, + &["Confirm Address"], + Some(&EYE), + "Approve", + Some(&VALIDATE_14), + "Reject", + Some(&CROSSMARK), + ); + + Ok(my_review.show()) +} diff --git a/src/app_ui/menu.rs b/src/app_ui/menu.rs new file mode 100644 index 0000000..ac39c3e --- /dev/null +++ b/src/app_ui/menu.rs @@ -0,0 +1,59 @@ +/***************************************************************************** + * Ledger App Boilerplate Rust. + * (c) 2023 Ledger SAS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +use include_gif::include_gif; +use ledger_device_sdk::io::{ApduHeader, Comm, Event}; +use ledger_device_ui_sdk::bitmaps::{Glyph, BACK, CERTIFICATE, DASHBOARD_X}; +use ledger_device_ui_sdk::ui::{EventOrPageIndex, MultiPageMenu, Page}; + +fn ui_about_menu(comm: &mut Comm) -> Event { + let pages = [ + &Page::from((["Rust Boilerplate", "(c) 2023 Ledger"], true)), + &Page::from(("Back", &BACK)), + ]; + loop { + match MultiPageMenu::new(comm, &pages).show() { + EventOrPageIndex::Event(e) => return e, + i => match i { + EventOrPageIndex::Index(1) => return ui_menu_main(comm), + _ => (), + }, + } + } +} + +pub fn ui_menu_main(comm: &mut Comm) -> Event { + const APP_ICON: Glyph = Glyph::from_include(include_gif!("crab.gif")); + let pages = [ + // The from trait allows to create different styles of pages + // without having to use the new() function. + &Page::from((["Boilerplate", "is ready"], &APP_ICON)), + &Page::from((["Version", env!("CARGO_PKG_VERSION")], true)), + &Page::from(("About", &CERTIFICATE)), + &Page::from(("Quit", &DASHBOARD_X)), + ]; + loop { + match MultiPageMenu::new(comm, &pages).show() { + EventOrPageIndex::Event(e) => return e, + i => match i { + EventOrPageIndex::Index(2) => return ui_about_menu(comm), + EventOrPageIndex::Index(3) => ledger_device_sdk::exit_app(0), + _ => (), + }, + } + } +} diff --git a/src/app_ui/sign.rs b/src/app_ui/sign.rs new file mode 100644 index 0000000..1531b81 --- /dev/null +++ b/src/app_ui/sign.rs @@ -0,0 +1,77 @@ +/***************************************************************************** + * Ledger App Boilerplate Rust. + * (c) 2023 Ledger SAS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +use crate::handlers::sign_tx::Tx; +use crate::utils::{concatenate, to_hex_all_caps}; +use crate::AppSW; +use core::str::from_utf8; +use ledger_device_ui_sdk::bitmaps::{CROSSMARK, EYE, VALIDATE_14}; +use ledger_device_ui_sdk::ui::{Field, MultiFieldReview}; +use numtoa::NumToA; + +pub fn ui_display_tx(tx: &Tx) -> Result { + // Format amount value + let mut amount_buf = [0u8; 20]; + let mut amount_with_denom_buf = [0u8; 25]; + concatenate( + &["CRAB", " ", tx.value.numtoa_str(10, &mut amount_buf)], + &mut amount_with_denom_buf, + ); + let amount_str_with_denom = from_utf8(&amount_with_denom_buf) + .map_err(|_| AppSW::TxDisplayFail)? + .trim_matches(char::from(0)); + + // Format destination address + let hex_addr_buf = to_hex_all_caps(&tx.to).map_err(|_| AppSW::TxDisplayFail)?; + let hex_addr_str = from_utf8(&hex_addr_buf).map_err(|_| AppSW::TxDisplayFail)?; + let mut addr_with_prefix_buf = [0u8; 42]; + concatenate(&["0x", hex_addr_str], &mut addr_with_prefix_buf); + let hex_addr_str_with_prefix = + from_utf8(&addr_with_prefix_buf).map_err(|_| AppSW::TxDisplayFail)?; + + // Format memo + let memo_str = from_utf8(&tx.memo[..tx.memo_len as usize]).map_err(|_| AppSW::TxDisplayFail)?; + + // Define transaction review fields + let my_fields = [ + Field { + name: "Amount", + value: amount_str_with_denom, + }, + Field { + name: "Destination", + value: hex_addr_str_with_prefix, + }, + Field { + name: "Memo", + value: memo_str, + }, + ]; + + // Create transaction review + let my_review = MultiFieldReview::new( + &my_fields, + &["Review ", "Transaction"], + Some(&EYE), + "Approve", + Some(&VALIDATE_14), + "Reject", + Some(&CROSSMARK), + ); + + Ok(my_review.show()) +} diff --git a/src/handlers/get_public_key.rs b/src/handlers/get_public_key.rs new file mode 100644 index 0000000..7d93ff7 --- /dev/null +++ b/src/handlers/get_public_key.rs @@ -0,0 +1,82 @@ +/***************************************************************************** + * Ledger App Boilerplate Rust. + * (c) 2023 Ledger SAS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +use crate::app_ui::address::ui_display_pk; +use crate::utils::{read_bip32_path, MAX_ALLOWED_PATH_LEN}; +use crate::AppSW; +use ledger_device_sdk::ecc::{Secp256k1, SeedDerive}; +use ledger_device_sdk::io::Comm; +use ledger_device_sdk::testing; +use ledger_secure_sdk_sys::{ + cx_hash_no_throw, cx_hash_t, cx_keccak_init_no_throw, cx_sha3_t, CX_LAST, CX_OK, +}; + +pub fn handler_get_public_key(comm: &mut Comm, display: bool) -> Result<(), AppSW> { + let mut path = [0u32; MAX_ALLOWED_PATH_LEN]; + let data = match comm.get_data() { + Ok(data) => data, + Err(_) => return Err(AppSW::WrongDataLength), + }; + + let path_len = read_bip32_path(data, &mut path)?; + + let pk = Secp256k1::derive_from_path(&path[..path_len]) + .public_key() + .map_err(|_| AppSW::KeyDeriveFail)?; + + // Display address on device if requested + if display { + let mut keccak256: cx_sha3_t = Default::default(); + let mut address: [u8; 32] = [0u8; 32]; + + unsafe { + if cx_keccak_init_no_throw(&mut keccak256, 256) != CX_OK { + return Err(AppSW::AddrDisplayFail); + } + + let mut pk_mut = pk.pubkey; + let pk_ptr = pk_mut.as_mut_ptr().offset(1); + if cx_hash_no_throw( + &mut keccak256.header as *mut cx_hash_t, + CX_LAST, + pk_ptr, + 64 as usize, + address.as_mut_ptr(), + address.len(), + ) != CX_OK + { + return Err(AppSW::AddrDisplayFail); + } + } + + testing::debug_print("showing public key\n"); + if !ui_display_pk(&address)? { + testing::debug_print("denied\n"); + return Err(AppSW::Deny); + } + } + + comm.append(&[pk.pubkey.len() as u8]); + comm.append(&pk.pubkey); + // Rust SDK key derivation API does not return chaincode yet + // so we just append a dummy chaincode. + const CHAINCODE_LEN: usize = 32; + comm.append(&[CHAINCODE_LEN as u8]); // Dummy chaincode length + comm.append(&[0u8; CHAINCODE_LEN]); // Dummy chaincode + + Ok(()) +} diff --git a/src/handlers/get_version.rs b/src/handlers/get_version.rs new file mode 100644 index 0000000..559a5af --- /dev/null +++ b/src/handlers/get_version.rs @@ -0,0 +1,39 @@ +/***************************************************************************** + * Ledger App Boilerplate Rust. + * (c) 2023 Ledger SAS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ +use crate::AppSW; +use core::str::FromStr; +use ledger_device_sdk::io; + +pub fn handler_get_version(comm: &mut io::Comm) -> Result<(), AppSW> { + if let Some((major, minor, patch)) = parse_version_string(env!("CARGO_PKG_VERSION")) { + comm.append(&[major, minor, patch]); + Ok(()) + } else { + Err(AppSW::VersionParsingFail) + } +} + +fn parse_version_string(input: &str) -> Option<(u8, u8, u8)> { + // Split the input string by '.'. + // Input should be of the form "major.minor.patch", + // where "major", "minor", and "patch" are integers. + let mut parts = input.split('.'); + let major = u8::from_str(parts.next()?).ok()?; + let minor = u8::from_str(parts.next()?).ok()?; + let patch = u8::from_str(parts.next()?).ok()?; + Some((major, minor, patch)) +} diff --git a/src/handlers/sign_tx.rs b/src/handlers/sign_tx.rs new file mode 100644 index 0000000..cc38939 --- /dev/null +++ b/src/handlers/sign_tx.rs @@ -0,0 +1,177 @@ +/***************************************************************************** + * Ledger App Boilerplate Rust. + * (c) 2023 Ledger SAS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ +use crate::app_ui::sign::ui_display_tx; +use crate::utils::{read_bip32_path, slice_or_err, varint_read, MAX_ALLOWED_PATH_LEN}; +use crate::AppSW; +use ledger_device_sdk::ecc::{Secp256k1, SeedDerive}; +use ledger_device_sdk::io::Comm; +use ledger_secure_sdk_sys::{ + cx_hash_no_throw, cx_hash_t, cx_keccak_init_no_throw, cx_sha3_t, CX_LAST, CX_OK, +}; + +const MAX_TRANSACTION_LEN: usize = 510; + +pub struct Tx<'a> { + nonce: u64, + pub value: u64, + pub to: &'a [u8], + pub memo: &'a [u8], + pub memo_len: usize, +} + +// Implement deserialize for Tx from a u8 array +impl<'a> TryFrom<&'a [u8]> for Tx<'a> { + type Error = (); + fn try_from(raw_tx: &'a [u8]) -> Result { + if raw_tx.len() > MAX_TRANSACTION_LEN { + return Err(()); + } + + // Try to parse the transaction fields : + // Nonce + let nonce = u64::from_be_bytes(slice_or_err(raw_tx, 0, 8)?.try_into().map_err(|_| ())?); + // Destination address + let to = slice_or_err(raw_tx, 8, 20)?; + // Amount value + let value = u64::from_be_bytes(slice_or_err(raw_tx, 28, 8)?.try_into().map_err(|_| ())?); + // Memo length + let (memo_len_u64, memo_len_size) = varint_read(&raw_tx[36..])?; + let memo_len = memo_len_u64 as usize; + // Memo + let memo = slice_or_err(raw_tx, 36 + memo_len_size, memo_len)?; + + // Check memo ASCII encoding + if !memo[..memo_len].iter().all(|&byte| byte.is_ascii()) { + return Err(()); + } + + Ok(Tx { + nonce, + value, + to, + memo, + memo_len, + }) + } +} + +// #[derive(Copy, Clone)] +pub struct TxContext { + raw_tx: [u8; MAX_TRANSACTION_LEN], // raw transaction serialized + raw_tx_len: usize, // length of raw transaction + path: [u32; MAX_ALLOWED_PATH_LEN], // BIP32 path for key derivation + path_len: usize, // length of BIP32 path +} + +// Implement constructor for TxInfo with default values +impl TxContext { + pub fn new() -> TxContext { + TxContext { + raw_tx: [0u8; MAX_TRANSACTION_LEN], + raw_tx_len: 0, + path: [0u32; MAX_ALLOWED_PATH_LEN], + path_len: 0, + } + } + // Implement reset for TxInfo + fn reset(&mut self) { + self.raw_tx = [0u8; MAX_TRANSACTION_LEN]; + self.raw_tx_len = 0; + self.path = [0u32; MAX_ALLOWED_PATH_LEN]; + self.path_len = 0; + } +} + +pub fn handler_sign_tx( + comm: &mut Comm, + chunk: u8, + more: bool, + ctx: &mut TxContext, +) -> Result<(), AppSW> { + // Try to get data from comm + let data = match comm.get_data() { + Ok(data) => data, + Err(_) => return Err(AppSW::WrongDataLength), + }; + // First chunk, try to parse the path + if chunk == 0 { + // Reset transaction context + ctx.reset(); + // This will propagate the error if the path is invalid + ctx.path_len = read_bip32_path(data, &mut ctx.path)?; + // Next chunks, append data to raw_tx and return or parse + // the transaction if it is the last chunk. + } else { + if ctx.raw_tx_len + data.len() > MAX_TRANSACTION_LEN { + return Err(AppSW::TxWrongLength); + } + + // Append data to raw_tx + ctx.raw_tx[ctx.raw_tx_len..ctx.raw_tx_len + data.len()].copy_from_slice(data); + ctx.raw_tx_len += data.len(); + + // If we expect more chunks, return + if more { + return Ok(()); + // Otherwise, try to parse the transaction + } else { + let tx = match Tx::try_from(&ctx.raw_tx[..ctx.raw_tx_len]) { + Ok(tx) => tx, + Err(_) => return Err(AppSW::TxParsingFail), + }; + // Display transaction. If user approves + // the transaction, sign it. Otherwise, + // return a "deny" status word. + if ui_display_tx(&tx)? { + return compute_signature_and_append(comm, ctx); + } else { + return Err(AppSW::Deny); + } + } + } + Ok(()) +} + +fn compute_signature_and_append(comm: &mut Comm, ctx: &mut TxContext) -> Result<(), AppSW> { + let mut keccak256: cx_sha3_t = Default::default(); + let mut message_hash: [u8; 32] = [0u8; 32]; + + unsafe { + if cx_keccak_init_no_throw(&mut keccak256, 256) != CX_OK { + return Err(AppSW::TxHashFail); + } + if cx_hash_no_throw( + &mut keccak256.header as *mut cx_hash_t, + CX_LAST, + ctx.raw_tx.as_ptr(), + ctx.raw_tx_len, + message_hash.as_mut_ptr(), + message_hash.len(), + ) != CX_OK + { + return Err(AppSW::TxHashFail); + } + } + + let (sig, siglen, parity) = Secp256k1::derive_from_path(&ctx.path[..ctx.path_len]) + .deterministic_sign(&message_hash) + .map_err(|_| AppSW::TxSignFail)?; + comm.append(&[siglen as u8]); + comm.append(&sig[..siglen as usize]); + comm.append(&[parity as u8]); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index e70ce1b..2966b8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,101 +1,121 @@ +/***************************************************************************** + * Ledger App Boilerplate Rust. + * (c) 2023 Ledger SAS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + #![no_std] #![no_main] mod utils; +mod app_ui { + pub mod address; + pub mod menu; + pub mod sign; +} +mod handlers { + pub mod get_public_key; + pub mod get_version; + pub mod sign_tx; +} -use core::str::from_utf8; -use nanos_sdk::buttons::ButtonEvent; -use nanos_sdk::ecc::{Secp256k1, SeedDerive}; -use nanos_sdk::io; -use nanos_sdk::io::SyscallError; -use nanos_ui::ui; - -nanos_sdk::set_panic!(nanos_sdk::exiting_panic); - -pub const BIP32_PATH: [u32; 5] = nanos_sdk::ecc::make_bip32_path(b"m/44'/535348'/0'/0/0"); - -/// Display public key in two separate -/// message scrollers -fn show_pubkey() { - let pubkey = Secp256k1::derive_from_path(&BIP32_PATH).public_key(); - match pubkey { - Ok(pk) => { - { - let hex0 = utils::to_hex(&pk.as_ref()[1..33]).unwrap(); - let m = from_utf8(&hex0).unwrap(); - ui::MessageScroller::new(m).event_loop(); - } - { - let hex1 = utils::to_hex(&pk.as_ref()[33..65]).unwrap(); - let m = from_utf8(&hex1).unwrap(); - ui::MessageScroller::new(m).event_loop(); - } - } - Err(_) => ui::popup("Error"), - } +use ledger_device_sdk::buttons::ButtonEvent; +use ledger_device_sdk::io::{ApduHeader, Comm, Event, Reply}; + +use ledger_device_ui_sdk::ui; + +use app_ui::menu::ui_menu_main; +use handlers::{ + get_public_key::handler_get_public_key, + get_version::handler_get_version, + sign_tx::{handler_sign_tx, TxContext}, +}; + +ledger_device_sdk::set_panic!(ledger_device_sdk::exiting_panic); + +// CLA (APDU class byte) for all APDUs. +const CLA: u8 = 0xe0; +// P2 for last APDU to receive. +const P2_SIGN_TX_LAST: u8 = 0x00; +// P2 for more APDU to receive. +const P2_SIGN_TX_MORE: u8 = 0x80; +// P1 for first APDU number. +const P1_SIGN_TX_START: u8 = 0x00; +// P1 for maximum APDU number. +const P1_SIGN_TX_MAX: u8 = 0x03; + +// Application status words. +#[repr(u16)] +pub enum AppSW { + Deny = 0x6985, + WrongP1P2 = 0x6A86, + WrongDataLength = 0x6A87, + InsNotSupported = 0x6D00, + ClaNotSupported = 0x6E00, + TxDisplayFail = 0xB001, + AddrDisplayFail = 0xB002, + TxWrongLength = 0xB004, + TxParsingFail = 0xB005, + TxHashFail = 0xB006, + TxSignFail = 0xB008, + KeyDeriveFail = 0xB009, + VersionParsingFail = 0xB00A, } -/// Basic nested menu. Will be subject -/// to simplifications in the future. -#[allow(clippy::needless_borrow)] -fn menu_example() { - loop { - match ui::Menu::new(&[&"PubKey", &"Infos", &"Back", &"Exit App"]).show() { - 0 => show_pubkey(), - 1 => loop { - match ui::Menu::new(&[&"Copyright", &"Authors", &"Back"]).show() { - 0 => ui::popup("2020 Ledger"), - 1 => ui::popup("???"), - _ => break, - } - }, - 2 => return, - 3 => nanos_sdk::exit_app(0), - _ => (), - } +impl From for Reply { + fn from(sw: AppSW) -> Reply { + Reply(sw as u16) } } -/// This is the UI flow for signing, composed of a scroller -/// to read the incoming message, a panel that requests user -/// validation, and an exit message. -fn sign_ui(message: &[u8]) -> Result, SyscallError> { - ui::popup("Message review"); - - { - let hex = utils::to_hex(message).map_err(|_| SyscallError::Overflow)?; - let m = from_utf8(&hex).map_err(|_| SyscallError::InvalidParameter)?; - - ui::MessageScroller::new(m).event_loop(); - } +#[repr(u8)] +// Instruction set for the app. +enum Ins { + GetVersion, + GetAppName, + GetPubkey, + SignTx, + UnknownIns, +} - if ui::Validator::new("Sign ?").ask() { - let signature = Secp256k1::derive_from_path(&BIP32_PATH) - .deterministic_sign(message) - .map_err(|_| SyscallError::Unspecified)?; - ui::popup("Done !"); - Ok(Some(signature)) - } else { - ui::popup("Cancelled"); - Ok(None) +impl From for Ins { + fn from(header: ApduHeader) -> Ins { + match header.ins { + 3 => Ins::GetVersion, + 4 => Ins::GetAppName, + 5 => Ins::GetPubkey, + 6 => Ins::SignTx, + _ => Ins::UnknownIns, + } } } #[no_mangle] extern "C" fn sample_pending() { - let mut comm = io::Comm::new(); + let mut comm = Comm::new(); loop { ui::SingleMessage::new("Pending").show(); match comm.next_event::() { - io::Event::Button(ButtonEvent::RightButtonRelease) => break, + Event::Button(ButtonEvent::RightButtonRelease) => break, _ => (), } } loop { ui::SingleMessage::new("Ledger review").show(); match comm.next_event::() { - io::Event::Button(ButtonEvent::BothButtonsRelease) => break, + Event::Button(ButtonEvent::BothButtonsRelease) => break, _ => (), } } @@ -103,67 +123,89 @@ extern "C" fn sample_pending() { #[no_mangle] extern "C" fn sample_main() { - let mut comm = io::Comm::new(); + let mut comm = Comm::new(); + let mut tx_ctx = TxContext::new(); loop { - // Draw some 'welcome' screen - ui::SingleMessage::new("W e l c o m e").show(); - // Wait for either a specific button push to exit the app // or an APDU command - match comm.next_event() { - io::Event::Button(ButtonEvent::RightButtonRelease) => nanos_sdk::exit_app(0), - io::Event::Command(ins) => match handle_apdu(&mut comm, ins) { + match ui_menu_main(&mut comm) { + Event::Command(ins) => match handle_apdu(&mut comm, ins.into(), &mut tx_ctx) { Ok(()) => comm.reply_ok(), - Err(sw) => comm.reply(sw), + Err(sw) => comm.reply(Reply::from(sw)), }, _ => (), } } } -#[repr(u8)] -enum Ins { - GetPubkey, - Sign, - Menu, - Exit, -} - -impl From for Ins { - fn from(header: io::ApduHeader) -> Ins { - match header.ins { - 2 => Ins::GetPubkey, - 3 => Ins::Sign, - 4 => Ins::Menu, - 0xff => Ins::Exit, - _ => panic!(), - } +fn handle_apdu(comm: &mut Comm, ins: Ins, ctx: &mut TxContext) -> Result<(), AppSW> { + if comm.rx == 0 { + return Err(AppSW::WrongDataLength); } -} -use nanos_sdk::io::Reply; + let apdu_metadata = comm.get_apdu_metadata(); -fn handle_apdu(comm: &mut io::Comm, ins: Ins) -> Result<(), Reply> { - if comm.rx == 0 { - return Err(io::StatusWords::NothingReceived.into()); + if apdu_metadata.cla != CLA { + return Err(AppSW::ClaNotSupported); } match ins { + Ins::GetAppName => { + if apdu_metadata.p1 != 0 || apdu_metadata.p2 != 0 { + return Err(AppSW::WrongP1P2); + } + comm.append(env!("CARGO_PKG_NAME").as_bytes()); + } + Ins::GetVersion => { + if apdu_metadata.p1 != 0 || apdu_metadata.p2 != 0 { + return Err(AppSW::WrongP1P2); + } + return handler_get_version(comm); + } Ins::GetPubkey => { - let pk = Secp256k1::derive_from_path(&BIP32_PATH) - .public_key() - .map_err(|x| Reply(0x6eu16 | (x as u16 & 0xff)))?; - comm.append(pk.as_ref()); + if apdu_metadata.p1 > 1 || apdu_metadata.p2 != 0 { + return Err(AppSW::WrongP1P2); + } + + match comm.get_data() { + Ok(data) => { + if data.len() == 0 { + return Err(AppSW::WrongDataLength); + } + } + Err(_) => return Err(AppSW::WrongDataLength), + } + + return handler_get_public_key(comm, apdu_metadata.p1 == 1); } - Ins::Sign => { - let out = sign_ui(comm.get_data()?)?; - if let Some((signature_buf, length, _)) = out { - comm.append(&signature_buf[..length as usize]) + Ins::SignTx => { + if (apdu_metadata.p1 == P1_SIGN_TX_START && apdu_metadata.p2 != P2_SIGN_TX_MORE) + || apdu_metadata.p1 > P1_SIGN_TX_MAX + || (apdu_metadata.p2 != P2_SIGN_TX_LAST && apdu_metadata.p2 != P2_SIGN_TX_MORE) + { + return Err(AppSW::WrongP1P2); + } + + match comm.get_data() { + Ok(data) => { + if data.len() == 0 { + return Err(AppSW::WrongDataLength); + } + } + Err(_) => return Err(AppSW::WrongDataLength), } + + return handler_sign_tx( + comm, + apdu_metadata.p1, + apdu_metadata.p2 == P2_SIGN_TX_MORE, + ctx, + ); + } + Ins::UnknownIns => { + return Err(AppSW::InsNotSupported); } - Ins::Menu => menu_example(), - Ins::Exit => nanos_sdk::exit_app(0), } Ok(()) } diff --git a/src/utils.rs b/src/utils.rs index 352bbab..a5ce270 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,16 @@ +use crate::AppSW; use core::char; +pub const MAX_ALLOWED_PATH_LEN: usize = 10; +const MAX_HEX_LEN: usize = 64; + /// Convert to hex. Returns a static buffer of 64 bytes #[inline] -pub fn to_hex(m: &[u8]) -> Result<[u8; 64], ()> { - if 2 * m.len() > 64 { +pub fn to_hex(m: &[u8]) -> Result<[u8; MAX_HEX_LEN], ()> { + if 2 * m.len() > MAX_HEX_LEN { return Err(()); } - let mut hex = [0u8; 64]; + let mut hex = [0u8; MAX_HEX_LEN]; let mut i = 0; for c in m { let c0 = char::from_digit((c >> 4).into(), 16).unwrap(); @@ -17,3 +21,108 @@ pub fn to_hex(m: &[u8]) -> Result<[u8; 64], ()> { } Ok(hex) } + +/// Convert to an all capitalized string. Returns a static buffer of 255 bytes +#[inline] +pub fn to_hex_all_caps(m: &[u8]) -> Result<[u8; MAX_HEX_LEN], ()> { + match to_hex(m) { + Ok(hex) => { + let mut hex_all_caps = hex; + hex_all_caps + .iter_mut() + .for_each(|x| *x = x.to_ascii_uppercase()); + Ok(hex_all_caps) + } + Err(_) => Err(()), + } +} + +/// Convert serialized derivation path to u32 array elements +pub fn read_bip32_path(data: &[u8], path: &mut [u32]) -> Result { + // Check input length and path buffer capacity + if data.len() < 1 || path.len() < data.len() / 4 { + return Err(AppSW::WrongDataLength); + } + + let path_len = data[0] as usize; // First byte is the length of the path + let path_data = &data[1..]; + + // Check path data length and alignment + if path_data.len() != path_len * 4 + || path_data.len() > MAX_ALLOWED_PATH_LEN * 4 + || path_data.len() % 4 != 0 + { + return Err(AppSW::WrongDataLength); + } + + let mut idx = 0; + for (i, chunk) in path_data.chunks(4).enumerate() { + path[idx] = u32::from_be_bytes(chunk.try_into().unwrap()); + idx = i + 1; + } + + Ok(idx) +} + +/// Concatenate multiple strings into a fixed-size array +pub fn concatenate(strings: &[&str], output: &mut [u8]) { + let mut offset = 0; + + for s in strings { + let s_len = s.len(); + let copy_len = core::cmp::min(s_len, output.len() - offset); + + if copy_len > 0 { + output[offset..offset + copy_len].copy_from_slice(&s.as_bytes()[..copy_len]); + offset += copy_len; + } else { + // If the output buffer is full, stop concatenating. + break; + } + } +} + +/// Get a subslice of a slice or return an error +#[inline] +pub fn slice_or_err(slice: &[u8], start: usize, len: usize) -> Result<&[u8], ()> { + match slice.get(start..start + len) { + Some(s) => Ok(s), + None => Err(()), + } +} + +/// Read a varint from a slice +pub fn varint_read(input: &[u8]) -> Result<(u64, usize), ()> { + let mut bytes = [0u8; 8]; + let int_length: usize; + + if input.is_empty() { + return Err(()); + } + + let prefix = input[0]; + + if prefix == 0xFD { + if input.len() < 3 { + return Err(()); + } + int_length = 2; + } else if prefix == 0xFE { + if input.len() < 5 { + return Err(()); + } + int_length = 4; + } else if prefix == 0xFF { + if input.len() < 9 { + return Err(()); + } + int_length = 8; + } else { + return Ok((u64::from(prefix), 1)); + } + + let buf = slice_or_err(input, 1, int_length)?; + bytes[..int_length].copy_from_slice(buf); + let result = u64::from_le_bytes(bytes); + Ok((result, int_length + 1)) +} diff --git a/test/menu.apdu b/test/menu.apdu deleted file mode 100644 index 9135215..0000000 --- a/test/menu.apdu +++ /dev/null @@ -1 +0,0 @@ -8004 \ No newline at end of file diff --git a/test/overflow.apdu b/test/overflow.apdu deleted file mode 100644 index eacb2f7..0000000 --- a/test/overflow.apdu +++ /dev/null @@ -1 +0,0 @@ -80050008 \ No newline at end of file diff --git a/test/quit.apdu b/test/quit.apdu deleted file mode 100644 index d8d26d5..0000000 --- a/test/quit.apdu +++ /dev/null @@ -1 +0,0 @@ -80FF \ No newline at end of file diff --git a/test/sign.apdu b/test/sign.apdu deleted file mode 100644 index 5cbddb7..0000000 --- a/test/sign.apdu +++ /dev/null @@ -1 +0,0 @@ -800300002000112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210 \ No newline at end of file diff --git a/test/test_cmds.py b/test/test_cmds.py deleted file mode 100644 index 80d9260..0000000 --- a/test/test_cmds.py +++ /dev/null @@ -1,31 +0,0 @@ -from ledgerblue.commTCP import getDongle as getDongleTCP -from ledgerblue.comm import getDongle - -from random import getrandbits as rnd -from binascii import hexlify, unhexlify - -rand_msg = hexlify(rnd(256).to_bytes(32, 'big')).decode() - -CMDS = [ - "8002", - "8003000020" + "00112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210", - "8003000020" + rand_msg, - "8004", - "80050008", - "80FE", - "80FF", -] - -d = getDongleTCP(port=9999) # Speculos -# d = getDongle() # Nano - -from time import sleep -for cmd in map(unhexlify,CMDS): - r = None - try: - r = d.exchange(cmd, 20) - sleep(1) - except Exception as e: - print(e) - if r is not None: - print("Response : ", hexlify(r)) diff --git a/tests/application_client/__init__.py b/tests/application_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/application_client/boilerplate_command_sender.py b/tests/application_client/boilerplate_command_sender.py new file mode 100644 index 0000000..cb83ce7 --- /dev/null +++ b/tests/application_client/boilerplate_command_sender.py @@ -0,0 +1,127 @@ +from enum import IntEnum +from typing import Generator, List, Optional +from contextlib import contextmanager + +from ragger.backend.interface import BackendInterface, RAPDU +from ragger.bip import pack_derivation_path + + +MAX_APDU_LEN: int = 255 + +CLA: int = 0xE0 + +class P1(IntEnum): + # Parameter 1 for first APDU number. + P1_START = 0x00 + # Parameter 1 for maximum APDU number. + P1_MAX = 0x03 + # Parameter 1 for screen confirmation for GET_PUBLIC_KEY. + P1_CONFIRM = 0x01 + +class P2(IntEnum): + # Parameter 2 for last APDU to receive. + P2_LAST = 0x00 + # Parameter 2 for more APDU to receive. + P2_MORE = 0x80 + +class InsType(IntEnum): + GET_VERSION = 0x03 + GET_APP_NAME = 0x04 + GET_PUBLIC_KEY = 0x05 + SIGN_TX = 0x06 + +class Errors(IntEnum): + SW_DENY = 0x6985 + SW_WRONG_P1P2 = 0x6A86 + SW_WRONG_DATA_LENGTH = 0x6A87 + SW_INS_NOT_SUPPORTED = 0x6D00 + SW_CLA_NOT_SUPPORTED = 0x6E00 + SW_WRONG_RESPONSE_LENGTH = 0xB000 + SW_DISPLAY_BIP32_PATH_FAIL = 0xB001 + SW_DISPLAY_ADDRESS_FAIL = 0xB002 + SW_DISPLAY_AMOUNT_FAIL = 0xB003 + SW_WRONG_TX_LENGTH = 0xB004 + SW_TX_PARSING_FAIL = 0xB005 + SW_TX_HASH_FAIL = 0xB006 + SW_BAD_STATE = 0xB007 + SW_SIGNATURE_FAIL = 0xB008 + + +def split_message(message: bytes, max_size: int) -> List[bytes]: + return [message[x:x + max_size] for x in range(0, len(message), max_size)] + + +class BoilerplateCommandSender: + def __init__(self, backend: BackendInterface) -> None: + self.backend = backend + + + def get_app_and_version(self) -> RAPDU: + return self.backend.exchange(cla=0xB0, # specific CLA for BOLOS + ins=0x01, # specific INS for get_app_and_version + p1=P1.P1_START, + p2=P2.P2_LAST, + data=b"") + + + def get_version(self) -> RAPDU: + return self.backend.exchange(cla=CLA, + ins=InsType.GET_VERSION, + p1=P1.P1_START, + p2=P2.P2_LAST, + data=b"") + + + def get_app_name(self) -> RAPDU: + return self.backend.exchange(cla=CLA, + ins=InsType.GET_APP_NAME, + p1=P1.P1_START, + p2=P2.P2_LAST, + data=b"") + + + def get_public_key(self, path: str) -> RAPDU: + return self.backend.exchange(cla=CLA, + ins=InsType.GET_PUBLIC_KEY, + p1=P1.P1_START, + p2=P2.P2_LAST, + data=pack_derivation_path(path)) + + + @contextmanager + def get_public_key_with_confirmation(self, path: str) -> Generator[None, None, None]: + with self.backend.exchange_async(cla=CLA, + ins=InsType.GET_PUBLIC_KEY, + p1=P1.P1_CONFIRM, + p2=P2.P2_LAST, + data=pack_derivation_path(path)) as response: + yield response + + + @contextmanager + def sign_tx(self, path: str, transaction: bytes) -> Generator[None, None, None]: + self.backend.exchange(cla=CLA, + ins=InsType.SIGN_TX, + p1=P1.P1_START, + p2=P2.P2_MORE, + data=pack_derivation_path(path)) + messages = split_message(transaction, MAX_APDU_LEN) + idx: int = P1.P1_START + 1 + + for msg in messages[:-1]: + self.backend.exchange(cla=CLA, + ins=InsType.SIGN_TX, + p1=idx, + p2=P2.P2_MORE, + data=msg) + idx += 1 + + with self.backend.exchange_async(cla=CLA, + ins=InsType.SIGN_TX, + p1=idx, + p2=P2.P2_LAST, + data=messages[-1]) as response: + yield response + + def get_async_response(self) -> Optional[RAPDU]: + return self.backend.last_async_response diff --git a/tests/application_client/boilerplate_response_unpacker.py b/tests/application_client/boilerplate_response_unpacker.py new file mode 100644 index 0000000..4e6fc9f --- /dev/null +++ b/tests/application_client/boilerplate_response_unpacker.py @@ -0,0 +1,69 @@ +from typing import Tuple +from struct import unpack + +# remainder, data_len, data +def pop_sized_buf_from_buffer(buffer:bytes, size:int) -> Tuple[bytes, bytes]: + return buffer[size:], buffer[0:size] + +# remainder, data_len, data +def pop_size_prefixed_buf_from_buf(buffer:bytes) -> Tuple[bytes, int, bytes]: + data_len = buffer[0] + return buffer[1+data_len:], data_len, buffer[1:data_len+1] + +# Unpack from response: +# response = app_name (var) +def unpack_get_app_name_response(response: bytes) -> str: + return response.decode("ascii") + +# Unpack from response: +# response = MAJOR (1) +# MINOR (1) +# PATCH (1) +def unpack_get_version_response(response: bytes) -> Tuple[int, int, int]: + assert len(response) == 3 + major, minor, patch = unpack("BBB", response) + return (major, minor, patch) + +# Unpack from response: +# response = format_id (1) +# app_name_raw_len (1) +# app_name_raw (var) +# version_raw_len (1) +# version_raw (var) +# unused_len (1) +# unused (var) +def unpack_get_app_and_version_response(response: bytes) -> Tuple[str, str]: + response, _ = pop_sized_buf_from_buffer(response, 1) + response, _, app_name_raw = pop_size_prefixed_buf_from_buf(response) + response, _, version_raw = pop_size_prefixed_buf_from_buf(response) + response, _, _ = pop_size_prefixed_buf_from_buf(response) + + assert len(response) == 0 + + return app_name_raw.decode("ascii"), version_raw.decode("ascii") + +# Unpack from response: +# response = pub_key_len (1) +# pub_key (var) +# chain_code_len (1) +# chain_code (var) +def unpack_get_public_key_response(response: bytes) -> Tuple[int, bytes, int, bytes]: + response, pub_key_len, pub_key = pop_size_prefixed_buf_from_buf(response) + response, chain_code_len, chain_code = pop_size_prefixed_buf_from_buf(response) + + assert pub_key_len == 65 + assert chain_code_len == 32 + assert len(response) == 0 + return pub_key_len, pub_key, chain_code_len, chain_code + +# Unpack from response: +# response = der_sig_len (1) +# der_sig (var) +# v (1) +def unpack_sign_tx_response(response: bytes) -> Tuple[int, bytes, int]: + response, der_sig_len, der_sig = pop_size_prefixed_buf_from_buf(response) + response, v = pop_sized_buf_from_buffer(response, 1) + + assert len(response) == 0 + + return der_sig_len, der_sig, int.from_bytes(v, byteorder='big') diff --git a/tests/application_client/boilerplate_transaction.py b/tests/application_client/boilerplate_transaction.py new file mode 100644 index 0000000..02bd01f --- /dev/null +++ b/tests/application_client/boilerplate_transaction.py @@ -0,0 +1,52 @@ +from io import BytesIO +from typing import Union + +from .boilerplate_utils import read, read_uint, read_varint, write_varint, UINT64_MAX + + +class TransactionError(Exception): + pass + + +class Transaction: + def __init__(self, + nonce: int, + to: Union[str, bytes], + value: int, + memo: str, + do_check: bool = True) -> None: + self.nonce: int = nonce + self.to: bytes = bytes.fromhex(to[2:]) if isinstance(to, str) else to + self.value: int = value + self.memo: bytes = memo.encode("ascii") + + if do_check: + if not 0 <= self.nonce <= UINT64_MAX: + raise TransactionError(f"Bad nonce: '{self.nonce}'!") + + if not 0 <= self.value <= UINT64_MAX: + raise TransactionError(f"Bad value: '{self.value}'!") + + if len(self.to) != 20: + raise TransactionError(f"Bad address: '{self.to.hex()}'!") + + def serialize(self) -> bytes: + return b"".join([ + self.nonce.to_bytes(8, byteorder="big"), + self.to, + self.value.to_bytes(8, byteorder="big"), + write_varint(len(self.memo)), + self.memo + ]) + + @classmethod + def from_bytes(cls, hexa: Union[bytes, BytesIO]): + buf: BytesIO = BytesIO(hexa) if isinstance(hexa, bytes) else hexa + + nonce: int = read_uint(buf, 64, byteorder="big") + to: bytes = read(buf, 20) + value: int = read_uint(buf, 64, byteorder="big") + memo_len: int = read_varint(buf) + memo: str = read(buf, memo_len).decode("ascii") + + return cls(nonce=nonce, to=to, value=value, memo=memo) diff --git a/tests/application_client/boilerplate_utils.py b/tests/application_client/boilerplate_utils.py new file mode 100644 index 0000000..fd96e62 --- /dev/null +++ b/tests/application_client/boilerplate_utils.py @@ -0,0 +1,61 @@ +from io import BytesIO +from typing import Optional, Literal + + +UINT64_MAX: int = 2**64-1 +UINT32_MAX: int = 2**32-1 +UINT16_MAX: int = 2**16-1 + + +def write_varint(n: int) -> bytes: + if n < 0xFC: + return n.to_bytes(1, byteorder="little") + + if n <= UINT16_MAX: + return b"\xFD" + n.to_bytes(2, byteorder="little") + + if n <= UINT32_MAX: + return b"\xFE" + n.to_bytes(4, byteorder="little") + + if n <= UINT64_MAX: + return b"\xFF" + n.to_bytes(8, byteorder="little") + + raise ValueError(f"Can't write to varint: '{n}'!") + + +def read_varint(buf: BytesIO, + prefix: Optional[bytes] = None) -> int: + b: bytes = prefix if prefix else buf.read(1) + + if not b: + raise ValueError(f"Can't read prefix: '{b.hex()}'!") + + n: int = {b"\xfd": 2, b"\xfe": 4, b"\xff": 8}.get(b, 1) # default to 1 + + b = buf.read(n) if n > 1 else b + + if len(b) != n: + raise ValueError("Can't read varint!") + + return int.from_bytes(b, byteorder="little") + + +def read(buf: BytesIO, size: int) -> bytes: + b: bytes = buf.read(size) + + if len(b) < size: + raise ValueError(f"Can't read {size} bytes in buffer!") + + return b + + +def read_uint(buf: BytesIO, + bit_len: int, + byteorder: Literal['big', 'little'] = 'little') -> int: + size: int = bit_len // 8 + b: bytes = buf.read(size) + + if len(b) < size: + raise ValueError(f"Can't read u{bit_len} in buffer!") + + return int.from_bytes(b, byteorder) diff --git a/tests/application_client/py.typed b/tests/application_client/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/boil-freeze.txt b/tests/boil-freeze.txt new file mode 100644 index 0000000..a74fc92 --- /dev/null +++ b/tests/boil-freeze.txt @@ -0,0 +1,60 @@ +aniso8601==9.0.1 +asn1crypto==1.5.1 +attrs==22.2.0 +bip-utils==2.7.0 +cbor2==5.4.6 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==3.0.1 +click==8.1.3 +coincurve==17.0.0 +construct==2.10.68 +crcmod==1.7 +cryptography==39.0.1 +ecdsa==0.16.1 +ed25519-blake2b==1.4 +exceptiongroup==1.1.0 +Flask==2.1.2 +Flask-RESTful==0.3.9 +hidapi==0.13.1 +idna==3.4 +importlib-metadata==6.0.0 +importlib-resources==5.12.0 +iniconfig==2.0.0 +intelhex==2.3.0 +itsdangerous==2.1.2 +Jinja2==3.1.2 +jsonschema==4.17.3 +ledgerwallet==0.2.3 +MarkupSafe==2.1.2 +mnemonic==0.20 +packaging==23.0 +Pillow==9.4.0 +pkg_resources==0.0.0 +pkgutil_resolve_name==1.3.10 +pluggy==1.0.0 +protobuf==3.20.3 +py-sr25519-bindings==0.1.4 +pycparser==2.21 +pycryptodome==3.17 +pyelftools==0.29 +PyNaCl==1.5.0 +PyQt5==5.15.9 +PyQt5-Qt5==5.15.2 +PyQt5-sip==12.11.1 +pyrsistent==0.19.3 +pysha3==1.0.2 +pytesseract==0.3.10 +pytest==7.2.1 +pytz==2022.7.1 +ragger==1.6.0 +requests==2.28.2 +semver==2.13.0 +six==1.16.0 +speculos==0.1.224 +tabulate==0.9.0 +toml==0.10.2 +tomli==2.0.1 +urllib3==1.26.14 +Werkzeug==2.2.3 +zipp==3.15.0 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..909ec8b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +from ragger.conftest import configuration + +########################### +### CONFIGURATION START ### +########################### + +# You can configure optional parameters by overriding the value of ragger.configuration.OPTIONAL_CONFIGURATION +# Please refer to ragger/conftest/configuration.py for their descriptions and accepted values + +######################### +### CONFIGURATION END ### +######################### + +# Pull all features from the base ragger conftest using the overridden configuration +pytest_plugins = ("ragger.conftest.base_conftest", ) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..0913153 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +pytest +ragger[speculos,ledgerwallet]>=1.11.4 +ecdsa>=0.16.1,<0.17.0 +pysha3>=1.0.0,<2.0.0 diff --git a/tests/setup.cfg b/tests/setup.cfg new file mode 100644 index 0000000..7d0d7e3 --- /dev/null +++ b/tests/setup.cfg @@ -0,0 +1,21 @@ +[tool:pytest] +addopts = --strict-markers + +[pylint] +disable = C0114, # missing-module-docstring + C0115, # missing-class-docstring + C0116, # missing-function-docstring + C0103, # invalid-name + R0801, # duplicate-code + R0913 # too-many-arguments +max-line-length=100 +extension-pkg-whitelist=hid + +[pycodestyle] +max-line-length = 100 + +[mypy-hid.*] +ignore_missing_imports = True + +[mypy-pytest.*] +ignore_missing_imports = True diff --git a/tests/snapshots/nanos/test_app_mainmenu/00000.png b/tests/snapshots/nanos/test_app_mainmenu/00000.png new file mode 100644 index 0000000..68ffb39 Binary files /dev/null and b/tests/snapshots/nanos/test_app_mainmenu/00000.png differ diff --git a/tests/snapshots/nanos/test_app_mainmenu/00001.png b/tests/snapshots/nanos/test_app_mainmenu/00001.png new file mode 100644 index 0000000..23b5eb1 Binary files /dev/null and b/tests/snapshots/nanos/test_app_mainmenu/00001.png differ diff --git a/tests/snapshots/nanos/test_app_mainmenu/00002.png b/tests/snapshots/nanos/test_app_mainmenu/00002.png new file mode 100644 index 0000000..3476b97 Binary files /dev/null and b/tests/snapshots/nanos/test_app_mainmenu/00002.png differ diff --git a/tests/snapshots/nanos/test_app_mainmenu/00003.png b/tests/snapshots/nanos/test_app_mainmenu/00003.png new file mode 100644 index 0000000..e227980 Binary files /dev/null and b/tests/snapshots/nanos/test_app_mainmenu/00003.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00000.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00000.png new file mode 100644 index 0000000..4eea762 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00000.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00001.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00001.png new file mode 100644 index 0000000..2d93906 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00001.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00002.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00002.png new file mode 100644 index 0000000..fbd41ff Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00002.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00003.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00003.png new file mode 100644 index 0000000..700668e Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00003.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00004.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00004.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00004.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00005.png b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00005.png new file mode 100644 index 0000000..68ffb39 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_accepted/00005.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00000.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00000.png new file mode 100644 index 0000000..4eea762 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00000.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00001.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00001.png new file mode 100644 index 0000000..2d93906 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00001.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00002.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00002.png new file mode 100644 index 0000000..fbd41ff Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00002.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00003.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00003.png new file mode 100644 index 0000000..700668e Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00003.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00004.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00004.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00004.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00005.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00005.png new file mode 100644 index 0000000..9c7e704 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00005.png differ diff --git a/tests/snapshots/nanos/test_get_public_key_confirm_refused/00006.png b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00006.png new file mode 100644 index 0000000..68ffb39 Binary files /dev/null and b/tests/snapshots/nanos/test_get_public_key_confirm_refused/00006.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00000.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00000.png new file mode 100644 index 0000000..e082b3c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00000.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00001.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00001.png new file mode 100644 index 0000000..5472cd1 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00001.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00002.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00002.png new file mode 100644 index 0000000..d2f7029 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00002.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00003.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00003.png new file mode 100644 index 0000000..9f97b45 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00004.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00004.png new file mode 100644 index 0000000..85692e8 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00004.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00005.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00005.png new file mode 100644 index 0000000..8977dab Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00005.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00006.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00006.png new file mode 100644 index 0000000..5ad5b7f Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00006.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00007.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00007.png new file mode 100644 index 0000000..9d783de Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00007.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00008.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00008.png new file mode 100644 index 0000000..138ddd9 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00008.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00009.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00009.png new file mode 100644 index 0000000..fdd5105 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00009.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00010.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00010.png new file mode 100644 index 0000000..51d286f Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00010.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00011.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00011.png new file mode 100644 index 0000000..eac7271 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00011.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00012.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00012.png new file mode 100644 index 0000000..3408005 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00012.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00013.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00013.png new file mode 100644 index 0000000..156402b Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00013.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00014.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00014.png new file mode 100644 index 0000000..ab852b6 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00014.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00015.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00015.png new file mode 100644 index 0000000..cc72967 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00015.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00016.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00016.png new file mode 100644 index 0000000..f15bbf7 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00016.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00017.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00017.png new file mode 100644 index 0000000..b511ceb Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00017.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00018.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00018.png new file mode 100644 index 0000000..3a87ff8 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00018.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00019.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00019.png new file mode 100644 index 0000000..2ca2342 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00019.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00020.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00020.png new file mode 100644 index 0000000..2a41e2c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00020.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00021.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00021.png new file mode 100644 index 0000000..ced1ab1 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00021.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00022.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00022.png new file mode 100644 index 0000000..9a47bb5 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00022.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00023.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00023.png new file mode 100644 index 0000000..6b8f5b7 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00023.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00024.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00024.png new file mode 100644 index 0000000..2470aba Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00024.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00025.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00025.png new file mode 100644 index 0000000..c7c4212 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00025.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00026.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00026.png new file mode 100644 index 0000000..f6d2b5d Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00026.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00027.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00027.png new file mode 100644 index 0000000..fd572e2 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00027.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00028.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00028.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00028.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_long_tx/00029.png b/tests/snapshots/nanos/test_sign_tx_long_tx/00029.png new file mode 100644 index 0000000..68ffb39 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_long_tx/00029.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00000.png b/tests/snapshots/nanos/test_sign_tx_refused/00000.png new file mode 100644 index 0000000..e082b3c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00000.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00001.png b/tests/snapshots/nanos/test_sign_tx_refused/00001.png new file mode 100644 index 0000000..5472cd1 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00001.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00002.png b/tests/snapshots/nanos/test_sign_tx_refused/00002.png new file mode 100644 index 0000000..d2f7029 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00002.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00003.png b/tests/snapshots/nanos/test_sign_tx_refused/00003.png new file mode 100644 index 0000000..9f97b45 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00004.png b/tests/snapshots/nanos/test_sign_tx_refused/00004.png new file mode 100644 index 0000000..85692e8 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00004.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00005.png b/tests/snapshots/nanos/test_sign_tx_refused/00005.png new file mode 100644 index 0000000..93ac15b Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00005.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00006.png b/tests/snapshots/nanos/test_sign_tx_refused/00006.png new file mode 100644 index 0000000..c88c94e Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00006.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00007.png b/tests/snapshots/nanos/test_sign_tx_refused/00007.png new file mode 100644 index 0000000..28d4952 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00007.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00008.png b/tests/snapshots/nanos/test_sign_tx_refused/00008.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00008.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00009.png b/tests/snapshots/nanos/test_sign_tx_refused/00009.png new file mode 100644 index 0000000..9c7e704 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00009.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_refused/00010.png b/tests/snapshots/nanos/test_sign_tx_refused/00010.png new file mode 100644 index 0000000..68ffb39 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_refused/00010.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_short_tx/00000.png b/tests/snapshots/nanos/test_sign_tx_short_tx/00000.png new file mode 100644 index 0000000..e082b3c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_short_tx/00000.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_short_tx/00001.png b/tests/snapshots/nanos/test_sign_tx_short_tx/00001.png new file mode 100644 index 0000000..5472cd1 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_short_tx/00001.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_short_tx/00002.png b/tests/snapshots/nanos/test_sign_tx_short_tx/00002.png new file mode 100644 index 0000000..d2f7029 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_short_tx/00002.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_short_tx/00003.png b/tests/snapshots/nanos/test_sign_tx_short_tx/00003.png new file mode 100644 index 0000000..9f97b45 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_short_tx/00003.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_short_tx/00004.png b/tests/snapshots/nanos/test_sign_tx_short_tx/00004.png new file mode 100644 index 0000000..85692e8 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_short_tx/00004.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_short_tx/00005.png b/tests/snapshots/nanos/test_sign_tx_short_tx/00005.png new file mode 100644 index 0000000..975573e Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_short_tx/00005.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_short_tx/00006.png b/tests/snapshots/nanos/test_sign_tx_short_tx/00006.png new file mode 100644 index 0000000..66c411c Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_short_tx/00006.png differ diff --git a/tests/snapshots/nanos/test_sign_tx_short_tx/00007.png b/tests/snapshots/nanos/test_sign_tx_short_tx/00007.png new file mode 100644 index 0000000..68ffb39 Binary files /dev/null and b/tests/snapshots/nanos/test_sign_tx_short_tx/00007.png differ diff --git a/tests/snapshots/nanosp/test_app_mainmenu/00000.png b/tests/snapshots/nanosp/test_app_mainmenu/00000.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanosp/test_app_mainmenu/00000.png differ diff --git a/tests/snapshots/nanosp/test_app_mainmenu/00001.png b/tests/snapshots/nanosp/test_app_mainmenu/00001.png new file mode 100644 index 0000000..5f85a2c Binary files /dev/null and b/tests/snapshots/nanosp/test_app_mainmenu/00001.png differ diff --git a/tests/snapshots/nanosp/test_app_mainmenu/00002.png b/tests/snapshots/nanosp/test_app_mainmenu/00002.png new file mode 100644 index 0000000..7e1a28c Binary files /dev/null and b/tests/snapshots/nanosp/test_app_mainmenu/00002.png differ diff --git a/tests/snapshots/nanosp/test_app_mainmenu/00003.png b/tests/snapshots/nanosp/test_app_mainmenu/00003.png new file mode 100644 index 0000000..ca20b18 Binary files /dev/null and b/tests/snapshots/nanosp/test_app_mainmenu/00003.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00000.png b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00000.png new file mode 100644 index 0000000..5e8d72b Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00000.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00001.png b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00001.png new file mode 100644 index 0000000..cd4a07c Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00001.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00002.png b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00002.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00002.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00003.png b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00003.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_accepted/00003.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00000.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00000.png new file mode 100644 index 0000000..5e8d72b Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00000.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00001.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00001.png new file mode 100644 index 0000000..cd4a07c Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00001.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00002.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00002.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00002.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00003.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00003.png new file mode 100644 index 0000000..c922246 Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00003.png differ diff --git a/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00004.png b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00004.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanosp/test_get_public_key_confirm_refused/00004.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00000.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00000.png new file mode 100644 index 0000000..51c7deb Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00000.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00001.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00001.png new file mode 100644 index 0000000..5e82af5 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00001.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00002.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00002.png new file mode 100644 index 0000000..c42e549 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00002.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00003.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00003.png new file mode 100644 index 0000000..6f31bc8 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00004.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00004.png new file mode 100644 index 0000000..5d9244e Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00004.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00005.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00005.png new file mode 100644 index 0000000..62e3af3 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00005.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00006.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00006.png new file mode 100644 index 0000000..5f524c2 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00006.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00007.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00007.png new file mode 100644 index 0000000..f9b82a3 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00007.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00008.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00008.png new file mode 100644 index 0000000..8d095ce Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00008.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00009.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00009.png new file mode 100644 index 0000000..46f04c2 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00009.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00010.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00010.png new file mode 100644 index 0000000..9f73d2b Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00010.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00011.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00011.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00011.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_long_tx/00012.png b/tests/snapshots/nanosp/test_sign_tx_long_tx/00012.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_long_tx/00012.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00000.png b/tests/snapshots/nanosp/test_sign_tx_refused/00000.png new file mode 100644 index 0000000..51c7deb Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00000.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00001.png b/tests/snapshots/nanosp/test_sign_tx_refused/00001.png new file mode 100644 index 0000000..5e82af5 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00001.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00002.png b/tests/snapshots/nanosp/test_sign_tx_refused/00002.png new file mode 100644 index 0000000..c42e549 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00002.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00003.png b/tests/snapshots/nanosp/test_sign_tx_refused/00003.png new file mode 100644 index 0000000..8a821ad Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00004.png b/tests/snapshots/nanosp/test_sign_tx_refused/00004.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00004.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00005.png b/tests/snapshots/nanosp/test_sign_tx_refused/00005.png new file mode 100644 index 0000000..c922246 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00005.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_refused/00006.png b/tests/snapshots/nanosp/test_sign_tx_refused/00006.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_refused/00006.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_short_tx/00000.png b/tests/snapshots/nanosp/test_sign_tx_short_tx/00000.png new file mode 100644 index 0000000..51c7deb Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_short_tx/00000.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_short_tx/00001.png b/tests/snapshots/nanosp/test_sign_tx_short_tx/00001.png new file mode 100644 index 0000000..5e82af5 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_short_tx/00001.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_short_tx/00002.png b/tests/snapshots/nanosp/test_sign_tx_short_tx/00002.png new file mode 100644 index 0000000..c42e549 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_short_tx/00002.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_short_tx/00003.png b/tests/snapshots/nanosp/test_sign_tx_short_tx/00003.png new file mode 100644 index 0000000..2a94e16 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_short_tx/00003.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_short_tx/00004.png b/tests/snapshots/nanosp/test_sign_tx_short_tx/00004.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_short_tx/00004.png differ diff --git a/tests/snapshots/nanosp/test_sign_tx_short_tx/00005.png b/tests/snapshots/nanosp/test_sign_tx_short_tx/00005.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanosp/test_sign_tx_short_tx/00005.png differ diff --git a/tests/snapshots/nanox/test_app_mainmenu/00000.png b/tests/snapshots/nanox/test_app_mainmenu/00000.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanox/test_app_mainmenu/00000.png differ diff --git a/tests/snapshots/nanox/test_app_mainmenu/00001.png b/tests/snapshots/nanox/test_app_mainmenu/00001.png new file mode 100644 index 0000000..5f85a2c Binary files /dev/null and b/tests/snapshots/nanox/test_app_mainmenu/00001.png differ diff --git a/tests/snapshots/nanox/test_app_mainmenu/00002.png b/tests/snapshots/nanox/test_app_mainmenu/00002.png new file mode 100644 index 0000000..7e1a28c Binary files /dev/null and b/tests/snapshots/nanox/test_app_mainmenu/00002.png differ diff --git a/tests/snapshots/nanox/test_app_mainmenu/00003.png b/tests/snapshots/nanox/test_app_mainmenu/00003.png new file mode 100644 index 0000000..ca20b18 Binary files /dev/null and b/tests/snapshots/nanox/test_app_mainmenu/00003.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00000.png b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00000.png new file mode 100644 index 0000000..5e8d72b Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00000.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00001.png b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00001.png new file mode 100644 index 0000000..cd4a07c Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00001.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00002.png b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00002.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00002.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00003.png b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00003.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_accepted/00003.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00000.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00000.png new file mode 100644 index 0000000..5e8d72b Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00000.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00001.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00001.png new file mode 100644 index 0000000..cd4a07c Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00001.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00002.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00002.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00002.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00003.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00003.png new file mode 100644 index 0000000..c922246 Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00003.png differ diff --git a/tests/snapshots/nanox/test_get_public_key_confirm_refused/00004.png b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00004.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanox/test_get_public_key_confirm_refused/00004.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00000.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00000.png new file mode 100644 index 0000000..51c7deb Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00000.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00001.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00001.png new file mode 100644 index 0000000..5e82af5 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00001.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00002.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00002.png new file mode 100644 index 0000000..c42e549 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00002.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00003.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00003.png new file mode 100644 index 0000000..6f31bc8 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00004.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00004.png new file mode 100644 index 0000000..5d9244e Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00004.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00005.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00005.png new file mode 100644 index 0000000..62e3af3 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00005.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00006.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00006.png new file mode 100644 index 0000000..5f524c2 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00006.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00007.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00007.png new file mode 100644 index 0000000..f9b82a3 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00007.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00008.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00008.png new file mode 100644 index 0000000..8d095ce Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00008.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00009.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00009.png new file mode 100644 index 0000000..46f04c2 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00009.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00010.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00010.png new file mode 100644 index 0000000..9f73d2b Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00010.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00011.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00011.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00011.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_long_tx/00012.png b/tests/snapshots/nanox/test_sign_tx_long_tx/00012.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_long_tx/00012.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00000.png b/tests/snapshots/nanox/test_sign_tx_refused/00000.png new file mode 100644 index 0000000..51c7deb Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00000.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00001.png b/tests/snapshots/nanox/test_sign_tx_refused/00001.png new file mode 100644 index 0000000..5e82af5 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00001.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00002.png b/tests/snapshots/nanox/test_sign_tx_refused/00002.png new file mode 100644 index 0000000..c42e549 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00002.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00003.png b/tests/snapshots/nanox/test_sign_tx_refused/00003.png new file mode 100644 index 0000000..8a821ad Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00004.png b/tests/snapshots/nanox/test_sign_tx_refused/00004.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00004.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00005.png b/tests/snapshots/nanox/test_sign_tx_refused/00005.png new file mode 100644 index 0000000..c922246 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00005.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_refused/00006.png b/tests/snapshots/nanox/test_sign_tx_refused/00006.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_refused/00006.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_short_tx/00000.png b/tests/snapshots/nanox/test_sign_tx_short_tx/00000.png new file mode 100644 index 0000000..51c7deb Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_short_tx/00000.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_short_tx/00001.png b/tests/snapshots/nanox/test_sign_tx_short_tx/00001.png new file mode 100644 index 0000000..5e82af5 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_short_tx/00001.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_short_tx/00002.png b/tests/snapshots/nanox/test_sign_tx_short_tx/00002.png new file mode 100644 index 0000000..c42e549 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_short_tx/00002.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_short_tx/00003.png b/tests/snapshots/nanox/test_sign_tx_short_tx/00003.png new file mode 100644 index 0000000..2a94e16 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_short_tx/00003.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_short_tx/00004.png b/tests/snapshots/nanox/test_sign_tx_short_tx/00004.png new file mode 100644 index 0000000..53ae651 Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_short_tx/00004.png differ diff --git a/tests/snapshots/nanox/test_sign_tx_short_tx/00005.png b/tests/snapshots/nanox/test_sign_tx_short_tx/00005.png new file mode 100644 index 0000000..03d57ea Binary files /dev/null and b/tests/snapshots/nanox/test_sign_tx_short_tx/00005.png differ diff --git a/tests/test_app_mainmenu.py b/tests/test_app_mainmenu.py new file mode 100644 index 0000000..de7f3ce --- /dev/null +++ b/tests/test_app_mainmenu.py @@ -0,0 +1,21 @@ +from ragger.navigator import NavInsID + +from utils import ROOT_SCREENSHOT_PATH + + +# In this test we check the behavior of the device main menu +def test_app_mainmenu(firmware, navigator, test_name): + # Navigate in the main menu + if firmware.device.startswith("nano"): + instructions = [ + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK, + NavInsID.RIGHT_CLICK + ] + else: + instructions = [ + NavInsID.USE_CASE_HOME_INFO, + NavInsID.USE_CASE_SETTINGS_SINGLE_PAGE_EXIT + ] + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, test_name, instructions, + screen_change_before_first_instruction=False) diff --git a/tests/test_appname_cmd.py b/tests/test_appname_cmd.py new file mode 100644 index 0000000..dd6446b --- /dev/null +++ b/tests/test_appname_cmd.py @@ -0,0 +1,12 @@ +from application_client.boilerplate_command_sender import BoilerplateCommandSender +from application_client.boilerplate_response_unpacker import unpack_get_app_name_response + + +# In this test we check that the GET_APP_NAME replies the application name +def test_app_name(backend): + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # Send the GET_APP_NAME instruction to the app + response = client.get_app_name() + # Assert that we have received the correct appname + assert unpack_get_app_name_response(response.data) == "app-boilerplate-rust" diff --git a/tests/test_error_cmd.py b/tests/test_error_cmd.py new file mode 100644 index 0000000..277f2f8 --- /dev/null +++ b/tests/test_error_cmd.py @@ -0,0 +1,56 @@ +import pytest + +from ragger.error import ExceptionRAPDU +from application_client.boilerplate_command_sender import CLA, InsType, P1, P2, Errors + + +# Ensure the app returns an error when a bad CLA is used +def test_bad_cla(backend): + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA + 1, ins=InsType.GET_VERSION) + assert e.value.status == Errors.SW_CLA_NOT_SUPPORTED + + +# Ensure the app returns an error when a bad INS is used +def test_bad_ins(backend): + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=0xff) + assert e.value.status == Errors.SW_INS_NOT_SUPPORTED + + +# Ensure the app returns an error when a bad P1 or P2 is used +def test_wrong_p1p2(backend): + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START + 1, p2=P2.P2_LAST) + assert e.value.status == Errors.SW_WRONG_P1P2 + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=InsType.GET_VERSION, p1=P1.P1_START, p2=P2.P2_MORE) + assert e.value.status == Errors.SW_WRONG_P1P2 + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=InsType.GET_APP_NAME, p1=P1.P1_START + 1, p2=P2.P2_LAST) + assert e.value.status == Errors.SW_WRONG_P1P2 + with pytest.raises(ExceptionRAPDU) as e: + backend.exchange(cla=CLA, ins=InsType.GET_APP_NAME, p1=P1.P1_START, p2=P2.P2_MORE) + assert e.value.status == Errors.SW_WRONG_P1P2 + +# Ensure the app returns an error when a bad data length is used +# def test_wrong_data_length(backend): +# # APDUs must be at least 5 bytes: CLA, INS, P1, P2, Lc. +# with pytest.raises(ExceptionRAPDU) as e: +# backend.exchange_raw(b"E0030000") +# assert e.value.status == Errors.SW_WRONG_DATA_LENGTH +# # APDUs advertises a too long length +# with pytest.raises(ExceptionRAPDU) as e: +# backend.exchange_raw(b"E003000005") +# assert e.value.status == Errors.SW_WRONG_DATA_LENGTH + + +# Ensure there is no state confusion when trying wrong APDU sequences +# def test_invalid_state(backend): +# with pytest.raises(ExceptionRAPDU) as e: +# backend.exchange(cla=CLA, +# ins=InsType.SIGN_TX, +# p1=P1.P1_START + 1, # Try to continue a flow instead of start a new one +# p2=P2.P2_MORE, +# data=b"abcde") # data is not parsed in this case +# assert e.value.status == Errors.SW_BAD_STATE diff --git a/tests/test_name_version.py b/tests/test_name_version.py new file mode 100644 index 0000000..5248663 --- /dev/null +++ b/tests/test_name_version.py @@ -0,0 +1,15 @@ +# from application_client.boilerplate_command_sender import BoilerplateCommandSender +# from application_client.boilerplate_response_unpacker import unpack_get_app_and_version_response + + +# # Test a specific APDU asking BOLOS (and not the app) the name and version of the current app +# def test_get_app_and_version(backend, backend_name): +# # Use the app interface instead of raw interface +# client = BoilerplateCommandSender(backend) +# # Send the special instruction to BOLOS +# response = client.get_app_and_version() +# # Use an helper to parse the response, assert the values +# app_name, version = unpack_get_app_and_version_response(response.data) + +# assert app_name == "app-boilerplate-rust" +# assert version == "1.0.0" diff --git a/tests/test_pubkey_cmd.py b/tests/test_pubkey_cmd.py new file mode 100644 index 0000000..f295411 --- /dev/null +++ b/tests/test_pubkey_cmd.py @@ -0,0 +1,87 @@ +import pytest + +from application_client.boilerplate_command_sender import BoilerplateCommandSender, Errors +from application_client.boilerplate_response_unpacker import unpack_get_public_key_response +from ragger.bip import calculate_public_key_and_chaincode, CurveChoice +from ragger.error import ExceptionRAPDU +from ragger.navigator import NavInsID, NavIns +from utils import ROOT_SCREENSHOT_PATH + + +# In this test we check that the GET_PUBLIC_KEY works in non-confirmation mode +def test_get_public_key_no_confirm(backend): + for path in ["m/44'/1'/0'/0/0", "m/44'/1'/0/0/0", "m/44'/1'/911'/0/0", "m/44'/1'/255/255/255", "m/44'/1'/2147483647/0/0/0/0/0/0/0"]: + client = BoilerplateCommandSender(backend) + response = client.get_public_key(path=path).data + _, public_key, _, _ = unpack_get_public_key_response(response) + + ref_public_key, _ = calculate_public_key_and_chaincode(CurveChoice.Secp256k1, path=path) + assert public_key.hex() == ref_public_key + + +# In this test we check that the GET_PUBLIC_KEY works in confirmation mode +def test_get_public_key_confirm_accepted(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + path = "m/44'/1'/0'/0/0" + with client.get_public_key_with_confirmation(path=path): + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name) + else: + instructions = [ + NavInsID.USE_CASE_REVIEW_TAP, + NavIns(NavInsID.TOUCH, (200, 335)), + NavInsID.USE_CASE_ADDRESS_CONFIRMATION_EXIT_QR, + NavInsID.USE_CASE_ADDRESS_CONFIRMATION_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS + ] + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, + test_name, + instructions) + response = client.get_async_response().data + _, public_key, _, _ = unpack_get_public_key_response(response) + + ref_public_key, _ = calculate_public_key_and_chaincode(CurveChoice.Secp256k1, path=path) + assert public_key.hex() == ref_public_key + + +# In this test we check that the GET_PUBLIC_KEY in confirmation mode replies an error if the user refuses +def test_get_public_key_confirm_refused(firmware, backend, navigator, test_name): + client = BoilerplateCommandSender(backend) + path = "m/44'/1'/0'/0/0" + + if firmware.device.startswith("nano"): + with pytest.raises(ExceptionRAPDU) as e: + with client.get_public_key_with_confirmation(path=path): + navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Reject", + ROOT_SCREENSHOT_PATH, + test_name) + # Assert that we have received a refusal + assert e.value.status == Errors.SW_DENY + assert len(e.value.data) == 0 + else: + instructions_set = [ + [ + NavInsID.USE_CASE_REVIEW_REJECT, + NavInsID.USE_CASE_STATUS_DISMISS + ], + [ + NavInsID.USE_CASE_REVIEW_TAP, + NavInsID.USE_CASE_ADDRESS_CONFIRMATION_CANCEL, + NavInsID.USE_CASE_STATUS_DISMISS + ] + ] + for i, instructions in enumerate(instructions_set): + with pytest.raises(ExceptionRAPDU) as e: + with client.get_public_key_with_confirmation(path=path): + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, + test_name + f"/part{i}", + instructions) + # Assert that we have received a refusal + assert e.value.status == Errors.SW_DENY + assert len(e.value.data) == 0 diff --git a/tests/test_sign_cmd.py b/tests/test_sign_cmd.py new file mode 100644 index 0000000..8e64813 --- /dev/null +++ b/tests/test_sign_cmd.py @@ -0,0 +1,142 @@ +import pytest + +from application_client.boilerplate_transaction import Transaction +from application_client.boilerplate_command_sender import BoilerplateCommandSender, Errors +from application_client.boilerplate_response_unpacker import unpack_get_public_key_response, unpack_sign_tx_response +from ragger.error import ExceptionRAPDU +from ragger.navigator import NavInsID +from utils import ROOT_SCREENSHOT_PATH, check_signature_validity + +# In this tests we check the behavior of the device when asked to sign a transaction + + +# In this test se send to the device a transaction to sign and validate it on screen +# The transaction is short and will be sent in one chunk +# We will ensure that the displayed information is correct by using screenshots comparison +def test_sign_tx_short_tx(firmware, backend, navigator, test_name): + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # The path used for this entire test + path: str = "m/44'/1'/0'/0/0" + + # First we need to get the public key of the device in order to build the transaction + rapdu = client.get_public_key(path=path) + _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + + # Create the transaction that will be sent to the device for signing + transaction = Transaction( + nonce=1, + to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", + value=666, + memo="For u EthDev" + ).serialize() + + # Send the sign device instruction. + # As it requires on-screen validation, the function is asynchronous. + # It will yield the result when the navigation is done + with client.sign_tx(path=path, transaction=transaction): + # Validate the on-screen request by performing the navigation appropriate for this device + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name) + else: + navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_TAP, + [NavInsID.USE_CASE_REVIEW_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name) + + # The device as yielded the result, parse it and ensure that the signature is correct + response = client.get_async_response().data + _, der_sig, _ = unpack_sign_tx_response(response) + + assert check_signature_validity(public_key, der_sig, transaction) + + +# In this test se send to the device a transaction to sign and validate it on screen +# This test is mostly the same as the previous one but with different values. +# In particular the long memo will force the transaction to be sent in multiple chunks +def test_sign_tx_long_tx(firmware, backend, navigator, test_name): + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + path: str = "m/44'/1'/0'/0/0" + + rapdu = client.get_public_key(path=path) + _, public_key, _, _ = unpack_get_public_key_response(rapdu.data) + + transaction = Transaction( + nonce=1, + to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", + value=666, + memo=("This is a very long memo. " + "It will force the app client to send the serialized transaction to be sent in chunk. " + "As the maximum chunk size is 255 bytes we will make this memo greater than 255 characters. " + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam.") + ).serialize() + + with client.sign_tx(path=path, transaction=transaction): + if firmware.device.startswith("nano"): + navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Approve", + ROOT_SCREENSHOT_PATH, + test_name) + else: + navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_TAP, + [NavInsID.USE_CASE_REVIEW_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + test_name) + response = client.get_async_response().data + _, der_sig, _ = unpack_sign_tx_response(response) + assert check_signature_validity(public_key, der_sig, transaction) + + +# Transaction signature refused test +# The test will ask for a transaction signature that will be refused on screen +def test_sign_tx_refused(firmware, backend, navigator, test_name): + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + path: str = "m/44'/1'/0'/0/0" + + rapdu = client.get_public_key(path=path) + _, pub_key, _, _ = unpack_get_public_key_response(rapdu.data) + + transaction = Transaction( + nonce=1, + to="0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", + value=666, + memo="This transaction will be refused by the user" + ).serialize() + + if firmware.device.startswith("nano"): + with pytest.raises(ExceptionRAPDU) as e: + with client.sign_tx(path=path, transaction=transaction): + navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK, + [NavInsID.BOTH_CLICK], + "Reject", + ROOT_SCREENSHOT_PATH, + test_name) + + # Assert that we have received a refusal + assert e.value.status == Errors.SW_DENY + assert len(e.value.data) == 0 + else: + for i in range(3): + instructions = [NavInsID.USE_CASE_REVIEW_TAP] * i + instructions += [NavInsID.USE_CASE_REVIEW_REJECT, + NavInsID.USE_CASE_CHOICE_CONFIRM, + NavInsID.USE_CASE_STATUS_DISMISS] + with pytest.raises(ExceptionRAPDU) as e: + with client.sign_tx(path=path, transaction=transaction): + navigator.navigate_and_compare(ROOT_SCREENSHOT_PATH, + test_name + f"/part{i}", + instructions) + # Assert that we have received a refusal + assert e.value.status == Errors.SW_DENY + assert len(e.value.data) == 0 diff --git a/tests/test_version_cmd.py b/tests/test_version_cmd.py new file mode 100644 index 0000000..cc9e4a0 --- /dev/null +++ b/tests/test_version_cmd.py @@ -0,0 +1,16 @@ +from application_client.boilerplate_command_sender import BoilerplateCommandSender +from application_client.boilerplate_response_unpacker import unpack_get_version_response + +# Taken from the Cargo.toml, to update every time the version is bumped +MAJOR = 1 +MINOR = 0 +PATCH = 0 + +# In this test we check the behavior of the device when asked to provide the app version +def test_version(backend): + # Use the app interface instead of raw interface + client = BoilerplateCommandSender(backend) + # Send the GET_VERSION instruction + rapdu = client.get_version() + # Use an helper to parse the response, assert the values + assert unpack_get_version_response(rapdu.data) == (MAJOR, MINOR, PATCH) diff --git a/tests/usage.md b/tests/usage.md new file mode 100644 index 0000000..be8890f --- /dev/null +++ b/tests/usage.md @@ -0,0 +1,74 @@ +# How to use the Ragger test framework + +This framework allows testing the application on the Speculos emulator or on a real device using LedgerComm or LedgerWallet + + +## Quickly get started with Ragger and Speculos + +### Install ragger and dependencies + +``` +pip install --extra-index-url https://test.pypi.org/simple/ -r requirements.txt +sudo apt-get update && sudo apt-get install qemu-user-static +``` + +### Compile the application + +The application to test must be compiled for all required devices. +You can use for this the container `ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite`: +``` +docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest +cd # replace with the name of your app, (eg boilerplate) +docker run --user "$(id -u)":"$(id -g)" --rm -ti -v "$(realpath .):/app" --privileged -v "/dev/bus/usb:/dev/bus/usb" ledger-app-builder-lite:latest +make clean && make BOLOS_SDK=$_SDK # replace with one of [NANOS, NANOX, NANOSP, STAX] +exit +``` + +### Run a simple test using the Speculos emulator + +You can use the following command to get your first experience with Ragger and Speculos +``` +pytest -v --tb=short --device nanox --display +``` +Or you can refer to the section `Available pytest options` to configure the options you want to use + + +### Run a simple test using a real device + +The application to test must be loaded and started on a Ledger device plugged in USB. +You can use for this the container `ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite`: +``` +docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest +cd app-/ # replace with the name of your app, (eg boilerplate) +docker run --user "$(id -u)":"$(id -g)" --rm -ti -v "$(realpath .):/app" --privileged -v "/dev/bus/usb:/dev/bus/usb" ledger-app-builder-lite:latest +make clean && make BOLOS_SDK=$_SDK load # replace with one of [NANOS, NANOX, NANOSP, STAX] +exit +``` + +You can use the following command to get your first experience with Ragger and Ledgerwallet on a NANOX. +Make sure that the device is plugged, unlocked, and that the tested application is open. +``` +pytest -v --tb=short --device nanox --backend ledgerwallet +``` +Or you can refer to the section `Available pytest options` to configure the options you want to use + + +## Available pytest options + +Standard useful pytest options +``` + -v formats the test summary in a readable way + -s enable logs for successful tests, on Speculos it will enable app logs if compiled with DEBUG=1 + -k only run the tests that contain in their names + --tb=short in case of errors, formats the test traceback in a readable way +``` + +Custom pytest options +``` + --device run the test on the specified device [nanos,nanox,nanosp,stax,all]. This parameter is mandatory + --backend run the tests against the backend [speculos, ledgercomm, ledgerwallet]. Speculos is the default + --display on Speculos, enables the display of the app screen using QT + --golden_run on Speculos, screen comparison functions will save the current screen instead of comparing + --log_apdu_file log all apdu exchanges to the file in parameter. The previous file content is erased +``` + diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..cb52233 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,23 @@ +from pathlib import Path +from hashlib import sha256 +from sha3 import keccak_256 + +from ecdsa.curves import SECP256k1 +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_der + + +ROOT_SCREENSHOT_PATH = Path(__file__).parent.resolve() + + +# Check if a signature of a given message is valid +def check_signature_validity(public_key: bytes, signature: bytes, message: bytes) -> bool: + pk: VerifyingKey = VerifyingKey.from_string( + public_key, + curve=SECP256k1, + hashfunc=sha256 + ) + return pk.verify(signature=signature, + data=message, + hashfunc=keccak_256, + sigdecode=sigdecode_der)