From 723f5f757ec2af6c42a5dbb81a032ae32595e3be Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 12 Aug 2026 10:28:33 -0600 Subject: [PATCH 01/10] F-7427: Remove WOLFSSL_MP_INVMOD_CONSTANT_TIME macro --- .wolfssl_known_macro_extras | 1 - wolfcrypt/src/dsa.c | 72 ++++--------------------------------- wolfcrypt/src/rsa.c | 9 ++--- 3 files changed, 9 insertions(+), 73 deletions(-) diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index 4c7d550bb03..8541dd89a91 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -1062,7 +1062,6 @@ WOLFSSL_MLKEM_NO_MALLOC WOLFSSL_MLKEM_NTT_UNROLL WOLFSSL_MONT_RED_CT WOLFSSL_MP_COND_COPY -WOLFSSL_MP_INVMOD_CONSTANT_TIME WOLFSSL_MULTICIRCULATE_ALTNAMELIST WOLFSSL_MX2_CONF_INCLUDE WOLFSSL_NETX_DUO diff --git a/wolfcrypt/src/dsa.c b/wolfcrypt/src/dsa.c index fbc01ad3611..6414405e333 100644 --- a/wolfcrypt/src/dsa.c +++ b/wolfcrypt/src/dsa.c @@ -806,15 +806,11 @@ int wc_DsaSign_ex(const byte* digest, word32 digestSz, byte* out, DsaKey* key, mp_int *r = NULL; mp_int *s = NULL; mp_int *H = NULL; -#ifndef WOLFSSL_MP_INVMOD_CONSTANT_TIME mp_int *b = NULL; -#endif byte *buffer = NULL; #else mp_int k[1], kInv[1], r[1], s[1], H[1]; -#ifndef WOLFSSL_MP_INVMOD_CONSTANT_TIME mp_int b[1]; -#endif byte buffer[DSA_MAX_HALF_SIZE]; #endif mp_int* qMinus1; @@ -838,9 +834,7 @@ int wc_DsaSign_ex(const byte* digest, word32 digestSz, byte* out, DsaKey* key, r = (mp_int *)XMALLOC(sizeof *r, key->heap, DYNAMIC_TYPE_TMP_BUFFER); s = (mp_int *)XMALLOC(sizeof *s, key->heap, DYNAMIC_TYPE_TMP_BUFFER); H = (mp_int *)XMALLOC(sizeof *H, key->heap, DYNAMIC_TYPE_TMP_BUFFER); -#ifndef WOLFSSL_MP_INVMOD_CONSTANT_TIME b = (mp_int *)XMALLOC(sizeof *b, key->heap, DYNAMIC_TYPE_TMP_BUFFER); -#endif buffer = (byte *)XMALLOC(DSA_MAX_HALF_SIZE, key->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -848,25 +842,18 @@ int wc_DsaSign_ex(const byte* digest, word32 digestSz, byte* out, DsaKey* key, (kInv == NULL) || (r == NULL) || (s == NULL) || - (H == NULL) -#ifndef WOLFSSL_MP_INVMOD_CONSTANT_TIME - || (b == NULL) -#endif - || (buffer == NULL)) { + (H == NULL) || + (b == NULL) || + (buffer == NULL)) { ret = MEMORY_E; break; } #endif -#ifdef WOLFSSL_MP_INVMOD_CONSTANT_TIME - if (mp_init_multi(k, kInv, r, s, H, 0) != MP_OKAY) -#else - if (mp_init_multi(k, kInv, r, s, H, b) != MP_OKAY) -#endif - { - ret = MP_INIT_E; - break; - } + if (mp_init_multi(k, kInv, r, s, H, b) != MP_OKAY) { + ret = MP_INIT_E; + break; + } halfSz = min(DSA_MAX_HALF_SIZE, (word32)mp_unsigned_bin_size(&key->q)); /* NIST FIPS 186-4: Sections 4.1 @@ -941,46 +928,6 @@ int wc_DsaSign_ex(const byte* digest, word32 digestSz, byte* out, DsaKey* key, break; } -#ifdef WOLFSSL_MP_INVMOD_CONSTANT_TIME - /* inverse k mod q */ - if (mp_invmod(k, &key->q, kInv) != MP_OKAY) { - ret = MP_INVMOD_E; - break; - } - - /* generate r, r = (g exp k mod p) mod q */ - if (mp_exptmod_ex(&key->g, k, key->q.used, &key->p, r) != MP_OKAY) { - ret = MP_EXPTMOD_E; - break; - } - - if (mp_mod(r, &key->q, r) != MP_OKAY) { - ret = MP_MOD_E; - break; - } - - /* generate H from sha digest */ - if (mp_read_unsigned_bin(H, digest, digestSz) != MP_OKAY) { - ret = MP_READ_E; - break; - } - - /* generate s, s = (kInv * (H + x*r)) % q */ - if (mp_mul(&key->x, r, s) != MP_OKAY) { - ret = MP_MUL_E; - break; - } - - if (mp_add(s, H, s) != MP_OKAY) { - ret = MP_ADD_E; - break; - } - - if (mp_mulmod(s, kInv, &key->q, s) != MP_OKAY) { - ret = MP_MULMOD_E; - break; - } -#else /* Blinding value * Generate b in range [1, q-1]. */ @@ -1069,7 +1016,6 @@ int wc_DsaSign_ex(const byte* digest, word32 digestSz, byte* out, DsaKey* key, ret = MP_MOD_E; break; } -#endif /* detect zero r or s */ if ((mp_iszero(r) == MP_YES) || (mp_iszero(s) == MP_YES)) { @@ -1119,14 +1065,12 @@ int wc_DsaSign_ex(const byte* digest, word32 digestSz, byte* out, DsaKey* key, mp_clear(H); XFREE(H, key->heap, DYNAMIC_TYPE_TMP_BUFFER); } -#ifndef WOLFSSL_MP_INVMOD_CONSTANT_TIME if (b) { if ((ret != WC_NO_ERR_TRACE(MP_INIT_E)) && (ret != WC_NO_ERR_TRACE(MEMORY_E))) mp_forcezero(b); XFREE(b, key->heap, DYNAMIC_TYPE_TMP_BUFFER); } -#endif if (buffer) { ForceZero(buffer, halfSz); XFREE(buffer, key->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -1139,9 +1083,7 @@ int wc_DsaSign_ex(const byte* digest, word32 digestSz, byte* out, DsaKey* key, #endif mp_forcezero(kInv); mp_forcezero(k); -#ifndef WOLFSSL_MP_INVMOD_CONSTANT_TIME mp_forcezero(b); -#endif mp_clear(H); mp_clear(s); mp_clear(r); diff --git a/wolfcrypt/src/rsa.c b/wolfcrypt/src/rsa.c index f59104af1c8..56dd6cbcbfb 100644 --- a/wolfcrypt/src/rsa.c +++ b/wolfcrypt/src/rsa.c @@ -92,7 +92,6 @@ RSA keys can be used to encrypt, decrypt, sign and verify data. * RSA_LOW_MEM: Non-CRT private ops, less memory default: off * WC_RSA_NONBLOCK: Non-blocking RSA operations default: off * WC_RSA_NONBLOCK_TIME: Time-based non-blocking RSA default: off - * WOLFSSL_MP_INVMOD_CONSTANT_TIME: Constant-time modular inverse default: off * WC_RSA_NO_FERMAT_CHECK: Skip Fermat factorization check on default: off * key generation (p and q closeness) * @@ -6082,15 +6081,11 @@ int wc_MakeRsaKey(RsaKey* key, int size, long e, WC_RNG* rng) err = mp_mod(&key->d, tmp1, &key->dP); if (err == MP_OKAY) /* key->dQ = d mod(q-1) */ err = mp_mod(&key->d, tmp2, &key->dQ); -#ifdef WOLFSSL_MP_INVMOD_CONSTANT_TIME - if (err == MP_OKAY) /* key->u = 1/q mod p */ - err = mp_invmod(q, p, &key->u); -#else + /* key->u = 1/q mod p = q^(p-2) mod p */ if (err == MP_OKAY) err = mp_sub_d(p, 2, tmp3); - if (err == MP_OKAY) /* key->u = 1/q mod p = q^p-2 mod p */ + if (err == MP_OKAY) err = mp_exptmod(q, tmp3, p, &key->u); -#endif if (err == MP_OKAY) err = mp_copy(p, &key->p); if (err == MP_OKAY) From 54b10eb49a48a34740f9dae1b1f3db4f3e8b55bb Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 12 Aug 2026 10:28:33 -0600 Subject: [PATCH 02/10] F-7097: Route secret ECC scalars around the fixed-point cache --- .github/configs/os-check-linux.json | 5 +- tests/api/test_ecc.c | 77 ++++++++++++++++ tests/api/test_ecc.h | 2 + tests/unit-mcdc/test_ecc_fault_whitebox.c | 15 +++- tests/unit-mcdc/test_ecc_whitebox.c | 105 +++++++++++++++++++++- wolfcrypt/src/ecc.c | 52 ++++++++++- 6 files changed, 244 insertions(+), 12 deletions(-) diff --git a/.github/configs/os-check-linux.json b/.github/configs/os-check-linux.json index 2dcbdfba8d2..08b8753391d 100644 --- a/.github/configs/os-check-linux.json +++ b/.github/configs/os-check-linux.json @@ -601,5 +601,8 @@ "configure": ["--enable-leanpsk"], "extra_cflags": "-Wdeclaration-after-statement -Wnull-dereference -DTEST_LIBWOLFSSL_SOURCES_INCLUSION_SEQUENCE -Werror=implicit-function-declaration -Wno-error=cpp", "check": false, - "run": [["./wolfcrypt/test/testwolfcrypt"]]} + "run": [["./wolfcrypt/test/testwolfcrypt"]]}, +{"name": "fpecc-no-shamir", + "comment": "FP_ECC with ECC_TIMING_RESISTANT but no ECC_SHAMIR: nothing consumes the fixed-point cache, so its table, lock and helpers are compiled out.", + "configure": ["--enable-fpecc", "--disable-eccshamir"]} ] diff --git a/tests/api/test_ecc.c b/tests/api/test_ecc.c index b3936ec0545..ef5e1940897 100644 --- a/tests/api/test_ecc.c +++ b/tests/api/test_ecc.c @@ -1731,6 +1731,83 @@ int test_wc_ecc_import_private_key(void) return EXPECT_RESULT(); } /* END test_wc_ecc_import_private_key */ +/* + * Testing wc_ecc_make_pub_ex() with private key d = n - 1 on secp256k1, whose + * public key is -G. The timing-resistant ladder ends by adding two points + * that are negatives of each other; the result must still be -G, including + * when FP_ECC routes the secret scalar around its cache. + */ +int test_wc_ecc_make_pub_order_minus_1(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ECC) && defined(HAVE_ECC_KOBLITZ) && \ + defined(HAVE_ECC_KEY_IMPORT) && defined(HAVE_ECC_KEY_EXPORT) && \ + !defined(WC_NO_RNG) && !defined(NO_ECC256) && \ + !defined(HAVE_SELFTEST) && !defined(NO_ECC_MAKE_PUB) && \ + !defined(WOLFSSL_NO_ECC_SW) && !defined(WOLF_CRYPTO_CB_ONLY_ECC) && \ + !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A) && \ + !defined(WOLFSSL_MICROCHIP_TA100) && !defined(WOLFSSL_CRYPTOCELL) && \ + !defined(WOLFSSL_KCAPI_ECC) && !defined(WOLFSSL_SILABS_SE_ACCEL) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(6,0,0)) + ecc_key key; + WC_RNG rng; + /* secp256k1 order n, minus 1. */ + static const byte d[32] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, + 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x40 + }; + /* -G = (Gx, p - Gy). */ + static const byte expX[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 byte expY[32] = { + 0xB7, 0xC5, 0x25, 0x88, 0xD9, 0x5C, 0x3B, 0x9A, + 0xA2, 0x5B, 0x04, 0x03, 0xF1, 0xEE, 0xF7, 0x57, + 0x02, 0xE8, 0x4B, 0xB7, 0x59, 0x7A, 0xAB, 0xE6, + 0x63, 0xB8, 0x2F, 0x6F, 0x04, 0xEF, 0x27, 0x77 + }; + byte qx[32]; + byte qy[32]; + word32 qxSz = (word32)sizeof(qx); + word32 qySz = (word32)sizeof(qy); + int i; + + XMEMSET(&key, 0, sizeof(ecc_key)); + XMEMSET(&rng, 0, sizeof(WC_RNG)); + + ExpectIntEQ(wc_ecc_init(&key), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_ecc_import_private_key_ex(d, (word32)sizeof(d), NULL, 0, + &key, ECC_SECP256K1), 0); + + /* Repeat so an FP_ECC build would have warmed its cache for G. */ + for (i = 0; (i < 3) && EXPECT_SUCCESS(); i++) { + PRIVATE_KEY_UNLOCK(); + ExpectIntEQ(wc_ecc_make_pub_ex(&key, NULL, &rng), 0); + PRIVATE_KEY_LOCK(); + qxSz = (word32)sizeof(qx); + qySz = (word32)sizeof(qy); + ExpectIntEQ(wc_ecc_export_public_raw(&key, qx, &qxSz, qy, &qySz), 0); + ExpectIntEQ(qxSz, sizeof(expX)); + ExpectIntEQ(qySz, sizeof(expY)); + ExpectBufEQ(qx, expX, sizeof(expX)); + ExpectBufEQ(qy, expY, sizeof(expY)); + } + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_ecc_free(&key); +#ifdef FP_ECC + wc_ecc_fp_free(); +#endif +#endif + return EXPECT_RESULT(); +} /* END test_wc_ecc_make_pub_order_minus_1 */ + /* * Testing wc_ecc_export_private_only() */ diff --git a/tests/api/test_ecc.h b/tests/api/test_ecc.h index 17b463b2720..be1c7e77310 100644 --- a/tests/api/test_ecc.h +++ b/tests/api/test_ecc.h @@ -49,6 +49,7 @@ int test_wc_ecc_import_x963_off_curve(void); int test_wc_ecc_import_x963_oversized(void); int test_wc_ecc_import_x963_non_canonical(void); int test_wc_ecc_import_private_key(void); +int test_wc_ecc_make_pub_order_minus_1(void); int test_wc_ecc_export_private_only(void); int test_wc_ecc_rs_to_sig(void); int test_wc_ecc_import_raw(void); @@ -114,6 +115,7 @@ int test_wc_ecc_make_key_id_dev(void); TEST_DECL_GROUP("ecc", test_wc_ecc_import_x963_oversized), \ TEST_DECL_GROUP("ecc", test_wc_ecc_import_x963_non_canonical), \ TEST_DECL_GROUP("ecc", test_wc_ecc_import_private_key), \ + TEST_DECL_GROUP("ecc", test_wc_ecc_make_pub_order_minus_1), \ TEST_DECL_GROUP("ecc", test_wc_ecc_export_private_only), \ TEST_DECL_GROUP("ecc", test_wc_ecc_rs_to_sig), \ TEST_DECL_GROUP("ecc", test_wc_ecc_import_raw), \ diff --git a/tests/unit-mcdc/test_ecc_fault_whitebox.c b/tests/unit-mcdc/test_ecc_fault_whitebox.c index 44f7f1676e5..c5e9dbd8363 100644 --- a/tests/unit-mcdc/test_ecc_fault_whitebox.c +++ b/tests/unit-mcdc/test_ecc_fault_whitebox.c @@ -321,7 +321,8 @@ static void wb_fault_sqrtmod_prime_direct(void) * leave a poisoned cache behind for the vector after it. * ========================================================================= */ #if defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) && \ - !defined(WOLFSSL_NO_MALLOC) + !defined(WOLFSSL_NO_MALLOC) && \ + (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) typedef struct wb_fp_ctx { mp_int a; @@ -535,6 +536,7 @@ static void wb_fp_build_lut_and_mul(wb_fp_ctx* c) wc_ecc_fp_free(); if (add_entry(0, c->G) == MP_OKAY && build_lut(0, &c->a, &c->prime, mp, &c->mu) == MP_OKAY) { + #ifndef ECC_TIMING_RESISTANT /* accepting row first, then the fault sweep over the LUT-copy chain */ (void)accel_fp_mul(0, &c->k, c->R, &c->a, &c->prime, mp, 1); for (n = 1; n <= 8; n++) { @@ -542,6 +544,7 @@ static void wb_fp_build_lut_and_mul(wb_fp_ctx* c) (void)accel_fp_mul(0, &c->k, c->R, &c->a, &c->prime, mp, 1); mcdc_fm_disarm(); } + #endif } else { wb_fail = 1; @@ -569,6 +572,7 @@ static void wb_fp_lru_and_lutset(wb_fp_ctx* c) * (guard TRUE, builds it) and lru 3 with the LUT set (guard FALSE on * LUT_set). */ +#ifndef ECC_TIMING_RESISTANT /* --- wc_ecc_mulmod_ex --- */ wc_ecc_fp_free(); for (i = 0; i < 3; i++) @@ -596,6 +600,7 @@ static void wb_fp_lru_and_lutset(wb_fp_ctx* c) else { wb_fail = 1; } +#endif #ifdef ECC_SHAMIR /* --- ecc_mul2add: warmed through itself, so both LUT-build guards see @@ -652,6 +657,7 @@ static void wb_fp_lru_and_lutset(wb_fp_ctx* c) (void)n; #endif +#ifndef ECC_TIMING_RESISTANT /* --- err == MP_OKAY FALSE halves (armed add_entry) --- */ wc_ecc_fp_free(); mcdc_fm_arm(1); @@ -663,6 +669,7 @@ static void wb_fp_lru_and_lutset(wb_fp_ctx* c) (void)wc_ecc_mulmod_ex2(&c->k, c->G, c->R, &c->a, &c->prime, &c->order, NULL, 1, NULL); mcdc_fm_disarm(); +#endif #ifdef ECC_SHAMIR wc_ecc_fp_free(); @@ -722,9 +729,11 @@ static void wb_fp_cache_suite(void) #else static void wb_fp_cache_suite(void) { - WB_NOTE("FP_ECC off (or no-malloc build); fixed-point cache suite skipped"); + WB_NOTE("FP_ECC off (or no-malloc, or timing-resistant without Shamir); " + "fixed-point cache suite skipped"); } -#endif /* FP_ECC && !WOLFSSL_SP_MATH && !WOLFSSL_NO_MALLOC */ +#endif /* FP_ECC && !WOLFSSL_SP_MATH && !WOLFSSL_NO_MALLOC && + * (!ECC_TIMING_RESISTANT || ECC_SHAMIR) */ /* ========================================================================= * Degenerate point operands. diff --git a/tests/unit-mcdc/test_ecc_whitebox.c b/tests/unit-mcdc/test_ecc_whitebox.c index c87c8bb6402..eca233d9621 100644 --- a/tests/unit-mcdc/test_ecc_whitebox.c +++ b/tests/unit-mcdc/test_ecc_whitebox.c @@ -1113,8 +1113,10 @@ static void wb_projective_safe_special_cases(void) } /* find_hole()/find_base()/add_entry() and fp_cache[] itself only exist - * inside ecc.c's own #ifdef FP_ECC (+ !WOLFSSL_SP_MATH) block. */ -#if defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) + * inside ecc.c's own #ifdef FP_ECC (+ !WOLFSSL_SP_MATH) block, and only when + * something consumes the cache (!ECC_TIMING_RESISTANT or ECC_SHAMIR). */ +#if defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) && \ + (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) /* ------------------------------------------------------------------------- * * Class 17: FP_ECC fixed-point cache internals: find_base() (line ~13585), * find_hole() (line ~13548). Both are file-static and process-global @@ -1196,9 +1198,103 @@ static void wb_fp_cache_internals(void) #else static void wb_fp_cache_internals(void) { - WB_NOTE("FP_ECC off (or WOLFSSL_SP_MATH); fp_cache internals skipped"); + WB_NOTE("FP_ECC off (or WOLFSSL_SP_MATH, or timing-resistant without " + "Shamir); fp_cache internals skipped"); } -#endif /* FP_ECC && !WOLFSSL_SP_MATH */ +#endif /* FP_ECC && !WOLFSSL_SP_MATH && + * (!ECC_TIMING_RESISTANT || ECC_SHAMIR) */ + +#if defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) && \ + defined(ECC_TIMING_RESISTANT) && defined(ECC_SHAMIR) +/* ------------------------------------------------------------------------- * + * Timing-resistant wc_ecc_mulmod_ex()/wc_ecc_mulmod_ex2() take secret + * scalars, and the FP-cache LUT is not constant-time, so they must never + * add, warm or consume a cache entry. Three calls each on one base point + * would have built a LUT (lru_count >= 2) on the cached path; the cache must + * still be empty afterwards. + * ------------------------------------------------------------------------- */ +static void wb_fp_cache_tr_bypass(void) +{ + int idx = wc_ecc_get_curve_idx(ECC_SECP256R1); + const ecc_set_type* cs; + ecc_point *G = NULL, *R = NULL; + mp_int k, a, prime, order; + unsigned x; + int i; + int touched = 0; + + if (idx == ECC_CURVE_INVALID) { + wb_fail = 1; + return; + } + cs = wc_ecc_get_curve_params(idx); + if (cs == NULL) { + wb_fail = 1; + return; + } + + wc_ecc_fp_free(); /* deterministic empty-cache start */ + + if (mp_init_multi(&k, &a, &prime, &order, NULL, NULL) != MP_OKAY) { + wb_fail = 1; + return; + } + G = wc_ecc_new_point(); + R = wc_ecc_new_point(); + if ((G == NULL) || (R == NULL) || + (mp_read_radix(&a, cs->Af, MP_RADIX_HEX) != MP_OKAY) || + (mp_read_radix(&prime, cs->prime, MP_RADIX_HEX) != MP_OKAY) || + (mp_read_radix(&order, cs->order, MP_RADIX_HEX) != MP_OKAY) || + (mp_read_radix(G->x, cs->Gx, MP_RADIX_HEX) != MP_OKAY) || + (mp_read_radix(G->y, cs->Gy, MP_RADIX_HEX) != MP_OKAY) || + (mp_set(G->z, 1) != MP_OKAY) || + (mp_set(&k, 0x1234567) != MP_OKAY)) { + wb_fail = 1; + goto out; + } + + for (i = 0; i < 3; i++) { + if (wc_ecc_mulmod_ex(&k, G, R, &a, &prime, 1, NULL) != MP_OKAY) { + WB_NOTE("timing-resistant wc_ecc_mulmod_ex failed"); + wb_fail = 1; + } + if (wc_ecc_mulmod_ex2(&k, G, R, &a, &prime, &order, NULL, 1, + NULL) != MP_OKAY) { + WB_NOTE("timing-resistant wc_ecc_mulmod_ex2 failed"); + wb_fail = 1; + } + } + + if (find_base(G) != -1) { + touched = 1; + } + for (x = 0; x < FP_ENTRIES; x++) { + if (fp_cache[x].g != NULL) { + touched = 1; + } + } + if (touched) { + WB_NOTE("timing-resistant mulmod touched the fp_cache"); + wb_fail = 1; + } + WB_NOTE("timing-resistant mulmod fp_cache bypass exercised"); + +out: + wc_ecc_del_point(G); + wc_ecc_del_point(R); + mp_clear(&k); + mp_clear(&a); + mp_clear(&prime); + mp_clear(&order); + wc_ecc_fp_free(); +} +#else +static void wb_fp_cache_tr_bypass(void) +{ + WB_NOTE("FP_ECC off (or WOLFSSL_SP_MATH, or not timing-resistant with " + "Shamir); fp_cache bypass check skipped"); +} +#endif /* FP_ECC && !WOLFSSL_SP_MATH && ECC_TIMING_RESISTANT && ECC_SHAMIR */ #ifdef HAVE_ECC_KEY_EXPORT /* ------------------------------------------------------------------------- * @@ -3855,6 +3951,7 @@ int main(void) wb_mul2add_and_bufsize(); wb_projective_safe_special_cases(); wb_fp_cache_internals(); + wb_fp_cache_tr_bypass(); wb_export_point_der(); wb_export_x963_internal(); wb_idx_dp_guard_export_paths(); diff --git a/wolfcrypt/src/ecc.c b/wolfcrypt/src/ecc.c index 312753f8008..caa1c260569 100644 --- a/wolfcrypt/src/ecc.c +++ b/wolfcrypt/src/ecc.c @@ -13428,6 +13428,10 @@ typedef struct { /* permitted (0) or not (1) */ } fp_cache_t; +#if !defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR) +/* The cache, its lock, the LUT table and the cache-entry helpers below are + * only compiled when something can consume the FP cache: the variable-time + * path, or Shamir's trick in ecc_mul2add(). */ /* if HAVE_THREAD_LS this cache is per thread, no locking needed */ static THREAD_LS_T fp_cache_t fp_cache[FP_ENTRIES]; @@ -14064,9 +14068,11 @@ static int add_entry(int idx, ecc_point *g) return MP_OKAY; } +#endif /* !ECC_TIMING_RESISTANT || ECC_SHAMIR */ #endif #if !defined(WOLFSSL_SP_MATH) +#if !defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR) /* build the LUT by spacing the bits of the input by #modulus/FP_LUT bits apart * * The algorithm builds patterns in increasing bit order by first making all @@ -14223,8 +14229,11 @@ static int build_lut(int idx, mp_int* a, mp_int* modulus, mp_digit mp, return err; } +#endif /* !ECC_TIMING_RESISTANT || ECC_SHAMIR */ -/* perform a fixed point ECC mulmod */ +#ifndef ECC_TIMING_RESISTANT +/* perform a fixed point ECC mulmod. Not constant-time; do not use with + * secret scalars. */ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, mp_int* modulus, mp_digit mp, int map) { @@ -14409,6 +14418,7 @@ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, return err; } +#endif /* !ECC_TIMING_RESISTANT */ #endif #ifdef ECC_SHAMIR @@ -14867,6 +14877,20 @@ int wc_ecc_mulmod_ex(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, mp_int* modulus, int map, void* heap) { #if !defined(WOLFSSL_SP_MATH) +#ifdef ECC_TIMING_RESISTANT + if (k == NULL || G == NULL || R == NULL || a == NULL || modulus == NULL) { + return ECC_BAD_ARG_E; + } + + /* k can't have more bits than modulus count plus 1 */ + if (mp_count_bits(k) > mp_count_bits(modulus) + 1) { + return ECC_OUT_OF_RANGE_E; + } + + /* The FP-cache LUT is not constant-time, so secret scalars bypass the + * cache and its lock entirely. */ + return normal_ecc_mulmod(k, G, R, a, modulus, NULL, map, heap); +#else int idx, err = MP_OKAY; mp_digit mp = 0; WC_DECLARE_VAR(mu, mp_int, 1, 0); @@ -14972,6 +14996,7 @@ int wc_ecc_mulmod_ex(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, WC_FREE_VAR_EX(mu, NULL, DYNAMIC_TYPE_ECC_BUFFER); return err; +#endif /* ECC_TIMING_RESISTANT */ #else /* WOLFSSL_SP_MATH */ @@ -15030,6 +15055,21 @@ int wc_ecc_mulmod_ex2(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, mp_int* modulus, mp_int* order, WC_RNG* rng, int map, void* heap) { #if !defined(WOLFSSL_SP_MATH) +#ifdef ECC_TIMING_RESISTANT + if (k == NULL || G == NULL || R == NULL || a == NULL || modulus == NULL || + order == NULL) { + return ECC_BAD_ARG_E; + } + + /* k can't have more bits than order */ + if (mp_count_bits(k) > mp_count_bits(order)) { + return ECC_OUT_OF_RANGE_E; + } + + /* The FP-cache LUT is not constant-time, so secret scalars bypass the + * cache and its lock entirely. */ + return normal_ecc_mulmod(k, G, R, a, modulus, rng, map, heap); +#else int idx, err = MP_OKAY; mp_digit mp = 0; WC_DECLARE_VAR(mu, mp_int, 1, 0); @@ -15136,6 +15176,7 @@ int wc_ecc_mulmod_ex2(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, WC_FREE_VAR_EX(mu, NULL, DYNAMIC_TYPE_ECC_BUFFER); return err; +#endif /* ECC_TIMING_RESISTANT */ #else /* WOLFSSL_SP_MATH */ @@ -15183,7 +15224,8 @@ int wc_ecc_mulmod_ex2(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, #endif /* WOLFSSL_SP_MATH */ } -#if !defined(WOLFSSL_SP_MATH) +#if !defined(WOLFSSL_SP_MATH) && \ + (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) /* helper function for freeing the cache ... must be called with the cache mutex locked */ static void wc_ecc_fp_free_cache(void) @@ -15210,7 +15252,8 @@ static void wc_ecc_fp_free_cache(void) /** Init the Fixed Point cache */ void wc_ecc_fp_init(void) { -#ifndef WOLFSSL_SP_MATH +#if !defined(WOLFSSL_SP_MATH) && \ + (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) #ifndef HAVE_THREAD_LS #ifndef WOLFSSL_MUTEX_INITIALIZER /* Losing the election is fine - the winner finishes the init. */ @@ -15225,7 +15268,8 @@ void wc_ecc_fp_init(void) WOLFSSL_ABI void wc_ecc_fp_free(void) { -#if !defined(WOLFSSL_SP_MATH) +#if !defined(WOLFSSL_SP_MATH) && \ + (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) #ifndef HAVE_THREAD_LS #ifndef WOLFSSL_MUTEX_INITIALIZER /* extra sanity check if wolfCrypt_Init not called */ From 9c59761474348b2c8ddb9b81466ccccca7558c62 Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 7 Oct 2026 15:51:53 -0600 Subject: [PATCH 03/10] F-7098: Blind the ECCSI signing inverse --- tests/unit-mcdc/test_eccsi_fault_whitebox.c | 65 +++++++++++++++++++++ wolfcrypt/src/eccsi.c | 28 ++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/tests/unit-mcdc/test_eccsi_fault_whitebox.c b/tests/unit-mcdc/test_eccsi_fault_whitebox.c index 6738d454722..420f7f5fca1 100644 --- a/tests/unit-mcdc/test_eccsi_fault_whitebox.c +++ b/tests/unit-mcdc/test_eccsi_fault_whitebox.c @@ -149,6 +149,51 @@ MCDC_FM_MAYBE_UNUSED static int wb_ea_addmod(const mp_int* a, const mp_int* b, #undef mp_addmod #define mp_addmod(a, b, c, d) wb_ea_addmod((a), (b), (c), (d)) +/* -------------------------------------------------------------------------- + * Value-forcing wc_ecc_gen_k() interposer for wc_SignEccsiHash()'s blinding + * factor loop: + * + * do { if (++loop_check > ECCSI_MAX_GEN_COUNT) { + * err = RNG_FAILURE_E; break; } + * err = wc_ecc_gen_k(rng, (int)sz, &key->tmp, &key->params.order); } + * while (err == WC_NO_ERR_TRACE(MP_ZERO_E)); + * + * wc_ecc_gen_k() returns MP_ZERO_E only for a zero draw, a ~2^-256 event, so + * neither the retry nor the ECCSI_MAX_GEN_COUNT cap is reached on real + * entropy. eccsi.c has one wc_ecc_gen_k() call site, so no disambiguation is + * needed. + * + * ZERO is ONE-SHOT, so the retry draws a genuine factor and the signature + * completes. ZERO_ALL keeps failing until disarmed; the ECCSI_MAX_GEN_COUNT + * cap bounds it. + * + * ecc.h is included first so its prototype is seen under the real name. + * ----------------------------------------------------------------------- */ +#include + +#define WB_GK_OFF 0 /* pass through */ +#define WB_GK_ZERO 1 /* report one zero draw */ +#define WB_GK_ZERO_ALL 2 /* report a zero draw every time */ + +static int wb_gk_mode = WB_GK_OFF; + +MCDC_FM_MAYBE_UNUSED static int wb_gk_gen_k(WC_RNG* rng, int size, mp_int* k, + mp_int* order) +{ + int mode = wb_gk_mode; + + if (mode == WB_GK_ZERO) { + wb_gk_mode = WB_GK_OFF; /* one-shot */ + } + if (mode != WB_GK_OFF) { + return MP_ZERO_E; + } + return wc_ecc_gen_k(rng, size, k, order); +} + +#undef wc_ecc_gen_k +#define wc_ecc_gen_k(r, s, k, o) wb_gk_gen_k((r), (s), (k), (o)) + #include #include "mcdc_fault_alloc.h" @@ -301,6 +346,26 @@ static void wb_gen_sig_reject(WC_RNG* rng) wb_ea_mode = WB_EA_OFF; printf(" [wb] gen_sig reject mode %d -> %d\n", mode, e); } + + /* Blinding factor loop: one zero draw retries and signs; zero draws + * every time stop at the cap with RNG_FAILURE_E. */ + for (mode = WB_GK_ZERO; mode <= WB_GK_ZERO_ALL; mode++) { + byte sg[257]; + word32 z = (word32)sizeof(sg); + int e; + + XMEMSET(sg, 0, sizeof(sg)); + wb_gk_mode = mode; + e = wc_SignEccsiHash(&k, rng, WC_HASH_TYPE_SHA256, hash, + WC_SHA256_DIGEST_SIZE, sg, &z); + wb_gk_mode = WB_GK_OFF; + printf(" [wb] sign blind gen_k mode %d -> %d\n", mode, e); + if (e != ((mode == WB_GK_ZERO) ? 0 : + WC_NO_ERR_TRACE(RNG_FAILURE_E))) { + WB_NOTE("sign blind gen_k: unexpected result"); + wb_fail = 1; + } + } } mp_free(&ssk); diff --git a/wolfcrypt/src/eccsi.c b/wolfcrypt/src/eccsi.c index 95cab2236cf..60000ce8289 100644 --- a/wolfcrypt/src/eccsi.c +++ b/wolfcrypt/src/eccsi.c @@ -2034,14 +2034,38 @@ int wc_SignEccsiHash(EccsiKey* key, WC_RNG* rng, enum wc_HashType hashType, err = eccsi_gen_sig(key, rng, hashType, msg, msgSz, r, s); } - /* Step 5: s' = ( (( HE + r * SSK )^-1) * j ) modulo q, erase j */ + /* Step 5: s' = ( (( HE + r * SSK )^-1) * j ) modulo q, erase j + * Invert with multiplicative blinding for constant-time operation. */ + if (err == 0) { + int loop_check = 0; + do { + if (++loop_check > ECCSI_MAX_GEN_COUNT) { + err = RNG_FAILURE_E; + break; + } + err = wc_ecc_gen_k(rng, (int)sz, &key->tmp, &key->params.order); + } + while (err == WC_NO_ERR_TRACE(MP_ZERO_E)); + } + /* Blind: s = (s * b) mod order */ + if (err == 0) { + err = mp_mulmod(s, &key->tmp, &key->params.order, s); + } + /* Invert: s = (s * b)^-1 mod order */ if (err == 0) { err = mp_invmod(s, &key->params.order, s); } + /* Unblind: s = (s * b)^-1 * b mod order = s^-1 mod order */ + if (err == 0) { + err = mp_mulmod(s, &key->tmp, &key->params.order, s); + } if (err == 0) { j = wc_ecc_key_get_priv(&key->pubkey); err = mp_mulmod(s, j, &key->params.order, s); - /* Erase j on the failure path too. */ + } + /* Erase blinding factor and j on every path past argument checks. */ + if (s != NULL) { + mp_forcezero(&key->tmp); ecc_forcezero_k(&key->pubkey); } if (err == 0) { From cfbb724fe8da15524f201fdd054278839e7dc43e Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 7 Oct 2026 15:52:34 -0600 Subject: [PATCH 04/10] F-7099: Blind the SAKKE RSK inverse --- .github/configs/os-check-linux.json | 6 +- CMakeLists.txt | 3 + configure.ac | 8 +- .../header_files-ja/doxygen_groups.h | 4 +- doc/dox_comments/header_files-ja/sakke.h | 5 + .../header_files/doxygen_groups.h | 4 +- doc/dox_comments/header_files/sakke.h | 5 + tests/api/test_sakke.c | 75 +++++--- tests/unit-mcdc/test_sakke_fault_whitebox.c | 167 ++++++++++------- tests/unit-mcdc/test_sakke_whitebox.c | 88 ++++++--- wolfcrypt/benchmark/benchmark.c | 8 +- wolfcrypt/src/sakke.c | 168 ++++++++++++++---- wolfcrypt/src/sp_int.c | 2 +- wolfssl/wolfcrypt/sakke.h | 2 + 14 files changed, 380 insertions(+), 165 deletions(-) diff --git a/.github/configs/os-check-linux.json b/.github/configs/os-check-linux.json index 08b8753391d..13bee57af74 100644 --- a/.github/configs/os-check-linux.json +++ b/.github/configs/os-check-linux.json @@ -604,5 +604,9 @@ "run": [["./wolfcrypt/test/testwolfcrypt"]]}, {"name": "fpecc-no-shamir", "comment": "FP_ECC with ECC_TIMING_RESISTANT but no ECC_SHAMIR: nothing consumes the fixed-point cache, so its table, lock and helpers are compiled out.", - "configure": ["--enable-fpecc", "--disable-eccshamir"]} + "configure": ["--enable-fpecc", "--disable-eccshamir"]}, +{"name": "sp-math-sakke", + "comment": "SAKKE on WOLFSSL_SP_MATH without RSA, DH, or ECCSI: its blinded RSK inverse needs sp_mulmod, which nothing else builds in this shape.", + "configure": ["--enable-cryptonly", "--enable-sp-math", "--enable-sp", + "--disable-rsa", "--disable-dh", "--enable-sakke"]} ] diff --git a/CMakeLists.txt b/CMakeLists.txt index 534714847a1..ec32d13ed50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2040,6 +2040,9 @@ if(WOLFSSL_SAKKE) if (NOT WOLFSSL_ECC) message(FATAL_ERROR "cannot enable SAKKE without enabling ECC.") endif() + if (NOT WOLFSSL_RNG) + message(FATAL_ERROR "cannot enable SAKKE without enabling RNG.") + endif() list(APPEND WOLFSSL_DEFINITIONS "-DWOLFCRYPT_HAVE_SAKKE") endif() diff --git a/configure.ac b/configure.ac index 8504097da7f..66116b7f299 100644 --- a/configure.ac +++ b/configure.ac @@ -1959,7 +1959,8 @@ then test "$enable_aessiv" = "" && enable_aessiv=yes # AFALG lacks AES-EAX test "$enable_aeseax" = "" && test "$enable_afalg" != "yes" && enable_aeseax=yes - test "$enable_sakke" = "" && test "$enable_ecc" != "no" && enable_sakke=yes + test "$enable_sakke" = "" && test "$enable_ecc" != "no" && \ + test "$enable_rng" != "no" && enable_sakke=yes test "$enable_atomicuser" = "" && enable_atomicuser=yes test "$enable_cryptocb" = "" && enable_cryptocb=yes test "$enable_pkcallbacks" = "" && enable_pkcallbacks=yes @@ -6721,6 +6722,11 @@ then AC_MSG_ERROR([SAKKE requires ECC.]) fi +if test "$ENABLED_SAKKE" != "no" && test "$ENABLED_RNG" = "no" +then + AC_MSG_ERROR([--enable-sakke requires --enable-rng]) +fi + if test "x$ENABLED_SAKKE" = "xsmall" then ENABLED_SAKKE="yes" diff --git a/doc/dox_comments/header_files-ja/doxygen_groups.h b/doc/dox_comments/header_files-ja/doxygen_groups.h index fa3bdd59bb8..4d807557780 100644 --- a/doc/dox_comments/header_files-ja/doxygen_groups.h +++ b/doc/dox_comments/header_files-ja/doxygen_groups.h @@ -170,7 +170,7 @@ -# wc_MakeSakkeKey(), wc_ExportSakkeKey(), wc_ExportSakkePublicKey() または -# wc_ImportSakkeKey() -# リクエストを待機: - -# クライアントのIDに基づいてRSKを作成: wc_MakeSakkeRsk() + -# クライアントのIDに基づいてRSKを作成: wc_MakeSakkeRsk_ex() -# クライアントへの転送のためにRSKをエンコード: wc_EncodeSakkeRsk() -# SAKKE鍵の解放: wc_FreeSakkeKey() @@ -231,7 +231,7 @@ これらの操作は、受信者秘密鍵(RSK)を作成、検証、エンコード、デコードします。 RSKは、SSVを導出するために必要です(wc_DeriveSakkeSSV()を参照)。\n - KMSで、クライアントのIDからRSKを作成(wc_MakeSakkeRsk())。\n + KMSで、クライアントのIDからRSKを作成(wc_MakeSakkeRsk_ex())。\n クライアントで、IDでRSKを検証(wc_ValidateSakkeRsk())。\n クライアントへの転送またはストレージのためにRSKをエンコード(wc_EncodeSakkeRsk())。\n 必要に応じてクライアントでRSKをデコード(wc_DecodeSakkeRsk())。\n diff --git a/doc/dox_comments/header_files-ja/sakke.h b/doc/dox_comments/header_files-ja/sakke.h index 6d7aa9e82c3..11c5d05c19d 100644 --- a/doc/dox_comments/header_files-ja/sakke.h +++ b/doc/dox_comments/header_files-ja/sakke.h @@ -30,6 +30,11 @@ int wc_MakeSakkeRsk(SakkeKey* key, const byte* id, word16 idSz, /*! \ingroup SAKKE_RSK */ +int wc_MakeSakkeRsk_ex(SakkeKey* key, const byte* id, word16 idSz, + ecc_point* rsk, WC_RNG* rng); +/*! + \ingroup SAKKE_RSK +*/ int wc_ValidateSakkeRsk(SakkeKey* key, const byte* id, word16 idSz, ecc_point* rsk, int* valid); /*! diff --git a/doc/dox_comments/header_files/doxygen_groups.h b/doc/dox_comments/header_files/doxygen_groups.h index e9ca1c2e2b2..2b34a6963cf 100644 --- a/doc/dox_comments/header_files/doxygen_groups.h +++ b/doc/dox_comments/header_files/doxygen_groups.h @@ -210,7 +210,7 @@ -# wc_MakeSakkeKey(), wc_ExportSakkeKey(), wc_ExportSakkePublicKey() or -# wc_ImportSakkeKey() -# Wait for request: - -# Make an RSK base on ID for the client: wc_MakeSakkeRsk() + -# Make an RSK base on ID for the client: wc_MakeSakkeRsk_ex() -# Encode RSK for transfer to client: wc_EncodeSakkeRsk() -# Free SAKKE Key: wc_FreeSakkeKey() @@ -271,7 +271,7 @@ These operations make, validate, encode and decode a Receiver Secret Key (RSK). An RSK is required to derive an SSV (see wc_DeriveSakkeSSV()).\n - On the KMS, make an RSK (wc_MakeSakkeRsk()) from the client's ID.\n + On the KMS, make an RSK (wc_MakeSakkeRsk_ex()) from the client's ID.\n On the client, validate the RSK (wc_ValidateSakkeRsk()) with the ID.\n Encode the RSK (wc_EncodeSakkeRsk()) to pass to client or for storage.\n Decode the RSK (wc_DecodeSakkeRsk()) on the client when needed.\n diff --git a/doc/dox_comments/header_files/sakke.h b/doc/dox_comments/header_files/sakke.h index cc812a5492e..9f26e5a8bef 100644 --- a/doc/dox_comments/header_files/sakke.h +++ b/doc/dox_comments/header_files/sakke.h @@ -30,6 +30,11 @@ int wc_MakeSakkeRsk(SakkeKey* key, const byte* id, word16 idSz, /*! \ingroup SAKKE_RSK */ +int wc_MakeSakkeRsk_ex(SakkeKey* key, const byte* id, word16 idSz, + ecc_point* rsk, WC_RNG* rng); +/*! + \ingroup SAKKE_RSK +*/ int wc_ValidateSakkeRsk(SakkeKey* key, const byte* id, word16 idSz, ecc_point* rsk, int* valid); /*! diff --git a/tests/api/test_sakke.c b/tests/api/test_sakke.c index 332c34100f5..23c8250a68f 100644 --- a/tests/api/test_sakke.c +++ b/tests/api/test_sakke.c @@ -50,61 +50,62 @@ * * Guards covered (operand order matches the source's left-to-right ||/&&): * - * wc_InitSakkeKey_ex() (sakke.c:119) key == NULL - * wc_FreeSakkeKey() (sakke.c:193) key != NULL - * wc_MakeSakkeKey() (sakke.c:507) key == NULL || rng == NULL - * wc_MakeSakkePublicKey (sakke.c:566) key == NULL || pub == NULL - * wc_ExportSakkeKey (sakke.c:604,608,612) + * wc_InitSakkeKey_ex() (sakke.c:132) key == NULL + * wc_FreeSakkeKey() (sakke.c:200) key != NULL + * wc_MakeSakkeKey() (sakke.c:552) key == NULL || rng == NULL + * wc_MakeSakkePublicKey (sakke.c:597) key == NULL || pub == NULL + * wc_ExportSakkeKey (sakke.c:635,639,643) * key == NULL || sz == NULL; data == NULL (LENGTH_ONLY_E); * *sz < 3*size (BUFFER_E) - * wc_ImportSakkeKey (sakke.c:659,662) + * wc_ImportSakkeKey (sakke.c:690,693) * key == NULL || data == NULL; sz != size*3 (BUFFER_E) - * wc_ExportSakkePrivateKey (sakke.c:713,717,721) -- same shape as + * wc_ExportSakkePrivateKey (sakke.c:747,751,755) -- same shape as * wc_ExportSakkeKey with size instead of 3*size - * wc_ImportSakkePrivateKey (sakke.c:756,760) -- same shape as + * wc_ImportSakkePrivateKey (sakke.c:790,794) -- same shape as * wc_ImportSakkeKey with size instead of size*3 - * wc_ExportSakkePublicKey (sakke.c:938,942) + * wc_ExportSakkePublicKey (sakke.c:975,979) * key == NULL || sz == NULL; data != NULL (drives sakke_z_from_mont()) - * wc_MakeSakkeRsk (sakke.c:976) key==NULL || id==NULL || rsk==NULL - * wc_EncodeSakkeRsk (sakke.c:1036) key==NULL || rsk==NULL || sz==NULL - * wc_ImportSakkePublicKey (sakke.c:1077,1089) + * wc_MakeSakkeRsk_ex (sakke.c:1055) key==NULL || id==NULL || rsk==NULL + * wc_MakeSakkeRsk -- same guard, via wc_MakeSakkeRsk_ex() with no RNG + * wc_EncodeSakkeRsk (sakke.c:1157) key==NULL || rsk==NULL || sz==NULL + * wc_ImportSakkePublicKey (sakke.c:1198,1210) * key==NULL || data==NULL; !trusted (drives wc_ecc_check_key()) - * wc_DecodeSakkeRsk (sakke.c:1119) key==NULL||data==NULL||rsk==NULL - * wc_ImportSakkeRsk (sakke.c:1151) key==NULL || data==NULL - * wc_GenerateSakkeRskTable (sakke.c:1319/1400 -- WOLFSSL_HAVE_SP_ECC has a + * wc_DecodeSakkeRsk (sakke.c:1240) key==NULL||data==NULL||rsk==NULL + * wc_ImportSakkeRsk (sakke.c:1272) key==NULL || data==NULL + * wc_GenerateSakkeRskTable (sakke.c:1440/1521 -- WOLFSSL_HAVE_SP_ECC has a * second, functionally-equivalent definition at the argument-validation * level) key==NULL || rsk==NULL || len==NULL; table==NULL * (LENGTH_ONLY_E); *len != 0 (BUFFER_E) - * wc_SetSakkeRsk (sakke.c:2350) key==NULL || rsk==NULL - * wc_ValidateSakkeRsk (sakke.c:2430) + * wc_SetSakkeRsk (sakke.c:2481) key==NULL || rsk==NULL + * wc_ValidateSakkeRsk (sakke.c:2561) * key==NULL||id==NULL||rsk==NULL||valid==NULL - * wc_GetSakkeAuthSize (sakke.c:2491) key==NULL || authSz==NULL - * wc_SetSakkeIdentity (sakke.c:6357) + * wc_GetSakkeAuthSize (sakke.c:2622) key==NULL || authSz==NULL + * wc_SetSakkeIdentity (sakke.c:6488) * key==NULL || id==NULL || idSz > SAKKE_ID_MAX_SIZE - * wc_MakeSakkePointI (sakke.c:6388) -- same shape as + * wc_MakeSakkePointI (sakke.c:6519) -- same shape as * wc_SetSakkeIdentity - * wc_GetSakkePointI (sakke.c:6428,6432,6436) + * wc_GetSakkePointI (sakke.c:6559,6563,6567) * key==NULL || sz==NULL; data==NULL (LENGTH_ONLY_E); * *sz < size*2 (BUFFER_E) - * wc_SetSakkePointI (sakke.c:6478,6481) + * wc_SetSakkePointI (sakke.c:6609,6612) * key==NULL||id==NULL||data==NULL; * idSz > SAKKE_ID_MAX_SIZE || sz != size*2 (BUFFER_E) - * wc_GenerateSakkePointITable (sakke.c:6527,6540,6544) + * wc_GenerateSakkePointITable (sakke.c:6658,6671,6675) * key==NULL || len==NULL; table==NULL (LENGTH_ONLY_E); * *len != 0 (BUFFER_E) - * wc_SetSakkePointITable (sakke.c:6575,6590) + * wc_SetSakkePointITable (sakke.c:6706,6721) * key==NULL || table==NULL; len != 0 (BUFFER_E) - * wc_ClearSakkePointITable (sakke.c:6616) key == NULL - * wc_MakeSakkeEncapsulatedSSV (sakke.c:6714,6717,6735,6738,6745) + * wc_ClearSakkePointITable (sakke.c:6747) key == NULL + * wc_MakeSakkeEncapsulatedSSV (sakke.c:6845,6848,6866,6869,6876) * key==NULL||ssv==NULL||authSz==NULL||ssvSz==0; * key->idSz == 0 (BAD_STATE_E); ssvSz > n; auth!=NULL && *authSzn); ssv==NULL (LENGTH_ONLY_E) - * wc_DeriveSakkeSSV (sakke.c:6897,6900,6911,6916) + * wc_DeriveSakkeSSV (sakke.c:7028,7031,7042,7047) * key==NULL||ssv==NULL||auth==NULL||ssvSz==0; * !key->rsk.set || key->idSz==0 (BAD_STATE_E); * authSz != 2*n+1; ssvSz > n; hashType (as above) @@ -146,6 +147,7 @@ int test_wc_Sakke_DecisionCoverage(void) SakkeKey key2; ecc_point* pub = NULL; ecc_point* rsk = NULL; + ecc_point* rsk2 = NULL; byte id[1] = { 0x00 }; byte idMax[SAKKE_ID_MAX_SIZE]; byte data[600]; @@ -173,6 +175,7 @@ int test_wc_Sakke_DecisionCoverage(void) ExpectIntEQ(wc_InitRng(&rng), 0); ExpectNotNull(rsk = wc_ecc_new_point()); + ExpectNotNull(rsk2 = wc_ecc_new_point()); ExpectNotNull(pub = wc_ecc_new_point()); /* --- wc_InitSakkeKey_ex() / wc_InitSakkeKey() --- */ @@ -212,6 +215,18 @@ int test_wc_Sakke_DecisionCoverage(void) WC_NO_ERR_TRACE(BAD_FUNC_ARG)); ExpectIntEQ(wc_MakeSakkeRsk(&key, id, 1, rsk), 0); + /* --- wc_MakeSakkeRsk_ex() --- */ + ExpectIntEQ(wc_MakeSakkeRsk_ex(NULL, id, 1, rsk2, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MakeSakkeRsk_ex(&key, NULL, 1, rsk2, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MakeSakkeRsk_ex(&key, id, 1, NULL, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* The blinding factor comes from the caller's RNG; the RSK itself is + * deterministic, so it must match the one made with a local RNG. */ + ExpectIntEQ(wc_MakeSakkeRsk_ex(&key, id, 1, rsk2, &rng), 0); + ExpectIntEQ(wc_ecc_cmp_point(rsk, rsk2), MP_EQ); + /* --- wc_ValidateSakkeRsk() --- */ ExpectIntEQ(wc_ValidateSakkeRsk(NULL, id, 1, rsk, &valid), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); @@ -630,6 +645,10 @@ int test_wc_Sakke_DecisionCoverage(void) wc_ecc_forcezero_point(rsk); wc_ecc_del_point(rsk); } + if (rsk2 != NULL) { + wc_ecc_forcezero_point(rsk2); + wc_ecc_del_point(rsk2); + } if (pub != NULL) { wc_ecc_del_point(pub); } diff --git a/tests/unit-mcdc/test_sakke_fault_whitebox.c b/tests/unit-mcdc/test_sakke_fault_whitebox.c index 4c694fee4f7..93ebb95279a 100644 --- a/tests/unit-mcdc/test_sakke_fault_whitebox.c +++ b/tests/unit-mcdc/test_sakke_fault_whitebox.c @@ -49,17 +49,17 @@ * llvm-cov attributes the coverage to sakke.c's own decisions. * * Targeted the uncovered-condition report residuals (err==0 FALSE half unless noted): - * 411 sakke_mulmod_base_add "(err==0) && map" (non-SP build only) - * 536 wc_MakeSakkeKey "(err==0) && mp_iszero(..)" cond 0 only; + * 423 sakke_mulmod_base_add "(err==0) && map" (non-SP build only) + * 524 sakke_rand_nonzero_mod_q "(err==0) && mp_iszero(..)" cond 0 only; * cond 1 (mp_iszero true, random scalar == 0) is crypto-unreachable. - * 1483 sakke_addmod "(err==0) && (mp_cmp!=MP_LT)" - * 1505/1508/1511 sakke_tplmod three sequential reductions, same shape - * 2282 sakke_pairing loop "(err==0) && (i>=0)" - * 2302 sakke_pairing "(err==0) && (i>0) && mp_is_bit_set(..)" - * 2454 wc_ValidateSakkeRsk "(err==0) && (idSz<=SAKKE_ID_MAX_SIZE)" - * 2646 sakke_modexp_loop "(err==0) && (i>=0)" - * 6289 sakke_hash_to_range "(err==0) && (i=0)" + * 2433 sakke_pairing "(err==0) && (i>0) && mp_is_bit_set(..)" + * 2585 wc_ValidateSakkeRsk "(err==0) && (idSz<=SAKKE_ID_MAX_SIZE)" + * 2777 sakke_modexp_loop "(err==0) && (i>=0)" + * 6420 sakke_hash_to_range "(err==0) && (i` halves of its - * big-integer chains (1490/1512/1515/1518 mp_cmp range checks, the 2653 - * bit-scan loop, the 543 key-generation retry). No mp_* call fails on a + * big-integer chains (1604/1626/1629/1632 mp_cmp range checks, the 2777 + * bit-scan loop, the 524 random-scalar retry). No mp_* call fails on a * healthy machine and the mp scratch is on the stack, so neither the ordinary * tests nor the heap-fault sweep below can drive `err == 0` FALSE there. * mcdc_fault_mp.h interposes the value-returning mp_* API for this TU only; @@ -92,23 +92,25 @@ #include "mcdc_fault_mp.h" /* -------------------------------------------------------------------------- - * Value-forcing mp_rand() interposer for the 543 master-secret retry loop. + * Value-forcing mp_rand() interposer for the 524 retry loop in + * sakke_rand_nonzero_mod_q(). * - * do { ... err = mp_rand(priv, digits, rng); - * err = mp_mod(priv, &key->params.q, priv); } - * while ((err == 0) && mp_iszero(wc_ecc_key_get_priv(&key->ecc))); + * do { ... err = mp_rand(r, digits, rng); + * err = mp_mod(r, &key->params.q, r); } + * while ((err == 0) && mp_iszero(r)); * * Both conditions need the loop to actually RETRY, which only happens when the * drawn scalar reduces to zero -- a ~2^-1024 event under real entropy, and one * a seeded RNG cannot force either, because mp_rand() is served by the sp_int * layer in a different TU (the seeded-RNG macro only rewrites call sites in the * TU it is included in). Interposing mp_rand() here is the one lever that - * reaches it: sakke.c has exactly ONE mp_rand() call site (line 536), so no - * disambiguation is needed. + * reaches it. sakke.c has one mp_rand() call site, shared by the master + * secret in wc_MakeSakkeKey() and the inverse's blinding factor in + * wc_MakeSakkeRsk_ex(); each is armed around its own entry point. * - * Modes are ONE-SHOT, so the loop's second iteration draws a genuine random - * scalar and the loop terminates on real data -- there is no retry loop here - * whose termination depends on a random draw going a particular way. + * ZERO and FAIL are ONE-SHOT, so the loop's second iteration draws a genuine + * random scalar and the loop terminates on real data. ZERO_ALL keeps returning + * zero so the loop reaches its SAKKE_MAX_GEN_COUNT cap; that cap bounds it. * * Ordering is the same load-bearing trick as mcdc_fault_mp.h: the wrapper is * compiled while mp_rand still names the real entry point, and only then is the @@ -117,6 +119,7 @@ #define WB_SR_OFF 0 /* pass through */ #define WB_SR_ZERO 1 /* succeed, but hand back the scalar 0 */ #define WB_SR_FAIL 2 /* fail the draw */ +#define WB_SR_ZERO_ALL 3 /* hand back 0 on every draw until disarmed */ static int wb_sr_mode = WB_SR_OFF; @@ -124,13 +127,13 @@ MCDC_FM_MAYBE_UNUSED static int wb_sr_rand(mp_int* a, int digits, WC_RNG* rng) { int mode = wb_sr_mode; - if (mode != WB_SR_OFF) { + if ((mode != WB_SR_OFF) && (mode != WB_SR_ZERO_ALL)) { wb_sr_mode = WB_SR_OFF; /* one-shot */ } if (mode == WB_SR_FAIL) { return MCDC_FM_ERR; } - if (mode == WB_SR_ZERO) { + if ((mode == WB_SR_ZERO) || (mode == WB_SR_ZERO_ALL)) { mp_zero(a); return 0; } @@ -197,7 +200,7 @@ static const word16 gIdSz = (word16)sizeof(gId); #ifndef SAKKE_K_VALIDATE #define SAKKE_K_VALIDATE 340 /* past the 261-alloc FP build into pairing, * so under WOLFSSL_SMALL_STACK the pairing - * accumulate-line faults flip 2282/2302 */ + * accumulate-line faults flip 2413/2433 */ #endif #ifndef SAKKE_K_ENCAP #define SAKKE_K_ENCAP 540 @@ -301,7 +304,8 @@ static void wb_mp_sweeps(SakkeKey* key, WC_RNG* rng, ecc_point* rsk) XMEMSET(auth2, 0, sizeof(auth2)); /* Fresh key per iteration: MakeSakkeKey mutates it, and its retry loop - * (543) plus the point-I/RSK derivation chains are what the sweep is for. */ + * (524) plus the point-I/RSK derivation chains are what the sweep is + * for. */ WB_MP_SWEEP("MakeSakkeKey", 150, { SakkeKey k2; @@ -476,8 +480,9 @@ int main(int argc, char** argv) * key internals. * ============================================================ */ - /* --- wc_MakeSakkeKey (536:0): the master-secret loop's mp_rand / - * mp_mod allocate; failing one sets err != 0 so the while's + /* --- wc_MakeSakkeKey (524:0, in sakke_rand_nonzero_mod_q): the + * master-secret loop's mp_rand / mp_mod allocate; failing one sets + * err != 0 so the while's * "(err==0) && mp_iszero(..)" exits on the err==0 false half. Also * drives the earlier "(err==0) && ..." chain in key generation. * Fresh init'd key per iteration (MakeSakkeKey overwrites state and a @@ -496,15 +501,16 @@ int main(int argc, char** argv) } WB_NOTE("wc_MakeSakkeKey fault sweep done"); - /* --- wc_MakeSakkeKey 543: the master-secret retry loop - * while ((err == 0) && mp_iszero(wc_ecc_key_get_priv(..))) + /* --- wc_MakeSakkeKey: the master-secret draw via the 524 loop + * while ((err == 0) && mp_iszero(r)) * Both conditions need the loop to actually RETRY, i.e. a drawn scalar * that reduces to zero -- a ~2^-1024 event that no heap fault and no * seeded RNG can produce (mp_rand() is served from another TU, so the * seeded-RNG macro never reaches it). The value-forcing mp_rand() - * interposer at the top of this file is the lever; sakke.c has exactly - * one mp_rand() call site, and both modes are ONE-SHOT so the second - * iteration draws real entropy and the loop terminates on real data. + * interposer at the top of this file is the lever; arming it around + * wc_MakeSakkeKey() alone selects the master secret, and both modes are + * ONE-SHOT so the second iteration draws real entropy and the loop + * terminates on real data. * * WB_SR_ZERO -> (T,T) decision TRUE, retries; the retry supplies the * accepting (T,F) row -> closes idx1 and gives idx0 @@ -529,6 +535,41 @@ int main(int argc, char** argv) } } + /* --- wc_MakeSakkeRsk_ex: the blinding-factor draw via the 524 loop + * if ((++genTryCnt) > SAKKE_MAX_GEN_COUNT) ... + * while ((err == 0) && mp_iszero(r)) + * Same lever as the master-secret draw above, armed around the RSK + * call alone so it selects the blinding draw: + * WB_SR_ZERO -> while TRUE, retries; the retry accepts + * WB_SR_FAIL -> while FALSE on err != 0 + * WB_SR_ZERO_ALL -> genTryCnt passes the cap -> RNG_FAILURE_E + * Only rsk is written, so the prepared key is reused. --- */ + { + int mode; + for (mode = WB_SR_ZERO; mode <= WB_SR_ZERO_ALL; mode++) { + ecc_point* rp = wc_ecc_new_point(); + if (rp != NULL) { + int e2; + wb_sr_mode = mode; + e2 = wc_MakeSakkeRsk(&key, gId, gIdSz, rp); + wb_sr_mode = WB_SR_OFF; + printf(" [wb] MakeSakkeRsk rand mode %d -> %d\n", + mode, e2); + if ((e2 == 0) != (mode == WB_SR_ZERO)) { + WB_NOTE("MakeSakkeRsk blind retry: unexpected result"); + wb_fail = 1; + } + if ((mode == WB_SR_ZERO_ALL) && + (e2 != WC_NO_ERR_TRACE(RNG_FAILURE_E))) { + WB_NOTE("MakeSakkeRsk blind cap: not RNG_FAILURE_E"); + wb_fail = 1; + } + wc_ecc_forcezero_point(rp); + wc_ecc_del_point(rp); + } + } + } + /* --- wc_MakeSakkeRsk: sweeps its own success chain. rsk output only; * key state (public key + master secret) untouched, so reuse key. * Fresh rsk point per iteration. --- */ @@ -546,7 +587,7 @@ int main(int argc, char** argv) WB_NOTE("wc_MakeSakkeRsk fault sweep done"); #ifndef WOLFSSL_HAVE_SP_ECC - /* --- sakke_mulmod_base_add (411): its wc_ecc_mulmod / + /* --- sakke_mulmod_base_add (423): its wc_ecc_mulmod / * mp_montgomery_setup / ecc_projective_add_point each allocate; * failing one sets err != 0 so "(err==0) && map" takes the err==0 * false half. map fixed at 1 (the value the existing whitebox could @@ -570,24 +611,24 @@ int main(int argc, char** argv) WB_NOTE("sakke_mulmod_base_add fault sweep done"); } - /* --- sakke_addmod (1483) / sakke_tplmod (1505/1508/1511): pure + /* --- sakke_addmod (1604) / sakke_tplmod (1626/1629/1632): pure * mp_int helpers. Their leading mp_add / mp_mul_d only allocate under * a heap-backed math backend; where they do, a fault sets err != 0 so * each "(err==0) && (mp_cmp != MP_LT)" reduction check takes its * err==0 false half. Where mp_add/mp_mul_d never allocate (fixed * sp_int), the sweep is a no-op and these stay justified residuals. */ { - /* sakke_tplmod 1518 idx0 (`err == 0` FALSE) -- the THIRD of the + /* sakke_tplmod 1632 idx0 (`err == 0` FALSE) -- the THIRD of the * helper's three sequential reductions: * - * err = mp_mul_d(a, 3, r); (1511) - * if ((err == 0) && (mp_cmp(r,m) != MP_LT)) sub; (1512) - * if ((err == 0) && (mp_cmp(r,m) != MP_LT)) sub; (1515) - * if ((err == 0) && (mp_cmp(r,m) != MP_LT)) sub; (1518) + * err = mp_mul_d(a, 3, r); (1625) + * if ((err == 0) && (mp_cmp(r,m) != MP_LT)) sub; (1626) + * if ((err == 0) && (mp_cmp(r,m) != MP_LT)) sub; (1629) + * if ((err == 0) && (mp_cmp(r,m) != MP_LT)) sub; (1632) * - * 1512 and 1515 get their err!=0 halves for free -- an earlier - * mp_sub failing carries err forward -- but 1518 needs the mp_sub - * at 1516 to have RUN and FAILED, which needs an input where the + * 1626 and 1629 get their err!=0 halves for free -- an earlier + * mp_sub failing carries err forward -- but 1632 needs the mp_sub + * at 1630 to have RUN and FAILED, which needs an input where the * first two reductions both fire. Every in-product caller passes * a < m, so 3a < 3m and the third check is only ever reached with * err == 0; a direct call on the file-static helper with a == m @@ -596,10 +637,10 @@ int main(int argc, char** argv) * The heap lever cannot do it: under a fixed sp_int backend * mp_sub() never allocates, so it never fails. mcdc_fault_mp.h's * lever can -- interposed mp_* call 1 is the mp_mul_d, 2 is the - * mp_sub at 1513 and 3 is the mp_sub at 1516, so arming index 3 - * fails exactly that one and 1518 is reached with err != 0. + * mp_sub at 1627 and 3 is the mp_sub at 1630, so arming index 3 + * fails exactly that one and 1632 is reached with err != 0. * - * The accepting (T,T) row at 1518 is the unarmed a == m call right + * The accepting (T,T) row at 1632 is the unarmed a == m call right * before it; the (T,F) row comes from the a=40,m=100 tplmod calls * in the sweep below (300 -> 200 -> 100 -> stop). */ { @@ -607,16 +648,16 @@ int main(int argc, char** argv) int e0, e1; mp_init(&a); mp_init(&m); mp_init(&r); mp_set(&a, 100); mp_set(&m, 100); - e1 = sakke_tplmod(&a, &m, &r); /* 1518 (T,T) */ + e1 = sakke_tplmod(&a, &m, &r); /* 1632 (T,T) */ mcdc_fm_arm(3); - e0 = sakke_tplmod(&a, &m, &r); /* 1518 (F,-) */ + e0 = sakke_tplmod(&a, &m, &r); /* 1632 (F,-) */ mcdc_fm_disarm(); - printf(" [wb] sakke_tplmod 1518 vectors: (T,T)=%d armed=%d\n", + printf(" [wb] sakke_tplmod 1632 vectors: (T,T)=%d armed=%d\n", e1, e0); mp_free(&a); mp_free(&m); mp_free(&r); } - /* sakke_addmod 1490 idx0, same shape but one reduction: a direct + /* sakke_addmod 1604 idx0, same shape but one reduction: a direct * armed call is the only way to reach it with err != 0. */ { mp_int a, b, m, r; @@ -624,12 +665,12 @@ int main(int argc, char** argv) mp_init(&a); mp_init(&b); mp_init(&m); mp_init(&r); mp_set(&a, 60); mp_set(&b, 70); mp_set(&m, 100); mcdc_fm_arm(1); - e0 = sakke_addmod(&a, &b, &m, &r); /* 1490 (F,-) */ + e0 = sakke_addmod(&a, &b, &m, &r); /* 1604 (F,-) */ mcdc_fm_disarm(); - (void)sakke_addmod(&a, &b, &m, &r); /* 1490 (T,T) */ + (void)sakke_addmod(&a, &b, &m, &r); /* 1604 (T,T) */ mp_set(&a, 10); mp_set(&b, 20); - (void)sakke_addmod(&a, &b, &m, &r); /* 1490 (T,F) */ - printf(" [wb] sakke_addmod 1490 armed=%d\n", e0); + (void)sakke_addmod(&a, &b, &m, &r); /* 1604 (T,F) */ + printf(" [wb] sakke_addmod 1604 armed=%d\n", e0); mp_free(&a); mp_free(&b); mp_free(&m); mp_free(&r); } @@ -650,12 +691,12 @@ int main(int argc, char** argv) } #endif /* !WOLFSSL_HAVE_SP_ECC */ - /* --- wc_ValidateSakkeRsk (2454, and 2282/2302 in sakke_pairing): + /* --- wc_ValidateSakkeRsk (2585, and 2413/2433 in sakke_pairing): * sakke_compute_point_i() runs first (heavy ECC precompute) -- a fault - * there sets err != 0 before the 2454 idSz guard, driving its err==0 + * there sets err != 0 before the 2585 idSz guard, driving its err==0 * false half. Faults deeper in the run land inside sakke_pairing's - * accumulate-line loop, setting err != 0 so the 2282 loop check and - * 2302 inner guard take their err==0 false halves on the next + * accumulate-line loop, setting err != 0 so the 2413 loop check and + * 2433 inner guard take their err==0 false halves on the next * iteration. Validate does not need the master secret (only the public * key + params), and resets key->i.table = NULL on recompute, so the * shared prepared key is reused; a fault leaves key->i partial but @@ -670,11 +711,11 @@ int main(int argc, char** argv) } WB_NOTE("wc_ValidateSakkeRsk fault sweep done"); - /* --- wc_MakeSakkeEncapsulatedSSV (6289 in sakke_hash_to_range, plus + /* --- wc_MakeSakkeEncapsulatedSSV (6420 in sakke_hash_to_range, plus * the pairing/modexp loops it drives): sakke_calc_a -> * sakke_hash_to_range runs the "(err==0) && (ii.idSz = 0) with idSz > SAKKE_ID_MAX_SIZE while faulting - * compute_point_i, isolating the 6657 idSz operand with err both 0 + * compute_point_i, isolating the 6788 idSz operand with err both 0 * (unarmed, from the baseline above) and != 0 (armed here). --- */ { mp_int rS; diff --git a/tests/unit-mcdc/test_sakke_whitebox.c b/tests/unit-mcdc/test_sakke_whitebox.c index 7884dcbc210..ce3f92830fd 100644 --- a/tests/unit-mcdc/test_sakke_whitebox.c +++ b/tests/unit-mcdc/test_sakke_whitebox.c @@ -40,35 +40,38 @@ * * Guards covered (line : owning function): * - * sakke.c:411 sakke_mulmod_base_add() "(err == 0) && map" -- every in-tree + * sakke.c:423 sakke_mulmod_base_add() "(err == 0) && map" -- every in-tree * caller (sakke_compute_point_i()) hardcodes map=1, so map=0 is * otherwise unreachable. Both values driven directly. - * sakke.c:1483 sakke_addmod() "(err == 0) && (mp_cmp(r, m) != MP_LT)" -- + * sakke.c:1076 wc_MakeSakkeRsk_ex() "(err == 0) && mp_iszero(a)" -- the + * identity q - z_T makes a + z_T == 0 mod q; must fail with MP_VAL + * rather than return the point at infinity. + * sakke.c:1604 sakke_addmod() "(err == 0) && (mp_cmp(r, m) != MP_LT)" -- * pure mp_int helper, no key state needed. r < m and r >= m both driven. - * sakke.c:1505/1508/1511 sakke_tplmod() -- same shape, three sequential + * sakke.c:1626/1629/1632 sakke_tplmod() -- same shape, three sequential * reduction checks; a/m chosen so each of the three independently sees * both "still >= m" and "already < m" across the four calls made. - * sakke.c:2454 wc_ValidateSakkeRsk() "(err == 0) && (idSz <= + * sakke.c:2585 wc_ValidateSakkeRsk() "(err == 0) && (idSz <= * SAKKE_ID_MAX_SIZE)" -- unlike wc_SetSakkeIdentity()/ * wc_MakeSakkePointI(), this public wrapper never bounds idSz before * computing point I, so idSz > SAKKE_ID_MAX_SIZE is reachable directly * through the public API; driven here to isolate the guard that keeps * the fixed 128-byte key->i.id copy from overflowing. - * sakke.c:2466 wc_ValidateSakkeRsk() "*valid = ((err == 0) && + * sakke.c:2597 wc_ValidateSakkeRsk() "*valid = ((err == 0) && * (mp_cmp(a, &key->params.g) == MP_EQ))" -- true (matching RSK/identity * pair) and false (RSK computed for a different identity) both driven. - * sakke.c:6653 sakke_compute_point_r() "(key->i.idSz == 0) || + * sakke.c:6784 sakke_compute_point_r() "(key->i.idSz == 0) || * (key->i.idSz != idSz) || (XMEMCMP(id, key->i.id, idSz) != 0)" -- each * operand toggled independently by controlling key->i.* state directly * (never-cached, size-mismatch, content-mismatch, cache-hit). - * sakke.c:6657 sakke_compute_point_r() "(err == 0) && (idSz <= - * SAKKE_ID_MAX_SIZE)" -- same shape/reason as 2454, driven as an - * idSz > SAKKE_ID_MAX_SIZE call that also forces the 6653 recompute + * sakke.c:6788 sakke_compute_point_r() "(err == 0) && (idSz <= + * SAKKE_ID_MAX_SIZE)" -- same shape/reason as 2585, driven as an + * idSz > SAKKE_ID_MAX_SIZE call that also forces the 6784 recompute * path. * - * Loop decisions at sakke.c:2282, 2302, 2646 and 6289 (bit/byte iteration + * Loop decisions at sakke.c:2413, 2433, 2777 and 6420 (bit/byte iteration * bounded by mp_count_bits(¶ms->q) or a digest size) are exercised for - * both loop-continue and loop-exit, and (2302) both mp_is_bit_set() + * both loop-continue and loop-exit, and (2433) both mp_is_bit_set() * outcomes, as a side effect of the full key-generation / RSK / validate / * encapsulate / derive round trip this file performs to reach the guards * above -- the loop bound and q's bit pattern are fixed curve parameters, @@ -81,8 +84,8 @@ * documented for test_aes_whitebox.c in * iso26262/mcdc-per-module/reports/aes/RESIDUALS.md). * - * Residual (not forced): sakke.c:536 "while ((err == 0) && - * mp_iszero(wc_ecc_key_get_priv(&key->ecc)))" in wc_MakeSakkeKey() only + * Residual (not forced): sakke.c:524 "while ((err == 0) && + * mp_iszero(r))" in sakke_rand_nonzero_mod_q() only * re-rolls when a freshly-generated 1024-bit random scalar is exactly 0 -- * cryptographically negligible, and not reachable without a corrupted or * mocked RNG. @@ -145,7 +148,34 @@ int main(void) if (wc_MakeSakkeRsk(&key, id2, (word16)sizeof(id2), rsk2) != 0) wb_fail = 1; - /* --- sakke.c:411 sakke_mulmod_base_add(): "(err == 0) && map" --- + /* --- sakke.c:1076 wc_MakeSakkeRsk_ex(): "mp_iszero(a)" --- + * Identity q - z_T makes a + z_T == 0 mod q, which has no inverse. + * Drives the guard's TRUE outcome; mp_invmod() of the blinded zero + * would also return MP_VAL, so this pins the result, not the guard. + * Needs the real master secret, so runs here. */ + { + mp_int idZ; + byte idZero[128]; + int idZeroSz = 0; + + if (mp_init(&idZ) != MP_OKAY) wb_fail = 1; + if (mp_sub(&key.params.q, wc_ecc_key_get_priv(&key.ecc), &idZ) + != MP_OKAY) wb_fail = 1; + idZeroSz = mp_unsigned_bin_size(&idZ); + if ((idZeroSz <= 0) || (idZeroSz > (int)sizeof(idZero)) || + (mp_to_unsigned_bin(&idZ, idZero) != MP_OKAY)) { + wb_fail = 1; + } + else if (wc_MakeSakkeRsk(&key, idZero, (word16)idZeroSz, rsk) != + WC_NO_ERR_TRACE(MP_VAL)) { + wb_fail = 1; + } + mp_forcezero(&idZ); + mp_free(&idZ); + WB_NOTE("wc_MakeSakkeRsk() zero a + z_T (line 1076) exercised"); + } + + /* --- sakke.c:423 sakke_mulmod_base_add(): "(err == 0) && map" --- * Only exists without WOLFSSL_HAVE_SP_ECC. Every in-tree caller * hardcodes map=1 (sakke_compute_point_i()); map=0 is otherwise * unreachable. Uses the real public key computed by wc_MakeSakkeKey() @@ -173,11 +203,11 @@ int main(void) } mp_free(&scalarN); - WB_NOTE("sakke_mulmod_base_add() map==0/map==1 (line 411) " + WB_NOTE("sakke_mulmod_base_add() map==0/map==1 (line 423) " "exercised"); } - /* --- sakke.c:1483 sakke_addmod() / 1505,1508,1511 sakke_tplmod() --- + /* --- sakke.c:1604 sakke_addmod() / 1626,1629,1632 sakke_tplmod() --- * Pure mp_int reduction helpers -- no key state needed at all. */ { mp_int a, b, m, r; @@ -193,7 +223,7 @@ int main(void) /* sakke_addmod(): r >= m (guard true, one subtraction). */ mp_set(&a, 60); mp_set(&b, 70); mp_set(&m, 100); if (sakke_addmod(&a, &b, &m, &r) != 0) wb_fail = 1; /* r = 130 */ - WB_NOTE("sakke_addmod() r=m (line 1483) exercised"); + WB_NOTE("sakke_addmod() r=m (line 1604) exercised"); /* sakke_tplmod(): 3*10=30 < 100 -- all three checks false. */ mp_set(&a, 10); mp_set(&m, 100); @@ -208,7 +238,7 @@ int main(void) /* 3*11=33 -> 23 -> 13 -> 3 -- all three checks true. */ mp_set(&a, 11); mp_set(&m, 10); if (sakke_tplmod(&a, &m, &r) != 0) wb_fail = 1; - WB_NOTE("sakke_tplmod() 3-stage reduction (lines 1505/1508/1511) " + WB_NOTE("sakke_tplmod() 3-stage reduction (lines 1626/1629/1632) " "all true/false combinations exercised"); mp_free(&a); @@ -222,27 +252,27 @@ int main(void) "(SP-math substitutes single-shot sp_ecc_* calls); skipped"); #endif - /* --- sakke.c:2454/2466 wc_ValidateSakkeRsk() --- */ - /* Matching RSK/identity pair: valid == 1 (line 2466 true). Also - * exercises the loops at 2282/2302 (sakke_pairing()) with real data. */ + /* --- sakke.c:2585/2597 wc_ValidateSakkeRsk() --- */ + /* Matching RSK/identity pair: valid == 1 (line 2597 true). Also + * exercises the loops at 2413/2433 (sakke_pairing()) with real data. */ valid = -1; if (wc_ValidateSakkeRsk(&key, id, (word16)sizeof(id), rsk, &valid) != 0) wb_fail = 1; if (valid != 1) wb_fail = 1; /* Mismatched pair: rsk2 was computed for id2, checked here against id. - * Isolates line 2466's mp_cmp(...) == MP_EQ operand to false while err + * Isolates line 2597's mp_cmp(...) == MP_EQ operand to false while err * stays 0. */ valid = -1; if (wc_ValidateSakkeRsk(&key, id, (word16)sizeof(id), rsk2, &valid) != 0) wb_fail = 1; if (valid != 0) wb_fail = 1; - WB_NOTE("wc_ValidateSakkeRsk() valid==1/valid==0 (line 2466) exercised"); + WB_NOTE("wc_ValidateSakkeRsk() valid==1/valid==0 (line 2597) exercised"); /* idSz > SAKKE_ID_MAX_SIZE: wc_ValidateSakkeRsk() never bounds idSz * before computing point I (unlike wc_SetSakkeIdentity()/ * wc_MakeSakkePointI()), so this is reachable directly through the - * public API. Isolates line 2454's "idSz <= SAKKE_ID_MAX_SIZE" operand + * public API. Isolates line 2585's "idSz <= SAKKE_ID_MAX_SIZE" operand * to false while err stays 0, confirming the fixed 128-byte key->i.id * copy is skipped rather than overflowed. From this call on, "key"'s * ecc private-key mp_int has been repurposed as scratch by @@ -250,10 +280,10 @@ int main(void) valid = -1; if (wc_ValidateSakkeRsk(&key, idBig, (word16)sizeof(idBig), rsk, &valid) != 0) wb_fail = 1; - WB_NOTE("wc_ValidateSakkeRsk() idSz > SAKKE_ID_MAX_SIZE (line 2454) " + WB_NOTE("wc_ValidateSakkeRsk() idSz > SAKKE_ID_MAX_SIZE (line 2585) " "exercised"); - /* --- sakke.c:6653/6657 sakke_compute_point_r() --- + /* --- sakke.c:6784/6788 sakke_compute_point_r() --- * Called directly (it is file-static) so key->i.* cache state can be * controlled precisely; none of these calls need the master secret. */ { @@ -288,18 +318,18 @@ int main(void) * skipped entirely. */ if (sakke_compute_point_r(&key, idC, (word16)sizeof(idC), &rScalar, 128, out) != 0) wb_fail = 1; - WB_NOTE("sakke_compute_point_r() cache-state guard (line 6653) all " + WB_NOTE("sakke_compute_point_r() cache-state guard (line 6784) all " "four operand combinations exercised"); /* idSz > SAKKE_ID_MAX_SIZE, with key->i.idSz reset to 0 to force - * the recompute branch: isolates line 6657's bound to false while + * the recompute branch: isolates line 6788's bound to false while * err stays 0, confirming the copy into the fixed 128-byte * key->i.id is skipped instead of overflowed. */ key.i.idSz = 0; if (sakke_compute_point_r(&key, idBig, (word16)sizeof(idBig), &rScalar, 128, out) != 0) wb_fail = 1; WB_NOTE("sakke_compute_point_r() idSz > SAKKE_ID_MAX_SIZE (line " - "6657) exercised"); + "6788) exercised"); mp_free(&rScalar); } diff --git a/wolfcrypt/benchmark/benchmark.c b/wolfcrypt/benchmark/benchmark.c index 66f25473897..5e7100e0593 100644 --- a/wolfcrypt/benchmark/benchmark.c +++ b/wolfcrypt/benchmark/benchmark.c @@ -16953,9 +16953,9 @@ void bench_sakkeRskGen(void) bench_stats_start(&count, &start); do { for (i = 0; i < genTimes; i++) { - ret = wc_MakeSakkeRsk(genKey, id, sizeof(id), rsk); + ret = wc_MakeSakkeRsk_ex(genKey, id, sizeof(id), rsk, &gRng); if (ret != 0) { - printf("wc_MakeSakkeRsk failed: %d\n", ret); + printf("wc_MakeSakkeRsk_ex failed: %d\n", ret); goto exit; } RECORD_MULTI_VALUE_STATS(); @@ -17001,7 +17001,7 @@ void bench_sakkeValidate(void) rsk = wc_ecc_new_point(); (void)wc_InitSakkeKey_ex(genKey, 128, ECC_SAKKE_1, HEAP_HINT, INVALID_DEVID); (void)wc_MakeSakkeKey(genKey, &gRng); - (void)wc_MakeSakkeRsk(genKey, id, sizeof(id), rsk); + (void)wc_MakeSakkeRsk_ex(genKey, id, sizeof(id), rsk, &gRng); (void)wc_ValidateSakkeRsk(genKey, id, sizeof(id), rsk, &valid); /* Validation of RSK */ @@ -17065,7 +17065,7 @@ void bench_sakke(void) rsk = wc_ecc_new_point(); (void)wc_InitSakkeKey_ex(genKey, 128, ECC_SAKKE_1, HEAP_HINT, INVALID_DEVID); (void)wc_MakeSakkeKey(genKey, &gRng); - (void)wc_MakeSakkeRsk(genKey, id, sizeof(id), rsk); + (void)wc_MakeSakkeRsk_ex(genKey, id, sizeof(id), rsk, &gRng); (void)wc_SetSakkeRsk(genKey, rsk, NULL, 0); (void)wc_SetSakkeIdentity(genKey, id, sizeof(id)); diff --git a/wolfcrypt/src/sakke.c b/wolfcrypt/src/sakke.c index 0b74ee52ec5..edac8f05001 100644 --- a/wolfcrypt/src/sakke.c +++ b/wolfcrypt/src/sakke.c @@ -34,6 +34,11 @@ #ifdef WOLFCRYPT_HAVE_SAKKE +#ifdef WC_NO_RNG + /* KMS key generation and RSK blinding draw random numbers. */ + #error "SAKKE requires a random number generator (WC_NO_RNG defined)" +#endif + #include #include @@ -150,13 +155,7 @@ int wc_InitSakkeKey_ex(SakkeKey* key, int keySize, int curveId, void* heap, #endif if (err == 0) { err = mp_init_multi(¶ms->prime, ¶ms->q, ¶ms->g, - ¶ms->a, &key->tmp.m1, -#ifdef WOLFCRYPT_SAKKE_CLIENT - &key->tmp.m2 -#else - NULL -#endif - ); + ¶ms->a, &key->tmp.m1, &key->tmp.m2); } if (err == 0) { key->mpInit = 1; @@ -207,9 +206,7 @@ void wc_FreeSakkeKey(SakkeKey* key) mp_free(¶ms->g); mp_free(¶ms->a); mp_free(&key->tmp.m1); -#ifdef WOLFCRYPT_SAKKE_CLIENT mp_free(&key->tmp.m2); -#endif key->mpInit = 0; } #ifdef WOLFCRYPT_SAKKE_CLIENT @@ -495,6 +492,40 @@ static int sakke_mulmod_point(SakkeKey* key, const mp_int* n, ecc_point* p, #endif #ifdef WOLFCRYPT_SAKKE_KMS +/** + * Generate a random number that is not 0 modulo q. + * + * @param [in] key SAKKE key with parameters loaded. + * @param [out] r Random number in the range [1, q-1]. + * @param [in] rng Random number generator. + * @return 0 on success. + * @return RNG_FAILURE_E when no non-zero value is drawn within + * SAKKE_MAX_GEN_COUNT attempts. + * @return Other value when an internal operation fails. + */ +static int sakke_rand_nonzero_mod_q(SakkeKey* key, mp_int* r, WC_RNG* rng) +{ + int err = 0; + int genTryCnt = 0; + int digits = (key->ecc.dp->size * 8 + DIGIT_BIT - 1) / DIGIT_BIT; + + do { + /* Don't infinitely loop on random number generation failure. */ + if ((++genTryCnt) > SAKKE_MAX_GEN_COUNT) { + err = RNG_FAILURE_E; + } + if (err == 0) { + err = mp_rand(r, digits, rng); + } + if (err == 0) { + err = mp_mod(r, &key->params.q, r); + } + } + while ((err == 0) && mp_iszero(r)); + + return err; +} + /** * Generate KMS Master Secret (z_T) and KMS Public Key (Z_T). * @@ -517,39 +548,20 @@ static int sakke_mulmod_point(SakkeKey* key, const mp_int* n, ecc_point* p, int wc_MakeSakkeKey(SakkeKey* key, WC_RNG* rng) { int err = 0; - int digits = 0; if ((key == NULL) || (rng == NULL)) { err = BAD_FUNC_ARG; } if (err == 0) { - digits = (key->ecc.dp->size * 8 + DIGIT_BIT - 1) / DIGIT_BIT; - err = sakke_load_params(key); } if (err == 0) { err = sakke_load_base_point(key); } if (err == 0) { - int genTryCnt = 0; - /* Generate a random number that is not 0 - master secret. */ - do { - /* Don't infinitely loop on random number generation failure. */ - if ((++genTryCnt) > SAKKE_MAX_GEN_COUNT) { - err = RNG_FAILURE_E; - } - if (err == 0) { - err = mp_rand(ecc_get_k_raw(&key->ecc), digits, rng); - } - if (err == 0) { - err = mp_mod(ecc_get_k_raw(&key->ecc), &key->params.q, - ecc_get_k_raw(&key->ecc)); - } - } - while ((err == 0) && mp_iszero(ecc_get_k_raw(&key->ecc))); - + err = sakke_rand_nonzero_mod_q(key, ecc_get_k_raw(&key->ecc), rng); if (err == 0) { err = ecc_blind_k_rng(&key->ecc, rng); } @@ -981,22 +993,64 @@ int wc_ExportSakkePublicKey(SakkeKey* key, byte* data, word32* sz, int raw) * * RFC 6508, Section 6.1.1 * - * RSK = [ (a + z_T) ^ 1 modulo q ]P\n + * RSK = [ (a + z_T) ^ -1 modulo q ]P\n * RSK is required by receiver to derive SSV. * + * The inverse is blinded with a random value from a random number generator + * instantiated for each call. Use wc_MakeSakkeRsk_ex() to pass an existing + * WC_RNG instead. + * * @param [in] key SAKKE key. * @param [in] id Identity to create hash for. * @param [in] idSz Length of identity in bytes. * @param [out] rsk Receiver Secret Key as an ECC point. * @return 0 on success. * @return BAD_FUNC_ARG when key, id, or rsk is NULL. + * @return MP_VAL when a + z_T is zero modulo q. * @return MP_MEM or MEMORY_E when dynamic memory allocation fails. - * @return Other value when an an internal operation fails. + * @return RNG_FAILURE_E when no non-zero blinding factor is drawn within + * SAKKE_MAX_GEN_COUNT attempts. + * @return RNG error when the random number generator cannot be instantiated. + * @return Other value when an internal operation fails. */ int wc_MakeSakkeRsk(SakkeKey* key, const byte* id, word16 idSz, ecc_point* rsk) +{ + return wc_MakeSakkeRsk_ex(key, id, idSz, rsk, NULL); +} + +/** + * Calculates the Receiver Secret Key (RSK) for the identity. + * + * RFC 6508, Section 6.1.1 + * + * RSK = [ (a + z_T) ^ -1 modulo q ]P\n + * RSK is required by receiver to derive SSV. + * + * The inverse is blinded with a random value from rng. When rng is NULL, a + * random number generator is instantiated for the call. + * + * @param [in] key SAKKE key. + * @param [in] id Identity to create hash for. + * @param [in] idSz Length of identity in bytes. + * @param [out] rsk Receiver Secret Key as an ECC point. + * @param [in] rng Random number generator. May be NULL. + * @return 0 on success. + * @return BAD_FUNC_ARG when key, id, or rsk is NULL. + * @return MP_VAL when a + z_T is zero modulo q. + * @return MP_MEM or MEMORY_E when dynamic memory allocation fails. + * @return RNG_FAILURE_E when no non-zero blinding factor is drawn within + * SAKKE_MAX_GEN_COUNT attempts. + * @return RNG error when rng is NULL and a random number generator cannot be + * instantiated. + * @return Other value when an internal operation fails. + */ +int wc_MakeSakkeRsk_ex(SakkeKey* key, const byte* id, word16 idSz, + ecc_point* rsk, WC_RNG* rng) { int err = 0; mp_int* a = NULL; + WC_DECLARE_VAR(localRng, WC_RNG, 1, NULL); + int localRngInit = 0; if ((key == NULL) || (id == NULL) || (rsk == NULL)) { err = BAD_FUNC_ARG; @@ -1009,7 +1063,7 @@ int wc_MakeSakkeRsk(SakkeKey* key, const byte* id, word16 idSz, ecc_point* rsk) err = sakke_load_base_point(key); } - /* Compute RSK = [ (a + z_T) ^ 1 modulo q ]P */ + /* Compute RSK = [ (a + z_T) ^ -1 modulo q ]P */ if (err == 0) { a = &key->tmp.m1; err = mp_read_unsigned_bin(a, id, idSz); @@ -1018,16 +1072,62 @@ int wc_MakeSakkeRsk(SakkeKey* key, const byte* id, word16 idSz, ecc_point* rsk) if (err == 0) { err = mp_addmod(a, wc_ecc_key_get_priv(&key->ecc), &key->params.q, a); } - /* (a + z_T) ^ 1 modulo q */ + /* Zero has no inverse. */ + if ((err == 0) && mp_iszero(a)) { + err = MP_VAL; + } + /* Invert with multiplicative blinding for constant-time operation. */ + if ((err == 0) && (rng == NULL)) { + WC_ALLOC_VAR_EX(localRng, WC_RNG, 1, key->heap, DYNAMIC_TYPE_RNG, + err = MEMORY_E); + if (err == 0) { + #ifdef WOLF_CRYPTO_CB + err = wc_InitRng_ex(localRng, key->heap, key->ecc.devId); + #else + err = wc_InitRng_ex(localRng, key->heap, INVALID_DEVID); + #endif + if (err != 0) { + WC_FREE_VAR_EX(localRng, key->heap, DYNAMIC_TYPE_RNG); + } + } + if (err == 0) { + localRngInit = 1; + rng = localRng; + } + } + /* Random non-zero blinding factor b modulo q. */ + if (err == 0) { + err = sakke_rand_nonzero_mod_q(key, &key->tmp.m2, rng); + } + if (localRngInit) { + wc_FreeRng(localRng); + WC_FREE_VAR_EX(localRng, key->heap, DYNAMIC_TYPE_RNG); + } + /* Blind: a = (a * b) mod q */ + if (err == 0) { + err = mp_mulmod(a, &key->tmp.m2, &key->params.q, a); + } + /* Invert: a = (a * b)^-1 mod q */ if (err == 0) { err = mp_invmod(a, &key->params.q, a); } + /* Unblind: a = (a * b)^-1 * b mod q = (a + z_T)^-1 mod q */ + if (err == 0) { + err = mp_mulmod(a, &key->tmp.m2, &key->params.q, a); + } - /* [ (a + z_T) ^ 1 modulo q ]P */ + /* [ (a + z_T) ^ -1 modulo q ]P */ if (err == 0) { err = sakke_mulmod_base(key, a, rsk, 1); } + if (a != NULL) { + mp_forcezero(a); + } + if ((key != NULL) && key->mpInit) { + mp_forcezero(&key->tmp.m2); + } + return err; } diff --git a/wolfcrypt/src/sp_int.c b/wolfcrypt/src/sp_int.c index db21b62fbff..8ba8e4c5769 100644 --- a/wolfcrypt/src/sp_int.c +++ b/wolfcrypt/src/sp_int.c @@ -12314,7 +12314,7 @@ int sp_mul(const sp_int* a, const sp_int* b, sp_int* r) #endif #if defined(WOLFSSL_SP_MATH_ALL) || defined(WOLFSSL_HAVE_SP_DH) || \ - defined(WOLFCRYPT_HAVE_ECCSI) || \ + defined(WOLFCRYPT_HAVE_ECCSI) || defined(WOLFCRYPT_HAVE_SAKKE) || \ (!defined(NO_RSA) && defined(WOLFSSL_KEY_GEN)) || defined(OPENSSL_ALL) /* Multiply a by b mod m and store in r: r = (a * b) mod m * diff --git a/wolfssl/wolfcrypt/sakke.h b/wolfssl/wolfcrypt/sakke.h index a0179c7bd17..d2f2e6b6bf4 100644 --- a/wolfssl/wolfcrypt/sakke.h +++ b/wolfssl/wolfcrypt/sakke.h @@ -173,6 +173,8 @@ WOLFSSL_API int wc_MakeSakkePublicKey(SakkeKey* key, ecc_point* pub); WOLFSSL_API int wc_MakeSakkeRsk(SakkeKey* key, const byte* id, word16 idSz, ecc_point* rsk); +WOLFSSL_API int wc_MakeSakkeRsk_ex(SakkeKey* key, const byte* id, + word16 idSz, ecc_point* rsk, WC_RNG* rng); WOLFSSL_API int wc_ValidateSakkeRsk(SakkeKey* key, const byte* id, word16 idSz, ecc_point* rsk, int* valid); From a86511c529da2f2ada84e955386a2eac6e38f072 Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 12 Aug 2026 10:28:33 -0600 Subject: [PATCH 05/10] F-7426: Implement heap-math CT primitives using branchless masks --- tests/api/test_wolfmath.c | 209 +++++++++++++++++ tests/api/test_wolfmath.h | 2 + tests/unit-mcdc/test_integer_fault_whitebox.c | 185 ++++++++++++++- tests/unit-mcdc/test_integer_whitebox.c | 28 +-- wolfcrypt/src/integer.c | 212 +++++++++++++++--- 5 files changed, 586 insertions(+), 50 deletions(-) diff --git a/tests/api/test_wolfmath.c b/tests/api/test_wolfmath.c index 8f4c80b21c8..1a08fe3b151 100644 --- a/tests/api/test_wolfmath.c +++ b/tests/api/test_wolfmath.c @@ -133,6 +133,215 @@ int test_mp_cond_copy(void) return EXPECT_RESULT(); } /* End test_mp_cond_copy */ +/* + * Testing heap-math mp_addmod_ct, mp_submod_ct and mp_div_2_mod_ct against + * the variable-time versions, with short operands and the ecc.c aliasing. + */ +int test_mp_addsubmod_ct_heap(void) +{ + EXPECT_DECLS; +#if defined(USE_INTEGER_HEAP_MATH) && defined(HAVE_ECC) && \ + defined(WOLFSSL_PUBLIC_MP) + mp_int m, a, b, r, t; + mp_int v[6]; + mp_int z[3]; + mp_int f[3]; + int k, i, j; + int nv = (int)(sizeof(v) / sizeof(v[0])); + + XMEMSET(&m, 0, sizeof(mp_int)); + XMEMSET(&a, 0, sizeof(mp_int)); + XMEMSET(&b, 0, sizeof(mp_int)); + XMEMSET(&r, 0, sizeof(mp_int)); + XMEMSET(&t, 0, sizeof(mp_int)); + XMEMSET(z, 0, sizeof(z)); + XMEMSET(f, 0, sizeof(f)); + XMEMSET(v, 0, sizeof(v)); + + ExpectIntEQ(mp_init_multi(&m, &a, &b, &r, &t, NULL), MP_OKAY); + ExpectIntEQ(mp_init_multi(&z[0], &z[1], &z[2], NULL, NULL, NULL), + MP_OKAY); + ExpectIntEQ(mp_init_multi(&v[0], &v[1], &v[2], &v[3], &v[4], &v[5]), + MP_OKAY); + + /* k == 0: P-256 prime. k == 1: 2^(4*DIGIT_BIT) - 1, whose full top limb + * makes a + b carry into limb c->used. */ + for (k = 0; (k < 2) && EXPECT_SUCCESS(); k++) { + if (k == 0) { + ExpectIntEQ(mp_read_radix(&m, + "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFF" + "FFFFFFFF", MP_RADIX_HEX), MP_OKAY); + } + else { + ExpectIntEQ(mp_2expt(&m, 4 * DIGIT_BIT), MP_OKAY); + ExpectIntEQ(mp_sub_d(&m, 1, &m), MP_OKAY); + } + /* 0, 1, 5 (short operands), m-1, m-2, (m-1)/2 */ + ExpectIntEQ(mp_set(&v[0], 0), MP_OKAY); + ExpectIntEQ(mp_set(&v[1], 1), MP_OKAY); + ExpectIntEQ(mp_set(&v[2], 5), MP_OKAY); + ExpectIntEQ(mp_sub_d(&m, 1, &v[3]), MP_OKAY); + ExpectIntEQ(mp_sub_d(&m, 2, &v[4]), MP_OKAY); + ExpectIntEQ(mp_div_2(&m, &v[5]), MP_OKAY); + + for (i = 0; (i < nv) && EXPECT_SUCCESS(); i++) { + for (j = 0; (j < nv) && EXPECT_SUCCESS(); j++) { + ExpectIntEQ(mp_copy(&v[i], &a), MP_OKAY); + ExpectIntEQ(mp_copy(&v[j], &b), MP_OKAY); + + ExpectIntEQ(mp_addmod_ct(&a, &b, &m, &r), MP_OKAY); + ExpectIntEQ(mp_addmod(&a, &b, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&r, &t), MP_EQ); + + /* (a - b) + b == a */ + ExpectIntEQ(mp_submod_ct(&a, &b, &m, &r), MP_OKAY); + ExpectIntEQ(mp_addmod(&r, &b, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&t, &a), MP_EQ); + + /* d == a */ + ExpectIntEQ(mp_copy(&a, &r), MP_OKAY); + ExpectIntEQ(mp_addmod_ct(&r, &b, &m, &r), MP_OKAY); + ExpectIntEQ(mp_addmod(&a, &b, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&r, &t), MP_EQ); + ExpectIntEQ(mp_copy(&a, &r), MP_OKAY); + ExpectIntEQ(mp_submod_ct(&r, &b, &m, &r), MP_OKAY); + ExpectIntEQ(mp_addmod(&r, &b, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&t, &a), MP_EQ); + + /* d == b, as in ECDSA signing's s = e + s (mod n). */ + ExpectIntEQ(mp_copy(&b, &r), MP_OKAY); + ExpectIntEQ(mp_addmod_ct(&a, &r, &m, &r), MP_OKAY); + ExpectIntEQ(mp_addmod(&a, &b, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&r, &t), MP_EQ); + ExpectIntEQ(mp_copy(&b, &r), MP_OKAY); + ExpectIntEQ(mp_submod_ct(&a, &r, &m, &r), MP_OKAY); + ExpectIntEQ(mp_addmod(&r, &b, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&t, &a), MP_EQ); + + /* d == c: the result replaces the modulus. */ + ExpectIntEQ(mp_copy(&m, &t), MP_OKAY); + ExpectIntEQ(mp_addmod_ct(&a, &b, &t, &t), MP_OKAY); + ExpectIntEQ(mp_addmod(&a, &b, &m, &r), MP_OKAY); + ExpectIntEQ(mp_cmp(&t, &r), MP_EQ); + ExpectIntEQ(mp_copy(&m, &t), MP_OKAY); + ExpectIntEQ(mp_submod_ct(&a, &b, &t, &t), MP_OKAY); + ExpectIntEQ(mp_addmod(&t, &b, &m, &r), MP_OKAY); + ExpectIntEQ(mp_cmp(&r, &a), MP_EQ); + } + + ExpectIntEQ(mp_copy(&v[i], &a), MP_OKAY); + + /* a == b == d */ + ExpectIntEQ(mp_copy(&a, &r), MP_OKAY); + ExpectIntEQ(mp_addmod_ct(&r, &r, &m, &r), MP_OKAY); + ExpectIntEQ(mp_addmod(&a, &a, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&r, &t), MP_EQ); + + /* a == c: m - a */ + ExpectIntEQ(mp_submod_ct(&m, &a, &m, &r), MP_OKAY); + ExpectIntEQ(mp_sub(&m, &a, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&r, &t), MP_EQ); + + /* div_2: doubling the result gives a back; also with c == a. */ + ExpectIntEQ(mp_div_2_mod_ct(&a, &m, &r), MP_OKAY); + ExpectIntEQ(mp_addmod(&r, &r, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&t, &a), MP_EQ); + ExpectIntEQ(mp_copy(&a, &t), MP_OKAY); + ExpectIntEQ(mp_div_2_mod_ct(&t, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&t, &r), MP_EQ); + /* ... and with c == b: the result replaces the modulus. */ + ExpectIntEQ(mp_copy(&m, &t), MP_OKAY); + ExpectIntEQ(mp_div_2_mod_ct(&a, &t, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&t, &r), MP_EQ); + } + } + + /* Zero operands that were never grown (dp == NULL). */ + ExpectIntEQ(mp_addmod_ct(&z[0], &v[1], &m, &r), MP_OKAY); + ExpectIntEQ(mp_cmp_d(&r, 1), MP_EQ); + ExpectIntEQ(mp_submod_ct(&v[1], &z[1], &m, &r), MP_OKAY); + ExpectIntEQ(mp_cmp_d(&r, 1), MP_EQ); + ExpectIntEQ(mp_div_2_mod_ct(&z[2], &m, &r), MP_OKAY); + ExpectIntEQ(mp_iszero(&r), MP_YES); + + /* Operand wider than the modulus is rejected. */ + ExpectIntEQ(mp_2expt(&a, 5 * DIGIT_BIT), MP_OKAY); + ExpectIntEQ(mp_addmod_ct(&a, &v[1], &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_addmod_ct(&v[1], &a, &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_submod_ct(&a, &v[1], &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_submod_ct(&v[1], &a, &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_div_2_mod_ct(&a, &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + + /* Negative operands are rejected. */ + ExpectIntEQ(mp_set(&a, 5), MP_OKAY); + a.sign = MP_NEG; + ExpectIntEQ(mp_addmod_ct(&a, &v[1], &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_addmod_ct(&v[1], &a, &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_submod_ct(&a, &v[1], &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_submod_ct(&v[1], &a, &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_div_2_mod_ct(&a, &m, &r), WC_NO_ERR_TRACE(MP_VAL)); + + /* Short inputs are read in place, not grown to the modulus width. */ + if (EXPECT_SUCCESS()) { + mp_digit* dp_a; + mp_digit* dp_b; + int alloc_a; + int alloc_b; + + mp_clear(&a); + mp_clear(&b); + ExpectIntEQ(mp_init_multi(&a, &b, NULL, NULL, NULL, NULL), MP_OKAY); + ExpectIntEQ(mp_set(&a, 5), MP_OKAY); + ExpectIntEQ(mp_set(&b, 7), MP_OKAY); + ExpectIntLT(a.alloc, m.used); + ExpectIntLT(b.alloc, m.used); + dp_a = a.dp; + dp_b = b.dp; + alloc_a = a.alloc; + alloc_b = b.alloc; + ExpectIntEQ(mp_addmod_ct(&a, &b, &m, &r), MP_OKAY); + ExpectIntEQ(mp_cmp_d(&r, 12), MP_EQ); + ExpectIntEQ(mp_submod_ct(&b, &a, &m, &r), MP_OKAY); + ExpectIntEQ(mp_cmp_d(&r, 2), MP_EQ); + ExpectIntEQ(mp_div_2_mod_ct(&a, &m, &r), MP_OKAY); + ExpectIntEQ(mp_addmod(&r, &r, &m, &t), MP_OKAY); + ExpectIntEQ(mp_cmp(&t, &a), MP_EQ); + ExpectPtrEq(a.dp, dp_a); + ExpectPtrEq(b.dp, dp_b); + ExpectIntEQ(a.alloc, alloc_a); + ExpectIntEQ(b.alloc, alloc_b); + } + + /* Zero modulus is rejected before any operand is grown. */ + mp_zero(&m); + ExpectIntEQ(mp_init_multi(&f[0], &f[1], &f[2], NULL, NULL, NULL), + MP_OKAY); + ExpectIntEQ(mp_addmod_ct(&f[0], &f[1], &m, &f[2]), + WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_submod_ct(&f[0], &f[1], &m, &f[2]), + WC_NO_ERR_TRACE(MP_VAL)); + ExpectIntEQ(mp_div_2_mod_ct(&f[0], &m, &f[2]), WC_NO_ERR_TRACE(MP_VAL)); + for (i = 0; i < 3; i++) { + ExpectNull(f[i].dp); + ExpectIntEQ(f[i].alloc, 0); + } + + for (i = 0; i < nv; i++) { + mp_clear(&v[i]); + } + for (i = 0; i < 3; i++) { + mp_clear(&z[i]); + mp_clear(&f[i]); + } + mp_clear(&m); + mp_clear(&a); + mp_clear(&b); + mp_clear(&r); + mp_clear(&t); +#endif + return EXPECT_RESULT(); +} /* End test_mp_addsubmod_ct_heap */ + /* * Testing mp_rand */ diff --git a/tests/api/test_wolfmath.h b/tests/api/test_wolfmath.h index 6313903d367..7879b1d9f6a 100644 --- a/tests/api/test_wolfmath.h +++ b/tests/api/test_wolfmath.h @@ -28,6 +28,7 @@ int test_mp_get_digit_count(void); int test_mp_get_digit(void); int test_mp_get_rand_digit(void); int test_mp_cond_copy(void); +int test_mp_addsubmod_ct_heap(void); int test_mp_rand(void); int test_wc_export_int(void); int test_wc_SpIntSizeDecisionCoverage(void); @@ -42,6 +43,7 @@ int test_wc_SpIntExptGcdDecisionCoverage(void); TEST_DECL_GROUP("wolfmath", test_mp_get_digit), \ TEST_DECL_GROUP("wolfmath", test_mp_get_rand_digit), \ TEST_DECL_GROUP("wolfmath", test_mp_cond_copy), \ + TEST_DECL_GROUP("wolfmath", test_mp_addsubmod_ct_heap), \ TEST_DECL_GROUP("wolfmath", test_mp_rand), \ TEST_DECL_GROUP("wolfmath", test_wc_export_int), \ TEST_DECL_GROUP("wolfmath", test_wc_SpIntSizeDecisionCoverage), \ diff --git a/tests/unit-mcdc/test_integer_fault_whitebox.c b/tests/unit-mcdc/test_integer_fault_whitebox.c index 1798f87afcb..c605fecd859 100644 --- a/tests/unit-mcdc/test_integer_fault_whitebox.c +++ b/tests/unit-mcdc/test_integer_fault_whitebox.c @@ -77,16 +77,16 @@ * * mp_sub(&ta, &tb, &ta): ta and tb are both MP_ZPOS (mp_abs results; * mp_mul_2d, s_mp_sub and mp_clamp all preserve/reassert MP_ZPOS), so - * mp_sub takes the sa == sb arm at :1913; the loop guard at :1600 + * mp_sub takes the sa == sb arm at :1977; the loop guard at :1600 * (mp_cmp(&tb,&ta) != MP_GT, which for two MP_ZPOS values is * mp_cmp_mag) guarantees mp_cmp_mag(ta,tb) != MP_LT, so it dispatches to - * s_mp_sub(ta, tb, ta) at :1921. There c == a, so "c->alloc < max_a" - * (:1834) is "ta->alloc < ta->used", false under the mp_int invariant + * s_mp_sub(ta, tb, ta) at :1993. There c == a, so "c->alloc < max_a" + * (:1900) is "ta->alloc < ta->used", false under the mp_int invariant * alloc >= used -- mp_grow is never called. The two sanity returns cannot * fire either: ta->dp is non-NULL because mp_abs -> mp_copy -> mp_grow * always allocates into a zero-alloc destination (:350, :392), and tb->dp * is non-NULL for the same reason whenever min_b = tb->used > 0. - * s_mp_sub therefore returns MP_OKAY at :1896 unconditionally. + * s_mp_sub therefore returns MP_OKAY at :1965 unconditionally. * * The allocation injector cannot change this: the only allocation-bearing op * inside the loop is the mp_add at :1602, and its failure goes straight to @@ -100,6 +100,10 @@ * plus mp_add_d's negative multi-digit inner operand. These are deterministic * direct calls (no injection) providing both halves of each pair in-binary. * + * A third section covers mp_addmod_ct, mp_submod_ct and mp_div_2_mod_ct. Each + * grows only its output, or a temporary when c == d, and that grow is failed; + * NULL and negative-operand rows drive each argument guard operand TRUE. + * * Crash-safety: every armed call fails an allocation that mp_div's own * goto-LBL_ERR cleanup absorbs (that cleanup is what is under test); operands * are (re)built while DISARMED each iteration and mp_div does not mutate its @@ -208,13 +212,13 @@ static void wb_argument_residuals(void) (void)mp_exptmod(&a, &b, &zero, &y); /* mp_iszero(P) == YES: cond0 T */ (void)mp_exptmod(&a, &b, &c, &y); /* positive P: both F */ - /* mp_prime_is_prime: "t <= 0 || t > PRIME_SIZE" 4920:*:0 -- the t<=0 half. + /* mp_prime_is_prime: "t <= 0 || t > PRIME_SIZE" 5065:*:0 -- the t<=0 half. * t == 0 (T) returns MP_VAL; t == 8 (both operands F) runs the test. */ (void)mp_set(&a, 17); (void)mp_prime_is_prime(&a, 0, &res); /* t <= 0: cond0 T */ (void)mp_prime_is_prime(&a, 8, &res); /* both F */ - /* mp_lcm: "mp_iszero(a) == MP_YES || mp_iszero(b) == MP_YES" 5167:*:0/1. + /* mp_lcm: "mp_iszero(a) == MP_YES || mp_iszero(b) == MP_YES" 5312:*:0/1. * lcm(0,b): cond0 T. lcm(a,0): cond0 F, cond1 T. lcm(a,b): both F. */ (void)mp_set(&a, 6); (void)mp_set(&b, 8); @@ -222,7 +226,7 @@ static void wb_argument_residuals(void) (void)mp_lcm(&a, &zero, &c); /* iszero(a) F, iszero(b) T: cond1 T */ (void)mp_lcm(&a, &b, &c); /* both F */ - /* mp_radix_size: "radix < MP_RADIX_BIN || radix > MP_RADIX_MAX" 5404:*:0 -- + /* mp_radix_size: "radix < MP_RADIX_BIN || radix > MP_RADIX_MAX" 5549:*:0 -- * the radix MAX): cond0 F, * cond1 T. radix 10 (valid): both F. */ (void)mp_set(&a, 0x1234); @@ -230,14 +234,14 @@ static void wb_argument_residuals(void) (void)mp_radix_size(&a, 65, &size); /* radix>MAX: cond1 T */ (void)mp_radix_size(&a, MP_RADIX_DEC, &size); /* both F */ - /* mp_toradix: same guard, its own copy 5465:*:0. */ + /* mp_toradix: same guard, its own copy 5610:*:0. */ XMEMSET(buf, 0, sizeof(buf)); (void)mp_toradix(&a, buf, 1); /* radixMAX: cond1 T */ XMEMSET(buf, 0, sizeof(buf)); (void)mp_toradix(&a, buf, MP_RADIX_DEC); /* both F */ - /* mp_add_d: "a->sign == MP_NEG && (a->used > 1 || a->dp[0] >= b)" 4411:*:1 + /* mp_add_d: "a->sign == MP_NEG && (a->used > 1 || a->dp[0] >= b)" 4549:*:1 * -- the "a->used > 1" inner-OR operand. A negative TWO-digit value drives * used>1 T (inner OR T -> outer AND T, the |a|-b subtraction path). A * negative ONE-digit value with dp[0] < b drives used>1 F AND dp[0]>=b F @@ -254,6 +258,160 @@ static void wb_argument_residuals(void) WB_NOTE("argument residuals (exptmod/prime/lcm/radix/add_d) exercised"); } +/* ---- Constant-time modular helpers: mp_addmod_ct / mp_submod_ct / + * mp_div_2_mod_ct. Each grows its output (or, when c == d, a temporary) to + * the modulus size before the branchless loops, and that grow can return + * MP_MEM; the inputs are only read. The NULL and negative rows give each + * argument guard operand its TRUE side. ---- */ +#define WB_CT_ADD 0 +#define WB_CT_SUB 1 +#define WB_CT_DIV2 2 + +/* Build the operands DISARMED; m is the P-256 prime. With fresh set, a, b and + * d keep the dp == NULL that mp_init() leaves, so the inputs are read with no + * allocation behind them. Otherwise a = 5 and b = 7, grown to m.used. Either + * way the output's (or the c == d temporary's) grow is the only allocation. + * Returns 0 on success. */ +static int wb_ct_setup(mp_int* m, mp_int* a, mp_int* b, mp_int* d, int fresh) +{ + if (mp_init_multi(m, a, b, d, NULL, NULL) != MP_OKAY) + return -1; + if (mp_read_radix(m, "FFFFFFFF00000001000000000000000000000000FFFFFFFF" + "FFFFFFFFFFFFFFFF", MP_RADIX_HEX) != MP_OKAY) + goto fail; + if (!fresh) { + if ((mp_set(a, 5) != MP_OKAY) || (mp_set(b, 7) != MP_OKAY) || + (mp_grow(a, m->used) != MP_OKAY) || + (mp_grow(b, m->used) != MP_OKAY)) + goto fail; + } + return 0; +fail: + mp_clear(m); mp_clear(a); mp_clear(b); mp_clear(d); + return -1; +} + +#ifndef MCDC_FA_UNAVAILABLE +static const char* const wb_ct_name[3] = { + "mp_addmod_ct", "mp_submod_ct", "mp_div_2_mod_ct" +}; + +/* mp_div_2_mod_ct() has no d, so alias applies to add and sub only. */ +static int wb_ct_call(int op, mp_int* a, mp_int* b, mp_int* m, mp_int* d, + int alias) +{ + mp_int* out = alias ? m : d; + + if (op == WB_CT_ADD) + return mp_addmod_ct(a, b, m, out); + if (op == WB_CT_SUB) + return mp_submod_ct(a, b, m, out); + return mp_div_2_mod_ct(a, m, d); +} + +/* Count the call's allocations unarmed, then fail each one in turn and + * require MP_MEM; one past the count must succeed. The fresh case fails the + * grow of d (c for mp_div_2_mod_ct); the c == d case fails the temporary's + * grow, whose error path clears it. */ +static void wb_ct_alloc_sweep(int op, int alias) +{ + mp_int m, a, b, d; + unsigned long allocs; + unsigned long want; + unsigned long n; + int r; + + want = 1; + + if (wb_ct_setup(&m, &a, &b, &d, !alias) != 0) { + WB_NOTE("ct alloc sweep: setup failed, skipped"); + wb_fail = 1; + return; + } + mcdc_fa_arm(1000000); + r = wb_ct_call(op, &a, &b, &m, &d, alias); + allocs = mcdc_fa_count; + mcdc_fa_disarm(); + mp_clear(&m); mp_clear(&a); mp_clear(&b); mp_clear(&d); + if ((r != MP_OKAY) || (allocs != want)) { + printf(" [wb] %s alias %d: r %d, %lu allocations, expected %lu\n", + wb_ct_name[op], alias, r, allocs, want); + wb_fail = 1; + return; + } + + for (n = 1; n <= allocs + 1; n++) { + if (wb_ct_setup(&m, &a, &b, &d, !alias) != 0) { + wb_fail = 1; + continue; + } + mcdc_fa_arm(n); + r = wb_ct_call(op, &a, &b, &m, &d, alias); + mcdc_fa_disarm(); + if (r != ((n <= allocs) ? WC_NO_ERR_TRACE(MP_MEM) : MP_OKAY)) { + printf(" [wb] %s alias %d: fail-index %lu -> %d\n", + wb_ct_name[op], alias, n, r); + wb_fail = 1; + } + mp_clear(&m); mp_clear(&a); mp_clear(&b); mp_clear(&d); + } + printf(" [wb] %s%s: %lu allocations, each failed in turn\n", + wb_ct_name[op], alias ? " (c == d)" : "", allocs); +} +#endif + +/* Each NULL, then each negative operand, in turn must return MP_VAL; the + * sweep's successful calls give the all-FALSE row. */ +static void wb_ct_null_args(void) +{ + mp_int m, a, b, d; + mp_int* p[4]; + int k; + + if (wb_ct_setup(&m, &a, &b, &d, 0) != 0) { + WB_NOTE("ct NULL args: setup failed, skipped"); + wb_fail = 1; + return; + } + for (k = 0; k < 4; k++) { + p[0] = &a; p[1] = &b; p[2] = &m; p[3] = &d; + p[k] = NULL; + if ((mp_addmod_ct(p[0], p[1], p[2], p[3]) != + WC_NO_ERR_TRACE(MP_VAL)) || + (mp_submod_ct(p[0], p[1], p[2], p[3]) != + WC_NO_ERR_TRACE(MP_VAL))) { + printf(" [wb] ct add/sub NULL arg %d not rejected\n", k); + wb_fail = 1; + } + } + for (k = 0; k < 3; k++) { + p[0] = &a; p[1] = &m; p[2] = &d; + p[k] = NULL; + if (mp_div_2_mod_ct(p[0], p[1], p[2]) != WC_NO_ERR_TRACE(MP_VAL)) { + printf(" [wb] mp_div_2_mod_ct NULL arg %d not rejected\n", k); + wb_fail = 1; + } + } + for (k = 0; k < 2; k++) { + p[0] = &a; p[1] = &b; + p[k]->sign = MP_NEG; + if ((mp_addmod_ct(&a, &b, &m, &d) != WC_NO_ERR_TRACE(MP_VAL)) || + (mp_submod_ct(&a, &b, &m, &d) != WC_NO_ERR_TRACE(MP_VAL))) { + printf(" [wb] ct add/sub negative arg %d not rejected\n", k); + wb_fail = 1; + } + p[k]->sign = MP_ZPOS; + } + a.sign = MP_NEG; + if (mp_div_2_mod_ct(&a, &m, &d) != WC_NO_ERR_TRACE(MP_VAL)) { + WB_NOTE("mp_div_2_mod_ct negative arg not rejected"); + wb_fail = 1; + } + a.sign = MP_ZPOS; + mp_clear(&m); mp_clear(&a); mp_clear(&b); mp_clear(&d); + WB_NOTE("ct NULL and sign argument guards exercised"); +} + int main(int argc, char** argv) { int do_sweep = !(argc > 1 && strcmp(argv[1], "baseline") == 0); @@ -322,7 +480,16 @@ int main(int argc, char** argv) } WB_NOTE("mp_div fault-index sweep done"); +#ifndef MCDC_FA_UNAVAILABLE + wb_ct_alloc_sweep(WB_CT_ADD, 0); + wb_ct_alloc_sweep(WB_CT_ADD, 1); + wb_ct_alloc_sweep(WB_CT_SUB, 0); + wb_ct_alloc_sweep(WB_CT_SUB, 1); + wb_ct_alloc_sweep(WB_CT_DIV2, 0); +#endif + wb_argument_residuals(); + wb_ct_null_args(); } mcdc_fa_disarm(); diff --git a/tests/unit-mcdc/test_integer_whitebox.c b/tests/unit-mcdc/test_integer_whitebox.c index aac40073eee..5a1471f9299 100644 --- a/tests/unit-mcdc/test_integer_whitebox.c +++ b/tests/unit-mcdc/test_integer_whitebox.c @@ -652,12 +652,12 @@ static void wb_IntegerDecisionCoverage(void) (void)mp_set(&b, 4); /* min_b(1) > 0 */ (void)s_mp_sub(&a, &b, &c); /* tmpa==NULL: true */ - /* mp_exptmod_fast (2107:*:1) / mp_exptmod_base_2 (2350:*:1) / - * mp_montgomery_reduce (2690:*:1): all three share + /* mp_exptmod_fast (2173:*:1) / mp_exptmod_base_2 (2416:*:1) / + * mp_montgomery_reduce (2756:*:1): all three share * (N->used * 2 + 1 < (int)MP_WARRAY) && * (N->used < (1L << (CHAR_BIT*sizeof(mp_word) - 2*DIGIT_BIT))) * (mp_montgomery_reduce spells the first operand with the hoisted - * digs = n->used * 2 + 1 at :2689) to pick between the comba-accelerated + * digs = n->used * 2 + 1 at :2755) to pick between the comba-accelerated * and the generic montgomery reduce - both arms are functionally * equivalent (just performance), so a small and a large (real, * legitimately grown) modulus both succeed; only the dispatch differs. @@ -671,7 +671,7 @@ static void wb_IntegerDecisionCoverage(void) * operand0 <=> 2u + 1 < 2B <=> 2u <= 2B - 2 <=> u <= B - 1 <=> u < B * which is operand1 verbatim. The two operands are not merely disjoint, * they are the SAME predicate, so the (T,F) row that operand1's - * independence pair needs does not exist. (Contrast mp_sqr at :3162, + * independence pair needs does not exist. (Contrast mp_sqr at :3320, * whose second operand uses "- 1" in the exponent, i.e. B/2; there the * pair IS satisfiable and is driven below.) u is int-typed ->used, which * mp_clamp/mp_grow keep in [0, alloc]; a used large enough to overflow @@ -742,8 +742,8 @@ static void wb_IntegerDecisionCoverage(void) wolfmath_big_fill(&a, 300, 3); /* > 1st threshold too */ (void)mp_sqr(&a, &c); /* both false: falls back */ - /* mp_mul (3199:*:1): "(digs < (int)MP_WARRAY) && - * MIN(a->used,b->used) <= B", digs = a->used + b->used + 1 (:3197) and + /* mp_mul (3357:*:1): "(digs < (int)MP_WARRAY) && + * MIN(a->used,b->used) <= B", digs = a->used + b->used + 1 (:3355) and * B == MP_WARRAY / 2 (see the identity spelled out above). The MIN * operand is EXCLUDED: operand0 true means a->used + b->used + 1 <= 2B-1, * hence a->used + b->used <= 2B-2, hence MIN <= B-1 <= B, so operand1 is @@ -774,18 +774,18 @@ static void wb_IntegerDecisionCoverage(void) (void)s_mp_mul_digs(&a, &b, &c, 305); /* digs(305)< WARRAY:T, MIN(300,300)<256: F */ - /* s_mp_exptmod tail (3959:*:1), identical idiom in mp_exptmod_fast - * (2279:*:1): "if (mode == 2 && bitcpy > 0)". + /* s_mp_exptmod tail (4117:*:1), identical idiom in mp_exptmod_fast + * (2345:*:1): "if (mode == 2 && bitcpy > 0)". * * The "bitcpy > 0" operand is EXCLUDED: the sliding-window state machine * maintains the invariant (mode == 2) => (bitcpy >= 1), so the (T,F) row * operand1 needs does not exist. Exhaustively, in each function there are * only three writes to 'mode' and two to 'bitcpy': - * :3884/:2204 mode = 0 and :3888/:2208 bitcpy = 0 (entry) - * :3928/:2248 bitbuf |= (y << (winsize - ++bitcpy)); -- PRE-increment - * :3929/:2249 mode = 2 -- immediately after, so bitcpy >= 1 here - * :3952/:2272 bitcpy = 0 } same straight-line block, guarded by - * :3954/:2274 mode = 1 } "if (bitcpy == winsize)", no break/goto + * :4042/:2270 mode = 0 and :4046/:2274 bitcpy = 0 (entry) + * :4086/:2314 bitbuf |= (y << (winsize - ++bitcpy)); -- PRE-increment + * :4087/:2315 mode = 2 -- immediately after, so bitcpy >= 1 here + * :4110/:2338 bitcpy = 0 } same straight-line block, guarded by + * :4112/:2340 mode = 1 } "if (bitcpy == winsize)", no break/goto * between them * The only assignment that clears bitcpy also leaves mode == 1, and the * only assignment that sets mode == 2 is preceded by an increment of @@ -803,7 +803,7 @@ static void wb_IntegerDecisionCoverage(void) (void)mp_set(&b, 0x3FFFFFFF); /* 30 bits: winsize 3 */ (void)s_mp_exptmod(&a, &b, &c, &a, 0); - /* s_mp_mul_high_digs (4170:*:1): "((a->used + b->used + 1) < + /* s_mp_mul_high_digs (4328:*:1): "((a->used + b->used + 1) < * (int)MP_WARRAY) && MIN(a->used,b->used) < B" - the 'digs' PARAMETER is * only used after the check; the check itself is SUM-based on * a->used+b->used, so this is the same coupling as mp_mul above (this diff --git a/wolfcrypt/src/integer.c b/wolfcrypt/src/integer.c index ccf7e2c2fe8..4418d47b94f 100644 --- a/wolfcrypt/src/integer.c +++ b/wolfcrypt/src/integer.c @@ -1676,22 +1676,88 @@ int mp_div_2(mp_int * a, mp_int * b) return MP_OKAY; } +/* mp_clamp() without branching on limb values: used is found by masking + * across all limbs rather than stopping at the first non-zero one. Limbs keep + * their top bit clear (DIGIT_BIT < bits in mp_digit), so (d - 1) has its top + * bit set exactly when d is zero. */ +static int mp_clamp_ct(mp_int* a) +{ + int i; + int used; + volatile mp_digit mask = (mp_digit)-1; + + used = a->used; + for (i = a->used - 1; i >= 0; i--) { + mp_digit isZero = (mp_digit)(a->dp[i] - 1) >> + (CHAR_BIT * sizeof(mp_digit) - 1); + mask &= (mp_digit)0 - isZero; + used -= (int)(mask & 1); + } + a->used = used; + + return MP_OKAY; +} + +/* Load limb i of a for the constant-time helpers below, without growing a: + * the index is clamped to the last allocated limb rather than the load + * skipped. Callers mask off limbs at and above a->used. */ +static mp_digit mp_load_ct(const mp_int* a, int i) +{ + int last = a->alloc - 1; + + if (a->alloc == 0) { + /* dp may be NULL; used is 0, so every limb is masked off. */ + return 0; + } + return a->dp[i - ((i - last) & (0 - (int)(i > last)))]; +} + /* c = a / 2 (mod b) - constant time (a < b and positive) */ int mp_div_2_mod_ct(mp_int *a, mp_int *b, mp_int *c) { - int res; + int res, i; + mp_digit u; + volatile mp_digit mask; + volatile mp_digit mask_a; + volatile mp_digit nz_a; - if (mp_isodd(a)) { - res = mp_add(a, b, c); - if (res == MP_OKAY) { - res = mp_div_2(c, c); - } + if ((a == NULL) || (b == NULL) || (c == NULL)) { + return MP_VAL; } - else { - res = mp_div_2(a, c); + + if ((b->used == 0) || (a->used > b->used) || (a->sign == MP_NEG)) { + return MP_VAL; } - return res; + if ((res = mp_grow(c, b->used + 1)) != MP_OKAY) { + return res; + } + + /* Limbs at and above a->used are masked off rather than skipped. */ + nz_a = (mp_digit)0 - (mp_digit)(a->used != 0); + mask = (mp_digit)0 - (mp_load_ct(a, 0) & nz_a & 1); + mask_a = (mp_digit)-1; + u = 0; + for (i = 0; i < b->used; i++) { + mask_a += (mp_digit)(i == a->used); + c->dp[i] = (mp_load_ct(a, i) & mask_a) + (b->dp[i] & mask) + u; + u = c->dp[i] >> DIGIT_BIT; + c->dp[i] &= MP_MASK; + } + c->dp[b->used] = u; + + for (i = 0; i < b->used; i++) { + c->dp[i] = (c->dp[i] >> 1) | ((c->dp[i+1] & 1) << (DIGIT_BIT - 1)); + } + + /* Clear up to alloc, not the old used, which may follow a secret. */ + c->used = b->used; + for (i = b->used; i < c->alloc; i++) { + c->dp[i] = 0; + } + c->sign = MP_ZPOS; + + return mp_clamp_ct(c); } @@ -3095,26 +3161,66 @@ int mp_submod_ct(mp_int* a, mp_int* b, mp_int* c, mp_int* d) int res; mp_int t; mp_int* r = d; + mp_digit u; + volatile mp_digit mask; + volatile mp_digit mask_a = (mp_digit)-1; + volatile mp_digit mask_b = (mp_digit)-1; + int i; + + if ((a == NULL) || (b == NULL) || (c == NULL) || (d == NULL)) { + return MP_VAL; + } + + if ((c->used == 0) || (a->used > c->used) || (b->used > c->used) || + (a->sign == MP_NEG) || (b->sign == MP_NEG)) { + return MP_VAL; + } if (c == d) { r = &t; - if ((res = mp_init (r)) != MP_OKAY) { return res; } } - res = mp_sub (a, b, r); - if (res == MP_OKAY) { - if (mp_isneg (r)) { - res = mp_add (r, c, d); - } else if (c == d) { - res = mp_copy (r, d); - } + if ((res = mp_grow(r, c->used)) != MP_OKAY) { + if (c == d) { mp_clear(r); } + return res; + } + + u = 0; + for (i = 0; i < c->used; i++) { + mp_digit val; + /* Limbs at and above used are masked off rather than skipped. */ + mask_a += (mp_digit)(i == a->used); + mask_b += (mp_digit)(i == b->used); + val = (mp_load_ct(a, i) & mask_a) - (mp_load_ct(b, i) & mask_b) - u; + r->dp[i] = val & MP_MASK; + u = val >> (CHAR_BIT * sizeof(mp_digit) - 1); + } + + mask = (mp_digit)0 - u; + u = 0; + for (i = 0; i < c->used; i++) { + r->dp[i] += (c->dp[i] & mask) + u; + u = r->dp[i] >> DIGIT_BIT; + r->dp[i] &= MP_MASK; } + /* Clear up to alloc, not the old used, which may follow a secret. */ + r->used = c->used; + for (i = c->used; i < r->alloc; i++) { + r->dp[i] = 0; + } + r->sign = MP_ZPOS; + + /* Copy before clamping: mp_copy() only walks r->used limbs. */ if (c == d) { - mp_clear (r); + res = mp_copy(r, d); + mp_clear(r); + } + if (res == MP_OKAY) { + res = mp_clamp_ct(d); } return res; @@ -3126,26 +3232,78 @@ int mp_addmod_ct(mp_int* a, mp_int* b, mp_int* c, mp_int* d) int res; mp_int t; mp_int* r = d; + mp_digit u, borrow; + volatile mp_digit mask; + volatile mp_digit mask_a = (mp_digit)-1; + volatile mp_digit mask_b = (mp_digit)-1; + int i; + + if ((a == NULL) || (b == NULL) || (c == NULL) || (d == NULL)) { + return MP_VAL; + } + + if ((c->used == 0) || (a->used > c->used) || (b->used > c->used) || + (a->sign == MP_NEG) || (b->sign == MP_NEG)) { + return MP_VAL; + } if (c == d) { r = &t; - if ((res = mp_init (r)) != MP_OKAY) { return res; } } - res = mp_add (a, b, r); - if (res == MP_OKAY) { - if (mp_cmp (r, c) != MP_LT) { - res = mp_sub (r, c, d); - } else if (c == d) { - res = mp_copy (r, d); - } + if ((res = mp_grow(r, c->used + 1)) != MP_OKAY) { + if (c == d) { mp_clear(r); } + return res; + } + + u = 0; + for (i = 0; i < c->used; i++) { + /* Limbs at and above used are masked off rather than skipped. */ + mask_a += (mp_digit)(i == a->used); + mask_b += (mp_digit)(i == b->used); + r->dp[i] = (mp_load_ct(a, i) & mask_a) + (mp_load_ct(b, i) & mask_b) + + u; + u = r->dp[i] >> DIGIT_BIT; + r->dp[i] &= MP_MASK; + } + r->dp[c->used] = u; + + borrow = 0; + for (i = 0; i < c->used; i++) { + mp_digit val = r->dp[i] - c->dp[i] - borrow; + borrow = val >> (CHAR_BIT * sizeof(mp_digit) - 1); + } + { + mp_digit val = r->dp[c->used] - borrow; + borrow = val >> (CHAR_BIT * sizeof(mp_digit) - 1); + } + + mask = (mp_digit)0 - (1 - borrow); + + borrow = 0; + for (i = 0; i < c->used; i++) { + mp_digit val = r->dp[i] - (c->dp[i] & mask) - borrow; + r->dp[i] = val & MP_MASK; + borrow = val >> (CHAR_BIT * sizeof(mp_digit) - 1); } + /* Clear up to alloc, not the old used, which may follow a secret. */ + r->used = c->used; + for (i = c->used; i < r->alloc; i++) { + r->dp[i] = 0; + } + r->sign = MP_ZPOS; + + /* Copy before clamping: mp_copy() only walks r->used limbs. */ if (c == d) { - mp_clear (r); + res = mp_copy(r, d); + mp_clear(r); + } + if (res == MP_OKAY) { + res = mp_clamp_ct(d); } return res; From 458e179c11a3952a18c8fcca2b7a4bda8417a7f2 Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Fri, 9 Oct 2026 13:36:40 -0600 Subject: [PATCH 06/10] Harden ConstantCompare() and use it for the ATECC GCM tag check --- .wolfssl_known_macro_extras | 1 + wolfcrypt/src/memory.c | 2 +- wolfcrypt/src/misc.c | 6 +++++- wolfcrypt/src/port/atmel/atmel.c | 2 +- wolfssl/wolfcrypt/memory.h | 2 +- wolfssl/wolfcrypt/settings.h | 6 ++++-- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index 8541dd89a91..ea0a9a0004b 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -1075,6 +1075,7 @@ WOLFSSL_NO_CHACHA20_POLY1305_FUSED_IFMA WOLFSSL_NO_CHACHA20_POLY1305_SHORT WOLFSSL_NO_CHACHA20_POLY1305_SMALL_ASM WOLFSSL_NO_CLIENT_CERT_ERROR +WOLFSSL_NO_CONST_CMP WOLFSSL_NO_COPY_KEY WOLFSSL_NO_CRL_DATE_CHECK WOLFSSL_NO_CRL_NEXT_DATE diff --git a/wolfcrypt/src/memory.c b/wolfcrypt/src/memory.c index 9151ccbe278..08ec3c4a0c6 100644 --- a/wolfcrypt/src/memory.c +++ b/wolfcrypt/src/memory.c @@ -1748,7 +1748,7 @@ void wc_ForceZero(void *mem, size_t len) } #endif -#ifndef WOLFSSL_NO_CONST_CMP +#if !defined(WOLFSSL_NO_CONST_CMP) && !defined(WC_NO_CONST_CMP_API) /* Exported version of ConstantCompare(). */ int wc_ConstantCompare(const byte* a, const byte* b, int length) diff --git a/wolfcrypt/src/misc.c b/wolfcrypt/src/misc.c index f4b81fb8373..3ba70d537c2 100644 --- a/wolfcrypt/src/misc.c +++ b/wolfcrypt/src/misc.c @@ -832,9 +832,13 @@ WC_MISC_STATIC WC_INLINE int ConstantCompare(const byte* a, const byte* b, { int i; int compareSum = 0; + /* Volatile loads keep the compiler from turning this into an early-exit + * memcmp() or a vectorised compare-and-branch. */ + const volatile byte* va = (const volatile byte*)a; + const volatile byte* vb = (const volatile byte*)b; for (i = 0; i < length; i++) { - compareSum |= a[i] ^ b[i]; + compareSum |= va[i] ^ vb[i]; } return compareSum; diff --git a/wolfcrypt/src/port/atmel/atmel.c b/wolfcrypt/src/port/atmel/atmel.c index a644432becf..b27eed6f34c 100644 --- a/wolfcrypt/src/port/atmel/atmel.c +++ b/wolfcrypt/src/port/atmel/atmel.c @@ -2037,7 +2037,7 @@ static int wc_Microchip_AesGcmCommon(Aes* aes, byte* out, const byte* in, in, sz, out, tag_buf); if (status == ATCA_SUCCESS) { copy_sz = authTagSz; - if (XMEMCMP(tag_buf, authTag, copy_sz) != 0) { + if (ConstantCompare(tag_buf, authTag, (int)copy_sz) != 0) { return AES_GCM_AUTH_E; } } diff --git a/wolfssl/wolfcrypt/memory.h b/wolfssl/wolfcrypt/memory.h index 363eb8721ec..64358d28868 100644 --- a/wolfssl/wolfcrypt/memory.h +++ b/wolfssl/wolfcrypt/memory.h @@ -341,7 +341,7 @@ WOLFSSL_LOCAL void wc_MemZero_Check(void* addr, size_t len); WOLFSSL_API void wc_ForceZero(void *mem, size_t len); #endif -#ifndef WOLFSSL_NO_CONST_CMP +#if !defined(WOLFSSL_NO_CONST_CMP) && !defined(WC_NO_CONST_CMP_API) WOLFSSL_API int wc_ConstantCompare(const byte* a, const byte* b, int length); #endif diff --git a/wolfssl/wolfcrypt/settings.h b/wolfssl/wolfcrypt/settings.h index 6564f4aa52a..a00eed2fa3e 100644 --- a/wolfssl/wolfcrypt/settings.h +++ b/wolfssl/wolfcrypt/settings.h @@ -5332,10 +5332,12 @@ blinding by defining WC_BLINDING_NO_RNG_ACKNOWLEDGE_WEAKNESS." #define WOLFSSL_NO_CT_OPS #endif +/* Minimal wolfCrypt-only builds leave out the exported wc_ConstantCompare(). + * The internal ConstantCompare() stays available to its callers. */ #if defined(WOLFCRYPT_ONLY) && defined(NO_AES) && !defined(HAVE_CURVE25519) && \ !defined(HAVE_CURVE448) && defined(WC_NO_RNG) && defined(WC_NO_RSA_OAEP) - #undef WOLFSSL_NO_CONST_CMP - #define WOLFSSL_NO_CONST_CMP + #undef WC_NO_CONST_CMP_API + #define WC_NO_CONST_CMP_API #endif #if defined(WOLFCRYPT_ONLY) && defined(NO_AES) && !defined(WOLFSSL_SHA384) && \ From c618c6dc1a55424136f68f6cace595bbfe71c42f Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 4 Nov 2026 17:03:22 -0800 Subject: [PATCH 07/10] Use the constant time map for secret scalars, and Fermat inverses in SAKKE --- src/pk_ec.c | 3 ++ wolfcrypt/src/ecc.c | 6 ++-- wolfcrypt/src/eccsi.c | 2 +- wolfcrypt/src/sakke.c | 66 +++++++++++++++++++++++++++++++++---------- 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/pk_ec.c b/src/pk_ec.c index 91da8ed6fb5..da1e8d5257c 100644 --- a/src/pk_ec.c +++ b/src/pk_ec.c @@ -2365,6 +2365,9 @@ static int ec_mul2add(ecc_point* r, ecc_point* b, mp_int* n, ecc_point* q, * * r = base point * n + q * m * + * With both n and q/m given the sum uses Shamir's trick, which is variable + * time: do not pass secret scalars that way. + * * @param [in] curveIdx Index of curve in ecc_set. * @param [out] r EC point that is result of operation. * @param [in] n Scalar to multiply by base point. May be NULL. diff --git a/wolfcrypt/src/ecc.c b/wolfcrypt/src/ecc.c index caa1c260569..ba69a3ea29f 100644 --- a/wolfcrypt/src/ecc.c +++ b/wolfcrypt/src/ecc.c @@ -4050,7 +4050,8 @@ int wc_ecc_mulmod_ex(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, #endif /* map R back from projective space */ if (err == MP_OKAY && map) - err = ecc_map(R, modulus, mp); + /* The scalar may be secret. Constant time if compiled in. */ + err = ecc_map_ex(R, modulus, mp, 1); exit: @@ -4283,7 +4284,8 @@ int wc_ecc_mulmod_ex2(const mp_int* k, ecc_point* G, ecc_point* R, mp_int* a, #endif /* map R back from projective space */ if (err == MP_OKAY && map) - err = ecc_map(R, modulus, mp); + /* The scalar may be secret. Constant time if compiled in. */ + err = ecc_map_ex(R, modulus, mp, 1); exit: diff --git a/wolfcrypt/src/eccsi.c b/wolfcrypt/src/eccsi.c index 60000ce8289..c6bbe05e96e 100644 --- a/wolfcrypt/src/eccsi.c +++ b/wolfcrypt/src/eccsi.c @@ -1395,7 +1395,7 @@ static int eccsi_mulmod_base_add(EccsiKey* key, const mp_int* n, ¶ms->prime, mp); } if ((err == 0) && map) { - err = ecc_map(res, ¶ms->prime, mp); + err = ecc_map_ex(res, ¶ms->prime, mp, 1); } } #else diff --git a/wolfcrypt/src/sakke.c b/wolfcrypt/src/sakke.c index edac8f05001..b145767f890 100644 --- a/wolfcrypt/src/sakke.c +++ b/wolfcrypt/src/sakke.c @@ -422,7 +422,7 @@ static int sakke_mulmod_base_add(SakkeKey* key, const mp_int* n, ecc_point* a, } if ((err == 0) && map) { /* Map result back to affine coordinates. */ - err = ecc_map(res, ¶ms->prime, mp); + err = ecc_map_ex(res, ¶ms->prime, mp, 1); } return err; @@ -1335,6 +1335,54 @@ static int sakke_load_pairing_base(SakkeKey* key) return err; } +#if defined(WOLFCRYPT_SAKKE_CLIENT) && !defined(WOLFSSL_HAVE_SP_ECC) +/* + * Convert a value from PF_p[q] to F_p: r = y / x. + * + * The ordinates are derived from secrets, so invert with Fermat's little + * theorem over the public prime when the math library has it. + * + * @param [in] v Value in PF_p[q] with ordinates in Montgomery form. + * y ordinate is modified. + * @param [in] prime Prime modulus. + * @param [in] mp Montgomery multiplier. + * @param [out] r MP integer to hold the result. + * @return 0 on success. + * @return MEMORY_E when dynamic memory allocation fails. + * @return Other -ve value on internal failure. + */ +static int sakke_proj_to_fp(mp_proj* v, mp_int* prime, mp_digit mp, mp_int* r) +{ + int err = 0; + +#if defined(ECC_TIMING_RESISTANT) && (defined(USE_FAST_MATH) || \ + defined(WOLFSSL_SP_MATH_ALL)) + /* Not every math library's constant time inversion rejects zero. */ + if (mp_iszero(v->x)) { + err = MP_VAL; + } + if (err == 0) { + err = mp_invmod_mont_ct(v->x, prime, r, mp); + } + if (err == 0) { + err = mp_montgomery_reduce(r, prime, mp); + } + if (err == 0) { + err = mp_montgomery_reduce(v->y, prime, mp); + } +#else + (void)mp; + /* The Montgomery factors of x and y cancel. */ + err = mp_invmod(v->x, prime, r); +#endif + if (err == 0) { + err = mp_mulmod(r, v->y, prime, r); + } + + return err; +} +#endif + #ifndef WOLFSSL_HAVE_SP_ECC /* * Put point into Montgomery form. @@ -2450,10 +2498,7 @@ static int sakke_pairing(SakkeKey* key, ecc_point* p, ecc_point* q, mp_int* r, } /* Convert from PF_p[q] to F_p */ if (err == 0) { - err = mp_invmod(v->x, prime, r); - } - if (err == 0) { - err = mp_mulmod(r, v->y, prime, r); + err = sakke_proj_to_fp(v, prime, mp, r); } if (err == 0) { @@ -6235,18 +6280,9 @@ static int sakke_modexp(SakkeKey* key, mp_int* b, mp_int* e, mp_int* r) err = sakke_modexp_loop(key, b, e, c, mp); } - if (err == 0) { - err = mp_montgomery_reduce(c->x, prime, mp); - } - if (err == 0) { - err = mp_montgomery_reduce(c->y, prime, mp); - } /* Convert value back from PF_p[q] to F_p* */ if (err == 0) { - err = mp_invmod(c->x, prime, c->x); - } - if (err == 0) { - err = mp_mulmod(c->x, c->y, prime, r); + err = sakke_proj_to_fp(c, prime, mp, r); } return err; From d825f9005e3b3e577f4956c83946a9a9e803c269 Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 4 Nov 2026 17:03:22 -0800 Subject: [PATCH 08/10] Blind the operand of wolfSSL_BN_mod_inverse --- src/ssl_bn.c | 87 +++++++++++++++++++++++++++++++++++++++- tests/api/test_ossl_bn.c | 20 +++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/src/ssl_bn.c b/src/ssl_bn.c index ea1355cb4b3..d9bb9de66f2 100644 --- a/src/ssl_bn.c +++ b/src/ssl_bn.c @@ -1969,10 +1969,90 @@ int wolfSSL_BN_mod_exp(WOLFSSL_BIGNUM *r, const WOLFSSL_BIGNUM *a, return ret; } +/* Number of random blinding factors to try. A modulus with many small factors + * rejects most of them. */ +#define BN_INVMOD_BLIND_TRIES 128 + +/* Compute r = a^-1 mod m with a blinded by a random factor. + * + * The factor is tested for an inverse by itself, so that whether one is used + * does not depend on a, and a failure with it means that a has no inverse. + * Invert directly when there is no random number generator or no factor found: + * this API has no way to ask for blinding, so it must not start failing for + * lack of it. r may alias a or m. + * + * @param [in] a MP integer to invert. + * @param [in] m Big number that is the modulus. + * @param [out] r MP integer to hold the result. + * @return MP_OKAY on success. + * @return Other value on error, including when there is no inverse. + */ +static int wolfssl_bn_invmod_blind(mp_int* a, const WOLFSSL_BIGNUM* m, + mp_int* r) +{ + mp_int* mod = (mp_int*)m->internal; + int err = MP_OKAY; + int found = 0; + WOLFSSL_BIGNUM* t = wolfSSL_BN_new(); +#ifndef WC_NO_RNG + int i; + WOLFSSL_BIGNUM* b = wolfSSL_BN_new(); + + if ((t == NULL) || (b == NULL)) { + err = MEMORY_E; + } + if ((err == MP_OKAY) && (mp_cmp_d(mod, 2) == MP_GT)) { + for (i = 0; (err == MP_OKAY) && (!found) && + (i < BN_INVMOD_BLIND_TRIES) && + (wolfSSL_BN_rand_range(b, m) == 1); i++) { + if (mp_iszero((mp_int*)b->internal)) { + continue; + } + err = mp_invmod((mp_int*)b->internal, mod, (mp_int*)t->internal); + if (err == MP_OKAY) { + found = 1; + } + else if (err == WC_NO_ERR_TRACE(MP_VAL)) { + err = MP_OKAY; + } + } + } + if ((err == MP_OKAY) && found) { + err = mp_mulmod(a, (mp_int*)b->internal, mod, (mp_int*)t->internal); + if (err == MP_OKAY) { + err = mp_invmod((mp_int*)t->internal, mod, (mp_int*)t->internal); + } + if (err == MP_OKAY) { + err = mp_mulmod((mp_int*)t->internal, (mp_int*)b->internal, mod, + (mp_int*)t->internal); + } + } +#else + if (t == NULL) { + err = MEMORY_E; + } +#endif + if ((err == MP_OKAY) && (!found)) { + err = mp_invmod(a, mod, (mp_int*)t->internal); + } + if (err == MP_OKAY) { + err = mp_copy((mp_int*)t->internal, r); + } + +#ifndef WC_NO_RNG + wolfSSL_BN_clear_free(b); +#endif + wolfSSL_BN_clear_free(t); + + return err; +} + /* Calculate the modular inverse of a mod m into r. r = a^-1 mod m * * A new big number is allocated when r is NULL. * + * The operand is always blinded; BN_FLG_CONSTTIME is not needed or checked. + * * @param [in, out] r Big number to hold result. May be NULL. * @param [in] a Big number to invert. * @param [in] m Big number that is the modulus. @@ -2009,8 +2089,8 @@ WOLFSSL_BIGNUM *wolfSSL_BN_mod_inverse(WOLFSSL_BIGNUM *r, WOLFSSL_BIGNUM *a, } /* Compute inverse of a modulo n and return in r */ - if ((!err) && (mp_invmod((mp_int *)a->internal, (mp_int *)m->internal, - (mp_int*)r->internal) != MP_OKAY)) { + if ((!err) && (wolfssl_bn_invmod_blind((mp_int *)a->internal, + (const WOLFSSL_BIGNUM*)m, (mp_int*)r->internal) != MP_OKAY)) { WOLFSSL_MSG("mp_invmod() error"); err = 1; } @@ -2153,6 +2233,9 @@ int wolfSSL_BN_rand(WOLFSSL_BIGNUM* bn, int bits, int top, int bottom) ret = 0; } /* Dispose of buffer - no longer needed. */ + if (buff != NULL) { + ForceZero(buff, len); + } XFREE(buff, NULL, DYNAMIC_TYPE_TMP_BUFFER); /* Truncate to requested bit length when not a whole number of bytes. */ diff --git a/tests/api/test_ossl_bn.c b/tests/api/test_ossl_bn.c index 46d3f67cd40..f288f34596c 100644 --- a/tests/api/test_ossl_bn.c +++ b/tests/api/test_ossl_bn.c @@ -734,6 +734,7 @@ int test_wolfSSL_BN_math_mod(void) BN_ULONG val1; BN_ULONG val2; BN_ULONG val3; + int i; /* Setup */ XMEMSET(&emptyBN, 0, sizeof(emptyBN)); @@ -846,6 +847,25 @@ int test_wolfSSL_BN_math_mod(void) ExpectIntEQ(BN_set_word(a, 3), 1); ExpectIntEQ(BN_set_word(m, 9), 1); ExpectNull(BN_mod_inverse(r, a, m, NULL)); + /* Composite modulus with small factors: blinding factors that share one + * have no inverse, so the true inverse must still be found. */ + ExpectIntEQ(BN_set_word(a, 11), 1); + ExpectIntEQ(BN_set_word(m, 210), 1); + for (i = 0; i < 64; i++) { + ExpectNotNull(BN_mod_inverse(r, a, m, NULL)); + ExpectIntEQ(BN_is_word(r, 191), 1); + } + /* Result may share storage with the operand or the modulus. */ + ExpectNotNull(BN_mod_inverse(a, a, m, NULL)); + ExpectIntEQ(BN_is_word(a, 191), 1); + ExpectIntEQ(BN_set_word(a, 11), 1); + ExpectNotNull(BN_mod_inverse(m, a, m, NULL)); + ExpectIntEQ(BN_is_word(m, 191), 1); + /* Modulus too small to blind with. */ + ExpectIntEQ(BN_set_word(a, 1), 1); + ExpectIntEQ(BN_set_word(m, 2), 1); + ExpectNotNull(BN_mod_inverse(r, a, m, NULL)); + ExpectIntEQ(BN_is_word(r, 1), 1); BN_free(r); BN_free(m); From 51fdd2ed2ab005b6a981059fbd9f15a63c6686b2 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 5 Nov 2026 07:31:34 -0800 Subject: [PATCH 09/10] Use a constant time comb for secret scalars with the FP_ECC cache --- .github/configs/os-check-linux.json | 2 +- tests/api/test_ecc.c | 81 ++++++- tests/api/test_ecc.h | 2 + tests/unit-mcdc/test_ecc_fault_whitebox.c | 19 +- tests/unit-mcdc/test_ecc_whitebox.c | 118 ++++++---- wolfcrypt/src/ecc.c | 274 +++++++++++++++++----- 6 files changed, 371 insertions(+), 125 deletions(-) diff --git a/.github/configs/os-check-linux.json b/.github/configs/os-check-linux.json index 13bee57af74..a3baa196383 100644 --- a/.github/configs/os-check-linux.json +++ b/.github/configs/os-check-linux.json @@ -603,7 +603,7 @@ "check": false, "run": [["./wolfcrypt/test/testwolfcrypt"]]}, {"name": "fpecc-no-shamir", - "comment": "FP_ECC with ECC_TIMING_RESISTANT but no ECC_SHAMIR: nothing consumes the fixed-point cache, so its table, lock and helpers are compiled out.", + "comment": "FP_ECC with ECC_TIMING_RESISTANT but no ECC_SHAMIR: only the constant-time comb consumes the fixed-point cache.", "configure": ["--enable-fpecc", "--disable-eccshamir"]}, {"name": "sp-math-sakke", "comment": "SAKKE on WOLFSSL_SP_MATH without RSA, DH, or ECCSI: its blinded RSK inverse needs sp_mulmod, which nothing else builds in this shape.", diff --git a/tests/api/test_ecc.c b/tests/api/test_ecc.c index ef5e1940897..8f3271500c4 100644 --- a/tests/api/test_ecc.c +++ b/tests/api/test_ecc.c @@ -1735,7 +1735,7 @@ int test_wc_ecc_import_private_key(void) * Testing wc_ecc_make_pub_ex() with private key d = n - 1 on secp256k1, whose * public key is -G. The timing-resistant ladder ends by adding two points * that are negatives of each other; the result must still be -G, including - * when FP_ECC routes the secret scalar around its cache. + * when FP_ECC takes the secret scalar through its cached comb. */ int test_wc_ecc_make_pub_order_minus_1(void) { @@ -1808,6 +1808,85 @@ int test_wc_ecc_make_pub_order_minus_1(void) return EXPECT_RESULT(); } /* END test_wc_ecc_make_pub_order_minus_1 */ +/* + * Testing that FP_ECC gives the same public key once its table is built as + * from the first, uncached, multiplications. + */ +int test_wc_ecc_fp_cached_matches_uncached(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ECC) && defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) && \ + defined(HAVE_ECC_KEY_IMPORT) && defined(HAVE_ECC_KEY_EXPORT) && \ + !defined(WC_NO_RNG) && !defined(NO_ECC256) && !defined(NO_ECC_SECP) && \ + !defined(HAVE_SELFTEST) && !defined(NO_ECC_MAKE_PUB) && \ + !defined(WOLFSSL_NO_ECC_SW) && !defined(WOLF_CRYPTO_CB_ONLY_ECC) && \ + !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A) && \ + !defined(WOLFSSL_MICROCHIP_TA100) && !defined(WOLFSSL_CRYPTOCELL) && \ + !defined(WOLFSSL_KCAPI_ECC) && !defined(WOLFSSL_SILABS_SE_ACCEL) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(6,0,0)) + ecc_key key; + WC_RNG rng; + /* 1, 2, the top bit alone, the P-256 order - 1, and 2^43, for which with + * the default FP_LUT the comb's accumulator equals the table entry being + * added. */ + static const byte d[5][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 }, + { 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, 2 }, + { 0x80, 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 }, + { 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, + 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x50 }, + { 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, 0x08, 0, 0, 0, 0, 0 } + }; + byte firstX[32]; + byte firstY[32]; + byte qx[32]; + byte qy[32]; + word32 qxSz; + word32 qySz; + int i; + int j; + + XMEMSET(&rng, 0, sizeof(WC_RNG)); + ExpectIntEQ(wc_InitRng(&rng), 0); + + for (i = 0; (i < 5) && EXPECT_SUCCESS(); i++) { + wc_ecc_fp_free(); + XMEMSET(&key, 0, sizeof(ecc_key)); + ExpectIntEQ(wc_ecc_init(&key), 0); + ExpectIntEQ(wc_ecc_import_private_key_ex(d[i], (word32)sizeof(d[i]), + NULL, 0, &key, ECC_SECP256R1), 0); + for (j = 0; (j < 4) && EXPECT_SUCCESS(); j++) { + PRIVATE_KEY_UNLOCK(); + ExpectIntEQ(wc_ecc_make_pub_ex(&key, NULL, &rng), 0); + PRIVATE_KEY_LOCK(); + qxSz = (word32)sizeof(qx); + qySz = (word32)sizeof(qy); + ExpectIntEQ(wc_ecc_export_public_raw(&key, qx, &qxSz, qy, &qySz), + 0); + if (j == 0) { + XMEMCPY(firstX, qx, sizeof(qx)); + XMEMCPY(firstY, qy, sizeof(qy)); + } + else { + ExpectBufEQ(qx, firstX, sizeof(qx)); + ExpectBufEQ(qy, firstY, sizeof(qy)); + } + } + wc_ecc_free(&key); + } + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_ecc_fp_free(); +#endif + return EXPECT_RESULT(); +} /* END test_wc_ecc_fp_cached_matches_uncached */ + /* * Testing wc_ecc_export_private_only() */ diff --git a/tests/api/test_ecc.h b/tests/api/test_ecc.h index be1c7e77310..38ed1311d01 100644 --- a/tests/api/test_ecc.h +++ b/tests/api/test_ecc.h @@ -50,6 +50,7 @@ int test_wc_ecc_import_x963_oversized(void); int test_wc_ecc_import_x963_non_canonical(void); int test_wc_ecc_import_private_key(void); int test_wc_ecc_make_pub_order_minus_1(void); +int test_wc_ecc_fp_cached_matches_uncached(void); int test_wc_ecc_export_private_only(void); int test_wc_ecc_rs_to_sig(void); int test_wc_ecc_import_raw(void); @@ -116,6 +117,7 @@ int test_wc_ecc_make_key_id_dev(void); TEST_DECL_GROUP("ecc", test_wc_ecc_import_x963_non_canonical), \ TEST_DECL_GROUP("ecc", test_wc_ecc_import_private_key), \ TEST_DECL_GROUP("ecc", test_wc_ecc_make_pub_order_minus_1), \ + TEST_DECL_GROUP("ecc", test_wc_ecc_fp_cached_matches_uncached), \ TEST_DECL_GROUP("ecc", test_wc_ecc_export_private_only), \ TEST_DECL_GROUP("ecc", test_wc_ecc_rs_to_sig), \ TEST_DECL_GROUP("ecc", test_wc_ecc_import_raw), \ diff --git a/tests/unit-mcdc/test_ecc_fault_whitebox.c b/tests/unit-mcdc/test_ecc_fault_whitebox.c index c5e9dbd8363..caff82774ba 100644 --- a/tests/unit-mcdc/test_ecc_fault_whitebox.c +++ b/tests/unit-mcdc/test_ecc_fault_whitebox.c @@ -321,8 +321,7 @@ static void wb_fault_sqrtmod_prime_direct(void) * leave a poisoned cache behind for the vector after it. * ========================================================================= */ #if defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) && \ - !defined(WOLFSSL_NO_MALLOC) && \ - (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) + !defined(WOLFSSL_NO_MALLOC) typedef struct wb_fp_ctx { mp_int a; @@ -536,15 +535,13 @@ static void wb_fp_build_lut_and_mul(wb_fp_ctx* c) wc_ecc_fp_free(); if (add_entry(0, c->G) == MP_OKAY && build_lut(0, &c->a, &c->prime, mp, &c->mu) == MP_OKAY) { - #ifndef ECC_TIMING_RESISTANT /* accepting row first, then the fault sweep over the LUT-copy chain */ - (void)accel_fp_mul(0, &c->k, c->R, &c->a, &c->prime, mp, 1); + (void)accel_fp_mul(0, &c->k, c->R, &c->a, &c->prime, mp, NULL, 1); for (n = 1; n <= 8; n++) { mcdc_fm_arm(n); - (void)accel_fp_mul(0, &c->k, c->R, &c->a, &c->prime, mp, 1); + (void)accel_fp_mul(0, &c->k, c->R, &c->a, &c->prime, mp, NULL, 1); mcdc_fm_disarm(); } - #endif } else { wb_fail = 1; @@ -572,7 +569,6 @@ static void wb_fp_lru_and_lutset(wb_fp_ctx* c) * (guard TRUE, builds it) and lru 3 with the LUT set (guard FALSE on * LUT_set). */ -#ifndef ECC_TIMING_RESISTANT /* --- wc_ecc_mulmod_ex --- */ wc_ecc_fp_free(); for (i = 0; i < 3; i++) @@ -600,7 +596,6 @@ static void wb_fp_lru_and_lutset(wb_fp_ctx* c) else { wb_fail = 1; } -#endif #ifdef ECC_SHAMIR /* --- ecc_mul2add: warmed through itself, so both LUT-build guards see @@ -657,7 +652,6 @@ static void wb_fp_lru_and_lutset(wb_fp_ctx* c) (void)n; #endif -#ifndef ECC_TIMING_RESISTANT /* --- err == MP_OKAY FALSE halves (armed add_entry) --- */ wc_ecc_fp_free(); mcdc_fm_arm(1); @@ -669,7 +663,6 @@ static void wb_fp_lru_and_lutset(wb_fp_ctx* c) (void)wc_ecc_mulmod_ex2(&c->k, c->G, c->R, &c->a, &c->prime, &c->order, NULL, 1, NULL); mcdc_fm_disarm(); -#endif #ifdef ECC_SHAMIR wc_ecc_fp_free(); @@ -729,11 +722,9 @@ static void wb_fp_cache_suite(void) #else static void wb_fp_cache_suite(void) { - WB_NOTE("FP_ECC off (or no-malloc, or timing-resistant without Shamir); " - "fixed-point cache suite skipped"); + WB_NOTE("FP_ECC off (or no-malloc build); fixed-point cache suite skipped"); } -#endif /* FP_ECC && !WOLFSSL_SP_MATH && !WOLFSSL_NO_MALLOC && - * (!ECC_TIMING_RESISTANT || ECC_SHAMIR) */ +#endif /* FP_ECC && !WOLFSSL_SP_MATH && !WOLFSSL_NO_MALLOC */ /* ========================================================================= * Degenerate point operands. diff --git a/tests/unit-mcdc/test_ecc_whitebox.c b/tests/unit-mcdc/test_ecc_whitebox.c index eca233d9621..a2fb8f7c537 100644 --- a/tests/unit-mcdc/test_ecc_whitebox.c +++ b/tests/unit-mcdc/test_ecc_whitebox.c @@ -1113,10 +1113,8 @@ static void wb_projective_safe_special_cases(void) } /* find_hole()/find_base()/add_entry() and fp_cache[] itself only exist - * inside ecc.c's own #ifdef FP_ECC (+ !WOLFSSL_SP_MATH) block, and only when - * something consumes the cache (!ECC_TIMING_RESISTANT or ECC_SHAMIR). */ -#if defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) && \ - (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) + * inside ecc.c's own #ifdef FP_ECC (+ !WOLFSSL_SP_MATH) block. */ +#if defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) /* ------------------------------------------------------------------------- * * Class 17: FP_ECC fixed-point cache internals: find_base() (line ~13585), * find_hole() (line ~13548). Both are file-static and process-global @@ -1198,30 +1196,27 @@ static void wb_fp_cache_internals(void) #else static void wb_fp_cache_internals(void) { - WB_NOTE("FP_ECC off (or WOLFSSL_SP_MATH, or timing-resistant without " - "Shamir); fp_cache internals skipped"); + WB_NOTE("FP_ECC off (or WOLFSSL_SP_MATH); fp_cache internals skipped"); } -#endif /* FP_ECC && !WOLFSSL_SP_MATH && - * (!ECC_TIMING_RESISTANT || ECC_SHAMIR) */ +#endif /* FP_ECC && !WOLFSSL_SP_MATH */ #if defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) && \ - defined(ECC_TIMING_RESISTANT) && defined(ECC_SHAMIR) + defined(ECC_TIMING_RESISTANT) /* ------------------------------------------------------------------------- * - * Timing-resistant wc_ecc_mulmod_ex()/wc_ecc_mulmod_ex2() take secret - * scalars, and the FP-cache LUT is not constant-time, so they must never - * add, warm or consume a cache entry. Three calls each on one base point - * would have built a LUT (lru_count >= 2) on the cached path; the cache must - * still be empty afterwards. + * Timing-resistant wc_ecc_mulmod_ex()/wc_ecc_mulmod_ex2() take secret scalars + * through fp_comb_ct() once a base point's table is built. The first calls run + * the ladder, so each scalar's cached result must equal its uncached one, + * including the scalars that make the comb's accumulator or table entry the + * point at infinity. * ------------------------------------------------------------------------- */ -static void wb_fp_cache_tr_bypass(void) +static void wb_fp_comb_ct(void) { int idx = wc_ecc_get_curve_idx(ECC_SECP256R1); const ecc_set_type* cs; - ecc_point *G = NULL, *R = NULL; + ecc_point *G = NULL, *R = NULL, *L = NULL; mp_int k, a, prime, order; - unsigned x; - int i; - int touched = 0; + int i, s; + int base; if (idx == ECC_CURVE_INVALID) { wb_fail = 1; @@ -1232,56 +1227,81 @@ static void wb_fp_cache_tr_bypass(void) wb_fail = 1; return; } - - wc_ecc_fp_free(); /* deterministic empty-cache start */ - if (mp_init_multi(&k, &a, &prime, &order, NULL, NULL) != MP_OKAY) { wb_fail = 1; return; } G = wc_ecc_new_point(); R = wc_ecc_new_point(); - if ((G == NULL) || (R == NULL) || + L = wc_ecc_new_point(); + if ((G == NULL) || (R == NULL) || (L == NULL) || (mp_read_radix(&a, cs->Af, MP_RADIX_HEX) != MP_OKAY) || (mp_read_radix(&prime, cs->prime, MP_RADIX_HEX) != MP_OKAY) || (mp_read_radix(&order, cs->order, MP_RADIX_HEX) != MP_OKAY) || (mp_read_radix(G->x, cs->Gx, MP_RADIX_HEX) != MP_OKAY) || (mp_read_radix(G->y, cs->Gy, MP_RADIX_HEX) != MP_OKAY) || - (mp_set(G->z, 1) != MP_OKAY) || - (mp_set(&k, 0x1234567) != MP_OKAY)) { + (mp_set(G->z, 1) != MP_OKAY)) { wb_fail = 1; goto out; } - for (i = 0; i < 3; i++) { - if (wc_ecc_mulmod_ex(&k, G, R, &a, &prime, 1, NULL) != MP_OKAY) { - WB_NOTE("timing-resistant wc_ecc_mulmod_ex failed"); - wb_fail = 1; + /* 1, 2, a small value, order - 1, order, the top bit alone, and 2^gap for + * which the accumulator equals the table entry being added. */ + for (s = 0; s < 7; s++) { + int ok; + if (s == 3 || s == 4) { + ok = (mp_copy(&order, &k) == MP_OKAY); + if (ok && (s == 3)) { + ok = (mp_sub_d(&k, 1, &k) == MP_OKAY); + } + } + else if (s == 5) { + mp_zero(&k); + ok = (mp_set_bit(&k, 255) == MP_OKAY); + } + else if (s == 6) { + mp_zero(&k); + ok = (mp_set_bit(&k, (256 + FP_LUT - 1) / FP_LUT) == MP_OKAY); + } + else { + ok = (mp_set(&k, (s == 2) ? 0x1234567 : (mp_digit)(s + 1)) == + MP_OKAY); } - if (wc_ecc_mulmod_ex2(&k, G, R, &a, &prime, &order, NULL, 1, - NULL) != MP_OKAY) { - WB_NOTE("timing-resistant wc_ecc_mulmod_ex2 failed"); + if (!ok) { wb_fail = 1; + goto out; } - } - if (find_base(G) != -1) { - touched = 1; - } - for (x = 0; x < FP_ENTRIES; x++) { - if (fp_cache[x].g != NULL) { - touched = 1; + wc_ecc_fp_free(); + for (i = 0; i < 4; i++) { + ecc_point* dst = (i == 0) ? L : R; + int err = (i & 1) ? + wc_ecc_mulmod_ex2(&k, G, dst, &a, &prime, &order, NULL, 1, + NULL) : + wc_ecc_mulmod_ex(&k, G, dst, &a, &prime, 1, NULL); + if (err != MP_OKAY) { + WB_NOTE("timing-resistant mulmod failed"); + wb_fail = 1; + } + } + base = find_base(G); + if (FP_MULMOD_USES_LUT && + ((base < 0) || (!fp_cache[base].LUT_set))) { + WB_NOTE("fp_cache table was not built"); + wb_fail = 1; + } + if ((mp_cmp(R->x, L->x) != MP_EQ) || (mp_cmp(R->y, L->y) != MP_EQ) || + (mp_cmp(R->z, L->z) != MP_EQ)) { + WB_NOTE("fp_comb_ct result differs from the ladder"); + wb_fail = 1; } } - if (touched) { - WB_NOTE("timing-resistant mulmod touched the fp_cache"); - wb_fail = 1; - } - WB_NOTE("timing-resistant mulmod fp_cache bypass exercised"); + WB_NOTE("timing-resistant fp_comb_ct compared with the ladder"); out: wc_ecc_del_point(G); wc_ecc_del_point(R); + wc_ecc_del_point(L); mp_clear(&k); mp_clear(&a); mp_clear(&prime); @@ -1289,12 +1309,12 @@ static void wb_fp_cache_tr_bypass(void) wc_ecc_fp_free(); } #else -static void wb_fp_cache_tr_bypass(void) +static void wb_fp_comb_ct(void) { - WB_NOTE("FP_ECC off (or WOLFSSL_SP_MATH, or not timing-resistant with " - "Shamir); fp_cache bypass check skipped"); + WB_NOTE("FP_ECC off (or WOLFSSL_SP_MATH, or not timing-resistant); " + "fp_comb_ct check skipped"); } -#endif /* FP_ECC && !WOLFSSL_SP_MATH && ECC_TIMING_RESISTANT && ECC_SHAMIR */ +#endif /* FP_ECC && !WOLFSSL_SP_MATH && ECC_TIMING_RESISTANT */ #ifdef HAVE_ECC_KEY_EXPORT /* ------------------------------------------------------------------------- * @@ -3951,7 +3971,7 @@ int main(void) wb_mul2add_and_bufsize(); wb_projective_safe_special_cases(); wb_fp_cache_internals(); - wb_fp_cache_tr_bypass(); + wb_fp_comb_ct(); wb_export_point_der(); wb_export_x963_internal(); wb_idx_dp_guard_export_paths(); diff --git a/wolfcrypt/src/ecc.c b/wolfcrypt/src/ecc.c index ba69a3ea29f..986d3444b39 100644 --- a/wolfcrypt/src/ecc.c +++ b/wolfcrypt/src/ecc.c @@ -160,6 +160,7 @@ Possible ECC enable options: * FP_ENTRIES: Number of FP cache entries default: 15 * FP_LUT: FP lookup table bit size (2-12). Larger default: 8 * values use more memory but faster verify. + * Defaults to 6 with ECC_TIMING_RESISTANT. * FP_ECC_CONTROL: Auto-selects cached FP ECC verify with default: on * SP when WOLFSSL_HAVE_SP_ECC is available. * @@ -13400,9 +13401,35 @@ int wc_ecc_sig_size(const ecc_key* key) #define FP_ENTRIES 15 #endif +#if defined(USE_INTEGER_HEAP_MATH) && defined(ECC_TIMING_RESISTANT) + /* No constant time conversion of the scalar to bytes: use the ladder. A + * table for mulmod would have no reader, so leave it to ecc_mul2add(). */ + #define FP_MULMOD_USES_LUT 0 + #define FP_K_FITS_COMB(k, m) 0 +#elif defined(ECC_TIMING_RESISTANT) + #define FP_MULMOD_USES_LUT 1 + /* Largest modulus accel_fp_mul()'s scalar buffer holds, in bytes. */ + #ifdef WOLFCRYPT_HAVE_SAKKE + #define FP_COMB_MAX_MOD_SZ 254 + #else + #define FP_COMB_MAX_MOD_SZ 126 + #endif + /* The comb covers the bits of the modulus. An order can be a bit longer. */ + #define FP_K_FITS_COMB(k, m) ((mp_count_bits(k) <= mp_count_bits(m)) && \ + (mp_unsigned_bin_size(m) <= FP_COMB_MAX_MOD_SZ)) +#else + #define FP_MULMOD_USES_LUT 1 + #define FP_K_FITS_COMB(k, m) 1 +#endif + /* number of bits in LUT */ #ifndef FP_LUT - #define FP_LUT 8U + #ifdef ECC_TIMING_RESISTANT + /* Every comb step reads the whole table when the scalar is secret. */ + #define FP_LUT 6U + #else + #define FP_LUT 8U + #endif #endif #ifdef ECC_SHAMIR @@ -13430,10 +13457,6 @@ typedef struct { /* permitted (0) or not (1) */ } fp_cache_t; -#if !defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR) -/* The cache, its lock, the LUT table and the cache-entry helpers below are - * only compiled when something can consume the FP cache: the variable-time - * path, or Shamir's trick in ecc_mul2add(). */ /* if HAVE_THREAD_LS this cache is per thread, no locking needed */ static THREAD_LS_T fp_cache_t fp_cache[FP_ENTRIES]; @@ -14070,11 +14093,9 @@ static int add_entry(int idx, ecc_point *g) return MP_OKAY; } -#endif /* !ECC_TIMING_RESISTANT || ECC_SHAMIR */ #endif #if !defined(WOLFSSL_SP_MATH) -#if !defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR) /* build the LUT by spacing the bits of the input by #modulus/FP_LUT bits apart * * The algorithm builds patterns in increasing bit order by first making all @@ -14231,13 +14252,150 @@ static int build_lut(int idx, mp_int* a, mp_int* modulus, mp_digit mp, return err; } -#endif /* !ECC_TIMING_RESISTANT || ECC_SHAMIR */ -#ifndef ECC_TIMING_RESISTANT -/* perform a fixed point ECC mulmod. Not constant-time; do not use with - * secret scalars. */ +#ifdef ECC_TIMING_RESISTANT +/* Clear n digits of an mp_int that holds at least that many. mp_forcezero() is + * not usable between table reads: it frees the digits of a heap-math mp_int. + */ +static void fp_lut_clear(mp_int* dst, int n) +{ + int i; + + for (i = 0; i < n; i++) { + dst->dp[i] = 0; + } + dst->used = 0; +} + +/* Select dst = src when sel is non-zero without branching on sel, reading the + * same digits of src whatever the value of sel. + * + * dst must have been cleared and hold at least n digits. + */ +static void fp_lut_select(mp_int* dst, mp_int* src, int n, word32 sel) +{ + int i; + /* volatile so that the compiler can't turn the mask back into a branch. */ + volatile mp_digit mask; + + mask = (mp_digit)0 - (mp_digit)(((sel | (0U - sel)) >> 31) & 1U); + for (i = 0; i < n; i++) { + dst->dp[i] |= mp_get_digit(src, i) & mask; + } + dst->used |= (wc_mp_size_t)((mp_digit)src->used & mask); +} + +/* Double and always add over the comb, reading every table entry each step. + * + * kb holds a little-endian scalar of fixed length so that neither its value nor + * its length decide the operations. + * + * Math on a point at infinity is much cheaper than on any other, so neither + * operand of an add may be one because of the scalar. R starts at the base + * point G, which the doublings make 2^gap * G: that is LUT[2], taken off at the + * end. A zero comb digit adds a real point and discards the result. + */ +static int fp_comb_ct(int idx, const unsigned char* kb, unsigned lut_gap, + ecc_point* R, mp_int* a, mp_int* modulus, mp_digit mp, WC_RNG* rng) +{ + int err; + int x; + int inf = 0; + int n = (int)modulus->used; + unsigned y, z, bitpos, j; + ecc_point* Q = NULL; + ecc_point* t = NULL; + + err = wc_ecc_new_point_ex(&Q, NULL); + if (err == MP_OKAY) { + err = wc_ecc_new_point_ex(&t, NULL); + } + if (err == MP_OKAY) { + err = mp_copy(fp_cache[idx].LUT[1]->x, R->x); + } + if (err == MP_OKAY) { + err = mp_copy(fp_cache[idx].LUT[1]->y, R->y); + } + if (err == MP_OKAY) { + err = mp_copy(&fp_cache[idx].mu, R->z); + } + if ((err == MP_OKAY) && (rng != NULL)) { + err = wc_ecc_gen_z(rng, mp_unsigned_bin_size(modulus), R, modulus, mp, + t->x, t->y, t->z); + } + if (err == MP_OKAY) { + err = mp_grow(Q->x, n + 1); + } + if (err == MP_OKAY) { + err = mp_grow(Q->y, n + 1); + } + if (err == MP_OKAY) { + err = mp_copy(&fp_cache[idx].mu, Q->z); + } + + for (x = (int)lut_gap - 1; (err == MP_OKAY) && (x >= 0); x--) { + volatile word32 zeroDigit; + + bitpos = (unsigned)x; + for (y = z = 0; y < FP_LUT; y++) { + z |= (((word32)kb[bitpos>>3U] >> (bitpos&7U)) & 1U) << y; + bitpos += lut_gap; + } + /* 1 when z is zero and otherwise 0 */ + zeroDigit = (((word32)z | (0U - (word32)z)) >> 31) ^ 1U; + + err = ecc_projective_dbl_point_safe(R, R, a, modulus, mp); + + if (err == MP_OKAY) { + fp_lut_clear(Q->x, n); + fp_lut_clear(Q->y, n); + for (j = 1; j < (1U << FP_LUT); j++) { + volatile word32 sel = (word32)(j ^ z); + /* 1 when j == z, or for the dummy when z is zero */ + sel = (((sel | (0U - sel)) >> 31) ^ 1U) | + (zeroDigit & (word32)(j == 1)); + fp_lut_select(Q->x, fp_cache[idx].LUT[j]->x, n, sel); + fp_lut_select(Q->y, fp_cache[idx].LUT[j]->y, n, sel); + } + err = ecc_projective_add_point_safe(R, Q, t, a, modulus, mp, &inf); + } + if (err == MP_OKAY) { + err = ecc_cond_copy_point(t, (int)(zeroDigit ^ 1U), R); + } + } + + /* Take off 2^gap * G. */ + if (err == MP_OKAY) { + err = mp_copy(fp_cache[idx].LUT[2]->x, Q->x); + } + if (err == MP_OKAY) { + err = mp_sub(modulus, fp_cache[idx].LUT[2]->y, Q->y); + } + if (err == MP_OKAY) { + err = ecc_projective_add_point_safe(R, Q, R, a, modulus, mp, &inf); + } + + /* Q's ordinates identify a window of the scalar and t is a partial sum. */ + if (Q != NULL) { + mp_forcezero(Q->x); + mp_forcezero(Q->y); + } + if (t != NULL) { + mp_forcezero(t->x); + mp_forcezero(t->y); + mp_forcezero(t->z); + } + wc_ecc_del_point_ex(t, NULL); + wc_ecc_del_point_ex(Q, NULL); + (void)inf; + + return err; +} +#endif /* ECC_TIMING_RESISTANT */ + +/* perform a fixed point ECC mulmod */ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, - mp_int* modulus, mp_digit mp, int map) + mp_int* modulus, mp_digit mp, WC_RNG* rng, int map) { #ifdef WOLFCRYPT_HAVE_SAKKE #define KB_SIZE 256 @@ -14255,10 +14413,17 @@ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, mp_int order[1]; #endif int x, err; - unsigned y, z = 0, bitlen, bitpos, lut_gap; + unsigned y, z = 0, bitlen, lut_gap; +#ifndef ECC_TIMING_RESISTANT + unsigned bitpos; int first; +#endif int tk_zeroize = 0; +#ifndef ECC_TIMING_RESISTANT + (void)rng; +#endif + #ifdef WOLFSSL_SMALL_STACK tk = (mp_int*)XMALLOC(sizeof(mp_int), NULL, DYNAMIC_TYPE_ECC); if (tk == NULL) { @@ -14308,7 +14473,7 @@ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, /* get bitlen and round up to next multiple of FP_LUT */ bitlen = (unsigned)mp_unsigned_bin_size(modulus) << 3; - x = bitlen % FP_LUT; + x = (int)(bitlen % FP_LUT); if (x) { bitlen += FP_LUT - (unsigned)x; } @@ -14318,6 +14483,12 @@ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, if (mp_unsigned_bin_size(tk) > (int)(KB_SIZE - 2)) { err = BUFFER_E; goto done; } +#ifdef ECC_TIMING_RESISTANT + /* The scalar is written at the length of the modulus. */ + if (mp_unsigned_bin_size(modulus) > (int)(KB_SIZE - 2)) { + err = BUFFER_E; goto done; + } +#endif /* store k */ #ifdef WOLFSSL_SMALL_STACK @@ -14328,6 +14499,21 @@ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, #endif XMEMSET(kb, 0, KB_SIZE); +#ifdef ECC_TIMING_RESISTANT + /* A fixed length so that the scalar's leading zeros are not revealed. */ + y = (unsigned)mp_unsigned_bin_size(modulus); + if ((err = mp_to_unsigned_bin_len_ct(tk, kb, (int)y)) == MP_OKAY) { + #ifdef WOLFSSL_CHECK_MEM_ZERO + wc_MemZero_Add("accel_fp_mul kb", kb, KB_SIZE); + #endif + y -= 1; + for (x = 0; (unsigned)x < y; x++, y--) { + z = kb[x]; kb[x] = kb[y]; kb[y] = (byte)z; + } + z = 0; + err = fp_comb_ct(idx, kb, lut_gap, R, a, modulus, mp, rng); + ForceZero(kb, KB_SIZE); + #else if ((err = mp_to_unsigned_bin(tk, kb)) == MP_OKAY) { #ifdef WOLFSSL_CHECK_MEM_ZERO wc_MemZero_Add("accel_fp_mul kb", kb, KB_SIZE); @@ -14385,6 +14571,7 @@ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, if ((err == MP_OKAY) && (first == 1)) { err = ecc_set_point_infinity(R); } + #endif /* ECC_TIMING_RESISTANT */ } if (err == MP_OKAY) { @@ -14393,7 +14580,7 @@ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, /* map R back from projective space */ if (map) { - err = ecc_map(R, modulus, mp); + err = ecc_map_ex(R, modulus, mp, 1); } else { err = MP_OKAY; } @@ -14420,7 +14607,6 @@ static int accel_fp_mul(int idx, const mp_int* k, ecc_point *R, mp_int* a, return err; } -#endif /* !ECC_TIMING_RESISTANT */ #endif #ifdef ECC_SHAMIR @@ -14544,7 +14730,7 @@ static int accel_fp_mul2add(int idx1, int idx2, /* get bitlen and round up to next multiple of FP_LUT */ bitlen = (unsigned)mp_unsigned_bin_size(modulus) << 3; - x = bitlen % FP_LUT; + x = (int)(bitlen % FP_LUT); if (x) { bitlen += FP_LUT - (unsigned)x; } @@ -14879,20 +15065,6 @@ int wc_ecc_mulmod_ex(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, mp_int* modulus, int map, void* heap) { #if !defined(WOLFSSL_SP_MATH) -#ifdef ECC_TIMING_RESISTANT - if (k == NULL || G == NULL || R == NULL || a == NULL || modulus == NULL) { - return ECC_BAD_ARG_E; - } - - /* k can't have more bits than modulus count plus 1 */ - if (mp_count_bits(k) > mp_count_bits(modulus) + 1) { - return ECC_OUT_OF_RANGE_E; - } - - /* The FP-cache LUT is not constant-time, so secret scalars bypass the - * cache and its lock entirely. */ - return normal_ecc_mulmod(k, G, R, a, modulus, NULL, map, heap); -#else int idx, err = MP_OKAY; mp_digit mp = 0; WC_DECLARE_VAR(mu, mp_int, 1, 0); @@ -14959,7 +15131,8 @@ int wc_ecc_mulmod_ex(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, if (err == MP_OKAY) { /* if it's 2 build the LUT, if it's higher just use the LUT */ - if (idx >= 0 && fp_cache[idx].lru_count >= 2 && !fp_cache[idx].LUT_set) { + if (FP_MULMOD_USES_LUT && idx >= 0 && fp_cache[idx].lru_count >= 2 && + !fp_cache[idx].LUT_set) { /* compute mp */ err = mp_montgomery_setup(modulus, &mp); @@ -14976,13 +15149,13 @@ int wc_ecc_mulmod_ex(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, } if (err == MP_OKAY) { - if (idx >= 0 && fp_cache[idx].LUT_set) { + if (idx >= 0 && fp_cache[idx].LUT_set && FP_K_FITS_COMB(k, modulus)) { if (mpSetup == 0) { /* compute mp */ err = mp_montgomery_setup(modulus, &mp); } if (err == MP_OKAY) - err = accel_fp_mul(idx, k, R, a, modulus, mp, map); + err = accel_fp_mul(idx, k, R, a, modulus, mp, NULL, map); } else { err = normal_ecc_mulmod(k, G, R, a, modulus, NULL, map, heap); } @@ -14998,7 +15171,6 @@ int wc_ecc_mulmod_ex(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, WC_FREE_VAR_EX(mu, NULL, DYNAMIC_TYPE_ECC_BUFFER); return err; -#endif /* ECC_TIMING_RESISTANT */ #else /* WOLFSSL_SP_MATH */ @@ -15057,21 +15229,6 @@ int wc_ecc_mulmod_ex2(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, mp_int* modulus, mp_int* order, WC_RNG* rng, int map, void* heap) { #if !defined(WOLFSSL_SP_MATH) -#ifdef ECC_TIMING_RESISTANT - if (k == NULL || G == NULL || R == NULL || a == NULL || modulus == NULL || - order == NULL) { - return ECC_BAD_ARG_E; - } - - /* k can't have more bits than order */ - if (mp_count_bits(k) > mp_count_bits(order)) { - return ECC_OUT_OF_RANGE_E; - } - - /* The FP-cache LUT is not constant-time, so secret scalars bypass the - * cache and its lock entirely. */ - return normal_ecc_mulmod(k, G, R, a, modulus, rng, map, heap); -#else int idx, err = MP_OKAY; mp_digit mp = 0; WC_DECLARE_VAR(mu, mp_int, 1, 0); @@ -15139,7 +15296,8 @@ int wc_ecc_mulmod_ex2(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, if (err == MP_OKAY) { /* if it's 2 build the LUT, if it's higher just use the LUT */ - if (idx >= 0 && fp_cache[idx].lru_count >= 2 && !fp_cache[idx].LUT_set) { + if (FP_MULMOD_USES_LUT && idx >= 0 && fp_cache[idx].lru_count >= 2 && + !fp_cache[idx].LUT_set) { /* compute mp */ err = mp_montgomery_setup(modulus, &mp); @@ -15156,13 +15314,13 @@ int wc_ecc_mulmod_ex2(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, } if (err == MP_OKAY) { - if (idx >= 0 && fp_cache[idx].LUT_set) { + if (idx >= 0 && fp_cache[idx].LUT_set && FP_K_FITS_COMB(k, modulus)) { if (mpSetup == 0) { /* compute mp */ err = mp_montgomery_setup(modulus, &mp); } if (err == MP_OKAY) - err = accel_fp_mul(idx, k, R, a, modulus, mp, map); + err = accel_fp_mul(idx, k, R, a, modulus, mp, rng, map); } else { err = normal_ecc_mulmod(k, G, R, a, modulus, rng, map, heap); } @@ -15178,7 +15336,6 @@ int wc_ecc_mulmod_ex2(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, WC_FREE_VAR_EX(mu, NULL, DYNAMIC_TYPE_ECC_BUFFER); return err; -#endif /* ECC_TIMING_RESISTANT */ #else /* WOLFSSL_SP_MATH */ @@ -15226,8 +15383,7 @@ int wc_ecc_mulmod_ex2(const mp_int* k, ecc_point *G, ecc_point *R, mp_int* a, #endif /* WOLFSSL_SP_MATH */ } -#if !defined(WOLFSSL_SP_MATH) && \ - (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) +#if !defined(WOLFSSL_SP_MATH) /* helper function for freeing the cache ... must be called with the cache mutex locked */ static void wc_ecc_fp_free_cache(void) @@ -15254,8 +15410,7 @@ static void wc_ecc_fp_free_cache(void) /** Init the Fixed Point cache */ void wc_ecc_fp_init(void) { -#if !defined(WOLFSSL_SP_MATH) && \ - (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) +#ifndef WOLFSSL_SP_MATH #ifndef HAVE_THREAD_LS #ifndef WOLFSSL_MUTEX_INITIALIZER /* Losing the election is fine - the winner finishes the init. */ @@ -15270,8 +15425,7 @@ void wc_ecc_fp_init(void) WOLFSSL_ABI void wc_ecc_fp_free(void) { -#if !defined(WOLFSSL_SP_MATH) && \ - (!defined(ECC_TIMING_RESISTANT) || defined(ECC_SHAMIR)) +#if !defined(WOLFSSL_SP_MATH) #ifndef HAVE_THREAD_LS #ifndef WOLFSSL_MUTEX_INITIALIZER /* extra sanity check if wolfCrypt_Init not called */ From 81f4296411be9d28571c818e5d01a79a2fbedf7e Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 6 Nov 2026 08:15:44 -0800 Subject: [PATCH 10/10] Correct the blinding note on wolfSSL_BN_mod_inverse --- src/ssl_bn.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ssl_bn.c b/src/ssl_bn.c index d9bb9de66f2..d07d0dd0996 100644 --- a/src/ssl_bn.c +++ b/src/ssl_bn.c @@ -2051,7 +2051,8 @@ static int wolfssl_bn_invmod_blind(mp_int* a, const WOLFSSL_BIGNUM* m, * * A new big number is allocated when r is NULL. * - * The operand is always blinded; BN_FLG_CONSTTIME is not needed or checked. + * The operand is blinded when a random blinding factor can be generated, and + * inverted unblinded otherwise. BN_FLG_CONSTTIME is not checked. * * @param [in, out] r Big number to hold result. May be NULL. * @param [in] a Big number to invert.