From d0f5c167da93939163c18a2679aef80b87cac400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 20 Jul 2026 19:19:30 +0200 Subject: [PATCH 01/11] Bound ECDSA r/s size in PSoC6 hardware verify path psoc6_ecc_verify_hash_ex serialized the signature r and s components into a fixed 132-byte stack buffer using mp_to_unsigned_bin without checking their sizes. The values come from attacker-supplied ASN.1 in DecodeECC_DSA_Sig with no magnitude cap beyond sp_int capacity, so an oversized r or s wrote past signature_buf, a pre-authentication stack overflow reachable during TLS signature verification. The generic path guards this with wc_ecc_check_r_s_range, but that check is compiled out on WOLFSSL_PSOC6_CRYPTO builds and the port function performed no r/s validation of its own. Reject any r or s whose serialized size exceeds the key size before writing into the buffer. Fixes F-6778. --- wolfcrypt/src/port/cypress/psoc6_crypto.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/wolfcrypt/src/port/cypress/psoc6_crypto.c b/wolfcrypt/src/port/cypress/psoc6_crypto.c index 655e4c2be4e..39e51de30c3 100644 --- a/wolfcrypt/src/port/cypress/psoc6_crypto.c +++ b/wolfcrypt/src/port/cypress/psoc6_crypto.c @@ -2093,7 +2093,11 @@ 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; + + /* Fail closed on both channels: every early return below leaves the + * caller's result flag in the failed state. */ + *verif_res = 0; /* Enable CRYPTO block if not enabled */ if (!Cy_Crypto_Core_IsEnabled(crypto_base)) { @@ -2105,7 +2109,12 @@ 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; + + /* Reject r or s values that would overflow their keySz slot in + * signature_buf when serialized. */ + if (rSz > keySz || sSz > keySz) + return BAD_FUNC_ARG; /* Prepare ECC key */ ecc_key.type = PK_PUBLIC; From 3b663585ea6899f19c04e7f29c8f662b9469a025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 20 Jul 2026 19:36:41 +0200 Subject: [PATCH 02/11] Reject unset key in wc_Chacha_Process wc_Chacha_Process validated only its pointer arguments and then produced keystream directly from the context state. A zero-initialized ChaCha context, common for static or global storage, that received a nonce via wc_Chacha_SetIV but never had wc_Chacha_SetKey called would encrypt with an all-zero, attacker-predictable key and still return success. This is the same fail-open class already guarded against in wc_Arc4Process. Add a keySet flag to the ChaCha struct, set it in wc_Chacha_SetKey, and return MISSING_KEY from wc_Chacha_Process when the key was never set. Fixes F-6893. --- doc/dox_comments/header_files-ja/chacha.h | 1 + doc/dox_comments/header_files/chacha.h | 2 ++ tests/api/test_chacha.c | 25 +++++++++++++++++++++++ tests/api/test_chacha.h | 4 +++- wolfcrypt/src/chacha.c | 4 ++++ wolfssl/wolfcrypt/chacha.h | 1 + 6 files changed, 36 insertions(+), 1 deletion(-) diff --git a/doc/dox_comments/header_files-ja/chacha.h b/doc/dox_comments/header_files-ja/chacha.h index 968827018d7..d1e587472ab 100644 --- a/doc/dox_comments/header_files-ja/chacha.h +++ b/doc/dox_comments/header_files-ja/chacha.h @@ -33,6 +33,7 @@ int wc_Chacha_SetIV(ChaCha* ctx, const byte* inIv, word32 counter); \return 0 入力の暗号化または復号に成功した場合に返されます \return BAD_FUNC_ARG ctx入力引数の処理中にエラーが発生した場合に返されます + \return MISSING_KEY 処理前にwc_Chacha_SetKeyでキーが設定されていない場合に返されます \param ctx ivを設定するChaCha構造体へのポインタ \param output 出力暗号文または復号された平文を格納するバッファへのポインタ diff --git a/doc/dox_comments/header_files/chacha.h b/doc/dox_comments/header_files/chacha.h index 890b8270208..499c7ffd5cc 100644 --- a/doc/dox_comments/header_files/chacha.h +++ b/doc/dox_comments/header_files/chacha.h @@ -41,6 +41,8 @@ int wc_Chacha_SetIV(ChaCha* ctx, const byte* inIv, word32 counter); \return 0 Returned upon successfully encrypting or decrypting the input \return BAD_FUNC_ARG returned if there is an error processing the ctx input argument + \return MISSING_KEY returned if wc_Chacha_SetKey was not called to set a + key before processing \param ctx pointer to the ChaCha structure on which to set the iv \param output pointer to a buffer in which to store the output ciphertext diff --git a/tests/api/test_chacha.c b/tests/api/test_chacha.c index 46c2beba7fd..80a8758c384 100644 --- a/tests/api/test_chacha.c +++ b/tests/api/test_chacha.c @@ -703,3 +703,28 @@ int test_wc_Chacha_XChachaSetKey(void) #endif return EXPECT_RESULT(); } /* END test_wc_Chacha_XChachaSetKey */ + +/* + * A zero-initialized context that only receives a nonce, with the key setup + * omitted, must not silently encrypt with an all-zero key. wc_Chacha_Process + * has to reject the missing key instead of returning success. + */ +int test_wc_Chacha_MissingKey(void) +{ + EXPECT_DECLS; +#ifdef HAVE_CHACHA + ChaCha ctx; + const byte iv[CHACHA_IV_BYTES] = { 0 }; + const byte input[32] = { 0 }; + byte cipher[32]; + + XMEMSET(&ctx, 0, sizeof(ctx)); + XMEMSET(cipher, 0, sizeof(cipher)); + + /* Nonce set, but wc_Chacha_SetKey() deliberately skipped. */ + ExpectIntEQ(wc_Chacha_SetIV(&ctx, iv, 0), 0); + ExpectIntEQ(wc_Chacha_Process(&ctx, cipher, input, sizeof(input)), + WC_NO_ERR_TRACE(MISSING_KEY)); +#endif + return EXPECT_RESULT(); +} /* END test_wc_Chacha_MissingKey */ diff --git a/tests/api/test_chacha.h b/tests/api/test_chacha.h index 57246b79486..7ef8c8fa502 100644 --- a/tests/api/test_chacha.h +++ b/tests/api/test_chacha.h @@ -32,6 +32,7 @@ int test_wc_Chacha_CounterOverflow(void); int test_wc_Chacha_InPlace(void); int test_wc_Chacha_UnalignedBuffers(void); int test_wc_Chacha_XChachaSetKey(void); +int test_wc_Chacha_MissingKey(void); #define TEST_CHACHA_DECLS \ TEST_DECL_GROUP("chacha", test_wc_Chacha_SetKey), \ @@ -41,6 +42,7 @@ int test_wc_Chacha_XChachaSetKey(void); TEST_DECL_GROUP("chacha", test_wc_Chacha_CounterOverflow), \ TEST_DECL_GROUP("chacha", test_wc_Chacha_InPlace), \ TEST_DECL_GROUP("chacha", test_wc_Chacha_UnalignedBuffers), \ - TEST_DECL_GROUP("chacha", test_wc_Chacha_XChachaSetKey) + TEST_DECL_GROUP("chacha", test_wc_Chacha_XChachaSetKey), \ + TEST_DECL_GROUP("chacha", test_wc_Chacha_MissingKey) #endif /* WOLFCRYPT_TEST_CHACHA_H */ diff --git a/wolfcrypt/src/chacha.c b/wolfcrypt/src/chacha.c index db6d7287c96..c4240f19b82 100644 --- a/wolfcrypt/src/chacha.c +++ b/wolfcrypt/src/chacha.c @@ -257,6 +257,7 @@ int wc_Chacha_SetKey(ChaCha* ctx, const byte* key, word32 keySz) #endif ctx->left = 0; /* resets state */ + ctx->keySet = 1; return 0; } @@ -370,6 +371,9 @@ int wc_Chacha_Process(ChaCha* ctx, byte* output, const byte* input, if (ctx == NULL || input == NULL || output == NULL) return BAD_FUNC_ARG; + if (!ctx->keySet) + return MISSING_KEY; + #ifdef USE_INTEL_CHACHA_SPEEDUP /* handle left overs */ if (msglen > 0 && ctx->left > 0) { diff --git a/wolfssl/wolfcrypt/chacha.h b/wolfssl/wolfcrypt/chacha.h index b7b15e7dff5..8528ee925d7 100644 --- a/wolfssl/wolfcrypt/chacha.h +++ b/wolfssl/wolfcrypt/chacha.h @@ -95,6 +95,7 @@ typedef struct ChaCha { #elif defined(USE_RISCV_CHACHA_SPEEDUP) ALIGN8 word32 over[CHACHA_CHUNK_WORDS]; #endif + WC_BITFIELD keySet:1; /* set to 1 once a key is set */ } ChaCha; /** From c508b402ca350474f2cbd86174edbb5d03969e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 20 Jul 2026 20:11:32 +0200 Subject: [PATCH 03/11] Reject identity-point ECDH shared secret wc_ecc_shared_secret_gen_sync ran the scalar multiplication and then copied the x-coordinate to the output without checking whether the result was the point at infinity. Both math backends report success for the identity: ecc_map_ex sets x, y to zero and z to one and returns success, and the single precision generators serialize the identity as an all-zero x-coordinate. Either way a shared secret that computed to infinity was handed back as an all-zero secret with a success code, where SP 800-56Ar3 5.7.1.2 requires an error and stop. Check the mapped point on the software path, and detect the all-zero output after the single precision generators, returning ECC_INF_E in both cases. The scan accumulates over the whole buffer so it does not branch on the secret. A key whose private value is resident in an SE050 carries no software scalar, so the software multiply legitimately yields the identity for it. Skip the check for those keys specifically, rather than for a zero scalar: on a prime-order curve a zero scalar is the one way the identity can arise, so exempting it would disable the check for the case it exists to catch. Fixes F-6770. --- doc/dox_comments/header_files-ja/ecc.h | 1 + doc/dox_comments/header_files/ecc.h | 2 + tests/api/test_ecc.c | 102 +++++++++++++++++++++++++ tests/api/test_ecc.h | 2 + wolfcrypt/src/ecc.c | 36 +++++++++ 5 files changed, 143 insertions(+) diff --git a/doc/dox_comments/header_files-ja/ecc.h b/doc/dox_comments/header_files-ja/ecc.h index 88920a5f7d7..59a745961d8 100644 --- a/doc/dox_comments/header_files-ja/ecc.h +++ b/doc/dox_comments/header_files-ja/ecc.h @@ -232,6 +232,7 @@ void wc_ecc_key_free(ecc_key* key); \return MP_MULMOD_E 共有鍵の計算中にエラーがある場合に返される可能性があります \return MP_TO_E 共有鍵の計算中にエラーがある場合に返される可能性があります \return MP_MEM 共有鍵の計算中にエラーがある場合に返される可能性があります + \return ECC_INF_E 計算された共有秘密鍵が無限遠点である場合に返されます \param private_key ローカル秘密鍵を含むecc_key構造体へのポインタ \param public_key 受信した公開鍵を含むecc_key構造体へのポインタ diff --git a/doc/dox_comments/header_files/ecc.h b/doc/dox_comments/header_files/ecc.h index f8f9f46d0a7..2bc782e3eb9 100644 --- a/doc/dox_comments/header_files/ecc.h +++ b/doc/dox_comments/header_files/ecc.h @@ -316,6 +316,8 @@ void wc_ecc_key_free(ecc_key* key); shared key \return MP_MEM may be returned if there is an error while computing the shared key + \return ECC_INF_E returned when the computed shared secret is the point at + infinity \param private_key pointer to the ecc_key structure containing the local private key diff --git a/tests/api/test_ecc.c b/tests/api/test_ecc.c index b8ed0599fd4..ed6927df4b4 100644 --- a/tests/api/test_ecc.c +++ b/tests/api/test_ecc.c @@ -578,6 +578,108 @@ int test_wc_ecc_shared_secret(void) return EXPECT_RESULT(); } /* END tests_wc_ecc_shared_secret */ +/* ECC_INF_E rejection is not present in the frozen ecc.c of older + * FIPS-certified modules, so restrict to non-FIPS or FIPS v7 and later, and + * skip the CAVP selftest build. The hardware ports listed here either do not + * compile wc_ecc_shared_secret_gen_sync at all or offload agreement to a + * secure element, so the software rejection never runs. */ +#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + !defined(HAVE_SELFTEST) && \ + defined(HAVE_ECC) && defined(HAVE_ECC_DHE) && \ + defined(HAVE_ECC_KEY_IMPORT) && !defined(WC_NO_RNG) && \ + !defined(WOLFSSL_VALIDATE_ECC_IMPORT) && \ + !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A) && \ + !defined(WOLFSSL_MICROCHIP_TA100) && !defined(WOLFSSL_CRYPTOCELL) && \ + !defined(WOLFSSL_SILABS_SE_ACCEL) && !defined(WOLFSSL_SE050) && \ + !defined(WOLFSSL_KCAPI_ECC) && !defined(WOLF_CRYPTO_CB_ONLY_ECC) +#define TEST_ECC_SHARED_SECRET_AT_INFINITY + +/* Agree with a private scalar equal to the curve order n. Every point on these + * curves has order n, so n times the peer point is the identity for any peer + * point, and wc_ecc_shared_secret() must fail instead of handing back an + * all-zero secret. */ +static int ecc_shared_secret_inf_case(const char* qx, const char* qy, + const char* order, const char* curveName) +{ + EXPECT_DECLS; + ecc_key key; + ecc_key pubKey; + WC_RNG rng; + byte out[MAX_ECC_BYTES]; + word32 outlen = (word32)sizeof(out); + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&pubKey, 0, sizeof(pubKey)); + XMEMSET(&rng, 0, sizeof(rng)); + + PRIVATE_KEY_UNLOCK(); + + ExpectIntEQ(wc_ecc_init(&key), 0); + ExpectIntEQ(wc_ecc_init(&pubKey), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + + ExpectIntEQ(wc_ecc_import_raw(&key, qx, qy, order, curveName), 0); + ExpectIntEQ(wc_ecc_import_raw(&pubKey, qx, qy, NULL, curveName), 0); + +#if defined(ECC_TIMING_RESISTANT) && (!defined(HAVE_FIPS) || \ + (!defined(HAVE_FIPS_VERSION) || (HAVE_FIPS_VERSION != 2))) && \ + !defined(HAVE_SELFTEST) + ExpectIntEQ(wc_ecc_set_rng(&key, &rng), 0); +#endif + + ExpectIntEQ(wc_ecc_shared_secret(&key, &pubKey, out, &outlen), + WC_NO_ERR_TRACE(ECC_INF_E)); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_ecc_free(&pubKey); + wc_ecc_free(&key); +#ifdef FP_ECC + wc_ecc_fp_free(); +#endif + PRIVATE_KEY_LOCK(); + + return EXPECT_RESULT(); +} +#endif + +/* + * A shared secret that computes to the point at infinity must be rejected + * (SP 800-56Ar3 5.7.1.2), not returned as an all-zero secret. Both math + * backends are covered because they detect the identity differently: the + * software path checks the mapped point, while the single precision + * generators report success and serialize the identity as an all-zero + * x-coordinate, so it has to be caught from the output. + */ +int test_wc_ecc_shared_secret_at_infinity(void) +{ + EXPECT_DECLS; +#ifdef TEST_ECC_SHARED_SECRET_AT_INFINITY +#if (defined(HAVE_ECC224) || defined(HAVE_ALL_CURVES)) && \ + (ECC_MIN_KEY_SZ <= 224) && !defined(WOLFSSL_SP_MATH) + /* secp224r1 is not single precision accelerated, so this drives the + * software ECDH path even in SP builds that offload P-256. */ + ExpectIntEQ(ecc_shared_secret_inf_case( + "b70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21", + "bd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34", + "ffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d", + "SECP224R1"), 1); +#endif +#if (!defined(NO_ECC256) || defined(HAVE_ALL_CURVES)) && \ + (ECC_MIN_KEY_SZ <= 256) && \ + (!defined(WOLFSSL_SP_MATH) || \ + (defined(WOLFSSL_HAVE_SP_ECC) && !defined(WOLFSSL_SP_NO_256))) + /* secp256r1 covers the single precision path in --enable-sp builds, the + * common TLS configuration. Uses the curve generator as the peer point. */ + ExpectIntEQ(ecc_shared_secret_inf_case( + "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", + "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", + "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", + "SECP256R1"), 1); +#endif +#endif + return EXPECT_RESULT(); +} /* END test_wc_ecc_shared_secret_at_infinity */ + #if defined(HAVE_ECC) && defined(HAVE_ECC_DHE) && !defined(WC_NO_RNG) && \ (defined(HAVE_ECC384) || defined(HAVE_ECC521) || \ defined(HAVE_ALL_CURVES)) && \ diff --git a/tests/api/test_ecc.h b/tests/api/test_ecc.h index ed3ee2054b3..6faafc2df88 100644 --- a/tests/api/test_ecc.h +++ b/tests/api/test_ecc.h @@ -36,6 +36,7 @@ int test_wc_ecc_size(void); int test_wc_ecc_params(void); int test_wc_ecc_signVerify_hash(void); int test_wc_ecc_shared_secret(void); +int test_wc_ecc_shared_secret_at_infinity(void); int test_wc_ecc_shared_secret_size_bounds(void); int test_wc_ecc_export_x963(void); int test_wc_ecc_export_x963_ex(void); @@ -83,6 +84,7 @@ int test_wc_EccDecisionCoverage4(void); TEST_DECL_GROUP("ecc", test_wc_ecc_params), \ TEST_DECL_GROUP("ecc", test_wc_ecc_signVerify_hash), \ TEST_DECL_GROUP("ecc", test_wc_ecc_shared_secret), \ + TEST_DECL_GROUP("ecc", test_wc_ecc_shared_secret_at_infinity), \ TEST_DECL_GROUP("ecc", test_wc_ecc_shared_secret_size_bounds), \ TEST_DECL_GROUP("ecc", test_wc_ecc_export_x963), \ TEST_DECL_GROUP("ecc", test_wc_ecc_export_x963_ex), \ diff --git a/wolfcrypt/src/ecc.c b/wolfcrypt/src/ecc.c index 34acd96ca87..eb6acc84b12 100644 --- a/wolfcrypt/src/ecc.c +++ b/wolfcrypt/src/ecc.c @@ -4869,6 +4869,14 @@ int wc_ecc_shared_secret_gen_sync(ecc_key* private_key, ecc_point* point, { int err = MP_OKAY; mp_int* k = ecc_get_k(private_key); +#ifdef WOLFSSL_SE050 + /* A key-id handle holds no software private scalar - ECDH for such a key + * runs on the SE050 - so the identity this function computes from the + * all-zero scalar is not a shared secret and must not be rejected here. */ + int checkInf = !private_key->keyIdSet; +#else + int checkInf = 1; +#endif #ifdef HAVE_ECC_CDH WC_DECLARE_VAR(k_lcl, mp_int, 1, 0); #endif @@ -4997,6 +5005,7 @@ int wc_ecc_shared_secret_gen_sync(ecc_key* private_key, ecc_point* point, #endif #if defined(WOLFSSL_SP_MATH) { + (void)checkInf; err = WC_KEY_SIZE_E; goto errout; } @@ -5064,6 +5073,14 @@ int wc_ecc_shared_secret_gen_sync(ecc_key* private_key, ecc_point* point, /* Use constant time map if compiled in */ err = ecc_map_ex(result, curve->prime, mp, 1); } + if (err == MP_OKAY) { + /* SP 800-56Ar3 5.7.1.2: the shared secret must not be the point at + * infinity. ecc_map_ex reports that case as x, y of zero with a + * success code, so check the point explicitly. */ + if (checkInf && wc_ecc_point_is_at_infinity(result)) { + err = ECC_INF_E; + } + } if (err == MP_OKAY) { x = mp_unsigned_bin_size(curve->prime); if (*outlen < (word32)x || x < mp_unsigned_bin_size(result->x)) { @@ -5087,6 +5104,25 @@ int wc_ecc_shared_secret_gen_sync(ecc_key* private_key, ecc_point* point, } #endif +#ifdef WOLFSSL_HAVE_SP_ECC + if ((err == MP_OKAY) && checkInf) { + /* The single precision implementations above serialize the point at + * infinity as an all-zero x-coordinate and report success, so the + * identity has to be recognized from the output. SP 800-56Ar3 5.7.1.2 + * requires an error and stop in that case. Accumulate over the whole + * buffer so the scan does not branch on the secret. */ + word32 i; + byte acc = 0; + + for (i = 0; i < *outlen; i++) { + acc |= out[i]; + } + if (acc == 0) { + err = ECC_INF_E; + } + } +#endif + errout: #ifdef HAVE_ECC_CDH From fd13b11755868e09dcc1dc4f2c978d2684cbf14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 09:09:02 +0200 Subject: [PATCH 04/11] Use random-witness primality test for untrusted DH modulus wc_DhSetKey_ex loads DH parameters as untrusted and validates that the modulus is prime, but it passed no RNG, so the check fell back to a Miller-Rabin test using the fixed small-prime bases 2 through 19. That test is defeatable: a composite crafted as a strong pseudoprime to those known bases passes as prime, letting an attacker supply a composite modulus with a smooth factorization for small-subgroup recovery of the private exponent and shared secret. When no RNG is supplied on the untrusted path, create a temporary RNG so mp_prime_is_prime_ex runs with random witnesses, which such crafted composites cannot reliably pass. Named FFDHE primes still short-circuit the check, and builds without an RNG keep the deterministic test. Fixes F-6776. --- doc/dox_comments/header_files/dh.h | 8 +++++++ tests/api/test_dh.c | 35 ++++++++++++++++++++++++++++++ tests/api/test_dh.h | 2 ++ wolfcrypt/src/dh.c | 33 +++++++++++++++++++++++++--- 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/doc/dox_comments/header_files/dh.h b/doc/dox_comments/header_files/dh.h index a7caf644a28..ba2afd24d8e 100644 --- a/doc/dox_comments/header_files/dh.h +++ b/doc/dox_comments/header_files/dh.h @@ -1050,6 +1050,14 @@ int wc_DhSetCheckKey(DhKey* key, const byte* p, word32 pSz, /*! \ingroup Diffie-Hellman + + \note Parameters that are not one of the built-in FFDHE groups are + untrusted, so the modulus is tested for primality with random + Miller-Rabin witnesses. This function takes no RNG, so it instantiates a + temporary one, which allocates memory and consumes entropy on every such + call. When neither is available the test falls back to fixed witness + bases, which a purpose-built composite can defeat. Call + wc_DhSetCheckKey() with an initialized WC_RNG to supply your own. */ int wc_DhSetKey_ex(DhKey* key, const byte* p, word32 pSz, const byte* g, word32 gSz, const byte* q, word32 qSz); diff --git a/tests/api/test_dh.c b/tests/api/test_dh.c index 8a2bd1020d5..d79c168305c 100644 --- a/tests/api/test_dh.c +++ b/tests/api/test_dh.c @@ -316,6 +316,41 @@ int test_wc_DhSetKey(void) return EXPECT_RESULT(); } +/* + * wc_DhSetKey_ex validates untrusted parameters, so it must reject a composite + * modulus even when that composite is a strong pseudoprime to the fixed + * small-prime Miller-Rabin bases. n = 341550071728321 = 10670053 * 32010157 is + * a strong pseudoprime to bases 2, 3, 5, 7, 11, 13, 17 and 19, which the + * deterministic fixed-base test wrongly accepts as prime. Random-witness + * testing rejects it. As with the library's own primality check, rejection is + * probabilistic: eight random Miller-Rabin rounds leave a negligible + * (well under 1e-4) chance of accepting the composite, so a one-off failure + * here is statistical, not a regression. + */ +int test_wc_DhSetKey_ex_pseudoprime(void) +{ + EXPECT_DECLS; + /* wc_DhSetKey_ex takes no RNG, so it has to create one to get random + * witnesses. Builds that cannot allocate it fall back to the fixed-base + * test by design, and that test accepts this composite. */ +#if !defined(NO_DH) && !defined(HAVE_SELFTEST) && !defined(HAVE_FIPS) && \ + !defined(WC_NO_RNG) && !defined(WOLFSSL_NO_MALLOC) && \ + !defined(WOLFSSL_STATIC_MEMORY) + DhKey key; + /* n = 341550071728321, big-endian. */ + byte p[] = { 0x01, 0x36, 0xA3, 0x52, 0xB2, 0xC8, 0xC1 }; + byte g[] = { 0x02 }; + + XMEMSET(&key, 0, sizeof(key)); + + ExpectIntEQ(wc_InitDhKey(&key), 0); + ExpectIntEQ(wc_DhSetKey_ex(&key, p, sizeof(p), g, sizeof(g), NULL, 0), + WC_NO_ERR_TRACE(DH_CHECK_PUB_E)); + wc_FreeDhKey(&key); +#endif + return EXPECT_RESULT(); +} + /* * Testing wc_DhSetNamedKey(), wc_DhGetNamedKeyParamSize(), * wc_DhCopyNamedKey() and wc_DhCmpNamedKey(). diff --git a/tests/api/test_dh.h b/tests/api/test_dh.h index 23a4a0bb265..3aa50855eeb 100644 --- a/tests/api/test_dh.h +++ b/tests/api/test_dh.h @@ -27,6 +27,7 @@ int test_wc_DhPublicKeyDecode(void); int test_wc_DhAgree_subgroup_check(void); int test_wc_DhSetKey(void); +int test_wc_DhSetKey_ex_pseudoprime(void); int test_wc_DhSetNamedKey_and_helpers(void); int test_wc_DhGenerateKeyPair_bad_args(void); int test_wc_DhGenerateKeyPair_and_Agree(void); @@ -42,6 +43,7 @@ int test_wc_DhGenerateKeyPair_CheckDhLN(void); TEST_DECL_GROUP("dh", test_wc_DhPublicKeyDecode), \ TEST_DECL_GROUP("dh", test_wc_DhAgree_subgroup_check), \ TEST_DECL_GROUP("dh", test_wc_DhSetKey), \ + TEST_DECL_GROUP("dh", test_wc_DhSetKey_ex_pseudoprime), \ TEST_DECL_GROUP("dh", test_wc_DhSetNamedKey_and_helpers), \ TEST_DECL_GROUP("dh", test_wc_DhGenerateKeyPair_bad_args), \ TEST_DECL_GROUP("dh", test_wc_DhGenerateKeyPair_and_Agree), \ diff --git a/wolfcrypt/src/dh.c b/wolfcrypt/src/dh.c index 5a6daa98531..96c6446b67b 100644 --- a/wolfcrypt/src/dh.c +++ b/wolfcrypt/src/dh.c @@ -2670,10 +2670,37 @@ static int _DhSetKey(DhKey* key, const byte* p, word32 pSz, const byte* g, else #endif { - if (rng != NULL) - ret = mp_prime_is_prime_ex(keyP, 8, &isPrime, rng); - else +#ifndef WC_NO_RNG + WC_RNG* checkRng = rng; + WC_RNG* tmpRng = NULL; + + /* A fixed-base Miller-Rabin test can be fooled by a crafted + * composite, so use random witnesses when an RNG is available. + * Create a temporary RNG when the caller did not supply one. */ + if (checkRng == NULL) { + if (wc_rng_new_ex(&tmpRng, NULL, 0, key->heap, + INVALID_DEVID) == 0) { + checkRng = tmpRng; + } + } + + if (checkRng != NULL) { + ret = mp_prime_is_prime_ex(keyP, 8, &isPrime, checkRng); + } + else { + /* Fall back to the deterministic test rather than failing the + * parameter load. This is the weaker check: a composite + * crafted against the fixed bases is accepted as prime. */ + WOLFSSL_MSG("DH: no RNG, primality test uses fixed bases"); ret = mp_prime_is_prime(keyP, 8, &isPrime); + } + + /* Safe on NULL and zeroizes the RNG state before freeing. */ + wc_rng_free(tmpRng); +#else + (void)rng; + ret = mp_prime_is_prime(keyP, 8, &isPrime); +#endif } if (ret == 0 && isPrime == 0) From 4c05429fb04e10e9d1715f5f93cf03e9e2ec6102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 10:57:43 +0200 Subject: [PATCH 05/11] Add negative tests for wc_ecc_check_key public-key checks wc_ecc_check_key validates a public key's coordinate range, that the point is on the curve, and its order, but the software path had no negative coverage: the existing test only exercised a valid key and NULL, and the off-curve case lived in the crypto-callback test, which validates the device path rather than the software on-curve check. A deletion of either the on-curve check or the coordinate-range checks therefore passed the suite. Add a test that imports secp256r1 public keys that are off the curve and out of coordinate range, asserting IS_POINT_E and ECC_OUT_OF_RANGE_E respectively, exercising the software validation path. Fixes F-6620. --- tests/api/test_ecc.c | 61 ++++++++++++++++++++++++++++++++++++++++++++ tests/api/test_ecc.h | 2 ++ 2 files changed, 63 insertions(+) diff --git a/tests/api/test_ecc.c b/tests/api/test_ecc.c index ed6927df4b4..95ed354da47 100644 --- a/tests/api/test_ecc.c +++ b/tests/api/test_ecc.c @@ -295,6 +295,67 @@ int test_wc_ecc_check_key(void) return EXPECT_RESULT(); } /* END test_wc_ecc_check_key */ +/* + * Negative coverage for the public-key checks in wc_ecc_check_key. A point off + * the curve must be rejected with IS_POINT_E, and a coordinate outside + * [0, p-1] with ECC_OUT_OF_RANGE_E. Uses secp224r1, which is not single + * precision accelerated, so the software validation path runs even in SP + * builds that offload P-256. + */ +int test_wc_ecc_check_key_invalid_pubkey(void) +{ + EXPECT_DECLS; + /* Older FIPS-certified modules ship a frozen source tree where these + * imports and checks behave differently, so restrict to non-FIPS or + * FIPS v7 and later, and skip the CAVP selftest build. */ +#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + !defined(HAVE_SELFTEST) && \ + defined(HAVE_ECC) && defined(HAVE_ECC_KEY_IMPORT) && \ + !defined(NO_ECC_CHECK_PUBKEY_ORDER) && \ + !defined(WOLF_CRYPTO_CB_ONLY_ECC) && \ + (defined(HAVE_ECC224) || defined(HAVE_ALL_CURVES)) && \ + (ECC_MIN_KEY_SZ <= 224) && \ + !defined(WOLFSSL_VALIDATE_ECC_IMPORT) && !defined(WOLFSSL_SP_MATH) && \ + !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A) && \ + !defined(WOLFSSL_MICROCHIP_TA100) && !defined(WOLFSSL_CRYPTOCELL) && \ + !defined(WOLFSSL_SILABS_SE_ACCEL) && !defined(WOLFSSL_SE050) && \ + !defined(WOLFSSL_STM32_PKA) && !defined(WOLFSSL_KCAPI_ECC) + ecc_key key; + const char* qx = + "b70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21"; + const char* qy = + "bd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34"; + /* Qy with its low bit flipped: still less than p, but not on the curve. */ + const char* qyOffCurve = + "bd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e35"; + /* p, the SECP224R1 field prime, used as an out-of-range coordinate. */ + const char* pModulus = + "ffffffffffffffffffffffffffffffff000000000000000000000001"; + + /* Point not on the curve: rejected by the on-curve check. */ + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_ecc_init(&key), 0); + ExpectIntEQ(wc_ecc_import_raw(&key, qx, qyOffCurve, NULL, "SECP224R1"), 0); + ExpectIntEQ(wc_ecc_check_key(&key), WC_NO_ERR_TRACE(IS_POINT_E)); + wc_ecc_free(&key); + + /* Qx == p: rejected by the coordinate-range check. */ + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_ecc_init(&key), 0); + ExpectIntEQ(wc_ecc_import_raw(&key, pModulus, qy, NULL, "SECP224R1"), 0); + ExpectIntEQ(wc_ecc_check_key(&key), WC_NO_ERR_TRACE(ECC_OUT_OF_RANGE_E)); + wc_ecc_free(&key); + + /* Qy == p: rejected by the coordinate-range check. */ + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_ecc_init(&key), 0); + ExpectIntEQ(wc_ecc_import_raw(&key, qx, pModulus, NULL, "SECP224R1"), 0); + ExpectIntEQ(wc_ecc_check_key(&key), WC_NO_ERR_TRACE(ECC_OUT_OF_RANGE_E)); + wc_ecc_free(&key); +#endif + return EXPECT_RESULT(); +} /* END test_wc_ecc_check_key_invalid_pubkey */ + /* * Testing wc_ecc_get_generator() */ diff --git a/tests/api/test_ecc.h b/tests/api/test_ecc.h index 6faafc2df88..31d475f160d 100644 --- a/tests/api/test_ecc.h +++ b/tests/api/test_ecc.h @@ -31,6 +31,7 @@ int test_wc_ecc_get_curve_id_from_dp_params(void); int test_wc_ecc_make_key(void); int test_wc_ecc_init(void); int test_wc_ecc_check_key(void); +int test_wc_ecc_check_key_invalid_pubkey(void); int test_wc_ecc_get_generator(void); int test_wc_ecc_size(void); int test_wc_ecc_params(void); @@ -79,6 +80,7 @@ int test_wc_EccDecisionCoverage4(void); TEST_DECL_GROUP("ecc", test_wc_ecc_make_key), \ TEST_DECL_GROUP("ecc", test_wc_ecc_init), \ TEST_DECL_GROUP("ecc", test_wc_ecc_check_key), \ + TEST_DECL_GROUP("ecc", test_wc_ecc_check_key_invalid_pubkey), \ TEST_DECL_GROUP("ecc", test_wc_ecc_get_generator), \ TEST_DECL_GROUP("ecc", test_wc_ecc_size), \ TEST_DECL_GROUP("ecc", test_wc_ecc_params), \ From d7a5e85716a0f5639eab6d40b0f2fd4db993a68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 11:29:26 +0200 Subject: [PATCH 06/11] Add negative test for Ed448 signature S-range check Ed448 verification rejects a non-canonical signature scalar S (S >= L) per RFC 8032, and that range check is the only guard against a malleated signature: because L times the base point is the identity, (R, S + L) recomputes the same R and would otherwise verify. The check had no negative coverage, so a deletion or boundary mutation passed the suite while all canonical KAT signatures kept working. Add a test that signs a message, then verifies crafted signatures whose S half equals the order, exceeds it in a high or low byte, and equals S + L, asserting BAD_FUNC_ARG, plus an in-range wrong S asserting SIG_VERIFY_E. Fixes F-6777. --- tests/api/test_ed448.c | 109 +++++++++++++++++++++++++++++++++++++++++ tests/api/test_ed448.h | 2 + 2 files changed, 111 insertions(+) diff --git a/tests/api/test_ed448.c b/tests/api/test_ed448.c index f468b0c0c48..4a32800b713 100644 --- a/tests/api/test_ed448.c +++ b/tests/api/test_ed448.c @@ -177,6 +177,115 @@ int test_wc_ed448_sign_msg(void) return EXPECT_RESULT(); } /* END test_wc_ed448_sign_msg */ +/* + * RFC 8032 requires the Ed448 signature scalar S to be canonical (S < L). + * Because L times the base point is the identity, a malleated signature with + * S' = S + L recomputes the same R, so the S-range check is the only guard + * against it. Confirm a signature with S >= L (including the malleability case + * S + L) is rejected with BAD_FUNC_ARG, while an in-range but wrong S fails + * verification with SIG_VERIFY_E. + */ +int test_wc_ed448_verify_sig_S_range(void) +{ + EXPECT_DECLS; + /* The S-range rejection may be absent in the frozen ed448.c of older + * FIPS-certified modules, so restrict to non-FIPS or FIPS v7 and later. */ +#if (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) && \ + defined(HAVE_ED448) && defined(HAVE_ED448_SIGN) && \ + defined(HAVE_ED448_VERIFY) + ed448_key key; + WC_RNG rng; + byte msg[] = "Everybody gets Friday off.\n"; + byte sig[ED448_SIG_SIZE]; + byte badSig[ED448_SIG_SIZE]; + word32 msglen = sizeof(msg); + word32 siglen = sizeof(sig); + int verify_ok = 0; + int i; + int carry; + int sum; + /* Ed448 group order L, little-endian, 57 bytes. */ + static const byte order[] = { + 0xf3, 0x44, 0x58, 0xab, 0x92, 0xc2, 0x78, 0x23, + 0x55, 0x8f, 0xc5, 0x8d, 0x72, 0xc2, 0x6c, 0x21, + 0x90, 0x36, 0xd6, 0xae, 0x49, 0xdb, 0x4e, 0xc4, + 0xe9, 0x23, 0xca, 0x7c, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, + 0x00 + }; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(sig, 0, sizeof(sig)); + + ExpectIntEQ(wc_ed448_init(&key), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_ed448_make_key(&rng, ED448_KEY_SIZE, &key), 0); + + /* Produce a valid signature and confirm it verifies. */ + ExpectIntEQ(wc_ed448_sign_msg(msg, msglen, sig, &siglen, &key, NULL, 0), 0); + ExpectIntEQ(siglen, ED448_SIG_SIZE); + ExpectIntEQ(wc_ed448_verify_msg(sig, siglen, msg, msglen, &verify_ok, &key, + NULL, 0), 0); + ExpectIntEQ(verify_ok, 1); + + /* Malleability: S' = S + L. The same R is recomputed, so only the S-range + * check can reject it. */ + XMEMCPY(badSig, sig, ED448_SIG_SIZE); + carry = 0; + for (i = 0; i < (int)sizeof(order); i++) { + sum = (int)badSig[ED448_SIG_SIZE / 2 + i] + (int)order[i] + carry; + badSig[ED448_SIG_SIZE / 2 + i] = (byte)(sum & 0xff); + carry = sum >> 8; + } + verify_ok = 1; + ExpectIntEQ(wc_ed448_verify_msg(badSig, ED448_SIG_SIZE, msg, msglen, + &verify_ok, &key, NULL, 0), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(verify_ok, 0); + + /* S exactly equal to L. */ + XMEMCPY(badSig, sig, ED448_SIG_SIZE); + XMEMCPY(badSig + ED448_SIG_SIZE / 2, order, sizeof(order)); + verify_ok = 1; + ExpectIntEQ(wc_ed448_verify_msg(badSig, ED448_SIG_SIZE, msg, msglen, + &verify_ok, &key, NULL, 0), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(verify_ok, 0); + + /* S greater than L in a high byte. */ + XMEMCPY(badSig, sig, ED448_SIG_SIZE); + XMEMCPY(badSig + ED448_SIG_SIZE / 2, order, sizeof(order)); + badSig[ED448_SIG_SIZE / 2 + 55] = 0x40; + verify_ok = 1; + ExpectIntEQ(wc_ed448_verify_msg(badSig, ED448_SIG_SIZE, msg, msglen, + &verify_ok, &key, NULL, 0), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(verify_ok, 0); + + /* S greater than L in a low byte. */ + XMEMCPY(badSig, sig, ED448_SIG_SIZE); + XMEMCPY(badSig + ED448_SIG_SIZE / 2, order, sizeof(order)); + badSig[ED448_SIG_SIZE / 2 + 0] = 0xf4; + verify_ok = 1; + ExpectIntEQ(wc_ed448_verify_msg(badSig, ED448_SIG_SIZE, msg, msglen, + &verify_ok, &key, NULL, 0), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(verify_ok, 0); + + /* S below L: passes the range check, fails verification instead. */ + XMEMCPY(badSig, sig, ED448_SIG_SIZE); + XMEMCPY(badSig + ED448_SIG_SIZE / 2, order, sizeof(order)); + badSig[ED448_SIG_SIZE / 2 + 0] = 0xf2; + verify_ok = 1; + ExpectIntEQ(wc_ed448_verify_msg(badSig, ED448_SIG_SIZE, msg, msglen, + &verify_ok, &key, NULL, 0), WC_NO_ERR_TRACE(SIG_VERIFY_E)); + ExpectIntEQ(verify_ok, 0); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_ed448_free(&key); +#endif + return EXPECT_RESULT(); +} /* END test_wc_ed448_verify_sig_S_range */ + /* * Test that wc_ed448_sign_msg() rejects a public-key-only key object. * A key with pubKeySet=1 but privKeySet=0 must not silently sign. diff --git a/tests/api/test_ed448.h b/tests/api/test_ed448.h index 169c88a39ce..53209b21651 100644 --- a/tests/api/test_ed448.h +++ b/tests/api/test_ed448.h @@ -27,6 +27,7 @@ int test_wc_ed448_make_key(void); int test_wc_ed448_init(void); int test_wc_ed448_sign_msg(void); +int test_wc_ed448_verify_sig_S_range(void); int test_wc_ed448_sign_msg_pubonly_fails(void); int test_wc_ed448_import_public(void); int test_wc_ed448_import_private_key(void); @@ -47,6 +48,7 @@ int test_wc_ed448_check_key_decisions(void); TEST_DECL_GROUP("ed448", test_wc_ed448_make_key), \ TEST_DECL_GROUP("ed448", test_wc_ed448_init), \ TEST_DECL_GROUP("ed448", test_wc_ed448_sign_msg), \ + TEST_DECL_GROUP("ed448", test_wc_ed448_verify_sig_S_range), \ TEST_DECL_GROUP("ed448", test_wc_ed448_sign_msg_pubonly_fails), \ TEST_DECL_GROUP("ed448", test_wc_ed448_import_public), \ TEST_DECL_GROUP("ed448", test_wc_ed448_import_private_key), \ From c19654676200e7b08c43cf3478212d3945deb10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 13:06:02 +0200 Subject: [PATCH 07/11] Surface hardware failures in Intel QAT sync cipher IntelQaSymCipher ran cpaCySymPerformOp synchronously but never translated its completion status into the return code. Only the AES-GCM decrypt auth check, driven by verifyResult, could set an error. For AES-CBC, AES-GCM encrypt, and 3DES-CBC a non-success status left ret at zero, so the exit path copied the working buffer to the output and returned success. On encrypt that buffer still holds the plaintext copy, so a hardware failure returned success with plaintext written to the ciphertext output. Translate a non-success perform-op status into ASYNC_OP_E, matching the asynchronous port, so hardware failures are surfaced instead of returning zero with unprocessed output. Fixes F-6623. --- wolfcrypt/src/port/intel/quickassist_sync.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/wolfcrypt/src/port/intel/quickassist_sync.c b/wolfcrypt/src/port/intel/quickassist_sync.c index 91cb76ae8b2..f9f0309a131 100644 --- a/wolfcrypt/src/port/intel/quickassist_sync.c +++ b/wolfcrypt/src/port/intel/quickassist_sync.c @@ -1085,10 +1085,13 @@ static int IntelQaSymCipher(IntelQaDev* dev, byte* out, const byte* in, status = cpaCySymPerformOp(dev->handle, dev, opData, bufferList, bufferList, &verifyResult); - if (symOperation == CPA_CY_SYM_OP_ALGORITHM_CHAINING && - cipherAlgorithm == CPA_CY_SYM_CIPHER_AES_GCM && - cipherDirection == CPA_CY_SYM_CIPHER_DIRECTION_DECRYPT && - hashAlgorithm == CPA_CY_SYM_HASH_AES_GCM) { + if (status != CPA_STATUS_SUCCESS) { + ret = ASYNC_OP_E; + } + else if (symOperation == CPA_CY_SYM_OP_ALGORITHM_CHAINING && + cipherAlgorithm == CPA_CY_SYM_CIPHER_AES_GCM && + cipherDirection == CPA_CY_SYM_CIPHER_DIRECTION_DECRYPT && + hashAlgorithm == CPA_CY_SYM_HASH_AES_GCM) { if (verifyResult == CPA_FALSE) { ret = AES_GCM_AUTH_E; } From 1673777231a771a84f21c9663085febd4edfd4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 13:12:50 +0200 Subject: [PATCH 08/11] Fix transposed register and mask in ESP32-C3 RSA unlock The ESP32-C3 deactivation path in esp_mp_hw_unlock passed its arguments to DPORT_REG_CLR_BIT and DPORT_REG_SET_BIT in the wrong order and added a stray DR_REG_RSA_BASE offset. The macros take (register address, bit mask), but the code used the bit mask as part of the register address and the register as the bit mask. As a result the RSA clock-enable and memory power-down bits were never updated at deactivation, and reads and writes landed at a bogus peripheral address. The accelerator was left powered up after every bignum and RSA operation. Pass the register address first and the bit mask second and drop the offset, mirroring the activation path and the other targets. Fixes F-6779. --- wolfcrypt/src/port/Espressif/esp32_mp.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/wolfcrypt/src/port/Espressif/esp32_mp.c b/wolfcrypt/src/port/Espressif/esp32_mp.c index 9ca4d90b3e9..124d3d23584 100644 --- a/wolfcrypt/src/port/Espressif/esp32_mp.c +++ b/wolfcrypt/src/port/Espressif/esp32_mp.c @@ -567,12 +567,10 @@ static int esp_mp_hw_unlock(void) * This releases the RSA Accelerator from reset.*/ portENTER_CRITICAL_SAFE(&wc_rsa_reg_lock); { - DPORT_REG_CLR_BIT( - (volatile void *)(DR_REG_RSA_BASE + SYSTEM_CRYPTO_RSA_CLK_EN), - SYSTEM_PERIP_CLK_EN1_REG); - DPORT_REG_SET_BIT( - (volatile void *)(DR_REG_RSA_BASE + SYSTEM_RSA_MEM_PD), - SYSTEM_RSA_PD_CTRL_REG); + DPORT_REG_CLR_BIT((volatile void *)(SYSTEM_PERIP_CLK_EN1_REG), + SYSTEM_CRYPTO_RSA_CLK_EN); + DPORT_REG_SET_BIT((volatile void *)(SYSTEM_RSA_PD_CTRL_REG), + SYSTEM_RSA_MEM_PD); } portEXIT_CRITICAL_SAFE(&wc_rsa_reg_lock); #elif defined(CONFIG_IDF_TARGET_ESP32C6) From af0385cb8d0fcafe988af3c2f4e64296544efa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 13:20:16 +0200 Subject: [PATCH 09/11] Avoid unaligned word32 read of RSA key id under SE050 wc_InitRsaKey_Id cast the byte array key->id to word32* and dereferenced it to recover the SE050 key id. key->id is a byte array with no alignment guarantee inside RsaKey, so the dereference is an unaligned 32-bit read that faults or mis-reads on strict-alignment targets, and it violates strict aliasing. Read the value with readUnalignedWord32 instead, which does an alignment-safe byte copy, matching wc_ecc_init_id. Fixes F-6624. --- wolfcrypt/src/rsa.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wolfcrypt/src/rsa.c b/wolfcrypt/src/rsa.c index 5f33baf7cdf..b1dd2f445e7 100644 --- a/wolfcrypt/src/rsa.c +++ b/wolfcrypt/src/rsa.c @@ -387,8 +387,8 @@ int wc_InitRsaKey_Id(RsaKey* key, unsigned char* id, int len, void* heap, { int ret = 0; #if defined(WOLFSSL_SE050) && !defined(WOLFSSL_SE050_NO_RSA) - /* SE050 TLS users store a word32 at id, need to cast back */ - word32* keyPtr = NULL; + /* SE050 TLS users store a word32 at id, need to read it back */ + word32 keyId = 0; #endif if (key == NULL) @@ -403,8 +403,8 @@ int wc_InitRsaKey_Id(RsaKey* key, unsigned char* id, int len, void* heap, #if defined(WOLFSSL_SE050) && !defined(WOLFSSL_SE050_NO_RSA) /* Set SE050 ID from word32, populate RsaKey with public from SE050 */ if (len == (int)sizeof(word32)) { - keyPtr = (word32*)key->id; - ret = wc_RsaUseKeyId(key, *keyPtr, 0); + keyId = readUnalignedWord32(key->id); + ret = wc_RsaUseKeyId(key, keyId, 0); } #endif } From ca449b1263228cdc23ec15944cc502d1bc938e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 15:16:44 +0200 Subject: [PATCH 10/11] Use alignment-safe reads in ML-KEM mlkem_cbd_eta3 The little-endian large-code path of mlkem_cbd_eta3 cast the caller-supplied byte buffer to word32* and read through it. The buffer has no alignment guarantee, so on strict-alignment targets such as Cortex-M3 and M4 with unaligned-access trapping enabled the read faults or returns wrong values, and the cast violates strict aliasing. Read each word with readUnalignedWord32, which does an alignment-safe byte copy, matching the neighboring mlkem_cbd_eta2. Fixes F-6781. --- wolfcrypt/src/wc_mlkem_poly.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/wolfcrypt/src/wc_mlkem_poly.c b/wolfcrypt/src/wc_mlkem_poly.c index c7e9956fd37..15db28e4fab 100644 --- a/wolfcrypt/src/wc_mlkem_poly.c +++ b/wolfcrypt/src/wc_mlkem_poly.c @@ -4436,12 +4436,14 @@ static void mlkem_cbd_eta3(sword16* p, const byte* r) #else /* Calculate eight integer coefficients at a time. */ for (i = 0; i < MLKEM_N; i += 16) { - const word32* r32 = (const word32*)r; + word32 r0 = readUnalignedWord32(r); + word32 r1 = readUnalignedWord32(r + 4); + word32 r2 = readUnalignedWord32(r + 8); /* Take the next 12 bytes, little endian, as 24 bit values. */ - word32 t0 = r32[0] & 0xffffff; - word32 t1 = ((r32[0] >> 24) | (r32[1] << 8)) & 0xffffff; - word32 t2 = ((r32[1] >> 16) | (r32[2] << 16)) & 0xffffff; - word32 t3 = r32[2] >> 8 ; + word32 t0 = r0 & 0xffffff; + word32 t1 = ((r0 >> 24) | (r1 << 8)) & 0xffffff; + word32 t2 = ((r1 >> 16) | (r2 << 16)) & 0xffffff; + word32 t3 = r2 >> 8 ; word32 d0; word32 d1; word32 d2; From 097ddc19d8d3d8a556441b411fa599cbb7c8c047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 21 Jul 2026 15:26:51 +0200 Subject: [PATCH 11/11] Use alignment-safe writes in ML-KEM mlkem_vec_compress_10_c The little-endian large-code path of mlkem_vec_compress_10_c cast the output byte buffer to word32* and issued five 32-bit stores through it. That buffer is the caller-supplied ML-KEM ciphertext, which has no alignment guarantee, so on strict-alignment targets the store bus-faults and the cast violates strict aliasing. Write each word with writeUnalignedWord32, which does an alignment-safe byte copy, matching mldsa_encode_w1_88_c and the neighboring ML-KEM sampling code. Fixes F-6782. --- wolfcrypt/src/wc_mlkem_poly.c | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/wolfcrypt/src/wc_mlkem_poly.c b/wolfcrypt/src/wc_mlkem_poly.c index 15db28e4fab..9737f54f1a2 100644 --- a/wolfcrypt/src/wc_mlkem_poly.c +++ b/wolfcrypt/src/wc_mlkem_poly.c @@ -5864,18 +5864,22 @@ static void mlkem_vec_compress_10_c(byte* r, sword16* v, unsigned int k) sword16 t14 = TO_COMP_WORD_10(v, i, j, 14); sword16 t15 = TO_COMP_WORD_10(v, i, j, 15); - word32* r32 = (word32*)r; /* Pack sixteen 10-bit values into byte array. */ - r32[0] = (word32)t0 | ((word32)t1 << 10) | - ((word32)t2 << 20) | ((word32)t3 << 30); - r32[1] = ((word32)t3 >> 2) | ((word32)t4 << 8) | - ((word32)t5 << 18) | ((word32)t6 << 28); - r32[2] = ((word32)t6 >> 4) | ((word32)t7 << 6) | - ((word32)t8 << 16) | ((word32)t9 << 26); - r32[3] = ((word32)t9 >> 6) | ((word32)t10 << 4) | - ((word32)t11 << 14) | ((word32)t12 << 24); - r32[4] = ((word32)t12 >> 8) | ((word32)t13 << 2) | - ((word32)t14 << 12) | ((word32)t15 << 22); + writeUnalignedWord32(r + 0, + (word32)t0 | ((word32)t1 << 10) | + ((word32)t2 << 20) | ((word32)t3 << 30)); + writeUnalignedWord32(r + 4, + ((word32)t3 >> 2) | ((word32)t4 << 8) | + ((word32)t5 << 18) | ((word32)t6 << 28)); + writeUnalignedWord32(r + 8, + ((word32)t6 >> 4) | ((word32)t7 << 6) | + ((word32)t8 << 16) | ((word32)t9 << 26)); + writeUnalignedWord32(r + 12, + ((word32)t9 >> 6) | ((word32)t10 << 4) | + ((word32)t11 << 14) | ((word32)t12 << 24)); + writeUnalignedWord32(r + 16, + ((word32)t12 >> 8) | ((word32)t13 << 2) | + ((word32)t14 << 12) | ((word32)t15 << 22)); /* Move over set bytes. */ r += 20;