Add unit tests for crypto_ecdsa_sign_sha256_hash_with_key
What changed, and why it matters
This commit only adds unit tests and a test-only bridge implementation for cryptographic signing functions. It does not change the actual device application code that users rely on, so it cannot introduce a security vulnerability in the shipped product. The changes make the test suite more complete by exercising an existing signing function with a known, precomputed signature.
No security action required. Review the test-only bridge for correctness if desired, but the change is not security-relevant.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff modifies two files under unit-tests/. In speculos_bridge.c, it replaces a STUB_ABORT placeholder for cx_ecdsa_sign_no_throw with a real bridge to sys_cx_ecdsa_sign, and adds a cx_ecdsa_verify_no_throw bridge. In test_crypto.c, it adds two cmocka tests for crypto_ecdsa_sign_sha256_hash_with_key using a hardcoded path and SHA256(“hello”) known vector. No production firmware code is altered.
Changed components
unit-tests/libs/speculos_bridge.cunit-tests/test_crypto.cInspect captured patch +107 / −11
diff --git a/unit-tests/libs/speculos_bridge.c b/unit-tests/libs/speculos_bridge.c
index 49a7a33..0110bed 100644
--- a/unit-tests/libs/speculos_bridge.c
+++ b/unit-tests/libs/speculos_bridge.c
@@ -321,23 +321,47 @@ end:
return error;
}
-cx_err_t cx_ecdsa_sign_no_throw(const void *pvkey,
+/* sys_cx_ecdsa_sign / sys_cx_ecdsa_verify are declared by speculos's
+ * cx_ec.h, pulled in transitively via bolos/cxlib.h above. They use
+ * unsigned int for the signature length (speculos returns the written
+ * length via the function's return value), while the SDK's _no_throw
+ * wrappers use size_t* in-out. Bridge the calling convention here. */
+
+cx_err_t cx_ecdsa_sign_no_throw(const cx_ecfp_private_key_t *pvkey,
uint32_t mode,
- uint32_t hashID,
+ cx_md_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");
+ unsigned int info_local = 0;
+ int n = sys_cx_ecdsa_sign(pvkey,
+ (int) mode,
+ hashID,
+ hash,
+ (unsigned int) hash_len,
+ sig,
+ (unsigned int) *sig_len,
+ &info_local);
+ if (n < 0) return 0xFFFFFF85; /* CX_INTERNAL_ERROR */
+ *sig_len = (size_t) n;
+ if (info != NULL) *info = info_local;
+ return 0; /* CX_OK */
+}
+
+bool cx_ecdsa_verify_no_throw(const cx_ecfp_public_key_t *pukey,
+ const uint8_t *hash,
+ size_t hash_len,
+ const uint8_t *sig,
+ size_t sig_len) {
+ return sys_cx_ecdsa_verify(pukey,
+ 0,
+ CX_SHA256,
+ hash,
+ (unsigned int) hash_len,
+ sig,
+ (unsigned int) sig_len) == 1;
}
/* ------------------------------------------------------------------
diff --git a/unit-tests/test_crypto.c b/unit-tests/test_crypto.c
index d6a4e0c..93b5573 100644
--- a/unit-tests/test_crypto.c
+++ b/unit-tests/test_crypto.c
@@ -516,6 +516,76 @@ static void test_base58_encode_address_four_byte_version(void **state) {
assert_memory_equal(out, expected, sizeof(expected) - 1);
}
+/* ---------------------------------------------------------------- */
+/* crypto_ecdsa_sign_sha256_hash_with_key */
+/* */
+/* Signs SHA256("hello") with the private key derived at */
+/* m/44'/0'/0'/0/0 against the speculos default seed. ECDSA over */
+/* secp256k1 with RFC6979 is deterministic, so the expected DER */
+/* signature is precomputed and asserted byte-for-byte. */
+/* ---------------------------------------------------------------- */
+
+static void test_crypto_ecdsa_sign_sha256_hash_with_key_known_vector(void **state) {
+ (void) state;
+ /* SHA256("hello"). */
+ static const uint8_t hash[32] = {
+ 0x2c, 0xf2, 0x4d, 0xba, 0x5f, 0xb0, 0xa3, 0x0e, 0x26, 0xe8,
+ 0x3b, 0x2a, 0xc5, 0xb9, 0xe2, 0x9e, 0x1b, 0x16, 0x1e, 0x5c,
+ 0x1f, 0xa7, 0x42, 0x5e, 0x73, 0x04, 0x33, 0x62, 0x93, 0x8b,
+ 0x98, 0x24,
+ };
+ static const uint32_t path[] = {
+ BIP32_HARDENED | 44, BIP32_HARDENED | 0, BIP32_HARDENED | 0, 0, 0};
+
+ /* Expected DER signature: the deterministic RFC6979 output the
+ * device produces for (privkey @ m/44'/0'/0'/0/0, SHA256("hello")).
+ * Captured directly from speculos and pinned here to catch any
+ * future regression in the sign code path. */
+ static const uint8_t expected_sig[] = {
+ 0x30, 0x45, 0x02, 0x21, 0x00, 0xc4, 0x44, 0xd1, 0xf2, 0x19, 0x1c, 0xa8,
+ 0x71, 0x39, 0xf7, 0x8f, 0x4f, 0x24, 0x1e, 0x42, 0x75, 0x96, 0xd0, 0x03,
+ 0x17, 0xd1, 0x2a, 0x49, 0xf7, 0xcb, 0x38, 0xb1, 0xd6, 0x13, 0xcd, 0x65,
+ 0x67, 0x02, 0x20, 0x1c, 0x51, 0x5c, 0x5e, 0x78, 0x84, 0x67, 0x31, 0x93,
+ 0x18, 0xaa, 0x33, 0xe0, 0x2c, 0x0d, 0x97, 0x4e, 0x33, 0x06, 0x47, 0x10,
+ 0x55, 0xc8, 0xbc, 0x56, 0xc4, 0x0a, 0x00, 0x03, 0x4b, 0x0a, 0x8c,
+ };
+ /* The compressed pubkey at the same path (also tested separately by
+ * test_crypto_get_compressed_pubkey_at_path_bip44_first_address). */
+ 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 sig[MAX_DER_SIG_LEN];
+ uint8_t pubkey[33];
+ uint32_t info = 0;
+
+ int sig_len = crypto_ecdsa_sign_sha256_hash_with_key(
+ path, sizeof(path) / sizeof(*path), hash, pubkey, sig, &info);
+ assert_int_equal(sig_len, (int) sizeof(expected_sig));
+ assert_memory_equal(sig, expected_sig, sizeof(expected_sig));
+ assert_memory_equal(pubkey, expected_pubkey, 33);
+}
+
+static void test_crypto_ecdsa_sign_sha256_hash_with_key_no_optional_outputs(
+ void **state) {
+ (void) state;
+ /* pubkey and info are documented as optional. */
+ static const uint8_t hash[32] = {0};
+ static const uint32_t path[] = {BIP32_HARDENED | 44,
+ BIP32_HARDENED | 0,
+ BIP32_HARDENED | 0,
+ 0,
+ 0};
+
+ uint8_t sig[MAX_DER_SIG_LEN];
+ int sig_len = crypto_ecdsa_sign_sha256_hash_with_key(
+ path, sizeof(path) / sizeof(*path), hash, NULL, sig, NULL);
+ assert_true(sig_len > 0);
+ assert_true(sig_len <= MAX_DER_SIG_LEN);
+}
+
int main(void) {
speculos_bridge_init();
const struct CMUnitTest tests[] = {
@@ -550,6 +620,8 @@ int main(void) {
cmocka_unit_test(test_base58_encode_address_p2pkh_testnet),
cmocka_unit_test(test_base58_encode_address_two_byte_version),
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),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
Why this scored 12/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.