Link unit tests with speculos implementations of crypto syscalls
What changed, and why it matters
This commit is purely about improving the project's automated test setup. It links unit tests to a software simulator (Speculos) so cryptographic code can be tested on a regular computer without a physical Ledger device. It adds tests for a Bitcoin key-derivation function and re-enables an existing test file. There is no change to the actual app code that runs on the device, and nothing in the commit suggests a security vulnerability or fix.
No security action required. This is a test-infrastructure change. Routine review/merge is appropriate.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit modifies the CMake build for unit tests to fetch and link the Speculos emulator’s C implementations of BOLOS cryptographic syscalls, enabling host-native testing of src/crypto.c. It introduces unit-tests/libs/speculos_bridge.c/h as a thin wrapper mapping SDK symbol names to Speculos sys_cx_* primitives, plus host-side implementations of some lib_cxng high-level wrappers and aborting stubs for unused syscalls. It re-enables unit-tests/test_crypto.c, adding cmocka tests for bip32_CKDpub using BIP32 test vector 2, and updates CI dependency installation to include libssl-dev. No production firmware code is changed.
Changed components
unit-tests/CMakeLists.txtunit-tests/libs/speculos_bridge.cunit-tests/libs/speculos_bridge.hunit-tests/test_crypto.c.github/workflows/unit_tests.ymlunit-tests/README.mdInspect captured patch +747 / −29
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index d10e78a..afea9fc 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -15,3 +15,7 @@ jobs:
secrets: inherit
with:
test_directory: unit-tests
+ # OpenSSL is needed by the speculos-backed crypto tests. The
+ # speculos sources themselves are fetched at cmake configure time
+ # by unit-tests/CMakeLists.txt (FetchContent, pinned tag).
+ additional_packages: libssl-dev
diff --git a/unit-tests/CMakeLists.txt b/unit-tests/CMakeLists.txt
index 2e457db..2556679 100644
--- a/unit-tests/CMakeLists.txt
+++ b/unit-tests/CMakeLists.txt
@@ -1,6 +1,6 @@
-cmake_minimum_required(VERSION 3.10)
+cmake_minimum_required(VERSION 3.14) # FetchContent_MakeAvailable
-if(${CMAKE_VERSION} VERSION_LESS 3.10)
+if(${CMAKE_VERSION} VERSION_LESS 3.14)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif()
@@ -36,6 +36,42 @@ if(NOT DEFINED ENV{BOLOS_SDK})
message(FATAL_ERROR "BOLOS_SDK is not defined.")
endif()
+# Speculos is used by the speculos-backed crypto tests as the pure-C
+# implementation of the BOLOS syscalls, so we can link the real
+# application crypto code against it instead of writing per-syscall mocks.
+#
+# By default the source tree is fetched from upstream at configure time,
+# pinned to a known-good tag. To override (e.g. for offline builds or to
+# test against a local speculos checkout), pass -DSPECULOS_SRC=<path>
+# or set the SPECULOS_SRC environment variable.
+#
+# To skip the speculos-backed targets entirely, pass -DSPECULOS=OFF.
+option(SPECULOS "Build the speculos-backed crypto tests" ON)
+set(SPECULOS_GIT_TAG "v0.25.13" CACHE STRING
+ "Tag/branch/commit of speculos to fetch when SPECULOS_SRC is not set.")
+
+if(SPECULOS)
+ if(NOT DEFINED SPECULOS_SRC AND DEFINED ENV{SPECULOS_SRC})
+ set(SPECULOS_SRC $ENV{SPECULOS_SRC})
+ endif()
+ if(NOT SPECULOS_SRC)
+ include(FetchContent)
+ FetchContent_Declare(speculos
+ GIT_REPOSITORY https://github.com/LedgerHQ/speculos.git
+ GIT_TAG ${SPECULOS_GIT_TAG}
+ GIT_SHALLOW TRUE
+ )
+ # Populate without running speculos's own CMakeLists.txt — that one
+ # is configured for an ARM cross-compile and is not relevant here.
+ # We only want the source tree.
+ FetchContent_GetProperties(speculos)
+ if(NOT speculos_POPULATED)
+ FetchContent_Populate(speculos)
+ endif()
+ set(SPECULOS_SRC "${speculos_SOURCE_DIR}")
+ endif()
+endif()
+
add_compile_definitions(TEST DEBUG=0 SKIP_FOR_CMOCKA PRINTF=printf COIN_NATIVE_SEGWIT_PREFIX=\"tb\")
include_directories(../src)
@@ -67,8 +103,6 @@ add_executable(test_stream_merkleized_map_value test_stream_merkleized_map_value
add_executable(test_get_merkleized_map test_get_merkleized_map.c)
add_executable(test_get_merkleized_map_value test_get_merkleized_map_value.c)
-# add_executable(test_crypto test_crypto.c)
-
# Mock libraries
add_library(crypto_mocks SHARED libs/crypto_mocks.c)
add_library(sha256 SHARED libs/sha-256.c)
@@ -179,4 +213,105 @@ add_test(test_stream_merkleized_map_value test_stream_merkleized_map_value)
add_test(test_get_merkleized_map test_get_merkleized_map)
add_test(test_get_merkleized_map_value test_get_merkleized_map_value)
-# add_test(test_crypto test_crypto)
+# ---------------------------------------------------------------------------
+# Speculos-backed crypto tests
+# ---------------------------------------------------------------------------
+# test_crypto exercises the real app crypto code (src/crypto.c) by
+# linking against the C implementation of the BOLOS syscalls provided by
+# speculos. Enabled by default (see the SPECULOS option above). Requires
+# OpenSSL development headers on the host (libssl-dev on Debian/Ubuntu).
+
+if(SPECULOS AND SPECULOS_SRC)
+ find_package(OpenSSL REQUIRED)
+
+ message(STATUS "Building speculos-backed crypto tests from ${SPECULOS_SRC}")
+
+ set(SPECULOS_BOLOS_SOURCES
+ ${SPECULOS_SRC}/src/bolos/cx_bn.c
+ ${SPECULOS_SRC}/src/bolos/cx_blake2b.c
+ ${SPECULOS_SRC}/src/bolos/cx_curve25519.c
+ ${SPECULOS_SRC}/src/bolos/cx_ec.c
+ ${SPECULOS_SRC}/src/bolos/cx_ec_domain.c
+ ${SPECULOS_SRC}/src/bolos/cx_ecpoint.c
+ ${SPECULOS_SRC}/src/bolos/cx_ed25519.c
+ ${SPECULOS_SRC}/src/bolos/cx_hash.c
+ ${SPECULOS_SRC}/src/bolos/cx_hmac.c
+ ${SPECULOS_SRC}/src/bolos/cx_math.c
+ ${SPECULOS_SRC}/src/bolos/cx_montgomery.c
+ ${SPECULOS_SRC}/src/bolos/cx_mpi.c
+ ${SPECULOS_SRC}/src/bolos/cx_ripemd160.c
+ ${SPECULOS_SRC}/src/bolos/cx_rng_rfc6979.c
+ ${SPECULOS_SRC}/src/bolos/cx_scc.c
+ ${SPECULOS_SRC}/src/bolos/cx_sha256.c
+ ${SPECULOS_SRC}/src/bolos/cx_sha3.c
+ ${SPECULOS_SRC}/src/bolos/cx_sha512.c
+ ${SPECULOS_SRC}/src/bolos/cx_twisted_edwards.c
+ ${SPECULOS_SRC}/src/bolos/cx_utils.c
+ ${SPECULOS_SRC}/src/bolos/cx_weierstrass.c
+ ${SPECULOS_SRC}/src/bolos/cxlib.c
+ )
+
+ add_library(speculos_bolos STATIC ${SPECULOS_BOLOS_SOURCES})
+ target_compile_options(speculos_bolos PRIVATE
+ -w # speculos sources emit warnings that are not our concern
+ )
+ target_compile_definitions(speculos_bolos PRIVATE
+ OS_LITTLE_ENDIAN NATIVE_64BITS HAVE_BOLOS=1
+ )
+ target_include_directories(speculos_bolos PRIVATE
+ ${SPECULOS_SRC}/sdk
+ ${SPECULOS_SRC}/src
+ ${SPECULOS_SRC}/src/bolos
+ ${SPECULOS_SRC}/src/bolos/io/sdk/include
+ )
+ target_link_libraries(speculos_bolos PUBLIC OpenSSL::Crypto)
+
+ add_library(speculos_bridge STATIC libs/speculos_bridge.c)
+ target_include_directories(speculos_bridge PRIVATE
+ ${SPECULOS_SRC}/sdk
+ ${SPECULOS_SRC}/src
+ ${SPECULOS_SRC}/src/bolos
+ )
+ target_link_libraries(speculos_bridge PUBLIC speculos_bolos OpenSSL::Crypto)
+
+ # Build the application's crypto.c against the real SDK headers
+ # (not the per-test mock_includes) so it sees the same syscall
+ # signatures the speculos bridge implements.
+ add_library(app_crypto STATIC
+ ../src/crypto.c
+ ../src/secp256k1.c
+ )
+ target_compile_options(app_crypto PRIVATE -fno-stack-protector)
+ target_compile_definitions(app_crypto PRIVATE
+ HAVE_HASH HAVE_RIPEMD160 HAVE_SHA256 HAVE_SHA512 HAVE_HMAC HAVE_MATH
+ HAVE_ECC HAVE_ECC_WEIERSTRASS HAVE_SECP256K1_CURVE HAVE_ECDSA
+ API_LEVEL=22 OS_IO_SEPH_BUFFER_SIZE=272 IO_USB_MAX_ENDPOINTS=6
+ )
+ # BEFORE: SDK headers take precedence over the mock_includes added at
+ # global scope above. The whole point of this target is to compile
+ # crypto.c against the REAL SDK declarations.
+ target_include_directories(app_crypto BEFORE PRIVATE
+ ../src
+ ../src/debug-helpers
+ ../src/boilerplate
+ $ENV{BOLOS_SDK}/target/nanox/include
+ $ENV{BOLOS_SDK}/include
+ $ENV{BOLOS_SDK}/io/include
+ $ENV{BOLOS_SDK}/io_legacy/include
+ $ENV{BOLOS_SDK}/protocol/include
+ $ENV{BOLOS_SDK}/lib_standard_app
+ $ENV{BOLOS_SDK}/lib_cxng/include
+ $ENV{BOLOS_SDK}/lib_cxng/src
+ $ENV{BOLOS_SDK}/lib_stusb/include
+ )
+
+ add_executable(test_crypto test_crypto.c)
+ target_include_directories(test_crypto PRIVATE libs)
+ target_link_libraries(test_crypto PRIVATE
+ cmocka gcov
+ app_crypto
+ base58 read write
+ speculos_bridge
+ )
+ add_test(test_crypto test_crypto)
+endif()
diff --git a/unit-tests/README.md b/unit-tests/README.md
index 23dc436..60e75b7 100644
--- a/unit-tests/README.md
+++ b/unit-tests/README.md
@@ -14,7 +14,7 @@ and for code coverage generation:
On Ubuntu, the following command will install the required dependencies:
```
-sudo apt install cmake libcmocka-dev lcov
+sudo apt install cmake libcmocka-dev lcov libssl-dev
```
## Overview
@@ -31,6 +31,27 @@ and run tests with
CTEST_OUTPUT_ON_FAILURE=1 make -C build test
```
+## Speculos-backed crypto tests
+
+A subset of tests links against the C implementation of the BOLOS syscalls provided by
+[speculos](https://github.com/LedgerHQ/speculos), removing the need for hand-written per-syscall mocks (usually for cryptography calls).
+
+The speculos sources are fetched automatically at cmake configure time
+(pinned to a known-good tag — see `SPECULOS_GIT_TAG` in
+`CMakeLists.txt`).
+
+To use an out-of-tree speculos checkout instead of the fetched one:
+
+```
+cmake -Bbuild -H. -DSPECULOS_SRC=/path/to/speculos
+```
+
+To skip these targets entirely:
+
+```
+cmake -Bbuild -H. -DSPECULOS=OFF
+```
+
## Generate code coverage
Just execute in `unit-tests` folder
diff --git a/unit-tests/libs/speculos_bridge.c b/unit-tests/libs/speculos_bridge.c
new file mode 100644
index 0000000..470e547
--- /dev/null
+++ b/unit-tests/libs/speculos_bridge.c
@@ -0,0 +1,419 @@
+/**
+ * Speculos bridge: SDK-name → speculos-name forwarders, and host-side
+ * implementations of the lib_cxng high-level wrappers that the
+ * application uses.
+ *
+ * The application is written against the Ledger SDK API (cx_bn_lock,
+ * cx_ecpoint_alloc, cx_hmac_sha512, cx_ecfp_add_point_no_throw, ...).
+ *
+ * Speculos provides the *pure-C* implementation of the underlying
+ * primitives, but under sys_-prefixed symbol names because on the
+ * device they're reached through an SVC dispatcher.
+ *
+ * Functions not used by code-under-test are stubbed with explicit
+ * aborts, in order to keep the linker happy but fail loudly if
+ * called. Stubs can be replaced with real implementations as needed.
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <openssl/rand.h>
+
+#include "speculos_bridge.h"
+
+/* Pull in the speculos type/prototype definitions. We use the speculos
+ * header chain rather than the SDK one to avoid double-defining
+ * cx_bn_t / cx_ecpoint_t. */
+#define _SDK_2_0_
+#include "bolos/cxlib.h"
+
+/* ------------------------------------------------------------------
+ * Forwarders: SDK name -> sys_cx_* implementation
+ * ------------------------------------------------------------------ */
+
+cx_err_t cx_bn_lock(size_t word_nbytes, uint32_t flags) {
+ return sys_cx_bn_lock(word_nbytes, flags);
+}
+
+uint32_t cx_bn_unlock(void) {
+ return sys_cx_bn_unlock();
+}
+
+cx_err_t cx_bn_alloc(cx_bn_t *bn_x, size_t size) {
+ return sys_cx_bn_alloc(bn_x, size);
+}
+
+cx_err_t cx_bn_alloc_init(cx_bn_t *bn_x, size_t size,
+ const uint8_t *bytes, size_t nbytes) {
+ return sys_cx_bn_alloc_init(bn_x, size, bytes, nbytes);
+}
+
+cx_err_t cx_bn_destroy(cx_bn_t *bn_x) { return sys_cx_bn_destroy(bn_x); }
+
+cx_err_t cx_bn_cmp(const cx_bn_t a, const cx_bn_t b, int *diff) {
+ return sys_cx_bn_cmp(a, b, diff);
+}
+
+cx_err_t cx_ecdomain_parameters_length(cx_curve_t curve, size_t *length) {
+ return sys_cx_ecdomain_parameters_length(curve, length);
+}
+
+cx_err_t cx_ecpoint_alloc(cx_ecpoint_t *P, cx_curve_t cv) {
+ return sys_cx_ecpoint_alloc(P, cv);
+}
+
+cx_err_t cx_ecpoint_destroy(cx_ecpoint_t *P) { return sys_cx_ecpoint_destroy(P); }
+
+cx_err_t cx_ecpoint_init(cx_ecpoint_t *p, const uint8_t *x, size_t x_len,
+ const uint8_t *y, size_t y_len) {
+ return sys_cx_ecpoint_init(p, x, x_len, y, y_len);
+}
+
+cx_err_t cx_ecpoint_export(const cx_ecpoint_t *p, uint8_t *x, size_t x_len,
+ uint8_t *y, size_t y_len) {
+ return sys_cx_ecpoint_export(p, x, x_len, y, y_len);
+}
+
+cx_err_t cx_ecpoint_add(cx_ecpoint_t *r, const cx_ecpoint_t *p,
+ const cx_ecpoint_t *q) {
+ return sys_cx_ecpoint_add(r, p, q);
+}
+
+cx_err_t cx_ecpoint_scalarmul(cx_ecpoint_t *p, const uint8_t *k, size_t k_len) {
+ return sys_cx_ecpoint_scalarmul(p, k, k_len);
+}
+
+cx_err_t cx_ecpoint_rnd_scalarmul(cx_ecpoint_t *p, const uint8_t *k,
+ size_t k_len) {
+ return sys_cx_ecpoint_rnd_scalarmul(p, k, k_len);
+}
+
+/* ------------------------------------------------------------------
+ * High-level lib_cxng wrappers, re-implemented here for the host.
+ *
+ * These are literal copies of the SDK source (lib_cxng/src/cx_math.c
+ * and cx_ecfp.c). We re-implement instead of compiling the SDK file
+ * because the SDK source pulls in a large set of headers that conflict
+ * with the unit-test mock environment.
+ * ------------------------------------------------------------------ */
+
+cx_err_t cx_math_cmp_no_throw(const uint8_t *a, const uint8_t *b,
+ size_t length, int *diff) {
+ cx_err_t error;
+ cx_bn_t bn_a, bn_b;
+
+ if ((error = sys_cx_bn_lock(length, 0))) return error;
+ if ((error = sys_cx_bn_alloc_init(&bn_a, length, a, length))) goto end;
+ if ((error = sys_cx_bn_alloc_init(&bn_b, length, b, length))) goto end;
+ error = sys_cx_bn_cmp(bn_a, bn_b, diff);
+end:
+ sys_cx_bn_unlock();
+ return error;
+}
+
+cx_err_t cx_ecfp_scalar_mult_no_throw(cx_curve_t curve, uint8_t *P,
+ const uint8_t *k, size_t k_len) {
+ size_t size;
+ cx_ecpoint_t ecP;
+ cx_err_t error;
+
+ if ((error = sys_cx_ecdomain_parameters_length(curve, &size))) return error;
+ if ((error = sys_cx_bn_lock(size, 0))) return error;
+
+ if ((error = sys_cx_ecpoint_alloc(&ecP, curve))) goto end;
+ if ((error = sys_cx_ecpoint_init(&ecP, P + 1, size, P + 1 + size, size)))
+ goto end;
+ if ((error = sys_cx_ecpoint_rnd_scalarmul(&ecP, k, k_len))) goto end;
+ P[0] = 0x04;
+ error = sys_cx_ecpoint_export(&ecP, &P[1], size, &P[1 + size], size);
+end:
+ sys_cx_bn_unlock();
+ return error;
+}
+
+cx_err_t cx_ecfp_add_point_no_throw(cx_curve_t curve, unsigned char *R,
+ const unsigned char *P,
+ const unsigned char *Q) {
+ size_t size;
+ cx_ecpoint_t ecR, ecP, ecQ;
+ cx_err_t error;
+
+ if ((error = sys_cx_ecdomain_parameters_length(curve, &size))) return error;
+ if ((error = sys_cx_bn_lock(size, 0))) return error;
+
+ if ((error = sys_cx_ecpoint_alloc(&ecP, curve))) goto end;
+ if ((error = sys_cx_ecpoint_alloc(&ecQ, curve))) goto end;
+ if ((error = sys_cx_ecpoint_alloc(&ecR, curve))) goto end;
+ if ((error = sys_cx_ecpoint_init(&ecP, P + 1, size, P + 1 + size, size)))
+ goto end;
+ if ((error = sys_cx_ecpoint_init(&ecQ, Q + 1, size, Q + 1 + size, size)))
+ goto end;
+ if ((error = sys_cx_ecpoint_add(&ecR, &ecP, &ecQ))) goto end;
+ R[0] = 0x04;
+ error = sys_cx_ecpoint_export(&ecR, &R[1], size, &R[1 + size], size);
+end:
+ sys_cx_bn_unlock();
+ return error;
+}
+
+/* HMAC-SHA512 is implemented directly by speculos under a spec_ prefix. */
+extern int spec_cx_hmac_sha512(const unsigned char *key, unsigned int key_len,
+ const unsigned char *in, unsigned int len,
+ unsigned char *out, unsigned int out_len);
+
+size_t cx_hmac_sha512(const uint8_t *key, size_t key_len, const uint8_t *in,
+ size_t len, uint8_t *out, size_t out_len) {
+ return (size_t) spec_cx_hmac_sha512(key, (unsigned int) key_len, in,
+ (unsigned int) len, out,
+ (unsigned int) out_len);
+}
+
+/* SHA-256 one-shot. */
+extern int sys_cx_hash_sha256(const uint8_t *in, size_t len, uint8_t *out,
+ size_t out_len);
+
+int cx_hash_sha256(const uint8_t *in, size_t len, uint8_t *out,
+ size_t out_len) {
+ return sys_cx_hash_sha256(in, len, out, out_len);
+}
+
+/* RIPEMD-160 one-shot, forwarded as iovec wrapper. */
+extern int sys_cx_hash_ripemd160(const uint8_t *in, size_t in_len,
+ uint8_t *out, size_t out_len);
+
+typedef struct {
+ const uint8_t *iov_base;
+ size_t iov_len;
+} cx_iovec_t_local;
+
+cx_err_t cx_ripemd160_hash_iovec(const cx_iovec_t_local *iovec,
+ size_t iovec_count, uint8_t digest[20]) {
+ /* The app currently uses a single-iovec call. Concatenate if needed. */
+ if (iovec_count == 1) {
+ return (sys_cx_hash_ripemd160(iovec[0].iov_base, iovec[0].iov_len,
+ digest, 20) == 20) ? 0 : 0xFFFFFF85;
+ }
+
+ size_t total = 0;
+ for (size_t i = 0; i < iovec_count; i++) total += iovec[i].iov_len;
+ uint8_t *buf = malloc(total);
+ if (!buf) return 0xFFFFFF8B; /* CX_MEMORY_FULL */
+ size_t off = 0;
+ for (size_t i = 0; i < iovec_count; i++) {
+ memcpy(buf + off, iovec[i].iov_base, iovec[i].iov_len);
+ off += iovec[i].iov_len;
+ }
+ int rc = sys_cx_hash_ripemd160(buf, total, digest, 20);
+ free(buf);
+ return (rc == 20) ? 0 : 0xFFFFFF85;
+}
+
+/* ------------------------------------------------------------------
+ * Stubs for syscalls referenced by other parts of crypto.c that this
+ * test target never exercises. Calling any of them is a programming
+ * error in the test, so we abort loudly.
+ * ------------------------------------------------------------------ */
+
+#define STUB_ABORT(name) \
+ fprintf(stderr, \
+ "speculos_bridge: %s called but not implemented in this test" \
+ " harness.\n", \
+ name); \
+ abort()
+
+cx_err_t cx_hash_no_throw(void *hash, int mode, const unsigned char *in,
+ size_t len, unsigned char *out, size_t out_len) {
+ (void) hash; (void) mode; (void) in; (void) len; (void) out; (void) out_len;
+ STUB_ABORT("cx_hash_no_throw");
+}
+
+cx_err_t cx_sha256_init_no_throw(void *hash) {
+ (void) hash; STUB_ABORT("cx_sha256_init_no_throw");
+}
+
+cx_err_t cx_sha256_hash_iovec(const void *iovec, size_t iovec_count,
+ uint8_t *out) {
+ (void) iovec; (void) iovec_count; (void) out;
+ STUB_ABORT("cx_sha256_hash_iovec");
+}
+
+cx_err_t cx_math_addm_no_throw(uint8_t *r, const uint8_t *a, const uint8_t *b,
+ const uint8_t *m, size_t len) {
+ cx_bn_t bn_r, bn_a, bn_b, bn_m;
+ cx_err_t error;
+ if ((error = sys_cx_bn_lock(len, 0))) return error;
+ if ((error = sys_cx_bn_alloc(&bn_r, len))) goto end;
+ if ((error = sys_cx_bn_alloc_init(&bn_a, len, a, len))) goto end;
+ if ((error = sys_cx_bn_alloc_init(&bn_b, len, b, len))) goto end;
+ if ((error = sys_cx_bn_alloc_init(&bn_m, len, m, len))) goto end;
+ if ((error = sys_cx_bn_mod_add(bn_r, bn_a, bn_b, bn_m))) goto end;
+ error = sys_cx_bn_export(bn_r, r, len);
+end:
+ sys_cx_bn_unlock();
+ return error;
+}
+
+cx_err_t cx_math_powm_no_throw(uint8_t *r, const uint8_t *a, const uint8_t *e,
+ size_t len_e, const uint8_t *m, size_t len) {
+ cx_bn_t bn_r, bn_a, bn_m;
+ cx_err_t error;
+ if ((error = sys_cx_bn_lock(len, 0))) return error;
+ if ((error = sys_cx_bn_alloc(&bn_r, len))) goto end;
+ if ((error = sys_cx_bn_alloc_init(&bn_a, len, a, len))) goto end;
+ if ((error = sys_cx_bn_alloc_init(&bn_m, len, m, len))) goto end;
+ if ((error = sys_cx_bn_mod_pow(bn_r, bn_a, e, (uint32_t) len_e, bn_m)))
+ goto end;
+ error = sys_cx_bn_export(bn_r, r, len);
+end:
+ sys_cx_bn_unlock();
+ return error;
+}
+
+cx_err_t cx_math_sub_no_throw(uint8_t *r, const uint8_t *a, const uint8_t *b,
+ size_t len) {
+ cx_bn_t bn_r, bn_a, bn_b;
+ cx_err_t error;
+ if ((error = sys_cx_bn_lock(len, 0))) return error;
+ if ((error = sys_cx_bn_alloc(&bn_r, len))) goto end;
+ if ((error = sys_cx_bn_alloc_init(&bn_a, len, a, len))) goto end;
+ if ((error = sys_cx_bn_alloc_init(&bn_b, len, b, len))) goto end;
+ /* CX_CARRY is expected for sub when a < b; treat as ok. */
+ error = sys_cx_bn_sub(bn_r, bn_a, bn_b);
+ if (error && error != 0xFFFFFF21) goto end;
+ error = sys_cx_bn_export(bn_r, r, len);
+end:
+ sys_cx_bn_unlock();
+ return error;
+}
+
+cx_err_t cx_ecfp_generate_pair_no_throw(cx_curve_t curve, void *pubkey,
+ void *privkey, int keepprivate) {
+ (void) curve; (void) pubkey; (void) privkey; (void) keepprivate;
+ STUB_ABORT("cx_ecfp_generate_pair_no_throw");
+}
+
+cx_err_t cx_ecdsa_sign_no_throw(const void *pvkey, uint32_t mode,
+ uint32_t hashID, const uint8_t *hash,
+ size_t hash_len, uint8_t *sig,
+ size_t *sig_len, uint32_t *info) {
+ (void) pvkey; (void) mode; (void) hashID; (void) hash; (void) hash_len;
+ (void) sig; (void) sig_len; (void) info;
+ STUB_ABORT("cx_ecdsa_sign_no_throw");
+}
+
+cx_err_t bip32_derive_with_seed_init_privkey_256(unsigned int derivation_mode,
+ cx_curve_t curve,
+ const uint32_t *path,
+ size_t path_len,
+ void *privkey,
+ uint8_t *chain_code,
+ unsigned char *seed,
+ size_t seed_len) {
+ (void) derivation_mode; (void) curve; (void) path; (void) path_len;
+ (void) privkey; (void) chain_code; (void) seed; (void) seed_len;
+ STUB_ABORT("bip32_derive_with_seed_init_privkey_256");
+}
+
+cx_err_t bip32_derive_with_seed_get_pubkey_256(unsigned int derivation_mode,
+ cx_curve_t curve,
+ const uint32_t *path,
+ size_t path_len,
+ uint8_t raw_pubkey[65],
+ uint8_t *chain_code,
+ cx_md_t hashID,
+ unsigned char *seed,
+ size_t seed_len) {
+ (void) derivation_mode; (void) curve; (void) path; (void) path_len;
+ (void) raw_pubkey; (void) chain_code; (void) hashID; (void) seed; (void) seed_len;
+ STUB_ABORT("bip32_derive_with_seed_get_pubkey_256");
+}
+
+unsigned long os_perso_derive_node_with_seed_key(unsigned int mode,
+ cx_curve_t curve,
+ const unsigned int *path,
+ unsigned int path_len,
+ unsigned char *privkey,
+ unsigned char *chain,
+ unsigned char *seed_key,
+ unsigned int seed_key_len) {
+ (void) mode; (void) curve; (void) path; (void) path_len; (void) privkey;
+ (void) chain; (void) seed_key; (void) seed_key_len;
+ STUB_ABORT("os_perso_derive_node_with_seed_key");
+}
+
+unsigned long os_perso_get_master_key_identifier(uint8_t *id, size_t id_len) {
+ (void) id; (void) id_len;
+ STUB_ABORT("os_perso_get_master_key_identifier");
+}
+
+/* ------------------------------------------------------------------
+ * BOLOS runtime stubs
+ * ------------------------------------------------------------------ */
+
+/* Custom setjmp/longjmp are ARM-asm in speculos. On the host we don't
+ * need an exception mechanism: any THROW path is treated as a fatal
+ * test failure. */
+void os_longjmp(unsigned int exception) {
+ fprintf(stderr, "os_longjmp(%u) — BOLOS exception in host test.\n",
+ exception);
+ abort();
+}
+
+/* sys_try_context_get is referenced by os_longjmp inside speculos's
+ * exception.c, but on the host we redefine os_longjmp above, so this
+ * is dead code. Provide a stub anyway. */
+void *sys_try_context_set(void *ctx) { (void) ctx; return NULL; }
+void *sys_try_context_get(void) { return NULL; }
+
+void assert_exit(bool confirm, const char *file, unsigned int line) {
+ (void) confirm; (void) file; (void) line;
+ fprintf(stderr, "assert_exit fired in host test at %s:%u\n", file, line);
+ abort();
+}
+
+/* sys_cx_rng is referenced by speculos's cxlib.c (sys_cx_get_random_bytes
+ * forwards to it). On the device it pulls from the TRNG; on the host we
+ * route to OpenSSL. */
+unsigned long sys_cx_rng(uint8_t *buffer, unsigned int length) {
+ if (RAND_bytes(buffer, (int) length) != 1) abort();
+ return (unsigned long) buffer;
+}
+
+/* ED25519 entry points from libcrypto are not directly available on
+ * modern OpenSSL; speculos references them from libsodium. Our test
+ * target does not exercise ED25519, so stubbing keeps the linker happy. */
+int ED25519_public_from_private(uint8_t out_pub[32], const uint8_t priv[32]) {
+ (void) out_pub; (void) priv; STUB_ABORT("ED25519_public_from_private");
+}
+int ED25519_sign(uint8_t *out_sig, const uint8_t *msg, size_t msg_len,
+ const uint8_t pub[32], const uint8_t priv[32]) {
+ (void) out_sig; (void) msg; (void) msg_len; (void) pub; (void) priv;
+ STUB_ABORT("ED25519_sign");
+}
+int ED25519_verify(const uint8_t *msg, size_t msg_len,
+ const uint8_t sig[64], const uint8_t pub[32]) {
+ (void) msg; (void) msg_len; (void) sig; (void) pub;
+ STUB_ABORT("ED25519_verify");
+}
+
+/* The application code references `try_context_get` / `try_context_set`
+ * via the SDK header. On the device these are the BOLOS exception-stack
+ * primitives. On the host we redirect THROW to abort, so these never
+ * fire; provide trivial forwarders. */
+void *try_context_set(void *ctx) { return sys_try_context_set(ctx); }
+void *try_context_get(void) { return sys_try_context_get(); }
+
+void speculos_bridge_init(void) {
+ /* Deterministic OpenSSL RNG seed for reproducible test runs. */
+ static const uint8_t seed[32] = {
+ 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x6f, 0x73,
+ 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2d,
+ 0x73, 0x65, 0x65, 0x64, 0x2d, 0x66, 0x6f, 0x72,
+ 0x2d, 0x75, 0x6e, 0x69, 0x74, 0x74, 0x65, 0x73,
+ };
+ RAND_seed(seed, sizeof(seed));
+}
diff --git a/unit-tests/libs/speculos_bridge.h b/unit-tests/libs/speculos_bridge.h
new file mode 100644
index 0000000..e34207b
--- /dev/null
+++ b/unit-tests/libs/speculos_bridge.h
@@ -0,0 +1,33 @@
+/**
+ * Speculos bridge for host-side unit tests.
+ *
+ * This header declares the (small) bridge layer that lets the application
+ * code link against the C implementation of the Ledger BOLOS syscalls
+ * provided by speculos (https://github.com/LedgerHQ/speculos), so that
+ * code under test can be exercised without needing a device or an
+ * ARM emulator.
+ *
+ * It is NOT a replacement for the SDK headers: it just provides the
+ * forwarders/wrappers that bind the SDK symbol names expected by the
+ * application code to the speculos primitives (`sys_cx_*`, `spec_cx_*`).
+ *
+ * Including this header from a test is optional: the bridge is linked
+ * at the symbol level.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Initialise OpenSSL RNG with a deterministic seed so test runs are
+ * reproducible. Call once from main() before exercising code under test. */
+void speculos_bridge_init(void);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/unit-tests/test_crypto.c b/unit-tests/test_crypto.c
index e165ecb..a7c236e 100644
--- a/unit-tests/test_crypto.c
+++ b/unit-tests/test_crypto.c
@@ -1,3 +1,7 @@
+/**
+ * Unit tests for the functions defined in src/crypto.c.
+ */
+
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
@@ -6,47 +10,143 @@
#include <setjmp.h>
#include <cmocka.h>
-#include "../src/crypto.h"
+#include "speculos_bridge.h"
+#include "crypto.h"
+
+/* ---------------------------------------------------------------- */
+/* bip32_CKDpub */
+/* */
+/* Test vectors come from BIP32 Test Vector 2 (m and m/0). */
+/* https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki */
+/* ---------------------------------------------------------------- */
+
+/* m: depth=0, parent_fp=0, child=0
+ * chain_code: 60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689
+ * key: 03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7
+ */
+static const serialized_extended_pubkey_t tv2_m = {
+ .version = {0x04, 0x88, 0xB2, 0x1E},
+ .depth = 0x00,
+ .parent_fingerprint = {0x00, 0x00, 0x00, 0x00},
+ .child_number = {0x00, 0x00, 0x00, 0x00},
+ .chain_code =
+ {
+ 0x60, 0x49, 0x9f, 0x80, 0x1b, 0x89, 0x6d, 0x83, 0x17, 0x9a, 0x43,
+ 0x74, 0xae, 0xb7, 0x82, 0x2a, 0xae, 0xac, 0xea, 0xa0, 0xdb, 0x1f,
+ 0x85, 0xee, 0x3e, 0x90, 0x4c, 0x4d, 0xef, 0xbd, 0x96, 0x89,
+ },
+ .compressed_pubkey =
+ {
+ 0x03, 0xcb, 0xca, 0xa9, 0xc9, 0x8c, 0x87, 0x7a, 0x26, 0x97, 0x7d,
+ 0x00, 0x82, 0x5c, 0x95, 0x6a, 0x23, 0x8e, 0x8d, 0xdd, 0xfb, 0xd3,
+ 0x22, 0xcc, 0xe4, 0xf7, 0x4b, 0x0b, 0x5b, 0xd6, 0xac, 0xe4, 0xa7,
+ },
+};
+
+/* m/0: depth=1, parent_fp=bd16bee5, child=0
+ * chain_code: f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c
+ * key: 02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea
+ */
+static const serialized_extended_pubkey_t tv2_m_0_expected = {
+ .version = {0x04, 0x88, 0xB2, 0x1E},
+ .depth = 0x01,
+ .parent_fingerprint = {0xbd, 0x16, 0xbe, 0xe5},
+ .child_number = {0x00, 0x00, 0x00, 0x00},
+ .chain_code =
+ {
+ 0xf0, 0x90, 0x9a, 0xff, 0xaa, 0x7e, 0xe7, 0xab, 0xe5, 0xdd, 0x4e,
+ 0x10, 0x05, 0x98, 0xd4, 0xdc, 0x53, 0xcd, 0x70, 0x9d, 0x5a, 0x5c,
+ 0x2c, 0xac, 0x40, 0xe7, 0x41, 0x2f, 0x23, 0x2f, 0x7c, 0x9c,
+ },
+ .compressed_pubkey =
+ {
+ 0x02, 0xfc, 0x9e, 0x5a, 0xf0, 0xac, 0x8d, 0x9b, 0x3c, 0xec, 0xfe,
+ 0x2a, 0x88, 0x8e, 0x21, 0x17, 0xba, 0x3d, 0x08, 0x9d, 0x85, 0x85,
+ 0x88, 0x6c, 0x9c, 0x82, 0x6b, 0x6b, 0x22, 0xa9, 0x8d, 0x12, 0xea,
+ },
+};
+
+static void test_ckdpub_tv2_m_to_m0(void **state) {
+ (void) state;
+
+ serialized_extended_pubkey_t child = {0};
+ uint8_t tweak[32];
+
+ int ret = bip32_CKDpub(&tv2_m, 0, &child, tweak);
+ assert_int_equal(ret, 0);
+
+ assert_memory_equal(child.version, tv2_m_0_expected.version, 4);
+ assert_int_equal(child.depth, tv2_m_0_expected.depth);
+ assert_memory_equal(child.parent_fingerprint, tv2_m_0_expected.parent_fingerprint, 4);
+ assert_memory_equal(child.child_number, tv2_m_0_expected.child_number, 4);
+ assert_memory_equal(child.chain_code, tv2_m_0_expected.chain_code, 32);
+ assert_memory_equal(child.compressed_pubkey, tv2_m_0_expected.compressed_pubkey, 33);
+}
+
+static void test_ckdpub_in_place(void **state) {
+ (void) state;
+
+ /* child == parent must be allowed per the docstring. */
+ serialized_extended_pubkey_t buf = tv2_m;
+
+ int ret = bip32_CKDpub(&buf, 0, &buf, NULL);
+ assert_int_equal(ret, 0);
+ assert_memory_equal(buf.chain_code, tv2_m_0_expected.chain_code, 32);
+ assert_memory_equal(buf.compressed_pubkey, tv2_m_0_expected.compressed_pubkey, 33);
+}
+
+static void test_ckdpub_rejects_hardened(void **state) {
+ (void) state;
+
+ serialized_extended_pubkey_t child = {0};
+ /* 0x80000000 is the first hardened index. */
+ int ret = bip32_CKDpub(&tv2_m, 0x80000000u, &child, NULL);
+ assert_int_equal(ret, -1);
+}
+
+static void test_ckdpub_rejects_max_depth(void **state) {
+ (void) state;
+
+ serialized_extended_pubkey_t parent = tv2_m;
+ parent.depth = 255;
+ serialized_extended_pubkey_t child = {0};
+
+ int ret = bip32_CKDpub(&parent, 0, &child, NULL);
+ assert_int_equal(ret, -1);
+}
+
+/* ---------------------------------------------------------------- */
+/* crypto_get_compressed_pubkey */
+/* ---------------------------------------------------------------- */
// clang-format off
-// HACK: define empty functions for the expected imports in cx.h and os.h.
-int cx_ecfp_generate_pair ( cx_curve_t curve, cx_ecfp_public_key_t * pubkey, cx_ecfp_private_key_t * privkey, int keepprivate ){return 0;}
-int cx_hash_sha256 ( const unsigned char * in, unsigned int len, unsigned char * out, unsigned int out_len ){return 0;}
-int cx_hash ( cx_hash_t * hash, int mode, const unsigned char * in, unsigned int len, unsigned char * out, unsigned int out_len ){return 0;}
-int cx_ecfp_init_private_key ( cx_curve_t curve, const unsigned char * rawkey, unsigned int key_len, cx_ecfp_private_key_t * pvkey ){return 0;}
-int cx_ripemd160_init ( cx_ripemd160_t * hash ){return 0;}
-void os_memmove(void * dst, const void * src, unsigned int length){}
-void os_perso_derive_node_bip32 ( cx_curve_t curve, const unsigned int * path, unsigned int pathLength, unsigned char * privateKey, unsigned char * chain ){}
-
-const uint8_t uncompressed_key_02[] = {
+static const uint8_t uncompressed_key_02[] = {
0x04,
0xee,0x86,0x08,0x20,0x7e,0x21,0x02,0x84,0x26,0xf6,0x9e,0x76,0x44,0x7d,0x7e,0x3d,
0x5e,0x07,0x70,0x49,0xf5,0xe6,0x83,0xc3,0x13,0x6c,0x23,0x14,0x76,0x2a,0x47,0x18,
0xb4,0x5f,0x52,0x24,0xb0,0x5e,0xbb,0xad,0x09,0xf4,0x35,0x94,0xb7,0xbd,0x8d,0xc0,
0xef,0xf4,0x51,0x9a,0x07,0xcb,0xab,0x37,0xec,0xc6,0x6e,0x00,0x01,0xab,0x95,0x9a // even
};
-const uint8_t compressed_key_02[] = {
+static const uint8_t compressed_key_02[] = {
0x02,
0xee,0x86,0x08,0x20,0x7e,0x21,0x02,0x84,0x26,0xf6,0x9e,0x76,0x44,0x7d,0x7e,0x3d,
0x5e,0x07,0x70,0x49,0xf5,0xe6,0x83,0xc3,0x13,0x6c,0x23,0x14,0x76,0x2a,0x47,0x18
};
-
-const uint8_t uncompressed_key_03[] = {
+static const uint8_t uncompressed_key_03[] = {
0x04,
0xdf,0x94,0x6e,0x0b,0x3f,0x6a,0xd7,0xf3,0x55,0x6b,0x53,0x71,0x62,0xf3,0x9f,0x07,
0xfa,0x04,0x60,0x63,0x41,0x26,0x5f,0xe9,0x95,0xf3,0xfa,0x51,0x1f,0x7f,0xc2,0x13,
0x1d,0x5e,0x56,0x4f,0xc5,0x1b,0x4f,0xb9,0x1a,0x83,0x67,0x73,0x3b,0x97,0xc7,0x6a,
0x5c,0x99,0x70,0x5d,0x7e,0x99,0x12,0x59,0xb7,0x9d,0x8c,0xa3,0x65,0x35,0x09,0xcb // odd
};
-const uint8_t compressed_key_03[] = {
+static const uint8_t compressed_key_03[] = {
0x03,
0xdf,0x94,0x6e,0x0b,0x3f,0x6a,0xd7,0xf3,0x55,0x6b,0x53,0x71,0x62,0xf3,0x9f,0x07,
0xfa,0x04,0x60,0x63,0x41,0x26,0x5f,0xe9,0x95,0xf3,0xfa,0x51,0x1f,0x7f,0xc2,0x13
};
-
-const uint8_t uncompressed_key_invalid[] = {
+static const uint8_t uncompressed_key_invalid[] = {
0x05, // does not start with 0x04; invalid
0xdf,0x94,0x6e,0x0b,0x3f,0x6a,0xd7,0xf3,0x55,0x6b,0x53,0x71,0x62,0xf3,0x9f,0x07,
0xfa,0x04,0x60,0x63,0x41,0x26,0x5f,0xe9,0x95,0xf3,0xfa,0x51,0x1f,0x7f,0xc2,0x13,
@@ -104,11 +204,17 @@ static void test_get_compressed_pubkey_invalid(void **state) {
assert_int_equal(ret, -1);
}
-int main() {
- const struct CMUnitTest tests[] = {cmocka_unit_test(test_get_compressed_pubkey_02),
- cmocka_unit_test(test_get_compressed_pubkey_03),
- cmocka_unit_test(test_get_compressed_pubkey_in_place),
- cmocka_unit_test(test_get_compressed_pubkey_invalid)};
-
+int main(void) {
+ speculos_bridge_init();
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(test_ckdpub_tv2_m_to_m0),
+ cmocka_unit_test(test_ckdpub_in_place),
+ cmocka_unit_test(test_ckdpub_rejects_hardened),
+ cmocka_unit_test(test_ckdpub_rejects_max_depth),
+ cmocka_unit_test(test_get_compressed_pubkey_02),
+ cmocka_unit_test(test_get_compressed_pubkey_03),
+ cmocka_unit_test(test_get_compressed_pubkey_in_place),
+ cmocka_unit_test(test_get_compressed_pubkey_invalid),
+ };
return cmocka_run_group_tests(tests, NULL, NULL);
}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.