Unit tests for the crypto_tr_* taproot helpers
What changed, and why it matters
This commit only adds unit tests and test infrastructure for Bitcoin Taproot cryptographic helpers. It does not change the actual app code that runs on the hardware wallet, so it cannot introduce a security vulnerability in shipped firmware. The changes make the test mock more complete by wiring it to the Speculos emulator's real cryptographic functions instead of crashing with a stub.
No security action required. Review the test vectors and mock bridge wiring for correctness as part of normal QA, but this commit is not a security patch or vulnerability introduction.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff is limited to unit-test files. It replaces stub abort implementations of cx_hash_no_throw, cx_sha256_init_no_throw, and cx_sha256_hash_iovec in unit-tests/libs/speculos_bridge.c with forwarders to Speculos’s sys_cx_hash/cx_sha256_init. It then adds 12 cmocka tests in unit-tests/test_crypto.c exercising crypto_tr_* helpers (tagged hashes, lift_x, taptree combination, pubkey/seckey tweaking) against known test vectors. No production source files are modified.
Changed components
unit-tests/libs/speculos_bridge.cunit-tests/test_crypto.cInspect captured patch +310 / −16
diff --git a/unit-tests/libs/speculos_bridge.c b/unit-tests/libs/speculos_bridge.c
index 0110bed..3112f03 100644
--- a/unit-tests/libs/speculos_bridge.c
+++ b/unit-tests/libs/speculos_bridge.c
@@ -240,31 +240,50 @@ cx_err_t cx_ripemd160_hash_iovec(const cx_iovec_t_local *iovec,
name); \
abort()
-cx_err_t cx_hash_no_throw(void *hash,
+extern unsigned long sys_cx_hash(cx_hash_t *hash,
+ int mode,
+ const uint8_t *in,
+ size_t len,
+ uint8_t *out,
+ size_t out_len);
+
+cx_err_t cx_hash_no_throw(cx_hash_t *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");
+ /* speculos's sys_cx_hash returns digest_len on CX_LAST success and 0
+ * otherwise; the SDK function returns CX_OK on success regardless.
+ * speculos THROWs on any error (mapped to abort by the bridge), so
+ * if we get here at all, treat it as success. */
+ (void) sys_cx_hash(hash, mode, in, len, out, out_len);
+ return 0; /* CX_OK */
}
-cx_err_t cx_sha256_init_no_throw(void *hash) {
- (void) hash;
- STUB_ABORT("cx_sha256_init_no_throw");
+/* cx_sha256_init is exposed directly by speculos (no sys_ prefix). */
+extern int cx_sha256_init(cx_sha256_t *hash);
+
+cx_err_t cx_sha256_init_no_throw(cx_sha256_t *hash) {
+ /* Per the SDK header, this function always returns CX_OK. */
+ cx_sha256_init(hash);
+ return 0;
}
-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");
+/* Reuses cx_iovec_t_local from the RIPEMD-160 forwarder above; same
+ * layout as the SDK / mock cx_iovec_t. */
+cx_err_t cx_sha256_hash_iovec(const cx_iovec_t_local *iovec, size_t iovec_count,
+ uint8_t *out) {
+ cx_sha256_t ctx;
+ cx_sha256_init(&ctx);
+ for (size_t i = 0; i < iovec_count; i++) {
+ if (iovec[i].iov_len > 0) {
+ sys_cx_hash(&ctx.header, 0, iovec[i].iov_base, iovec[i].iov_len,
+ NULL, 0);
+ }
+ }
+ sys_cx_hash(&ctx.header, CX_LAST, NULL, 0, out, 32);
+ return 0;
}
cx_err_t cx_math_addm_no_throw(uint8_t *r,
diff --git a/unit-tests/test_crypto.c b/unit-tests/test_crypto.c
index 93b5573..54dfc7c 100644
--- a/unit-tests/test_crypto.c
+++ b/unit-tests/test_crypto.c
@@ -586,6 +586,269 @@ static void test_crypto_ecdsa_sign_sha256_hash_with_key_no_optional_outputs(
assert_true(sig_len <= MAX_DER_SIG_LEN);
}
+/* ---------------------------------------------------------------- */
+/* crypto_tr_* (BIP-340/BIP-341 helpers) */
+/* */
+/* Test vectors verified against test_utils/taproot.py. */
+/* */
+/* The streaming-init helpers take a cx_sha256_t*. The mock_includes */
+/* layout of that struct is a few bytes smaller than the real SDK */
+/* one used by app_crypto, so the tests allocate an oversized */
+/* aligned buffer and cast through it. */
+/* ---------------------------------------------------------------- */
+
+#define CX_SHA256_T_STORAGE_BYTES 128
+
+static void test_crypto_tr_tagged_hash_init_custom_tag(void **state) {
+ (void) state;
+ /* tagged_hash("TestTag", "hello" || "world") */
+ static const uint8_t tag[] = "TestTag";
+ static const uint8_t expected[32] = {
+ 0x81, 0x13, 0xad, 0xac, 0x16, 0x04, 0xa6, 0xd0, 0x74, 0x0c,
+ 0x15, 0x9f, 0xc4, 0xcf, 0x2b, 0xbe, 0x54, 0xc8, 0x10, 0x34,
+ 0x19, 0x35, 0x10, 0x54, 0xbe, 0x4e, 0x77, 0xe4, 0xaf, 0x80,
+ 0x20, 0xd9,
+ };
+
+ uint8_t ctx_storage[CX_SHA256_T_STORAGE_BYTES] __attribute__((aligned(8)));
+ cx_sha256_t *ctx = (cx_sha256_t *) ctx_storage;
+ crypto_tr_tagged_hash_init(ctx, tag, sizeof(tag) - 1);
+ assert_int_equal(crypto_hash_update(&ctx->header, "hello", 5), 0);
+ assert_int_equal(crypto_hash_update(&ctx->header, "world", 5), 0);
+ uint8_t out[32];
+ assert_int_equal(crypto_hash_digest(&ctx->header, out, sizeof(out)), 0);
+ assert_memory_equal(out, expected, 32);
+}
+
+static void test_crypto_tr_tapleaf_hash_init(void **state) {
+ (void) state;
+ /* tagged_hash("TapLeaf", "abc") */
+ static const uint8_t expected[32] = {
+ 0x83, 0xa5, 0x63, 0x08, 0xa9, 0xc5, 0x6f, 0x46, 0x7e, 0x8d,
+ 0xf2, 0x93, 0xda, 0x5a, 0xe5, 0xfd, 0xbc, 0x85, 0xb8, 0x71,
+ 0x95, 0x2a, 0x83, 0xc4, 0xbf, 0x05, 0x75, 0xee, 0x94, 0x8e,
+ 0xc2, 0x30,
+ };
+
+ uint8_t ctx_storage[CX_SHA256_T_STORAGE_BYTES] __attribute__((aligned(8)));
+ cx_sha256_t *ctx = (cx_sha256_t *) ctx_storage;
+ crypto_tr_tapleaf_hash_init(ctx);
+ assert_int_equal(crypto_hash_update(&ctx->header, "abc", 3), 0);
+ uint8_t out[32];
+ assert_int_equal(crypto_hash_digest(&ctx->header, out, sizeof(out)), 0);
+ assert_memory_equal(out, expected, 32);
+}
+
+static void test_crypto_tr_lift_x_secp256k1_generator(void **state) {
+ (void) state;
+ /* lift_x of the secp256k1 generator's x-only coordinate produces
+ * the (even-y) generator point itself, in uncompressed form. */
+ static const uint8_t Gx[32] = {
+ 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62,
+ 0x95, 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce,
+ 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98,
+ };
+ static const uint8_t expected[65] = {
+ 0x04,
+ 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95,
+ 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9,
+ 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98,
+ 0x48, 0x3a, 0xda, 0x77, 0x26, 0xa3, 0xc4, 0x65, 0x5d, 0xa4, 0xfb, 0xfc,
+ 0x0e, 0x11, 0x08, 0xa8, 0xfd, 0x17, 0xb4, 0x48, 0xa6, 0x85, 0x54, 0x19,
+ 0x9c, 0x47, 0xd0, 0x8f, 0xfb, 0x10, 0xd4, 0xb8,
+ };
+ uint8_t out[65];
+ assert_int_equal(crypto_tr_lift_x(Gx, out), 0);
+ assert_memory_equal(out, expected, 65);
+}
+
+static void test_crypto_tr_lift_x_not_on_curve(void **state) {
+ (void) state;
+ /* x = 5: with secp256k1's b=7, y^2 = 5^3 + 7 = 132 is not a
+ * quadratic residue mod p, so no point with this x exists. */
+ static const uint8_t x_invalid[32] = {0};
+ uint8_t x_with_5[32] = {0};
+ x_with_5[31] = 5;
+ uint8_t out[65];
+ assert_int_equal(crypto_tr_lift_x(x_with_5, out), -1);
+ /* Sanity: zero-init not_on_curve too */
+ (void) x_invalid;
+}
+
+static void test_crypto_tr_tagged_hash_single_data(void **state) {
+ (void) state;
+ /* tagged_hash("TapTweak", G_x) */
+ static const uint8_t tag[] = "TapTweak";
+ static const uint8_t data[32] = {
+ 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62,
+ 0x95, 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce,
+ 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98,
+ };
+ static const uint8_t expected[32] = {
+ 0x3c, 0xf5, 0x21, 0x6d, 0x47, 0x6a, 0x5e, 0x63, 0x7b, 0xf0,
+ 0xda, 0x67, 0x4e, 0x50, 0xdd, 0xf5, 0x5c, 0x40, 0x32, 0x70,
+ 0xdd, 0x36, 0x49, 0x4d, 0xfc, 0xca, 0x43, 0x81, 0x32, 0xfa,
+ 0x30, 0xe7,
+ };
+ uint8_t out[32];
+ crypto_tr_tagged_hash(tag, sizeof(tag) - 1, data, sizeof(data), NULL, 0,
+ out);
+ assert_memory_equal(out, expected, 32);
+}
+
+static void test_crypto_tr_tagged_hash_two_parts(void **state) {
+ (void) state;
+ /* tagged_hash("TestTag", "hello" || "world") — same expected as the
+ * streaming variant above; the two ways of feeding data must agree. */
+ static const uint8_t tag[] = "TestTag";
+ static const uint8_t expected[32] = {
+ 0x81, 0x13, 0xad, 0xac, 0x16, 0x04, 0xa6, 0xd0, 0x74, 0x0c,
+ 0x15, 0x9f, 0xc4, 0xcf, 0x2b, 0xbe, 0x54, 0xc8, 0x10, 0x34,
+ 0x19, 0x35, 0x10, 0x54, 0xbe, 0x4e, 0x77, 0xe4, 0xaf, 0x80,
+ 0x20, 0xd9,
+ };
+ uint8_t out[32];
+ crypto_tr_tagged_hash(tag, sizeof(tag) - 1,
+ (const uint8_t *) "hello", 5,
+ (const uint8_t *) "world", 5, out);
+ assert_memory_equal(out, expected, 32);
+}
+
+static void test_crypto_tr_combine_taptree_hashes_sorted(void **state) {
+ (void) state;
+ static const uint8_t left[32] = {
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ };
+ static const uint8_t right[32] = {
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ };
+ static const uint8_t expected[32] = {
+ 0x9e, 0xbc, 0xd2, 0x4c, 0xc8, 0xce, 0x07, 0x23, 0x30, 0x06,
+ 0x0f, 0xa7, 0x64, 0xfc, 0x18, 0xf5, 0x17, 0xb3, 0x85, 0x6b,
+ 0x26, 0x41, 0x15, 0x73, 0x9c, 0xb2, 0xbb, 0xf3, 0xa0, 0x55,
+ 0x14, 0x2b,
+ };
+ uint8_t out[32];
+ crypto_tr_combine_taptree_hashes(left, right, out);
+ assert_memory_equal(out, expected, 32);
+}
+
+static void test_crypto_tr_combine_taptree_hashes_reversed(void **state) {
+ (void) state;
+ /* The function must sort by lexicographic order, so swapping left
+ * and right should yield the same combined hash. */
+ static const uint8_t a[32] = {
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ };
+ static const uint8_t b[32] = {
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ };
+ uint8_t out_ab[32], out_ba[32];
+ crypto_tr_combine_taptree_hashes(a, b, out_ab);
+ crypto_tr_combine_taptree_hashes(b, a, out_ba);
+ assert_memory_equal(out_ab, out_ba, 32);
+}
+
+static void test_crypto_tr_tweak_pubkey_empty_h(void **state) {
+ (void) state;
+ /* taproot_tweak_pubkey(G_x, "") */
+ static const uint8_t pubkey[32] = {
+ 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62,
+ 0x95, 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce,
+ 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98,
+ };
+ static const uint8_t expected[32] = {
+ 0xda, 0x47, 0x10, 0x96, 0x4f, 0x78, 0x52, 0x69, 0x5d, 0xe2,
+ 0xda, 0x02, 0x52, 0x90, 0xe2, 0x4a, 0xf6, 0xd8, 0xc2, 0x81,
+ 0xde, 0x5a, 0x0b, 0x90, 0x2b, 0x71, 0x35, 0xfd, 0x9f, 0xd7,
+ 0x4d, 0x21,
+ };
+ uint8_t out[32];
+ uint8_t y_parity = 0;
+ assert_int_equal(crypto_tr_tweak_pubkey(pubkey, NULL, 0, &y_parity, out), 0);
+ assert_int_equal(y_parity, 1);
+ assert_memory_equal(out, expected, 32);
+}
+
+static void test_crypto_tr_tweak_pubkey_with_h(void **state) {
+ (void) state;
+ /* taproot_tweak_pubkey(G_x, h) for a non-empty 32-byte h */
+ static const uint8_t pubkey[32] = {
+ 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62,
+ 0x95, 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce,
+ 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98,
+ };
+ static const uint8_t h[32] = {
+ 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23,
+ 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67,
+ 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
+ 0xcd, 0xef,
+ };
+ static const uint8_t expected[32] = {
+ 0x5a, 0x10, 0x17, 0x2d, 0x23, 0x92, 0xd8, 0xe0, 0x2f, 0x3f,
+ 0x67, 0x1e, 0x13, 0x51, 0x13, 0x34, 0x0d, 0x17, 0xff, 0x17,
+ 0x4f, 0x65, 0x0b, 0x57, 0xd7, 0x68, 0xc1, 0xc6, 0x5e, 0x4d,
+ 0xe9, 0xe4,
+ };
+ uint8_t out[32];
+ uint8_t y_parity = 0;
+ assert_int_equal(crypto_tr_tweak_pubkey(pubkey, h, sizeof(h), &y_parity, out),
+ 0);
+ assert_int_equal(y_parity, 1);
+ assert_memory_equal(out, expected, 32);
+}
+
+static void test_crypto_tr_tweak_seckey_empty_h(void **state) {
+ (void) state;
+ /* taproot_tweak_seckey(seckey=1, ""). Since pubkey(1) = G has even y,
+ * the tweaked seckey is t = tagged_hash("TapTweak", G_x). */
+ static const uint8_t seckey_1[32] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ };
+ static const uint8_t expected[32] = {
+ 0x3c, 0xf5, 0x21, 0x6d, 0x47, 0x6a, 0x5e, 0x63, 0x7b, 0xf0,
+ 0xda, 0x67, 0x4e, 0x50, 0xdd, 0xf5, 0x5c, 0x40, 0x32, 0x70,
+ 0xdd, 0x36, 0x49, 0x4d, 0xfc, 0xca, 0x43, 0x81, 0x32, 0xfa,
+ 0x30, 0xe8,
+ };
+ uint8_t out[32];
+ assert_int_equal(crypto_tr_tweak_seckey(seckey_1, NULL, 0, out), 0);
+ assert_memory_equal(out, expected, 32);
+}
+
+static void test_crypto_tr_tweak_seckey_with_h(void **state) {
+ (void) state;
+ /* taproot_tweak_seckey(seckey=3, h) */
+ static const uint8_t seckey_3[32] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3,
+ };
+ static const uint8_t h[32] = {
+ 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23,
+ 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67,
+ 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
+ 0xcd, 0xef,
+ };
+ static const uint8_t expected[32] = {
+ 0x34, 0xbb, 0x44, 0x56, 0xbb, 0x5d, 0xc8, 0xd1, 0x7e, 0x49,
+ 0xca, 0xf2, 0x97, 0x20, 0x04, 0x09, 0x91, 0x5c, 0x33, 0xf7,
+ 0x04, 0x04, 0x0a, 0x4c, 0x43, 0x9d, 0x41, 0xe3, 0x10, 0xe2,
+ 0x1b, 0xc0,
+ };
+ uint8_t out[32];
+ assert_int_equal(crypto_tr_tweak_seckey(seckey_3, h, sizeof(h), out), 0);
+ assert_memory_equal(out, expected, 32);
+}
+
int main(void) {
speculos_bridge_init();
const struct CMUnitTest tests[] = {
@@ -622,6 +885,18 @@ int main(void) {
cmocka_unit_test(test_base58_encode_address_four_byte_version),
cmocka_unit_test(test_crypto_ecdsa_sign_sha256_hash_with_key_known_vector),
cmocka_unit_test(test_crypto_ecdsa_sign_sha256_hash_with_key_no_optional_outputs),
+ cmocka_unit_test(test_crypto_tr_tagged_hash_init_custom_tag),
+ cmocka_unit_test(test_crypto_tr_tapleaf_hash_init),
+ cmocka_unit_test(test_crypto_tr_lift_x_secp256k1_generator),
+ cmocka_unit_test(test_crypto_tr_lift_x_not_on_curve),
+ cmocka_unit_test(test_crypto_tr_tagged_hash_single_data),
+ cmocka_unit_test(test_crypto_tr_tagged_hash_two_parts),
+ cmocka_unit_test(test_crypto_tr_combine_taptree_hashes_sorted),
+ cmocka_unit_test(test_crypto_tr_combine_taptree_hashes_reversed),
+ cmocka_unit_test(test_crypto_tr_tweak_pubkey_empty_h),
+ cmocka_unit_test(test_crypto_tr_tweak_pubkey_with_h),
+ cmocka_unit_test(test_crypto_tr_tweak_seckey_empty_h),
+ cmocka_unit_test(test_crypto_tr_tweak_seckey_with_h),
};
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.