From 42c6eaa7768997b5cff400c5b21dc56784bd2366 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Wed, 15 Jul 2026 14:23:10 -0700 Subject: [PATCH] fix: bound ECDSA r/s before copy in PSoC6 verify psoc6_ecc_verify_hash_ex copied mp_unsigned_bin_size(r/s) bytes of attacker-controlled signature into the fixed signature_buf keyed only on keySz, with no upper bound on r/s. The generic path guards this via wc_ecc_check_r_s_range(), which is compiled out for WOLFSSL_PSOC6_CRYPTO and skipped by the early return in wc_ecc_verify_hash_ex. Reject rSz/sSz > keySz before the copies, preventing a stack buffer overflow. Also correct three inverted error returns in the same function: 'return -BAD_FUNC_ARG' returned +173 (BAD_FUNC_ARG is already -173), violating the negative-error contract; two predate this change. Add a path-agnostic negative test (oversized r/s of keySz+1 bytes) to test_wc_EccDecisionCoverage2: exercises wc_ecc_check_r_s_range on software builds and the new guard under WOLFSSL_PSOC6_CRYPTO on-device. Found by Fenrir (finding 6778). --- tests/api/test_ecc.c | 25 +++++++++++++++++++++++ wolfcrypt/src/port/cypress/psoc6_crypto.c | 12 +++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/api/test_ecc.c b/tests/api/test_ecc.c index 732c0df513d..aff8544c430 100644 --- a/tests/api/test_ecc.c +++ b/tests/api/test_ecc.c @@ -2385,6 +2385,31 @@ int test_wc_EccDecisionCoverage2(void) mp_clear(&s); mp_clear(&bigVal); } + + /* r/s with MORE bytes than the curve size (keySz + 1). This is the + * byte-length overshoot that overflows the fixed signature_buf on hardware + * ports; the software path rejects it in wc_ecc_check_r_s_range() (value >= + * order) and the PSoC6 port rejects it via the rSz/sSz > keySz guard. + * Assert path-agnostically: a failure return with verify == 0. */ + if (key.dp != NULL) + { + mp_int r, s; + int verify = 0; + byte oversized[MAX_ECC_BYTES + 1]; + word32 osSz = (word32)key.dp->size + 1; + byte digest[] = TEST_STRING; + + XMEMSET(oversized, 0xFF, sizeof(oversized)); + ExpectIntEQ(mp_init(&r), MP_OKAY); + ExpectIntEQ(mp_init(&s), MP_OKAY); + ExpectIntEQ(mp_read_unsigned_bin(&r, oversized, osSz), MP_OKAY); + ExpectIntEQ(mp_read_unsigned_bin(&s, oversized, osSz), MP_OKAY); + ExpectIntNE(ret = wc_ecc_verify_hash_ex(&r, &s, digest, + (word32)TEST_STRING_SZ, &verify, &key), 0); + ExpectIntEQ(verify, 0); + mp_clear(&r); + mp_clear(&s); + } #endif /* ---- wc_ecc_import_point_der_ex / wc_ecc_export_point_der{,_compressed}: diff --git a/wolfcrypt/src/port/cypress/psoc6_crypto.c b/wolfcrypt/src/port/cypress/psoc6_crypto.c index 655e4c2be4e..65ce5f8d5fb 100644 --- a/wolfcrypt/src/port/cypress/psoc6_crypto.c +++ b/wolfcrypt/src/port/cypress/psoc6_crypto.c @@ -2093,7 +2093,7 @@ int psoc6_ecc_verify_hash_ex(MATH_INT_T* r, MATH_INT_T* s, const byte* hash, uint8_t k[MAX_ECC_KEYSIZE] = { 0 }; if (!key || !verif_res || !r || !s || !hash) - return -BAD_FUNC_ARG; + return BAD_FUNC_ARG; /* Enable CRYPTO block if not enabled */ if (!Cy_Crypto_Core_IsEnabled(crypto_base)) { @@ -2105,7 +2105,15 @@ int psoc6_ecc_verify_hash_ex(MATH_INT_T* r, MATH_INT_T* s, const byte* hash, sSz = mp_unsigned_bin_size(s); if (keySz > MAX_ECC_KEYSIZE) - return -BAD_FUNC_ARG; + return BAD_FUNC_ARG; + + /* r and s come from the (untrusted) signature. A valid ECDSA r,s is in + * [1, order-1], so each fits in keySz bytes. Reject anything larger before + * it is copied into the fixed-size signature_buf, which would otherwise + * overflow. The software path enforces this via wc_ecc_check_r_s_range(), + * which is compiled out for the PSoC6 port. */ + if (rSz > keySz || sSz > keySz) + return BAD_FUNC_ARG; /* Prepare ECC key */ ecc_key.type = PK_PUBLIC;