From c44708ef621cd0c6f032a065142fc35571def840 Mon Sep 17 00:00:00 2001 From: ftheirs Date: Mon, 12 Feb 2024 17:49:44 -0300 Subject: [PATCH] Initial commit add Jubjub add Blake2 and update deps keys generation cpp tests compute public address update JS package compute sapling keys use local blake2s fix Rust compilation flags update APDU spec add publish JS package script disable Ledger CI --- .clang-format | 5 + .clang-tidy | 22 + .editorconfig | 16 + .github/workflows/check_version.yml | 53 +++ .github/workflows/guidelines_enforcer.yml | 23 + .github/workflows/lint.yml | 43 ++ .github/workflows/main.yml | 165 ++++++++ .github/workflows/publish.yaml | 54 +++ .gitignore | 80 ++++ .gitmodules | 21 + CMakeLists.txt | 231 ++++++++++ LICENSE | 201 +++++++++ Makefile | 42 ++ README.md | 203 +++++++++ app/.gitignore | 51 +++ app/LICENSE | 201 +++++++++ app/Makefile | 104 +++++ app/Makefile.version | 6 + app/glyphs/icon_app.gif | Bin 0 -> 107 bytes app/glyphs/icon_stax_32.gif | Bin 0 -> 167 bytes app/glyphs/icon_stax_64.gif | Bin 0 -> 313 bytes app/nanos_icon.gif | Bin 0 -> 107 bytes app/nanox_icon.gif | Bin 0 -> 104 bytes app/output/.gitkeep | 0 app/pkg/.gitkeep | 0 app/rust/.cargo/config | 13 + app/rust/.gitignore | 1 + app/rust/Cargo.lock | 114 +++++ app/rust/Cargo.toml | 35 ++ app/rust/include/rslib.h | 9 + app/rust/src/constants.rs | 65 +++ app/rust/src/lib.rs | 103 +++++ app/script_s2.ld | 170 ++++++++ app/script_x.ld | 175 ++++++++ app/src/addr.c | 70 ++++ app/src/addr.h | 32 ++ app/src/apdu_handler.c | 223 ++++++++++ app/src/blake2s/blake2-impl.h | 160 +++++++ app/src/blake2s/blake2.h | 128 ++++++ app/src/blake2s/blake2s-ref.c | 395 ++++++++++++++++++ app/src/coin.h | 54 +++ app/src/common/actions.c | 19 + app/src/common/actions.h | 72 ++++ app/src/common/main.c | 38 ++ app/src/common/parser.h | 43 ++ app/src/common/parser_common.h | 69 +++ app/src/common/tx.c | 141 +++++++ app/src/common/tx.h | 52 +++ app/src/crypto.c | 172 ++++++++ app/src/crypto.h | 37 ++ app/src/crypto_helper.c | 87 ++++ app/src/crypto_helper.h | 46 ++ app/src/keys_def.h | 69 +++ app/src/keys_personalizations.h | 39 ++ app/src/parser.c | 122 ++++++ app/src/parser_impl.c | 59 +++ app/src/parser_impl.h | 33 ++ app/src/parser_txdef.h | 33 ++ app/stax_icon.gif | Bin 0 -> 167 bytes cmake/cmake-modules | 1 + cmake/conan/CMakeLists.txt | 12 + cmake/gtest/CMakeLists.txt | 32 ++ cmake/gtest/CMakeLists.txt.gtest.in | 16 + conanfile.txt | 6 + deps/blake2 | 1 + deps/ledger-zxlib | 1 + deps/nanos-secure-sdk | 1 + deps/nanosplus-secure-sdk | 1 + deps/nanox-secure-sdk | 1 + deps/stax-secure-sdk | 1 + docs/APDUSPEC.md | 141 +++++++ docs/zondax_dark.png | Bin 0 -> 21692 bytes docs/zondax_light.png | Bin 0 -> 26737 bytes fuzz/parser_parse.cpp | 73 ++++ fuzz/run-fuzz-crashes.py | 41 ++ fuzz/run-fuzzers.py | 39 ++ js/.eslintignore | 1 + js/.eslintrc.json | 14 + js/.gitignore | 74 ++++ js/.npmignore | 1 + js/.prettierignore | 2 + js/.prettierrc | 3 + js/LICENSE | 201 +++++++++ js/README.md | 12 + js/package.json | 46 ++ js/src/consts.ts | 14 + js/src/helper.ts | 25 ++ js/src/index.ts | 145 +++++++ js/src/types.ts | 19 + js/tsconfig.json | 14 + ledger_app.toml | 7 + tests/keys.cpp | 305 ++++++++++++++ tests/parser_impl.cpp | 63 +++ tests/testcases.json | 74 ++++ tests/ui_tests.cpp | 139 ++++++ tests/utils/common.cpp | 66 +++ tests/utils/common.h | 21 + tests_zemu/.editorconfig | 12 + tests_zemu/.eslintrc.js | 24 ++ tests_zemu/.gitignore | 71 ++++ tests_zemu/.npmignore | 1 + tests_zemu/.prettierignore | 2 + tests_zemu/.prettierrc | 9 + tests_zemu/globalsetup.js | 16 + tests_zemu/jest.config.js | 5 + tests_zemu/jest.js | 0 tests_zemu/package.json | 44 ++ tests_zemu/snapshots/s-mainmenu/00000.png | Bin 0 -> 447 bytes tests_zemu/snapshots/s-mainmenu/00001.png | Bin 0 -> 513 bytes tests_zemu/snapshots/s-mainmenu/00002.png | Bin 0 -> 500 bytes tests_zemu/snapshots/s-mainmenu/00003.png | Bin 0 -> 513 bytes tests_zemu/snapshots/s-mainmenu/00004.png | Bin 0 -> 438 bytes tests_zemu/snapshots/s-mainmenu/00005.png | Bin 0 -> 536 bytes tests_zemu/snapshots/s-mainmenu/00006.png | Bin 0 -> 488 bytes tests_zemu/snapshots/s-mainmenu/00007.png | Bin 0 -> 271 bytes tests_zemu/snapshots/s-mainmenu/00008.png | Bin 0 -> 488 bytes tests_zemu/snapshots/s-mainmenu/00009.png | Bin 0 -> 536 bytes tests_zemu/snapshots/s-mainmenu/00010.png | Bin 0 -> 438 bytes tests_zemu/snapshots/s-mainmenu/00011.png | Bin 0 -> 513 bytes tests_zemu/snapshots/s-mainmenu/00012.png | Bin 0 -> 447 bytes tests_zemu/snapshots/s-show_address/00000.png | Bin 0 -> 604 bytes tests_zemu/snapshots/s-show_address/00001.png | Bin 0 -> 616 bytes tests_zemu/snapshots/s-show_address/00002.png | Bin 0 -> 619 bytes tests_zemu/snapshots/s-show_address/00003.png | Bin 0 -> 571 bytes tests_zemu/snapshots/s-show_address/00004.png | Bin 0 -> 583 bytes tests_zemu/snapshots/s-show_address/00005.png | Bin 0 -> 598 bytes tests_zemu/snapshots/s-show_address/00006.png | Bin 0 -> 367 bytes tests_zemu/snapshots/s-show_address/00007.png | Bin 0 -> 249 bytes tests_zemu/snapshots/s-show_address/00008.png | Bin 0 -> 447 bytes tests_zemu/snapshots/sp-mainmenu/00000.png | Bin 0 -> 498 bytes tests_zemu/snapshots/sp-mainmenu/00001.png | Bin 0 -> 440 bytes tests_zemu/snapshots/sp-mainmenu/00002.png | Bin 0 -> 426 bytes tests_zemu/snapshots/sp-mainmenu/00003.png | Bin 0 -> 440 bytes tests_zemu/snapshots/sp-mainmenu/00004.png | Bin 0 -> 353 bytes tests_zemu/snapshots/sp-mainmenu/00005.png | Bin 0 -> 462 bytes tests_zemu/snapshots/sp-mainmenu/00006.png | Bin 0 -> 411 bytes tests_zemu/snapshots/sp-mainmenu/00007.png | Bin 0 -> 333 bytes tests_zemu/snapshots/sp-mainmenu/00008.png | Bin 0 -> 411 bytes tests_zemu/snapshots/sp-mainmenu/00009.png | Bin 0 -> 462 bytes tests_zemu/snapshots/sp-mainmenu/00010.png | Bin 0 -> 353 bytes tests_zemu/snapshots/sp-mainmenu/00011.png | Bin 0 -> 440 bytes tests_zemu/snapshots/sp-mainmenu/00012.png | Bin 0 -> 498 bytes .../snapshots/sp-show_address/00000.png | Bin 0 -> 473 bytes .../snapshots/sp-show_address/00001.png | Bin 0 -> 887 bytes .../snapshots/sp-show_address/00002.png | Bin 0 -> 471 bytes .../snapshots/sp-show_address/00003.png | Bin 0 -> 859 bytes .../snapshots/sp-show_address/00004.png | Bin 0 -> 428 bytes .../snapshots/sp-show_address/00005.png | Bin 0 -> 852 bytes .../snapshots/sp-show_address/00006.png | Bin 0 -> 427 bytes .../snapshots/sp-show_address/00007.png | Bin 0 -> 395 bytes .../snapshots/sp-show_address/00008.png | Bin 0 -> 355 bytes .../snapshots/sp-show_address/00009.png | Bin 0 -> 498 bytes tests_zemu/snapshots/st-mainmenu/00000.png | Bin 0 -> 10737 bytes tests_zemu/snapshots/st-mainmenu/00001.png | Bin 0 -> 13423 bytes tests_zemu/snapshots/st-mainmenu/00002.png | Bin 0 -> 6472 bytes tests_zemu/snapshots/st-mainmenu/00003.png | Bin 0 -> 6287 bytes tests_zemu/snapshots/st-mainmenu/00004.png | Bin 0 -> 6472 bytes tests_zemu/snapshots/st-mainmenu/00005.png | Bin 0 -> 10737 bytes tests_zemu/snapshots/x-mainmenu/00000.png | Bin 0 -> 498 bytes tests_zemu/snapshots/x-mainmenu/00001.png | Bin 0 -> 440 bytes tests_zemu/snapshots/x-mainmenu/00002.png | Bin 0 -> 426 bytes tests_zemu/snapshots/x-mainmenu/00003.png | Bin 0 -> 440 bytes tests_zemu/snapshots/x-mainmenu/00004.png | Bin 0 -> 353 bytes tests_zemu/snapshots/x-mainmenu/00005.png | Bin 0 -> 462 bytes tests_zemu/snapshots/x-mainmenu/00006.png | Bin 0 -> 411 bytes tests_zemu/snapshots/x-mainmenu/00007.png | Bin 0 -> 333 bytes tests_zemu/snapshots/x-mainmenu/00008.png | Bin 0 -> 411 bytes tests_zemu/snapshots/x-mainmenu/00009.png | Bin 0 -> 462 bytes tests_zemu/snapshots/x-mainmenu/00010.png | Bin 0 -> 353 bytes tests_zemu/snapshots/x-mainmenu/00011.png | Bin 0 -> 440 bytes tests_zemu/snapshots/x-mainmenu/00012.png | Bin 0 -> 498 bytes tests_zemu/snapshots/x-show_address/00000.png | Bin 0 -> 473 bytes tests_zemu/snapshots/x-show_address/00001.png | Bin 0 -> 887 bytes tests_zemu/snapshots/x-show_address/00002.png | Bin 0 -> 471 bytes tests_zemu/snapshots/x-show_address/00003.png | Bin 0 -> 859 bytes tests_zemu/snapshots/x-show_address/00004.png | Bin 0 -> 428 bytes tests_zemu/snapshots/x-show_address/00005.png | Bin 0 -> 852 bytes tests_zemu/snapshots/x-show_address/00006.png | Bin 0 -> 427 bytes tests_zemu/snapshots/x-show_address/00007.png | Bin 0 -> 395 bytes tests_zemu/snapshots/x-show_address/00008.png | Bin 0 -> 355 bytes tests_zemu/snapshots/x-show_address/00009.png | Bin 0 -> 498 bytes tests_zemu/tests/common.ts | 27 ++ tests_zemu/tests/pullImageKillOld.ts | 4 + tests_zemu/tests/standard.test.ts | 201 +++++++++ tests_zemu/tsconfig.json | 15 + 185 files changed, 6443 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .editorconfig create mode 100644 .github/workflows/check_version.yml create mode 100644 .github/workflows/guidelines_enforcer.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/publish.yaml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/LICENSE create mode 100755 app/Makefile create mode 100644 app/Makefile.version create mode 100644 app/glyphs/icon_app.gif create mode 100644 app/glyphs/icon_stax_32.gif create mode 100644 app/glyphs/icon_stax_64.gif create mode 100644 app/nanos_icon.gif create mode 100644 app/nanox_icon.gif create mode 100644 app/output/.gitkeep create mode 100644 app/pkg/.gitkeep create mode 100644 app/rust/.cargo/config create mode 100644 app/rust/.gitignore create mode 100644 app/rust/Cargo.lock create mode 100644 app/rust/Cargo.toml create mode 100644 app/rust/include/rslib.h create mode 100644 app/rust/src/constants.rs create mode 100644 app/rust/src/lib.rs create mode 100644 app/script_s2.ld create mode 100644 app/script_x.ld create mode 100644 app/src/addr.c create mode 100644 app/src/addr.h create mode 100644 app/src/apdu_handler.c create mode 100644 app/src/blake2s/blake2-impl.h create mode 100644 app/src/blake2s/blake2.h create mode 100644 app/src/blake2s/blake2s-ref.c create mode 100644 app/src/coin.h create mode 100644 app/src/common/actions.c create mode 100644 app/src/common/actions.h create mode 100644 app/src/common/main.c create mode 100644 app/src/common/parser.h create mode 100644 app/src/common/parser_common.h create mode 100644 app/src/common/tx.c create mode 100644 app/src/common/tx.h create mode 100644 app/src/crypto.c create mode 100644 app/src/crypto.h create mode 100644 app/src/crypto_helper.c create mode 100644 app/src/crypto_helper.h create mode 100644 app/src/keys_def.h create mode 100644 app/src/keys_personalizations.h create mode 100644 app/src/parser.c create mode 100644 app/src/parser_impl.c create mode 100644 app/src/parser_impl.h create mode 100644 app/src/parser_txdef.h create mode 100644 app/stax_icon.gif create mode 160000 cmake/cmake-modules create mode 100644 cmake/conan/CMakeLists.txt create mode 100644 cmake/gtest/CMakeLists.txt create mode 100644 cmake/gtest/CMakeLists.txt.gtest.in create mode 100644 conanfile.txt create mode 160000 deps/blake2 create mode 160000 deps/ledger-zxlib create mode 160000 deps/nanos-secure-sdk create mode 160000 deps/nanosplus-secure-sdk create mode 160000 deps/nanox-secure-sdk create mode 160000 deps/stax-secure-sdk create mode 100644 docs/APDUSPEC.md create mode 100644 docs/zondax_dark.png create mode 100644 docs/zondax_light.png create mode 100644 fuzz/parser_parse.cpp create mode 100755 fuzz/run-fuzz-crashes.py create mode 100755 fuzz/run-fuzzers.py create mode 100644 js/.eslintignore create mode 100644 js/.eslintrc.json create mode 100644 js/.gitignore create mode 100644 js/.npmignore create mode 100644 js/.prettierignore create mode 100644 js/.prettierrc create mode 100644 js/LICENSE create mode 100644 js/README.md create mode 100644 js/package.json create mode 100644 js/src/consts.ts create mode 100644 js/src/helper.ts create mode 100644 js/src/index.ts create mode 100644 js/src/types.ts create mode 100644 js/tsconfig.json create mode 100644 ledger_app.toml create mode 100644 tests/keys.cpp create mode 100644 tests/parser_impl.cpp create mode 100644 tests/testcases.json create mode 100644 tests/ui_tests.cpp create mode 100644 tests/utils/common.cpp create mode 100644 tests/utils/common.h create mode 100644 tests_zemu/.editorconfig create mode 100644 tests_zemu/.eslintrc.js create mode 100644 tests_zemu/.gitignore create mode 100644 tests_zemu/.npmignore create mode 100644 tests_zemu/.prettierignore create mode 100644 tests_zemu/.prettierrc create mode 100644 tests_zemu/globalsetup.js create mode 100644 tests_zemu/jest.config.js create mode 100644 tests_zemu/jest.js create mode 100644 tests_zemu/package.json create mode 100644 tests_zemu/snapshots/s-mainmenu/00000.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00001.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00002.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00003.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00004.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00005.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00006.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00007.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00008.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00009.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00010.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00011.png create mode 100644 tests_zemu/snapshots/s-mainmenu/00012.png create mode 100644 tests_zemu/snapshots/s-show_address/00000.png create mode 100644 tests_zemu/snapshots/s-show_address/00001.png create mode 100644 tests_zemu/snapshots/s-show_address/00002.png create mode 100644 tests_zemu/snapshots/s-show_address/00003.png create mode 100644 tests_zemu/snapshots/s-show_address/00004.png create mode 100644 tests_zemu/snapshots/s-show_address/00005.png create mode 100644 tests_zemu/snapshots/s-show_address/00006.png create mode 100644 tests_zemu/snapshots/s-show_address/00007.png create mode 100644 tests_zemu/snapshots/s-show_address/00008.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00000.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00001.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00002.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00003.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00004.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00005.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00006.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00007.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00008.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00009.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00010.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00011.png create mode 100644 tests_zemu/snapshots/sp-mainmenu/00012.png create mode 100644 tests_zemu/snapshots/sp-show_address/00000.png create mode 100644 tests_zemu/snapshots/sp-show_address/00001.png create mode 100644 tests_zemu/snapshots/sp-show_address/00002.png create mode 100644 tests_zemu/snapshots/sp-show_address/00003.png create mode 100644 tests_zemu/snapshots/sp-show_address/00004.png create mode 100644 tests_zemu/snapshots/sp-show_address/00005.png create mode 100644 tests_zemu/snapshots/sp-show_address/00006.png create mode 100644 tests_zemu/snapshots/sp-show_address/00007.png create mode 100644 tests_zemu/snapshots/sp-show_address/00008.png create mode 100644 tests_zemu/snapshots/sp-show_address/00009.png create mode 100644 tests_zemu/snapshots/st-mainmenu/00000.png create mode 100644 tests_zemu/snapshots/st-mainmenu/00001.png create mode 100644 tests_zemu/snapshots/st-mainmenu/00002.png create mode 100644 tests_zemu/snapshots/st-mainmenu/00003.png create mode 100644 tests_zemu/snapshots/st-mainmenu/00004.png create mode 100644 tests_zemu/snapshots/st-mainmenu/00005.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00000.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00001.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00002.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00003.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00004.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00005.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00006.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00007.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00008.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00009.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00010.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00011.png create mode 100644 tests_zemu/snapshots/x-mainmenu/00012.png create mode 100644 tests_zemu/snapshots/x-show_address/00000.png create mode 100644 tests_zemu/snapshots/x-show_address/00001.png create mode 100644 tests_zemu/snapshots/x-show_address/00002.png create mode 100644 tests_zemu/snapshots/x-show_address/00003.png create mode 100644 tests_zemu/snapshots/x-show_address/00004.png create mode 100644 tests_zemu/snapshots/x-show_address/00005.png create mode 100644 tests_zemu/snapshots/x-show_address/00006.png create mode 100644 tests_zemu/snapshots/x-show_address/00007.png create mode 100644 tests_zemu/snapshots/x-show_address/00008.png create mode 100644 tests_zemu/snapshots/x-show_address/00009.png create mode 100644 tests_zemu/tests/common.ts create mode 100644 tests_zemu/tests/pullImageKillOld.ts create mode 100644 tests_zemu/tests/standard.test.ts create mode 100644 tests_zemu/tsconfig.json diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1f7d191 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +BasedOnStyle: Google +IndentWidth: 4 +ColumnLimit: 125 +DerivePointerAlignment: false +PointerAlignment: Right diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..2cefa09 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,22 @@ +Checks: "-*, + clang-diagnostic-*, + clang-analyzer-*, + cppcoreguidelines-init-variables, + google-runtime-int, + google-readability-avoid-underscore-in-googletest-name, + misc-*, + performance-*, + portability-*, + readability-*, + -misc-no-recursion, + -readability-function-cognitive-complexity" +WarningsAsErrors: "*" +CheckOptions: + - key: readability-identifier-length.MinimumVariableNameLength + value: 2 + - key: readability-identifier-length.MinimumParameterNameLength + value: 2 + - key: readability-identifier-length.MinimumLoopCounterNameLength + value: 1 + - key: readability-magic-numbers.IgnorePowersOf2IntegerValues + value: true diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2d7db14 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true + +[*.{c,h,cpp,hpp}] +indent_style = space +indent_size = 4 + +[*.{yml,sh}] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/check_version.yml b/.github/workflows/check_version.yml new file mode 100644 index 0000000..4205186 --- /dev/null +++ b/.github/workflows/check_version.yml @@ -0,0 +1,53 @@ +name: Verify PRs to main + +on: + workflow_dispatch: + pull_request: + branches: + - main + - develop + +jobs: + configure: + runs-on: ubuntu-latest + outputs: + uid_gid: ${{ steps.get-user.outputs.uid_gid }} + steps: + - id: get-user + run: echo "uid_gid=$(id -u):$(id -g)" >> $GITHUB_OUTPUT + + get_version: + needs: configure + runs-on: ubuntu-latest + container: + image: zondax/ledger-app-builder:latest + options: --user ${{ needs.configure.outputs.uid_gid }} + env: + SDK_VARNAME: NANOSP_SDK + outputs: + version: ${{ steps.store-version.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - run: make version + - id: store-version + run: echo "version=$(cat ./app/app.version)" >> $GITHUB_OUTPUT + + check_app_version: + needs: get_version + runs-on: ubuntu-latest + steps: + - id: checkTag + uses: mukunku/tag-exists-action@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag: ${{ needs.get_version.outputs.version }} + + - run: echo ${{ steps.checkTag.outputs.exists }} + + - name: Tag exists + if: ${{ steps.checkTag.outputs.exists == 'true' }} + run: exit 1 diff --git a/.github/workflows/guidelines_enforcer.yml b/.github/workflows/guidelines_enforcer.yml new file mode 100644 index 0000000..215c34f --- /dev/null +++ b/.github/workflows/guidelines_enforcer.yml @@ -0,0 +1,23 @@ +name: Ensure compliance with Ledger guidelines + +# This workflow is mandatory in all applications +# It calls a reusable workflow guidelines_enforcer developed by Ledger's internal developer team. +# The successful completion of the reusable workflow is a mandatory step for an app to be available on the Ledger +# application store. +# +# More information on the guidelines can be found in the repository: +# LedgerHQ/ledger-app-workflows/ + +on: + workflow_dispatch: + # push: + # branches: + # - master + # - main + # - develop + # pull_request: + +jobs: + guidelines_enforcer: + name: Call Ledger guidelines_enforcer + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_guidelines_enforcer.yml@v1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..166b5f3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,43 @@ +name: Lint and format 💅 + +on: + workflow_dispatch: + # push: + # pull_request: + # branches: + # - main + # - develop + +jobs: + lint: + runs-on: ubuntu-latest + container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Add missing deps + run: | + DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y bear sudo + - name: Generate compilation database + run: bear -- make -j BOLOS_SDK="$NANOSP_SDK" + - name: Lint and format 💅 + uses: cpp-linter/cpp-linter-action@v2 + id: linter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + file-annotations: true + files-changed-only: false + ignore: "app/build|cmake|deps|fuzz|tests" + step-summary: true + style: file # uses .clang-format + thread-comments: true + tidy-checks: "" # use only .clang-tidy checks + - name: Fail if errors + if: steps.linter.outputs.checks-failed > 0 + run: | + echo "Linter or formatter failed!" + exit 1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..da6d771 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,165 @@ +name: Build +on: + workflow_dispatch: + push: + pull_request: + branches: + - main + - develop + +jobs: + configure: + runs-on: ubuntu-latest + outputs: + uid_gid: ${{ steps.get-user.outputs.uid_gid }} + steps: + - id: get-user + run: echo "uid_gid=$(id -u):$(id -g)" >> $GITHUB_OUTPUT + + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Install deps + run: | + sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 + make deps + - run: make cpp_test + + build_ledger: + needs: configure + runs-on: ubuntu-latest + container: + image: zondax/ledger-app-builder:latest + options: --user ${{ needs.configure.outputs.uid_gid }} + env: + BOLOS_SDK: /opt/nanos-secure-sdk + outputs: + size: ${{steps.build.outputs.size}} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Build Standard app + id: build + shell: bash -l {0} + run: | + make + echo "size=$(python3 deps/ledger-zxlib/scripts/getSize.py s)" >> $GITHUB_OUTPUT + + size_nano_s: + needs: build_ledger + runs-on: ubuntu-latest + env: + NANOS_LIMIT_SIZE: 136 + steps: + - run: | + echo "LNS app size: ${{needs.build_ledger.outputs.size}} KiB" + [ ${{needs.build_ledger.outputs.size}} -le $NANOS_LIMIT_SIZE ] + + test_zemu: + runs-on: ubuntu-latest + steps: + - name: Test + run: | + id + echo $HOME + echo $DISPLAY + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - run: sudo apt-get update -y && sudo apt-get install -y libusb-1.0.0 libudev-dev + - name: Install node + uses: actions/setup-node@v3 + - name: Install yarn + run: | + npm install -g yarn + - name: Build Ledger app + run: make + - name: Build/Install build js deps + run: make zemu_install + - name: Run zemu tests + run: "cd tests_zemu; yarn test" + - name: Upload Snapshots (only failure) + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: snapshots-tmp + path: tests_zemu/snapshots-tmp/ + + build_package_nanos: + needs: [configure, build, build_ledger, test_zemu] + if: ${{ github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + container: + image: zondax/ledger-app-builder:latest + options: --user ${{ needs.configure.outputs.uid_gid }} + env: + BOLOS_SDK: /opt/nanos-secure-sdk + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Install deps + run: pip install ledgerblue + + - name: Build NanoS + shell: bash -l {0} + run: | + make + mv ./app/pkg/installer_s.sh ./app/pkg/installer_nanos.sh + - name: Set tag + id: nanos + run: echo "tag_name=$(./app/pkg/installer_nanos.sh version)" >> $GITHUB_OUTPUT + - name: Create or Update Release (1) + id: create_release_0 + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + files: ./app/pkg/installer_nanos.sh + tag_name: ${{ steps.nanos.outputs.tag_name }} + draft: false + prerelease: false + + build_package_nanosp: + needs: [configure, build, build_ledger, test_zemu] + if: ${{ github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + container: + image: zondax/ledger-app-builder:latest + options: --user ${{ needs.configure.outputs.uid_gid }} + env: + BOLOS_SDK: /opt/nanosplus-secure-sdk + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Install deps + run: pip install ledgerblue + + - name: Build NanoSP + shell: bash -l {0} + run: | + make + mv ./app/pkg/installer_s2.sh ./app/pkg/installer_nanos_plus.sh + - name: Set tag + id: nanosp + run: echo "tag_name=$(./app/pkg/installer_nanos_plus.sh version)" >> $GITHUB_OUTPUT + - name: Update Release + id: update_release_2 + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + files: ./app/pkg/installer_nanos_plus.sh + tag_name: ${{ steps.nanosp.outputs.tag_name }} + draft: false + prerelease: false diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..9229e6d --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,54 @@ +name: "Publish js packages" + +on: + release: + types: + - created + tags: + - "npm_v[0-9]+(\\.[0-9]+)*" + +jobs: + publish_npm_package: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Install node + uses: actions/setup-node@v3 + with: + registry-url: "https://registry.npmjs.org" + scope: "@zondax" + - name: Install yarn + run: npm install -g yarn + - name: Build package + run: | + cd js + yarn install + yarn build + - name: Get latest release version number + id: get_version + run: | + GITHUB_REF=${{ github.ref }} + echo "version=${GITHUB_REF##*/}" >> $GITHUB_OUTPUT + - name: Show version + run: echo ${{ steps.get_version.outputs.version }} + - name: Clean latest release version number + id: get_version_cleaned + uses: bhowell2/github-substring-action@v1 + with: + output_name: version + value: ${{ steps.get_version.outputs.version }} + index_of_str: "npm_" + - name: Update tag + run: | + cd js + echo Publishing as ${{ steps.get_version_cleaned.outputs.version }} + npm --allow-same-version --no-git-tag-version version ${{ steps.get_version_cleaned.outputs.version }} + - name: Publish package + run: | + cd js + npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_PUBLISH_AUTO }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b432952 --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# Created by .ignore support plugin (hsz.mobi) +### C Ironfish +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd + +########################## + +tests_zemu/.yarn/* +tests_zemu/!.yarn/patches +tests_zemu/!.yarn/releases +tests_zemu/!.yarn/plugins +tests_zemu/!.yarn/sdks +tests_zemu/!.yarn/versions +tests_zemu/.pnp.* + +######################### + +.vscode +.idea + +node_modules +fuzz/corpora + +!build/.gitkeep +build/* +cmake-build-debug +app/build/* + +tests_zemu/snapshots-tmp +tests_zemu/yarn.lock +tests_tools/target +fuzz-*.log +/scan-build +app/rust/.cargo/registry +app/rust/.cargo/.package-cache* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..87f3031 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,21 @@ +[submodule "deps/nanos-secure-sdk"] + path = deps/nanos-secure-sdk + url = https://github.com/LedgerHQ/nanos-secure-sdk +[submodule "deps/nanosplus-secure-sdk"] + path = deps/nanosplus-secure-sdk + url = https://github.com/LedgerHQ/ledger-secure-sdk +[submodule "deps/nanox-secure-sdk"] + path = deps/nanox-secure-sdk + url = https://github.com/LedgerHQ/ledger-secure-sdk +[submodule "deps/ledger-zxlib"] + path = deps/ledger-zxlib + url = https://github.com/Zondax/ledger-zxlib.git +[submodule "cmake/cmake-modules"] + path = cmake/cmake-modules + url = https://github.com/bilke/cmake-modules +[submodule "deps/stax-secure-sdk"] + path = deps/stax-secure-sdk + url = https://github.com/LedgerHQ/ledger-secure-sdk +[submodule "deps/blake2"] + path = deps/blake2 + url = https://github.com/Zondax/BLAKE2.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3b89d3c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,231 @@ +#******************************************************************************* +#* (c) 2018 -2023 Zondax AG +#* +#* 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. +#******************************************************************************** +cmake_minimum_required(VERSION 3.0) +project(ledger-ironfish VERSION 0.0.0) +enable_testing() + +cmake_policy(SET CMP0025 NEW) +set(CMAKE_CXX_STANDARD 20) + +option(ENABLE_FUZZING "Build with fuzzing instrumentation and build fuzz targets" OFF) +option(ENABLE_COVERAGE "Build with source code coverage instrumentation" OFF) +option(ENABLE_SANITIZERS "Build with ASAN and UBSAN" OFF) + +string(APPEND CMAKE_C_FLAGS " -fno-omit-frame-pointer -g") +string(APPEND CMAKE_CXX_FLAGS " -fno-omit-frame-pointer -g") +string(APPEND CMAKE_LINKER_FLAGS " -fno-omit-frame-pointer -g") + +add_definitions(-DAPP_STANDARD) +add_definitions(-DSUBSTRATE_PARSER_FULL) + +if(ENABLE_FUZZING) + add_definitions(-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1) + SET(ENABLE_SANITIZERS ON CACHE BOOL "Sanitizer automatically enabled" FORCE) + SET(CMAKE_BUILD_TYPE Debug) + + if (DEFINED ENV{FUZZ_LOGGING}) + add_definitions(-DFUZZING_LOGGING) + message(FATAL_ERROR "Fuzz logging enabled") + endif() + + set(CMAKE_CXX_CLANG_TIDY clang-tidy -checks=-*,bugprone-*,cert-*,clang-analyzer-*,-cert-err58-cpp,misc-*) + + if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + # require at least clang 3.2 + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 10.0) + message(FATAL_ERROR "Clang version must be at least 10.0!") + endif() + else() + message(FATAL_ERROR + "You are using an unsupported compiler! Fuzzing only works with Clang 10.\n" + "1. Install clang-10 \n" + "2. Pass -DCMAKE_C_COMPILER=clang-10 -DCMAKE_CXX_COMPILER=clang++-10") + endif() + + string(APPEND CMAKE_C_FLAGS " -fsanitize=fuzzer-no-link") + string(APPEND CMAKE_CXX_FLAGS " -fsanitize=fuzzer-no-link") + string(APPEND CMAKE_LINKER_FLAGS " -fsanitize=fuzzer-no-link") +endif() + +if(ENABLE_COVERAGE) + string(APPEND CMAKE_C_FLAGS " -fprofile-instr-generate -fcoverage-mapping") + string(APPEND CMAKE_CXX_FLAGS " -fprofile-instr-generate -fcoverage-mapping") + string(APPEND CMAKE_LINKER_FLAGS " -fprofile-instr-generate -fcoverage-mapping") +endif() + +if(ENABLE_SANITIZERS) + string(APPEND CMAKE_C_FLAGS " -fsanitize=address,undefined -fsanitize-recover=address,undefined") + string(APPEND CMAKE_CXX_FLAGS " -fsanitize=address,undefined -fsanitize-recover=address,undefined") + string(APPEND CMAKE_LINKER_FLAGS " -fsanitize=address,undefined -fsanitize-recover=address,undefined") +endif() + +include(cmake/conan/CMakeLists.txt) +add_subdirectory(cmake/gtest) + +set (RETRIEVE_MAJOR_CMD + "cat ${CMAKE_CURRENT_SOURCE_DIR}/app/Makefile.version | grep APPVERSION_M | cut -b 14- | tr -d '\n'" +) +set (RETRIEVE_MINOR_CMD + "cat ${CMAKE_CURRENT_SOURCE_DIR}/app/Makefile.version | grep APPVERSION_N | cut -b 14- | tr -d '\n'" +) +execute_process( + COMMAND bash "-c" ${RETRIEVE_MAJOR_CMD} + RESULT_VARIABLE MAJOR_RESULT + OUTPUT_VARIABLE MAJOR_VERSION +) +execute_process( + COMMAND bash "-c" ${RETRIEVE_MINOR_CMD} + RESULT_VARIABLE MINOR_RESULT + OUTPUT_VARIABLE MINOR_VERSION +) + +message(STATUS "LEDGER_MAJOR_VERSION [${MAJOR_RESULT}]: ${MAJOR_VERSION}" ) +message(STATUS "LEDGER_MINOR_VERSION [${MINOR_RESULT}]: ${MINOR_VERSION}" ) + +add_definitions( + -DLEDGER_MAJOR_VERSION=${MAJOR_VERSION} + -DLEDGER_MINOR_VERSION=${MINOR_VERSION} +) + + +############################################################## +##{TODO} +############################################################## +# static libs +file(GLOB_RECURSE LIB_SRC + ${CMAKE_CURRENT_SOURCE_DIR}/deps/ledger-zxlib/src/app_mode.c + ${CMAKE_CURRENT_SOURCE_DIR}/deps/ledger-zxlib/src/base58.c + ${CMAKE_CURRENT_SOURCE_DIR}/deps/ledger-zxlib/src/bech32.c + ${CMAKE_CURRENT_SOURCE_DIR}/deps/ledger-zxlib/src/bignum.c + ${CMAKE_CURRENT_SOURCE_DIR}/deps/ledger-zxlib/src/hexutils.c + ${CMAKE_CURRENT_SOURCE_DIR}/deps/ledger-zxlib/src/zxmacros.c + ${CMAKE_CURRENT_SOURCE_DIR}/deps/ledger-zxlib/src/zxformat.c + #### + ${CMAKE_CURRENT_SOURCE_DIR}/app/src/parser.c + ${CMAKE_CURRENT_SOURCE_DIR}/app/src/parser_impl.c + #### + ${CMAKE_CURRENT_SOURCE_DIR}/app/src/crypto_helper.c + #### + ${CMAKE_CURRENT_SOURCE_DIR}/deps/blake2/ref/blake2b-ref.c + ${CMAKE_CURRENT_SOURCE_DIR}/app/src/blake2s/blake2s-ref.c + ) + +add_library(app_lib STATIC ${LIB_SRC}) + +target_include_directories(app_lib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/deps/ledger-zxlib/include + ${CMAKE_CURRENT_SOURCE_DIR}/app/src + ${CMAKE_CURRENT_SOURCE_DIR}/app/src/lib + ${CMAKE_CURRENT_SOURCE_DIR}/app/src/common + ### + ${CMAKE_CURRENT_SOURCE_DIR}/app/rust/include + ### + ${CMAKE_CURRENT_SOURCE_DIR}/deps/blake2/ref + + ) + +############################################################## +## Rust library for CPP tests +set(RUST_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/app/rust") + +# Determine the Rust target triple based on the host system +if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") + if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "aarch64") + set(RUST_TARGET_TRIPLE "aarch64-unknown-linux-gnu") + elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64") + set(RUST_TARGET_TRIPLE "x86_64-unknown-linux-gnu") + else() + message(FATAL_ERROR "Unsupported processor: ${CMAKE_HOST_SYSTEM_PROCESSOR}") + endif() +elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") + if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "arm64") + set(RUST_TARGET_TRIPLE "aarch64-apple-darwin") + elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64") + set(RUST_TARGET_TRIPLE "x86_64-apple-darwin") + else() + message(FATAL_ERROR "Unsupported processor: ${CMAKE_HOST_SYSTEM_PROCESSOR}") + endif() +else() + message(FATAL_ERROR "Unsupported host system: ${CMAKE_HOST_SYSTEM_NAME}") +endif() + +# Use debug mode for debugging tests +set(RUST_TARGET_DIR "${RUST_LIB_DIR}/target/${RUST_TARGET_TRIPLE}/debug") + +# Custom target for the Rust library +add_custom_target(RustLibClean + COMMAND cargo clean + WORKING_DIRECTORY ${RUST_LIB_DIR} +) +add_custom_target(RustLibBuild + COMMAND cargo build --target ${RUST_TARGET_TRIPLE} --features cpp_tests + WORKING_DIRECTORY ${RUST_LIB_DIR} + DEPENDS RustLibClean +) + +# Assuming the Rust library outputs a file named librslib.a +set(RUST_LIB "${RUST_TARGET_DIR}/librslib.a") + +# Ensure the Rust library is built before the C++ project +add_library(rslib STATIC IMPORTED) +set_property(TARGET rslib PROPERTY IMPORTED_LOCATION ${RUST_LIB}) +add_dependencies(rslib RustLibBuild) + +# Ensure your C++ targets depend on the Rust library being built first +# For example, for your app_lib static library: +add_dependencies(app_lib rslib) +############################################################## +# Tests +file(GLOB_RECURSE TESTS_SRC + ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp) + +add_executable(unittests ${TESTS_SRC}) +target_include_directories(unittests PRIVATE + ${gtest_SOURCE_DIR}/include + ${gmock_SOURCE_DIR}/include + ${CONAN_INCLUDE_DIRS_FMT} + ${CONAN_INCLUDE_DIRS_JSONCPP} + ${CMAKE_CURRENT_SOURCE_DIR}/app/src + ${CMAKE_CURRENT_SOURCE_DIR}/app/src/lib + ### + ${CMAKE_CURRENT_SOURCE_DIR}/deps/blake2/ref + ) + +target_link_libraries(unittests PRIVATE + gtest_main + app_lib + rslib + CONAN_PKG::fmt + CONAN_PKG::jsoncpp) + +add_compile_definitions(TESTVECTORS_DIR="${CMAKE_CURRENT_SOURCE_DIR}/tests/") +add_test(unittests ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/unittests) +set_tests_properties(unittests PROPERTIES WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests) + +############################################################## +############################################################## +# Fuzz Targets +if(ENABLE_FUZZING) + set(FUZZ_TARGETS + parser_parse + ) + + foreach(target ${FUZZ_TARGETS}) + add_executable(fuzz-${target} ${CMAKE_CURRENT_SOURCE_DIR}/fuzz/${target}.cpp) + target_link_libraries(fuzz-${target} PRIVATE app_lib) + target_link_options(fuzz-${target} PRIVATE "-fsanitize=fuzzer") + endforeach() +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bfd6018 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2023 Zondax AG + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..52c95b4 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +#******************************************************************************* +#* (c) 2018 -2023 Zondax AG +#* +#* 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. +#******************************************************************************** + +# We use BOLOS_SDK to determine the development environment that is being used +# BOLOS_SDK IS DEFINED We use the plain Makefile for Ledger +# BOLOS_SDK NOT DEFINED We use a containerized build approach + +TESTS_JS_PACKAGE = "@zondax/ledger-ironfish" +TESTS_JS_DIR = $(CURDIR)/js + +ifeq ($(BOLOS_SDK),) +# In this case, there is not predefined SDK and we run dockerized +# When not using the SDK, we override and build the XL complete app + +# ZXLIB_COMPILE_STAX ?= 1 +include $(CURDIR)/deps/ledger-zxlib/dockerized_build.mk + +else +default: + $(MAKE) -C app +%: + $(info "Calling app Makefile for target $@") + COIN=$(COIN) $(MAKE) -C app $@ +endif + +test_all: + make + make zemu_install + make zemu_test diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad476c8 --- /dev/null +++ b/README.md @@ -0,0 +1,203 @@ +# Ledger Ironfish app +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![GithubActions](https://github.com/Zondax/ledger-ironfish/actions/workflows/main.yml/badge.svg)](https://github.com/Zondax/ledger-ironfish/blob/main/.github/workflows/main.yaml) + +--- + +![zondax_light](docs/zondax_light.png#gh-light-mode-only) +![zondax_dark](docs/zondax_dark.png#gh-dark-mode-only) + +_Please visit our website at [zondax.ch](https://www.zondax.ch)_ + +--- + +This project contains the Ironfish app for Ledger Nano S Nano S+ and X. + +- Ledger Nano S/S+/X Ironfish app +- Specs / Documentation +- C++ unit tests +- Zemu tests + +## ATTENTION + +Please: + +- **Do not use in production** +- **Do not use a Ledger device with funds for development purposes.** +- **Have a separate and marked device that is used ONLY for development and testing** + + +## Download and install a prerelease + +*Once the app is approved by Ledger, it will be available in their app store (Ledger Live). +You can get builds generated by Github Actions from the release tab. THESE ARE UNVETTED DEVELOPMENT RELEASES* + +Download a release from here (https://github.com/Zondax/ledger-ironfish/releases). You only need `installer.sh` + +If the file is not executable, run +```sh +chmod +x ./installer.sh +``` + +then run: + +```sh +./installer.sh load +``` + +# Development + +## Preconditions + +- Be sure you checkout submodules too: + + ``` + git submodule update --init --recursive + ``` + +- Install Docker CE + - Instructions can be found here: https://docs.docker.com/install/ + +- We only officially support Ubuntu. Install the following packages: + ``` + sudo apt update && apt-get -y install build-essential git wget cmake \ + libssl-dev libgmp-dev autoconf libtool + ``` + +- Install `node > v13.0`. We typically recommend using `n` + +- You will need python 3 and then run + - `make deps` + +- This project requires Ledger firmware 2.0 + - The current repository keeps track of Ledger's SDK but it is possible to override it by changing the git submodule. + +*Warning*: Some IDEs may not use the same python interpreter or virtual enviroment as the one you used when running `pip`. +If you see conan is not found, check that you installed the package in the same interpreter as the one that launches `cmake`. + +## How to build ? + +> We like clion or vscode but let's have some reproducible command line steps +> + +- Building the app itself + + If you installed the what is described above, just run: + ```bash + make + ``` + +## Running tests + +- Running rust tests (x64) + + If you installed the what is described above, just run: + ```bash + make rust_test + ``` + +- Running C/C++ tests (x64) + + If you installed the what is described above, just run: + ```bash + make cpp_test + ``` + +- Running device emulation+integration tests!! + + ```bash + Use Zemu! Explained below! + ``` + +## How to test with Zemu? + +> What is Zemu?? Great you asked!! +> As part of this project, we are making public a beta version of our internal testing+emulation framework for Ledger apps. +> +> Npm Package here: https://www.npmjs.com/package/@zondax/zemu +> +> Repo here: https://github.com/Zondax/zemu + +Let's go! First install everything: +> At this moment, if you change the app you will need to run `make` before running the test again. + +```bash +make zemu_install +``` + +Then you can run JS tests: + +```bash +make zemu_test +``` + +To run a single specific test: + +> At the moment, the recommendation is to run from the IDE. Remember to run `make` if you change the app. + +## Using a real device + +### How to prepare your DEVELOPMENT! device: + +> You can use an emulated device for development. This is only required if you are using a physical device +> +> **Please do not use a Ledger device with funds for development purposes.** +>> +> **Have a separate and marked device that is used ONLY for development and testing** + + There are a few additional steps that increase reproducibility and simplify development: + +**1 - Ensure your device works in your OS** +- In Linux hosts it might be necessary to adjust udev rules, etc. + + Refer to Ledger documentation: https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues + +**2 - Set a test mnemonic** + +Many of our integration tests expect the device to be configured with a known test mnemonic. + +- Plug your device while pressing the right button + +- Your device will show "Recovery" in the screen + +- Double click + +- Run `make dev_init`. This will take about 2 minutes. The device will be initialized to: + + ``` + PIN: 5555 + Mnemonic: equip will roof matter pink blind book anxiety banner elbow sun young + ``` + +**3 - Add a development certificate** + +- Plug your device while pressing the right button + +- Your device will show "Recovery" in the screen + +- Click both buttons at the same time + +- Enter your pin if necessary + +- Run `make dev_ca`. The device will receive a development certificate to avoid constant manual confirmations. + + +### Loading into your development device + +The Makefile will build the firmware in a docker container and leave the binary in the correct directory. + +- Build + + ``` + make # Builds the app + ``` + +- Upload to a device + The following command will upload the application to the ledger. _Warning: The application will be deleted before uploading._ + ``` + make load # Builds and loads the app to the device + ``` + +## APDU Specifications + +- [APDU Protocol](docs/APDUSPEC.md) diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..13a58d2 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,51 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# OS related files +.DS_Store + +# Others +cmake-build-debug/ +\.idea/workspace\.xml +\.idea/ + +glyphs/glyphs\.h +glyphs/glyphs\.c +obj/ +bin/ +debug/ +bin/app.sha256 +bin/app.apdu + +output/app* +pkg/installer_*.sh diff --git a/app/LICENSE b/app/LICENSE new file mode 100644 index 0000000..bfd6018 --- /dev/null +++ b/app/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2023 Zondax AG + + 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. diff --git a/app/Makefile b/app/Makefile new file mode 100755 index 0000000..8f84086 --- /dev/null +++ b/app/Makefile @@ -0,0 +1,104 @@ +#******************************************************************************* +# Ledger App +# (c) 2018 - 2024 Zondax AG +# (c) 2017 Ledger +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#******************************************************************************* + +ifeq ($(BOLOS_SDK),) +$(error BOLOS_SDK is not set) +endif + +MY_DIR := $(dir $(lastword $(MAKEFILE_LIST))) + +include $(CURDIR)/../deps/ledger-zxlib/makefiles/Makefile.installer_script + +include $(BOLOS_SDK)/Makefile.defines + +$(info ************ TARGET_NAME = [$(TARGET_NAME)]) + +include $(CURDIR)/../deps/ledger-zxlib/makefiles/Makefile.app_testing + +ifndef COIN +COIN=IRON +endif + +include $(CURDIR)/Makefile.version + +$(info COIN = [$(COIN)]) + +ifeq ($(COIN),IRON) +# Main app configuration +APPNAME = "Ironfish" +APPPATH = "44'/133'" + +else +define error_message + +COIN value not supported: [$(COIN)] + +endef +$(error "$(error_message)") +endif + +APP_LOAD_PARAMS = --curve ed25519 --delete $(COMMON_LOAD_PARAMS) --path $(APPPATH) + +include $(CURDIR)/../deps/ledger-zxlib/makefiles/Makefile.devices + +$(info TARGET_NAME = [$(TARGET_NAME)]) +$(info ICONNAME = [$(ICONNAME)]) + +ifndef ICONNAME +$(error ICONNAME is not set) +endif + +include $(CURDIR)/../deps/ledger-zxlib/makefiles/Makefile.platform + +# Add SDK BLAKE2b +DEFINES += HAVE_HASH HAVE_BLAKE2 +INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/src + + +# Building Rust +LDFLAGS += -z muldefs +LDLIBS += -L$(MY_DIR)rust/target/$(RUST_TARGET)/release -lrslib + +APP_SOURCE_PATH += $(CURDIR)/rust/include +APP_CUSTOM_LINK_DEPENDENCIES = rust +RUST_TARGET:=thumbv6m-none-eabi + +.PHONY: rust +rust: + cd rust && RUSTC_BOOTSTRAP=1 CARGO_HOME="$(CURDIR)/rust/.cargo" cargo build --target $(RUST_TARGET) --release + +.PHONY: rust_clean +rust_clean: + cd rust && CARGO_HOME="$(CURDIR)/rust/.cargo" cargo clean + +clean: rust_clean + +include $(CURDIR)/../deps/ledger-zxlib/makefiles/Makefile.side_loading + +# Import generic rules from the SDK +include $(BOLOS_SDK)/Makefile.rules + +#add dependency on custom makefile filename +dep/%.d: %.c Makefile + +listvariants: + @echo VARIANTS COIN IRON + +.PHONY: version +version: + @echo "v$(APPVERSION)" > app.version diff --git a/app/Makefile.version b/app/Makefile.version new file mode 100644 index 0000000..378e594 --- /dev/null +++ b/app/Makefile.version @@ -0,0 +1,6 @@ +# This is the major version +APPVERSION_M=0 +# This is the minor version +APPVERSION_N=0 +# This is the patch version +APPVERSION_P=3 diff --git a/app/glyphs/icon_app.gif b/app/glyphs/icon_app.gif new file mode 100644 index 0000000000000000000000000000000000000000..2eb9ba82015f28b93c3597cc74ae18046eb6371f GIT binary patch literal 107 zcmZ?wbhEHb6krfw_`m=H|NsA2{K*1lD*os8%uP&B^-WCAOwQ&@Pt46tv^CH(F$F;e z9grH3J_aV0p8l1m*E0C>G+ed3e0N%ymhOx%ii_3TGFBG8X^fS9wf9`_b5;gx0DGGw AEC2ui literal 0 HcmV?d00001 diff --git a/app/glyphs/icon_stax_32.gif b/app/glyphs/icon_stax_32.gif new file mode 100644 index 0000000000000000000000000000000000000000..db6e52dbceb9b9afcf0b1ee641a0e417a64ba6f1 GIT binary patch literal 167 zcmZ?wbhEHbRA5kG_`m=H|NsA2{K*1lD*os8%uP&B^-WCAOwQ&@Pt46tv^CH(F$F;e z9grH3J_e?Qp8l1m-|{b>v*p(5hI<*!|7_>6O={Pi87H~yVO7yYt-~U{+dpedXM8yQ zq0VLQVYVxZ{d-lon{?Qu15+n+R#~}*y`3`ivt;>0-7TEIgD!40G*jEF_ejU|S>O5B M-~a4e$iQF?0Nrpw?f?J) literal 0 HcmV?d00001 diff --git a/app/glyphs/icon_stax_64.gif b/app/glyphs/icon_stax_64.gif new file mode 100644 index 0000000000000000000000000000000000000000..177e8a5aa340ef6c2207db7a9fb58d969999837b GIT binary patch literal 313 zcmV-90mlAENk%w1VL$*t0Pp|+00030|NkNR1ONa4001HX3rTHZXJt)cXK7<=4rgI) zZDBnyE;KbXH8eEQY=-e{Y=A#Z)u#&rb%e4 z+RgTo-%+uAeW;hYm$v>LDc-i3TpRSiRSugn7TwsDCXF0o z%{$#UOZR+5I>#&G+ed3e0N%ymhOx%ii_3TGFBG8X^fS9wf9`_b5;gx0DGGw AEC2ui literal 0 HcmV?d00001 diff --git a/app/nanox_icon.gif b/app/nanox_icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..c77a8afd61886b1cac173059a2a1ef8830ef704e GIT binary patch literal 104 zcmZ?wbhEHbB&gM%`%*{=-HPACL z1wjTKkP?tS1}4Rx{*~DecxQF#Uvd7t<)T+esrdKadp}DSEOW|>?ox9$W@WGj04}v5 AD*ylh literal 0 HcmV?d00001 diff --git a/app/output/.gitkeep b/app/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/pkg/.gitkeep b/app/pkg/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/rust/.cargo/config b/app/rust/.cargo/config new file mode 100644 index 0000000..90cae2f --- /dev/null +++ b/app/rust/.cargo/config @@ -0,0 +1,13 @@ +[build] + +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +rustflags = [ + "--emit", "asm", + "-C", "relocation-model=ropi", + "-C", "link-arg=-nostartfiles", + "-C", "link-arg=-Tlink.ld", + "-C", "inline-threshold=0" +] +[unstable] +build-std=["core"] +build-std-features=["panic_immediate_abort"] diff --git a/app/rust/.gitignore b/app/rust/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/app/rust/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/app/rust/Cargo.lock b/app/rust/Cargo.lock new file mode 100644 index 0000000..6858d7f --- /dev/null +++ b/app/rust/Cargo.lock @@ -0,0 +1,114 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "jubjub" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8499f7a74008aafbecb2a2e608a3e13e4dd3e84df198b604451efe93f2de6e61" +dependencies = [ + "bitvec", + "bls12_381", + "ff", + "group", + "rand_core", + "subtle", +] + +[[package]] +name = "panic-halt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rslib" +version = "0.0.1" +dependencies = [ + "jubjub", + "panic-halt", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] diff --git a/app/rust/Cargo.toml b/app/rust/Cargo.toml new file mode 100644 index 0000000..2ce3b04 --- /dev/null +++ b/app/rust/Cargo.toml @@ -0,0 +1,35 @@ +[package] +authors = ["Zondax AG "] +name = "rslib" +version = "0.0.1" +edition = "2021" +readme = "README.md" +resolver = "2" + +[lib] +name = "rslib" +crate-type = ["staticlib"] + +[dependencies] +jubjub = { version = "0.10.0", default-features = false } + +[target.thumbv6m-none-eabi.dev-dependencies] +panic-halt = "0.2.0" + +[profile.release] +lto = false +codegen-units = 1 +debug = false +opt-level = "z" + +[profile.dev] +lto = "fat" +codegen-units = 1 +debug=true +opt-level = "s" +panic = "abort" + +[features] +default = [] +# use when compiling this crate as a lib for the cpp_tests suite +cpp_tests = [] diff --git a/app/rust/include/rslib.h b/app/rust/include/rslib.h new file mode 100644 index 0000000..72eca71 --- /dev/null +++ b/app/rust/include/rslib.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include "parser_common.h" +#include "keys_def.h" + +/* Interface functions with jubjub crate */ +parser_error_t from_bytes_wide(const uint8_t input[64], uint8_t output[32]); +parser_error_t scalar_multiplication(const uint8_t input[32], constant_key_t key, uint8_t output[32]); diff --git a/app/rust/src/constants.rs b/app/rust/src/constants.rs new file mode 100644 index 0000000..687c472 --- /dev/null +++ b/app/rust/src/constants.rs @@ -0,0 +1,65 @@ +/******************************************************************************* +* (c) 2018 - 2024 Zondax AG +* +* 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 jubjub::{AffineNielsPoint, AffinePoint, Fq}; + +pub const SPENDING_KEY_GENERATOR: AffineNielsPoint = AffinePoint::from_raw_unchecked( + Fq::from_raw([ + 0x47bf_4692_0a95_a753, + 0xd5b9_a7d3_ef8e_2827, + 0xd418_a7ff_2675_3b6a, + 0x0926_d4f3_2059_c712, + ]), + Fq::from_raw([ + 0x3056_32ad_aaf2_b530, + 0x6d65_674d_cedb_ddbc, + 0x53bb_37d0_c21c_fd05, + 0x57a1_019e_6de9_b675, + ]), +) +.to_niels(); + +pub const PROOF_GENERATION_KEY_GENERATOR: AffineNielsPoint = AffinePoint::from_raw_unchecked( + Fq::from_raw([ + 0x3af2_dbef_b96e_2571, + 0xadf2_d038_f2fb_b820, + 0x7043_03f1_e890_6081, + 0x1457_a502_31cd_e2df, + ]), + Fq::from_raw([ + 0x467a_f9f7_e05d_e8e7, + 0x50df_51ea_f5a1_49d2, + 0xdec9_0184_0f49_48cc, + 0x54b6_d107_18df_2a7a, + ]), +) +.to_niels(); + +pub const PUBLIC_KEY_GENERATOR: AffineNielsPoint = AffinePoint::from_raw_unchecked( + Fq::from_raw([ + 0x3edc_c85f_4d1a_44cd, + 0x77ff_8c90_a9a0_d8f4, + 0x0daf_03b5_47e2_022b, + 0x6dad_65e6_2328_d37a, + ]), + Fq::from_raw([ + 0x5095_1f1f_eff0_8278, + 0xf0b7_03d5_3a3e_dd4e, + 0xca01_f580_9c00_eee2, + 0x6996_932c_ece1_f4bb, + ]), +) +.to_niels(); diff --git a/app/rust/src/lib.rs b/app/rust/src/lib.rs new file mode 100644 index 0000000..2edfd42 --- /dev/null +++ b/app/rust/src/lib.rs @@ -0,0 +1,103 @@ +/******************************************************************************* +* (c) 2018 - 2024 Zondax AG +* +* 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] +#![no_builtins] +#![allow(dead_code, unused_imports)] + +use core::panic::PanicInfo; + +use constants::{SPENDING_KEY_GENERATOR}; +mod constants; + +use jubjub::{Fr, AffinePoint, ExtendedPoint}; + +fn debug(_msg: &str) {} + +// ParserError should mirror parser_error_t from parser_common. +// At the moment, just implement OK or Error +#[repr(C)] +pub enum ParserError { + ParserOk = 0, + ParserUnexpectedError = 5, +} + +#[repr(C)] +pub enum ConstantKey { + SpendingKeyGenerator, + ProofGenerationKeyGenerator, + PublicKeyGenerator, +} + +#[no_mangle] +pub extern "C" fn from_bytes_wide(input: &[u8; 64], output: &mut [u8; 32]) -> ParserError { + let result = Fr::from_bytes_wide(input).to_bytes(); + output.copy_from_slice(&result[0..32]); + ParserError::ParserOk +} + +#[no_mangle] +pub extern "C" fn scalar_multiplication(input: &[u8; 32], key: ConstantKey, output: *mut [u8; 32]) -> ParserError { + let key_point = match key { + ConstantKey::SpendingKeyGenerator => constants::SPENDING_KEY_GENERATOR, + ConstantKey::ProofGenerationKeyGenerator => constants::PROOF_GENERATION_KEY_GENERATOR, + ConstantKey::PublicKeyGenerator => constants::PUBLIC_KEY_GENERATOR, + }; + + let extended_point = key_point.multiply_bits(input); + let result = AffinePoint::from(&extended_point); + + unsafe { + let output_slice = &mut *output; + output_slice.copy_from_slice(&result.to_bytes()); + } + + ParserError::ParserOk +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + + +#[cfg(test)] +mod tests { + // use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT; + // use curve25519_dalek::edwards::EdwardsPoint; + // use curve25519_dalek::scalar::Scalar; + // use log::{debug, info}; + // use schnorrkel::{context::*, Keypair, PublicKey, SecretKey, Signature}; + + // use crate::*; + // use core::ops::Mul; + + // fn init_logging() { + // let _ = env_logger::builder().is_test(true).try_init(); + // } + + // fn ristretto_scalarmult(sk: &[u8], pk: &mut [u8]) { + // let mut seckey = [0u8; 32]; + // seckey.copy_from_slice(&sk[0..32]); + // let pubkey = RISTRETTO_BASEPOINT_POINT + // .mul(Scalar::from_bits(seckey)) + // .compress() + // .0; + // pk.copy_from_slice(&pubkey); + // } + +} diff --git a/app/script_s2.ld b/app/script_s2.ld new file mode 100644 index 0000000..db3150b --- /dev/null +++ b/app/script_s2.ld @@ -0,0 +1,170 @@ +/******************************************************************************* +* Ledger Blue - Secure firmware +* (c) 2016, 2017, 2018, 2019 Ledger +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ + +/** + * Global chip memory layout and constants + * + */ + +MEMORY +{ + DISCARD (rwx) : ORIGIN = 0xd0000000, LENGTH = 1M + + FLASH (rx) : ORIGIN = 0xc0de0000, LENGTH = 400K + DATA (r) : ORIGIN = 0xc0de0000, LENGTH = 400K + SRAM (rwx) : ORIGIN = 0xda7a0000, LENGTH = 30K +} + +PAGE_SIZE = 512; +STACK_SIZE = 8192; +END_STACK = ORIGIN(SRAM) + LENGTH(SRAM); + +ENTRY(main); + +SECTIONS +{ + /****************************************************************/ + /* This section locates the code in FLASH */ + /****************************************************************/ + + /** put text in Flash memory, VMA will be equal to LMA */ + .text : + { + /* provide start code symbol, shall be zero */ + _text = .; + _nvram_start = .; + + /* ensure main is always @ 0xC0D00000 */ + *(.boot*) + + /* place the other code and rodata defined BUT nvram variables that are displaced in a r/w area */ + *(.text*) + *(.rodata) + *(.rodata.[^N]*) /*.data.rel.ro* not here to detect invalid PIC usage */ + *(.rodata.N[^_]*) + + . = ALIGN(4); + + /* all code placed */ + _etext = .; + + . = ALIGN(PAGE_SIZE); + + _nvram_data = .; + + /* NVM data (ex-filesystem) */ + *(.bss.N_* .rodata.N_*) + + . = ALIGN(PAGE_SIZE); + _envram_data = .; + + _install_parameters = .; + _nvram_end = .; + } > FLASH = 0x00 + + .data (NOLOAD): + { + . = ALIGN(4); + + /** + * Place RAM initialized variables + */ + _data = .; + + *(vtable) + *(.data*) + + _edata = .; + + } > DISCARD /*> SRAM AT>FLASH = 0x00 */ + + .bss : + { + /** + * Place RAM uninitialized variables + */ + _bss = .; + *(.bss*) + _ebss = .; + + + /** + * Reserve stack size + */ + . = ALIGN(4); + app_stack_canary = .; + PROVIDE(app_stack_canary = .); + . += 4; + _stack_validation = .; + . = _stack_validation + STACK_SIZE; + _stack = ABSOLUTE(END_STACK) - STACK_SIZE; + PROVIDE( _stack = ABSOLUTE(END_STACK) - STACK_SIZE); + _estack = ABSOLUTE(END_STACK); + PROVIDE( _estack = ABSOLUTE(END_STACK) ); + + } > SRAM = 0x00 + + /****************************************************************/ + /* DEBUG */ + /****************************************************************/ + + /* remove the debugging information from the standard libraries */ + DEBUG (NOLOAD) : + { + libc.a ( * ) + libm.a ( * ) + libgcc.a ( * ) + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + } > DISCARD + + /* Stabs debugging sections. */ + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .comment 0 : { *(.comment) } + /* DWARF debug sections. + Symbols in the DWARF debugging sections are relative to the beginning + of the section so we begin them at 0. */ + /* DWARF 1 */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + /* GNU DWARF 1 extensions */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + /* DWARF 1.1 and DWARF 2 */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + /* DWARF 2 */ + .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + /* SGI/MIPS DWARF 2 extensions */ + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } +} + +PROVIDE(_nvram = ABSOLUTE(_nvram_start)); +PROVIDE(_envram = ABSOLUTE(_nvram_end)); \ No newline at end of file diff --git a/app/script_x.ld b/app/script_x.ld new file mode 100644 index 0000000..0d174c4 --- /dev/null +++ b/app/script_x.ld @@ -0,0 +1,175 @@ +/******************************************************************************* +* Ledger Blue - Secure firmware +* (c) 2016, 2017, 2018, 2019 Ledger +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ + +/** + * Global chip memory layout and constants + * + */ + +MEMORY +{ + DISCARD (rwx) : ORIGIN = 0xd0000000, LENGTH = 1M + + FLASH (rx) : ORIGIN = 0xc0de0000, LENGTH = 400K + DATA (r) : ORIGIN = 0xc0de0000, LENGTH = 400K + SRAM (rwx) : ORIGIN = 0xda7a0000, LENGTH = 30K +} + +PAGE_SIZE = 256; +STACK_SIZE = 8192; +END_STACK = ORIGIN(SRAM) + LENGTH(SRAM); + +ENTRY(main) + +SECTIONS +{ + + /****************************************************************/ + /* This section locates the code in FLASH */ + /****************************************************************/ + + /** put text in Flash memory, VMA will be equal to LMA */ + .text : + { + /* provide start code symbol, shall be zero */ + _text = .; + _nvram = .; + PROVIDE(_nvram = . + ORIGIN(FLASH)); + + PROVIDE(_setjmp = setjmp); /*thanks clang*/ + + /* ensure main is always @ 0xC0D00000 */ + *(.boot*) + + /* place the other code and rodata defined BUT nvram variables that are displaced in a r/w area */ + *(.text*) + *(.rodata) + *(.rodata.[^N]*) /*.data.rel.ro* not here to detect invalid PIC usage */ + *(.rodata.N[^_]*) + + . = ALIGN(4); + + /* all code placed */ + _etext = .; + + . = ALIGN(PAGE_SIZE); + + _nvram_data = .; + + /* NVM data (ex-filesystem) */ + *(.bss.N_* .rodata.N_*) + + . = ALIGN(PAGE_SIZE); + _envram_data = .; + /* _nvram_data_size = _envram_data - _nvram_data; */ + _install_parameters = .; + PROVIDE(N_install_parameters = .); + _envram = .; + PROVIDE(_envram = . + ORIGIN(FLASH)); + + } > FLASH = 0x00 + + .data (NOLOAD): + { + . = ALIGN(4); + + /** + * Place RAM initialized variables + */ + _data = .; + + *(vtable) + *(.data*) + + _edata = .; + + } > DISCARD /*> SRAM AT>FLASH = 0x00 */ + + .bss : + { + /** + * Place RAM uninitialized variables + */ + _bss = .; + *(.bss*) + _ebss = .; + + + /** + * Reserve stack size + */ + . = ALIGN(4); + app_stack_canary = .; + PROVIDE(app_stack_canary = .); + . += 4; + _stack_validation = .; + . = _stack_validation + STACK_SIZE; + _stack = ABSOLUTE(END_STACK) - STACK_SIZE; + PROVIDE( _stack = ABSOLUTE(END_STACK) - STACK_SIZE); + _estack = ABSOLUTE(END_STACK); + PROVIDE( _estack = ABSOLUTE(END_STACK) ); + + } > SRAM = 0x00 + + /****************************************************************/ + /* DEBUG */ + /****************************************************************/ + + /* remove the debugging information from the standard libraries */ + DEBUG (NOLOAD) : + { + libc.a ( * ) + libm.a ( * ) + libgcc.a ( * ) + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + } > DISCARD + + /* Stabs debugging sections. */ + .stab 0 : { *(.stab) } + .stabstr 0 : { *(.stabstr) } + .stab.excl 0 : { *(.stab.excl) } + .stab.exclstr 0 : { *(.stab.exclstr) } + .stab.index 0 : { *(.stab.index) } + .stab.indexstr 0 : { *(.stab.indexstr) } + .comment 0 : { *(.comment) } + /* DWARF debug sections. + Symbols in the DWARF debugging sections are relative to the beginning + of the section so we begin them at 0. */ + /* DWARF 1 */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + /* GNU DWARF 1 extensions */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + /* DWARF 1.1 and DWARF 2 */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + /* DWARF 2 */ + .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + /* SGI/MIPS DWARF 2 extensions */ + .debug_weaknames 0 : { *(.debug_weaknames) } + .debug_funcnames 0 : { *(.debug_funcnames) } + .debug_typenames 0 : { *(.debug_typenames) } + .debug_varnames 0 : { *(.debug_varnames) } + +} diff --git a/app/src/addr.c b/app/src/addr.c new file mode 100644 index 0000000..cf87f91 --- /dev/null +++ b/app/src/addr.c @@ -0,0 +1,70 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#include + +#include "app_mode.h" +#include "coin.h" +#include "crypto.h" +#include "zxerror.h" +#include "zxformat.h" +#include "zxmacros.h" +#include "keys_def.h" +#include "os.h" + +zxerr_t addr_getNumItems(uint8_t *num_items) { + if (num_items == NULL) { + return zxerr_no_data; + } + // Display [public address | ivk | ovk | path] + *num_items = 4; + return zxerr_ok; +} + +zxerr_t addr_getItem(int8_t displayIdx, char *outKey, uint16_t outKeyLen, char *outVal, uint16_t outValLen, uint8_t pageIdx, + uint8_t *pageCount) { + ZEMU_LOGF(50, "[addr_getItem] %d/%d\n", displayIdx, pageIdx) + + switch (displayIdx) { + case 0: + snprintf(outKey, outKeyLen, "Address"); + const char* address = (const char*)G_io_apdu_buffer; + pageStringHex(outVal, outValLen, address, KEY_LENGTH, pageIdx, pageCount); + break; + case 1: + snprintf(outKey, outKeyLen, "IVK"); + const char* ivk = (const char*)G_io_apdu_buffer + KEY_LENGTH; + pageStringHex(outVal, outValLen, ivk, KEY_LENGTH, pageIdx, pageCount); + break; + case 2: + snprintf(outKey, outKeyLen, "OVK"); + const char* ovk = (const char*)G_io_apdu_buffer + 2 * KEY_LENGTH; + pageStringHex(outVal, outValLen, ovk, KEY_LENGTH, pageIdx, pageCount); + break; + case 3: { + snprintf(outKey, outKeyLen, "HD Path"); + char buffer[300]; + bip32_to_str(buffer, sizeof(buffer), hdPath, HDPATH_LEN_DEFAULT); + pageString(outVal, outValLen, buffer, pageIdx, pageCount); + break; + } + + default: + return zxerr_no_data; + } + + return zxerr_ok; +} diff --git a/app/src/addr.h b/app/src/addr.h new file mode 100644 index 0000000..2245856 --- /dev/null +++ b/app/src/addr.h @@ -0,0 +1,32 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * 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. + ********************************************************************************/ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +// Return the number of items in the address view +zxerr_t addr_getNumItems(uint8_t *num_items); + +// Gets an specific item from the address view (including paging) +zxerr_t addr_getItem(int8_t displayIdx, char *outKey, uint16_t outKeyLen, char *outValue, uint16_t outValueLen, + uint8_t pageIdx, uint8_t *pageCount); + +#ifdef __cplusplus +} +#endif diff --git a/app/src/apdu_handler.c b/app/src/apdu_handler.c new file mode 100644 index 0000000..fedaafb --- /dev/null +++ b/app/src/apdu_handler.c @@ -0,0 +1,223 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * (c) 2016 Ledger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#include +#include +#include +#include + +#include "actions.h" +#include "addr.h" +#include "app_main.h" +#include "coin.h" +#include "crypto.h" +#include "tx.h" +#include "view.h" +#include "view_internal.h" +#include "zxmacros.h" + +static bool tx_initialized = false; + +void extractHDPath(uint32_t rx, uint32_t offset) { + tx_initialized = false; + + if ((rx - offset) < sizeof(uint32_t) * HDPATH_LEN_DEFAULT) { + THROW(APDU_CODE_WRONG_LENGTH); + } + + memcpy(hdPath, G_io_apdu_buffer + offset, sizeof(uint32_t) * HDPATH_LEN_DEFAULT); + + // #{TODO} --> testnet necessary? + const bool mainnet = hdPath[0] == HDPATH_0_DEFAULT && hdPath[1] == HDPATH_1_DEFAULT; + + if (!mainnet) { + THROW(APDU_CODE_DATA_INVALID); + } +} + +__Z_INLINE bool process_chunk(__Z_UNUSED volatile uint32_t *tx, uint32_t rx) { + const uint8_t payloadType = G_io_apdu_buffer[OFFSET_PAYLOAD_TYPE]; + if (rx < OFFSET_DATA) { + THROW(APDU_CODE_WRONG_LENGTH); + } + + uint32_t added; + switch (payloadType) { + case P1_INIT: + tx_initialize(); + tx_reset(); + extractHDPath(rx, OFFSET_DATA); + tx_initialized = true; + return false; + case P1_ADD: + if (!tx_initialized) { + THROW(APDU_CODE_TX_NOT_INITIALIZED); + } + added = tx_append(&(G_io_apdu_buffer[OFFSET_DATA]), rx - OFFSET_DATA); + if (added != rx - OFFSET_DATA) { + tx_initialized = false; + THROW(APDU_CODE_OUTPUT_BUFFER_TOO_SMALL); + } + return false; + case P1_LAST: + if (!tx_initialized) { + THROW(APDU_CODE_TX_NOT_INITIALIZED); + } + added = tx_append(&(G_io_apdu_buffer[OFFSET_DATA]), rx - OFFSET_DATA); + tx_initialized = false; + if (added != rx - OFFSET_DATA) { + tx_initialized = false; + THROW(APDU_CODE_OUTPUT_BUFFER_TOO_SMALL); + } + tx_initialized = false; + return true; + } + + THROW(APDU_CODE_INVALIDP1P2); +} + +__Z_INLINE void handleGetAddr(volatile uint32_t *flags, volatile uint32_t *tx, uint32_t rx) { + extractHDPath(rx, OFFSET_DATA); + + const uint8_t requireConfirmation = G_io_apdu_buffer[OFFSET_P1]; + zxerr_t zxerr = app_fill_address(); + if (zxerr != zxerr_ok) { + *tx = 0; + THROW(APDU_CODE_DATA_INVALID); + } + if (requireConfirmation) { + view_review_init(addr_getItem, addr_getNumItems, app_reply_address); + view_review_show(REVIEW_ADDRESS); + *flags |= IO_ASYNCH_REPLY; + return; + } + *tx = action_addrResponseLen; + THROW(APDU_CODE_OK); +} + +__Z_INLINE void handleSign(volatile uint32_t *flags, volatile uint32_t *tx, uint32_t rx) { + zemu_log("handleSign\n"); + if (!process_chunk(tx, rx)) { + THROW(APDU_CODE_OK); + } + + const char *error_msg = tx_parse(); + CHECK_APP_CANARY() + if (error_msg != NULL) { + const int error_msg_length = strnlen(error_msg, sizeof(G_io_apdu_buffer)); + memcpy(G_io_apdu_buffer, error_msg, error_msg_length); + *tx += (error_msg_length); + THROW(APDU_CODE_DATA_INVALID); + } + + view_review_init(tx_getItem, tx_getNumItems, app_sign); + view_review_show(REVIEW_TXN); + *flags |= IO_ASYNCH_REPLY; +} + +__Z_INLINE void handle_getversion(__Z_UNUSED volatile uint32_t *flags, volatile uint32_t *tx) { + G_io_apdu_buffer[0] = 0; + +#if defined(APP_TESTING) + G_io_apdu_buffer[0] = 0x01; +#endif + + G_io_apdu_buffer[1] = (LEDGER_MAJOR_VERSION >> 8) & 0xFF; + G_io_apdu_buffer[2] = (LEDGER_MAJOR_VERSION >> 0) & 0xFF; + + G_io_apdu_buffer[3] = (LEDGER_MINOR_VERSION >> 8) & 0xFF; + G_io_apdu_buffer[4] = (LEDGER_MINOR_VERSION >> 0) & 0xFF; + + G_io_apdu_buffer[5] = (LEDGER_PATCH_VERSION >> 8) & 0xFF; + G_io_apdu_buffer[6] = (LEDGER_PATCH_VERSION >> 0) & 0xFF; + + G_io_apdu_buffer[7] = !IS_UX_ALLOWED; + + G_io_apdu_buffer[8] = (TARGET_ID >> 24) & 0xFF; + G_io_apdu_buffer[9] = (TARGET_ID >> 16) & 0xFF; + G_io_apdu_buffer[10] = (TARGET_ID >> 8) & 0xFF; + G_io_apdu_buffer[11] = (TARGET_ID >> 0) & 0xFF; + + *tx += 12; + THROW(APDU_CODE_OK); +} + +#if defined(APP_TESTING) +void handleTest(volatile uint32_t *flags, volatile uint32_t *tx, uint32_t rx) { THROW(APDU_CODE_OK); } +#endif + +void handleApdu(volatile uint32_t *flags, volatile uint32_t *tx, uint32_t rx) { + volatile uint16_t sw = 0; + + BEGIN_TRY { + TRY { + if (G_io_apdu_buffer[OFFSET_CLA] != CLA) { + THROW(APDU_CODE_CLA_NOT_SUPPORTED); + } + + if (rx < APDU_MIN_LENGTH) { + THROW(APDU_CODE_WRONG_LENGTH); + } + + switch (G_io_apdu_buffer[OFFSET_INS]) { + case INS_GET_VERSION: { + handle_getversion(flags, tx); + break; + } + + case INS_GET_ADDR: { + CHECK_PIN_VALIDATED() + handleGetAddr(flags, tx, rx); + break; + } + + case INS_SIGN: { + CHECK_PIN_VALIDATED() + handleSign(flags, tx, rx); + break; + } + +#if defined(APP_TESTING) + case INS_TEST: { + handleTest(flags, tx, rx); + THROW(APDU_CODE_OK); + break; + } +#endif + default: + THROW(APDU_CODE_INS_NOT_SUPPORTED); + } + } + CATCH(EXCEPTION_IO_RESET) { THROW(EXCEPTION_IO_RESET); } + CATCH_OTHER(e) { + switch (e & 0xF000) { + case 0x6000: + case APDU_CODE_OK: + sw = e; + break; + default: + sw = 0x6800 | (e & 0x7FF); + break; + } + G_io_apdu_buffer[*tx] = sw >> 8; + G_io_apdu_buffer[*tx + 1] = sw & 0xFF; + *tx += 2; + } + FINALLY {} + } + END_TRY; +} diff --git a/app/src/blake2s/blake2-impl.h b/app/src/blake2s/blake2-impl.h new file mode 100644 index 0000000..c1df82e --- /dev/null +++ b/app/src/blake2s/blake2-impl.h @@ -0,0 +1,160 @@ +/* + BLAKE2 reference source code package - reference C implementations + + Copyright 2012, Samuel Neves . You may use this under the + terms of the CC0, the OpenSSL Licence, or the Apache Public License 2.0, at + your option. The terms of these licenses can be found at: + + - CC0 1.0 Universal : http://creativecommons.org/publicdomain/zero/1.0 + - OpenSSL license : https://www.openssl.org/source/license.html + - Apache 2.0 : http://www.apache.org/licenses/LICENSE-2.0 + + More information about the BLAKE2 hash function can be found at + https://blake2.net. +*/ +#ifndef BLAKE2_IMPL_H +#define BLAKE2_IMPL_H + +#include +#include + +#if !defined(__cplusplus) && (!defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L) + #if defined(_MSC_VER) + #define BLAKE2_INLINE __inline + #elif defined(__GNUC__) + #define BLAKE2_INLINE __inline__ + #else + #define BLAKE2_INLINE + #endif +#else + #define BLAKE2_INLINE inline +#endif + +static BLAKE2_INLINE uint32_t load32( const void *src ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + uint32_t w; + memcpy(&w, src, sizeof w); + return w; +#else + const uint8_t *p = ( const uint8_t * )src; + return (( uint32_t )( p[0] ) << 0) | + (( uint32_t )( p[1] ) << 8) | + (( uint32_t )( p[2] ) << 16) | + (( uint32_t )( p[3] ) << 24) ; +#endif +} + +static BLAKE2_INLINE uint64_t load64( const void *src ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + uint64_t w; + memcpy(&w, src, sizeof w); + return w; +#else + const uint8_t *p = ( const uint8_t * )src; + return (( uint64_t )( p[0] ) << 0) | + (( uint64_t )( p[1] ) << 8) | + (( uint64_t )( p[2] ) << 16) | + (( uint64_t )( p[3] ) << 24) | + (( uint64_t )( p[4] ) << 32) | + (( uint64_t )( p[5] ) << 40) | + (( uint64_t )( p[6] ) << 48) | + (( uint64_t )( p[7] ) << 56) ; +#endif +} + +static BLAKE2_INLINE uint16_t load16( const void *src ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + uint16_t w; + memcpy(&w, src, sizeof w); + return w; +#else + const uint8_t *p = ( const uint8_t * )src; + return ( uint16_t )((( uint32_t )( p[0] ) << 0) | + (( uint32_t )( p[1] ) << 8)); +#endif +} + +static BLAKE2_INLINE void store16( void *dst, uint16_t w ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + memcpy(dst, &w, sizeof w); +#else + uint8_t *p = ( uint8_t * )dst; + *p++ = ( uint8_t )w; w >>= 8; + *p++ = ( uint8_t )w; +#endif +} + +static BLAKE2_INLINE void store32( void *dst, uint32_t w ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + memcpy(dst, &w, sizeof w); +#else + uint8_t *p = ( uint8_t * )dst; + p[0] = (uint8_t)(w >> 0); + p[1] = (uint8_t)(w >> 8); + p[2] = (uint8_t)(w >> 16); + p[3] = (uint8_t)(w >> 24); +#endif +} + +static BLAKE2_INLINE void store64( void *dst, uint64_t w ) +{ +#if defined(NATIVE_LITTLE_ENDIAN) + memcpy(dst, &w, sizeof w); +#else + uint8_t *p = ( uint8_t * )dst; + p[0] = (uint8_t)(w >> 0); + p[1] = (uint8_t)(w >> 8); + p[2] = (uint8_t)(w >> 16); + p[3] = (uint8_t)(w >> 24); + p[4] = (uint8_t)(w >> 32); + p[5] = (uint8_t)(w >> 40); + p[6] = (uint8_t)(w >> 48); + p[7] = (uint8_t)(w >> 56); +#endif +} + +static BLAKE2_INLINE uint64_t load48( const void *src ) +{ + const uint8_t *p = ( const uint8_t * )src; + return (( uint64_t )( p[0] ) << 0) | + (( uint64_t )( p[1] ) << 8) | + (( uint64_t )( p[2] ) << 16) | + (( uint64_t )( p[3] ) << 24) | + (( uint64_t )( p[4] ) << 32) | + (( uint64_t )( p[5] ) << 40) ; +} + +static BLAKE2_INLINE void store48( void *dst, uint64_t w ) +{ + uint8_t *p = ( uint8_t * )dst; + p[0] = (uint8_t)(w >> 0); + p[1] = (uint8_t)(w >> 8); + p[2] = (uint8_t)(w >> 16); + p[3] = (uint8_t)(w >> 24); + p[4] = (uint8_t)(w >> 32); + p[5] = (uint8_t)(w >> 40); +} + +static BLAKE2_INLINE uint32_t rotr32( const uint32_t w, const unsigned c ) +{ + return ( w >> c ) | ( w << ( 32 - c ) ); +} + +static BLAKE2_INLINE uint64_t rotr64( const uint64_t w, const unsigned c ) +{ + return ( w >> c ) | ( w << ( 64 - c ) ); +} + +/* prevents compiler optimizing out memset() */ +static BLAKE2_INLINE void secure_zero_memory(void *v, size_t n) +{ + static void *(*const volatile memset_v)(void *, int, size_t) = &memset; + memset_v(v, 0, n); +} + +#endif diff --git a/app/src/blake2s/blake2.h b/app/src/blake2s/blake2.h new file mode 100644 index 0000000..845226e --- /dev/null +++ b/app/src/blake2s/blake2.h @@ -0,0 +1,128 @@ +/* + BLAKE2 reference source code package - reference C implementations + + Copyright 2012, Samuel Neves . You may use this under the + terms of the CC0, the OpenSSL Licence, or the Apache Public License 2.0, at + your option. The terms of these licenses can be found at: + + - CC0 1.0 Universal : http://creativecommons.org/publicdomain/zero/1.0 + - OpenSSL license : https://www.openssl.org/source/license.html + - Apache 2.0 : http://www.apache.org/licenses/LICENSE-2.0 + + More information about the BLAKE2 hash function can be found at + https://blake2.net. +*/ +#ifndef BLAKE2_H +#define BLAKE2_H + +#include +#include + +#if defined(_MSC_VER) +#define BLAKE2_PACKED(x) __pragma(pack(push, 1)) x __pragma(pack(pop)) +#else +#define BLAKE2_PACKED(x) x __attribute__((packed)) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + + enum blake2s_constant + { + BLAKE2S_BLOCKBYTES = 64, + BLAKE2S_OUTBYTES = 32, + BLAKE2S_KEYBYTES = 32, + BLAKE2S_SALTBYTES = 8, + BLAKE2S_PERSONALBYTES = 8 + }; + + typedef struct blake2s_state__ + { + uint32_t h[8]; + uint32_t t[2]; + uint32_t f[2]; + uint8_t buf[BLAKE2S_BLOCKBYTES]; + size_t buflen; + size_t outlen; + uint8_t last_node; + } blake2s_state; + + typedef struct blake2sp_state__ + { + blake2s_state S[8][1]; + blake2s_state R[1]; + uint8_t buf[8 * BLAKE2S_BLOCKBYTES]; + size_t buflen; + size_t outlen; + } blake2sp_state; + + BLAKE2_PACKED(struct blake2s_param__ + { + uint8_t digest_length; /* 1 */ + uint8_t key_length; /* 2 */ + uint8_t fanout; /* 3 */ + uint8_t depth; /* 4 */ + uint32_t leaf_length; /* 8 */ + uint32_t node_offset; /* 12 */ + uint16_t xof_length; /* 14 */ + uint8_t node_depth; /* 15 */ + uint8_t inner_length; /* 16 */ + /* uint8_t reserved[0]; */ + uint8_t salt[BLAKE2S_SALTBYTES]; /* 24 */ + uint8_t personal[BLAKE2S_PERSONALBYTES]; /* 32 */ + }); + + typedef struct blake2s_param__ blake2s_param; + + typedef struct blake2b_param__ blake2b_param; + + typedef struct blake2xs_state__ + { + blake2s_state S[1]; + blake2s_param P[1]; + } blake2xs_state; + + /* Padded structs result in a compile-time error */ + enum { + BLAKE2_DUMMY_1 = 1/(int)(sizeof(blake2s_param) == BLAKE2S_OUTBYTES), + // BLAKE2_DUMMY_2 = 1/(int)(sizeof(blake2b_param) == BLAKE2B_OUTBYTES) + }; + + /* Streaming API */ + int blake2s_init( blake2s_state *S, size_t outlen ); + int blake2s_init_key( blake2s_state *S, size_t outlen, const void *key, size_t keylen ); + int blake2s_init_param( blake2s_state *S, const blake2s_param *P ); + int blake2s_update( blake2s_state *S, const void *in, size_t inlen ); + int blake2s_final( blake2s_state *S, void *out, size_t outlen ); + + int blake2sp_init( blake2sp_state *S, size_t outlen ); + int blake2sp_init_key( blake2sp_state *S, size_t outlen, const void *key, size_t keylen ); + int blake2s_init_with_personalization( blake2s_state *S, size_t outlen, const uint8_t* personalization, uint8_t personalizationlen ); + int blake2sp_update( blake2sp_state *S, const void *in, size_t inlen ); + int blake2sp_final( blake2sp_state *S, void *out, size_t outlen ); + + /* Variable output length API */ + int blake2xs_init( blake2xs_state *S, const size_t outlen ); + int blake2xs_init_key( blake2xs_state *S, const size_t outlen, const void *key, size_t keylen ); + int blake2xs_update( blake2xs_state *S, const void *in, size_t inlen ); + int blake2xs_final(blake2xs_state *S, void *out, size_t outlen); + + /* Simple API */ + int blake2s( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + int blake2b( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + + int blake2sp( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + int blake2bp( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + + int blake2xs( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + int blake2xb( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + + /* This is simply an alias for blake2b */ + int blake2( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/app/src/blake2s/blake2s-ref.c b/app/src/blake2s/blake2s-ref.c new file mode 100644 index 0000000..84fd5cf --- /dev/null +++ b/app/src/blake2s/blake2s-ref.c @@ -0,0 +1,395 @@ +/* + Oficial BLAKE2 implementation tweak by Zondax AG + + BLAKE2 reference source code package - reference C implementations + + Copyright 2012, Samuel Neves . You may use this under the + terms of the CC0, the OpenSSL Licence, or the Apache Public License 2.0, at + your option. The terms of these licenses can be found at: + + - CC0 1.0 Universal : http://creativecommons.org/publicdomain/zero/1.0 + - OpenSSL license : https://www.openssl.org/source/license.html + - Apache 2.0 : http://www.apache.org/licenses/LICENSE-2.0 + + More information about the BLAKE2 hash function can be found at + https://blake2.net. +*/ + +#include +#include +#include + +#include "blake2.h" +#include "blake2-impl.h" + +static const uint32_t blake2s_IV[8] = +{ + 0x6A09E667UL, 0xBB67AE85UL, 0x3C6EF372UL, 0xA54FF53AUL, + 0x510E527FUL, 0x9B05688CUL, 0x1F83D9ABUL, 0x5BE0CD19UL +}; + +static const uint8_t blake2s_sigma[10][16] = +{ + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } , + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 } , + { 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 } , + { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 } , + { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 } , + { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 } , + { 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 } , + { 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 } , + { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 } , + { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13 , 0 } , +}; + +static void blake2s_set_lastnode( blake2s_state *S ) +{ + S->f[1] = (uint32_t)-1; +} + +/* Some helper functions, not necessarily useful */ +static int blake2s_is_lastblock( const blake2s_state *S ) +{ + return S->f[0] != 0; +} + +static void blake2s_set_lastblock( blake2s_state *S ) +{ + if( S->last_node ) blake2s_set_lastnode( S ); + + S->f[0] = (uint32_t)-1; +} + +static void blake2s_increment_counter( blake2s_state *S, const uint32_t inc ) +{ + S->t[0] += inc; + S->t[1] += ( S->t[0] < inc ); +} + +static void blake2s_init0( blake2s_state *S ) +{ + size_t i; + memset( S, 0, sizeof( blake2s_state ) ); + + for( i = 0; i < 8; ++i ) S->h[i] = blake2s_IV[i]; +} + +/* init2 xors IV with input parameter block */ +int blake2s_init_param( blake2s_state *S, const blake2s_param *P ) +{ + const unsigned char *p = ( const unsigned char * )( P ); + size_t i; + + blake2s_init0( S ); + + /* IV XOR ParamBlock */ + for( i = 0; i < 8; ++i ) + S->h[i] ^= load32( &p[i * 4] ); + + S->outlen = P->digest_length; + return 0; +} + + +/* Sequential blake2s initialization */ +int blake2s_init( blake2s_state *S, size_t outlen ) +{ + blake2s_param P[1]; + + /* Move interval verification here? */ + if ( ( !outlen ) || ( outlen > BLAKE2S_OUTBYTES ) ) return -1; + + P->digest_length = (uint8_t)outlen; + P->key_length = 0; + P->fanout = 1; + P->depth = 1; + store32( &P->leaf_length, 0 ); + store32( &P->node_offset, 0 ); + store16( &P->xof_length, 0 ); + P->node_depth = 0; + P->inner_length = 0; + /* memset(P->reserved, 0, sizeof(P->reserved) ); */ + memset( P->salt, 0, sizeof( P->salt ) ); + memset( P->personal, 0, sizeof( P->personal ) ); + return blake2s_init_param( S, P ); +} + +/* blake2s init with personalization */ +int blake2s_init_with_personalization( blake2s_state *S, size_t outlen, const uint8_t* personalization, uint8_t personalizationlen ) +{ + blake2s_param P[1]; + + /* Move interval verification here? */ + if ( ( !outlen ) || ( outlen > BLAKE2S_OUTBYTES ) ) return -1; + if ( ( !personalization ) || ( personalizationlen > sizeof(P->personal) ) ) return -1; + + P->digest_length = (uint8_t)outlen; + P->key_length = 0; + P->fanout = 1; + P->depth = 1; + store32( &P->leaf_length, 0 ); + store32( &P->node_offset, 0 ); + store16( &P->xof_length, 0 ); + P->node_depth = 0; + P->inner_length = 0; + /* memset(P->reserved, 0, sizeof(P->reserved) ); */ + memset( P->salt, 0, sizeof( P->salt ) ); + memset( P->personal, 0, sizeof( P->personal ) ); + memcpy(P->personal, personalization, personalizationlen); + + return blake2s_init_param( S, P ); +} + +int blake2s_init_key( blake2s_state *S, size_t outlen, const void *key, size_t keylen ) +{ + blake2s_param P[1]; + + if ( ( !outlen ) || ( outlen > BLAKE2S_OUTBYTES ) ) return -1; + + if ( !key || !keylen || keylen > BLAKE2S_KEYBYTES ) return -1; + + P->digest_length = (uint8_t)outlen; + P->key_length = (uint8_t)keylen; + P->fanout = 1; + P->depth = 1; + store32( &P->leaf_length, 0 ); + store32( &P->node_offset, 0 ); + store16( &P->xof_length, 0 ); + P->node_depth = 0; + P->inner_length = 0; + /* memset(P->reserved, 0, sizeof(P->reserved) ); */ + memset( P->salt, 0, sizeof( P->salt ) ); + memset( P->personal, 0, sizeof( P->personal ) ); + + if( blake2s_init_param( S, P ) < 0 ) return -1; + + { + uint8_t block[BLAKE2S_BLOCKBYTES]; + memset( block, 0, BLAKE2S_BLOCKBYTES ); + memcpy( block, key, keylen ); + blake2s_update( S, block, BLAKE2S_BLOCKBYTES ); + secure_zero_memory( block, BLAKE2S_BLOCKBYTES ); /* Burn the key from stack */ + } + return 0; +} + +#define G(r,i,a,b,c,d) \ + do { \ + a = a + b + m[blake2s_sigma[r][2*i+0]]; \ + d = rotr32(d ^ a, 16); \ + c = c + d; \ + b = rotr32(b ^ c, 12); \ + a = a + b + m[blake2s_sigma[r][2*i+1]]; \ + d = rotr32(d ^ a, 8); \ + c = c + d; \ + b = rotr32(b ^ c, 7); \ + } while(0) + +#define ROUND(r) \ + do { \ + G(r,0,v[ 0],v[ 4],v[ 8],v[12]); \ + G(r,1,v[ 1],v[ 5],v[ 9],v[13]); \ + G(r,2,v[ 2],v[ 6],v[10],v[14]); \ + G(r,3,v[ 3],v[ 7],v[11],v[15]); \ + G(r,4,v[ 0],v[ 5],v[10],v[15]); \ + G(r,5,v[ 1],v[ 6],v[11],v[12]); \ + G(r,6,v[ 2],v[ 7],v[ 8],v[13]); \ + G(r,7,v[ 3],v[ 4],v[ 9],v[14]); \ + } while(0) + +static void blake2s_compress( blake2s_state *S, const uint8_t in[BLAKE2S_BLOCKBYTES] ) +{ + uint32_t m[16]; + uint32_t v[16]; + size_t i; + + for( i = 0; i < 16; ++i ) { + m[i] = load32( in + i * sizeof( m[i] ) ); + } + + for( i = 0; i < 8; ++i ) { + v[i] = S->h[i]; + } + + v[ 8] = blake2s_IV[0]; + v[ 9] = blake2s_IV[1]; + v[10] = blake2s_IV[2]; + v[11] = blake2s_IV[3]; + v[12] = S->t[0] ^ blake2s_IV[4]; + v[13] = S->t[1] ^ blake2s_IV[5]; + v[14] = S->f[0] ^ blake2s_IV[6]; + v[15] = S->f[1] ^ blake2s_IV[7]; + + ROUND( 0 ); + ROUND( 1 ); + ROUND( 2 ); + ROUND( 3 ); + ROUND( 4 ); + ROUND( 5 ); + ROUND( 6 ); + ROUND( 7 ); + ROUND( 8 ); + ROUND( 9 ); + + for( i = 0; i < 8; ++i ) { + S->h[i] = S->h[i] ^ v[i] ^ v[i + 8]; + } +} + +#undef G +#undef ROUND + +int blake2s_update( blake2s_state *S, const void *pin, size_t inlen ) +{ + const unsigned char * in = (const unsigned char *)pin; + if( inlen > 0 ) + { + size_t left = S->buflen; + size_t fill = BLAKE2S_BLOCKBYTES - left; + if( inlen > fill ) + { + S->buflen = 0; + memcpy( S->buf + left, in, fill ); /* Fill buffer */ + blake2s_increment_counter( S, BLAKE2S_BLOCKBYTES ); + blake2s_compress( S, S->buf ); /* Compress */ + in += fill; inlen -= fill; + while(inlen > BLAKE2S_BLOCKBYTES) { + blake2s_increment_counter(S, BLAKE2S_BLOCKBYTES); + blake2s_compress( S, in ); + in += BLAKE2S_BLOCKBYTES; + inlen -= BLAKE2S_BLOCKBYTES; + } + } + memcpy( S->buf + S->buflen, in, inlen ); + S->buflen += inlen; + } + return 0; +} + +int blake2s_final( blake2s_state *S, void *out, size_t outlen ) +{ + uint8_t buffer[BLAKE2S_OUTBYTES] = {0}; + size_t i; + + if( out == NULL || outlen < S->outlen ) + return -1; + + if( blake2s_is_lastblock( S ) ) + return -1; + + blake2s_increment_counter( S, ( uint32_t )S->buflen ); + blake2s_set_lastblock( S ); + memset( S->buf + S->buflen, 0, BLAKE2S_BLOCKBYTES - S->buflen ); /* Padding */ + blake2s_compress( S, S->buf ); + + for( i = 0; i < 8; ++i ) /* Output full hash to temp buffer */ + store32( buffer + sizeof( S->h[i] ) * i, S->h[i] ); + + memcpy( out, buffer, outlen ); + memset(buffer, 0 ,sizeof(buffer)); + return 0; +} + +int blake2s( void *out, size_t outlen, const void *in, size_t inlen, const void *key, size_t keylen ) +{ + blake2s_state S[1]; + + /* Verify parameters */ + if ( NULL == in && inlen > 0 ) return -1; + + if ( NULL == out ) return -1; + + if ( NULL == key && keylen > 0) return -1; + + if( !outlen || outlen > BLAKE2S_OUTBYTES ) return -1; + + if( keylen > BLAKE2S_KEYBYTES ) return -1; + + if( keylen > 0 ) + { + if( blake2s_init_key( S, outlen, key, keylen ) < 0 ) return -1; + } + else + { + if( blake2s_init( S, outlen ) < 0 ) return -1; + } + + blake2s_update( S, ( const uint8_t * )in, inlen ); + blake2s_final( S, out, outlen ); + return 0; +} + +#if defined(SUPERCOP) +int crypto_hash( unsigned char *out, unsigned char *in, unsigned long long inlen ) +{ + return blake2s( out, BLAKE2S_OUTBYTES, in, inlen, NULL, 0 ); +} +#endif + +#if defined(BLAKE2S_SELFTEST) +#include +#include "blake2-kat.h" +int main( void ) +{ + uint8_t key[BLAKE2S_KEYBYTES]; + uint8_t buf[BLAKE2_KAT_LENGTH]; + size_t i, step; + + for( i = 0; i < BLAKE2S_KEYBYTES; ++i ) + key[i] = ( uint8_t )i; + + for( i = 0; i < BLAKE2_KAT_LENGTH; ++i ) + buf[i] = ( uint8_t )i; + + /* Test simple API */ + for( i = 0; i < BLAKE2_KAT_LENGTH; ++i ) + { + uint8_t hash[BLAKE2S_OUTBYTES]; + blake2s( hash, BLAKE2S_OUTBYTES, buf, i, key, BLAKE2S_KEYBYTES ); + + if( 0 != memcmp( hash, blake2s_keyed_kat[i], BLAKE2S_OUTBYTES ) ) + { + goto fail; + } + } + + /* Test streaming API */ + for(step = 1; step < BLAKE2S_BLOCKBYTES; ++step) { + for (i = 0; i < BLAKE2_KAT_LENGTH; ++i) { + uint8_t hash[BLAKE2S_OUTBYTES]; + blake2s_state S; + uint8_t * p = buf; + size_t mlen = i; + int err = 0; + + if( (err = blake2s_init_key(&S, BLAKE2S_OUTBYTES, key, BLAKE2S_KEYBYTES)) < 0 ) { + goto fail; + } + + while (mlen >= step) { + if ( (err = blake2s_update(&S, p, step)) < 0 ) { + goto fail; + } + mlen -= step; + p += step; + } + if ( (err = blake2s_update(&S, p, mlen)) < 0) { + goto fail; + } + if ( (err = blake2s_final(&S, hash, BLAKE2S_OUTBYTES)) < 0) { + goto fail; + } + + if (0 != memcmp(hash, blake2s_keyed_kat[i], BLAKE2S_OUTBYTES)) { + goto fail; + } + } + } + + puts( "ok" ); + return 0; +fail: + puts("error"); + return -1; +} +#endif diff --git a/app/src/coin.h b/app/src/coin.h new file mode 100644 index 0000000..2498756 --- /dev/null +++ b/app/src/coin.h @@ -0,0 +1,54 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * 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. + ********************************************************************************/ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +// #{TODO} ---> Replace CLA, Token symbol, HDPATH, etc etc +#define CLA 0x80 + +#define HDPATH_LEN_DEFAULT 5 +#define HDPATH_0_DEFAULT (0x80000000u | 0x2c) // 44 +#define HDPATH_1_DEFAULT (0x80000000u | 0x85) // 133 + +#define HDPATH_2_DEFAULT (0x80000000u | 0u) +#define HDPATH_3_DEFAULT (0u) +#define HDPATH_4_DEFAULT (0u) + +#define SECP256K1_PK_LEN 65u + +#define SK_LEN_25519 64u +#define SCALAR_LEN_ED25519 32u +#define SIG_PLUS_TYPE_LEN 65u + +#define ED25519_SIGNATURE_SIZE 64u + +#define PK_LEN_25519 32u + +#define COIN_AMOUNT_DECIMAL_PLACES 6 +#define COIN_TICKER "IRON " + +#define MENU_MAIN_APP_LINE1 "Ironfish" +#define MENU_MAIN_APP_LINE2 "Ready" +#define MENU_MAIN_APP_LINE2_SECRET "???" +#define APPVERSION_LINE1 "Ironfish" +#define APPVERSION_LINE2 "v" APPVERSION + +#ifdef __cplusplus +} +#endif diff --git a/app/src/common/actions.c b/app/src/common/actions.c new file mode 100644 index 0000000..a006e3f --- /dev/null +++ b/app/src/common/actions.c @@ -0,0 +1,19 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#include "actions.h" + +uint16_t action_addrResponseLen; diff --git a/app/src/common/actions.h b/app/src/common/actions.h new file mode 100644 index 0000000..ec2da6d --- /dev/null +++ b/app/src/common/actions.h @@ -0,0 +1,72 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * 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. + ********************************************************************************/ +#pragma once + +#include +#include + +#include "apdu_codes.h" +#include "coin.h" +#include "crypto.h" +#include "tx.h" +#include "zxerror.h" + +extern uint16_t action_addrResponseLen; + +__Z_INLINE zxerr_t app_fill_address() { + // Put data directly in the apdu buffer + MEMZERO(G_io_apdu_buffer, IO_APDU_BUFFER_SIZE); + + action_addrResponseLen = 0; + const zxerr_t err = crypto_fillAddress(G_io_apdu_buffer, IO_APDU_BUFFER_SIZE, &action_addrResponseLen); + + if (err != zxerr_ok || action_addrResponseLen == 0) { + THROW(APDU_CODE_EXECUTION_ERROR); + } + + return zxerr_ok; +} + +__Z_INLINE void app_sign() { + const uint8_t *message = tx_get_buffer(); + const uint16_t messageLength = tx_get_buffer_length(); + + const zxerr_t err = crypto_sign(G_io_apdu_buffer, IO_APDU_BUFFER_SIZE - 3, message, messageLength); + + if (err != zxerr_ok) { + set_code(G_io_apdu_buffer, 0, APDU_CODE_SIGN_VERIFY_ERROR); + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 2); + } else { + set_code(G_io_apdu_buffer, SK_LEN_25519, APDU_CODE_OK); + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, SK_LEN_25519 + 2); + } +} + +__Z_INLINE void app_reject() { + MEMZERO(G_io_apdu_buffer, IO_APDU_BUFFER_SIZE); + set_code(G_io_apdu_buffer, 0, APDU_CODE_COMMAND_NOT_ALLOWED); + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 2); +} + +__Z_INLINE void app_reply_address() { + set_code(G_io_apdu_buffer, action_addrResponseLen, APDU_CODE_OK); + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, action_addrResponseLen + 2); +} + +__Z_INLINE void app_reply_error() { + set_code(G_io_apdu_buffer, 0, APDU_CODE_DATA_INVALID); + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, 2); +} diff --git a/app/src/common/main.c b/app/src/common/main.c new file mode 100644 index 0000000..901cb50 --- /dev/null +++ b/app/src/common/main.c @@ -0,0 +1,38 @@ +/******************************************************************************* + * (c) 2016 Ledger + * (c) 2018 - 2023 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ +#include + +#include "app_main.h" +#include "view.h" + +__attribute__((section(".boot"))) int main(void) { + // exit critical section + __asm volatile("cpsie i"); + + view_init(); + os_boot(); + + BEGIN_TRY { + TRY { + app_init(); + app_main(); + } + CATCH_OTHER(e) {} + FINALLY {} + } + END_TRY; +} diff --git a/app/src/common/parser.h b/app/src/common/parser.h new file mode 100644 index 0000000..07b9012 --- /dev/null +++ b/app/src/common/parser.h @@ -0,0 +1,43 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * 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. + ********************************************************************************/ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "parser_impl.h" + +const char *parser_getErrorDescription(parser_error_t err); +const char *parser_getMsgPackTypeDescription(uint8_t type); + +//// parses a tx buffer +parser_error_t parser_parse(parser_context_t *ctx, const uint8_t *data, size_t dataLen, parser_tx_t *tx_obj); + +//// verifies tx fields +parser_error_t parser_validate(parser_context_t *ctx); + +//// returns the number of items in the current parsing context +parser_error_t parser_getNumItems(const parser_context_t *ctx, uint8_t *num_items); + +// retrieves a readable output for each field / page +parser_error_t parser_getItem(const parser_context_t *ctx, uint8_t displayIdx, char *outKey, uint16_t outKeyLen, + char *outVal, uint16_t outValLen, uint8_t pageIdx, uint8_t *pageCount); + +#ifdef __cplusplus +} +#endif diff --git a/app/src/common/parser_common.h b/app/src/common/parser_common.h new file mode 100644 index 0000000..cc09e98 --- /dev/null +++ b/app/src/common/parser_common.h @@ -0,0 +1,69 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * 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. + ********************************************************************************/ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "parser_txdef.h" + +#define CHECK_ERROR(__CALL) \ + { \ + parser_error_t __err = __CALL; \ + CHECK_APP_CANARY() \ + if (__err != parser_ok) return __err; \ + } + +typedef enum { + // Generic errors + parser_ok = 0, + parser_no_data, + parser_init_context_empty, + parser_display_idx_out_of_range, + parser_display_page_out_of_range, + parser_unexpected_error, + + // Coin generic + parser_unexpected_type, + parser_unexpected_method, + parser_unexpected_buffer_end, + parser_unexpected_value, + parser_unexpected_number_items, + parser_unexpected_version, + parser_unexpected_characters, + parser_unexpected_field, + parser_duplicated_field, + parser_value_out_of_range, + parser_invalid_address, + parser_unexpected_chain, + parser_missing_field, + paser_unknown_transaction, +} parser_error_t; + +typedef struct { + const uint8_t *buffer; + uint16_t bufferLen; + uint16_t offset; + parser_tx_t *tx_obj; +} parser_context_t; + +#ifdef __cplusplus +} +#endif diff --git a/app/src/common/tx.c b/app/src/common/tx.c new file mode 100644 index 0000000..8d7f148 --- /dev/null +++ b/app/src/common/tx.c @@ -0,0 +1,141 @@ +/******************************************************************************* +* (c) 2018 - 2023 Zondax AG +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ + +#include "tx.h" +#include "apdu_codes.h" +#include "buffering.h" +#include "parser.h" +#include +#include "zxmacros.h" + +#if defined(TARGET_NANOX) || defined(TARGET_NANOS2) || defined(TARGET_STAX) +#define RAM_BUFFER_SIZE 8192 +#define FLASH_BUFFER_SIZE 16384 +#elif defined(TARGET_NANOS) +#define RAM_BUFFER_SIZE 256 +#define FLASH_BUFFER_SIZE 8192 +#endif + +// Ram +uint8_t ram_buffer[RAM_BUFFER_SIZE]; + +// Flash +typedef struct { + uint8_t buffer[FLASH_BUFFER_SIZE]; +} storage_t; + +#if defined(TARGET_NANOS) || defined(TARGET_NANOX) || defined(TARGET_NANOS2) || defined(TARGET_STAX) +storage_t NV_CONST N_appdata_impl __attribute__((aligned(64))); +#define N_appdata (*(NV_VOLATILE storage_t *)PIC(&N_appdata_impl)) +#endif + +static parser_tx_t tx_obj; +static parser_context_t ctx_parsed_tx; + +void tx_initialize() { + buffering_init( + ram_buffer, + sizeof(ram_buffer), + (uint8_t *)N_appdata.buffer, + sizeof(N_appdata.buffer)); +} + +void tx_reset() { + buffering_reset(); +} + +uint32_t tx_append(unsigned char *buffer, uint32_t length) { + return buffering_append(buffer, length); +} + +uint32_t tx_get_buffer_length() { + return buffering_get_buffer()->pos; +} + +uint8_t *tx_get_buffer() { + return buffering_get_buffer()->data; +} + +const char *tx_parse() { + MEMZERO(&tx_obj, sizeof(tx_obj)); + + uint8_t err = parser_parse( + &ctx_parsed_tx, + tx_get_buffer(), + tx_get_buffer_length(), + &tx_obj); + + CHECK_APP_CANARY() + + if (err != parser_ok) { + return parser_getErrorDescription(err); + } + + err = parser_validate(&ctx_parsed_tx); + CHECK_APP_CANARY() + + if (err != parser_ok) { + return parser_getErrorDescription(err); + } + + return NULL; +} + +void tx_parse_reset() +{ + MEMZERO(&tx_obj, sizeof(tx_obj)); +} + +zxerr_t tx_getNumItems(uint8_t *num_items) { + parser_error_t err = parser_getNumItems(&ctx_parsed_tx, num_items); + + if (err != parser_ok) { + return zxerr_unknown; + } + + return zxerr_ok; +} + +zxerr_t tx_getItem(int8_t displayIdx, + char *outKey, uint16_t outKeyLen, + char *outVal, uint16_t outValLen, + uint8_t pageIdx, uint8_t *pageCount) +{ + uint8_t numItems = 0; + + CHECK_ZXERR(tx_getNumItems(&numItems)) + + if (displayIdx > numItems) { + return zxerr_no_data; + } + + parser_error_t err = parser_getItem(&ctx_parsed_tx, + displayIdx, + outKey, outKeyLen, + outVal, outValLen, + pageIdx, pageCount); + + // Convert error codes + if (err == parser_no_data || + err == parser_display_idx_out_of_range || + err == parser_display_page_out_of_range) + return zxerr_no_data; + + if (err != parser_ok) + return zxerr_unknown; + + return zxerr_ok; +} diff --git a/app/src/common/tx.h b/app/src/common/tx.h new file mode 100644 index 0000000..ab2376d --- /dev/null +++ b/app/src/common/tx.h @@ -0,0 +1,52 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * 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. + ********************************************************************************/ +#pragma once + +#include "coin.h" +#include "os.h" +#include "zxerror.h" + +void tx_initialize(); + +/// Clears the transaction buffer +void tx_reset(); + +/// Appends buffer to the end of the current transaction buffer +/// Transaction buffer will grow until it reaches the maximum allowed size +/// \param buffer +/// \param length +/// \return It returns an error message if the buffer is too small. +uint32_t tx_append(unsigned char *buffer, uint32_t length); + +/// Returns size of the raw json transaction buffer +/// \return +uint32_t tx_get_buffer_length(); + +/// Returns the raw json transaction buffer +/// \return +uint8_t *tx_get_buffer(); + +/// Parse message stored in transaction buffer +/// This function should be called as soon as full buffer data is loaded. +/// \return It returns NULL if data is valid or error message otherwise. +const char *tx_parse(); + +/// Return the number of items in the transaction +zxerr_t tx_getNumItems(uint8_t *num_items); + +/// Gets an specific item from the transaction (including paging) +zxerr_t tx_getItem(int8_t displayIdx, char *outKey, uint16_t outKeyLen, char *outValue, uint16_t outValueLen, + uint8_t pageIdx, uint8_t *pageCount); diff --git a/app/src/crypto.c b/app/src/crypto.c new file mode 100644 index 0000000..2a249ec --- /dev/null +++ b/app/src/crypto.c @@ -0,0 +1,172 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#include "crypto.h" + +#include "coin.h" +#include "cx.h" +#include "zxmacros.h" +#include "keys_def.h" +#include "crypto_helper.h" + +uint32_t hdPath[HDPATH_LEN_DEFAULT]; + +#define CHECK_PARSER_OK(CALL) \ + do { \ + cx_err_t __cx_err = CALL; \ + if (__cx_err != parser_ok) { \ + return zxerr_unknown; \ + } \ + } while (0) + +static zxerr_t computeKeys(keys_t * saplingKeys) { + if (saplingKeys == NULL) { + return zxerr_no_data; + } + + // Compute ask, nsk + CHECK_PARSER_OK(convertKey(saplingKeys->spendingKey, MODIFIER_ASK, saplingKeys->ask, true)); + CHECK_PARSER_OK(convertKey(saplingKeys->spendingKey, MODIFIER_NSK, saplingKeys->nsk, true)); + + // Compute ak, nsk + // This function will make a copy of the first param --> There shouldn't be problems to overwrite the union + CHECK_PARSER_OK(generate_key(saplingKeys->ask, SpendingKeyGenerator, saplingKeys->ak)); + CHECK_PARSER_OK(generate_key(saplingKeys->nsk, ProofGenerationKeyGenerator, saplingKeys->nk)); + + // Compute ivk and ovk + CHECK_PARSER_OK(computeIVK(saplingKeys->ak, saplingKeys->nk, saplingKeys->ivk)); + CHECK_PARSER_OK(convertKey(saplingKeys->spendingKey, MODIFIER_OVK, saplingKeys->ovk, false)); + + // Compute public address + CHECK_PARSER_OK(generate_key(saplingKeys->ivk, PublicKeyGenerator, saplingKeys->address)); + + return zxerr_ok; +} + +zxerr_t crypto_generateSaplingKeys(uint8_t *output, uint16_t outputLen) { + if (output == NULL || outputLen < 3 * KEY_LENGTH) { + return zxerr_buffer_too_small; + } + + zxerr_t error = zxerr_unknown; + MEMZERO(output, outputLen); + + // Generate spending key + uint8_t privateKeyData[SK_LEN_25519] = {0}; + CATCH_CXERROR(os_derive_bip32_with_seed_no_throw(HDW_NORMAL, + CX_CURVE_Ed25519, + hdPath, + HDPATH_LEN_DEFAULT, + privateKeyData, + NULL, NULL, 0)); + + keys_t saplingKeys = {0}; + memcpy(saplingKeys.spendingKey, privateKeyData, KEY_LENGTH); + error = computeKeys(&saplingKeys); + + // Copy keys + if (error == zxerr_ok) { + memcpy(output, saplingKeys.address, KEY_LENGTH); + memcpy(output + KEY_LENGTH, saplingKeys.ivk, KEY_LENGTH); + memcpy(output + 2*KEY_LENGTH, saplingKeys.ovk, KEY_LENGTH); + } + +catch_cx_error: + MEMZERO(privateKeyData, sizeof(privateKeyData)); + MEMZERO(&saplingKeys, sizeof(saplingKeys)); + + return error; +} + + +zxerr_t crypto_extractPublicKey(uint8_t *pubKey, uint16_t pubKeyLen) { + if (pubKey == NULL || pubKeyLen < PK_LEN_25519) { + return zxerr_invalid_crypto_settings; + } + cx_ecfp_public_key_t cx_publicKey; + cx_ecfp_private_key_t cx_privateKey; + uint8_t privateKeyData[SK_LEN_25519] = {0}; + + zxerr_t error = zxerr_unknown; + + // Generate keys + CATCH_CXERROR(os_derive_bip32_with_seed_no_throw(HDW_NORMAL, CX_CURVE_Ed25519, hdPath, HDPATH_LEN_DEFAULT, + privateKeyData, NULL, NULL, 0)); + + CATCH_CXERROR(cx_ecfp_init_private_key_no_throw(CX_CURVE_Ed25519, privateKeyData, 32, &cx_privateKey)); + CATCH_CXERROR(cx_ecfp_init_public_key_no_throw(CX_CURVE_Ed25519, NULL, 0, &cx_publicKey)); + CATCH_CXERROR(cx_ecfp_generate_pair_no_throw(CX_CURVE_Ed25519, &cx_publicKey, &cx_privateKey, 1)); + for (unsigned int i = 0; i < PK_LEN_25519; i++) { + pubKey[i] = cx_publicKey.W[64 - i]; + } + + if ((cx_publicKey.W[PK_LEN_25519] & 1) != 0) { + pubKey[31] |= 0x80; + } + error = zxerr_ok; + +catch_cx_error: + MEMZERO(&cx_privateKey, sizeof(cx_privateKey)); + MEMZERO(privateKeyData, sizeof(privateKeyData)); + + if (error != zxerr_ok) { + MEMZERO(pubKey, pubKeyLen); + } + return error; +} + +zxerr_t crypto_sign(uint8_t *signature, uint16_t signatureMaxlen, const uint8_t *message, uint16_t messageLen) { + if (signature == NULL || message == NULL || signatureMaxlen < ED25519_SIGNATURE_SIZE || messageLen == 0) { + return zxerr_invalid_crypto_settings; + } + + cx_ecfp_private_key_t cx_privateKey; + uint8_t privateKeyData[SK_LEN_25519] = {0}; + + zxerr_t error = zxerr_unknown; + // Generate keys + CATCH_CXERROR(os_derive_bip32_with_seed_no_throw(HDW_NORMAL, CX_CURVE_Ed25519, hdPath, HDPATH_LEN_DEFAULT, + privateKeyData, NULL, NULL, 0)); + + CATCH_CXERROR(cx_ecfp_init_private_key_no_throw(CX_CURVE_Ed25519, privateKeyData, SCALAR_LEN_ED25519, &cx_privateKey)); + + // Sign + CATCH_CXERROR(cx_eddsa_sign_no_throw(&cx_privateKey, CX_SHA512, message, messageLen, signature, signatureMaxlen)); + + error = zxerr_ok; + +catch_cx_error: + MEMZERO(&cx_privateKey, sizeof(cx_privateKey)); + MEMZERO(privateKeyData, sizeof(privateKeyData)); + + if (error != zxerr_ok) { + MEMZERO(signature, signatureMaxlen); + } + + return error; +} + +zxerr_t crypto_fillAddress(uint8_t *buffer, uint16_t bufferLen, uint16_t *addrResponseLen) { + if (buffer == NULL || addrResponseLen == NULL) { + return zxerr_unknown; + } + + MEMZERO(buffer, bufferLen); + CHECK_ZXERR(crypto_generateSaplingKeys(buffer, bufferLen)); + *addrResponseLen = 3 * KEY_LENGTH; + + return zxerr_ok; +} diff --git a/app/src/crypto.h b/app/src/crypto.h new file mode 100644 index 0000000..22c57f0 --- /dev/null +++ b/app/src/crypto.h @@ -0,0 +1,37 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * 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. + ********************************************************************************/ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "coin.h" +#include "zxerror.h" + +extern uint32_t hdPath[HDPATH_LEN_DEFAULT]; + +zxerr_t crypto_fillAddress(uint8_t *buffer, uint16_t bufferLen, uint16_t *addrResponseLen); + +zxerr_t crypto_sign(uint8_t *signature, uint16_t signatureMaxlen, const uint8_t *message, uint16_t messageLen); + +#ifdef __cplusplus +} +#endif diff --git a/app/src/crypto_helper.c b/app/src/crypto_helper.c new file mode 100644 index 0000000..46d4d8b --- /dev/null +++ b/app/src/crypto_helper.c @@ -0,0 +1,87 @@ +/******************************************************************************* + * (c) 2018 - 2024 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ +#include "crypto_helper.h" +#include "keys_personalizations.h" +#include +#include "zxformat.h" + +#include "rslib.h" + + #if defined (LEDGER_SPECIFIC) + #include "cx.h" + #include "cx_blake2b.h" +#endif + #include "blake2.h" + +static void swap_endian(uint8_t *data, int8_t len) { + for (int8_t i = 0; i < len / 2; i++) { + uint8_t t = data[len - i - 1]; + data[len - i - 1] = data[i]; + data[i] = t; + } +} + +parser_error_t convertKey(const uint8_t spendingKey[KEY_LENGTH], const uint8_t modifier, uint8_t outputKey[KEY_LENGTH], bool reduceWideByte) { + uint8_t output[64] = {0}; +#if defined (LEDGER_SPECIFIC) + cx_blake2b_t ctx = {0}; + ASSERT_CX_OK(cx_blake2b_init2_no_throw(&ctx, BLAKE2B_OUTPUT_LEN, NULL, 0, (uint8_t *)EXPANDED_SPEND_BLAKE2_KEY, sizeof(EXPANDED_SPEND_BLAKE2_KEY))); + ASSERT_CX_OK(cx_blake2b_update(&ctx, spendingKey, KEY_LENGTH)); + ASSERT_CX_OK(cx_blake2b_update(&ctx, &modifier, 1)); + cx_blake2b_final(&ctx, output); +#else + blake2b_state state = {0}; + blake2b_init_with_personalization(&state, BLAKE2B_OUTPUT_LEN, (const uint8_t*)EXPANDED_SPEND_BLAKE2_KEY, sizeof(EXPANDED_SPEND_BLAKE2_KEY)); + blake2b_update(&state, spendingKey, KEY_LENGTH); + blake2b_update(&state, &modifier, 1); + blake2b_final(&state, output, sizeof(output)); +#endif + + if (reduceWideByte) { + from_bytes_wide(output, outputKey); + swap_endian(outputKey, KEY_LENGTH); + } else { + memcpy(outputKey, output, KEY_LENGTH); + } + + return parser_ok; +} + +parser_error_t generate_key(const uint8_t expandedKey[KEY_LENGTH], constant_key_t keyType, uint8_t output[KEY_LENGTH]) { + if (keyType >= InvalidKey) { + return parser_value_out_of_range; + } + uint8_t tmpExpandedKey[KEY_LENGTH] = {0}; + memcpy(tmpExpandedKey, expandedKey, KEY_LENGTH); + swap_endian(tmpExpandedKey, KEY_LENGTH); + scalar_multiplication(tmpExpandedKey, keyType, output); + return parser_ok; +} + +parser_error_t computeIVK(const ak_t ak, const nk_t nk, ivk_t ivk) { + blake2s_state state = {0}; + blake2s_init_with_personalization(&state, 32, (const uint8_t*)CRH_IVK_PERSONALIZATION, sizeof(CRH_IVK_PERSONALIZATION)); + blake2s_update(&state, ak, KEY_LENGTH); + blake2s_update(&state, nk, KEY_LENGTH); + blake2s_final(&state, ivk, KEY_LENGTH); + + ivk[31] &= 0x07; + swap_endian(ivk, KEY_LENGTH); + // if ivk == [0; 32] { + // return Err(IronfishError::new(IronfishErrorKind::InvalidViewingKey)); + // } + return parser_ok; +} diff --git a/app/src/crypto_helper.h b/app/src/crypto_helper.h new file mode 100644 index 0000000..cfe4b35 --- /dev/null +++ b/app/src/crypto_helper.h @@ -0,0 +1,46 @@ +/******************************************************************************* + * (c) 2018 - 2024 Zondax AG + * + * 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. + ********************************************************************************/ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include "parser_common.h" +#include "keys_def.h" + +#define ASSERT_CX_OK(CALL) \ + do { \ + cx_err_t __cx_err = CALL; \ + if (__cx_err != CX_OK) { \ + return parser_unexpected_error; \ + } \ + } while (0) + +#define MODIFIER_ASK 0x00 +#define MODIFIER_NSK 0x01 +#define MODIFIER_OVK 0x02 + +parser_error_t convertKey(const uint8_t spendingKey[32], const uint8_t modifier, uint8_t outputKey[32], bool reduceWideByte); +parser_error_t generate_key(const uint8_t expandedKey[32], constant_key_t keyType, uint8_t output[32]); +parser_error_t computeIVK(const ak_t ak, const nk_t nk, ivk_t ivk); + +#ifdef __cplusplus +} +#endif diff --git a/app/src/keys_def.h b/app/src/keys_def.h new file mode 100644 index 0000000..c850366 --- /dev/null +++ b/app/src/keys_def.h @@ -0,0 +1,69 @@ +/******************************************************************************* + * (c) 2018 - 2024 Zondax AG + * + * 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. + ********************************************************************************/ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +typedef struct { + uint8_t *ptr; + uint16_t len; +} bytes_t; + + +typedef enum { + SpendingKeyGenerator, + ProofGenerationKeyGenerator, + PublicKeyGenerator, + InvalidKey, +} constant_key_t; + +#define KEY_LENGTH 32 + +typedef uint8_t spending_key_t[KEY_LENGTH]; +typedef uint8_t ask_t[KEY_LENGTH]; +typedef uint8_t nsk_t[KEY_LENGTH]; + +typedef uint8_t ak_t[KEY_LENGTH]; +typedef uint8_t nk_t[KEY_LENGTH]; + +typedef uint8_t ivk_t[KEY_LENGTH]; +typedef uint8_t ovk_t[KEY_LENGTH]; + +typedef uint8_t public_address_t[KEY_LENGTH]; + +typedef struct { + spending_key_t spendingKey; + union { + ask_t ask; + ak_t ak; + }; + union { + nsk_t nsk; + nk_t nk; + }; + + ivk_t ivk; + ovk_t ovk; + public_address_t address; +} keys_t; + +#ifdef __cplusplus +} +#endif diff --git a/app/src/keys_personalizations.h b/app/src/keys_personalizations.h new file mode 100644 index 0000000..577a211 --- /dev/null +++ b/app/src/keys_personalizations.h @@ -0,0 +1,39 @@ +/******************************************************************************* + * (c) 2018 - 2024 Zondax AG + * + * 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. + ********************************************************************************/ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#if defined (LEDGER_SPECIFIC) +// blake2 needs to define output size in bits 512 bits = 64 bytes +#define BLAKE2B_OUTPUT_LEN 512 +#else +#define BLAKE2B_OUTPUT_LEN 64 +#endif + +const char EXPANDED_SPEND_BLAKE2_KEY[16] = "Iron Fish Money "; +const char CRH_IVK_PERSONALIZATION[8] = "Zcashivk"; + +#ifdef __cplusplus +} +#endif + + diff --git a/app/src/parser.c b/app/src/parser.c new file mode 100644 index 0000000..b53a429 --- /dev/null +++ b/app/src/parser.c @@ -0,0 +1,122 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#include "parser.h" + +#include +#include +#include +#include + +#include "coin.h" +#include "crypto.h" +#include "parser_common.h" +#include "parser_impl.h" + +parser_error_t parser_init_context(parser_context_t *ctx, const uint8_t *buffer, uint16_t bufferSize) { + ctx->offset = 0; + ctx->buffer = NULL; + ctx->bufferLen = 0; + + if (bufferSize == 0 || buffer == NULL) { + // Not available, use defaults + return parser_init_context_empty; + } + + ctx->buffer = buffer; + ctx->bufferLen = bufferSize; + return parser_ok; +} + +parser_error_t parser_parse(parser_context_t *ctx, const uint8_t *data, size_t dataLen, parser_tx_t *tx_obj) { + CHECK_ERROR(parser_init_context(ctx, data, dataLen)) + ctx->tx_obj = tx_obj; + return _read(ctx, tx_obj); +} + +parser_error_t parser_validate(parser_context_t *ctx) { + // Iterate through all items to check that all can be shown and are valid + uint8_t numItems = 0; + CHECK_ERROR(parser_getNumItems(ctx, &numItems)) + + char tmpKey[40] = {0}; + char tmpVal[40] = {0}; + + for (uint8_t idx = 0; idx < numItems; idx++) { + uint8_t pageCount = 0; + CHECK_ERROR(parser_getItem(ctx, idx, tmpKey, sizeof(tmpKey), tmpVal, sizeof(tmpVal), 0, &pageCount)) + } + return parser_ok; +} + +parser_error_t parser_getNumItems(const parser_context_t *ctx, uint8_t *num_items) { + // #{TODO} --> function to retrieve num Items + // *num_items = _getNumItems(); + UNUSED(ctx); + *num_items = 1; + if (*num_items == 0) { + return parser_unexpected_number_items; + } + return parser_ok; +} + +static void cleanOutput(char *outKey, uint16_t outKeyLen, char *outVal, uint16_t outValLen) { + MEMZERO(outKey, outKeyLen); + MEMZERO(outVal, outValLen); + snprintf(outKey, outKeyLen, "?"); + snprintf(outVal, outValLen, " "); +} + +static parser_error_t checkSanity(uint8_t numItems, uint8_t displayIdx) { + if (displayIdx >= numItems) { + return parser_display_idx_out_of_range; + } + return parser_ok; +} + +parser_error_t parser_getItem(const parser_context_t *ctx, uint8_t displayIdx, char *outKey, uint16_t outKeyLen, + char *outVal, uint16_t outValLen, uint8_t pageIdx, uint8_t *pageCount) { + UNUSED(pageIdx); + *pageCount = 1; + uint8_t numItems = 0; + CHECK_ERROR(parser_getNumItems(ctx, &numItems)) + CHECK_APP_CANARY() + + CHECK_ERROR(checkSanity(numItems, displayIdx)) + cleanOutput(outKey, outKeyLen, outVal, outValLen); + + switch (displayIdx) { + case 0: + // Display Item 0 + snprintf(outKey, outKeyLen, "Title #0"); + snprintf(outVal, outValLen, "Value #0"); + return parser_ok; + case 1: + // Display Item 1 + snprintf(outKey, outKeyLen, "Title #1"); + snprintf(outVal, outValLen, "Value #1"); + return parser_ok; + case 10: + // Display Item 10 + snprintf(outKey, outKeyLen, "Title #N"); + snprintf(outVal, outValLen, "Value #N"); + return parser_ok; + default: + break; + } + + return parser_display_idx_out_of_range; +} diff --git a/app/src/parser_impl.c b/app/src/parser_impl.c new file mode 100644 index 0000000..29f0455 --- /dev/null +++ b/app/src/parser_impl.c @@ -0,0 +1,59 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#include "parser_impl.h" + +parser_error_t _read(parser_context_t *c, parser_tx_t *v) { + UNUSED(c); + UNUSED(v); + // #{TODO} --> parse parameters: read from c->buffer and store in v + return parser_ok; +} + +const char *parser_getErrorDescription(parser_error_t err) { + switch (err) { + case parser_ok: + return "No error"; + case parser_no_data: + return "No more data"; + case parser_init_context_empty: + return "Initialized empty context"; + case parser_unexpected_buffer_end: + return "Unexpected buffer end"; + case parser_unexpected_version: + return "Unexpected version"; + case parser_unexpected_characters: + return "Unexpected characters"; + case parser_unexpected_field: + return "Unexpected field"; + case parser_duplicated_field: + return "Unexpected duplicated field"; + case parser_value_out_of_range: + return "Value out of range"; + case parser_unexpected_chain: + return "Unexpected chain"; + case parser_missing_field: + return "missing field"; + + case parser_display_idx_out_of_range: + return "display index out of range"; + case parser_display_page_out_of_range: + return "display page out of range"; + + default: + return "Unrecognized error code"; + } +} diff --git a/app/src/parser_impl.h b/app/src/parser_impl.h new file mode 100644 index 0000000..9944891 --- /dev/null +++ b/app/src/parser_impl.h @@ -0,0 +1,33 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * 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. + ********************************************************************************/ +#pragma once + +#include + +#include "parser_common.h" +#include "parser_txdef.h" +#include "zxtypes.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// #{TODO} --> functions to parse, get, process transaction fields +parser_error_t _read(parser_context_t *c, parser_tx_t *v); + +#ifdef __cplusplus +} +#endif diff --git a/app/src/parser_txdef.h b/app/src/parser_txdef.h new file mode 100644 index 0000000..5b410f6 --- /dev/null +++ b/app/src/parser_txdef.h @@ -0,0 +1,33 @@ +/******************************************************************************* + * (c) 2018 - 2023 Zondax AG + * + * 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. + ********************************************************************************/ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +typedef struct { + uint8_t txn_param_0; + uint8_t txn_param_1; + uint8_t txn_param_N; +} parser_tx_t; + +#ifdef __cplusplus +} +#endif diff --git a/app/stax_icon.gif b/app/stax_icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..db6e52dbceb9b9afcf0b1ee641a0e417a64ba6f1 GIT binary patch literal 167 zcmZ?wbhEHbRA5kG_`m=H|NsA2{K*1lD*os8%uP&B^-WCAOwQ&@Pt46tv^CH(F$F;e z9grH3J_e?Qp8l1m-|{b>v*p(5hI<*!|7_>6O={Pi87H~yVO7yYt-~U{+dpedXM8yQ zq0VLQVYVxZ{d-lon{?Qu15+n+R#~}*y`3`ivt;>0-7TEIgD!40G*jEF_ejU|S>O5B M-~a4e$iQF?0Nrpw?f?J) literal 0 HcmV?d00001 diff --git a/cmake/cmake-modules b/cmake/cmake-modules new file mode 160000 index 0000000..1fcf7f4 --- /dev/null +++ b/cmake/cmake-modules @@ -0,0 +1 @@ +Subproject commit 1fcf7f4a2179a7b649be15473906882871ef5f0b diff --git a/cmake/conan/CMakeLists.txt b/cmake/conan/CMakeLists.txt new file mode 100644 index 0000000..4899e70 --- /dev/null +++ b/cmake/conan/CMakeLists.txt @@ -0,0 +1,12 @@ +# Download automatically, you can also just copy the conan.cmake file + +if (NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake") + message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan") + file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/v0.13/conan.cmake" + "${CMAKE_BINARY_DIR}/conan.cmake") +endif () +include(${CMAKE_BINARY_DIR}/conan.cmake) + +conan_check(REQUIRED) + +conan_cmake_run(CONANFILE conanfile.txt BASIC_SETUP CMAKE_TARGETS BUILD missing) diff --git a/cmake/gtest/CMakeLists.txt b/cmake/gtest/CMakeLists.txt new file mode 100644 index 0000000..5962ac3 --- /dev/null +++ b/cmake/gtest/CMakeLists.txt @@ -0,0 +1,32 @@ +############################## +# Google Test +# Based on instructions in https://github.com/google/googletest/tree/master/googletest#incorporating-into-an-existing-cmake-project +# Download and unpack googletest at configure time +configure_file(CMakeLists.txt.gtest.in ${CMAKE_BINARY_DIR}/googletest-download/CMakeLists.txt) + +execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . + RESULT_VARIABLE result + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download) +if (result) + message(FATAL_ERROR "CMake step for googletest failed: ${result}") +endif () + +execute_process(COMMAND ${CMAKE_COMMAND} --build . + RESULT_VARIABLE result + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download) +if (result) + message(FATAL_ERROR "Build step for googletest failed: ${result}") +endif () + +# Prevent overriding the parent project's compiler/linker settings on Windows +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + +add_subdirectory( + ${CMAKE_BINARY_DIR}/googletest-src + ${CMAKE_BINARY_DIR}/googletest-build +) + +if (CMAKE_VERSION VERSION_LESS 2.8.11) + include_directories("${gtest_SOURCE_DIR}/include") + include_directories("${gtest_SOURCE_DIR}/include") +endif () diff --git a/cmake/gtest/CMakeLists.txt.gtest.in b/cmake/gtest/CMakeLists.txt.gtest.in new file mode 100644 index 0000000..71b26c5 --- /dev/null +++ b/cmake/gtest/CMakeLists.txt.gtest.in @@ -0,0 +1,16 @@ +# Based on https://github.com/google/googletest/tree/master/googletest#incorporating-into-an-existing-cmake-project +cmake_minimum_required(VERSION 3.0.0) + +project(googletest-download NONE) + +include(ExternalProject) +ExternalProject_Add(googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG release-1.12.1 + SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src" + BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build" + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" + ) diff --git a/conanfile.txt b/conanfile.txt new file mode 100644 index 0000000..0d3d2a7 --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,6 @@ +[requires] +jsoncpp/1.9.3 +fmt/7.0.2 + +[generators] +cmake diff --git a/deps/blake2 b/deps/blake2 new file mode 160000 index 0000000..93fefa2 --- /dev/null +++ b/deps/blake2 @@ -0,0 +1 @@ +Subproject commit 93fefa278c1a738685ce1a118ead7c85b5babc58 diff --git a/deps/ledger-zxlib b/deps/ledger-zxlib new file mode 160000 index 0000000..f19112b --- /dev/null +++ b/deps/ledger-zxlib @@ -0,0 +1 @@ +Subproject commit f19112bb6340721c276af459924b3c4261c0291a diff --git a/deps/nanos-secure-sdk b/deps/nanos-secure-sdk new file mode 160000 index 0000000..62dd047 --- /dev/null +++ b/deps/nanos-secure-sdk @@ -0,0 +1 @@ +Subproject commit 62dd047774b5e8a6b4e6158b493ee029453b5bae diff --git a/deps/nanosplus-secure-sdk b/deps/nanosplus-secure-sdk new file mode 160000 index 0000000..e98fad9 --- /dev/null +++ b/deps/nanosplus-secure-sdk @@ -0,0 +1 @@ +Subproject commit e98fad9fe6b1b726a7563274659f94ba258e547c diff --git a/deps/nanox-secure-sdk b/deps/nanox-secure-sdk new file mode 160000 index 0000000..e98fad9 --- /dev/null +++ b/deps/nanox-secure-sdk @@ -0,0 +1 @@ +Subproject commit e98fad9fe6b1b726a7563274659f94ba258e547c diff --git a/deps/stax-secure-sdk b/deps/stax-secure-sdk new file mode 160000 index 0000000..e98fad9 --- /dev/null +++ b/deps/stax-secure-sdk @@ -0,0 +1 @@ +Subproject commit e98fad9fe6b1b726a7563274659f94ba258e547c diff --git a/docs/APDUSPEC.md b/docs/APDUSPEC.md new file mode 100644 index 0000000..a77d26a --- /dev/null +++ b/docs/APDUSPEC.md @@ -0,0 +1,141 @@ +# Ironfish App + +## General structure + +# #{TODO} --> Update CLA, HDPATH and APDU messages + +The general structure of commands and responses is as follows: + +### Commands + +| Field | Type | Content | Note | +| :------ | :------- | :--------------------- | ---- | +| CLA | byte (1) | Application Identifier | 0x59 | +| INS | byte (1) | Instruction ID | | +| P1 | byte (1) | Parameter 1 | | +| P2 | byte (1) | Parameter 2 | | +| L | byte (1) | Bytes in payload | | +| PAYLOAD | byte (L) | Payload | | + +### Response + +| Field | Type | Content | Note | +| ------- | -------- | ----------- | ------------------------ | +| ANSWER | byte (?) | Answer | depends on the command | +| SW1-SW2 | byte (2) | Return code | see list of return codes | + +### Return codes + +| Return code | Description | +| ----------- | ----------------------- | +| 0x6400 | Execution Error | +| 0x6700 | Wrong buffer length | +| 0x6982 | Empty buffer | +| 0x6983 | Output buffer too small | +| 0x6984 | Data is invalid | +| 0x6986 | Command not allowed | +| 0x6987 | Tx is not initialized | +| 0x6B00 | P1/P2 are invalid | +| 0x6D00 | INS not supported | +| 0x6E00 | CLA not supported | +| 0x6F00 | Unknown | +| 0x6F01 | Sign / verify error | +| 0x9000 | Success | + +--- + +## Command definition + +### GET_VERSION + +#### Command + +| Field | Type | Content | Expected | +| ----- | -------- | ---------------------- | -------- | +| CLA | byte (1) | Application Identifier | 0x59 | +| INS | byte (1) | Instruction ID | 0x00 | +| P1 | byte (1) | Parameter 1 | ignored | +| P2 | byte (1) | Parameter 2 | ignored | +| L | byte (1) | Bytes in payload | 0 | + +#### Response + +| Field | Type | Content | Note | +| ------- | -------- | ---------------- | ------------------------------- | +| TEST | byte (1) | Test Mode | 0xFF means test mode is enabled | +| MAJOR | byte (2) | Version Major | 0..65535 | +| MINOR | byte (2) | Version Minor | 0..65535 | +| PATCH | byte (2) | Version Patch | 0..65535 | +| LOCKED | byte (1) | Device is locked | | +| SW1-SW2 | byte (2) | Return code | see list of return codes | + +--- + +### INS_GET_ADDR + +#### Command + +| Field | Type | Content | Expected | +| ------- | -------- | ------------------------- | ---------- | +| CLA | byte (1) | Application Identifier | 0x59 | +| INS | byte (1) | Instruction ID | 0x01 | +| P1 | byte (1) | Request User confirmation | No = 0 | +| P2 | byte (1) | Parameter 2 | ignored | +| L | byte (1) | Bytes in payload | (depends) | +| Path[0] | byte (4) | Derivation Path Data | 0x80000000 \| 44 | +| Path[1] | byte (4) | Derivation Path Data | 0x80000000 \| 133 | +| Path[2] | byte (4) | Derivation Path Data | ? | +| Path[3] | byte (4) | Derivation Path Data | ? | +| Path[4] | byte (4) | Derivation Path Data | ? | + +#### Response + +| Field | Type | Content | Note | +| ------- | --------- | --------------- | ------------------------ | +| Address | byte (32) | Public address | | +| IVK | byte (32) | IncomingViewingKey | | +| OVK | byte (32) | OutgoingViewingKey | | +| SW1-SW2 | byte (2) | Return code | see list of return codes | + +--- + +### INS_SIGN + +#### Command + +| Field | Type | Content | Expected | +| ----- | -------- | ---------------------- | --------- | +| CLA | byte (1) | Application Identifier | 0x98 | +| INS | byte (1) | Instruction ID | 0x02 | +| P1 | byte (1) | Payload desc | 0 = init | +| | | | 1 = add | +| | | | 2 = last | +| P2 | byte (1) | ---- | not used | +| L | byte (1) | Bytes in payload | (depends) | + +The first packet/chunk includes only the derivation path + +All other packets/chunks contain data chunks that are described below + +##### First Packet + +| Field | Type | Content | Expected | +| ------- | -------- | -------------------- | -------- | +| Path[0] | byte (4) | Derivation Path Data | 44 | +| Path[1] | byte (4) | Derivation Path Data | 434 | +| Path[2] | byte (4) | Derivation Path Data | ? | +| Path[3] | byte (4) | Derivation Path Data | ? | +| Path[4] | byte (4) | Derivation Path Data | ? | + +##### Other Chunks/Packets + +| Field | Type | Content | Expected | +| ------- | -------- | --------------- | -------- | +| Message | bytes... | Message to Sign | | + +#### Response + +| Field | Type | Content | Note | +| ------- | --------- | ----------- | ------------------------ | +| SIG | byte (65) | Signature | | +| SW1-SW2 | byte (2) | Return code | see list of return codes | diff --git a/docs/zondax_dark.png b/docs/zondax_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c14ba36853f86af8747f8d2c880b574261b2f77b GIT binary patch literal 21692 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 YxFb&M;g{{rFQERxO|4H99&^3^f06jK0RR91 literal 0 HcmV?d00001 diff --git a/fuzz/parser_parse.cpp b/fuzz/parser_parse.cpp new file mode 100644 index 0000000..220f3a3 --- /dev/null +++ b/fuzz/parser_parse.cpp @@ -0,0 +1,73 @@ +#include +#include +#include + +#include "parser.h" +#include "zxformat.h" + +#ifdef NDEBUG +#error "This fuzz target won't work correctly with NDEBUG defined, which will cause asserts to be eliminated" +#endif + + +using std::size_t; + +namespace { + char PARSER_KEY[16384]; + char PARSER_VALUE[16384]; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + parser_tx_t txObj; + MEMZERO(&txObj, sizeof(txObj)); + parser_context_t ctx; + parser_error_t rc; + + rc = parser_parse(&ctx, data, size, &txObj); + if (rc != parser_ok) { + return 0; + } + + rc = parser_validate(&ctx); + if (rc != parser_ok) { + return 0; + } + + uint8_t num_items; + rc = parser_getNumItems(&ctx, &num_items); + if (rc != parser_ok) { + fprintf(stderr, + "error in parser_getNumItems: %s\n", + parser_getErrorDescription(rc)); + assert(false); + } + + (void)fprintf(stderr, "----------------------------------------------\n"); + + for (uint8_t i = 0; i < num_items; i += 1) { + uint8_t page_idx = 0; + uint8_t page_count = 1; + while (page_idx < page_count) { + rc = parser_getItem(&ctx, i, + PARSER_KEY, sizeof(PARSER_KEY), + PARSER_VALUE, sizeof(PARSER_VALUE), + page_idx, &page_count); + +// (void)fprintf(stderr, "%s = %s\n", PARSER_KEY, PARSER_VALUE); + + if (rc != parser_ok) { + (void)fprintf(stderr, + "error getting item %u at page index %u: %s\n", + (unsigned)i, + (unsigned)page_idx, + parser_getErrorDescription(rc)); + assert(false); + } + + page_idx += 1; + } + } + + return 0; +} diff --git a/fuzz/run-fuzz-crashes.py b/fuzz/run-fuzz-crashes.py new file mode 100755 index 0000000..d6d32be --- /dev/null +++ b/fuzz/run-fuzz-crashes.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import os +import random +import shlex +import subprocess + +MAX_SECONDS_PER_RUN = 600 +MUTATE_DEPTH = random.randint(1, 20) + +# (fuzzer name, max length, max time scale factor) +CONFIGS = [ + ('parser_parse', 17000, 4), +] + +for config in CONFIGS: + fuzzer, max_len, scale_factor = config + max_time = MAX_SECONDS_PER_RUN * scale_factor + print(f'######## {fuzzer} ########') + + artifact_dir = os.path.join('fuzz', 'corpora', f'{fuzzer}-artifacts') + corpus_dir = os.path.join('fuzz', 'corpora', f'{fuzzer}') + fuzz_path = os.path.join(f'build/bin/fuzz-{fuzzer}') + + os.makedirs(artifact_dir, exist_ok=True) + os.makedirs(corpus_dir, exist_ok=True) + + env = os.environ.copy() + env['ASAN_OPTIONS'] = 'halt_on_error=1:print_stacktrace=1' + env['UBSAN_OPTIONS'] = 'halt_on_error=1:print_stacktrace=1' + + crash_files = os.listdir(artifact_dir) + for c in crash_files: + c_full_path = os.path.join(artifact_dir, c) + cmd = [fuzz_path, f'{c_full_path}'] + print(' '.join(shlex.quote(c) for c in cmd)) + error_code = subprocess.call(cmd, env=env) + if error_code != 0: + exit(error_code) + + diff --git a/fuzz/run-fuzzers.py b/fuzz/run-fuzzers.py new file mode 100755 index 0000000..3937753 --- /dev/null +++ b/fuzz/run-fuzzers.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +import os +import random +import shlex +import subprocess + +MAX_SECONDS_PER_RUN = 600 +MUTATE_DEPTH = random.randint(1, 20) + +# (fuzzer name, max length, max time scale factor) +CONFIGS = [ + ('parser_parse', 17000, 4), +] + +for config in CONFIGS: + fuzzer, max_len, scale_factor = config + max_time = MAX_SECONDS_PER_RUN * scale_factor + print(f'######## {fuzzer} ########') + + artifact_dir = os.path.join('fuzz', 'corpora', f'{fuzzer}-artifacts') + corpus_dir = os.path.join('fuzz', 'corpora', f'{fuzzer}') + fuzz_path = os.path.join(f'build/bin/fuzz-{fuzzer}') + + os.makedirs(artifact_dir, exist_ok=True) + os.makedirs(corpus_dir, exist_ok=True) + + env = os.environ.copy() + env['ASAN_OPTIONS'] = 'halt_on_error=1:print_stacktrace=1' + env['UBSAN_OPTIONS'] = 'halt_on_error=1:print_stacktrace=1' + + cmd = [fuzz_path, f'-max_total_time={max_time}', + f'-jobs=32' + f'-max_len={max_len}', + f'-mutate_depth={MUTATE_DEPTH}', + f'-artifact_prefix={artifact_dir}/', + corpus_dir] + print(' '.join(shlex.quote(c) for c in cmd)) + subprocess.call(cmd, env=env) diff --git a/js/.eslintignore b/js/.eslintignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/js/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/js/.eslintrc.json b/js/.eslintrc.json new file mode 100644 index 0000000..c48f805 --- /dev/null +++ b/js/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": ["standard-with-typescript", "prettier"], + "parserOptions": { + "project": "tsconfig.json", + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": {} +} diff --git a/js/.gitignore b/js/.gitignore new file mode 100644 index 0000000..053bb63 --- /dev/null +++ b/js/.gitignore @@ -0,0 +1,74 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +yarn.lock + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +\.idea/ + +.vscode + +TODO\.md + +\dist +/certs/cert.pem +/certs/server.cert +/certs/server.key diff --git a/js/.npmignore b/js/.npmignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/js/.npmignore @@ -0,0 +1 @@ +node_modules diff --git a/js/.prettierignore b/js/.prettierignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/js/.prettierignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/js/.prettierrc b/js/.prettierrc new file mode 100644 index 0000000..963354f --- /dev/null +++ b/js/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} diff --git a/js/LICENSE b/js/LICENSE new file mode 100644 index 0000000..d9005ef --- /dev/null +++ b/js/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 - 2023 Zondax AG + + 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. diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..d02ea92 --- /dev/null +++ b/js/README.md @@ -0,0 +1,12 @@ +# @zondax/ledger-ironfish + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![npm version](https://badge.fury.io/js/%40zondax%2Fledger-ironfish.svg)](https://badge.fury.io/js/%40zondax%2Fledger-ironfish) + +This package provides a basic client library to communicate with the Ironfish App running in a Ledger Nano S/X + +We recommend using the npmjs package in order to receive updates/fixes. + +## Notes + +Use `yarn install` to avoid issues. diff --git a/js/package.json b/js/package.json new file mode 100644 index 0000000..e832037 --- /dev/null +++ b/js/package.json @@ -0,0 +1,46 @@ +{ + "name": "@zondax/ledger-ironfish", + "author": "Zondax AG", + "license": "Apache-2.0", + "version": "0.0.1", + "description": "Node API for the Ironfish App (Ledger Nano S/X/S+)", + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "types": "./dist/index.d.ts", + "homepage": "https://github.com/zondax/ledger-ironfish", + "repository": { + "type": "git", + "url": "git+https://github.com/zondax/ledger-ironfish.git" + }, + "keywords": [ + "Zondax", + "Ledger", + "Javascript", + "Ironfish" + ], + "scripts": { + "build": "tsc" + }, + "bugs": { + "url": "https://github.com/zondax/ledger-ironfish/issues" + }, + "dependencies": { + "@zondax/ledger-js": "^0.2.1" + }, + "devDependencies": { + "@types/node": "^20.11.27", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-config-standard-with-typescript": "^43.0.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.1.1", + "prettier": "^3.2.5", + "typescript": "^5.4.2" + }, + "files": [ + "dist/**/*" + ] +} diff --git a/js/src/consts.ts b/js/src/consts.ts new file mode 100644 index 0000000..3b35c61 --- /dev/null +++ b/js/src/consts.ts @@ -0,0 +1,14 @@ +export const APP_KEY = "Ironfish"; + +export const P2_VALUES = { + DEFAULT: 0x00, +}; + +export const KEY_LENGTH = 32; + + +export const PKLEN = 65; +export const ADDRLEN = 32; +export const PRINCIPAL_LEN = 29; +export const PREHASH_LEN = 43; +export const SIGRSLEN = 64; diff --git a/js/src/helper.ts b/js/src/helper.ts new file mode 100644 index 0000000..c546dc9 --- /dev/null +++ b/js/src/helper.ts @@ -0,0 +1,25 @@ +import { errorCodeToString } from "@zondax/ledger-js"; +import { ADDRLEN, KEY_LENGTH, PRINCIPAL_LEN } from "./consts"; +import { ResponseAddress } from "./types"; + +export function processGetAddrResponse(response: Buffer): ResponseAddress { + const errorCodeData = response.subarray(-2); + const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; + + const publicAddress = Buffer.from(response.subarray(0, KEY_LENGTH)); + response = response.subarray(KEY_LENGTH); + + const ivk = Buffer.from(response.subarray(0, KEY_LENGTH)); + response = response.subarray(KEY_LENGTH); + + const ovk = Buffer.from(response.subarray(0, KEY_LENGTH)); + response = response.subarray(KEY_LENGTH); + + return { + publicAddress, + ivk, + ovk, + returnCode, + errorMessage: errorCodeToString(returnCode), + }; +} diff --git a/js/src/index.ts b/js/src/index.ts new file mode 100644 index 0000000..9e12d69 --- /dev/null +++ b/js/src/index.ts @@ -0,0 +1,145 @@ +/** ****************************************************************************** + * (c) 2019-2020 Zondax GmbH + * (c) 2016-2017 Ledger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************* */ +import { P2_VALUES, PREHASH_LEN, SIGRSLEN } from "./consts"; +import { ResponseAddress, ResponseSign, IronfishIns } from "./types"; + +import GenericApp, { + ConstructorParams, + errorCodeToString, + LedgerError, + PAYLOAD_TYPE, + processErrorResponse, + Transport, +} from "@zondax/ledger-js"; +import { processGetAddrResponse } from "./helper"; + +export * from "./types"; + +export default class IronfishApp extends GenericApp { + readonly INS!: IronfishIns; + constructor(transport: Transport) { + if (transport == null) throw new Error("Transport has not been defined"); + + const params: ConstructorParams = { + cla: 0x80, + ins: { + GET_VERSION: 0x00, + GET_ADDR: 0x01, + SIGN: 0x02, + }, + p1Values: { + ONLY_RETRIEVE: 0x00, + SHOW_ADDRESS_IN_DEVICE: 0x01, + }, + acceptedPathLengths: [4, 5, 6], + chunkSize: 250, + }; + super(transport, params); + } + + async getAddressAndPubKey(path: string): Promise { + const serializedPath = this.serializePath(path); + return await this.transport + .send(this.CLA, this.INS.GET_ADDR, this.P1_VALUES.ONLY_RETRIEVE, P2_VALUES.DEFAULT, serializedPath, [ + LedgerError.NoErrors, + ]) + .then(processGetAddrResponse, processErrorResponse); + } + + async showAddressAndPubKey(path: string): Promise { + const serializedPath = this.serializePath(path); + + return await this.transport + .send(this.CLA, this.INS.GET_ADDR, this.P1_VALUES.SHOW_ADDRESS_IN_DEVICE, P2_VALUES.DEFAULT, serializedPath, [ + LedgerError.NoErrors, + ]) + .then(processGetAddrResponse, processErrorResponse); + } + + // #{TODO} --> Create sign methods, this are example ones! + async signSendChunk(chunkIdx: number, chunkNum: number, chunk: Buffer, txtype: number): Promise { + let payloadType = PAYLOAD_TYPE.ADD; + if (chunkIdx === 1) { + payloadType = PAYLOAD_TYPE.INIT; + } + if (chunkIdx === chunkNum) { + payloadType = PAYLOAD_TYPE.LAST; + } + + return await this.transport + .send(this.CLA, this.INS.SIGN, payloadType, txtype, chunk, [ + LedgerError.NoErrors, + LedgerError.DataIsInvalid, + LedgerError.BadKeyHandle, + LedgerError.SignVerifyError, + ]) + .then((response: Buffer) => { + const errorCodeData = response.subarray(-2); + const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; + let errorMessage = errorCodeToString(returnCode); + + let preSignHash = Buffer.alloc(0); + let signatureRS = Buffer.alloc(0); + let signatureDER = Buffer.alloc(0); + + if ( + returnCode === LedgerError.BadKeyHandle || + returnCode === LedgerError.DataIsInvalid || + returnCode === LedgerError.SignVerifyError + ) { + errorMessage = `${errorMessage} : ${response.subarray(0, response.length - 2).toString("ascii")}`; + } + + if (returnCode === LedgerError.NoErrors && response.length > 2) { + preSignHash = response.subarray(0, PREHASH_LEN); + signatureRS = response.subarray(PREHASH_LEN, PREHASH_LEN + SIGRSLEN); + signatureDER = response.subarray(PREHASH_LEN + SIGRSLEN + 1, response.length - 2); + return { + preSignHash, + signatureRS, + signatureDER, + returnCode, + errorMessage, + }; + } + + return { + returnCode, + errorMessage, + }; + }, processErrorResponse); + } + + async sign(path: string, message: Buffer, txtype: number): Promise { + const chunks = this.prepareChunks(path, message); + return await this.signSendChunk(1, chunks.length, chunks[0], txtype % 256).then(async (response) => { + let result: ResponseSign = { + returnCode: response.returnCode, + errorMessage: response.errorMessage, + }; + + for (let i = 1; i < chunks.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + result = await this.signSendChunk(1 + i, chunks.length, chunks[i], txtype % 256); + if (result.returnCode !== LedgerError.NoErrors) { + break; + } + } + return result; + }, processErrorResponse); + } +} diff --git a/js/src/types.ts b/js/src/types.ts new file mode 100644 index 0000000..9b9dee7 --- /dev/null +++ b/js/src/types.ts @@ -0,0 +1,19 @@ +import { INSGeneric, ResponseBase } from "@zondax/ledger-js"; + +export interface IronfishIns extends INSGeneric { + GET_VERSION: 0x00; + GET_ADDR: 0x01; + SIGN: 0x02; +} + +export interface ResponseAddress extends ResponseBase { + publicAddress?: Buffer; + ivk?: Buffer; + ovk?: Buffer; +} + +export interface ResponseSign extends ResponseBase { + preSignHash?: Buffer; + signatureRS?: Buffer; + signatureDER?: Buffer; +} diff --git a/js/tsconfig.json b/js/tsconfig.json new file mode 100644 index 0000000..04515f3 --- /dev/null +++ b/js/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2020", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "./dist" + }, + "exclude": ["dist"] +} diff --git a/ledger_app.toml b/ledger_app.toml new file mode 100644 index 0000000..de2864f --- /dev/null +++ b/ledger_app.toml @@ -0,0 +1,7 @@ +[app] +build_directory = "./app/" +sdk = "C" +devices = ["nanos", "nanox", "nanos+"] + +[tests] +unit_directory = "./tests/" diff --git a/tests/keys.cpp b/tests/keys.cpp new file mode 100644 index 0000000..9c04b85 --- /dev/null +++ b/tests/keys.cpp @@ -0,0 +1,305 @@ +/******************************************************************************* +* (c) 2018 - 2024 Zondax AG +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ +#include "gmock/gmock.h" + +#include +#include +#include +#include "parser_txdef.h" +#include "keys_def.h" +#include "crypto_helper.h" +#include "rslib.h" + +using namespace std; +struct IronfishKeys { + string spendingKey; + string spendAuthorizationKey; //ask + string proofAuthorizationKey; //nsk + string authorizing_key; //ak + string nullifier_deriving_key; //nk + string viewKey; //full viewing key (FVK) + string incomingViewingKey; //ivk + string outgoingViewingKey; //ovk + string publicAddress; +}; + +string toHexString(const uint8_t* data, size_t length) { + std::stringstream hexStream; + hexStream << std::hex << std::setfill('0'); + for (size_t i = 0; i < length; ++i) { + hexStream << std::setw(2) << static_cast(data[i]); + } + return hexStream.str(); +} + +// Generated using ironfish SDK (public_address_generation) +vector testvectors { + { + "0000000000000000000000000000000000000000000000000000000000000000", + "02dddb7d511f89769aaaefa94c08a0cc64f8f962fb1484fb58f24e3b6191a813", + "0123383cde1dc45f757e9852e650b38d222563386c55335b322ca8aec65e4bda", + "94c0bf0aff4653756184e7564d6bace42ba28ded17cb6969bd3db2b5bb813183", + "be0383711ddcb0beaa8b7ef72fc20f1798f813e3008bb9d704a5da590d967ed7", + "94c0bf0aff4653756184e7564d6bace42ba28ded17cb6969bd3db2b5bb813183be0383711ddcb0beaa8b7ef72fc20f1798f813e3008bb9d704a5da590d967ed7", + "03171a55bcdd54018c224a261cdc99badce181a60ba4d4f6be401dc9841e07bb", + "efd8642e69a4bf55196ed4d4528fcf50a842319284332f708fc1254c8358ae1e", + "4f766c63bf2a18f2434add5dfda393886c80b1fe4ef1d968c7c87dc0484b5c9d" + }, + { + "0000000000000000000000000000000000000000000000000000000000000001", + "029b909181abb2a48bf09a6274179836eb125cb7b58ec9c049e2cc242b95dc77", + "0af942f4f07578aa419d06ab11580238211e4ed5e78e21df08c26eb5d2c79cec", + "46095954bc6e220bceb8e9dd92ad551c241df5fc78575e09b457a3b5c7ccfa59", + "71a7ec7feae16363b6daff256f8033778fdff4404af51f4702f6bb3723441fc3", + "46095954bc6e220bceb8e9dd92ad551c241df5fc78575e09b457a3b5c7ccfa5971a7ec7feae16363b6daff256f8033778fdff4404af51f4702f6bb3723441fc3", + "01e8b953ebef20c9df67ac99075f7fff3bc2f1ca93822299d6f6247617e2efd4", + "1824f96c702f47b199457273994c35d7e9343bc1a8aff9b4037fdd06d0b3a660", + "796d7ecc523ae042265314f70641daa158576f537fbb408e8ce7f17e0900fc99" + }, + { + "71af0f431ad2b93fdc9bbe907b03314f607c09461c421a2873d4807f8841bbc7", + "07e7ec847e4a0eb7bcdf8682568cac4aef8189d379e96154eb170289d1bb50fb", + "0c76463c6c683187511a59c9a0bb72a398656f35aaefc4fb03509c513d09f960", + "283e16fc078a556b2be63e7f4661474cbffa5397ececee7376e144abb5aff71e", + "f68210181abe9d2d1178773eeda7d869f619db56b6ed928b79474b532158d267", + "283e16fc078a556b2be63e7f4661474cbffa5397ececee7376e144abb5aff71ef68210181abe9d2d1178773eeda7d869f619db56b6ed928b79474b532158d267", + "035c7bcf0c6f8ffd2c5afd44fda50eda233f0b2b9747154d66fe2a827a35a78d", + "86be13371588a5e3803dc57678578aba2877f4b0212c994727197df2307d9691", + "f308f7b7c1dcea1d5c9c2ae9085afcbc476e2916ce58c3ed142336884919959a" + }, + { + "36dea1c88f60c02b01ab6a0494e16e6d939b44a2a3dd75e9e0360399dc8650f4", + "0dbe2541a65fe65aa63170bb687b53edcd66173cd1850bb8e5a42b232a2b09db", + "04eae190efdd076715dfd439acf64228e14e0fc96da8bf131eb71002c0d3233e", + "c8117bd8d872097494bff6ce631e7d84bc5ab4113d2a04eeb24f1717fad833e1", + "90a7eb4c20f5993c791de70d3da81501da041744c27a5ad135b3ea1618870330", + "c8117bd8d872097494bff6ce631e7d84bc5ab4113d2a04eeb24f1717fad833e190a7eb4c20f5993c791de70d3da81501da041744c27a5ad135b3ea1618870330", + "07d77856b0a3d2a7b6fdba054beeb199d7ddd9dd66ad3e6287f00b405b1535ec", + "25f0fa2b575874eb93644e81c44c2631efe5a1ab5d1ab7258060847670367068", + "3f88c1b9f3c4338d9445ade292f2838c2aa2de712b62250ed1ae172090c6f037" + }, + { + "b4334a7fb7f3795906688fef68dbbaf714739691fca288f722e54136ffa0d327", + "0cccedb48d345c193a64a2a830277f00a8cf315b8ba3a9073dbb041bf8b6698e", + "061af229deed0a4e5aa4632b88b6fdf8295163821d0fbf37c876fe3f2a86cd7c", + "d2ee54d95d8410fe79968df4add0bbb46c7db66c256957b965d8f4295acd1cf1", + "011abad7ce51e5163e5d46cccfa323d458ffb1c6fafb3bea42cb96df70378870", + "d2ee54d95d8410fe79968df4add0bbb46c7db66c256957b965d8f4295acd1cf1011abad7ce51e5163e5d46cccfa323d458ffb1c6fafb3bea42cb96df70378870", + "04f81f076f6d0d3509171033e837e966c48dbdbc85063ffc5ab4053fc7d00839", + "323d72bcfa2f7b2702744c3253753d0b749849cf3aa4f179e65c7b7617f4b057", + "4d30630129be18acf77b5297d5bfd7ac2f49009d50676d9cf31e1175e35203b1" + }, + { + "994f855e8e49063b1c58e348bdf80f513cb2ea24d4d3b238f77484ede3c2d92a", + "03f3a7fd902fa5d081fd04fdbd21c61f3434f9264213e19582c1078c45514b03", + "01fe8df4ca283ac4e921ab95cffead22b9cc26b4144cb18c265a0eb187f4a727", + "14f9260c82a77d7f171dfd97df572b4103cf91d89c15b55450144c99fb5d9d3e", + "b430f67ce7fbd4d3a1126b7e339db35fe6060bbe98cbda062118a9fc166213a1", + "14f9260c82a77d7f171dfd97df572b4103cf91d89c15b55450144c99fb5d9d3eb430f67ce7fbd4d3a1126b7e339db35fe6060bbe98cbda062118a9fc166213a1", + "047228f8313b24db2ad37a552cc15279dd515d156ac39f57da1a4d4dd40a4a61", + "c1068c4c44619ac9fa1294475de5f42d1f9257984c7792fdda241d7b127be8c8", + "3d8288b5df34654ba97c23e1914a1a2392d4798be05aa420895dddd2eecbd953" + }, + { + "b75e9a8a5f24d3299a196cb4b4124a211dd4f253a9d227290b0b3f72313a5532", + "041f1e202aa64ecb9151272257837b9b06866816246143d5fcd2567bd764d77a", + "04acf490916df2031404a4b2ecc50f5c43d7d0838a8aef5cbafb370df6467ea6", + "e616286d0f2729f3fa26a5f4d89fa03214b4310c7f7a40b9ed703361e6b40b3e", + "98f87650c039a601e875ca924b8fd48a17b443da3ce0536bef019bdc20db2c1b", + "e616286d0f2729f3fa26a5f4d89fa03214b4310c7f7a40b9ed703361e6b40b3e98f87650c039a601e875ca924b8fd48a17b443da3ce0536bef019bdc20db2c1b", + "0163e0f0202685ec96529cf5c5edf69327a292f82c77ef3204814c6e939c9b7e", + "784ffe8b8cd62e7be3e2b0a34e38ec63cd6b9739a2573c312daef3d7bb3ae262", + "fb721350fa746a9dab381c534eeeeb9edf49f07dd5fb397fee46313c2c8c5c40" + }, + { + "683f1521a8c6f76d4835f87ecee51cf899ec71555102e595e5bc2c8ed6c1ff82", + "07917c5521adf7d07f284e376312f992ac71721cc162ecae09f5a53726e6b887", + "080df08692a104f94ce208a328cdbcd9ed0b073d3c850987a4101c64d3c97f6c", + "c9305843719d27ca9a35bd165651b210a087fe0a6c3e906dbb7e3018ec016e39", + "cfd751257c062c86b1e4461ed2370ec98dd4f2e7e22331ce315f6ee1c42544c1", + "c9305843719d27ca9a35bd165651b210a087fe0a6c3e906dbb7e3018ec016e39cfd751257c062c86b1e4461ed2370ec98dd4f2e7e22331ce315f6ee1c42544c1", + "0428f0f7d4a43973d7535c47adbd16752fd54105ecf1e6e346a1ac029c4ca85b", + "b4c1fa6741f7250188c42947a9012c0ca7f863695a93b1d71fb22dc063143c77", + "cbcb4371bc45865bcdd40ab17f0bd7b015dc0a5328850c203cbf669fed38cb90" + }, + { + "c81b034031656bfceb51300a0088b012eafe1700b22f115d747e23d5e76f66dd", + "078e175c2a111900c703685793183696fe8bf0ad341e285179fee178d694129a", + "0041f378cfc074626c63ff318cf79faf18162c030f8655e36c53890a032e2457", + "fee8c30df362ec50f8169441c95b860a00c8b9d57f020a9946c54ab47484c026", + "aa46f2468269142649f58c42a272cbbdf3791c2dc0242c715ccd12acda2ab361", + "fee8c30df362ec50f8169441c95b860a00c8b9d57f020a9946c54ab47484c026aa46f2468269142649f58c42a272cbbdf3791c2dc0242c715ccd12acda2ab361", + "04c6e8fc3a669acacc5b0b078125cca65ca63424c06b3ac36565a65280958c3b", + "c0cf9084ad644b5005fac345a40b38392759194bd7488f434af8ecbac9b6224f", + "752b5325119879d1427d8df427e28206a59819acc69e4003db656812bda6cc4e" + }, + { + "9e0de76b502d8fea848086e90cb776832516e68fa7715758410a11dac33279a5", + "0590d7a189c8de9fa423dda7cd2c9d7513e47c76ae52f89ce41e01185fe1e6bb", + "0be98efdb1a394a97922fbc9b687fd984d0db6cd4bba37a00157528fe0ba9b96", + "f83d80b645d4fda5c5e5b598ce855f96abc8fb7c385a406f8c2d32b1cc4b089b", + "3fa04bab93e6a17eb3b98b0371ac4c46194b1ab3b0f5472ae76e830e51b068c2", + "f83d80b645d4fda5c5e5b598ce855f96abc8fb7c385a406f8c2d32b1cc4b089b3fa04bab93e6a17eb3b98b0371ac4c46194b1ab3b0f5472ae76e830e51b068c2", + "06009065dc3b4eb05b3c238d20e2e6d897a2bd13e0359846c80fc0814b22667e", + "fa46321b8da3da8282df1f8cdb980be5c9606e55200c110c8b500b330f84ecfc", + "a31441f468d12e4ced4677f7e1b2b89e6d6efe4dec940f13d8d4880034282e51" + }, + { + "6171008a41c077d30853dbd4a365d44f5df46e3d84974acf64aec45d3dfa8518", + "06074714498be4d8c55ca6b25de8ec2be2d4d60edc016f2f339db397a6e06cc6", + "05b9dfc1859fe50046e6dd85dd496baeee26b7595732c422d2c2bd63eb8daf97", + "8c3337ccc68bf985094ff4099f40d136662e840adcad8b3e4398ac1c2e3cc30b", + "a3f9d913b854a7f8bfea0575b5447dad7caeb5384bcf127178ba2bfffe99cfd0", + "8c3337ccc68bf985094ff4099f40d136662e840adcad8b3e4398ac1c2e3cc30ba3f9d913b854a7f8bfea0575b5447dad7caeb5384bcf127178ba2bfffe99cfd0", + "0688006785e98dbc7754eff5e8077abbda8e3c731f91ad1a14d69fc5d2627783", + "ff403d8ab3394b14a1d1dc35c07797755e27a39ad140ab32d046fc8f4636c74c", + "649b86129fd4ca89cff804a98f63cc023200f9aeb486b9cd60458b456ec96eac" + }, + { + "eb18bb634960b813b7d876fb30fc13fb0786d6a43d4b66622c56665d2fbdaf76", + "03f29b56313cda55b140e0438564846653cec507c32e605f12de186cb4520214", + "0d2b2af96208d61b73c29ee85899a9bd1d05d4bf3758c3b56e123eb5787bc338", + "c1a69c99d0b31e6e1f8084fd19985144a66fa7985614dfc524d2f8d2da293a3a", + "6356136ea5c85f2c082c42e5db40de5f788eb5cf94992cc8d1f2391d1a646596", + "c1a69c99d0b31e6e1f8084fd19985144a66fa7985614dfc524d2f8d2da293a3a6356136ea5c85f2c082c42e5db40de5f788eb5cf94992cc8d1f2391d1a646596", + "05ad202783a7ecbb734eb405831a2a2fe32d6916135451a4e81a825cf6152666", + "12e5048f89d52d5ed178d5da49e4e7fe6d597bffc855b56c72334f04a462c4d1", + "566399b7710d75a20246d7e36fa3b4f4d3b0d7b7ced586be9f31c78fb1688595" + }, + { + "6ed609b68227ccdb65f13f4119301fd03f392276aa50e6c5fbc1dd24eb448a6e", + "0b5ee2715d224575fa47846bf5a6655d2f377c5a42e6e6561b1eef9f181c9909", + "006d75e2183b7d4152c14a84e88ad44c67707176e0abd9a3c69c3cfe96afcfff", + "57fe8e536aa4d64b2af0186be557aedabc88bc68471d1f92346bef86af459e10", + "431cee7e49cf5097ea1c8c56b2e54fa9cdbb63310fac0a2b48a55842ab335e8e", + "57fe8e536aa4d64b2af0186be557aedabc88bc68471d1f92346bef86af459e10431cee7e49cf5097ea1c8c56b2e54fa9cdbb63310fac0a2b48a55842ab335e8e", + "033013c1407fecb398c16f144e4bba03efd5cbbf8ceead92f05b57cdd1a6b44c", + "7ddee0eda90360658301542af1dbb08566db8e4bf0e4d3a8679fa01cf6361799", + "8ddca1d54e02bb2d13a3f0a485820a20908427c2067c78490a3072afb6b4efa9" + }, + { + "7b94d9ceffcf2d7a764a16146ce7ce90cd710f717cd9812b8bc85555364c06f3", + "0c8505f1ef2b5c18d93d95674b531098bc2bba4f18410e6920a0b016ea5009ba", + "0bb1f088ff779da607dc22968ff239d6878e9f605dd3185861b4679ba0aa5702", + "aeeeb23556eba42bf22632c7ba5f9871f9b34196f6c87091fb82111668edfd41", + "36b6122e6dbe1d04f6cfa16edd03f25e54397cfa02f1d2fc3c529fd3888980e2", + "aeeeb23556eba42bf22632c7ba5f9871f9b34196f6c87091fb82111668edfd4136b6122e6dbe1d04f6cfa16edd03f25e54397cfa02f1d2fc3c529fd3888980e2", + "03a3e8d415ef18aca5f4b16e2d2b89ec7ec1439b062d217896625aeb0bbbb8a4", + "88fcbdd13b18a27f1f0c0507384e1716d5004ef0edcbbe7e5fce63c3ba29a8c4", + "cfbe977de06351c864bd35b942e1715d0f9efa78b2e3c79d28e0f5d7f1b5b03c" + }, + { + "4c7da515de11409b3101665a6c91fbccfc9f4ff77076b8dd71bcfef34533e9be", + "000ff60e8c05f36fd4edb66a82e22152dd6d5cd887234e8a648e51acb3c7ca55", + "0212a1850d2830b52aea267cc188fa5ed0a67d1e0e8a134cdee8900217be212c", + "49a722eee2330bd3ec9f00ce2d76f51ede01db1f2584f02f941eff01ca3732af", + "531bf079f72b51eeeec7a8ba7e17687fe1bdf2dd437abeed7d80d23675dcc89b", + "49a722eee2330bd3ec9f00ce2d76f51ede01db1f2584f02f941eff01ca3732af531bf079f72b51eeeec7a8ba7e17687fe1bdf2dd437abeed7d80d23675dcc89b", + "01b0c1949caefe5b1ae6619165c2c8b844cc48b60fad8bbc225301f1a5482236", + "1c7b2453eb96c94a07337a4d6e233fa4d8bdab2f0129d5ebdbbbb6ff92fa5988", + "fe7f51c8d744d29323862d5c80e58575d539995413f01d99e35ceccf1464e884" + }, + { + "bb20c0b30bfd58582994c99d5096928574786600de6e5068d6578793f810778c", + "05605dc6766494b5adf665fc306dddefe84b8556fc7486879d5d1b06987f95ba", + "0c8c40a559828fe9b87e650eaa4f37eebd3a0076085b141d232a49471d5e80bd", + "0e2f36dc1d56cc8000175f330606849ad725aa8dfa775e618d57e76992dea3ef", + "276a2d02fc6dd50fb4e7e471fd46e2c4c0545d48fd34dcdaf8b1361ea0f73618", + "0e2f36dc1d56cc8000175f330606849ad725aa8dfa775e618d57e76992dea3ef276a2d02fc6dd50fb4e7e471fd46e2c4c0545d48fd34dcdaf8b1361ea0f73618", + "072f1e5b04f3af53d19728dd147efcbf3693e5471798d44a0d304c546aa653e3", + "87f244bce4971951a25a9cfcf5b17300314f737c274ff49ec3c7c1795759538b", + "6a8e8ccd89e684f41656985c642316a7457865c03a916cdb5bbe84e0174f0d4b" + }, + { + "9c9a9edaeb2d4c671f797d80f253a0b274bdd635b1e09d4223d05c84bb69f42c", + "09e1d308114cacc0fbf65167b4654196a92d51622411e9bb63103c188d6948e7", + "0d72a3f8abfa526afd1d7a0f529ccfe92439fe784a331e9ae5ba21beebdc2d7b", + "07313b1fe45c897f9a3cb09b0bfde097cbec0ea2912f14e0229cd4230fb6e9be", + "2421e49f1195516b6366fee8a6f0e617b24284c3942bdfe121755c8ca31154cd", + "07313b1fe45c897f9a3cb09b0bfde097cbec0ea2912f14e0229cd4230fb6e9be2421e49f1195516b6366fee8a6f0e617b24284c3942bdfe121755c8ca31154cd", + "050305d983a25a349b8c6a98a37573eba02bc02db5d606d398f5a4698073e3e4", + "a6e0bd607f94019525a75c9f3b87b40e0844757cd782fa8abebadbffe378c133", + "911f0a4dc6db375ab01357959581f626a3d8d0681b764c5ee960f9dd542719c3" + }, + { + "5ecd495e8d1a3aa3fe9a0f038c2b984d361df073202d2d9e48d2a592960adf8c", + "0bd9d324949da2664db619d7707a4874fc8b7687d4be046e7d776fa791792c11", + "04b597a4a3f6e60eb59ecd09828224ab9a849c1a0d0229e4cbd5cfed6db2979b", + "8f2111bea95fb2056fd78b9b035c1e1c776f01aea3777a0ca4c28f0f87c98758", + "91e874444a8c49b5b46ba831db44f783c6d172b81df00ee05bf47bb9baaecc81", + "8f2111bea95fb2056fd78b9b035c1e1c776f01aea3777a0ca4c28f0f87c9875891e874444a8c49b5b46ba831db44f783c6d172b81df00ee05bf47bb9baaecc81", + "064f88f47275e77c362390ef04b1bd657e31fe1a0bdce74b84e494e6276bb625", + "cc691cbcb17a713f67eb2977d735c5f34e28ca885d10e78a93106198f333fd85", + "a944ec349e29edde71ce202384d3b2b65cb7e86f9ec43d7757097e8597f47ced" + }, + // Keys from Zemu tests + { + "e89dab1a3e40eb2c120defd577ce743ba0893df22aaec97de46a62cd934b1257", + "0ddce28653e08c129da8b1c6485a8a7f2d987163742c91a26209014cf92ee6de", + "010cd3f2276aeac2b3a6a98278bacb878606d4fa3301f70713425b5761facdd4", + "bdd8b8dd269e11c0f9ff401e284fb19ea2cfc008c791590c08e3616865ece534", + "fc912a61108229dc6af38809fa3c938d6b3d0bce2de236751a445a72f061e488", + "bdd8b8dd269e11c0f9ff401e284fb19ea2cfc008c791590c08e3616865ece534fc912a61108229dc6af38809fa3c938d6b3d0bce2de236751a445a72f061e488", + "043e34aa9a6323b82a899d984081ce53e3bb47b2ffa18a0dcfa6910a6d278c73", + "316c96f058f7e188acc90d90d1d765bd9b9ce9e5fa3655c74e8450df0191ee21", + "b3ad098e86bc31de35ec5a77cce6aed08d5336bf273abef5e7eb420278a0c19c" + }, +}; + + +TEST(Keys, SpendingAuthorizationKey) { + for (const auto& testcase : testvectors) { + keys_t keys = {0}; + + // Read spendingKey from testvectors + parseHexString(keys.spendingKey, sizeof(keys.spendingKey), testcase.spendingKey.c_str()); + + // Compute ask and nsk + ASSERT_EQ(convertKey(keys.spendingKey, MODIFIER_ASK, keys.ask, true), parser_ok); + const string ask = toHexString(keys.ask, 32); + EXPECT_EQ(ask, testcase.spendAuthorizationKey); + + ASSERT_EQ(convertKey(keys.spendingKey, MODIFIER_NSK, keys.nsk, true), parser_ok); + const string nsk = toHexString(keys.nsk, 32); + EXPECT_EQ(nsk, testcase.proofAuthorizationKey); + + // Compute ak and nk + generate_key(keys.ask, SpendingKeyGenerator, keys.ak); + const string ak = toHexString(keys.ak, 32); + EXPECT_EQ(ak, testcase.authorizing_key); + + generate_key(keys.nsk, ProofGenerationKeyGenerator, keys.nk); + const string nk = toHexString(keys.nk, 32); + EXPECT_EQ(nk, testcase.nullifier_deriving_key); + + const string viewKey = ak + nk; + EXPECT_EQ(viewKey, testcase.viewKey); + + // Compute ivk and ovk + computeIVK(keys.ak, keys.nk, keys.ivk); + const string ivk = toHexString(keys.ivk, 32); + EXPECT_EQ(ivk, testcase.incomingViewingKey); + + ASSERT_EQ(convertKey(keys.spendingKey, MODIFIER_OVK, keys.ovk, false), parser_ok); + const string ovk = toHexString(keys.ovk, 32); + EXPECT_EQ(ovk, testcase.outgoingViewingKey); + + + ASSERT_EQ(generate_key(keys.ivk, PublicKeyGenerator, keys.address), parser_ok); + const string address = toHexString(keys.address, 32); + EXPECT_EQ(address, testcase.publicAddress); + } +} diff --git a/tests/parser_impl.cpp b/tests/parser_impl.cpp new file mode 100644 index 0000000..937d808 --- /dev/null +++ b/tests/parser_impl.cpp @@ -0,0 +1,63 @@ +/******************************************************************************* +* (c) 2018 - 2023 Zondax AG +* +* 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. +********************************************************************************/ + +// #{TODO} --> Apply tests that check this app's encoding/libraries + +#include "gmock/gmock.h" + +#include +#include +#include +#include "parser_txdef.h" +#include "parser.h" +#include "parser_impl.h" + +using namespace std; + +TEST(SCALE, ReadBytes) { + parser_context_t ctx; + parser_tx_t tx_obj; + parser_error_t err; + uint8_t buffer[100]; + auto bufferLen = parseHexString( + buffer, + sizeof(buffer), + "45" + "123456" + "12345678901234567890" + ); + + parser_parse(&ctx, buffer, bufferLen, &tx_obj); + + // uint8_t bytesArray[100] = {0}; + // err = _readBytes(&ctx, bytesArray, 1); + // EXPECT_EQ(err, parser_ok) << parser_getErrorDescription(err); + // EXPECT_EQ(bytesArray[0], 0x45); + + // uint8_t testArray[3] = {0x12, 0x34, 0x56}; + // err = _readBytes(&ctx, bytesArray+1, 3); + // EXPECT_EQ(err, parser_ok) << parser_getErrorDescription(err); + // for (uint8_t i = 0; i < 3; i++) { + // EXPECT_EQ(testArray[i], bytesArray[i+1]); + // } + + // uint8_t testArray2[10] = {0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90}; + // err = _readBytes(&ctx, bytesArray+4, 10); + // EXPECT_EQ(err, parser_ok) << parser_getErrorDescription(err); + // for (uint8_t i = 0; i < 10; i++) { + // EXPECT_EQ(testArray2[i], bytesArray[i+4]); + // } +} diff --git a/tests/testcases.json b/tests/testcases.json new file mode 100644 index 0000000..bc16c98 --- /dev/null +++ b/tests/testcases.json @@ -0,0 +1,74 @@ +[ + { + "index": 0, + "name": "Asset_Freeze", + "blob": "8ba466616464c4204b2a4ad9d4d900ea16f9dcee534b9c0189daa1acbccace73d794bf168b8a73e3a466616964ce000b6717a3666565ce001e163fa26676ce000153c8a367656eac6d61696e6e65742d76312e30a26768c420c061c4d8fc1dbdded2d7604be4568e3f6d041987ac37bde4b620b5ab39248adfa26c76ce00018b66a46e6f7465c41c48656c6c6f2074686572652c20746869732069732061206e6f746521a572656b6579c4206a08f935de3bb9eb65b9d206598f5d34419257491c4024ca5f88ea73749de231a3736e64c4209fa4543b7caf05ad7334a5a74033acdc130137d7afa1f6733509f6d4377c30d7a474797065a46166727a", + "output": [ + "0 | Txn type : Asset Freeze", + "1 | Sender [1/2] : T6SFIO34V4C224ZUUWTUAM5M3QJQCN6XV6Q7M4", + "1 | Sender [2/2] : ZVBH3NIN34GDLSK2ETKM", + "2 | Rekey to [1/2] : WARNING: NIEPSNO6HO46WZNZ2IDFTD25GRAZE", + "2 | Rekey to [2/2] : V2JDRACJSS7RDVHG5E54IYZOIA3MI", + "3 | Fee : ALGO 1.971775", + "4 | Genesis ID : mainnet-v1.0", + "5 | Genesis hash [1/2] : wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qz", + "5 | Genesis hash [2/2] : kkit8=", + "6 | Note : 28 bytes", + "7 | Asset ID : 747287", + "8 | Asset account [1/2] : JMVEVWOU3EAOUFXZ3TXFGS44AGE5VINMXTFM44", + "8 | Asset account [2/2] : 6XSS7RNC4KOPR5HR537U", + "9 | Freeze flag : Unfrozen" + ], + "output_expert": [ + "0 | Txn type : Asset Freeze", + "1 | Sender [1/2] : T6SFIO34V4C224ZUUWTUAM5M3QJQCN6XV6Q7M4", + "1 | Sender [2/2] : ZVBH3NIN34GDLSK2ETKM", + "2 | Rekey to [1/2] : WARNING: NIEPSNO6HO46WZNZ2IDFTD25GRAZE", + "2 | Rekey to [2/2] : V2JDRACJSS7RDVHG5E54IYZOIA3MI", + "3 | Fee : ALGO 1.971775", + "4 | Genesis ID : mainnet-v1.0", + "5 | Genesis hash [1/2] : wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qz", + "5 | Genesis hash [2/2] : kkit8=", + "6 | Note : 28 bytes", + "7 | Asset ID : 747287", + "8 | Asset account [1/2] : JMVEVWOU3EAOUFXZ3TXFGS44AGE5VINMXTFM44", + "8 | Asset account [2/2] : 6XSS7RNC4KOPR5HR537U", + "9 | Freeze flag : Unfrozen" + ] + }, + { + "index": 1, + "name": "Asset_Freeze", + "blob": "8aa466616464c4204b2a4ad9d4d900ea16f9dcee534b9c0189daa1acbccace73d794bf168b8a73e3a466616964ce00038ce6a3666565ce00272460a26676cd6584a367656eac6d61696e6e65742d76312e30a26768c420c061c4d8fc1dbdded2d7604be4568e3f6d041987ac37bde4b620b5ab39248adfa26c76ce0001cd3ca572656b6579c42075895bccedfa0c747cea13601babff1de5cecf4dd7a3fe86eaeccb74b60124dea3736e64c420aba83eaca22287f68cad3906661fc72b1c3537dbbae4ba2c6e35a51c03c87f80a474797065a46166727a", + "output": [ + "0 | Txn type : Asset Freeze", + "1 | Sender [1/2] : VOUD5LFCEKD7NDFNHEDGMH6HFMODKN63XLSLUL", + "1 | Sender [2/2] : DOGWSRYA6IP6ADQBWT6Y", + "2 | Rekey to [1/2] : WARNING: OWEVXTHN7IGHI7HKCNQBXK77DXS45", + "2 | Rekey to [2/2] : T2N26R75BXK5TFXJNQBETPAS7VK74", + "3 | Fee : ALGO 2.565216", + "4 | Genesis ID : mainnet-v1.0", + "5 | Genesis hash [1/2] : wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qz", + "5 | Genesis hash [2/2] : kkit8=", + "6 | Asset ID : 232678", + "7 | Asset account [1/2] : JMVEVWOU3EAOUFXZ3TXFGS44AGE5VINMXTFM44", + "7 | Asset account [2/2] : 6XSS7RNC4KOPR5HR537U", + "8 | Freeze flag : Unfrozen" + ], + "output_expert": [ + "0 | Txn type : Asset Freeze", + "1 | Sender [1/2] : VOUD5LFCEKD7NDFNHEDGMH6HFMODKN63XLSLUL", + "1 | Sender [2/2] : DOGWSRYA6IP6ADQBWT6Y", + "2 | Rekey to [1/2] : WARNING: OWEVXTHN7IGHI7HKCNQBXK77DXS45", + "2 | Rekey to [2/2] : T2N26R75BXK5TFXJNQBETPAS7VK74", + "3 | Fee : ALGO 2.565216", + "4 | Genesis ID : mainnet-v1.0", + "5 | Genesis hash [1/2] : wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qz", + "5 | Genesis hash [2/2] : kkit8=", + "6 | Asset ID : 232678", + "7 | Asset account [1/2] : JMVEVWOU3EAOUFXZ3TXFGS44AGE5VINMXTFM44", + "7 | Asset account [2/2] : 6XSS7RNC4KOPR5HR537U", + "8 | Freeze flag : Unfrozen" + ] + } +] \ No newline at end of file diff --git a/tests/ui_tests.cpp b/tests/ui_tests.cpp new file mode 100644 index 0000000..aeca1ef --- /dev/null +++ b/tests/ui_tests.cpp @@ -0,0 +1,139 @@ +/******************************************************************************* +* (c) 2018 - 2023 Zondax AG +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ + +#include "gmock/gmock.h" + +#include +#include +#include +#include +#include +#include "parser.h" +#include "app_mode.h" +#include "utils/common.h" + +using ::testing::TestWithParam; + +typedef struct { + uint64_t index; + std::string name; + std::string blob; + std::vector expected; + std::vector expected_expert; +} testcase_t; + +class JsonTestsA : public ::testing::TestWithParam { +public: + struct PrintToStringParamName { + template + std::string operator()(const testing::TestParamInfo &info) const { + auto p = static_cast(info.param); + std::stringstream ss; + ss << p.index << "_" << p.name; + return ss.str(); + } + }; +}; + +// Retrieve testcases from json file +std::vector GetJsonTestCases(std::string jsonFile) { + auto answer = std::vector(); + + Json::CharReaderBuilder builder; + Json::Value obj; + + std::string fullPathJsonFile = std::string(TESTVECTORS_DIR) + jsonFile; + + std::ifstream inFile(fullPathJsonFile); + if (!inFile.is_open()) { + return answer; + } + + // Retrieve all test cases + JSONCPP_STRING errs; + Json::parseFromStream(builder, inFile, &obj, &errs); + std::cout << "Number of testcases: " << obj.size() << std::endl; + + for (int i = 0; i < obj.size(); i++) { + + auto outputs = std::vector(); + for (auto s : obj[i]["output"]) { + outputs.push_back(s.asString()); + } + + auto outputs_expert = std::vector(); + for (auto s : obj[i]["output_expert"]) { + outputs_expert.push_back(s.asString()); + } + + answer.push_back(testcase_t{ + obj[i]["index"].asUInt64(), + obj[i]["name"].asString(), + obj[i]["blob"].asString(), + outputs, + outputs_expert + }); + } + + return answer; +} + +void check_testcase(const testcase_t &tc, bool expert_mode) { + + app_mode_set_expert(expert_mode); + + parser_context_t ctx; + parser_error_t err; + + uint8_t buffer[5000]; + uint16_t bufferLen = parseHexString(buffer, sizeof(buffer), tc.blob.c_str()); + + parser_tx_t tx_obj; + memset(&tx_obj, 0, sizeof(tx_obj)); + + err = parser_parse(&ctx, buffer, bufferLen, &tx_obj); + ASSERT_EQ(err, parser_ok) << parser_getErrorDescription(err); + + auto output = dumpUI(&ctx, 39, 39); + + std::cout << std::endl; + for (const auto &i : output) { + std::cout << i << std::endl; + } + std::cout << std::endl << std::endl; + + std::vector expected = app_mode_expert() ? tc.expected_expert : tc.expected; + + // #{TODO} --> After updating testvector, enable this part + #if 0 + EXPECT_EQ(output.size(), expected.size()); + for (size_t i = 0; i < expected.size(); i++) { + if (i < output.size()) { + EXPECT_THAT(output[i], testing::Eq(expected[i])); + } + } + #endif +} + +INSTANTIATE_TEST_SUITE_P + +( + JsonTestCasesCurrentTxVer, + JsonTestsA, + ::testing::ValuesIn(GetJsonTestCases("testcases.json")), + JsonTestsA::PrintToStringParamName() +); +TEST_P(JsonTestsA, CheckUIOutput_CurrentTX_Expert) { check_testcase(GetParam(), true); } diff --git a/tests/utils/common.cpp b/tests/utils/common.cpp new file mode 100644 index 0000000..3f16893 --- /dev/null +++ b/tests/utils/common.cpp @@ -0,0 +1,66 @@ +/******************************************************************************* +* (c) 2018 - 2023 Zondax AG +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ +#include +#include +#include +#include "common.h" + +std::vector dumpUI(parser_context_t *ctx, + uint16_t maxKeyLen, + uint16_t maxValueLen) { + auto answer = std::vector(); + + uint8_t numItems; + parser_error_t err = parser_getNumItems(ctx, &numItems); + if (err != parser_ok) { + return answer; + } + + for (uint16_t idx = 0; idx < numItems; idx++) { + char keyBuffer[1000]; + char valueBuffer[1000]; + uint8_t pageIdx = 0; + uint8_t pageCount = 1; + + while (pageIdx < pageCount) { + std::stringstream ss; + + err = parser_getItem(ctx, + idx, + keyBuffer, maxKeyLen, + valueBuffer, maxValueLen, + pageIdx, &pageCount); + + ss << idx << " | " << keyBuffer; + if (pageCount > 1) { + ss << " [" << (int) pageIdx + 1 << "/" << (int) pageCount << "]"; + } + ss << " : "; + + if (err == parser_ok) { + ss << valueBuffer; + } else { + ss << parser_getErrorDescription(err); + } + + answer.push_back(ss.str()); + + pageIdx++; + } + } + + return answer; +} diff --git a/tests/utils/common.h b/tests/utils/common.h new file mode 100644 index 0000000..d9a0cc5 --- /dev/null +++ b/tests/utils/common.h @@ -0,0 +1,21 @@ +/******************************************************************************* +* (c) 2018 - 2023 Zondax AG +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ +#include +#include + +std::vector dumpUI(parser_context_t *ctx, + uint16_t maxKeyLen, + uint16_t maxValueLen); diff --git a/tests_zemu/.editorconfig b/tests_zemu/.editorconfig new file mode 100644 index 0000000..4a7ea30 --- /dev/null +++ b/tests_zemu/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/tests_zemu/.eslintrc.js b/tests_zemu/.eslintrc.js new file mode 100644 index 0000000..caf9805 --- /dev/null +++ b/tests_zemu/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'jest'], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:jest/recommended', 'prettier'], + env: { + browser: true, + es6: true, + node: true, + }, + settings: {}, + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly', + }, + parserOptions: {}, + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/ban-ts-comment': 'off', + }, +} diff --git a/tests_zemu/.gitignore b/tests_zemu/.gitignore new file mode 100644 index 0000000..8474aa6 --- /dev/null +++ b/tests_zemu/.gitignore @@ -0,0 +1,71 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ +/docs/.vuepress/dist + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +\.idea/ + +.vscode + +TODO\.md + +\dist +snapshots-tmp diff --git a/tests_zemu/.npmignore b/tests_zemu/.npmignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/tests_zemu/.npmignore @@ -0,0 +1 @@ +node_modules diff --git a/tests_zemu/.prettierignore b/tests_zemu/.prettierignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/tests_zemu/.prettierignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/tests_zemu/.prettierrc b/tests_zemu/.prettierrc new file mode 100644 index 0000000..6523ceb --- /dev/null +++ b/tests_zemu/.prettierrc @@ -0,0 +1,9 @@ +{ + "trailingComma": "all", + "singleQuote": true, + "arrowParens": "avoid", + "semi": false, + "useTabs": false, + "printWidth": 140, + "tabWidth": 2 +} diff --git a/tests_zemu/globalsetup.js b/tests_zemu/globalsetup.js new file mode 100644 index 0000000..a7875ea --- /dev/null +++ b/tests_zemu/globalsetup.js @@ -0,0 +1,16 @@ +import Zemu from '@zondax/zemu' + +const catchExit = async () => { + process.on('SIGINT', () => { + Zemu.stopAllEmuContainers(function () { + process.exit() + }) + }) +} + +module.exports = async () => { + await catchExit() + await Zemu.checkAndPullImage() + await Zemu.stopAllEmuContainers() +} +// diff --git a/tests_zemu/jest.config.js b/tests_zemu/jest.config.js new file mode 100644 index 0000000..c4a20eb --- /dev/null +++ b/tests_zemu/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transformIgnorePatterns: ['^.+\\.js$'], +} diff --git a/tests_zemu/jest.js b/tests_zemu/jest.js new file mode 100644 index 0000000..e69de29 diff --git a/tests_zemu/package.json b/tests_zemu/package.json new file mode 100644 index 0000000..6871fdb --- /dev/null +++ b/tests_zemu/package.json @@ -0,0 +1,44 @@ +{ + "name": "integration-tests", + "author": "Zondax AG", + "license": "Apache-2.0", + "version": "1.0.0", + "description": "", + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/Zondax/ledger-template" + }, + "keywords": [ + "Zondax", + "Ledger" + ], + "scripts": { + "clean": "ts-node tests/pullImageKillOld.ts", + "test": "yarn clean && jest tests/standard.test.ts" + }, + "dependencies": { + "@zondax/ledger-ironfish": "../js", + "@zondax/zemu": "^0.47.0" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "@types/ledgerhq__hw-transport": "^4.21.8", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "blakejs": "^1.1.1", + "crypto-js": "4.2.0", + "ed25519-supercop": "^2.0.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jest": "^27.6.1", + "eslint-plugin-prettier": "^5.1.2", + "jest": "29.7.0", + "jssha": "^3.2.0", + "prettier": "^3.1.1", + "ts-jest": "^29.0.3", + "ts-node": "^10.9.2", + "typescript": "^5.4.2" + } +} diff --git a/tests_zemu/snapshots/s-mainmenu/00000.png b/tests_zemu/snapshots/s-mainmenu/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..c931308fee83737bab4fc62f60f79cd2813201cd GIT binary patch literal 447 zcmV;w0YLtVP)OU2SeY;%C%^|Fqn zl>esG9SNEMcq>H}Z^%0?_U){Nd$FY;?<<8k9ZOTW9UqB{fQFRmjBw-Zl#&EhQ1jgL zC8M?o=f<`V=vFBpN|D_4Moye5h%RxKtx)j^{~FLd9o5HTW~$obR)&?Ha{`$9)LaFD z2CKG@6`DTvouQ@^{dWl!Ny&MD pfXR=cqCdzKb2GX77e!IT@&f&T&P>fQ_aFcO002ovPDHLkV1mIg%)I~r literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-mainmenu/00001.png b/tests_zemu/snapshots/s-mainmenu/00001.png new file mode 100644 index 0000000000000000000000000000000000000000..853a0611df7d142612629325deac6359141a8454 GIT binary patch literal 513 zcmV+c0{;DpP)6c5TwWk|hN-+CImIrgg1 zw1oXFP3W`}(p{;;np2y>o@KX28M;{)g7SLQ7ZIH&oO93WlO5L7Dox%$`ad=Ucnv-x z7GEL050GJQ2%>E6z#{t*P)@Cuzl2!Z6r1A8=Zj0sD^k2o7nqV z8zGU3oL3RGl(`nN`~uw#rvYT@Y1e)&8ITdIRwBDne3VtVYJ~KkgrU|>?+lE4HSH&V zE9X*uAlmT4nL<+tN}n1zU0n9n=iSxW-qR-DQWibPNN5~`hiz|fg=-sN%4M9<_Z#j$ zS$)@C<~5l0D4))r^z=>GQmDLN)(V3l2!bF8f&n00000NkvXXu0mjf DX#(my literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-mainmenu/00002.png b/tests_zemu/snapshots/s-mainmenu/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..802e312d24065c6d5222b36b5559e4533cd7e93f GIT binary patch literal 500 zcmVOzr z+rOc$bH;tll))Oo+yE5Hf_Oafb17l=rSRB+y$l02V|__aqO<;{`qop~&9SRa z(-QW!RH2`p(AX6lR-MKSwk(@%^r4&UBq+~EdlJ#_gx}mV`(%UKu*#74_x?{?0K5So zp~Wlc`v4#2N)QDue+HD4vw$)PyE7JOw-#OW53rX%T`4<9}W%_RF9E*h?(Wbeh=Fd?t{zr(5rLa)Tq~o0iarh=fG{ z!)@FyPi?u$WCl$4LxOC|K#9#D2>^QsV_=MaOne(E-GuGKLe`OJqd#;JKxJ!}yG4W% z4Us{a3 z{v8qo=L&%5g4J23ohI7GwhcVKcX_o2i`|HIrd8ETmGzi%7L literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-mainmenu/00003.png b/tests_zemu/snapshots/s-mainmenu/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..853a0611df7d142612629325deac6359141a8454 GIT binary patch literal 513 zcmV+c0{;DpP)6c5TwWk|hN-+CImIrgg1 zw1oXFP3W`}(p{;;np2y>o@KX28M;{)g7SLQ7ZIH&oO93WlO5L7Dox%$`ad=Ucnv-x z7GEL050GJQ2%>E6z#{t*P)@Cuzl2!Z6r1A8=Zj0sD^k2o7nqV z8zGU3oL3RGl(`nN`~uw#rvYT@Y1e)&8ITdIRwBDne3VtVYJ~KkgrU|>?+lE4HSH&V zE9X*uAlmT4nL<+tN}n1zU0n9n=iSxW-qR-DQWibPNN5~`hiz|fg=-sN%4M9<_Z#j$ zS$)@C<~5l0D4))r^z=>GQmDLN)(V3l2!bF8f&n00000NkvXXu0mjf DX#(my literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-mainmenu/00004.png b/tests_zemu/snapshots/s-mainmenu/00004.png new file mode 100644 index 0000000000000000000000000000000000000000..ae1ef43fe351969992ccc7d4a92548e6f167a886 GIT binary patch literal 438 zcmV;n0ZIOeP)(TFkV8x69q=hw7j%L1ONcwt!$mXa~?!Q+x|=(4&{e^ z>Z70Jd-M;8USGJ?k(-CS-So2U$yoLO z=7Or;(I2p;^_0Y25an$dsvR3G(MoHmIv_n-c?kv!vCAH0RehB<92LS gPyhe`0DyP$1IDz{G8c@oQ2+n{07*qoM6N<$g1QvL4gdfE literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-mainmenu/00005.png b/tests_zemu/snapshots/s-mainmenu/00005.png new file mode 100644 index 0000000000000000000000000000000000000000..14687cbc7efab47dead6cf89e548c7a3b7517490 GIT binary patch literal 536 zcmV+z0_XjSP)a|NmeY-bEb-d827k4QEv&A(U5uwpJ(<3WZg%4Lav>e?4~Z$+j0agXb9s zwboj@b?0bd+n?`1?6ENZW()9YjBTgBhJO-DgB97%cZsZAx4`^0Ig_z6HUc<6Tu?FA zHP^loLz=0Yo=>#1G~j=;dlgKL08mrh^)yP+9$X`sOnZ(%WPTuNhL{1e@@0J?PT%nw z#H`-ClOW-IfT=0Sb|M_tuHHF;xCPQqP?E@T{4&T`Wvl@}I1m?f?aJ*^auCJjAQ`|-1-Mu|vT8E%{Ro22r&Y`rHKD-i z5?6$xH%5B+Yw_9%14_s@Ky?HOkeT6wny7j{>VCRN+MU$Y>b)(b$@v> z)Bjow|0F3sYb*A;2r;c4F>y&##Zu6RqKk=(%rM+fQKZKf=O1obBfBnXl0uP)%^7{Q>}h)M-D`0!Ywi15WR0j7>>g1a!DSSni=o zaD0%l`Fy|OGr*`5ed=pH^Jt=HRh*C@1#zWDwZr*G8=H00b#4kG>u0BVF}}-N4(IIM z{9_ZaxO(xA!J7)6e{+9xDh5*!_pGejyI(;vfSoIwr{j$|3HgFxZ1V&Iki!mvsE?A1`lF<>*-WqL#)_nZfF-ijZ&y~nSk+9gy=-L z7+{RjF1bu%9hBS>$Np3)6ow-}Oe6k}JQPKXC+8!aSuWGgEvOcB*ym0Eb=Hm&a7r~) z%v7Qfns82U^*c3LTH>tY+lIag4|!g{d%TY1_}c2K9R`g>*m#`1{{Jo07m*VDAu~5+ efTAdhdAS3=U=S6)!0Y?~0000fMsIxbZ*Qbeh#!%WaOXK(!1E!8<*RlTM`l@^lV2*wuySxfMlG5 z{@>gmk?cp0_g`OrboE!OO$@#3`JO&lo7AgUp?$yfR0Yrc3qB7|XC&QFx+Z?~;G>Sj zwM=!NmUd5GqVvkdjb+JOtNj1&0ipuU7Ww}V?d+{()T|Y>nxUrcaM}MwLf6OL3-0az zpe;T1o|(ygmzSlN@0iZ*+cE25ZpPkwWmD#FtW^%1|Ef&(gT|vIn;vG6EB{PaUvCtB T#$=}FHW1g-)z4*}Q$iB}XOVQe literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-mainmenu/00008.png b/tests_zemu/snapshots/s-mainmenu/00008.png new file mode 100644 index 0000000000000000000000000000000000000000..616c3d4be49ec2dc4671a2afb7f91fc68032d19d GIT binary patch literal 488 zcmVP)%^7{Q>}h)M-D`0!Ywi15WR0j7>>g1a!DSSni=o zaD0%l`Fy|OGr*`5ed=pH^Jt=HRh*C@1#zWDwZr*G8=H00b#4kG>u0BVF}}-N4(IIM z{9_ZaxO(xA!J7)6e{+9xDh5*!_pGejyI(;vfSoIwr{j$|3HgFxZ1V&Iki!mvsE?A1`lF<>*-WqL#)_nZfF-ijZ&y~nSk+9gy=-L z7+{RjF1bu%9hBS>$Np3)6ow-}Oe6k}JQPKXC+8!aSuWGgEvOcB*ym0Eb=Hm&a7r~) z%v7Qfns82U^*c3LTH>tY+lIag4|!g{d%TY1_}c2K9R`g>*m#`1{{Jo07m*VDAu~5+ efTAdhdAS3=U=S6)!0Y?~0000a|NmeY-bEb-d827k4QEv&A(U5uwpJ(<3WZg%4Lav>e?4~Z$+j0agXb9s zwboj@b?0bd+n?`1?6ENZW()9YjBTgBhJO-DgB97%cZsZAx4`^0Ig_z6HUc<6Tu?FA zHP^loLz=0Yo=>#1G~j=;dlgKL08mrh^)yP+9$X`sOnZ(%WPTuNhL{1e@@0J?PT%nw z#H`-ClOW-IfT=0Sb|M_tuHHF;xCPQqP?E@T{4&T`Wvl@}I1m?f?aJ*^auCJjAQ`|-1-Mu|vT8E%{Ro22r&Y`rHKD-i z5?6$xH%5B+Yw_9%14_s@Ky?HOkeT6wny7j{>VCRN+MU$Y>b)(b$@v> z)Bjow|0F3sYb*A;2r;c4F>y&##Zu6RqKk=(%rM+fQKZKf=O1obBfBnXl0u(TFkV8x69q=hw7j%L1ONcwt!$mXa~?!Q+x|=(4&{e^ z>Z70Jd-M;8USGJ?k(-CS-So2U$yoLO z=7Or;(I2p;^_0Y25an$dsvR3G(MoHmIv_n-c?kv!vCAH0RehB<92LS gPyhe`0DyP$1IDz{G8c@oQ2+n{07*qoM6N<$g1QvL4gdfE literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-mainmenu/00011.png b/tests_zemu/snapshots/s-mainmenu/00011.png new file mode 100644 index 0000000000000000000000000000000000000000..853a0611df7d142612629325deac6359141a8454 GIT binary patch literal 513 zcmV+c0{;DpP)6c5TwWk|hN-+CImIrgg1 zw1oXFP3W`}(p{;;np2y>o@KX28M;{)g7SLQ7ZIH&oO93WlO5L7Dox%$`ad=Ucnv-x z7GEL050GJQ2%>E6z#{t*P)@Cuzl2!Z6r1A8=Zj0sD^k2o7nqV z8zGU3oL3RGl(`nN`~uw#rvYT@Y1e)&8ITdIRwBDne3VtVYJ~KkgrU|>?+lE4HSH&V zE9X*uAlmT4nL<+tN}n1zU0n9n=iSxW-qR-DQWibPNN5~`hiz|fg=-sN%4M9<_Z#j$ zS$)@C<~5l0D4))r^z=>GQmDLN)(V3l2!bF8f&n00000NkvXXu0mjf DX#(my literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-mainmenu/00012.png b/tests_zemu/snapshots/s-mainmenu/00012.png new file mode 100644 index 0000000000000000000000000000000000000000..c931308fee83737bab4fc62f60f79cd2813201cd GIT binary patch literal 447 zcmV;w0YLtVP)OU2SeY;%C%^|Fqn zl>esG9SNEMcq>H}Z^%0?_U){Nd$FY;?<<8k9ZOTW9UqB{fQFRmjBw-Zl#&EhQ1jgL zC8M?o=f<`V=vFBpN|D_4Moye5h%RxKtx)j^{~FLd9o5HTW~$obR)&?Ha{`$9)LaFD z2CKG@6`DTvouQ@^{dWl!Ny&MD pfXR=cqCdzKb2GX77e!IT@&f&T&P>fQ_aFcO002ovPDHLkV1mIg%)I~r literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-show_address/00000.png b/tests_zemu/snapshots/s-show_address/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..e8132487f464634e24f4a6a0125274b5a92471a0 GIT binary patch literal 604 zcmV-i0;BzjP)wbKI&I=fA3BPFC~_fMfY;ME1?Xea&tLl~N)#!}m068z zZ(!Ckm{b5Lqn*bLWf;TXV(uX&)WK`yc(^}BfT?9T9`t%%n%r>!jrccz=YvFVgPoy? z8T1qEYe91cKXWBrRy#%Jx#8C%Afi2ZZKNDaJ)rOn74Lg{#*{YSTI z_*`hEldD+oDomb44o5T{fL0du#Ek&yxVci^z!-=e&T_KY9~VQfZX%z+0XXG)Vt^$> z(2Ce9#Ih7|6=o$4;ttB(3`grvY3?RoN$-8{m>X%p6NDYb-j#Ey4Ux`*y81J7+4&R9 zi(3m#Op|aSE!H$IW9Kd_L=sWiV8u>o;d32}5Cc9OM0z>3oViC~o|yX z((W0t7-3ON-q_gKD1bQzoY3CW6RtP_xtm|Vy?c-tf% z(G_(7$c2UleWs(}O+NyOoD200I8laH=xdjPFKMU1XE%b0MKy+UhaCkxJ8V=K;Pe3T zniy+W?6!G;mH?0&Q$}oogi!GW1vKE1mhZ&7Hw$rLgfGHwMcj;N8fs||kW%Gq!EopS q-XLu0Nw~Tx#x@Dv?^qifUxGh*q%hJBOK`UU0000G+2|&+-E}o#@?ton5~$1vUq-EWl;PRCB0I*6L=A_o*S-;8A6Q8(-CK2uFAt!`0G> zZ&-*NyjKAV%nliBuH;jfsXlZDs0B<}rdGw0Cn!PfV8CU0$`QJQtXCzsk7Qt7Mrkn^ z0k$q%tR$wi8#WvEWapy669INQ4xaT!|Hr2$u>AVtVm zq&{*+SXh_|o>l|`*Y^-3 zi6h61BqELI?5SfI^98W$Km6Y0>OdejAn?XleX;VQ!#yBX37mU>Oph1rl*OjHdUm#0 z4>~M>`n|h(UC(aXELW)c%8*c?v=Jt_2_v(zdE!K(Lo6e}fG}VYjqfix9-B}r+OE4a+fQFcd@SYh z&x<6UVtp;wwY7WKl1bpm$RB{`v+*eOdSAVKo}dj0M!lK!*SLIYA6!M=!uqd;M<0mD zdHF|rX(W0>*u4=qK!1AHy{90@Co^Fe^(HwQ z+-qRaf)hC4<7@?nu^BOYt|u`4!W6i$gdouLi{%zsAS5wO;Wfn^f<~y-fn14n#v4Zg z<^j<}g*oFqx0-H@GTmOGD*_v*hp~6S77<-BWJFkQ`hOF-fEzobwJ+L$8oFS4C@Q)06xvxz|`;6D0mr$(TdBhp|Seo zT|iIpfS7DyVPRoD(4z<#t%zKdGv; zHrPnfh)ALg-Lb#Vmgmjh!lyCNu}i5oi~~X~6I}u^=4)61vc1aCXjh~|_E|>WV`7e6h}8R|2ykKzCT002ovPDHLk FV1gW2CzSvI literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-show_address/00003.png b/tests_zemu/snapshots/s-show_address/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..c29cd8c034d248e03301f5f5c02f1411f98a75cd GIT binary patch literal 571 zcmV-B0>u4^P)Q;NdY9_~NOH3Xj3qU=wkC{^<7zMpM)2y2ulL(SK17r)tc- zVOboq``oU$*CI7#(-yUO7~;M0Gl77?2%aKZ5$C&kDdcsZn+Pk&eUM7IY}%sj zOf$F#3OsSHh^quVj9>{erhdp5RcFSmuOPf{%Ic8$pB~6pd-^P8PJGyyo|C}(kd?p3 zyCmPsq*|-(kO9#RQSd_lZ|$OmZkO;cb-Iav-QJ=VjYoV$@I}$R-QsO*Y^;N$y~?yi zYW3G%K{Rtx^uCO&yDo`_DCw|fe;^?JROP53*RJbHrP?}-XvBJt3d%6(jCTk?lP?hq zNTiu}49Hnd-+p1I#Oyhb3sD50BG}zrI+;LFAOfI&3BnJlC|DLeaWq@0 zqz^jGg2rOCNk;1o5XHQpWg#Nl7lNnKc)284fpg+$1%6Qj(Yy! zQ31`l+3lZ+@7Ti(e7JVcTl$YO%U!SrF*?O=QFRB7!xWY<3#gub#X8G~2p;n*77-Lv zg!c_odPDRELN|M+!QdU$84=vKz#f9v;q}vKu?O&{5uQJfLiH>ol?cb`%#mwLp(+7P zT*O;3r^Z}_JZS3J<-U1Ja^m(3=CV-hQG`nzsuIA&wD?wfs3>Ccr+q=-^9?-F3_`8> zP=%9@p|$g+0d#I!ZKPG1n9D2!MtGdbkEcxp-7Q}fK5BU0v~6EEbqUTg=lmWZaBM-hPQ(2Jh@gk zH0n)~tv zV<%67CMf^(B?^1iiiJExMVz#J5=vQ*A9Z`>Dbb9p=&4|PVH-REuCuGF=xL&~0RW`u zA66pG2|6XpO__gQ`u$5mj7|O}oO zj{=Hub|xN8#y##Z2A#2cUg$sVDObUMh*1~2MAluHhb_FK!O_em*bR?q`YJY$S47H| z6=Z6HNA-=jMPUGXKcIPuS*N%0AHo(>%<3yDWy~EQ`JJ;$kw&pMnV#DNr^2*s8&OSl z8=#8H<5Tx{ItOot){a$P!vB>N5M=b(DEEAIfdtdvG6y{ZwN zb3z!!N5F(Klzp{37t&AN={(M;H{J9CDJmz6*R`@*o_KgTvt5y;FoNesg;a1nB3@! zmSqBvQ6rL!{X(@dga+70x@-#8^uYIU+3w=?fBEPXR9wDH`VDPwgVbmL#f9m7ag$CQ z^cA#YU`bgGfAp$g#LHPd$rPbGz|0YYHj*}zR5KOfJg-lMOP8P@(JHq=^@KZsyiprk z`4V*p)eQgufT3_NcJ6jczC-wOiA2&*QAbky4f5Y`{9^KfYs*RAdMMVu$68ithPznA z@>s3yv><6U**V0PRoalxepR0)b!VPzPOIp-18OP8X4&X%F_uXBDe4UTD9|SWE`yU) z&dL9nJ}HP~30LIgpd*rFD5q9?3{Cb^t0Uv8}9 N002ovPDHLkV1in*oT~r; literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-show_address/00007.png b/tests_zemu/snapshots/s-show_address/00007.png new file mode 100644 index 0000000000000000000000000000000000000000..006c26abaac6c76b2e871b194165596c0b416694 GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!2~2j9iA5fsRN!ajv*Cu-d?-N*JQxc`taO| z-}VOL1%{U&gdOO)vH0QtN3CIpH=On}yd9NxI9#@L#=GYozkR+NZ@s(h_s<*5szDx| zmTO$v85!1fT#rxFzI@yL`PcK^*L-*W3fIF8>~K zzhRaAS`H?-=QM1i=Tu6{1-oD!M<5AJP1 literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/s-show_address/00008.png b/tests_zemu/snapshots/s-show_address/00008.png new file mode 100644 index 0000000000000000000000000000000000000000..c931308fee83737bab4fc62f60f79cd2813201cd GIT binary patch literal 447 zcmV;w0YLtVP)OU2SeY;%C%^|Fqn zl>esG9SNEMcq>H}Z^%0?_U){Nd$FY;?<<8k9ZOTW9UqB{fQFRmjBw-Zl#&EhQ1jgL zC8M?o=f<`V=vFBpN|D_4Moye5h%RxKtx)j^{~FLd9o5HTW~$obR)&?Ha{`$9)LaFD z2CKG@6`DTvouQ@^{dWl!Ny&MD pfXR=cqCdzKb2GX77e!IT@&f&T&P>fQ_aFcO002ovPDHLkV1mIg%)I~r literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00000.png b/tests_zemu/snapshots/sp-mainmenu/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..f0fb480c374248263d9d42ea214b423363417ddc GIT binary patch literal 498 zcmVG;;#AI;=OzE@A+)4_nnlr*3{@Nc)*>*jTak_PnNx97o7e7r5EzM1{3p1?_3YrXeP zeQ!f8N=86Xe+(84f|3RV^@kr;P<<W5nxIEG3YC@D$tBuOw|Z@8!!L>;8T!WDEr3p_4D|~+~l-(UGyB`(j^PAK<>mT zFF<`O*Ou1-N7Zbuu(soU#ha_&-6DDS(=hnYY#wMWL%lRdB8HCJ?#5&$f4VSA{7=v_ zz#dBF9DOlsug&|Oa0AI1a6A6jF83fe%tZ~z`Qb{&&}q`;Y3QzkMk2bt*O(u|00000 zupF&D;TxBXRsJidScnz literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00001.png b/tests_zemu/snapshots/sp-mainmenu/00001.png new file mode 100644 index 0000000000000000000000000000000000000000..8472e5d9cda5ebb93b4d7b4e7b9096cc96903486 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_8Gba4!+nDh4L#dXaJJPnEG zite?1uUEO3Dt_9-IlJ^slJWodS%n-^xma{qfx2Lzg@f$rvmoH=d6AC$%=Q=jSJKIT-wu*=_G3~M%K@Xo_i(g zHdj=f=l)-H()mog&*u5_*A=XeElu2~Zj>*9r-ABd>TJY1jPr+jQbB^Au6{1-oD!M&#t)<-u_Rin1Vb&hon4Y08(IX4aSQr=c#^wADw_hbLpM zV@M9O{&Q`0H8a}^y1vu=S{|vpoz&QsGR@=loXMK|YInxqVbT`#d>_XzQQc43=QD3# zU*XoU_EUbo^??~%&;b$l_c<{*Zb3D<{2Ovhxr*EdWpjfC>aiEK6L4W0CF1e&mn+@= z$(oOC&GIajRqcu4m^Zb#o;I?%$t^~(+2#3TvE~FMIn=id#Ih5R;>^LC0_{QziggaJ z=G%t%Z5rp(XXl&rM+{q{q#~MJoBX0MzA68~4ichx>GNOX#N4>po(oy+EyAsUlV#37 zGFg-f*qMg=Gz%0Vt2)LZe6tWMKz=@y2IqlHI5)rc-!%aM00000000000Km8U0|OK) UYAcrAzW@LL07*qoM6N<$f&mK0rT_o{ literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00003.png b/tests_zemu/snapshots/sp-mainmenu/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..8472e5d9cda5ebb93b4d7b4e7b9096cc96903486 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_8Gba4!+nDh4L#dXaJJPnEG zite?1uUEO3Dt_9-IlJ^slJWodS%n-^xma{qfx2Lzg@f$rvmoH=d6AC$%=Q=jSJKIT-wu*=_G3~M%K@Xo_i(g zHdj=f=l)-H()mog&*u5_*A=XeElu2~Zj>*9r-ABd>TJY1jPr+jQbB^Au6{1-oD!ME&w1ii9>nqc68cHY+Z^6%GX&E%b#J~ONSRv^3V zqrktcY$qDKTB=(D81LLY^ULILrR+W9xAEIPvmaJ@yVaoblhF4=$?3Hb(TaYi5pjCA z)3ynx%8CA*njyO2tmoHG|4;M!clVLU!^UVBGpQf61>AzuCK&M_v=l_RZmiHcAz0m(A#w(AvQ|CvF zV?>Fy#gzMt-9GwRw5ZO$E6~Nt(S1gJ_wf>Go!dXKeOlG=THImI1>ucx6((&= qKLx#R2yY8-|Ftv#=2;;9A$;EMFGtEfrF9>JL_J;oT-G@yGywpkT$Pmo literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00005.png b/tests_zemu/snapshots/sp-mainmenu/00005.png new file mode 100644 index 0000000000000000000000000000000000000000..1adff7ee65dc3c9d4ac64ba994d3027ee4e4093a GIT binary patch literal 462 zcmV;<0WtoGP)741i%zr#oT)OL8ygftgGO2w+XH*8h9z1tU!Wm2MjV0000000000000Q_=NN0> zcl|b{xGoU8jl@#k-*V*sPA*)oIy`EU-vgmJ*9{Yjt86atzA9_~HWa%N-EI2{Jm!SS2((`(l_qsvz5LtF6dopvn`eNW!m9hZm<4WhG zyOQf+zlSaBvROTEhMKuO3mxx+;5WT-bR3Th;uoP_`isTSAkWmksN8hyXQ4A7DEP0{ z;9ksBQ-cM5^E!MNUyLGv|YxvF=G&%C=7`#PIN z#dF!S?!t##X7e9*n)RzS47k8GGr;$uuTGrrmVKxd{|6Kz9=WXzxbJsC0T#$Lj^0Uj zaoG}-U&u!ECC@9N6!TS0000000000003_7A9%`R0m8&e6aWAK07*qoM6N<$ Eg6!zkLjV8( literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00006.png b/tests_zemu/snapshots/sp-mainmenu/00006.png new file mode 100644 index 0000000000000000000000000000000000000000..86e715d1dd4f3d6bfdcc19ed0134f801855560f7 GIT binary patch literal 411 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|>x0ba4!+nDh2#EZ-po0k^=U zJDk7cg{nU`dZ_*J$;_R(aqsh!3P*qWE);VHYJ!9EDM4PEFLl3v+h*4o)pPNe$h4yH zhMJ$#jZWO0AiZ1H@Yp50SIc_RBEC+)%6?wsWlE^GK;tv1Ey`2oo!YqV&e?`VydKB) zW(!!_uSz}}ZnK{?;LH*;uDdoLyY8#5@Xg9EEBY%boBeO`>UoTdkA4Zd`gik*@QpRw zb8pIQE}3or%}V;C)lU7pw+&J*#V->!eW~xptFw_S@)pNTshCsVdzQ~Re$VmJ^iS{J zr+;Pt^!?A@+j4Rbr}*}5-f^RA#+{u!XO*MBuRQfF@HPJy+pudhx5FzYZ>8xEor>*#5HpDC-KP39Bd6Sx^0Y zJMVMtt?C^y{8JP2-)Y((sQ&X$@w0#kJe(K~wA!sauTUekqIV}q(9_k=Wt~$(696%n Bx}N|5 literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00007.png b/tests_zemu/snapshots/sp-mainmenu/00007.png new file mode 100644 index 0000000000000000000000000000000000000000..c770372642f8ce21b3e6ba77fbd43321e8c0b76c GIT binary patch literal 333 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|;yRx*ioLy2|aen&Di%VW^?(p#fYJvcZt4DV?yq_6aJGZ!O%@grSj9b>` zYaT23s+Vvx@cZ-ahlFM~+c__-Mgz&e8Yn1#J;+gvpyO;Cpm9$ zGFm7ztTEX9(<~%UUCWYDez(r`UyJ_B3)FKP=9heUW`Ftw%qO_Qwe;T>N`Y zuDy_d5%-I*69?oae<(Bm5pR6XcsJmjxy{qFpJp$z*mb#e>0!3c$($z_cP#q0J-4KK z<8GnJ1q{WdqNgm~<0fw7bzlYAnSPO3@ WOHH59+S9)yL1Lb+elF{r5}E+&*N8m; literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00008.png b/tests_zemu/snapshots/sp-mainmenu/00008.png new file mode 100644 index 0000000000000000000000000000000000000000..86e715d1dd4f3d6bfdcc19ed0134f801855560f7 GIT binary patch literal 411 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|>x0ba4!+nDh2#EZ-po0k^=U zJDk7cg{nU`dZ_*J$;_R(aqsh!3P*qWE);VHYJ!9EDM4PEFLl3v+h*4o)pPNe$h4yH zhMJ$#jZWO0AiZ1H@Yp50SIc_RBEC+)%6?wsWlE^GK;tv1Ey`2oo!YqV&e?`VydKB) zW(!!_uSz}}ZnK{?;LH*;uDdoLyY8#5@Xg9EEBY%boBeO`>UoTdkA4Zd`gik*@QpRw zb8pIQE}3or%}V;C)lU7pw+&J*#V->!eW~xptFw_S@)pNTshCsVdzQ~Re$VmJ^iS{J zr+;Pt^!?A@+j4Rbr}*}5-f^RA#+{u!XO*MBuRQfF@HPJy+pudhx5FzYZ>8xEor>*#5HpDC-KP39Bd6Sx^0Y zJMVMtt?C^y{8JP2-)Y((sQ&X$@w0#kJe(K~wA!sauTUekqIV}q(9_k=Wt~$(696%n Bx}N|5 literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00009.png b/tests_zemu/snapshots/sp-mainmenu/00009.png new file mode 100644 index 0000000000000000000000000000000000000000..1adff7ee65dc3c9d4ac64ba994d3027ee4e4093a GIT binary patch literal 462 zcmV;<0WtoGP)741i%zr#oT)OL8ygftgGO2w+XH*8h9z1tU!Wm2MjV0000000000000Q_=NN0> zcl|b{xGoU8jl@#k-*V*sPA*)oIy`EU-vgmJ*9{Yjt86atzA9_~HWa%N-EI2{Jm!SS2((`(l_qsvz5LtF6dopvn`eNW!m9hZm<4WhG zyOQf+zlSaBvROTEhMKuO3mxx+;5WT-bR3Th;uoP_`isTSAkWmksN8hyXQ4A7DEP0{ z;9ksBQ-cM5^E!MNUyLGv|YxvF=G&%C=7`#PIN z#dF!S?!t##X7e9*n)RzS47k8GGr;$uuTGrrmVKxd{|6Kz9=WXzxbJsC0T#$Lj^0Uj zaoG}-U&u!ECC@9N6!TS0000000000003_7A9%`R0m8&e6aWAK07*qoM6N<$ Eg6!zkLjV8( literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00010.png b/tests_zemu/snapshots/sp-mainmenu/00010.png new file mode 100644 index 0000000000000000000000000000000000000000..168c493b13e9dee894f5bd25b59f02474be27213 GIT binary patch literal 353 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|`hnba4!+nDh2#piq+mkL$%( zJ0`!W_q^FH&*S6EP`Kh5@4e5hA>E&w1ii9>nqc68cHY+Z^6%GX&E%b#J~ONSRv^3V zqrktcY$qDKTB=(D81LLY^ULILrR+W9xAEIPvmaJ@yVaoblhF4=$?3Hb(TaYi5pjCA z)3ynx%8CA*njyO2tmoHG|4;M!clVLU!^UVBGpQf61>AzuCK&M_v=l_RZmiHcAz0m(A#w(AvQ|CvF zV?>Fy#gzMt-9GwRw5ZO$E6~Nt(S1gJ_wf>Go!dXKeOlG=THImI1>ucx6((&= qKLx#R2yY8-|Ftv#=2;;9A$;EMFGtEfrF9>JL_J;oT-G@yGywpkT$Pmo literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-mainmenu/00011.png b/tests_zemu/snapshots/sp-mainmenu/00011.png new file mode 100644 index 0000000000000000000000000000000000000000..8472e5d9cda5ebb93b4d7b4e7b9096cc96903486 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_8Gba4!+nDh4L#dXaJJPnEG zite?1uUEO3Dt_9-IlJ^slJWodS%n-^xma{qfx2Lzg@f$rvmoH=d6AC$%=Q=jSJKIT-wu*=_G3~M%K@Xo_i(g zHdj=f=l)-H()mog&*u5_*A=XeElu2~Zj>*9r-ABd>TJY1jPr+jQbB^Au6{1-oD!MG;;#AI;=OzE@A+)4_nnlr*3{@Nc)*>*jTak_PnNx97o7e7r5EzM1{3p1?_3YrXeP zeQ!f8N=86Xe+(84f|3RV^@kr;P<<W5nxIEG3YC@D$tBuOw|Z@8!!L>;8T!WDEr3p_4D|~+~l-(UGyB`(j^PAK<>mT zFF<`O*Ou1-N7Zbuu(soU#ha_&-6DDS(=hnYY#wMWL%lRdB8HCJ?#5&$f4VSA{7=v_ zz#dBF9DOlsug&|Oa0AI1a6A6jF83fe%tZ~z`Qb{&&}q`;Y3QzkMk2bt*O(u|00000 zupF&D;TxBXRsJidScnz literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-show_address/00000.png b/tests_zemu/snapshots/sp-show_address/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..31051f85d8c3a47763e885993484817926c4e0eb GIT binary patch literal 473 zcmV;~0Ve*5P)SXt+lNOTg11Ia?v-l-_;$ssFYG`ZPE8> z&>}ekLiD#r(Lo@YfDrvHA0`-m6HUM*`f{_7WCCm)eYrs(nSe0i_~^@x0LfiYzx5Q2 zzKM!QCk`V*N7igAmnj)1R00s!DH`h7&AT~O$pdYySB9M{t=!rv6@UE#neb`tho z@2hVudB+=<@a6P$BVeLUM`3mfEsZ}I@7}wLpM}zmfKh0|gO``rTURliU;Hs)4M>-O zmy?6PVDuzl6!hf_OkXgUnS##7F6XNfeNWPDLqius<9rd`ZA`cb0{{R(o_|pnI4#RT z%)i((DJ}HHwfRY{|A*NgGbAcS_}KNuOCPvV_~v)@m%Z`J9G0sXxBJHVLG!y(6-2#U z2?(zJNR#}u4@xuzHKPd5r|IP)r3t*qUyW&cS33F+&$W-Jhrc0vFo~?U;E#<)(YR-HDcU0xU=E9uBapUl9~^o z?sksMfGFJh4jsPQHrvpr*B= zDY^or3HJ!lo9GK)xj%mt>z9nv*Obp^eE@cOB7_h^wAR`e{_LLL`;$InqgTRQT$ux3 zRI7D|&eT1(A;3D&X;tk>F5#`8JQ&zq!qkr`g|oi7diWay;~OSVMKJO1j`qfL+w7bJ zwXmuv@OQa1O_U9fZYA#c&&Kx?FoD^v4? z-!QP-Ru#DtY|DnOv_Gx*3o|ggTsoDE6C~p?beN2Icg068PzY368DJ6AicW#Os%RrE zbOPv;K^2-h8_z8wBJ5=K{>yN>TD|{bGzRZPtI#`1I$2|I6x+Q?Hk4PFrU{#OSu0;> z7E>|ynmwBk1?|Eo(Va|97o2iAXBTTA(^H@#;~o%BQW3MGT8?gfuSr9xaABO{Ju7n> z6d>VeC4m4IcHKzrV&~JZk$cz&>yqna4?6=~Y-AmZ6svaq;VH1iq)0)| zw!9*?=9832$&HOW{3}s5^v&ddlz;qicQ(AsftR^%Nnb17gol%2VG}OA6JT7zB|-=x zgb?CEo(;7pLXZVYk7z)~)yY3UwVHu)QYL+$I{Ug{8ZxmLUHX~oQ)xv;Y$^j$wjL#U z(gfN-HTpE<{BTauR>%J&gUBt6tOK8cKUkP= z_5K-9e*KxQq2&jrSkm2i80G5`J)2}~=l-XBEdxr9L^(%`{bAJhE8d`_(EIJs0MuOh z6GZO}%T3Pz9sp8mMdL{q> N002ovPDHLkV1f<7q&olr literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-show_address/00002.png b/tests_zemu/snapshots/sp-show_address/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..16f7ab8bb980e74a0b702515969f836859bc7f73 GIT binary patch literal 471 zcmV;|0Vw{7P)N|B{hEsy$6z zb{k}#NzMD2QE2@bGs-_o#xdudwC2_6GS7_1`3*18pgw>)5_8VIDEL!Uy-Uv9!^O{l zCZNr1Xr4_?Or0^S2D-Udru3>fsyAsanl%HO<~qmzxf1H&I>X&$J2W_A&FX;gks?QW z-8b)n@2u35ZQ0u^cnSKnyIu<30g{y(XwHy58$l79#W5 zDZ@6TY9{rnGg<|&kWI$yoyx<7bU;W;008(G{2A(0Zx@;rnP`9c4fCAdEq?{|B(C?v zxZODVZm!<#q;~0P=ta&0OLaHyEP4I3xK{_%hZb^=s;*Z?2`4IDZ)XSCPyV@D|A?wL zq?{1jIzUF>GZL@&gl8J9H03YJp}n*IR@xb`EHuOSp7Weiz9;OG<<;RWfd=8d$*g>~ zZjg!oGD@h4{$ljOPOL&Z4T4M`OhtGB000000000000000004k#{s4p0erc|sp`ZW& N002ovPDHLkV1jwD+e`oe literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-show_address/00003.png b/tests_zemu/snapshots/sp-show_address/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..668c4892347884369d4ebd68057069cc7c13f322 GIT binary patch literal 859 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 S83txjAYo5eKbLh*2~7a}F1z#q literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-show_address/00007.png b/tests_zemu/snapshots/sp-show_address/00007.png new file mode 100644 index 0000000000000000000000000000000000000000..3654452ae6cae3b35fb42ba9b4f7fbfce8d286da GIT binary patch literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|}ya^Qf!e({%Ucv>9?>nuUp;4 zJC}bkSta#c{dhvU#9C1BZAr2IYwj63{w0P*N00ay-i|(aTGk+Y(y{&xrz^Ua_g}An zXQf~6u=|{e$nqV!rc(OM1yObrrOy{^-!o;w{-?DJmo_IZNm;%#K6YVM95*8aLxNH6 z)9J@oA3wS((C*g6iFLbj-S++Ey|pykQq&iQ$m1Wg?6lpdEsWZ{`1FM=d-vNtUkaj@%V!HGo;N@Q?)52Nw)urpB_k9L5BJzf1=);T3K0RX=Jtc(Bv literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-show_address/00008.png b/tests_zemu/snapshots/sp-show_address/00008.png new file mode 100644 index 0000000000000000000000000000000000000000..1e4be69934787c368c2b5ed677cc7f5a2470be3b GIT binary patch literal 355 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|`hpba4!+nDh2#G+&c}K^PbX0`eJS}y70`%8T>n^%dV>bk>TM}C(cF@9*Jbj^5WTF;gKGIO)y zjlJ*9*PE9W@jdm~5r5wEu#TD8y6+Pnh8_O<-8#^Bx$bnOy3REWn^jXeAFev)TXe-F zTky7j?62N_lL@yzwx7O!=&4O1W7^|)7fcr%%i5*3D%0_G;tl)cpW3Io3*7!cdXW1o sV&B=e%knGs=l(tai5cc!1_ryAe9w9}zGZ%X&;TUp>FVdQ&MBb@086r+N&o-= literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/sp-show_address/00009.png b/tests_zemu/snapshots/sp-show_address/00009.png new file mode 100644 index 0000000000000000000000000000000000000000..f0fb480c374248263d9d42ea214b423363417ddc GIT binary patch literal 498 zcmVG;;#AI;=OzE@A+)4_nnlr*3{@Nc)*>*jTak_PnNx97o7e7r5EzM1{3p1?_3YrXeP zeQ!f8N=86Xe+(84f|3RV^@kr;P<<W5nxIEG3YC@D$tBuOw|Z@8!!L>;8T!WDEr3p_4D|~+~l-(UGyB`(j^PAK<>mT zFF<`O*Ou1-N7Zbuu(soU#ha_&-6DDS(=hnYY#wMWL%lRdB8HCJ?#5&$f4VSA{7=v_ zz#dBF9DOlsug&|Oa0AI1a6A6jF83fe%tZ~z`Qb{&&}q`;Y3QzkMk2bt*O(u|00000 zupF&D;TxBXRsJidScnz literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/st-mainmenu/00000.png b/tests_zemu/snapshots/st-mainmenu/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..0da315715a5fe1647209c943dd1a29f9bca30229 GIT binary patch literal 10737 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 Ru5wans_ES?y$gBy-vHiEE$ILN literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/st-mainmenu/00001.png b/tests_zemu/snapshots/st-mainmenu/00001.png new file mode 100644 index 0000000000000000000000000000000000000000..77a5ea96f093067957fbacd819198820494ef12c GIT binary patch literal 13423 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 V4FY`${38*_*vR~H@kQ99{{hnayGZ~5 literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/st-mainmenu/00002.png b/tests_zemu/snapshots/st-mainmenu/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..f644c83fb7ae36c389c91712dc9a138cf621d9e0 GIT binary patch literal 6472 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 literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/st-mainmenu/00003.png b/tests_zemu/snapshots/st-mainmenu/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..7360580209282eabe73b825a33eab2a8ebdd3bee GIT binary patch literal 6287 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 literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/st-mainmenu/00004.png b/tests_zemu/snapshots/st-mainmenu/00004.png new file mode 100644 index 0000000000000000000000000000000000000000..f644c83fb7ae36c389c91712dc9a138cf621d9e0 GIT binary patch literal 6472 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 literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/st-mainmenu/00005.png b/tests_zemu/snapshots/st-mainmenu/00005.png new file mode 100644 index 0000000000000000000000000000000000000000..0da315715a5fe1647209c943dd1a29f9bca30229 GIT binary patch literal 10737 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 Ru5wans_ES?y$gBy-vHiEE$ILN literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00000.png b/tests_zemu/snapshots/x-mainmenu/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..f0fb480c374248263d9d42ea214b423363417ddc GIT binary patch literal 498 zcmVG;;#AI;=OzE@A+)4_nnlr*3{@Nc)*>*jTak_PnNx97o7e7r5EzM1{3p1?_3YrXeP zeQ!f8N=86Xe+(84f|3RV^@kr;P<<W5nxIEG3YC@D$tBuOw|Z@8!!L>;8T!WDEr3p_4D|~+~l-(UGyB`(j^PAK<>mT zFF<`O*Ou1-N7Zbuu(soU#ha_&-6DDS(=hnYY#wMWL%lRdB8HCJ?#5&$f4VSA{7=v_ zz#dBF9DOlsug&|Oa0AI1a6A6jF83fe%tZ~z`Qb{&&}q`;Y3QzkMk2bt*O(u|00000 zupF&D;TxBXRsJidScnz literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00001.png b/tests_zemu/snapshots/x-mainmenu/00001.png new file mode 100644 index 0000000000000000000000000000000000000000..8472e5d9cda5ebb93b4d7b4e7b9096cc96903486 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_8Gba4!+nDh4L#dXaJJPnEG zite?1uUEO3Dt_9-IlJ^slJWodS%n-^xma{qfx2Lzg@f$rvmoH=d6AC$%=Q=jSJKIT-wu*=_G3~M%K@Xo_i(g zHdj=f=l)-H()mog&*u5_*A=XeElu2~Zj>*9r-ABd>TJY1jPr+jQbB^Au6{1-oD!M&#t)<-u_Rin1Vb&hon4Y08(IX4aSQr=c#^wADw_hbLpM zV@M9O{&Q`0H8a}^y1vu=S{|vpoz&QsGR@=loXMK|YInxqVbT`#d>_XzQQc43=QD3# zU*XoU_EUbo^??~%&;b$l_c<{*Zb3D<{2Ovhxr*EdWpjfC>aiEK6L4W0CF1e&mn+@= z$(oOC&GIajRqcu4m^Zb#o;I?%$t^~(+2#3TvE~FMIn=id#Ih5R;>^LC0_{QziggaJ z=G%t%Z5rp(XXl&rM+{q{q#~MJoBX0MzA68~4ichx>GNOX#N4>po(oy+EyAsUlV#37 zGFg-f*qMg=Gz%0Vt2)LZe6tWMKz=@y2IqlHI5)rc-!%aM00000000000Km8U0|OK) UYAcrAzW@LL07*qoM6N<$f&mK0rT_o{ literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00003.png b/tests_zemu/snapshots/x-mainmenu/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..8472e5d9cda5ebb93b4d7b4e7b9096cc96903486 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_8Gba4!+nDh4L#dXaJJPnEG zite?1uUEO3Dt_9-IlJ^slJWodS%n-^xma{qfx2Lzg@f$rvmoH=d6AC$%=Q=jSJKIT-wu*=_G3~M%K@Xo_i(g zHdj=f=l)-H()mog&*u5_*A=XeElu2~Zj>*9r-ABd>TJY1jPr+jQbB^Au6{1-oD!ME&w1ii9>nqc68cHY+Z^6%GX&E%b#J~ONSRv^3V zqrktcY$qDKTB=(D81LLY^ULILrR+W9xAEIPvmaJ@yVaoblhF4=$?3Hb(TaYi5pjCA z)3ynx%8CA*njyO2tmoHG|4;M!clVLU!^UVBGpQf61>AzuCK&M_v=l_RZmiHcAz0m(A#w(AvQ|CvF zV?>Fy#gzMt-9GwRw5ZO$E6~Nt(S1gJ_wf>Go!dXKeOlG=THImI1>ucx6((&= qKLx#R2yY8-|Ftv#=2;;9A$;EMFGtEfrF9>JL_J;oT-G@yGywpkT$Pmo literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00005.png b/tests_zemu/snapshots/x-mainmenu/00005.png new file mode 100644 index 0000000000000000000000000000000000000000..1adff7ee65dc3c9d4ac64ba994d3027ee4e4093a GIT binary patch literal 462 zcmV;<0WtoGP)741i%zr#oT)OL8ygftgGO2w+XH*8h9z1tU!Wm2MjV0000000000000Q_=NN0> zcl|b{xGoU8jl@#k-*V*sPA*)oIy`EU-vgmJ*9{Yjt86atzA9_~HWa%N-EI2{Jm!SS2((`(l_qsvz5LtF6dopvn`eNW!m9hZm<4WhG zyOQf+zlSaBvROTEhMKuO3mxx+;5WT-bR3Th;uoP_`isTSAkWmksN8hyXQ4A7DEP0{ z;9ksBQ-cM5^E!MNUyLGv|YxvF=G&%C=7`#PIN z#dF!S?!t##X7e9*n)RzS47k8GGr;$uuTGrrmVKxd{|6Kz9=WXzxbJsC0T#$Lj^0Uj zaoG}-U&u!ECC@9N6!TS0000000000003_7A9%`R0m8&e6aWAK07*qoM6N<$ Eg6!zkLjV8( literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00006.png b/tests_zemu/snapshots/x-mainmenu/00006.png new file mode 100644 index 0000000000000000000000000000000000000000..86e715d1dd4f3d6bfdcc19ed0134f801855560f7 GIT binary patch literal 411 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|>x0ba4!+nDh2#EZ-po0k^=U zJDk7cg{nU`dZ_*J$;_R(aqsh!3P*qWE);VHYJ!9EDM4PEFLl3v+h*4o)pPNe$h4yH zhMJ$#jZWO0AiZ1H@Yp50SIc_RBEC+)%6?wsWlE^GK;tv1Ey`2oo!YqV&e?`VydKB) zW(!!_uSz}}ZnK{?;LH*;uDdoLyY8#5@Xg9EEBY%boBeO`>UoTdkA4Zd`gik*@QpRw zb8pIQE}3or%}V;C)lU7pw+&J*#V->!eW~xptFw_S@)pNTshCsVdzQ~Re$VmJ^iS{J zr+;Pt^!?A@+j4Rbr}*}5-f^RA#+{u!XO*MBuRQfF@HPJy+pudhx5FzYZ>8xEor>*#5HpDC-KP39Bd6Sx^0Y zJMVMtt?C^y{8JP2-)Y((sQ&X$@w0#kJe(K~wA!sauTUekqIV}q(9_k=Wt~$(696%n Bx}N|5 literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00007.png b/tests_zemu/snapshots/x-mainmenu/00007.png new file mode 100644 index 0000000000000000000000000000000000000000..c770372642f8ce21b3e6ba77fbd43321e8c0b76c GIT binary patch literal 333 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|;yRx*ioLy2|aen&Di%VW^?(p#fYJvcZt4DV?yq_6aJGZ!O%@grSj9b>` zYaT23s+Vvx@cZ-ahlFM~+c__-Mgz&e8Yn1#J;+gvpyO;Cpm9$ zGFm7ztTEX9(<~%UUCWYDez(r`UyJ_B3)FKP=9heUW`Ftw%qO_Qwe;T>N`Y zuDy_d5%-I*69?oae<(Bm5pR6XcsJmjxy{qFpJp$z*mb#e>0!3c$($z_cP#q0J-4KK z<8GnJ1q{WdqNgm~<0fw7bzlYAnSPO3@ WOHH59+S9)yL1Lb+elF{r5}E+&*N8m; literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00008.png b/tests_zemu/snapshots/x-mainmenu/00008.png new file mode 100644 index 0000000000000000000000000000000000000000..86e715d1dd4f3d6bfdcc19ed0134f801855560f7 GIT binary patch literal 411 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|>x0ba4!+nDh2#EZ-po0k^=U zJDk7cg{nU`dZ_*J$;_R(aqsh!3P*qWE);VHYJ!9EDM4PEFLl3v+h*4o)pPNe$h4yH zhMJ$#jZWO0AiZ1H@Yp50SIc_RBEC+)%6?wsWlE^GK;tv1Ey`2oo!YqV&e?`VydKB) zW(!!_uSz}}ZnK{?;LH*;uDdoLyY8#5@Xg9EEBY%boBeO`>UoTdkA4Zd`gik*@QpRw zb8pIQE}3or%}V;C)lU7pw+&J*#V->!eW~xptFw_S@)pNTshCsVdzQ~Re$VmJ^iS{J zr+;Pt^!?A@+j4Rbr}*}5-f^RA#+{u!XO*MBuRQfF@HPJy+pudhx5FzYZ>8xEor>*#5HpDC-KP39Bd6Sx^0Y zJMVMtt?C^y{8JP2-)Y((sQ&X$@w0#kJe(K~wA!sauTUekqIV}q(9_k=Wt~$(696%n Bx}N|5 literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00009.png b/tests_zemu/snapshots/x-mainmenu/00009.png new file mode 100644 index 0000000000000000000000000000000000000000..1adff7ee65dc3c9d4ac64ba994d3027ee4e4093a GIT binary patch literal 462 zcmV;<0WtoGP)741i%zr#oT)OL8ygftgGO2w+XH*8h9z1tU!Wm2MjV0000000000000Q_=NN0> zcl|b{xGoU8jl@#k-*V*sPA*)oIy`EU-vgmJ*9{Yjt86atzA9_~HWa%N-EI2{Jm!SS2((`(l_qsvz5LtF6dopvn`eNW!m9hZm<4WhG zyOQf+zlSaBvROTEhMKuO3mxx+;5WT-bR3Th;uoP_`isTSAkWmksN8hyXQ4A7DEP0{ z;9ksBQ-cM5^E!MNUyLGv|YxvF=G&%C=7`#PIN z#dF!S?!t##X7e9*n)RzS47k8GGr;$uuTGrrmVKxd{|6Kz9=WXzxbJsC0T#$Lj^0Uj zaoG}-U&u!ECC@9N6!TS0000000000003_7A9%`R0m8&e6aWAK07*qoM6N<$ Eg6!zkLjV8( literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00010.png b/tests_zemu/snapshots/x-mainmenu/00010.png new file mode 100644 index 0000000000000000000000000000000000000000..168c493b13e9dee894f5bd25b59f02474be27213 GIT binary patch literal 353 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|`hnba4!+nDh2#piq+mkL$%( zJ0`!W_q^FH&*S6EP`Kh5@4e5hA>E&w1ii9>nqc68cHY+Z^6%GX&E%b#J~ONSRv^3V zqrktcY$qDKTB=(D81LLY^ULILrR+W9xAEIPvmaJ@yVaoblhF4=$?3Hb(TaYi5pjCA z)3ynx%8CA*njyO2tmoHG|4;M!clVLU!^UVBGpQf61>AzuCK&M_v=l_RZmiHcAz0m(A#w(AvQ|CvF zV?>Fy#gzMt-9GwRw5ZO$E6~Nt(S1gJ_wf>Go!dXKeOlG=THImI1>ucx6((&= qKLx#R2yY8-|Ftv#=2;;9A$;EMFGtEfrF9>JL_J;oT-G@yGywpkT$Pmo literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-mainmenu/00011.png b/tests_zemu/snapshots/x-mainmenu/00011.png new file mode 100644 index 0000000000000000000000000000000000000000..8472e5d9cda5ebb93b4d7b4e7b9096cc96903486 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|_8Gba4!+nDh4L#dXaJJPnEG zite?1uUEO3Dt_9-IlJ^slJWodS%n-^xma{qfx2Lzg@f$rvmoH=d6AC$%=Q=jSJKIT-wu*=_G3~M%K@Xo_i(g zHdj=f=l)-H()mog&*u5_*A=XeElu2~Zj>*9r-ABd>TJY1jPr+jQbB^Au6{1-oD!MG;;#AI;=OzE@A+)4_nnlr*3{@Nc)*>*jTak_PnNx97o7e7r5EzM1{3p1?_3YrXeP zeQ!f8N=86Xe+(84f|3RV^@kr;P<<W5nxIEG3YC@D$tBuOw|Z@8!!L>;8T!WDEr3p_4D|~+~l-(UGyB`(j^PAK<>mT zFF<`O*Ou1-N7Zbuu(soU#ha_&-6DDS(=hnYY#wMWL%lRdB8HCJ?#5&$f4VSA{7=v_ zz#dBF9DOlsug&|Oa0AI1a6A6jF83fe%tZ~z`Qb{&&}q`;Y3QzkMk2bt*O(u|00000 zupF&D;TxBXRsJidScnz literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-show_address/00000.png b/tests_zemu/snapshots/x-show_address/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..31051f85d8c3a47763e885993484817926c4e0eb GIT binary patch literal 473 zcmV;~0Ve*5P)SXt+lNOTg11Ia?v-l-_;$ssFYG`ZPE8> z&>}ekLiD#r(Lo@YfDrvHA0`-m6HUM*`f{_7WCCm)eYrs(nSe0i_~^@x0LfiYzx5Q2 zzKM!QCk`V*N7igAmnj)1R00s!DH`h7&AT~O$pdYySB9M{t=!rv6@UE#neb`tho z@2hVudB+=<@a6P$BVeLUM`3mfEsZ}I@7}wLpM}zmfKh0|gO``rTURliU;Hs)4M>-O zmy?6PVDuzl6!hf_OkXgUnS##7F6XNfeNWPDLqius<9rd`ZA`cb0{{R(o_|pnI4#RT z%)i((DJ}HHwfRY{|A*NgGbAcS_}KNuOCPvV_~v)@m%Z`J9G0sXxBJHVLG!y(6-2#U z2?(zJNR#}u4@xuzHKPd5r|IP)r3t*qUyW&cS33F+&$W-Jhrc0vFo~?U;E#<)(YR-HDcU0xU=E9uBapUl9~^o z?sksMfGFJh4jsPQHrvpr*B= zDY^or3HJ!lo9GK)xj%mt>z9nv*Obp^eE@cOB7_h^wAR`e{_LLL`;$InqgTRQT$ux3 zRI7D|&eT1(A;3D&X;tk>F5#`8JQ&zq!qkr`g|oi7diWay;~OSVMKJO1j`qfL+w7bJ zwXmuv@OQa1O_U9fZYA#c&&Kx?FoD^v4? z-!QP-Ru#DtY|DnOv_Gx*3o|ggTsoDE6C~p?beN2Icg068PzY368DJ6AicW#Os%RrE zbOPv;K^2-h8_z8wBJ5=K{>yN>TD|{bGzRZPtI#`1I$2|I6x+Q?Hk4PFrU{#OSu0;> z7E>|ynmwBk1?|Eo(Va|97o2iAXBTTA(^H@#;~o%BQW3MGT8?gfuSr9xaABO{Ju7n> z6d>VeC4m4IcHKzrV&~JZk$cz&>yqna4?6=~Y-AmZ6svaq;VH1iq)0)| zw!9*?=9832$&HOW{3}s5^v&ddlz;qicQ(AsftR^%Nnb17gol%2VG}OA6JT7zB|-=x zgb?CEo(;7pLXZVYk7z)~)yY3UwVHu)QYL+$I{Ug{8ZxmLUHX~oQ)xv;Y$^j$wjL#U z(gfN-HTpE<{BTauR>%J&gUBt6tOK8cKUkP= z_5K-9e*KxQq2&jrSkm2i80G5`J)2}~=l-XBEdxr9L^(%`{bAJhE8d`_(EIJs0MuOh z6GZO}%T3Pz9sp8mMdL{q> N002ovPDHLkV1f<7q&olr literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-show_address/00002.png b/tests_zemu/snapshots/x-show_address/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..16f7ab8bb980e74a0b702515969f836859bc7f73 GIT binary patch literal 471 zcmV;|0Vw{7P)N|B{hEsy$6z zb{k}#NzMD2QE2@bGs-_o#xdudwC2_6GS7_1`3*18pgw>)5_8VIDEL!Uy-Uv9!^O{l zCZNr1Xr4_?Or0^S2D-Udru3>fsyAsanl%HO<~qmzxf1H&I>X&$J2W_A&FX;gks?QW z-8b)n@2u35ZQ0u^cnSKnyIu<30g{y(XwHy58$l79#W5 zDZ@6TY9{rnGg<|&kWI$yoyx<7bU;W;008(G{2A(0Zx@;rnP`9c4fCAdEq?{|B(C?v zxZODVZm!<#q;~0P=ta&0OLaHyEP4I3xK{_%hZb^=s;*Z?2`4IDZ)XSCPyV@D|A?wL zq?{1jIzUF>GZL@&gl8J9H03YJp}n*IR@xb`EHuOSp7Weiz9;OG<<;RWfd=8d$*g>~ zZjg!oGD@h4{$ljOPOL&Z4T4M`OhtGB000000000000000004k#{s4p0erc|sp`ZW& N002ovPDHLkV1jwD+e`oe literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-show_address/00003.png b/tests_zemu/snapshots/x-show_address/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..668c4892347884369d4ebd68057069cc7c13f322 GIT binary patch literal 859 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 S83txjAYo5eKbLh*2~7a}F1z#q literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-show_address/00007.png b/tests_zemu/snapshots/x-show_address/00007.png new file mode 100644 index 0000000000000000000000000000000000000000..3654452ae6cae3b35fb42ba9b4f7fbfce8d286da GIT binary patch literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|}ya^Qf!e({%Ucv>9?>nuUp;4 zJC}bkSta#c{dhvU#9C1BZAr2IYwj63{w0P*N00ay-i|(aTGk+Y(y{&xrz^Ua_g}An zXQf~6u=|{e$nqV!rc(OM1yObrrOy{^-!o;w{-?DJmo_IZNm;%#K6YVM95*8aLxNH6 z)9J@oA3wS((C*g6iFLbj-S++Ey|pykQq&iQ$m1Wg?6lpdEsWZ{`1FM=d-vNtUkaj@%V!HGo;N@Q?)52Nw)urpB_k9L5BJzf1=);T3K0RX=Jtc(Bv literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-show_address/00008.png b/tests_zemu/snapshots/x-show_address/00008.png new file mode 100644 index 0000000000000000000000000000000000000000..1e4be69934787c368c2b5ed677cc7f5a2470be3b GIT binary patch literal 355 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h6`U|`hpba4!+nDh2#G+&c}K^PbX0`eJS}y70`%8T>n^%dV>bk>TM}C(cF@9*Jbj^5WTF;gKGIO)y zjlJ*9*PE9W@jdm~5r5wEu#TD8y6+Pnh8_O<-8#^Bx$bnOy3REWn^jXeAFev)TXe-F zTky7j?62N_lL@yzwx7O!=&4O1W7^|)7fcr%%i5*3D%0_G;tl)cpW3Io3*7!cdXW1o sV&B=e%knGs=l(tai5cc!1_ryAe9w9}zGZ%X&;TUp>FVdQ&MBb@086r+N&o-= literal 0 HcmV?d00001 diff --git a/tests_zemu/snapshots/x-show_address/00009.png b/tests_zemu/snapshots/x-show_address/00009.png new file mode 100644 index 0000000000000000000000000000000000000000..f0fb480c374248263d9d42ea214b423363417ddc GIT binary patch literal 498 zcmVG;;#AI;=OzE@A+)4_nnlr*3{@Nc)*>*jTak_PnNx97o7e7r5EzM1{3p1?_3YrXeP zeQ!f8N=86Xe+(84f|3RV^@kr;P<<W5nxIEG3YC@D$tBuOw|Z@8!!L>;8T!WDEr3p_4D|~+~l-(UGyB`(j^PAK<>mT zFF<`O*Ou1-N7Zbuu(soU#ha_&-6DDS(=hnYY#wMWL%lRdB8HCJ?#5&$f4VSA{7=v_ zz#dBF9DOlsug&|Oa0AI1a6A6jF83fe%tZ~z`Qb{&&}q`;Y3QzkMk2bt*O(u|00000 zupF&D;TxBXRsJidScnz literal 0 HcmV?d00001 diff --git a/tests_zemu/tests/common.ts b/tests_zemu/tests/common.ts new file mode 100644 index 0000000..9947827 --- /dev/null +++ b/tests_zemu/tests/common.ts @@ -0,0 +1,27 @@ +import { IDeviceModel, DEFAULT_START_OPTIONS } from '@zondax/zemu' + +import { resolve } from 'path' + +export const APP_SEED = 'equip will roof matter pink blind book anxiety banner elbow sun young' + +const APP_PATH_S = resolve('../app/output/app_s.elf') +const APP_PATH_X = resolve('../app/output/app_x.elf') +const APP_PATH_SP = resolve('../app/output/app_s2.elf') +const APP_PATH_ST = resolve('../app/output/app_stax.elf') + +export const models: IDeviceModel[] = [ + { name: 'nanos', prefix: 'S', path: APP_PATH_S }, + { name: 'nanox', prefix: 'X', path: APP_PATH_X }, + { name: 'nanosp', prefix: 'SP', path: APP_PATH_SP }, +// { name: 'stax', prefix: 'ST', path: APP_PATH_ST }, +] + +export const defaultOptions = { + ...DEFAULT_START_OPTIONS, + logging: true, + custom: `-s "${APP_SEED}"`, + X11: false, +} + +export const txBlobExample = + 'de0011a46170616198c4fa9fdd8fe9420e5d401d02050d78f5d13ea9f209d0a101e382f70d13d4dced8f5425a96ace5c8858b8654b2a3904cf175b1f9cee6069d765f1fe42fa23151dd9baa382f709a1681267b8ee9f47f7964c0e7e9ab3cf426964433ec29a17150ac82bce2b5044512412d0522586f2d52338584173242117110426cfbf22fb416a298bcdbfc02018dc8689d773f564516bd40eec2831147cc56729d92b743cfa3b585b1bc7c9383735cf5c6459efce18bc773a5014d1f7000011e671f05ad740cc4349e365f700fb73f06dc4da14c46bbb47e1b920d9b2b5c7506bc2e3af13a561f065d54c3ca6333078e803f12b12d6b1e6276b76e8ce5c95a8930607c4fa1a79a6c371f3bdb6ecaef12c98118b51e64cc6190a362a521c4feff89f3ac9b6ea2804e52ee1a09601a3dd4d6da599052238ef69b14da54fac103081ac627d0c9cd6196103e2798427f999c6737ef0e770f8096a97100ed05b5127d1bc686c1d2f634fc87f569792de426e496e6f2d36e586961c1babeca1f9122fdea0be1ac6f3bd89eae5ced97066983c30a41bd946265083aef7cab65394586a0226e9fb9f9caf65ef7e9ac69a711b483e21b60f501aea434af068985547a8065eb1a8db313e83e5551e2535be7cc6f05b48920b2cb2a2d84be39f8cfb3f20ea66adda16bd4396f7703827ad77edab3f1b3953f12e193367e994f9943972afc4fa69f8e9851b4e31fcd4c7980552a273ad0a982685b50e9b2e2481131a4901b5d9ecc8595c6f3d29a6f4f7a9afe5e3466e7edd1bd811050744cf5ea26f36d5e2d82ec4e86a98104da56fd1a20e41e75694f2329d2dc9910cb48dc1927d43926c424c3f012634c7f778fee148372025dccf07af54e01d308a63f5cd0b99eea963a914e1f366bba3fb026e43e6244fa934ce990bb3ba6d8d408e017eb2a7aba1bbb191c5868e044ce2628ec70ee5506efe1f3ae7f326db07438b3ec99b9c64a960078c03f88c77b4ab6a9826c8f7399fdfd80008c9da7622886805bc1b0b9fef2b3a9fddd94b8f7852bc24d2f8d081003a5e7bd54b40df125cf3d639c4fa6bf2401f9bbf4550cf056c47d1b5b18fd11e2f1b6a910650a7bc161b38bf8c18a97de0909ec1d42154f07530fa15f658484e1403f4bbbb836e106837b56977f43ce72e1cf6c8222c5726dc9ab61261c59c0308d0caec0e79f629fea21c4d5e37657d350f2bed1320f4df74f90078c6800779700a1b6f851bd6a4662ee1a2f1965491615cdd484827cfe830fe4e7c482f956431f8cef99614cc367165ddc3e75877ecbca5223c73c8e7a9aadc7a007c7f40a69ae9f97469f08bf2cd1d62f1aea8833b5e063ce743c6a7670e6ad82c802563d217efc9e461b4b503fec0c8c28ab5ee4ab55bcf5789cff1aabed964b7fbe642ad0a4584d31e66d1c9c4fa4badbbfe681e82d4fcc34edcfb176fc16221a9271086bc5933e43ea47be65d75aa3e225b1b223a8376dd7a7751cc0c825b032da8d202f251f19b58a7313ea10bd791d59b942937bfd24d4d7f781cce8c58ba9350fa3adefca37b1fb070c9c4fa6eb7f1cc6ed99a7a98d8d8f00279ca68a1885393131be65d330ca93dd76f99297c48a2ed5c853aefe61a0758aa359d61b9e1aefb1106303a05fc4ba843662be86d97f70b01241dd0693d5f01aa0f2938da6b2c47b0d96042a21470b03fee201a97f4b2070743250c3a640e2647c36920c40c63348c037acdd4d9f6e4b86a7d5cf87f1b1d8ccbf6f7cedcf9cfe9c2105ca300d0e074cf5c5b9c0bc4fae111faa1ce7b4cbd47f528fb5327c3badec3db5698695af695d7c9db8210f924c1220801769ad2b65ea72bd518b46f351a1804b3e93496d1e9834e3546abe76493481992e24fc4573f457e4aea0084cbea1eb91caee15da54452b41858da726e9ac10b4232f107908c9f2b936865a19377890aaad5b2f158d88cc53df26bdf4d51b3a0b1b94d4441de1a0c0a8c517de54538c647290dbb4db054699989f206dbd1299a63b2d5672059cada5134e0cd0478e17989c5ea55f1b298984ba5728f7c79bc84e79083f83853a9f09bbe0560da7e6cfe3c7657578d1994b824eb7c25b4c0803f9768040aa522d4e9fba8531800498bd12785f7d9d28753c4fa388cdf4dee8b9874da3c94bf4cedd8b8b5bd9a5ce3eb407224fc3b19e422c9455e090b6052cedc4d1107d8613578a775b058b91af5b0450836e0d768fad6fdbfcb9686ec326719d86d8ffdd5c91ceb6fe05e0e2fc84f477a43435b2112807b6858a590cc6bbfc22630dee70732bc44b1278ec540a8828575f4d1c1ce5f67d1a3c56adec705b7079441f8263b6a0b6e4cf88196e56ec1ec3e21162d4bc1d5d73552eb5870172932e3ef4899e8ded2f2ee203466afa87a48dbad2c7f90a8924ced012ace8b03fbee69dcc01c61691261da545973c2c41e1f4af7cd2c97c3068f6bd32bb1efcded16cae7c0698269efda034aaae5651eeb3fc58280c4fa9c97c887a9136dbcfb6b496bbe84b32448d8e3e4be62f3db55b702b8e950b351dcd9593297aa1e890e82511caa3aec28c9da8204aca8cb1fbd5389a9de3f653bacf053d8b875ca08080fe7ed5dac2dfc77745416e5a30a51535d473939bd167cc4c687047c44f9fa6bcd6f978b7005a135c6b0b8c0416d9e17ea3ba8a5089089c39151b6a27e1e1fa07fe3c8daabec26c865767882e0e6ef7201f3b4865514843000b6ad84817f2c08916bba9ba5f4195a2c9c6e3b0b80426620f8cd206932a89e6e8e82d4fbb77b9a2c584e02252619d7478768ab43390251a4d7577063516d18fc62c299fe62b0bfc8cd79a133f2a7976132e1ac1d8fcf16f0a46170616e04a461706170c480c0721b691b335da47a695a7246492eb3fb88c3b3463024439091287154a3c409d4f0389bb7d9a9ab49237b671cfe5b293141039e91555f76bab6cd5adedc5489c207e20070c47eb6d7cc12330c4fb4f048f28fe24f1ab69e7a58e29b51753b33146e8c32a3bdc716e5956572281d63c27d7e7b59e1fd5d42feedb568ada19c5ea46170617392cd1b70cd1d4ea46170617494c42033627e03aaa4c34b2e3ae7aa2d049a776afdf3d8beebde452f27e60837febc32c420eb3b7a3800eae990c379c60c3ab0f571225954f7be5190435810332d695ac82ac42044d211e4acc09eb27d59773c62e5e9e8a8fbc76d460bd2c542ea618eb63f03dcc420931468e76ebfb4b466ab82edfb5b0c395f05f7d70c0ea970c8d96b01c43d2841a46170666192cd0137cd079ea46170677382a36e6273ce17c2f571a36e7569ce35326631a461706c7382a36e6273ce039892fca36e7569ce42f762faa461707375c4201be56dfbb007190ed78a890b9a613c0e8b6656bee4e874f53f6d6dae9e54df14a3666565cd03e8a26676ce000dc8cda367656eac746573746e65742d76312e30a26768c4204863b518a4b3c84ec810f22d4f1081cb0f71f059a7ac20dec62f7f70e5093a22a26c76ce0017c94ca46e6f7465c504007475727069732065676573746173207072657469756d2061656e65616e207068617265747261206d61676e6120616320706c61636572617420766573746962756c756d206c6563747573206d617572697320756c7472696365732065726f7320696e2063757273757320747572706973206d617373612074696e636964756e7420647569207574206f726e617265206c65637475732073697420616d65742065737420706c61636572617420696e2065676573746173206572617420696d706572646965742073656420657569736d6f64206e69736920706f727461206c6f72656d206d6f6c6c697320616c697175616d20757420706f72747469746f72206c656f2061206469616d20736f6c6c696369747564696e2074656d706f72206964206575206e69736c206e756e63206d6920697073756d20666175636962757320766974616520616c6971756574206e656320756c6c616d636f727065722073697420616d6574207269737573206e756c6c616d20656765742066656c69732065676574206e756e63206c6f626f72746973206d617474697320616c697175616d20666175636962757320707572757320696e206d617373612074656d706f72206e65632066657567696174206e69736c207072657469756d2066757363652069642076656c697420757420746f72746f72207072657469756d20766976657272612073757370656e646973736520706f74656e7469206e756c6c616d20616320746f72746f72207669746165207075727573206661756369627573206f726e6172652073757370656e646973736520736564206e697369206c616375732073656420746f72746f72207669746165207075727573206661756369627573206f726e6172652073757370656e646973736520736564206e697369206c616375732073656420746f72746f72207669746165207075727573206661756369627573206f726e6172652073757370656e646973736520736564206e697369206c616375732073656420746f72746f72207669746165207075727573206661756369627573206f726e6172652073757370656e646973736520736564206e697369206c616375732073656420746f72746f72207669746165207075727573206661756369627573206f726e6172652073757370656e646973736520736564206e697369206c616375732073656420746f72746f72207669746165207075727573206661756369627573206f726e6172652073757370656e646973736520736564206e697369206c616375732073656420746f72746f72207669746165207075727573206661756369627573206f726e6172652073757370656e646973736520736564206e697369206c616375732073656420746f72746f72207669746165207075727573a3736e64c420626def77f13c1e0ec9ec64d9bc10a4f3111b4986a01dd15e4e11a36af98b3d2da474797065a46170706c' diff --git a/tests_zemu/tests/pullImageKillOld.ts b/tests_zemu/tests/pullImageKillOld.ts new file mode 100644 index 0000000..ddd30b7 --- /dev/null +++ b/tests_zemu/tests/pullImageKillOld.ts @@ -0,0 +1,4 @@ +import Zemu from '@zondax/zemu' + +Zemu.checkAndPullImage() +Zemu.stopAllEmuContainers() diff --git a/tests_zemu/tests/standard.test.ts b/tests_zemu/tests/standard.test.ts new file mode 100644 index 0000000..9f00aa2 --- /dev/null +++ b/tests_zemu/tests/standard.test.ts @@ -0,0 +1,201 @@ +/** ****************************************************************************** + * (c) 2018 - 2023 Zondax AG + * + * 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. + ******************************************************************************* */ + +import Zemu, { ButtonKind, zondaxMainmenuNavigation } from '@zondax/zemu' +import { defaultOptions, models, txBlobExample } from './common' +import IronfishApp from '@zondax/ledger-ironfish' + +jest.setTimeout(60000) + +const PATH = "m/44'/133'/0'/0/0" + +const expectedPublicAddress = "b3ad098e86bc31de35ec5a77cce6aed08d5336bf273abef5e7eb420278a0c19c" +const expectedIVK = "043e34aa9a6323b82a899d984081ce53e3bb47b2ffa18a0dcfa6910a6d278c73" +const expectedOVK = "316c96f058f7e188acc90d90d1d765bd9b9ce9e5fa3655c74e8450df0191ee21" + +describe('Standard', function () { + test.concurrent.each(models)('can start and stop container', async function (m) { + const sim = new Zemu(m.path) + try { + await sim.start({ ...defaultOptions, model: m.name }) + } finally { + await sim.close() + } + }) + + test.concurrent.each(models)('main menu', async function (m) { + const sim = new Zemu(m.path) + try { + await sim.start({ ...defaultOptions, model: m.name }) + const nav = zondaxMainmenuNavigation(m.name, [1, 0, 0, 4, -5]) + await sim.navigateAndCompareSnapshots('.', `${m.prefix.toLowerCase()}-mainmenu`, nav.schedule) + } finally { + await sim.close() + } + }) + + test.concurrent.each(models)('get app version', async function (m) { + const sim = new Zemu(m.path) + try { + await sim.start({ ...defaultOptions, model: m.name }) + const app = new IronfishApp(sim.getTransport()) + const resp = await app.getVersion() + + console.log(resp) + + expect(resp.returnCode).toEqual(0x9000) + expect(resp.errorMessage).toEqual('No errors') + expect(resp).toHaveProperty('testMode') + expect(resp).toHaveProperty('major') + expect(resp).toHaveProperty('minor') + expect(resp).toHaveProperty('patch') + } finally { + await sim.close() + } + }) + + test.concurrent.each(models)('get address', async function (m) { + const sim = new Zemu(m.path) + try { + await sim.start({ ...defaultOptions, model: m.name }) + const app = new IronfishApp(sim.getTransport()) + + const resp = await app.getAddressAndPubKey(PATH) + console.log(resp) + + expect(resp.returnCode).toEqual(0x9000) + expect(resp.errorMessage).toEqual('No errors') + + expect(resp.publicAddress?.toString('hex')).toEqual(expectedPublicAddress) + expect(resp.ivk?.toString('hex')).toEqual(expectedIVK) + expect(resp.ovk?.toString('hex')).toEqual(expectedOVK) + } finally { + await sim.close() + } + }) + + test.concurrent.each(models)('show address', async function (m) { + const sim = new Zemu(m.path) + try { + await sim.start({...defaultOptions, model: m.name, + approveKeyword: m.name === 'stax' ? 'QR' : '', + approveAction: ButtonKind.ApproveTapButton,}) + const app = new IronfishApp(sim.getTransport()) + + const respRequest = app.showAddressAndPubKey(PATH) + // Wait until we are not in the main menu + await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot()) + await sim.compareSnapshotsAndApprove('.', `${m.prefix.toLowerCase()}-show_address`) + + const resp = await respRequest + console.log(resp) + + expect(resp.returnCode).toEqual(0x9000) + expect(resp.errorMessage).toEqual('No errors') + + } finally { + await sim.close() + } + }) + + // test.concurrent.each(models)('show address - reject', async function (m) { + // const sim = new Zemu(m.path) + // try { + // await sim.start({...defaultOptions, model: m.name, + // rejectKeyword: m.name === 'stax' ? 'QR' : ''}) + // const app = new TemplateApp(sim.getTransport()) + + // const respRequest = app.getAddressAndPubKey(accountId, true) + // // Wait until we are not in the main menu + // await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot()) + + // await sim.compareSnapshotsAndReject('.', `${m.prefix.toLowerCase()}-show_address_reject`, 'REJECT') + + // const resp = await respRequest + // console.log(resp) + + // expect(resp.return_code).toEqual(0x6986) + // expect(resp.error_message).toEqual('Transaction rejected') + // } finally { + // await sim.close() + // } + // }) + + // #{TODO} --> Add Zemu tests for different transactions. Include expert mode if needed + // test.concurrent.each(models)('sign tx0 normal', async function (m) { + // const sim = new Zemu(m.path) + // try { + // await sim.start({ ...defaultOptions, model: m.name }) + // const app = new TemplateApp(sim.getTransport()) + + // const txBlob = Buffer.from(txBlobExample) + // const responseAddr = await app.getAddressAndPubKey(accountId) + // const pubKey = responseAddr.publicKey + + // // do not wait here.. we need to navigate + // const signatureRequest = app.sign(accountId, txBlob) + + // // Wait until we are not in the main menu + // await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot()) + // await sim.compareSnapshotsAndApprove('.', `${m.prefix.toLowerCase()}-sign_asset_freeze`,50000) + + // const signatureResponse = await signatureRequest + // console.log(signatureResponse) + + // expect(signatureResponse.return_code).toEqual(0x9000) + // expect(signatureResponse.error_message).toEqual('No errors') + + // // Now verify the signature + // const prehash = Buffer.concat([Buffer.from('TX'), txBlob]); + // const valid = ed25519.verify(signatureResponse.signature, prehash, pubKey) + // expect(valid).toEqual(true) + // } finally { + // await sim.close() + // } + // }) + + // test.concurrent.each(models)('sign tx1 normal', async function (m) { + // const sim = new Zemu(m.path) + // try { + // await sim.start({ ...defaultOptions, model: m.name }) + // const app = new TemplateApp(sim.getTransport()) + + // const txBlob = Buffer.from(txBlobExample) + // const responseAddr = await app.getAddressAndPubKey(accountId) + // const pubKey = responseAddr.publicKey + + // // do not wait here.. we need to navigate + // const signatureRequest = app.sign(accountId, txBlob) + + // // Wait until we are not in the main menu + // await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot()) + // await sim.compareSnapshotsAndApprove('.', `${m.prefix.toLowerCase()}-sign_asset_freeze`,50000) + + // const signatureResponse = await signatureRequest + // console.log(signatureResponse) + + // expect(signatureResponse.return_code).toEqual(0x9000) + // expect(signatureResponse.error_message).toEqual('No errors') + + // // Now verify the signature + // const prehash = Buffer.concat([Buffer.from('TX'), txBlob]); + // const valid = ed25519.verify(signatureResponse.signature, prehash, pubKey) + // expect(valid).toEqual(true) + // } finally { + // await sim.close() + // } + // }) +}) diff --git a/tests_zemu/tsconfig.json b/tests_zemu/tsconfig.json new file mode 100644 index 0000000..3435f95 --- /dev/null +++ b/tests_zemu/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "./dist" + }, + "exclude": [ + "node_modules", + "./dist/**" + ] +}