Add unit tests for crypto_get_compressed_pubkey_at_path
What changed, and why it matters
This commit only adds unit tests and test-support code. It does not change the actual Bitcoin app that runs on Ledger devices, so it cannot introduce a security vulnerability in shipped firmware. The changes make the test harness able to simulate BIP32 key derivation using the Speculos emulator's own implementation, with a fixed public test seed.
No security action required. Treat as normal test-coverage improvement. If reviewing the broader PR, verify that the production implementation of crypto_get_compressed_pubkey_at_path was not changed in adjacent commits.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit extends the host unit-test harness (speculos_bridge) to link Speculos’s os_bip32.c and forward BIP32/os_perso and cx_ecfp_ calls to Speculos. It then adds four cmocka tests for crypto_get_compressed_pubkey_at_path covering m, m/0, m/44’/0’/0’, and m/44’/0’/0’/0/0, with expected values derived from the Speculos default test seed. The production code under test is not modified; only test scaffolding and assertions are added.
Changed components
unit-tests/CMakeLists.txtunit-tests/libs/speculos_bridge.cunit-tests/libs/speculos_bridge.hunit-tests/test_crypto.cInspect captured patch +429 / −116
diff --git a/unit-tests/CMakeLists.txt b/unit-tests/CMakeLists.txt
index 2556679..67d6b91 100644
--- a/unit-tests/CMakeLists.txt
+++ b/unit-tests/CMakeLists.txt
@@ -249,6 +249,7 @@ if(SPECULOS AND SPECULOS_SRC)
${SPECULOS_SRC}/src/bolos/cx_utils.c
${SPECULOS_SRC}/src/bolos/cx_weierstrass.c
${SPECULOS_SRC}/src/bolos/cxlib.c
+ ${SPECULOS_SRC}/src/bolos/os_bip32.c
)
add_library(speculos_bolos STATIC ${SPECULOS_BOLOS_SOURCES})
@@ -258,7 +259,10 @@ if(SPECULOS AND SPECULOS_SRC)
target_compile_definitions(speculos_bolos PRIVATE
OS_LITTLE_ENDIAN NATIVE_64BITS HAVE_BOLOS=1
)
- target_include_directories(speculos_bolos PRIVATE
+ # BEFORE: speculos ships its own appflags.h (with `extern app_flags`)
+ # that must shadow the Ledger SDK's stripped-down appflags.h added
+ # at global scope above.
+ target_include_directories(speculos_bolos BEFORE PRIVATE
${SPECULOS_SRC}/sdk
${SPECULOS_SRC}/src
${SPECULOS_SRC}/src/bolos
@@ -267,7 +271,8 @@ if(SPECULOS AND SPECULOS_SRC)
target_link_libraries(speculos_bolos PUBLIC OpenSSL::Crypto)
add_library(speculos_bridge STATIC libs/speculos_bridge.c)
- target_include_directories(speculos_bridge PRIVATE
+ target_compile_definitions(speculos_bridge PRIVATE HAVE_BOLOS=1)
+ target_include_directories(speculos_bridge BEFORE PRIVATE
${SPECULOS_SRC}/sdk
${SPECULOS_SRC}/src
${SPECULOS_SRC}/src/bolos
diff --git a/unit-tests/libs/speculos_bridge.c b/unit-tests/libs/speculos_bridge.c
index 470e547..49a7a33 100644
--- a/unit-tests/libs/speculos_bridge.c
+++ b/unit-tests/libs/speculos_bridge.c
@@ -47,12 +47,13 @@ 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) {
+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_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);
@@ -66,20 +67,27 @@ 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_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) {
+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) {
+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) {
+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);
}
@@ -87,8 +95,7 @@ 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) {
+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);
}
@@ -101,8 +108,7 @@ cx_err_t cx_ecpoint_rnd_scalarmul(cx_ecpoint_t *p, const uint8_t *k,
* 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 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;
@@ -115,8 +121,10 @@ end:
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) {
+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;
@@ -125,8 +133,7 @@ cx_err_t cx_ecfp_scalar_mult_no_throw(cx_curve_t curve, uint8_t *P,
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_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);
@@ -135,7 +142,8 @@ end:
return error;
}
-cx_err_t cx_ecfp_add_point_no_throw(cx_curve_t curve, unsigned char *R,
+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;
@@ -148,10 +156,8 @@ cx_err_t cx_ecfp_add_point_no_throw(cx_curve_t curve, unsigned char *R,
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_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);
@@ -161,29 +167,36 @@ end:
}
/* 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,
+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);
+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) {
+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);
+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;
@@ -191,11 +204,13 @@ typedef struct {
} 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]) {
+ 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;
+ return (sys_cx_hash_ripemd160(iovec[0].iov_base, iovec[0].iov_len, digest, 20) == 20)
+ ? 0
+ : 0xFFFFFF85;
}
size_t total = 0;
@@ -218,31 +233,45 @@ cx_err_t cx_ripemd160_hash_iovec(const cx_iovec_t_local *iovec,
* 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); \
+#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;
+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");
+ (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;
+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_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;
@@ -257,24 +286,26 @@ end:
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_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;
+ 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_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;
@@ -290,32 +321,145 @@ end:
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 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");
+/* ------------------------------------------------------------------
+ * BIP32 seed derivation — forwarders to speculos's os_bip32.c.
+ *
+ * Speculos exposes:
+ * sys_os_perso_derive_node_with_seed_key(mode, curve, path, len,
+ * privkey, chain, seed_key, seed_key_len)
+ * sys_os_perso_get_master_key_identifier(identifier, length)
+ * sys_cx_ecfp_init_private_key(curve, raw_key, key_len, privkey)
+ * sys_cx_ecfp_generate_pair(curve, pubkey, privkey, keep_private)
+ *
+ * The seed itself is read by os_bip32.c through env_get_seed(); we
+ * provide a deterministic stub for env_get_seed below.
+ * ------------------------------------------------------------------ */
+
+/* sys_cx_ecfp_init_private_key and sys_cx_ecfp_generate_pair are
+ * already declared by speculos's cx_ec.h, pulled in transitively via
+ * bolos/cxlib.h above. Declare here only what isn't exposed in a
+ * speculos public header. */
+extern unsigned long sys_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);
+extern unsigned long sys_os_perso_get_master_key_identifier(uint8_t *identifier,
+ size_t identifier_length);
+
+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) {
+ return sys_os_perso_derive_node_with_seed_key(mode,
+ curve,
+ path,
+ path_len,
+ privkey,
+ chain,
+ seed_key,
+ seed_key_len);
+}
+
+cx_err_t os_derive_bip32_with_seed_no_throw(unsigned int mode,
+ cx_curve_t curve,
+ const uint32_t *path,
+ size_t path_len,
+ unsigned char *privkey,
+ unsigned char *chain,
+ unsigned char *seed_key,
+ size_t seed_key_len) {
+ sys_os_perso_derive_node_with_seed_key(mode,
+ curve,
+ path,
+ (unsigned int) path_len,
+ privkey,
+ chain,
+ seed_key,
+ (unsigned int) seed_key_len);
+ return 0; /* CX_OK; THROW path is fatal in our test harness. */
+}
+
+unsigned long os_perso_get_master_key_identifier(uint8_t *id, size_t id_len) {
+ return sys_os_perso_get_master_key_identifier(id, id_len);
+}
+
+cx_err_t cx_ecfp_generate_pair_no_throw(cx_curve_t curve,
+ cx_ecfp_public_key_t *pubkey,
+ cx_ecfp_private_key_t *privkey,
+ int keepprivate) {
+ int rc = sys_cx_ecfp_generate_pair(curve, pubkey, privkey, keepprivate);
+ return rc == 0 ? 0 : 0xFFFFFF85;
}
+cx_err_t cx_ecfp_generate_pair2_no_throw(cx_curve_t curve,
+ cx_ecfp_public_key_t *pubkey,
+ cx_ecfp_private_key_t *privkey,
+ int keepprivate,
+ cx_md_t hashID) {
+ /* hashID only affects ed25519 derivation; for secp256k1 it is ignored. */
+ (void) hashID;
+ return cx_ecfp_generate_pair_no_throw(curve, pubkey, privkey, keepprivate);
+}
+
+cx_err_t cx_ecfp_init_private_key_no_throw(cx_curve_t curve,
+ const uint8_t *raw,
+ size_t raw_len,
+ cx_ecfp_private_key_t *privkey) {
+ int rc = sys_cx_ecfp_init_private_key(curve, raw, (unsigned int) raw_len, privkey);
+ return rc >= 0 ? 0 : 0xFFFFFF85;
+}
+
+/* SDK helpers, reimplemented inline. Equivalent to
+ * lib_standard_app/crypto_helpers.c — but rewritten here because that
+ * file pulls in the full SDK header chain (incompatible with the
+ * unit-test mock environment). */
+
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,
+ cx_ecfp_private_key_t *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");
+ uint8_t raw[64] = {0};
+ cx_err_t err = os_derive_bip32_with_seed_no_throw(derivation_mode,
+ curve,
+ path,
+ path_len,
+ raw,
+ chain_code,
+ seed,
+ seed_len);
+ if (err != 0) return err;
+ return cx_ecfp_init_private_key_no_throw(curve, raw, 32, privkey);
}
cx_err_t bip32_derive_with_seed_get_pubkey_256(unsigned int derivation_mode,
@@ -327,27 +471,66 @@ cx_err_t bip32_derive_with_seed_get_pubkey_256(unsigned int derivation_mode,
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");
+ cx_ecfp_private_key_t privkey = {0};
+ cx_ecfp_public_key_t pubkey = {0};
+
+ cx_err_t err = bip32_derive_with_seed_init_privkey_256(derivation_mode,
+ curve,
+ path,
+ path_len,
+ &privkey,
+ chain_code,
+ seed,
+ seed_len);
+ if (err != 0) return err;
+
+ err = cx_ecfp_generate_pair2_no_throw(curve, &pubkey, &privkey, 1, hashID);
+ if (err != 0) return err;
+ if (pubkey.W_len != 65) return 0xFFFFFFA3; /* CX_EC_INVALID_CURVE */
+ memcpy(raw_pubkey, pubkey.W, 65);
+ return 0;
}
-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");
-}
+/* ------------------------------------------------------------------
+ * Speculos environment globals.
+ *
+ * os_bip32.c reads the BIP32 master seed via env_get_seed(), references
+ * the app_flags global, and consults get_app_derivation_path() to check
+ * whether the requested derivation path is allowed. None of those
+ * concepts apply to the host test harness, so we provide trivial
+ * fixtures: a fixed 64-byte test seed, no flag restrictions, no
+ * derivation-path restriction.
+ * ------------------------------------------------------------------ */
-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");
+// NOTE: the app does not have this flag, but allowing any derivation simplifies unit test
+// by allowing the use of any BIP32 test vector.
+/* 0x10 = APPLICATION_FLAG_DERIVE_MASTER — required by speculos's
+ * os_bip32 to allow non-hardened derivation paths from the master. */
+uint64_t app_flags = 0x10u;
+
+unsigned long get_app_derivation_path(uint8_t **derivationPath) {
+ (void) derivationPath;
+ return 0; /* no restriction */
+}
+
+/* The same 64-byte test seed speculos uses when no SPECULOS_SEED env
+ * variable is set. Matches the mnemonic
+ * "glory promote mansion idle axis finger extra february uncover one
+ * trip resource lawn turtle enact monster seven myth punch hobby
+ * comfort wild raise skin".
+ * Tests use this fixed seed to assert against known BIP32 derivations. */
+static const uint8_t speculos_bridge_seed[64] = {
+ 0xb1, 0x19, 0x97, 0xfa, 0xff, 0x42, 0x0a, 0x33, 0x1b, 0xb4, 0xa4, 0xff, 0xdc, 0x8b, 0xdc, 0x8b,
+ 0xa7, 0xc0, 0x17, 0x32, 0xa9, 0x9a, 0x30, 0xd8, 0x3d, 0xbb, 0xeb, 0xd4, 0x69, 0x66, 0x6c, 0x84,
+ 0xb4, 0x7d, 0x09, 0xd3, 0xf5, 0xf4, 0x72, 0xb3, 0xb9, 0x38, 0x4a, 0xc6, 0x34, 0xbe, 0xba, 0x2a,
+ 0x44, 0x0b, 0xa3, 0x6e, 0xc7, 0x66, 0x11, 0x44, 0x13, 0x2f, 0x35, 0xe2, 0x06, 0x87, 0x35, 0x64,
+};
+
+size_t env_get_seed(uint8_t *seed, size_t max_size) {
+ size_t n = sizeof(speculos_bridge_seed);
+ if (n > max_size) n = max_size;
+ memcpy(seed, speculos_bridge_seed, n);
+ return n;
}
/* ------------------------------------------------------------------
@@ -358,20 +541,24 @@ unsigned long os_perso_get_master_key_identifier(uint8_t *id, size_t id_len) {
* 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);
+ 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 *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);
+void __attribute__((noreturn)) assert_exit(bool confirm) {
+ (void) confirm;
+ fprintf(stderr, "assert_exit fired in host test\n");
abort();
}
@@ -387,16 +574,30 @@ unsigned long sys_cx_rng(uint8_t *buffer, unsigned int length) {
* 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;
+ (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;
+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");
}
@@ -404,16 +605,19 @@ int ED25519_verify(const uint8_t *msg, size_t msg_len,
* 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 *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,
+ 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
index e34207b..a1e1a2a 100644
--- a/unit-tests/libs/speculos_bridge.h
+++ b/unit-tests/libs/speculos_bridge.h
@@ -20,6 +20,11 @@
#include <stdint.h>
#include <stddef.h>
+/* Re-export the SDK's cx_err_t error codes (CX_OK, CX_INTERNAL_ERROR,
+ * ...) so tests can use the symbolic names instead of magic numbers
+ * when asserting against return values from speculos-backed code. */
+#include "cx_errors.h"
+
#ifdef __cplusplus
extern "C" {
#endif
diff --git a/unit-tests/test_crypto.c b/unit-tests/test_crypto.c
index c8980a2..3b2c049 100644
--- a/unit-tests/test_crypto.c
+++ b/unit-tests/test_crypto.c
@@ -316,6 +316,101 @@ static void test_crypto_get_checksum_hello(void **state) {
assert_memory_equal(out, expected, 4);
}
+/* ---------------------------------------------------------------- */
+/* crypto_get_compressed_pubkey_at_path */
+/* */
+/* Expected values were precomputed from the BIP32 master seed used */
+/* by speculos when no SPECULOS_SEED override is set (the speculos */
+/* default test seed, mirrored in libs/speculos_bridge.c). */
+/* ---------------------------------------------------------------- */
+
+#define BIP32_HARDENED 0x80000000u
+
+static void test_crypto_get_compressed_pubkey_at_path_master(void **state) {
+ (void) state;
+ /* m — passes a non-NULL but empty path. speculos rejects NULL path
+ * with an exception regardless of length; this matches what real
+ * app callers do. */
+ static const uint32_t path[1] = {0};
+ static const uint8_t expected_pubkey[33] = {
+ 0x02, 0x51, 0xec, 0x84, 0xe3, 0x3a, 0x31, 0x19, 0x48, 0x64, 0x61,
+ 0xa4, 0x42, 0x40, 0xe9, 0x06, 0xff, 0x94, 0xbf, 0x40, 0xcf, 0x80,
+ 0x7b, 0x02, 0x5b, 0x1c, 0xa4, 0x33, 0x32, 0xb8, 0x0d, 0xc9, 0xdb,
+ };
+ static const uint8_t expected_chain[32] = {
+ 0xeb, 0x47, 0x3a, 0x0f, 0xa0, 0xaf, 0x50, 0x31, 0xf1, 0x4d, 0xb9,
+ 0xfe, 0x7c, 0x37, 0xbb, 0x84, 0x16, 0xa4, 0xff, 0x01, 0xbb, 0x69,
+ 0xda, 0xe9, 0x96, 0x6d, 0xc8, 0x3b, 0x5e, 0x5b, 0xf9, 0x21,
+ };
+ uint8_t pubkey[33];
+ uint8_t chain[32];
+
+ cx_err_t err = crypto_get_compressed_pubkey_at_path(path, 0, pubkey, chain);
+ assert_int_equal(err, CX_OK);
+ assert_memory_equal(pubkey, expected_pubkey, 33);
+ assert_memory_equal(chain, expected_chain, 32);
+}
+
+static void test_crypto_get_compressed_pubkey_at_path_unhardened(void **state) {
+ (void) state;
+ /* m/0 */
+ static const uint32_t path[] = {0};
+ static const uint8_t expected_pubkey[33] = {
+ 0x02, 0x6f, 0x76, 0x0e, 0x57, 0x38, 0x3e, 0x3b, 0x59, 0x00, 0xf7,
+ 0xc2, 0x3b, 0x78, 0xa4, 0x24, 0xe7, 0x4b, 0xeb, 0xbe, 0x9b, 0x7b,
+ 0x46, 0x31, 0x6d, 0xa7, 0xc0, 0xb4, 0xb9, 0xc2, 0xc9, 0x30, 0x1c,
+ };
+ static const uint8_t expected_chain[32] = {
+ 0x7b, 0x45, 0xad, 0x96, 0xc9, 0x4b, 0x30, 0xc4, 0x29, 0x4c, 0x28,
+ 0x04, 0x7f, 0xf3, 0x94, 0x6d, 0x28, 0x42, 0x04, 0x4d, 0xe1, 0x00,
+ 0xd7, 0x0f, 0xc3, 0xa6, 0xad, 0x9c, 0x0e, 0xcc, 0x82, 0x42,
+ };
+ uint8_t pubkey[33];
+ uint8_t chain[32];
+
+ cx_err_t err = crypto_get_compressed_pubkey_at_path(path, 1, pubkey, chain);
+ assert_int_equal(err, CX_OK);
+ assert_memory_equal(pubkey, expected_pubkey, 33);
+ assert_memory_equal(chain, expected_chain, 32);
+}
+
+static void test_crypto_get_compressed_pubkey_at_path_bip44_account(void **state) {
+ (void) state;
+ /* m/44'/0'/0' — BIP44 Bitcoin mainnet account 0 */
+ static const uint32_t path[] = {BIP32_HARDENED | 44, BIP32_HARDENED | 0, BIP32_HARDENED | 0};
+ static const uint8_t expected_pubkey[33] = {
+ 0x03, 0x85, 0xe8, 0xc0, 0xcf, 0x6e, 0xed, 0x65, 0x2d, 0xc1, 0x23,
+ 0x85, 0x8b, 0xc1, 0x5a, 0xd3, 0x03, 0x83, 0xef, 0x08, 0x80, 0x66,
+ 0x22, 0xcb, 0x2e, 0x06, 0x2d, 0x32, 0xad, 0xca, 0x36, 0x08, 0x56,
+ };
+ uint8_t pubkey[33];
+
+ cx_err_t err = crypto_get_compressed_pubkey_at_path(path, 3, pubkey, NULL);
+ assert_int_equal(err, CX_OK);
+ assert_memory_equal(pubkey, expected_pubkey, 33);
+}
+
+static void test_crypto_get_compressed_pubkey_at_path_bip44_first_address(void **state) {
+ (void) state;
+ /* m/44'/0'/0'/0/0 — first receive address of the first BIP44 account.
+ * Also exercises chain_code == NULL. */
+ static const uint32_t path[] = {BIP32_HARDENED | 44,
+ BIP32_HARDENED | 0,
+ BIP32_HARDENED | 0,
+ 0,
+ 0};
+ static const uint8_t expected_pubkey[33] = {
+ 0x03, 0x42, 0x51, 0x35, 0x8d, 0xa2, 0x06, 0xb3, 0xd5, 0x49, 0xf5,
+ 0xc5, 0x38, 0xa7, 0xbc, 0x46, 0x64, 0x27, 0xee, 0x81, 0x09, 0x9a,
+ 0xa8, 0xb4, 0x0d, 0xd0, 0xf5, 0xd4, 0xbf, 0xac, 0x5f, 0x5f, 0x15,
+ };
+ uint8_t pubkey[33];
+
+ cx_err_t err = crypto_get_compressed_pubkey_at_path(path, 5, pubkey, NULL);
+ assert_int_equal(err, CX_OK);
+ assert_memory_equal(pubkey, expected_pubkey, 33);
+}
+
int main(void) {
speculos_bridge_init();
const struct CMUnitTest tests[] = {
@@ -335,6 +430,10 @@ int main(void) {
cmocka_unit_test(test_crypto_get_compressed_pubkey_invalid),
cmocka_unit_test(test_crypto_get_checksum_empty),
cmocka_unit_test(test_crypto_get_checksum_hello),
+ cmocka_unit_test(test_crypto_get_compressed_pubkey_at_path_master),
+ cmocka_unit_test(test_crypto_get_compressed_pubkey_at_path_unhardened),
+ cmocka_unit_test(test_crypto_get_compressed_pubkey_at_path_bip44_account),
+ cmocka_unit_test(test_crypto_get_compressed_pubkey_at_path_bip44_first_address),
};
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.