From f26e61793dc36446cc067e6bab3b40d40cb5e291 Mon Sep 17 00:00:00 2001 From: Aido Date: Sat, 4 Nov 2023 22:25:01 +0000 Subject: [PATCH] Added unit tests for shamir functions --- .github/workflows/ci-workflow.yml | 35 ++++++- CHANGELOG.md | 4 +- README.md | 9 +- TODO.md | 9 +- tests/unit/.gitignore | 22 ++++ tests/unit/CMakeLists.txt | 69 +++++++++++++ tests/unit/Makefile | 41 ++++++++ tests/unit/README.md | 38 +++++++ tests/unit/lib/bolos_target.h | 0 tests/unit/lib/random.c | 10 ++ tests/unit/tests/shamir.c | 162 ++++++++++++++++++++++++++++++ 11 files changed, 387 insertions(+), 12 deletions(-) create mode 100644 tests/unit/.gitignore create mode 100644 tests/unit/CMakeLists.txt create mode 100644 tests/unit/Makefile create mode 100644 tests/unit/README.md create mode 100644 tests/unit/lib/bolos_target.h create mode 100644 tests/unit/lib/random.c create mode 100644 tests/unit/tests/shamir.c diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 5e272088..c5c6785d 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -21,7 +21,7 @@ jobs: upload_app_binaries_artifact: compiled_app_binaries run_for_devices: '["nanos", "nanox", "nanosp", "stax"]' - ledger_app_test: + ledger_app_test_function: name: Run ragger tests using the reusable workflow needs: ledger_app_build uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1 @@ -29,3 +29,36 @@ jobs: download_app_binaries_artifact: compiled_app_binaries test_dir: tests/functional run_for_devices: '["nanos"]' + + ledger_app_test_unit: + name: Unit tests + needs: ledger_app_build + runs-on: ubuntu-latest + + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + + steps: + - name: Clone + uses: actions/checkout@v2 + - name: Build unit tests + run: | + cd tests/unit/ + make + - name: Generate code coverage + run: | + cd tests/unit/ + make coverage + - uses: actions/upload-artifact@v2 + with: + name: code-coverage + path: tests/unit/coverage + - name: Upload to codecov.io + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./tests/unit/coverage.info + flags: unittests + name: codecov-app-seed-tool + fail_ci_if_error: true + verbose: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 8af036a3..32857020 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Change log -## [1.5.1] - 2023-11-02 +## [1.5.1] - 2023-11-04 ### Added -- +- Added unit tests for shamir ### Changed - Reduce size of Nano binaries slightly by removing duplicate flows diff --git a/README.md b/README.md index a8688f72..d11bab48 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,18 @@ # Seed Tool: A Ledger application that provides some useful seed management utilities -[![Build app-seed-tool](https://github.com/aido/app-seed-tool/actions/workflows/ci-workflow.yml/badge.svg)](https://github.com/aido/app-seed-tool/actions/workflows/ci-workflow.yml) -[![CodeQL](https://github.com/aido/app-seed-tool/actions/workflows/codeql-workflow.yml/badge.svg)](https://github.com/aido/app-seed-tool/actions/workflows/codeql-workflow.yml) -[![Code style check](https://github.com/aido/app-seed-tool/actions/workflows/lint-workflow.yml/badge.svg)](https://github.com/aido/app-seed-tool/actions/workflows/lint-workflow.yml) [![License](https://img.shields.io/github/license/aido/app-seed-tool)](https://github.com/aido/app-seed-tool/blob/develop/LICENSE) - [![Release](https://img.shields.io/github/release/aido/app-seed-tool)](https://github.com/aido/app-seed-tool/releases) ![nanos](https://img.shields.io/badge/nanos-working-green) ![nanox](https://img.shields.io/badge/nanox-working-green]) ![nanosp](https://img.shields.io/badge/nanosp-working-green) ![stax](https://img.shields.io/badge/stax-in_progress-orange) +[![Build app-seed-tool](https://github.com/aido/app-seed-tool/actions/workflows/ci-workflow.yml/badge.svg)](https://github.com/aido/app-seed-tool/actions/workflows/ci-workflow.yml) +[![CodeQL](https://github.com/aido/app-seed-tool/actions/workflows/codeql-workflow.yml/badge.svg)](https://github.com/aido/app-seed-tool/actions/workflows/codeql-workflow.yml) +[![Code style check](https://github.com/aido/app-seed-tool/actions/workflows/lint-workflow.yml/badge.svg)](https://github.com/aido/app-seed-tool/actions/workflows/lint-workflow.yml) +[![codecov](https://codecov.io/gh/aido/app-seed-tool/branch/develop/graph/badge.svg?token=uCkGEbhGl3)](https://codecov.io/gh/aido/app-seed-tool/tree/develop) + Use the utilities provided by this Ledger application to check a backed up seed or generate [Shamir's Secret Sharing (SSS)](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing) for a seed. ## Check BIP39 diff --git a/TODO.md b/TODO.md index 3c377dcc..4b40459b 100755 --- a/TODO.md +++ b/TODO.md @@ -2,23 +2,22 @@ ### Todo -- [ ] Add unit tests - [ ] Update automated tests to test on nanox and nanosp -- [ ] Add code coverage to GitHub actions - [ ] Save memory by setting the SSKR word buffer (G_bolos_ux_context.sskr_words_buffer) to a sensible size. Maybe just store SSKR Bytewords as shorter two letter minimal Bytewords rather than a 4 letter Byteword plus spaace for each share. Convert minimal ByteWords back to four letter Bytewords just prior to display. ### In Progress - [ ] Add Ledger Stax to list of devices app works on - [x] Add SSKR Generate option to Stax - - [ ] Write BIP39 to SSKR functionality - [ ] Add SSKR Check option to Stax - [ ] Write SSKR to BIP39 functionality - - [ ] Test with 29-word SSKR shares - - [ ] Test with 46-word SSKR shares + - [ ] Functional Test with 29-word SSKR shares + - [ ] Functional Test with 46-word SSKR shares ### Done ✓ +- [x] Add unit tests +- [x] Add code coverage to GitHub actions - [x] Add option to generate BIP39 mnemonics from SSKR shares even if shares do not validate against seed on device - A user may have lost or damaged original device and now needs to generate the recovery phrase from another secure device - [x] Fix warnings about deprecated functions during build diff --git a/tests/unit/.gitignore b/tests/unit/.gitignore new file mode 100644 index 00000000..2cbee4ed --- /dev/null +++ b/tests/unit/.gitignore @@ -0,0 +1,22 @@ +# Editor Files and Folders + +.idea/ +.vscode/ +.DS_Store +*~ +\#*# + +# Build Files and Binaries + +*.log +*.o +*.so +*.dll +*.dylib +cmake-build-*/ +*build/ +build/ + +# Coverage file +coverage.info +coverage \ No newline at end of file diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 00000000..9bf5b0b8 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.10) + +if(${CMAKE_VERSION} VERSION_LESS 3.10) + cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) +endif() + +# project information +project(unit_tests + VERSION 0.1 + DESCRIPTION "Unit tests for app-seed-tool Ledger Application" + LANGUAGES C) + + +# guard against bad build-type strings +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") +endif() + +include(CTest) +ENABLE_TESTING() + +# specify C standard +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED True) +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -pedantic -g -O0 --coverage") + +set(GCC_COVERAGE_LINK_FLAGS "--coverage -lgcov") +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") + +# guard against in-source builds +if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) + message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt. ") +endif() + +# Fetch cmocka +find_package(cmocka QUIET) +include(FetchContent) +FetchContent_Declare( + cmocka + GIT_REPOSITORY https://git.cryptomilk.org/projects/cmocka.git + GIT_TAG cmocka-1.1.5 + GIT_SHALLOW 1 +) +set(WITH_STATIC_LIB ON CACHE BOOL "CMocka: Build with a static library" FORCE) +set(WITH_CMOCKERY_SUPPORT OFF CACHE BOOL "CMocka: Install a cmockery header" FORCE) +set(WITH_EXAMPLES OFF CACHE BOOL "CMocka: Build examples" FORCE) +set(UNIT_TESTING OFF CACHE BOOL "CMocka: Build with unit testing" FORCE) +set(PICKY_DEVELOPER OFF CACHE BOOL "CMocka: Build with picky developer flags" FORCE) +FetchContent_MakeAvailable(cmocka) + +add_compile_definitions(TEST DEBUG=0 SKIP_FOR_CMOCKA) +add_compile_definitions(HAVE_HASH HAVE_HMAC HAVE_SHA256) + +include_directories(./lib ../../src/bc-sskr/bc-shamir $ENV{LEDGER_SECURE_SDK}/include $ENV{LEDGER_SECURE_SDK}/lib_cxng/include $ENV{LEDGER_SECURE_SDK}/lib_cxng/src) + +# add src +set(LIB_SOURCES ./lib/random.c $ENV{LEDGER_SECURE_SDK}/lib_cxng/src/cx_ram.c $ENV{LEDGER_SECURE_SDK}/lib_cxng/src/cx_hash.c $ENV{LEDGER_SECURE_SDK}/lib_cxng/src/cx_sha256.c $ENV{LEDGER_SECURE_SDK}/lib_cxng/src/cx_hmac.c $ENV{LEDGER_SECURE_SDK}/lib_cxng/src/cx_utils.c) +add_library(unittest SHARED ${LIB_SOURCES} ) + +# add cmocka tests +set(LIB_SOURCES ../../src/bc-sskr/bc-shamir/shamir.c ../../src/bc-sskr/bc-shamir/interpolate.c ../../src/bc-sskr/bc-shamir/hazmat.c) +add_library(shamir SHARED ${LIB_SOURCES} ) +add_executable(test_shamir tests/shamir.c) +target_link_libraries(test_shamir PUBLIC cmocka gcov shamir unittest) + +foreach(target test_shamir) + add_test(NAME ${target} COMMAND ${target}) +endforeach() diff --git a/tests/unit/Makefile b/tests/unit/Makefile new file mode 100644 index 00000000..940b913b --- /dev/null +++ b/tests/unit/Makefile @@ -0,0 +1,41 @@ +MAKEFLAGS += --no-print-directory + +RM ?= rm -f +ECHO = `which echo` + +ifneq (,$(findstring xterm,${TERM})) +GREEN := \e[0;32m +RED := \e[0;31m +CYAN := \e[0;36m +RESET := \e[0m +else +GREEN := "" +RED := "" +RESET := "" +endif + +BUILD_DIRECTORY = $(realpath build/) + +DIRECTORY_BUILD = build + +all: + @cmake -B ${DIRECTORY_BUILD} -H. + @make -C ${DIRECTORY_BUILD} + @CTEST_OUTPUT_ON_FAILURE=1 make -C ${DIRECTORY_BUILD} test + +coverage: all + @lcov --directory . -b "${BUILD_DIRECTORY}" --capture --initial -o coverage.base + @lcov --rc lcov_branch_coverage=1 --directory . -b "${BUILD_DIRECTORY}" --capture -o coverage.capture + @lcov --directory . -b "${BUILD_DIRECTORY}" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info + @lcov --directory . -b "${BUILD_DIRECTORY}" --remove coverage.info '*/build/_deps/cmocka-src/src/*' '*/unit/lib/*' '*/unit/tests/*' '${LEDGER_SECURE_SDK}/lib_cxng/src/*' -o coverage.info + @$(ECHO) -e "${GREEN}[ OK ]${RESET} Generated 'coverage.info'." + @genhtml coverage.info -o coverage + @if [ -f coverage.base ]; then $(ECHO) -e "${RED}[ RM ]${RESET}" coverage.base && $(RM) -r coverage.base ; fi; + @if [ -f coverage.capture ]; then $(ECHO) -e "${RED}[ RM ]${RESET}" coverage.capture && $(RM) -r coverage.capture ; fi; + +clean: + @if [ -d ${DIRECTORY_BUILD} ]; then $(ECHO) -e "${RED}[ RM ]${RESET}" ${DIRECTORY_BUILD} && $(RM) -r ${DIRECTORY_BUILD} ; fi; + @if [ -d coverage ]; then $(ECHO) -e "${RED}[ RM ]${RESET}" coverage && $(RM) -r coverage ; fi; + @if [ -f coverage.info ]; then $(ECHO) -e "${RED}[ RM ]${RESET}" coverage.info && $(RM) -r coverage.info ; fi; + +.PHONY: all coverage clean \ No newline at end of file diff --git a/tests/unit/README.md b/tests/unit/README.md new file mode 100644 index 00000000..68a98f28 --- /dev/null +++ b/tests/unit/README.md @@ -0,0 +1,38 @@ +# Unit tests + +It is important to unit test your functions. +This also allows you to document how your functions work. +We use the library [**cmocka**](https://cmocka.org/#features) + +## Requirement + +- [CMake >= 3.10](https://cmake.org/download/) +- [lcov >= 1.14](http://ltp.sourceforge.net/coverage/lcov.php) + +Don't worry, you don't necessarily need to install the `cmocka library` because the **cmakelist automatically fetches** the library + +## Add new test + +Create new file into `tests` folder and follow [this initiation](https://cmocka.org/talks/cmocka_unit_testing_and_mocking.pdf) + +Now go to the `CMakeLists.txt` file and add your test with the specific file you want to test. + +## Usage + +### Build + +The `default rules` of makefile will compile the tests and run them. + +```sh +make +``` + +The `coverage rule` will launch the default rules and generate the coverage and you will be **automatically redirected** to the generated .html +```sh +make coverage +``` + +The `clean rule` will delete the folders and files generated +```sh +make clean +``` \ No newline at end of file diff --git a/tests/unit/lib/bolos_target.h b/tests/unit/lib/bolos_target.h new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/lib/random.c b/tests/unit/lib/random.c new file mode 100644 index 00000000..9ca7ef11 --- /dev/null +++ b/tests/unit/lib/random.c @@ -0,0 +1,10 @@ +#include +#include + +unsigned char *fake_rng(uint8_t *buffer, size_t len) +{ + for (size_t i = 0; i < len; i++) { + buffer[i] = i % 256; + } + return buffer; +} diff --git a/tests/unit/tests/shamir.c b/tests/unit/tests/shamir.c new file mode 100644 index 00000000..998bb1bf --- /dev/null +++ b/tests/unit/tests/shamir.c @@ -0,0 +1,162 @@ +/* +* The test seeds and shares used for these tests have been taken from: +* https://github.com/BlockchainCommons/crypto-commons/blob/master/Docs/sskr-test-vector.md#256-bit-seed +* +* The seed is generated from the following BIP39 words: +* toe priority custom gauge jacket theme arrest bargain +* gloom wide ill fit eagle prepare capable fish limb +* cigar reform other priority speak rough imitate +* +* The set of 2-of-3 shares as SSKR ByteWords are: +* tuna acid epic hard data love wolf able acid able +* duty surf belt task judo legs ruby cost belt pose +* ruby logo iron vows luck bald user lazy tuna belt +* guru buzz limp exam obey kept task cash saga pool +* love brag roof owls news junk +* +* tuna acid epic hard data love wolf able acid acid +* barn peck luau keys each duty waxy quad open bias +* what cusp zaps math kick dark join nail legs oboe +* also twin yank road very blue gray saga oboe city +* gear beta quad draw knob main +* +* tuna acid epic hard data love wolf able acid also +* fund able city road whiz zone claw high frog work +* deli slot gush cats kiwi gyro numb puma join fund +* when math inch even curl rich vows oval also unit +* brew door atom love gyro figs +*/ + +#include +#include +#include +#include + +#include "shamir.h" +#include "interpolate.h" + +unsigned char *fake_rng(uint8_t *buffer, size_t len); + +const uint8_t seed[] = {0xE3, 0x95, 0x5C, 0xDA, 0x30, 0x47, 0x71, 0xC0, + 0x03, 0x18, 0x95, 0x63, 0x7F, 0x55, 0xC3, 0xAB, + 0xE4, 0x51, 0x53, 0xC8, 0x7A, 0xBD, 0x81, 0xC5, + 0x1E, 0xD1, 0x4E, 0x8A, 0xAF, 0xA1, 0xAF, 0x13}; + + +static void test_shamir_recover(void **state) { + + const uint8_t share_1[] = {0x30, 0xCC, 0x0D, 0xCF, 0x70, 0x83, 0xBD, 0x1F, + 0x0D, 0xAF, 0xBD, 0x88, 0x69, 0xE8, 0x8C, 0x0B, + 0xDF, 0x81, 0xD9, 0x0D, 0x53, 0x15, 0x85, 0x37, + 0xA1, 0x77, 0xCF, 0x17, 0xC2, 0xAE, 0x8A, 0x12}; + + const uint8_t share_2[] = {0x0C, 0xAA, 0x8B, 0x78, 0x31, 0x30, 0xEE, 0xB3, + 0xA5, 0x0F, 0xF0, 0x22, 0xFA, 0x90, 0x79, 0x24, + 0x6D, 0x99, 0x83, 0xA2, 0x02, 0xDA, 0xF5, 0xBA, + 0xE1, 0x10, 0x51, 0xC2, 0xA2, 0x1A, 0x4B, 0x0E}; + + const uint8_t share_3[] = {0x48, 0x00, 0x1A, 0xBA, 0xF2, 0xFE, 0x1B, 0x5C, + 0x46, 0xF4, 0x27, 0xC7, 0x54, 0x18, 0x7D, 0x55, + 0xA0, 0xB1, 0x6D, 0x48, 0xF1, 0x90, 0x65, 0x36, + 0x21, 0xB9, 0xE8, 0xA6, 0x02, 0xDD, 0x13, 0x2A}; + + uint8_t member_indexs[] = {0x00, 0x01}; + uint8_t threshold = sizeof(member_indexs); + uint8_t share_length = sizeof(seed); + uint8_t secret[share_length]; + + const uint8_t *shares[] = {share_1, share_2}; + + int recovery = shamir_recover_secret(threshold, + member_indexs, + shares, + share_length, + secret); + + assert_int_equal(recovery, share_length); + assert_memory_equal(secret, seed, share_length); + + shares[0] = share_3; + member_indexs[0] = 0x02; + + recovery = shamir_recover_secret(threshold, + member_indexs, + shares, + share_length, + secret); + + assert_int_equal(recovery, share_length); + assert_memory_equal(secret, seed, share_length); + + shares[1] = share_1; + member_indexs[1] = 0x00; + + recovery = shamir_recover_secret(threshold, + member_indexs, + shares, + share_length, + secret); + + assert_int_equal(recovery, share_length); + assert_memory_equal(secret, seed, share_length); + + shares[0] = share_1; + member_indexs[0] = 0x01; + + recovery = shamir_recover_secret(threshold, + member_indexs, + shares, + share_length, + secret); + + assert_int_equal(recovery, SHAMIR_ERROR_CHECKSUM_FAILURE); + assert_memory_not_equal(secret, seed, share_length); +} + +static void test_shamir_split(void **state) { + const uint8_t shares[] = {0xFF, 0x0F, 0xBB, 0x2A, 0x8B, 0xCC, 0x6F, 0x00, + 0xC8, 0xE6, 0x83, 0xDF, 0xB0, 0xEB, 0x5F, 0x1C, + 0x55, 0xEF, 0x12, 0xD9, 0x4B, 0x62, 0x97, 0x42, + 0x42, 0xDA, 0x21, 0x11, 0xBA, 0xC6, 0x5F, 0xAA, + 0xB4, 0x8E, 0x81, 0x9F, 0xBB, 0x8A, 0x1C, 0xC3, + 0xCF, 0xFB, 0x10, 0xBB, 0xC7, 0xB7, 0x96, 0xBC, + 0xBD, 0xB3, 0x4F, 0x1E, 0x21, 0xCE, 0x04, 0x94, + 0x48, 0x1E, 0x79, 0x8C, 0x0D, 0x7E, 0xEA, 0xA2, + 0x69, 0x16, 0xCF, 0x5B, 0xEB, 0x40, 0x89, 0x9D, + 0xC6, 0xDC, 0xBE, 0x17, 0x5E, 0x53, 0xD6, 0x47, + 0x9E, 0x57, 0xA8, 0x4C, 0x9F, 0x21, 0xAA, 0xF5, + 0x56, 0x49, 0x91, 0x30, 0xCF, 0xAD, 0x2E, 0xBA}; + + uint8_t threshold = 2; + const uint8_t share_count = 3; + const uint8_t seed_length = sizeof(seed); + uint8_t result[seed_length * share_count]; + + int32_t ret_val = shamir_split_secret(threshold, + share_count, + seed, + seed_length, + result, + fake_rng); + + assert_int_equal(ret_val, share_count); + assert_memory_equal(result, shares, sizeof(result)); + + threshold = 4; + ret_val = shamir_split_secret(threshold, + share_count, + seed, + seed_length, + result, + fake_rng); + + assert_int_equal(ret_val, SHAMIR_ERROR_INVALID_THRESHOLD); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_shamir_recover), + cmocka_unit_test(test_shamir_split) + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}