diff --git a/unit-tests/CMakeLists.txt b/unit-tests/CMakeLists.txt new file mode 100644 index 0000000..7e64b05 --- /dev/null +++ b/unit-tests/CMakeLists.txt @@ -0,0 +1,62 @@ +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 Ledger OpenPGP application" + LANGUAGES C) + + +# guard against bad build-type strings +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") +endif() + +# 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() + +include(CTest) + +# specify C standard +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED True) + +add_compile_options(-Wall -Wextra -g -pedantic --coverage) +# Flag depending on the Build Type +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0") +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}-O3") + +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}") + +set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../src") +set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src") +set(MOCK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/mocks") + +add_compile_definitions(TEST) + +include_directories( + ${MOCK_DIR} + ${APP_DIR} +) +# include_directories($ENV{BOLOS_SDK}/lib_standard_app) + +add_executable(test_io + ${SRC_DIR}/test_io.c + ${MOCK_DIR}/mocks.c + ${APP_DIR}/gpg_io.c + ${APP_DIR}/gpg_vars.c +) + +target_link_libraries(test_io PUBLIC + cmocka + gcov) + +add_test(test_io test_io) diff --git a/unit-tests/README.md b/unit-tests/README.md new file mode 100644 index 0000000..133fad0 --- /dev/null +++ b/unit-tests/README.md @@ -0,0 +1,48 @@ +# Unit tests + +## Prerequisite + +Be sure to have installed: + +- CMake >= 3.10 +- CMocka >= 1.1.5 + +and for code coverage generation: + +- lcov >= 1.14 + +## Overview + +In `unit-tests` folder, compile with: + +```shell +cmake -Bbuild -H. && make -C build +``` + +and run tests with: + +```shell +CTEST_OUTPUT_ON_FAILURE=1 make -C build test +``` + +To get more verbose output, use: + +```shell +CTEST_OUTPUT_ON_FAILURE=1 make -C build test ARGS="-V" +``` + +Or also directly with: + +```shell +CTEST_OUTPUT_ON_FAILURE=1 build/test_io +``` + +## Generate code coverage + +Just execute in `unit-tests` folder: + +```shell +./gen_coverage.sh +``` + +it will output `coverage.total` and `coverage/` folder with HTML details (in `coverage/index.html`). diff --git a/unit-tests/gen_coverage.sh b/unit-tests/gen_coverage.sh new file mode 100755 index 0000000..8048270 --- /dev/null +++ b/unit-tests/gen_coverage.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -x +set -e + +BUILD_DIRECTORY=$(realpath build/) + +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 '*/unit-tests/*' -o coverage.info && +echo "Generated 'coverage.info'." && +genhtml coverage.info -o coverage + +rm -f coverage.base coverage.capture diff --git a/unit-tests/mocks/bolos_target.h b/unit-tests/mocks/bolos_target.h new file mode 100644 index 0000000..e69de29 diff --git a/unit-tests/mocks/cx.h b/unit-tests/mocks/cx.h new file mode 100644 index 0000000..b475a99 --- /dev/null +++ b/unit-tests/mocks/cx.h @@ -0,0 +1,30 @@ + + +#define cx_rsa_public_key_t char +#define cx_rsa_1024_public_key_t char +#define cx_rsa_2048_public_key_t char +#define cx_rsa_3072_public_key_t char +#define cx_rsa_4096_public_key_t char + +#define cx_rsa_private_key_t char +#define cx_rsa_1024_private_key_t char +#define cx_rsa_2048_private_key_t char +#define cx_rsa_3072_private_key_t char +#define cx_rsa_4096_private_key_t char + +#define cx_ecfp_public_key_t char +#define cx_ecfp_256_public_key_t char +#define cx_ecfp_384_public_key_t char +#define cx_ecfp_512_public_key_t char +#define cx_ecfp_640_public_key_t char + +#define cx_ecfp_private_key_t char +#define cx_ecfp_256_private_key_t char +#define cx_ecfp_384_private_key_t char +#define cx_ecfp_512_private_key_t char +#define cx_ecfp_640_private_key_t char + +#define cx_sha3_t char +#define cx_sha256_t char + +#define cx_aes_key_t char diff --git a/unit-tests/mocks/lcx_sha3.h b/unit-tests/mocks/lcx_sha3.h new file mode 100644 index 0000000..e69de29 diff --git a/unit-tests/mocks/ledger_assert.h b/unit-tests/mocks/ledger_assert.h new file mode 100644 index 0000000..90e97d4 --- /dev/null +++ b/unit-tests/mocks/ledger_assert.h @@ -0,0 +1,6 @@ +#define LEDGER_ASSERT(test, message) \ + do { \ + if (!(test)) { \ + return; \ + } \ + } while (0) diff --git a/unit-tests/mocks/mocks.c b/unit-tests/mocks/mocks.c new file mode 100644 index 0000000..ebb703d --- /dev/null +++ b/unit-tests/mocks/mocks.c @@ -0,0 +1,17 @@ + +#include "os.h" + +unsigned char G_io_apdu_buffer[IO_APDU_BUFFER_SIZE]; + +unsigned short io_exchange(unsigned char channel_and_flags, unsigned short tx_len) { + (void) channel_and_flags; + (void) tx_len; + return 0; +} + +void nvm_write(void *dst_adr, void *src_adr, unsigned int src_len) { + (void) dst_adr; + (void) src_adr; + (void) src_len; + return; +} diff --git a/unit-tests/mocks/offsets.h b/unit-tests/mocks/offsets.h new file mode 100644 index 0000000..3db4bc1 --- /dev/null +++ b/unit-tests/mocks/offsets.h @@ -0,0 +1,26 @@ +#pragma once + +/** + * Offset of instruction class. + */ +#define OFFSET_CLA 0 +/** + * Offset of instruction code. + */ +#define OFFSET_INS 1 +/** + * Offset of instruction parameter 1. + */ +#define OFFSET_P1 2 +/** + * Offset of instruction parameter 2. + */ +#define OFFSET_P2 3 +/** + * Offset of command data length. + */ +#define OFFSET_LC 4 +/** + * Offset of command data. + */ +#define OFFSET_CDATA 5 diff --git a/unit-tests/mocks/os.h b/unit-tests/mocks/os.h new file mode 100644 index 0000000..a6c7cad --- /dev/null +++ b/unit-tests/mocks/os.h @@ -0,0 +1,22 @@ + +#include +#include + +#undef MAX +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) + +#define PRINTF(...) +#define THROW(x) + +// send tx_len bytes (atr or rapdu) and retrieve the length of the next command apdu (over the +// requested channel) +#define CHANNEL_APDU 0 +#define IO_RETURN_AFTER_TX 0x20 +#define IO_ASYNCH_REPLY 0x10 // avoid apdu state reset if tx_len == 0 when we're expected to reply + +#define IO_APDU_BUFFER_SIZE (255 + 5 + 64) + +extern unsigned char G_io_apdu_buffer[IO_APDU_BUFFER_SIZE]; + +extern unsigned short io_exchange(unsigned char channel_and_flags, unsigned short tx_len); +extern void nvm_write(void *dst_adr, void *src_adr, unsigned int src_len); diff --git a/unit-tests/mocks/os_utils.h b/unit-tests/mocks/os_utils.h new file mode 100644 index 0000000..510f2ef --- /dev/null +++ b/unit-tests/mocks/os_utils.h @@ -0,0 +1,23 @@ +#pragma once + +#define U2(hi, lo) ((((hi) &0xFFu) << 8) | ((lo) &0xFFu)) +#define U4(hi3, hi2, lo1, lo0) \ + ((((hi3) &0xFFu) << 24) | (((hi2) &0xFFu) << 16) | (((lo1) &0xFFu) << 8) | ((lo0) &0xFFu)) +static inline uint16_t U2BE(const uint8_t *buf, size_t off) { + return (buf[off] << 8) | buf[off + 1]; +} +static inline uint32_t U4BE(const uint8_t *buf, size_t off) { + return (((uint32_t) buf[off]) << 24) | (buf[off + 1] << 16) | (buf[off + 2] << 8) | + buf[off + 3]; +} + +static inline void U2BE_ENCODE(uint8_t *buf, size_t off, uint32_t value) { + buf[off + 0] = (value >> 8) & 0xFF; + buf[off + 1] = value & 0xFF; +} +static inline void U4BE_ENCODE(uint8_t *buf, size_t off, uint32_t value) { + buf[off + 0] = (value >> 24) & 0xFF; + buf[off + 1] = (value >> 16) & 0xFF; + buf[off + 2] = (value >> 8) & 0xFF; + buf[off + 3] = value & 0xFF; +} diff --git a/unit-tests/mocks/usbd_ccid_if.h b/unit-tests/mocks/usbd_ccid_if.h new file mode 100644 index 0000000..37dc501 --- /dev/null +++ b/unit-tests/mocks/usbd_ccid_if.h @@ -0,0 +1 @@ +#define PIN_OPR_APDU_CLA 0xEF diff --git a/unit-tests/mocks/ux.h b/unit-tests/mocks/ux.h new file mode 100644 index 0000000..7256169 --- /dev/null +++ b/unit-tests/mocks/ux.h @@ -0,0 +1 @@ +#define ux_state_t char diff --git a/unit-tests/src/test_io.c b/unit-tests/src/test_io.c new file mode 100644 index 0000000..e7d4fbe --- /dev/null +++ b/unit-tests/src/test_io.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include "gpg_vars.h" + +static int setup(void **state) { + (void) state; + + // Init tests + gpg_io_discard(1); + return 0; +} + +static void test_io(void **state) { + (void) state; + + unsigned int v32 = 0x789ABCDE; + unsigned int v16 = 0x3456; + unsigned int v8 = 0x12; + + gpg_io_insert_u8(v8); + + gpg_io_insert_u16(v16); + + // Mark the current offset + gpg_io_mark(); + + gpg_io_insert_u32(v32); + + // rewind offset to the beginning to the buffer + gpg_io_set_offset(0); + + assert_int_equal(gpg_io_fetch_u8(), v8); + assert_int_equal(gpg_io_fetch_u16(), v16); + assert_int_equal(gpg_io_fetch_u32(), v32); + + // rewind offset to the mark + gpg_io_set_offset(IO_OFFSET_MARK); + + assert_int_equal(gpg_io_fetch_u32(), v32); +} + +int main() { + const struct CMUnitTest tests[] = {cmocka_unit_test_setup(test_io, setup)}; + + return cmocka_run_group_tests(tests, NULL, NULL); +}