From 69a66c95120b8479e2f389a75bce7f27ae6366d8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 14:37:12 +0200 Subject: [PATCH 01/20] tests: drive the SP FP-ECC cache mutex guards --- tests/include.am | 1 + tests/unit-mcdc/mcdc_fault_mutex.h | 121 ++++++++++++++++++++ tests/unit-mcdc/test_sp_arm32_whitebox.c | 103 +++++++++++++++++ tests/unit-mcdc/test_sp_arm64_whitebox.c | 103 +++++++++++++++++ tests/unit-mcdc/test_sp_armthumb_whitebox.c | 103 +++++++++++++++++ tests/unit-mcdc/test_sp_c32_whitebox.c | 103 +++++++++++++++++ tests/unit-mcdc/test_sp_c64_whitebox.c | 103 +++++++++++++++++ tests/unit-mcdc/test_sp_x86_64_whitebox.c | 116 +++++++++++++++++++ 8 files changed, 753 insertions(+) create mode 100644 tests/unit-mcdc/mcdc_fault_mutex.h diff --git a/tests/include.am b/tests/include.am index e4fc2ff7b25..a51a1ca3f7e 100644 --- a/tests/include.am +++ b/tests/include.am @@ -121,6 +121,7 @@ DISTCLEANFILES+= tests/.libs/unit.test EXTRA_DIST += \ tests/unit-mcdc/README.md \ tests/unit-mcdc/mcdc_fault_alloc.h \ + tests/unit-mcdc/mcdc_fault_mutex.h \ tests/unit-mcdc/test_aes_whitebox.c \ tests/unit-mcdc/test_asn_cert_whitebox.c \ tests/unit-mcdc/test_asn_certgen_whitebox.c \ diff --git a/tests/unit-mcdc/mcdc_fault_mutex.h b/tests/unit-mcdc/mcdc_fault_mutex.h new file mode 100644 index 00000000000..d65cb72ba1f --- /dev/null +++ b/tests/unit-mcdc/mcdc_fault_mutex.h @@ -0,0 +1,121 @@ +/* mcdc_fault_mutex.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * mcdc_fault_mutex.h -- mutex-fault injector for the per-module MC/DC campaign. + * + * PURPOSE + * ------- + * Every SP backend guards its FP-ECC point cache with + * + * if ((err == MP_OKAY) && (wc_LockMutex(&sp_cache__lock) != 0)) + * err = BAD_MUTEX_E; + * + * A live, correctly initialised mutex always locks, so an ordinary run only + * ever observes (T,F). Both operands therefore stay uncovered. The two missing + * vectors are: + * + * (T,T) the lock is refused -> covers the wc_LockMutex operand + * (F,-) err is already not MP_OKAY -> covers the err operand + * + * In these functions err can only be non-MP_OKAY at that point when the + * one-shot wc_InitMutex above it failed, so both hooks are needed. + * + * USAGE + * ----- + * Two-phase include, in the white-box TU: + * + * #include "mcdc_fault_mutex.h" + * #include + * #define MCDC_FM_IMPL + * #include "mcdc_fault_mutex.h" + * + * The first phase redirects this TU's wc_InitMutex/wc_LockMutex calls; the + * prototypes in wc_port.h expand into the hooks' own declarations. The second + * phase defines the hooks, reaching the real functions through the #undef'd + * names. + * + * Drive with: + * + * mcdc_fm_init_fail = 1; (F,-) + * mcdc_fm_init_fail = 0; mcdc_fm_lock_fail = 1; (T,T) + * mcdc_fm_lock_fail = 0; (T,F) + * + * The init hook must fail on the FIRST call that reaches the cache: the + * initialisation is one-shot behind an atomic, and resets to "uninitialised" + * on failure so a later call retries. + * + * A failed lock leaves err = BAD_MUTEX_E, so the caller skips the block that + * would wc_UnLockMutex() a mutex this hook never locked. + * + * Compiles to inert no-ops where the guarded code does not exist or the mutex + * ops are not plain functions (MCDC_FM_UNAVAILABLE). + */ + +#if defined(SINGLE_THREADED) || defined(HAVE_THREAD_LS) || \ + defined(WC_MUTEX_OPS_INLINE) || defined(HAVE_FIPS) + #define MCDC_FM_UNAVAILABLE +#endif + +#ifndef MCDC_FM_IMPL + +#ifndef MCDC_FAULT_MUTEX_H +#define MCDC_FAULT_MUTEX_H + +static int mcdc_fm_init_fail = 0; +static int mcdc_fm_lock_fail = 0; + +#ifndef MCDC_FM_UNAVAILABLE + #define wc_InitMutex(m) mcdc_fm_init(m) + #define wc_LockMutex(m) mcdc_fm_lock(m) +#endif + +#endif /* MCDC_FAULT_MUTEX_H */ + +#else /* MCDC_FM_IMPL */ + +#ifndef MCDC_FM_UNAVAILABLE + +#undef wc_InitMutex +#undef wc_LockMutex + +extern int wc_InitMutex(wolfSSL_Mutex* m); +extern int wc_LockMutex(wolfSSL_Mutex* m); + +int mcdc_fm_init(wolfSSL_Mutex* m) +{ + if (mcdc_fm_init_fail) { + return BAD_MUTEX_E; + } + return wc_InitMutex(m); +} + +int mcdc_fm_lock(wolfSSL_Mutex* m) +{ + if (mcdc_fm_lock_fail) { + return BAD_MUTEX_E; + } + return wc_LockMutex(m); +} + +#endif /* !MCDC_FM_UNAVAILABLE */ + +#endif /* MCDC_FM_IMPL */ diff --git a/tests/unit-mcdc/test_sp_arm32_whitebox.c b/tests/unit-mcdc/test_sp_arm32_whitebox.c index e5fb0c6f3f0..f68efa88631 100644 --- a/tests/unit-mcdc/test_sp_arm32_whitebox.c +++ b/tests/unit-mcdc/test_sp_arm32_whitebox.c @@ -104,7 +104,20 @@ * s == 0, both cryptographically negligible (1/order probability). */ +/* The FP-ECC cache lock is statically initialised on pthreads, which compiles + * out the lazy-init block above the guard and leaves its `err == MP_OKAY` + * operand structurally true. WOLFSSL_TEST_NO_MUTEX_INITIALIZER is wolfSSL's + * own knob for that; setting it here compiles the lazy path into this TU so + * both operands of the guard are reachable in this one binary, which is what + * MC/DC-per-binary requires. */ +#define WOLFSSL_TEST_NO_MUTEX_INITIALIZER + +#include "mcdc_fault_mutex.h" + #include +#define MCDC_FM_IMPL +#include "mcdc_fault_mutex.h" + #include #include @@ -1662,6 +1675,95 @@ static void wb_run_dh_gaps(void) } #endif /* WOLFSSL_HAVE_SP_DH && !NO_DH */ +/* FP-ECC cache guard, once per curve: + * + * if ((err == MP_OKAY) && (wc_LockMutex(&sp_cache__lock) != 0)) + * + * Three vectors: init refused (err operand false), lock refused (T,T), and the + * ordinary success path (T,F). The mutex init is one-shot per curve, so the + * init-failure vector has to be the first call that reaches the cache -- this + * runs before anything else in main(). */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + defined(FP_ECC) && !defined(MCDC_FM_UNAVAILABLE) +static void wb_run_cache_mutex(void) +{ + ecc_point* gm = NULL; + ecc_point* rOut = NULL; + mp_int k; + int vec; + + if (mp_init(&k) != MP_OKAY) { + WB_NOTE("mp_init failed (cache_mutex)"); + wb_fail = 1; + return; + } + gm = wc_ecc_new_point(); + rOut = wc_ecc_new_point(); + if (gm == NULL || rOut == NULL) { + WB_NOTE("wc_ecc_new_point failed (cache_mutex)"); + wb_fail = 1; + } + else if (mp_set(&k, 3) != MP_OKAY) { + WB_NOTE("mp_set failed (cache_mutex)"); + wb_fail = 1; + } + else { + for (vec = 0; vec < 3; vec++) { + int curveIdx; + const ecc_set_type* dp; + + mcdc_fm_init_fail = (vec == 0); + mcdc_fm_lock_fail = (vec == 1); + +#ifndef WOLFSSL_SP_NO_256 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP256R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_256(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_384 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP384R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_384(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_521 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP521R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_521(&k, gm, rOut, 1, NULL); + } +#endif + (void)curveIdx; + (void)dp; + } + mcdc_fm_init_fail = 0; + mcdc_fm_lock_fail = 0; + } + + if (gm != NULL) { + wc_ecc_del_point(gm); + } + if (rOut != NULL) { + wc_ecc_del_point(rOut); + } + mp_free(&k); +} +#else +static void wb_run_cache_mutex(void) +{ + WB_NOTE("FP_ECC cache mutex path not compiled; skipped"); +} +#endif + #endif /* WOLFSSL_HAVE_SP_ECC || WOLFSSL_HAVE_SP_RSA || WOLFSSL_HAVE_SP_DH */ int main(void) @@ -1672,6 +1774,7 @@ int main(void) "dispatch)\n"); #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) + wb_run_cache_mutex(); wb_run_ecc(); wb_run_rsa(); wb_run_dh(); diff --git a/tests/unit-mcdc/test_sp_arm64_whitebox.c b/tests/unit-mcdc/test_sp_arm64_whitebox.c index 785f99ac69e..e7e96700ea7 100644 --- a/tests/unit-mcdc/test_sp_arm64_whitebox.c +++ b/tests/unit-mcdc/test_sp_arm64_whitebox.c @@ -106,7 +106,20 @@ * land on a vanishingly small set of values -- cryptographically negligible. */ +/* The FP-ECC cache lock is statically initialised on pthreads, which compiles + * out the lazy-init block above the guard and leaves its `err == MP_OKAY` + * operand structurally true. WOLFSSL_TEST_NO_MUTEX_INITIALIZER is wolfSSL's + * own knob for that; setting it here compiles the lazy path into this TU so + * both operands of the guard are reachable in this one binary, which is what + * MC/DC-per-binary requires. */ +#define WOLFSSL_TEST_NO_MUTEX_INITIALIZER + +#include "mcdc_fault_mutex.h" + #include +#define MCDC_FM_IMPL +#include "mcdc_fault_mutex.h" + #include #include @@ -1242,6 +1255,95 @@ static void wb_run_rsa_dh_bounds(void) } #endif /* (WOLFSSL_HAVE_SP_RSA && !NO_RSA) || (WOLFSSL_HAVE_SP_DH && !NO_DH) */ +/* FP-ECC cache guard, once per curve: + * + * if ((err == MP_OKAY) && (wc_LockMutex(&sp_cache__lock) != 0)) + * + * Three vectors: init refused (err operand false), lock refused (T,T), and the + * ordinary success path (T,F). The mutex init is one-shot per curve, so the + * init-failure vector has to be the first call that reaches the cache -- this + * runs before anything else in main(). */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + defined(FP_ECC) && !defined(MCDC_FM_UNAVAILABLE) +static void wb_run_cache_mutex(void) +{ + ecc_point* gm = NULL; + ecc_point* rOut = NULL; + mp_int k; + int vec; + + if (mp_init(&k) != MP_OKAY) { + WB_NOTE("mp_init failed (cache_mutex)"); + wb_fail = 1; + return; + } + gm = wc_ecc_new_point(); + rOut = wc_ecc_new_point(); + if (gm == NULL || rOut == NULL) { + WB_NOTE("wc_ecc_new_point failed (cache_mutex)"); + wb_fail = 1; + } + else if (mp_set(&k, 3) != MP_OKAY) { + WB_NOTE("mp_set failed (cache_mutex)"); + wb_fail = 1; + } + else { + for (vec = 0; vec < 3; vec++) { + int curveIdx; + const ecc_set_type* dp; + + mcdc_fm_init_fail = (vec == 0); + mcdc_fm_lock_fail = (vec == 1); + +#ifndef WOLFSSL_SP_NO_256 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP256R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_256(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_384 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP384R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_384(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_521 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP521R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_521(&k, gm, rOut, 1, NULL); + } +#endif + (void)curveIdx; + (void)dp; + } + mcdc_fm_init_fail = 0; + mcdc_fm_lock_fail = 0; + } + + if (gm != NULL) { + wc_ecc_del_point(gm); + } + if (rOut != NULL) { + wc_ecc_del_point(rOut); + } + mp_free(&k); +} +#else +static void wb_run_cache_mutex(void) +{ + WB_NOTE("FP_ECC cache mutex path not compiled; skipped"); +} +#endif + #endif /* WOLFSSL_HAVE_SP_ECC || WOLFSSL_HAVE_SP_RSA || WOLFSSL_HAVE_SP_DH */ int main(void) @@ -1250,6 +1352,7 @@ int main(void) printf("sp_arm64.c white-box supplement\n"); #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) + wb_run_cache_mutex(); wb_run_ecc(); wb_run_rsa(); wb_run_dh(); diff --git a/tests/unit-mcdc/test_sp_armthumb_whitebox.c b/tests/unit-mcdc/test_sp_armthumb_whitebox.c index ab35060235d..77737d4ec97 100644 --- a/tests/unit-mcdc/test_sp_armthumb_whitebox.c +++ b/tests/unit-mcdc/test_sp_armthumb_whitebox.c @@ -91,7 +91,20 @@ * point respectively. */ +/* The FP-ECC cache lock is statically initialised on pthreads, which compiles + * out the lazy-init block above the guard and leaves its `err == MP_OKAY` + * operand structurally true. WOLFSSL_TEST_NO_MUTEX_INITIALIZER is wolfSSL's + * own knob for that; setting it here compiles the lazy path into this TU so + * both operands of the guard are reachable in this one binary, which is what + * MC/DC-per-binary requires. */ +#define WOLFSSL_TEST_NO_MUTEX_INITIALIZER + +#include "mcdc_fault_mutex.h" + #include +#define MCDC_FM_IMPL +#include "mcdc_fault_mutex.h" + #include #include @@ -1466,6 +1479,95 @@ static void wb_run_gap_521(void) } #endif /* WOLFSSL_SP_521 */ +/* FP-ECC cache guard, once per curve: + * + * if ((err == MP_OKAY) && (wc_LockMutex(&sp_cache__lock) != 0)) + * + * Three vectors: init refused (err operand false), lock refused (T,T), and the + * ordinary success path (T,F). The mutex init is one-shot per curve, so the + * init-failure vector has to be the first call that reaches the cache -- this + * runs before anything else in main(). */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + defined(FP_ECC) && !defined(MCDC_FM_UNAVAILABLE) +static void wb_run_cache_mutex(void) +{ + ecc_point* gm = NULL; + ecc_point* rOut = NULL; + mp_int k; + int vec; + + if (mp_init(&k) != MP_OKAY) { + WB_NOTE("mp_init failed (cache_mutex)"); + wb_fail = 1; + return; + } + gm = wc_ecc_new_point(); + rOut = wc_ecc_new_point(); + if (gm == NULL || rOut == NULL) { + WB_NOTE("wc_ecc_new_point failed (cache_mutex)"); + wb_fail = 1; + } + else if (mp_set(&k, 3) != MP_OKAY) { + WB_NOTE("mp_set failed (cache_mutex)"); + wb_fail = 1; + } + else { + for (vec = 0; vec < 3; vec++) { + int curveIdx; + const ecc_set_type* dp; + + mcdc_fm_init_fail = (vec == 0); + mcdc_fm_lock_fail = (vec == 1); + +#ifndef WOLFSSL_SP_NO_256 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP256R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_256(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_384 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP384R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_384(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_521 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP521R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_521(&k, gm, rOut, 1, NULL); + } +#endif + (void)curveIdx; + (void)dp; + } + mcdc_fm_init_fail = 0; + mcdc_fm_lock_fail = 0; + } + + if (gm != NULL) { + wc_ecc_del_point(gm); + } + if (rOut != NULL) { + wc_ecc_del_point(rOut); + } + mp_free(&k); +} +#else +static void wb_run_cache_mutex(void) +{ + WB_NOTE("FP_ECC cache mutex path not compiled; skipped"); +} +#endif + #endif /* WOLFSSL_HAVE_SP_ECC || WOLFSSL_HAVE_SP_RSA || WOLFSSL_HAVE_SP_DH */ int main(void) @@ -1476,6 +1578,7 @@ int main(void) "no cpuid dispatch)\n"); #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) + wb_run_cache_mutex(); wb_run_ecc(); wb_run_rsa(); wb_run_dh(); diff --git a/tests/unit-mcdc/test_sp_c32_whitebox.c b/tests/unit-mcdc/test_sp_c32_whitebox.c index 907f361a9f2..9fd818e48f6 100644 --- a/tests/unit-mcdc/test_sp_c32_whitebox.c +++ b/tests/unit-mcdc/test_sp_c32_whitebox.c @@ -98,8 +98,21 @@ * (1/order probability) with a real RNG and a real private key. */ +/* The FP-ECC cache lock is statically initialised on pthreads, which compiles + * out the lazy-init block above the guard and leaves its `err == MP_OKAY` + * operand structurally true. WOLFSSL_TEST_NO_MUTEX_INITIALIZER is wolfSSL's + * own knob for that; setting it here compiles the lazy path into this TU so + * both operands of the guard are reachable in this one binary, which is what + * MC/DC-per-binary requires. */ +#define WOLFSSL_TEST_NO_MUTEX_INITIALIZER + +#include "mcdc_fault_mutex.h" + #include +#define MCDC_FM_IMPL +#include "mcdc_fault_mutex.h" + #include #include #include @@ -1094,6 +1107,95 @@ static void wb_run_gap_521(void) } #endif /* WOLFSSL_SP_521 */ +/* FP-ECC cache guard, once per curve: + * + * if ((err == MP_OKAY) && (wc_LockMutex(&sp_cache__lock) != 0)) + * + * Three vectors: init refused (err operand false), lock refused (T,T), and the + * ordinary success path (T,F). The mutex init is one-shot per curve, so the + * init-failure vector has to be the first call that reaches the cache -- this + * runs before anything else in main(). */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + defined(FP_ECC) && !defined(MCDC_FM_UNAVAILABLE) +static void wb_run_cache_mutex(void) +{ + ecc_point* gm = NULL; + ecc_point* rOut = NULL; + mp_int k; + int vec; + + if (mp_init(&k) != MP_OKAY) { + WB_NOTE("mp_init failed (cache_mutex)"); + wb_fail = 1; + return; + } + gm = wc_ecc_new_point(); + rOut = wc_ecc_new_point(); + if (gm == NULL || rOut == NULL) { + WB_NOTE("wc_ecc_new_point failed (cache_mutex)"); + wb_fail = 1; + } + else if (mp_set(&k, 3) != MP_OKAY) { + WB_NOTE("mp_set failed (cache_mutex)"); + wb_fail = 1; + } + else { + for (vec = 0; vec < 3; vec++) { + int curveIdx; + const ecc_set_type* dp; + + mcdc_fm_init_fail = (vec == 0); + mcdc_fm_lock_fail = (vec == 1); + +#ifndef WOLFSSL_SP_NO_256 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP256R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_256(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_384 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP384R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_384(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_521 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP521R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_521(&k, gm, rOut, 1, NULL); + } +#endif + (void)curveIdx; + (void)dp; + } + mcdc_fm_init_fail = 0; + mcdc_fm_lock_fail = 0; + } + + if (gm != NULL) { + wc_ecc_del_point(gm); + } + if (rOut != NULL) { + wc_ecc_del_point(rOut); + } + mp_free(&k); +} +#else +static void wb_run_cache_mutex(void) +{ + WB_NOTE("FP_ECC cache mutex path not compiled; skipped"); +} +#endif + #endif /* WOLFSSL_HAVE_SP_ECC || WOLFSSL_HAVE_SP_RSA || WOLFSSL_HAVE_SP_DH */ int main(void) @@ -1102,6 +1204,7 @@ int main(void) "dispatch)\n"); #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) + wb_run_cache_mutex(); wb_run_ecc(); wb_run_rsa(); wb_run_dh(); diff --git a/tests/unit-mcdc/test_sp_c64_whitebox.c b/tests/unit-mcdc/test_sp_c64_whitebox.c index 9bd0cc8e911..37d2eb294ad 100644 --- a/tests/unit-mcdc/test_sp_c64_whitebox.c +++ b/tests/unit-mcdc/test_sp_c64_whitebox.c @@ -108,7 +108,20 @@ * negligible, not something this supplement forces. */ +/* The FP-ECC cache lock is statically initialised on pthreads, which compiles + * out the lazy-init block above the guard and leaves its `err == MP_OKAY` + * operand structurally true. WOLFSSL_TEST_NO_MUTEX_INITIALIZER is wolfSSL's + * own knob for that; setting it here compiles the lazy path into this TU so + * both operands of the guard are reachable in this one binary, which is what + * MC/DC-per-binary requires. */ +#define WOLFSSL_TEST_NO_MUTEX_INITIALIZER + +#include "mcdc_fault_mutex.h" + #include +#define MCDC_FM_IMPL +#include "mcdc_fault_mutex.h" + #include #include @@ -767,6 +780,95 @@ static void wb_run_dh(void) } #endif /* WOLFSSL_HAVE_SP_DH && !NO_DH */ +/* FP-ECC cache guard, once per curve: + * + * if ((err == MP_OKAY) && (wc_LockMutex(&sp_cache__lock) != 0)) + * + * Three vectors: init refused (err operand false), lock refused (T,T), and the + * ordinary success path (T,F). The mutex init is one-shot per curve, so the + * init-failure vector has to be the first call that reaches the cache -- this + * runs before anything else in main(). */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + defined(FP_ECC) && !defined(MCDC_FM_UNAVAILABLE) +static void wb_run_cache_mutex(void) +{ + ecc_point* gm = NULL; + ecc_point* rOut = NULL; + mp_int k; + int vec; + + if (mp_init(&k) != MP_OKAY) { + WB_NOTE("mp_init failed (cache_mutex)"); + wb_fail = 1; + return; + } + gm = wc_ecc_new_point(); + rOut = wc_ecc_new_point(); + if (gm == NULL || rOut == NULL) { + WB_NOTE("wc_ecc_new_point failed (cache_mutex)"); + wb_fail = 1; + } + else if (mp_set(&k, 3) != MP_OKAY) { + WB_NOTE("mp_set failed (cache_mutex)"); + wb_fail = 1; + } + else { + for (vec = 0; vec < 3; vec++) { + int curveIdx; + const ecc_set_type* dp; + + mcdc_fm_init_fail = (vec == 0); + mcdc_fm_lock_fail = (vec == 1); + +#ifndef WOLFSSL_SP_NO_256 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP256R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_256(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_384 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP384R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_384(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_521 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP521R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_521(&k, gm, rOut, 1, NULL); + } +#endif + (void)curveIdx; + (void)dp; + } + mcdc_fm_init_fail = 0; + mcdc_fm_lock_fail = 0; + } + + if (gm != NULL) { + wc_ecc_del_point(gm); + } + if (rOut != NULL) { + wc_ecc_del_point(rOut); + } + mp_free(&k); +} +#else +static void wb_run_cache_mutex(void) +{ + WB_NOTE("FP_ECC cache mutex path not compiled; skipped"); +} +#endif + #endif /* WOLFSSL_HAVE_SP_ECC || WOLFSSL_HAVE_SP_RSA || WOLFSSL_HAVE_SP_DH */ int main(void) @@ -774,6 +876,7 @@ int main(void) printf("sp_c64.c white-box supplement\n"); #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) + wb_run_cache_mutex(); wb_run_ecc(); wb_run_rsa(); wb_run_dh(); diff --git a/tests/unit-mcdc/test_sp_x86_64_whitebox.c b/tests/unit-mcdc/test_sp_x86_64_whitebox.c index 994f5be6215..4575736ed87 100644 --- a/tests/unit-mcdc/test_sp_x86_64_whitebox.c +++ b/tests/unit-mcdc/test_sp_x86_64_whitebox.c @@ -168,7 +168,20 @@ static int wb_intr_ret = 0; #define WC_CHECK_FOR_INTR_SIGNALS() (wb_intr_ret) +/* The FP-ECC cache lock is statically initialised on pthreads, which compiles + * out the lazy-init block above the guard and leaves its `err == MP_OKAY` + * operand structurally true. WOLFSSL_TEST_NO_MUTEX_INITIALIZER is wolfSSL's + * own knob for that; setting it here compiles the lazy path into this TU so + * both operands of the guard are reachable in this one binary, which is what + * MC/DC-per-binary requires. */ +#define WOLFSSL_TEST_NO_MUTEX_INITIALIZER + +#include "mcdc_fault_mutex.h" + #include +#define MCDC_FM_IMPL +#include "mcdc_fault_mutex.h" + #include #include @@ -1763,6 +1776,108 @@ static void wb_run_crafted(void) } #endif /* WOLFSSL_HAVE_SP_ECC && HAVE_ECC */ +/* FP-ECC cache guard, once per curve: + * + * if ((err == MP_OKAY) && (wc_LockMutex(&sp_cache__lock) != 0)) + * + * Three vectors: init refused (err operand false), lock refused (T,T), and the + * ordinary success path (T,F). The mutex init is one-shot per curve, so the + * init-failure vector has to be the first call that reaches the cache -- this + * runs before anything else in main(). */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + defined(FP_ECC) && !defined(MCDC_FM_UNAVAILABLE) +static void wb_run_cache_mutex(void) +{ + ecc_point* gm = NULL; + ecc_point* rOut = NULL; + mp_int k; + int vec; + + if (mp_init(&k) != MP_OKAY) { + WB_NOTE("mp_init failed (cache_mutex)"); + wb_fail = 1; + return; + } + gm = wc_ecc_new_point(); + rOut = wc_ecc_new_point(); + if (gm == NULL || rOut == NULL) { + WB_NOTE("wc_ecc_new_point failed (cache_mutex)"); + wb_fail = 1; + } + else if (mp_set(&k, 3) != MP_OKAY) { + WB_NOTE("mp_set failed (cache_mutex)"); + wb_fail = 1; + } + else { + cpuid_flags_t real = cpuid_get_flags(); + int mask; + + for (vec = 0; vec < 3; vec++) { + /* Each curve carries the guard twice, once in sp__ecc_mulmod_ + * and once in its _avx2_ sibling, behind one shared init atomic. + * Sweeping the mask inside the vector loop reaches both copies while + * the atomic is still in the state that vector needs -- the other + * order would leave the second copy's init already done. */ + for (mask = 0; mask < 2; mask++) { + int curveIdx; + const ecc_set_type* dp; + + cpuid_select_flags((mask == 0) ? + (real & ~(cpuid_flags_t)CPUID_AVX2) : real); + mcdc_fm_init_fail = (vec == 0); + mcdc_fm_lock_fail = (vec == 1); + +#ifndef WOLFSSL_SP_NO_256 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP256R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_256(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_384 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP384R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_384(&k, gm, rOut, 1, NULL); + } +#endif +#ifdef WOLFSSL_SP_521 + curveIdx = wc_ecc_get_curve_idx(ECC_SECP521R1); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + if (dp != NULL && mp_read_radix(gm->x, dp->Gx, 16) == MP_OKAY && + mp_read_radix(gm->y, dp->Gy, 16) == MP_OKAY && + mp_set(gm->z, 1) == MP_OKAY) { + (void)sp_ecc_mulmod_521(&k, gm, rOut, 1, NULL); + } +#endif + (void)curveIdx; + (void)dp; + } + } + mcdc_fm_init_fail = 0; + mcdc_fm_lock_fail = 0; + cpuid_select_flags(real); + } + + if (gm != NULL) { + wc_ecc_del_point(gm); + } + if (rOut != NULL) { + wc_ecc_del_point(rOut); + } + mp_free(&k); +} +#else +static void wb_run_cache_mutex(void) +{ + WB_NOTE("FP_ECC cache mutex path not compiled; skipped"); +} +#endif + #endif /* WOLFSSL_HAVE_SP_ECC || WOLFSSL_HAVE_SP_RSA || WOLFSSL_HAVE_SP_DH */ int main(void) @@ -1804,6 +1919,7 @@ int main(void) * mulmod_add/check_key/is_point inputs) is likewise fast and runs * under every mask. */ cpuid_select_flags(real); + wb_run_cache_mutex(); wb_run_ecc(); wb_run_rsa_keygen(); wb_run_rsa_signverify(); From 44895a310b076939f8869843999eafeb8b9f33d4 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 14:37:12 +0200 Subject: [PATCH 02/20] tests: cover the new puf argument guards --- tests/unit-mcdc/test_puf_whitebox.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit-mcdc/test_puf_whitebox.c b/tests/unit-mcdc/test_puf_whitebox.c index 881f486a79f..6435675fa1d 100644 --- a/tests/unit-mcdc/test_puf_whitebox.c +++ b/tests/unit-mcdc/test_puf_whitebox.c @@ -89,6 +89,25 @@ static void puf_whitebox_drive(void) wb_calls += (wc_PufSetTestData(NULL, wb_buf, (word32)sizeof(wb_buf)) != 0); wb_calls += (wc_PufSetTestData(&wb_ctx, NULL, (word32)sizeof(wb_buf)) != 0); + /* wc_PufGetParams: the all-NULL rejection is a five-operand chain, so each + * operand needs the vector where it alone is non-NULL. */ + { + int v; + + wb_calls += (wc_PufGetParams(NULL, NULL, NULL, NULL, NULL) != 0); + wb_calls += (wc_PufGetParams(&v, NULL, NULL, NULL, NULL) == 0); + wb_calls += (wc_PufGetParams(NULL, &v, NULL, NULL, NULL) == 0); + wb_calls += (wc_PufGetParams(NULL, NULL, &v, NULL, NULL) == 0); + wb_calls += (wc_PufGetParams(NULL, NULL, NULL, &v, NULL) == 0); + wb_calls += (wc_PufGetParams(NULL, NULL, NULL, NULL, &v) == 0); + } + + /* wc_PufGetHelperData: ctx == NULL || helper == NULL */ + wb_calls += (wc_PufGetHelperData(NULL, wb_buf, + (word32)sizeof(wb_buf)) != 0); + wb_calls += (wc_PufGetHelperData(&wb_ctx, NULL, + (word32)sizeof(wb_buf)) != 0); + (void)wc_PufZeroize(&wb_ctx); } From 124741531267eaa92523e22166109c5ca3800d38 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 14:49:00 +0200 Subject: [PATCH 03/20] tests: drive the ecc argument-guard operands --- tests/unit-mcdc/mcdc_fault_mutex.h | 8 + tests/unit-mcdc/test_ecc_whitebox.c | 239 ++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) diff --git a/tests/unit-mcdc/mcdc_fault_mutex.h b/tests/unit-mcdc/mcdc_fault_mutex.h index d65cb72ba1f..54fce2e9105 100644 --- a/tests/unit-mcdc/mcdc_fault_mutex.h +++ b/tests/unit-mcdc/mcdc_fault_mutex.h @@ -82,6 +82,10 @@ static int mcdc_fm_init_fail = 0; static int mcdc_fm_lock_fail = 0; +/* One-shot: fail the next lock only, then disarm. For guards whose first + * operand is an error a *different*, earlier lock produced -- arming the whole + * process would short-circuit the very decision under test. */ +static int mcdc_fm_lock_once = 0; #ifndef MCDC_FM_UNAVAILABLE #define wc_InitMutex(m) mcdc_fm_init(m) @@ -110,6 +114,10 @@ int mcdc_fm_init(wolfSSL_Mutex* m) int mcdc_fm_lock(wolfSSL_Mutex* m) { + if (mcdc_fm_lock_once) { + mcdc_fm_lock_once = 0; + return BAD_MUTEX_E; + } if (mcdc_fm_lock_fail) { return BAD_MUTEX_E; } diff --git a/tests/unit-mcdc/test_ecc_whitebox.c b/tests/unit-mcdc/test_ecc_whitebox.c index 86be41390df..e2a4bf90fc5 100644 --- a/tests/unit-mcdc/test_ecc_whitebox.c +++ b/tests/unit-mcdc/test_ecc_whitebox.c @@ -1558,8 +1558,246 @@ static void wb_make_pub_privatekey_only(void) #endif /* HAVE_ECC && !WOLF_CRYPTO_CB_ONLY_ECC */ + +/* Argument-guard vectors that ordinary use never produces. Each guard needs + * BOTH halves inside THIS binary: llvm-cov derives MC/DC per binary, so a + * rejection vector on its own proves nothing without the accepting vector of + * the same decision to pair it against. */ +static void wb_arg_guards(void) +{ + WC_RNG rng; + ecc_key key; + ecc_point* pt = NULL; + mp_int m1; + word32 sz = 0; + byte buf[256]; + word32 bufLen = (word32)sizeof(buf); + int haveKey = 0; + int haveM1 = 0; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&m1, 0, sizeof(m1)); + XMEMSET(buf, 0, sizeof(buf)); + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; wb_arg_guards skipped"); + wb_fail = 1; + return; + } + if (wc_ecc_init(&key) == 0) { + haveKey = (wc_ecc_make_key_ex(&rng, 0, &key, ECC_SECP256R1) == 0); + } + if (!haveKey) { + WB_NOTE("key setup failed; wb_arg_guards skipped"); + wb_fail = 1; + wc_ecc_free(&key); + wc_FreeRng(&rng); + return; + } + pt = wc_ecc_new_point(); + haveM1 = (mp_init(&m1) == MP_OKAY); + + /* The import guards run against a scratch key: their accepting vectors + * overwrite whatever they are handed, and the operations further down need + * `key` to still hold a valid point. */ + { + ecc_key imp; + word32 xLen = (word32)sizeof(buf); + int haveImp = (wc_ecc_init(&imp) == 0); + + /* key == NULL || qx == NULL || qy == NULL */ + (void)_ecc_import_raw_private(NULL, "1", "1", "1", ECC_SECP256R1, + WC_TYPE_HEX_STR); + if (haveImp) { + (void)_ecc_import_raw_private(&imp, NULL, "1", "1", ECC_SECP256R1, + WC_TYPE_HEX_STR); + (void)_ecc_import_raw_private(&imp, "1", NULL, "1", ECC_SECP256R1, + WC_TYPE_HEX_STR); + (void)_ecc_import_raw_private(&imp, "1", "1", "1", ECC_SECP256R1, + WC_TYPE_HEX_STR); + } + + /* in == NULL || key == NULL */ + (void)_ecc_import_x963_ex2(NULL, 1, &imp, ECC_SECP256R1, 0); + (void)_ecc_import_x963_ex2(buf, 1, NULL, ECC_SECP256R1, 0); + if (haveImp && wc_ecc_export_x963(&key, buf, &xLen) == 0) { + wc_ecc_free(&imp); + if (wc_ecc_init(&imp) == 0) { + (void)_ecc_import_x963_ex2(buf, xLen, &imp, ECC_SECP256R1, 0); + } + } + if (haveImp) { + wc_ecc_free(&imp); + } + } + + /* ecc_public_key_size: key == NULL || key->dp == NULL */ + { + const ecc_set_type* savedDp = key.dp; + + (void)ecc_public_key_size(NULL, &sz); + key.dp = NULL; + (void)ecc_public_key_size(&key, &sz); + key.dp = savedDp; + (void)ecc_public_key_size(&key, &sz); + } + + /* The accepting vectors below need the real curve constants: a zero + * modulus/order would make wc_ecc_gen_deterministic_k's RFC 6979 retry + * loop spin, and the campaign kills the variant on TEST_TIMEOUT. */ + { + mp_int prime; + mp_int order; + mp_int af; + int haveCurve = 0; + + XMEMSET(&prime, 0, sizeof(prime)); + XMEMSET(&order, 0, sizeof(order)); + XMEMSET(&af, 0, sizeof(af)); + + if (key.dp != NULL && mp_init(&prime) == MP_OKAY) { + if (mp_init(&order) == MP_OKAY) { + if (mp_init(&af) == MP_OKAY) { + haveCurve = + (mp_read_radix(&prime, key.dp->prime, 16) == MP_OKAY) && + (mp_read_radix(&order, key.dp->order, 16) == MP_OKAY) && + (mp_read_radix(&af, key.dp->Af, 16) == MP_OKAY); + } + } + } + + /* hash == NULL || k == NULL || order == NULL */ + if (haveM1 && haveCurve) { + (void)wc_ecc_gen_deterministic_k(NULL, 32, WC_HASH_TYPE_SHA256, + key.k, &m1, &order, key.heap); + (void)wc_ecc_gen_deterministic_k(buf, 32, WC_HASH_TYPE_SHA256, + key.k, NULL, &order, key.heap); + (void)wc_ecc_gen_deterministic_k(buf, 32, WC_HASH_TYPE_SHA256, + key.k, &m1, NULL, key.heap); + (void)wc_ecc_gen_deterministic_k(buf, 32, WC_HASH_TYPE_SHA256, + key.k, &m1, &order, key.heap); + } + + /* wc_ecc_mulmod_ex / _ex2 NULL chains, then the accepting vector. */ + if (pt != NULL && haveCurve && + wc_ecc_copy_point(&key.pubkey, pt) == MP_OKAY) { + ecc_point* out = wc_ecc_new_point(); + +#ifdef WOLFSSL_SP_MATH + /* Only the SP build rejects these: the generic implementation has + * no such guard and dereferences both operands. */ + (void)wc_ecc_mulmod_ex(key.k, pt, pt, NULL, &prime, 1, key.heap); + (void)wc_ecc_mulmod_ex2(key.k, pt, pt, NULL, &prime, &order, &rng, + 1, key.heap); + (void)wc_ecc_mulmod_ex2(key.k, pt, pt, &af, &prime, NULL, &rng, 1, + key.heap); +#endif + if (out != NULL) { + (void)wc_ecc_mulmod_ex(key.k, pt, out, &af, &prime, 1, + key.heap); + (void)wc_ecc_mulmod_ex2(key.k, pt, out, &af, &prime, &order, + &rng, 1, key.heap); + wc_ecc_del_point(out); + } + + /* point == NULL || out == NULL || outLen == NULL */ + sz = (word32)sizeof(buf); + (void)wc_ecc_export_point_der_compressed(key.idx, pt, buf, NULL); + (void)wc_ecc_export_point_der_compressed(key.idx, pt, buf, &sz); + } + + mp_free(&af); + mp_free(&order); + mp_free(&prime); + } + +#ifdef WC_ECC_NONBLOCK + /* wc_ecc_set_nonblock: key->nb_ctx != NULL && key->nb_ctx != ctx */ + { + ecc_nb_ctx_t nb; + + XMEMSET(&nb, 0, sizeof(nb)); + (void)wc_ecc_set_nonblock(&key, &nb); + (void)wc_ecc_set_nonblock(&key, &nb); + (void)wc_ecc_set_nonblock(&key, NULL); + } +#endif + + /* The is_valid_idx/dp family: an invalid idx fires the first operand, a + * valid idx with dp cleared fires the second, an untouched key neither. */ + { + const ecc_set_type* savedDp = key.dp; + int savedIdx = key.idx; + mp_int r; + mp_int s; + int stat = 0; + + key.idx = ECC_CUSTOM_IDX - 1; + bufLen = (word32)sizeof(buf); + (void)wc_ecc_shared_secret(&key, &key, buf, &bufLen); + (void)wc_ecc_shared_secret_ex(&key, &key.pubkey, buf, &bufLen); + key.idx = savedIdx; + + key.dp = NULL; + bufLen = (word32)sizeof(buf); + (void)wc_ecc_shared_secret(&key, &key, buf, &bufLen); + (void)wc_ecc_shared_secret_ex(&key, &key.pubkey, buf, &bufLen); + key.dp = savedDp; + + PRIVATE_KEY_UNLOCK(); + bufLen = (word32)sizeof(buf); + (void)wc_ecc_shared_secret(&key, &key, buf, &bufLen); + bufLen = (word32)sizeof(buf); + (void)wc_ecc_shared_secret_ex(&key, &key.pubkey, buf, &bufLen); + PRIVATE_KEY_LOCK(); + + if (mp_init(&r) == MP_OKAY) { + if (mp_init(&s) == MP_OKAY) { + key.idx = ECC_CUSTOM_IDX - 1; + (void)wc_ecc_sign_hash_ex(buf, 32, &rng, &key, &r, &s); + (void)wc_ecc_verify_hash_ex(&r, &s, buf, 32, &stat, &key); + key.idx = savedIdx; + + key.dp = NULL; + (void)wc_ecc_sign_hash_ex(buf, 32, &rng, &key, &r, &s); + (void)wc_ecc_verify_hash_ex(&r, &s, buf, 32, &stat, &key); + key.dp = savedDp; + + if (wc_ecc_sign_hash_ex(buf, 32, &rng, &key, &r, &s) == 0) { + (void)wc_ecc_verify_hash_ex(&r, &s, buf, 32, &stat, &key); + } + mp_free(&s); + } + mp_free(&r); + } + } + + if (pt != NULL) { + wc_ecc_del_point(pt); + } + if (haveM1) { + mp_free(&m1); + } + wc_ecc_free(&key); + wc_FreeRng(&rng); + + /* wc_ecc_free: key->deallocSet && key->dp != NULL */ + { + ecc_key k2; + + if (wc_ecc_init(&k2) == 0) { + k2.dp = NULL; + (void)wc_ecc_free(&k2); + } + } +} + int main(void) { + /* Unbuffered: on a timeout or a fault the process is killed and anything + * still buffered is lost, which reads as an empty log. */ + setvbuf(stdout, NULL, _IONBF, 0); + printf("ecc.c white-box MC/DC supplement\n"); #if !defined(HAVE_ECC) || defined(WOLF_CRYPTO_CB_ONLY_ECC) printf(" HAVE_ECC off (or crypto-cb-only build); nothing to exercise\n"); @@ -1583,6 +1821,7 @@ int main(void) wb_export_x963_internal(); wb_idx_dp_guard_export_paths(); wb_make_pub_privatekey_only(); + wb_arg_guards(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures are surfaced as skips, not test failures: the campaign * treats a nonzero exit as a failed variant and discards its coverage. */ From 2de5ee5da5c41c28c13fb5d30bc242ffae13f6bb Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 14:52:06 +0200 Subject: [PATCH 04/20] tests: drive the PEM-to-DER and EncryptedInfoParse guards --- tests/unit-mcdc/test_asn_certgen_whitebox.c | 90 +++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/unit-mcdc/test_asn_certgen_whitebox.c b/tests/unit-mcdc/test_asn_certgen_whitebox.c index 6a05ab4f8d7..9209cb3ccef 100644 --- a/tests/unit-mcdc/test_asn_certgen_whitebox.c +++ b/tests/unit-mcdc/test_asn_certgen_whitebox.c @@ -1682,6 +1682,93 @@ static void wb_parse_x509_acert(void) } #endif + +/* PEM->DER entry guards. Each rejection vector is paired with the accepting + * one in the same binary; the accepting vector only has to get PAST the guard, + * so a well-sized garbage buffer is enough for the argument chain, and a real + * PEM is built only where a successful conversion is required. */ +#if !defined(NO_CERTS) && defined(WOLFSSL_PEM_TO_DER) +static void wb_pem_to_der_guards(void) +{ + byte pem[4096]; + byte out[4096]; + word32 b64Len = (word32)sizeof(pem); + int pemSz = 0; + static const char hdr[] = "-----BEGIN CERTIFICATE-----\n"; + static const char ftr[] = "-----END CERTIFICATE-----\n"; + + XMEMSET(pem, 0, sizeof(pem)); + XMEMSET(out, 0, sizeof(out)); + + /* pem == NULL || buff == NULL || buffSz <= 0 || pemSz <= 0 */ + (void)wc_CertPemToDer(NULL, 32, out, (int)sizeof(out), CERT_TYPE); + (void)wc_CertPemToDer(out, 32, NULL, (int)sizeof(out), CERT_TYPE); + (void)wc_CertPemToDer(out, 32, out, 0, CERT_TYPE); + (void)wc_CertPemToDer(out, 0, out, (int)sizeof(out), CERT_TYPE); + (void)wc_CertPemToDer(out, 32, out, (int)sizeof(out), CERT_TYPE); + + /* pem == NULL || (buff != NULL && buffSz <= 0) || pemSz <= 0 */ + (void)wc_KeyPemToDer(NULL, 32, out, (int)sizeof(out), NULL); + (void)wc_KeyPemToDer(out, 32, out, 0, NULL); + (void)wc_KeyPemToDer(out, 0, out, (int)sizeof(out), NULL); + (void)wc_KeyPemToDer(out, 32, NULL, 0, NULL); + (void)wc_KeyPemToDer(out, 32, out, (int)sizeof(out), NULL); + + (void)wc_PubKeyPemToDer(NULL, 32, out, (int)sizeof(out)); + (void)wc_PubKeyPemToDer(out, 32, out, 0); + (void)wc_PubKeyPemToDer(out, 0, out, (int)sizeof(out)); + (void)wc_PubKeyPemToDer(out, 32, NULL, 0); + (void)wc_PubKeyPemToDer(out, 32, out, (int)sizeof(out)); + + /* A real PEM, so the post-conversion `ret < 0 || der == NULL` guard sees + * its accepting vector too. */ + XMEMCPY(pem, hdr, sizeof(hdr) - 1); + pemSz = (int)(sizeof(hdr) - 1); + b64Len = (word32)(sizeof(pem) - (size_t)pemSz - sizeof(ftr)); + if (Base64_Encode(client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, pem + pemSz, &b64Len) == 0) { + pemSz += (int)b64Len; + XMEMCPY(pem + pemSz, ftr, sizeof(ftr) - 1); + pemSz += (int)(sizeof(ftr) - 1); + (void)wc_CertPemToDer(pem, pemSz, out, (int)sizeof(out), CERT_TYPE); + } + else { + WB_NOTE("Base64_Encode failed; PEM accepting vector skipped"); + } +} +#else +static void wb_pem_to_der_guards(void) +{ + WB_NOTE("PEM-to-DER not compiled; skipped"); +} +#endif + +/* wc_EncryptedInfoParse: info == NULL || pBuffer == NULL || bufSz == 0 */ +#if defined(WOLFSSL_ENCRYPTED_KEYS) && !defined(NO_CERTS) +static void wb_encrypted_info_parse_guards(void) +{ + EncryptedInfo info; + static const char body[] = + "Proc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,0123456789ABCDEF\n\n"; + const char* p = body; + + XMEMSET(&info, 0, sizeof(info)); + + (void)wc_EncryptedInfoParse(NULL, &p, sizeof(body) - 1); + p = body; + (void)wc_EncryptedInfoParse(&info, NULL, sizeof(body) - 1); + p = body; + (void)wc_EncryptedInfoParse(&info, &p, 0); + p = body; + (void)wc_EncryptedInfoParse(&info, &p, sizeof(body) - 1); +} +#else +static void wb_encrypted_info_parse_guards(void) +{ + WB_NOTE("WOLFSSL_ENCRYPTED_KEYS off; skipped"); +} +#endif + int main(void) { printf("asn.c certgen white-box MC/DC supplement\n"); @@ -1714,6 +1801,9 @@ int main(void) wb_verify_x509_acert_bad_args(); wb_parse_x509_acert(); + wb_pem_to_der_guards(); + wb_encrypted_info_parse_guards(); + printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage * entirely in the campaign harness. Failures are surfaced via the From a20e94ea969c67eb0133f16c4903bf68634a3fea Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 14:53:21 +0200 Subject: [PATCH 05/20] tests: drive the PKCS7 public argument chains --- tests/unit-mcdc/test_pkcs7_whitebox.c | 127 ++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/tests/unit-mcdc/test_pkcs7_whitebox.c b/tests/unit-mcdc/test_pkcs7_whitebox.c index a48649ed7fa..89706702fed 100644 --- a/tests/unit-mcdc/test_pkcs7_whitebox.c +++ b/tests/unit-mcdc/test_pkcs7_whitebox.c @@ -1905,6 +1905,132 @@ static void wb_verify_signed_data_guards(void) WB_CHECK(ret < 0, "malformed short SignedData rejected (exercises wrapper)"); } + +/* Public-entry argument chains. Each operand gets the vector where it alone + * fires, plus the all-false vector -- without the latter no operand in the + * chain gets an independence pair inside this binary. The accepting vectors + * only have to pass the guard; failing further in is fine and expected. */ +static void wb_public_arg_guards(void) +{ + wc_PKCS7 pkcs7; + byte key[32]; + byte content[32]; + byte out[4096]; + byte salt[8]; + + XMEMSET(key, 0x0b, sizeof(key)); + XMEMSET(content, 0x0c, sizeof(content)); + XMEMSET(out, 0, sizeof(out)); + XMEMSET(salt, 0x0d, sizeof(salt)); + + if (wc_PKCS7_Init(&pkcs7, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_public_arg_guards skipped"); + wb_fail = 1; + return; + } + + /* pkcs7 == NULL || key == NULL || keySz == 0 */ + (void)wc_PKCS7_SetKey(NULL, key, (word32)sizeof(key)); + (void)wc_PKCS7_SetKey(&pkcs7, NULL, (word32)sizeof(key)); + (void)wc_PKCS7_SetKey(&pkcs7, key, 0); + (void)wc_PKCS7_SetKey(&pkcs7, key, (word32)sizeof(key)); + + /* pkcs7 == NULL || (in == NULL && inSz > 0) */ + (void)wc_PKCS7_SetCustomSKID(NULL, key, (word16)sizeof(key)); + (void)wc_PKCS7_SetCustomSKID(&pkcs7, NULL, (word16)sizeof(key)); + (void)wc_PKCS7_SetCustomSKID(&pkcs7, NULL, 0); + (void)wc_PKCS7_SetCustomSKID(&pkcs7, key, (word16)sizeof(key)); + +#if defined(HAVE_PKCS7) && !defined(NO_PKCS7_ENCRYPTED_DATA) + /* pkcs7 == NULL || privateKey == NULL || privateKeySz == 0 || + * content == NULL || contentSz == 0 || output == NULL || outputSz == 0 */ + (void)wc_PKCS7_EncodeSignedFPD(NULL, (byte*)client_key_der_2048, + (word32)sizeof_client_key_der_2048, RSAk, SHA256h, content, + (word32)sizeof(content), NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedFPD(&pkcs7, NULL, + (word32)sizeof_client_key_der_2048, RSAk, SHA256h, content, + (word32)sizeof(content), NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedFPD(&pkcs7, (byte*)client_key_der_2048, 0, + RSAk, SHA256h, content, (word32)sizeof(content), NULL, 0, out, + (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedFPD(&pkcs7, (byte*)client_key_der_2048, + (word32)sizeof_client_key_der_2048, RSAk, SHA256h, NULL, + (word32)sizeof(content), NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedFPD(&pkcs7, (byte*)client_key_der_2048, + (word32)sizeof_client_key_der_2048, RSAk, SHA256h, content, 0, + NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedFPD(&pkcs7, (byte*)client_key_der_2048, + (word32)sizeof_client_key_der_2048, RSAk, SHA256h, content, + (word32)sizeof(content), NULL, 0, NULL, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedFPD(&pkcs7, (byte*)client_key_der_2048, + (word32)sizeof_client_key_der_2048, RSAk, SHA256h, content, + (word32)sizeof(content), NULL, 0, out, 0); + (void)wc_PKCS7_EncodeSignedFPD(&pkcs7, (byte*)client_key_der_2048, + (word32)sizeof_client_key_der_2048, RSAk, SHA256h, content, + (word32)sizeof(content), NULL, 0, out, (word32)sizeof(out)); + + /* pkcs7 == NULL || encryptKey == NULL || encryptKeySz == 0 || ... */ + (void)wc_PKCS7_EncodeSignedEncryptedFPD(NULL, key, (word32)sizeof(key), + (byte*)client_key_der_2048, (word32)sizeof_client_key_der_2048, + AES256CBCb, RSAk, SHA256h, content, (word32)sizeof(content), NULL, 0, + NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedEncryptedFPD(&pkcs7, NULL, (word32)sizeof(key), + (byte*)client_key_der_2048, (word32)sizeof_client_key_der_2048, + AES256CBCb, RSAk, SHA256h, content, (word32)sizeof(content), NULL, 0, + NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedEncryptedFPD(&pkcs7, key, 0, + (byte*)client_key_der_2048, (word32)sizeof_client_key_der_2048, + AES256CBCb, RSAk, SHA256h, content, (word32)sizeof(content), NULL, 0, + NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedEncryptedFPD(&pkcs7, key, (word32)sizeof(key), + NULL, (word32)sizeof_client_key_der_2048, + AES256CBCb, RSAk, SHA256h, content, (word32)sizeof(content), NULL, 0, + NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedEncryptedFPD(&pkcs7, key, (word32)sizeof(key), + (byte*)client_key_der_2048, 0, + AES256CBCb, RSAk, SHA256h, content, (word32)sizeof(content), NULL, 0, + NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedEncryptedFPD(&pkcs7, key, (word32)sizeof(key), + (byte*)client_key_der_2048, (word32)sizeof_client_key_der_2048, + AES256CBCb, RSAk, SHA256h, NULL, (word32)sizeof(content), NULL, 0, + NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedEncryptedFPD(&pkcs7, key, (word32)sizeof(key), + (byte*)client_key_der_2048, (word32)sizeof_client_key_der_2048, + AES256CBCb, RSAk, SHA256h, content, 0, NULL, 0, + NULL, 0, out, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedEncryptedFPD(&pkcs7, key, (word32)sizeof(key), + (byte*)client_key_der_2048, (word32)sizeof_client_key_der_2048, + AES256CBCb, RSAk, SHA256h, content, (word32)sizeof(content), NULL, 0, + NULL, 0, NULL, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeSignedEncryptedFPD(&pkcs7, key, (word32)sizeof(key), + (byte*)client_key_der_2048, (word32)sizeof_client_key_der_2048, + AES256CBCb, RSAk, SHA256h, content, (word32)sizeof(content), NULL, 0, + NULL, 0, out, 0); + (void)wc_PKCS7_EncodeSignedEncryptedFPD(&pkcs7, key, (word32)sizeof(key), + (byte*)client_key_der_2048, (word32)sizeof_client_key_der_2048, + AES256CBCb, RSAk, SHA256h, content, (word32)sizeof(content), NULL, 0, + NULL, 0, out, (word32)sizeof(out)); +#endif + +#if defined(HAVE_PKCS7) && !defined(NO_PWDBASED) + /* pkcs7 == NULL || passwd == NULL || pLen == 0 || salt == NULL || ... */ + (void)wc_PKCS7_AddRecipient_PWRI(NULL, key, (word32)sizeof(key), salt, + (word32)sizeof(salt), PBKDF2_OID, WC_SHA256, 1000, AES256_WRAP, 0); + (void)wc_PKCS7_AddRecipient_PWRI(&pkcs7, NULL, (word32)sizeof(key), salt, + (word32)sizeof(salt), PBKDF2_OID, WC_SHA256, 1000, AES256_WRAP, 0); + (void)wc_PKCS7_AddRecipient_PWRI(&pkcs7, key, 0, salt, + (word32)sizeof(salt), PBKDF2_OID, WC_SHA256, 1000, AES256_WRAP, 0); + (void)wc_PKCS7_AddRecipient_PWRI(&pkcs7, key, (word32)sizeof(key), NULL, + (word32)sizeof(salt), PBKDF2_OID, WC_SHA256, 1000, AES256_WRAP, 0); + (void)wc_PKCS7_AddRecipient_PWRI(&pkcs7, key, (word32)sizeof(key), salt, 0, + PBKDF2_OID, WC_SHA256, 1000, AES256_WRAP, 0); + (void)wc_PKCS7_AddRecipient_PWRI(&pkcs7, key, (word32)sizeof(key), salt, + (word32)sizeof(salt), PBKDF2_OID, WC_SHA256, 1000, AES256_WRAP, 0); +#endif + + wc_PKCS7_Free(&pkcs7); +} + int main(void) { printf("pkcs7.c white-box MC/DC supplement\n"); @@ -1926,6 +2052,7 @@ int main(void) wb_parse_signer_info(); wb_handle_octet_strings(); wb_verify_signed_data_guards(); + wb_public_arg_guards(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage From 99763b49fcf281747a690c8a412088447bc8e63f Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 14:54:17 +0200 Subject: [PATCH 06/20] tests: add a wc_encrypt white-box for the argument chains --- tests/include.am | 1 + tests/unit-mcdc/test_wc_encrypt_whitebox.c | 137 +++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 tests/unit-mcdc/test_wc_encrypt_whitebox.c diff --git a/tests/include.am b/tests/include.am index a51a1ca3f7e..d2a2866d676 100644 --- a/tests/include.am +++ b/tests/include.am @@ -186,6 +186,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_tfm_whitebox.c \ tests/unit-mcdc/test_tsp_fault_whitebox.c \ tests/unit-mcdc/test_tsp_whitebox.c \ + tests/unit-mcdc/test_wc_encrypt_whitebox.c \ tests/unit-mcdc/test_wc_lms_impl_whitebox.c \ tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c \ tests/unit-mcdc/test_wc_mldsa_whitebox.c \ diff --git a/tests/unit-mcdc/test_wc_encrypt_whitebox.c b/tests/unit-mcdc/test_wc_encrypt_whitebox.c new file mode 100644 index 00000000000..520a59b2292 --- /dev/null +++ b/tests/unit-mcdc/test_wc_encrypt_whitebox.c @@ -0,0 +1,137 @@ +/* test_wc_encrypt_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC supplement for wolfcrypt/src/wc_encrypt.c. + * + * The file's argument chains are only ever called with valid arguments by the + * in-tree tests, so no operand of + * + * if (out == NULL || in == NULL || key == NULL || iv == NULL) + * if (password == NULL || salt == NULL || input == NULL) + * + * gets an independence pair. Each operand needs the vector where it alone is + * NULL, and the chain needs its all-non-NULL vector in THIS binary: llvm-cov + * derives MC/DC per binary, so a rejection on its own proves nothing. + * + * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * as the instrumented library, then linked against that variant's + * libwolfssl.a with wc_encrypt.o removed. Not part of the wolfSSL build. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include + +#include + +#include +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if !defined(NO_AES) && defined(HAVE_AES_CBC) +static void wb_aes_cbc_with_key(void) +{ + byte out[WC_AES_BLOCK_SIZE * 2]; + byte in[WC_AES_BLOCK_SIZE * 2]; + byte key[16]; + byte iv[WC_AES_BLOCK_SIZE]; + + XMEMSET(out, 0, sizeof(out)); + XMEMSET(in, 0x11, sizeof(in)); + XMEMSET(key, 0x22, sizeof(key)); + XMEMSET(iv, 0x33, sizeof(iv)); + + (void)wc_AesCbcEncryptWithKey(NULL, in, (word32)sizeof(in), key, + (word32)sizeof(key), iv); + (void)wc_AesCbcEncryptWithKey(out, NULL, (word32)sizeof(in), key, + (word32)sizeof(key), iv); + (void)wc_AesCbcEncryptWithKey(out, in, (word32)sizeof(in), NULL, + (word32)sizeof(key), iv); + (void)wc_AesCbcEncryptWithKey(out, in, (word32)sizeof(in), key, + (word32)sizeof(key), NULL); + (void)wc_AesCbcEncryptWithKey(out, in, (word32)sizeof(in), key, + (word32)sizeof(key), iv); + + (void)wc_AesCbcDecryptWithKey(NULL, in, (word32)sizeof(in), key, + (word32)sizeof(key), iv); + (void)wc_AesCbcDecryptWithKey(out, in, (word32)sizeof(in), key, + (word32)sizeof(key), iv); +} +#else +static void wb_aes_cbc_with_key(void) +{ + WB_NOTE("AES-CBC not compiled; skipped"); +} +#endif + +#if defined(HAVE_PKCS8) || defined(HAVE_PKCS12) +static void wb_crypt_key(void) +{ + static const char pw[] = "password"; + byte salt[8]; + byte input[32]; + byte cbcIv[WC_AES_BLOCK_SIZE]; + + XMEMSET(salt, 0x44, sizeof(salt)); + XMEMSET(input, 0x55, sizeof(input)); + XMEMSET(cbcIv, 0x66, sizeof(cbcIv)); + + (void)wc_CryptKey(NULL, (int)sizeof(pw) - 1, salt, (int)sizeof(salt), 1000, + PBE_AES256_CBC, input, (int)sizeof(input), PKCS5v2, cbcIv, 1, + WC_SHA256); + (void)wc_CryptKey(pw, (int)sizeof(pw) - 1, NULL, (int)sizeof(salt), 1000, + PBE_AES256_CBC, input, (int)sizeof(input), PKCS5v2, cbcIv, 1, + WC_SHA256); + (void)wc_CryptKey(pw, (int)sizeof(pw) - 1, salt, (int)sizeof(salt), 1000, + PBE_AES256_CBC, NULL, (int)sizeof(input), PKCS5v2, cbcIv, 1, + WC_SHA256); + (void)wc_CryptKey(pw, (int)sizeof(pw) - 1, salt, (int)sizeof(salt), 1000, + PBE_AES256_CBC, input, (int)sizeof(input), PKCS5v2, cbcIv, 1, + WC_SHA256); +} +#else +static void wb_crypt_key(void) +{ + WB_NOTE("PKCS8/PKCS12 not compiled; wc_CryptKey skipped"); +} +#endif + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + + printf("wc_encrypt.c white-box MC/DC supplement\n"); + + wb_aes_cbc_with_key(); + wb_crypt_key(); + + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Always 0: a nonzero exit discards this variant's whole coverage. */ + (void)wb_fail; + return 0; +} From f9a0b91794594508db2826e812624ebcc74a2454 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 15:51:13 +0200 Subject: [PATCH 07/20] tests: add heap-fault white-boxes for the SP backends and sp_int --- tests/include.am | 5 + tests/unit-mcdc/test_sp_c32_fault_whitebox.c | 46 +++ tests/unit-mcdc/test_sp_c64_fault_whitebox.c | 46 +++ tests/unit-mcdc/test_sp_fault_common.h | 318 ++++++++++++++++++ tests/unit-mcdc/test_sp_int_fault_whitebox.c | 256 ++++++++++++++ .../unit-mcdc/test_sp_x86_64_fault_whitebox.c | 46 +++ 6 files changed, 717 insertions(+) create mode 100644 tests/unit-mcdc/test_sp_c32_fault_whitebox.c create mode 100644 tests/unit-mcdc/test_sp_c64_fault_whitebox.c create mode 100644 tests/unit-mcdc/test_sp_fault_common.h create mode 100644 tests/unit-mcdc/test_sp_int_fault_whitebox.c create mode 100644 tests/unit-mcdc/test_sp_x86_64_fault_whitebox.c diff --git a/tests/include.am b/tests/include.am index d2a2866d676..b6d6f77f6ed 100644 --- a/tests/include.am +++ b/tests/include.am @@ -178,10 +178,15 @@ EXTRA_DIST += \ tests/unit-mcdc/test_sp_arm32_whitebox.c \ tests/unit-mcdc/test_sp_arm64_whitebox.c \ tests/unit-mcdc/test_sp_armthumb_whitebox.c \ + tests/unit-mcdc/test_sp_c32_fault_whitebox.c \ tests/unit-mcdc/test_sp_c32_whitebox.c \ + tests/unit-mcdc/test_sp_c64_fault_whitebox.c \ tests/unit-mcdc/test_sp_c64_whitebox.c \ tests/unit-mcdc/test_sp_cortexm_whitebox.c \ + tests/unit-mcdc/test_sp_fault_common.h \ + tests/unit-mcdc/test_sp_int_fault_whitebox.c \ tests/unit-mcdc/test_sp_int_whitebox.c \ + tests/unit-mcdc/test_sp_x86_64_fault_whitebox.c \ tests/unit-mcdc/test_sp_x86_64_whitebox.c \ tests/unit-mcdc/test_tfm_whitebox.c \ tests/unit-mcdc/test_tsp_fault_whitebox.c \ diff --git a/tests/unit-mcdc/test_sp_c32_fault_whitebox.c b/tests/unit-mcdc/test_sp_c32_fault_whitebox.c new file mode 100644 index 00000000000..6ad68da16df --- /dev/null +++ b/tests/unit-mcdc/test_sp_c32_fault_whitebox.c @@ -0,0 +1,46 @@ +/* test_sp_c32_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Heap-fault MC/DC supplement for wolfcrypt/src/sp_c32.c. + * + * Drives the `err == MP_OKAY` operand of the file's success chains by failing + * an SP temporary allocation. See tests/unit-mcdc/test_sp_fault_common.h for + * why that operand is otherwise dead, and what the sweep does. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include + +#include + +/* After the .c: only the test key material is wanted here, and the + * module config does not ask for the buffers itself. */ +#ifndef USE_CERT_BUFFERS_2048 + #define USE_CERT_BUFFERS_2048 +#endif +#include + +#define SP_FAULT_LABEL "sp_c32.c" +#include "test_sp_fault_common.h" diff --git a/tests/unit-mcdc/test_sp_c64_fault_whitebox.c b/tests/unit-mcdc/test_sp_c64_fault_whitebox.c new file mode 100644 index 00000000000..8046509de98 --- /dev/null +++ b/tests/unit-mcdc/test_sp_c64_fault_whitebox.c @@ -0,0 +1,46 @@ +/* test_sp_c64_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Heap-fault MC/DC supplement for wolfcrypt/src/sp_c64.c. + * + * Drives the `err == MP_OKAY` operand of the file's success chains by failing + * an SP temporary allocation. See tests/unit-mcdc/test_sp_fault_common.h for + * why that operand is otherwise dead, and what the sweep does. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include + +#include + +/* After the .c: only the test key material is wanted here, and the + * module config does not ask for the buffers itself. */ +#ifndef USE_CERT_BUFFERS_2048 + #define USE_CERT_BUFFERS_2048 +#endif +#include + +#define SP_FAULT_LABEL "sp_c64.c" +#include "test_sp_fault_common.h" diff --git a/tests/unit-mcdc/test_sp_fault_common.h b/tests/unit-mcdc/test_sp_fault_common.h new file mode 100644 index 00000000000..809d19d8466 --- /dev/null +++ b/tests/unit-mcdc/test_sp_fault_common.h @@ -0,0 +1,318 @@ +/* test_sp_fault_common.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Shared body for the SP backend heap-fault white-boxes. + * + * WHY + * --- + * Every SP backend carries hundreds of decisions shaped + * + * if ((err == MP_OKAY) && ) + * + * whose `err == MP_OKAY` operand has no false side in the campaign's builds. + * The reason is not that the failure is hard to produce, it is that nothing in + * the compiled code can produce it: SP_ALLOC_VAR is + * + * #ifdef WOLFSSL_SP_SMALL_STACK + * if (err == MP_OKAY) { + * (NAME) = XMALLOC(...); + * if ((NAME) == NULL) { err = MEMORY_E; } + * } + * #else + * WC_DO_NOTHING + * + * so without WOLFSSL_SP_SMALL_STACK the variables are plain stack arrays, err + * stays MP_OKAY from entry to exit, and the operand is dead by construction. + * + * This driver therefore only does useful work in a variant built with + * WOLFSSL_SP_SMALL_STACK. Elsewhere it runs the same operations with the + * injector never armed, which costs one quick pass and keeps the file building + * in every variant of the module (a white-box that fails to build is a silent + * skip, and the campaign has lost a module's evidence to that twice). + * + * HOW + * --- + * mcdc_fault_alloc.h fails the n-th and every later allocation. Sweeping n + * across the allocation sites of an operation walks the MEMORY_E failure down + * the whole success chain, so each `(err == MP_OKAY)` checkpoint sees both a + * run where it holds and a run where it does not. + * + * The operations are the public SP entry points, driven with valid operands so + * that an unarmed pass completes normally: the only thing under test is where + * the failure lands, not the arithmetic. + * + * The including TU defines SP_FAULT_LABEL and includes the wolfCrypt .c under + * test before including this header. + */ + +#ifndef SP_FAULT_LABEL + #error "define SP_FAULT_LABEL before including test_sp_fault_common.h" +#endif + +#include "mcdc_fault_alloc.h" + +#include +#include +#include +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +/* Sweep depth. SP entry points allocate a handful of temporaries each, so the + * failure index only has to walk a little past the deepest chain. Kept low on + * purpose: every index repeats a full keygen/sign/verify, TEST_TIMEOUT is wall + * clock, and variants run concurrently under MAXPAR -- a driver that finishes + * alone can still be killed under load, and a killed driver is a silent skip. */ +#ifndef SP_FAULT_MAX_N + #define SP_FAULT_MAX_N 24 +#endif + +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + !defined(MCDC_FA_UNAVAILABLE) +/* ECC. One armed region per operation, not one around the whole chain: the + * injector fails the n-th allocation AND every later one, so an arming that + * spans make_key + sign + verify + ECDH loses the failure inside make_key and + * the later three never run at all. Each operation therefore gets its own + * sweep, with its inputs built while disarmed. */ +static void wb_fault_ecc(int curveId, int fieldSz) +{ + int n; + + /* make_key: the failure walks this operation's own allocation sites. */ + for (n = 1; n <= SP_FAULT_MAX_N; n++) { + ecc_key key; + WC_RNG rng; + + if (wc_InitRng(&rng) != 0) { + return; + } + if (wc_ecc_init(&key) == 0) { + mcdc_fa_arm(n); + (void)wc_ecc_make_key_ex(&rng, fieldSz, &key, curveId); + mcdc_fa_disarm(); + wc_ecc_free(&key); + } + wc_FreeRng(&rng); + } + + /* sign / verify / ECDH: each off a key built with the injector disarmed, + * so the operation under test starts from a valid state and the failure + * lands inside it rather than in its setup. */ + for (n = 1; n <= SP_FAULT_MAX_N; n++) { + ecc_key key; + WC_RNG rng; + byte sig[144]; + byte secret[80]; + byte digest[32]; + word32 sigLen; + word32 secretLen; + int res = 0; + + XMEMSET(digest, 0x5a, sizeof(digest)); + + if (wc_InitRng(&rng) != 0) { + return; + } + if (wc_ecc_init(&key) != 0) { + wc_FreeRng(&rng); + return; + } + if (wc_ecc_make_key_ex(&rng, fieldSz, &key, curveId) == 0) { + sigLen = (word32)sizeof(sig); + mcdc_fa_arm(n); + (void)wc_ecc_sign_hash(digest, (word32)sizeof(digest), sig, + &sigLen, &rng, &key); + mcdc_fa_disarm(); + + /* A real signature to verify, made while disarmed. */ + sigLen = (word32)sizeof(sig); + if (wc_ecc_sign_hash(digest, (word32)sizeof(digest), sig, &sigLen, + &rng, &key) == 0) { + mcdc_fa_arm(n); + (void)wc_ecc_verify_hash(sig, sigLen, digest, + (word32)sizeof(digest), &res, &key); + mcdc_fa_disarm(); + } + + secretLen = (word32)sizeof(secret); + PRIVATE_KEY_UNLOCK(); + mcdc_fa_arm(n); + (void)wc_ecc_shared_secret(&key, &key, secret, &secretLen); + mcdc_fa_disarm(); + PRIVATE_KEY_LOCK(); + + mcdc_fa_arm(n); + (void)wc_ecc_check_key(&key); + mcdc_fa_disarm(); + } + wc_ecc_free(&key); + wc_FreeRng(&rng); + } +} +#else +static void wb_fault_ecc(int curveId, int fieldSz) +{ + (void)curveId; + (void)fieldSz; +} +#endif + +#if defined(WOLFSSL_HAVE_SP_DH) && !defined(NO_DH) && \ + !defined(MCDC_FA_UNAVAILABLE) +/* DH: key agreement over a compiled-in FFDHE group. */ +static void wb_fault_dh(void) +{ + int n; + + for (n = 1; n <= SP_FAULT_MAX_N; n++) { + DhKey key; + WC_RNG rng; + byte priv[384]; + byte pub[384]; + byte agree[384]; + word32 privSz = (word32)sizeof(priv); + word32 pubSz = (word32)sizeof(pub); + word32 agreeSz = (word32)sizeof(agree); + + if (wc_InitRng(&rng) != 0) { + return; + } + if (wc_InitDhKey(&key) != 0) { + wc_FreeRng(&rng); + return; + } + + if (wc_DhSetNamedKey(&key, WC_FFDHE_2048) == 0) { + mcdc_fa_arm(n); + (void)wc_DhGenerateKeyPair(&key, &rng, priv, &privSz, pub, &pubSz); + mcdc_fa_disarm(); + + privSz = (word32)sizeof(priv); + pubSz = (word32)sizeof(pub); + if (wc_DhGenerateKeyPair(&key, &rng, priv, &privSz, pub, + &pubSz) == 0) { + mcdc_fa_arm(n); + (void)wc_DhAgree(&key, agree, &agreeSz, priv, privSz, pub, + pubSz); + mcdc_fa_disarm(); + } + } + + wc_FreeDhKey(&key); + wc_FreeRng(&rng); + } +} +#else +static void wb_fault_dh(void) +{ +} +#endif + +#if defined(WOLFSSL_HAVE_SP_RSA) && !defined(NO_RSA) && \ + !defined(MCDC_FA_UNAVAILABLE) +/* RSA: public/private operations off a decoded key, under the sweep. */ +static void wb_fault_rsa(void) +{ + int n; + + for (n = 1; n <= SP_FAULT_MAX_N; n++) { + RsaKey key; + WC_RNG rng; + byte out[256]; + byte plain[256]; + word32 idx = 0; + + if (wc_InitRng(&rng) != 0) { + return; + } + if (wc_InitRsaKey(&key, NULL) != 0) { + wc_FreeRng(&rng); + return; + } + if (wc_RsaPrivateKeyDecode(client_key_der_2048, &idx, &key, + (word32)sizeof_client_key_der_2048) == 0) { + mcdc_fa_arm(n); + (void)wc_RsaPublicEncrypt((const byte*)"mcdc", 4, out, + (word32)sizeof(out), &key, &rng); + mcdc_fa_disarm(); + + if (wc_RsaPublicEncrypt((const byte*)"mcdc", 4, out, + (word32)sizeof(out), &key, &rng) > 0) { + mcdc_fa_arm(n); + (void)wc_RsaPrivateDecrypt(out, (word32)sizeof(out), plain, + (word32)sizeof(plain), &key); + mcdc_fa_disarm(); + } + } + + wc_FreeRsaKey(&key); + wc_FreeRng(&rng); + } +} +#else +static void wb_fault_rsa(void) +{ +} +#endif + +int main(void) +{ + /* Unbuffered: on a timeout the process is killed and anything still + * buffered is lost, which reads as an empty log. */ + setvbuf(stdout, NULL, _IONBF, 0); + + printf("%s heap-fault white-box supplement\n", SP_FAULT_LABEL); + +#ifdef MCDC_FA_UNAVAILABLE + WB_NOTE("allocator hooks unavailable in this config; nothing to sweep"); +#else + #ifndef WOLFSSL_SP_SMALL_STACK + WB_NOTE("WOLFSSL_SP_SMALL_STACK off: SP temporaries are stack arrays and " + "err cannot leave MP_OKAY; sweep runs but cannot fail an SP alloc"); + #endif + mcdc_fa_install(); + + #ifndef WOLFSSL_SP_NO_256 + wb_fault_ecc(ECC_SECP256R1, 32); + #endif + #ifdef WOLFSSL_SP_384 + wb_fault_ecc(ECC_SECP384R1, 48); + #endif + #ifdef WOLFSSL_SP_521 + wb_fault_ecc(ECC_SECP521R1, 66); + #endif + + wb_fault_dh(); + wb_fault_rsa(); + + mcdc_fa_disarm(); + mcdc_fa_restore(); +#endif + + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Always 0: a nonzero exit discards this variant's whole coverage. */ + (void)wb_fail; + return 0; +} diff --git a/tests/unit-mcdc/test_sp_int_fault_whitebox.c b/tests/unit-mcdc/test_sp_int_fault_whitebox.c new file mode 100644 index 00000000000..b4d607de865 --- /dev/null +++ b/tests/unit-mcdc/test_sp_int_fault_whitebox.c @@ -0,0 +1,256 @@ +/* test_sp_int_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Heap-fault MC/DC supplement for wolfcrypt/src/sp_int.c. + * + * TARGET + * ------ + * sp_int.c carries its error state in `err` and gates every subsequent step on + * it: + * + * if ((err == MP_OKAY) && useMont) { + * if ((!done) && (err == MP_OKAY)) { + * if ((err == MP_OKAY) && sp_isone(a)) { + * + * Called with valid operands nothing sets `err`, so the first operand of each + * of these never takes its false side. The failure that does set it is a + * temporary allocation: under WOLFSSL_SMALL_STACK (sp_int.h:872) + * DECL_MP_INT_SIZE/NEW_MP_INT_SIZE become a real XMALLOC whose result is + * checked, whereas otherwise the temporaries are stack arrays and `err` cannot + * change at all. + * + * The campaign's sp-math module already builds a `small_stack` variant, so + * unlike the SP backends this needs no new configuration -- only this driver. + * + * METHOD + * ------ + * mcdc_fault_alloc.h fails the n-th and every later allocation; sweeping n + * walks the MEMORY_E down the allocation sites of each operation, so every + * `err == MP_OKAY` checkpoint downstream of one is observed both holding and + * not holding. + * + * Operands are deliberately small. These decisions test the error state and + * the shape of the operands, not their magnitude, and the sweep repeats every + * operation once per fail-index -- TEST_TIMEOUT is wall clock and variants run + * concurrently under MAXPAR, so a full-size modexp here would be a timeout + * rather than evidence. + * + * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * as the instrumented library, then linked against that variant's + * libwolfssl.a with sp_int.o removed. Not part of the wolfSSL build. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include + +#include + +#include "mcdc_fault_alloc.h" + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFSSL_SP_MATH) || defined(WOLFSSL_SP_MATH_ALL) + +#ifndef SP_FAULT_MAX_N + #define SP_FAULT_MAX_N 40 +#endif + +/* Small primes/moduli: big enough to take the montgomery and non-montgomery + * routes, small enough that the whole sweep stays well inside TEST_TIMEOUT. */ +static const char* WB_M_ODD = "F0000000000000000000000000000037"; +static const char* WB_M_EVEN = "F0000000000000000000000000000038"; +static const char* WB_B = "0123456789ABCDEF0123456789ABCDEF"; +static const char* WB_E = "10001"; + +static void wb_exptmod_sweep(void) +{ + int n; + + for (n = 1; n <= SP_FAULT_MAX_N; n++) { + mp_int b; + mp_int e; + mp_int m; + mp_int r; + + if (mp_init_multi(&b, &e, &m, &r, NULL, NULL) != MP_OKAY) { + wb_fail = 1; + return; + } + if ((mp_read_radix(&b, WB_B, MP_RADIX_HEX) == MP_OKAY) && + (mp_read_radix(&e, WB_E, MP_RADIX_HEX) == MP_OKAY)) { + /* Odd modulus takes the montgomery route, even the divide one. */ + if (mp_read_radix(&m, WB_M_ODD, MP_RADIX_HEX) == MP_OKAY) { + mcdc_fa_arm(n); + (void)mp_exptmod(&b, &e, &m, &r); + (void)mp_exptmod_ex(&b, &e, (int)m.used, &m, &r); + (void)mp_exptmod_nct(&b, &e, &m, &r); + mcdc_fa_disarm(); + } + if (mp_read_radix(&m, WB_M_EVEN, MP_RADIX_HEX) == MP_OKAY) { + mcdc_fa_arm(n); + (void)mp_exptmod(&b, &e, &m, &r); + (void)mp_exptmod_nct(&b, &e, &m, &r); + mcdc_fa_disarm(); + } + } + mp_free(&b); + mp_free(&e); + mp_free(&m); + mp_free(&r); + } +} + +static void wb_invmod_sweep(void) +{ + int n; + + for (n = 1; n <= SP_FAULT_MAX_N; n++) { + mp_int a; + mp_int m; + mp_int r; + + if (mp_init_multi(&a, &m, &r, NULL, NULL, NULL) != MP_OKAY) { + wb_fail = 1; + return; + } + if (mp_read_radix(&a, WB_B, MP_RADIX_HEX) == MP_OKAY) { + if (mp_read_radix(&m, WB_M_ODD, MP_RADIX_HEX) == MP_OKAY) { + mcdc_fa_arm(n); + (void)mp_invmod(&a, &m, &r); +#ifdef WOLFSSL_SP_INVMOD_MONT_CT + (void)mp_invmod_mont_ct(&a, &m, &r, (sp_digit)1); +#endif + mcdc_fa_disarm(); + } + /* Even modulus routes through the division-based inverse. */ + if (mp_read_radix(&m, WB_M_EVEN, MP_RADIX_HEX) == MP_OKAY) { + mcdc_fa_arm(n); + (void)mp_invmod(&a, &m, &r); + mcdc_fa_disarm(); + } + } + mp_free(&a); + mp_free(&m); + mp_free(&r); + } +} + +static void wb_div_mul_sweep(void) +{ + int n; + + for (n = 1; n <= SP_FAULT_MAX_N; n++) { + mp_int a; + mp_int b; + mp_int q; + mp_int rem; + + if (mp_init_multi(&a, &b, &q, &rem, NULL, NULL) != MP_OKAY) { + wb_fail = 1; + return; + } + if ((mp_read_radix(&a, WB_B, MP_RADIX_HEX) == MP_OKAY) && + (mp_read_radix(&b, WB_M_ODD, MP_RADIX_HEX) == MP_OKAY)) { + mcdc_fa_arm(n); + (void)mp_div(&a, &b, &q, &rem); + (void)mp_mod(&a, &b, &rem); + (void)mp_mulmod(&a, &a, &b, &rem); + (void)mp_sqrmod(&a, &b, &rem); + (void)mp_gcd(&a, &b, &q); + mcdc_fa_disarm(); + } + mp_free(&a); + mp_free(&b); + mp_free(&q); + mp_free(&rem); + } +} + +#if defined(WOLFSSL_KEY_GEN) || !defined(NO_DH) || !defined(NO_DSA) +static void wb_prime_sweep(void) +{ + int n; + + for (n = 1; n <= SP_FAULT_MAX_N; n++) { + mp_int a; + int res = 0; + + if (mp_init(&a) != MP_OKAY) { + wb_fail = 1; + return; + } + if (mp_read_radix(&a, WB_M_ODD, MP_RADIX_HEX) == MP_OKAY) { + mcdc_fa_arm(n); + (void)mp_prime_is_prime(&a, 2, &res); + mcdc_fa_disarm(); + } + mp_free(&a); + } +} +#else +static void wb_prime_sweep(void) +{ + WB_NOTE("prime testing not compiled; skipped"); +} +#endif + +#endif /* WOLFSSL_SP_MATH || WOLFSSL_SP_MATH_ALL */ + +int main(void) +{ + /* Unbuffered: on a timeout the process is killed and anything still + * buffered is lost, which reads as an empty log. */ + setvbuf(stdout, NULL, _IONBF, 0); + + printf("sp_int.c heap-fault white-box supplement\n"); + +#if !defined(WOLFSSL_SP_MATH) && !defined(WOLFSSL_SP_MATH_ALL) + WB_NOTE("SP math not compiled; nothing to exercise"); +#elif defined(MCDC_FA_UNAVAILABLE) + WB_NOTE("allocator hooks unavailable in this config; nothing to sweep"); +#else + #ifndef WOLFSSL_SMALL_STACK + WB_NOTE("WOLFSSL_SMALL_STACK off: mp_int temporaries are stack arrays, so " + "err cannot leave MP_OKAY; sweep runs but cannot fail one"); + #endif + mcdc_fa_install(); + + wb_exptmod_sweep(); + wb_invmod_sweep(); + wb_div_mul_sweep(); + wb_prime_sweep(); + + mcdc_fa_disarm(); + mcdc_fa_restore(); +#endif + + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Always 0: a nonzero exit discards this variant's whole coverage. */ + (void)wb_fail; + return 0; +} diff --git a/tests/unit-mcdc/test_sp_x86_64_fault_whitebox.c b/tests/unit-mcdc/test_sp_x86_64_fault_whitebox.c new file mode 100644 index 00000000000..d2fa467aa78 --- /dev/null +++ b/tests/unit-mcdc/test_sp_x86_64_fault_whitebox.c @@ -0,0 +1,46 @@ +/* test_sp_x86_64_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Heap-fault MC/DC supplement for wolfcrypt/src/sp_x86_64.c. + * + * Drives the `err == MP_OKAY` operand of the file's success chains by failing + * an SP temporary allocation. See tests/unit-mcdc/test_sp_fault_common.h for + * why that operand is otherwise dead, and what the sweep does. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include + +#include + +/* After the .c: only the test key material is wanted here, and the + * module config does not ask for the buffers itself. */ +#ifndef USE_CERT_BUFFERS_2048 + #define USE_CERT_BUFFERS_2048 +#endif +#include + +#define SP_FAULT_LABEL "sp_x86_64.c" +#include "test_sp_fault_common.h" From 63390ae5b935449442bf5ef8ff7fdcbadad3ac03 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 16:31:55 +0200 Subject: [PATCH 08/20] tests: sweep allocation failures through the sp_x86_64 static drivers --- tests/unit-mcdc/test_sp_x86_64_whitebox.c | 63 +++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/unit-mcdc/test_sp_x86_64_whitebox.c b/tests/unit-mcdc/test_sp_x86_64_whitebox.c index 4575736ed87..14c9ffa4a53 100644 --- a/tests/unit-mcdc/test_sp_x86_64_whitebox.c +++ b/tests/unit-mcdc/test_sp_x86_64_whitebox.c @@ -165,6 +165,11 @@ * before the .c below pulls in any wolfSSL header -- routes every * SAVE_VECTOR_REGISTERS2() site through a variable this file controls. Same * arrangement as test_wc_mlkem_poly_whitebox.c. */ +/* Sweep depth for the allocation-failure pass. Each index repeats the + * whole dispatch+crafted driving, and TEST_TIMEOUT is wall clock under + * MAXPAR, so this stays modest. */ +#define WB_FAULT_MAX_N 20 + static int wb_intr_ret = 0; #define WC_CHECK_FOR_INTR_SIGNALS() (wb_intr_ret) @@ -183,6 +188,8 @@ static int wb_intr_ret = 0; #include "mcdc_fault_mutex.h" +#include "mcdc_fault_alloc.h" + #include #include #include @@ -1549,6 +1556,7 @@ static void wb_run_crafted_curve(int curve_id, int fieldSz, byte bigbuf[80]; /* fieldSz+1 <= 67 (P-521); comfortably fits */ int inMont; int map; + int fa; int ok = 1; XMEMSET(&keyA, 0, sizeof(keyA)); @@ -1606,6 +1614,18 @@ static void wb_run_crafted_curve(int curve_id, int fieldSz, inMont, r, map, NULL); } } + /* The `(err == MP_OKAY) && (!inMont)` triples above only ever + * see err holding. err can only move when SP_ALLOC_VAR is a + * real allocation (WOLFSSL_SP_SMALL_STACK), and only for THIS + * function's own two allocations -- so the arming has to hug + * the call. Anything wider and the failure lands in the setup + * and the target is never entered. */ + for (fa = 1; fa <= 4; fa++) { + mcdc_fa_arm(fa); + (void)mulmod_add(&km, &keyA.pubkey, &keyB.pubkey, 0, r, 1, + NULL); + mcdc_fa_disarm(); + } mp_clear(&km); } wc_ecc_del_point(r); @@ -1905,6 +1925,12 @@ int main(void) * TT+FT+TF => full MC/DC of each BMI2&&ADX dispatch within this binary. */ { cpuid_flags_t real = cpuid_get_flags(); + int wb_n; + + /* Installed before the first pass: the drivers below arm the injector + * around individual calls themselves, and an arm() with no allocators + * installed is a no-op. */ + mcdc_fa_install(); /* Many dispatches live inside data-dependent blocks (e.g. * sp__calc_vfy_point / ecc_is_point / calc_s), only reached with @@ -1969,6 +1995,43 @@ int main(void) wb_intr_ret = 0; wb_run_rsa_free(); + + /* Allocation-failure pass. + * + * Under WOLFSSL_SP_SMALL_STACK, SP_ALLOC_VAR is a real XMALLOC whose + * result is checked, so a failed allocation is the only thing that can + * put `err` anywhere other than MP_OKAY -- which is what every + * `if ((err == MP_OKAY) && ...)` in this file needs to see its first + * operand go false. Without the macro those temporaries are stack + * arrays and the operand is dead by construction; the sweep then costs + * one pass and proves nothing, which is why it runs last. + * + * It reuses wb_run_dispatch()/wb_run_crafted() rather than the public + * API on purpose: the chains live in the file-static + * sp_*_ecc_mulmod_add/calc_vfy_point/calc_s family, and those two are + * already written to call every one of them. + * + * mcdc_fa_arm(n) fails allocation n AND every later one, so each pass + * gets its own arming with the setup done disarmed. */ + cpuid_select_flags(real); + for (wb_n = 1; wb_n <= WB_FAULT_MAX_N; wb_n++) { + mcdc_fa_arm(wb_n); + wb_run_dispatch(); + mcdc_fa_disarm(); + + mcdc_fa_arm(wb_n); + wb_run_rsa_signverify(); + mcdc_fa_disarm(); + + mcdc_fa_arm(wb_n); + wb_run_dh(); + mcdc_fa_disarm(); + } + /* wb_run_crafted() is deliberately NOT wrapped: a blanket arming makes + * its own key setup fail, so it returns before reaching the targets. + * Its mulmod_add sweep is armed around the call itself instead. */ + mcdc_fa_disarm(); + mcdc_fa_restore(); } printf("done (%s)\n", wb_fail ? "with skips" : "ok"); From 8d1bd770fdbc0f79cf5ad30dcd2576867f5d74b8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 17:25:16 +0200 Subject: [PATCH 09/20] tests: close the mlkem and mldsa sampler, dispatch and guard rows --- tests/unit-mcdc/test_mldsa_fault_whitebox.c | 100 ++++ tests/unit-mcdc/test_wc_mldsa_whitebox.c | 461 ++++++++++++++++- tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c | 469 +++++++++++++++++- 3 files changed, 1018 insertions(+), 12 deletions(-) diff --git a/tests/unit-mcdc/test_mldsa_fault_whitebox.c b/tests/unit-mcdc/test_mldsa_fault_whitebox.c index 69ffdc5e90b..a125b94de4c 100644 --- a/tests/unit-mcdc/test_mldsa_fault_whitebox.c +++ b/tests/unit-mcdc/test_mldsa_fault_whitebox.c @@ -301,6 +301,103 @@ static void sweep_decode(const byte* pubDer, word32 pubDerLen, } #endif /* WB_MLDSA_ASN1 */ } + +/* ------------------------------------------------------------------------- * + * The nine AVX2 generators -- wc_mldsa_gen_matrix_{4x4,6x5,8x7}_avx2, + * wc_mldsa_gen_s_{4_4,5_6,7_8}_avx2 and wc_mldsa_gen_y_{4,5,7}_avx2 -- each + * open with + * + * rand = XMALLOC(...); + * state = XMALLOC(...); + * if ((rand == NULL) || (state == NULL)) { ... return MEMORY_E; } + * + * Despite reading like an argument check this is an ALLOCATION check, live + * only under WOLFSSL_SMALL_STACK (forced for this TU, see the file header). + * Its three rows map exactly onto the injector's fail-index: + * + * armed at 1 -> first XMALLOC fails -> (T,-) decision true + * armed at 2 -> only the second fails -> (F,T) decision true + * disarmed -> both succeed -> (F,F) decision false + * + * and cond 0 needs the (F,F) row as its pair partner, so the successful call + * is part of the proof, not just a sanity check. Only ML-DSA-44 sizes are + * reachable from this file's WB_LEVEL key operations, so the 6x5 / 8x7 / 5_6 / + * 7_8 / y_5 / y_7 generators are called directly with their own buffers -- + * they are file-static, and this TU #includes wc_mldsa.c, so they are in + * scope. Each arming brackets ONE generator call with its buffers already + * built while disarmed. + * ------------------------------------------------------------------------- */ +#if defined(USE_INTEL_SPEEDUP) && !defined(WC_SHA3_NO_ASM) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ + !defined(WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM) && \ + !defined(WOLFSSL_MLDSA_NO_SIGN) && \ + !defined(WOLFSSL_NO_ML_DSA_44) && !defined(WOLFSSL_NO_ML_DSA_65) && \ + !defined(WOLFSSL_NO_ML_DSA_87) + +#define WB_GEN_AVX2 + +/* Largest shapes: A is k x l (8 x 7 for ML-DSA-87), s1/s2 and y are at most 8 + * polynomials. File scope keeps ~90 KB off the stack. */ +static sword32 s_genA[8 * 7 * MLDSA_N]; +static sword32 s_genS1[8 * MLDSA_N]; +static sword32 s_genS2[8 * MLDSA_N]; +static sword32 s_genY[8 * MLDSA_N]; +/* Seed inputs are read, never written: MLDSA_GEN_A_SEED_SZ (34) for the matrix + * generators and MLDSA_PRIV_SEED_SZ (64) for the s/y generators. */ +static byte s_genSeed[128]; + +/* Disarmed call first (its allocations must succeed), then the two armed + * positions, each disarmed again immediately so the next row starts clean. */ +#define WB_GEN_ROWS(call) \ + do { \ + mcdc_fa_disarm(); \ + (void)(call); \ + mcdc_fa_arm(1); \ + (void)(call); \ + mcdc_fa_disarm(); \ + mcdc_fa_arm(2); \ + (void)(call); \ + mcdc_fa_disarm(); \ + } while (0) + +static void sweep_gen_avx2_allocs(void) +{ + sword32* s[2]; + wc_Shake shake256; + int haveShake; + + XMEMSET(s_genSeed, 0x3c, sizeof(s_genSeed)); + XMEMSET(s_genA, 0, sizeof(s_genA)); + XMEMSET(s_genS1, 0, sizeof(s_genS1)); + XMEMSET(s_genS2, 0, sizeof(s_genS2)); + XMEMSET(s_genY, 0, sizeof(s_genY)); + s[0] = s_genS1; + s[1] = s_genS2; + + /* Built while disarmed: gen_y_5 squeezes through this object. */ + haveShake = (wc_InitShake256(&shake256, NULL, INVALID_DEVID) == 0); + + WB_GEN_ROWS(wc_mldsa_gen_matrix_4x4_avx2(s_genA, s_genSeed)); + WB_GEN_ROWS(wc_mldsa_gen_matrix_6x5_avx2(s_genA, s_genSeed)); + WB_GEN_ROWS(wc_mldsa_gen_matrix_8x7_avx2(s_genA, s_genSeed)); + + WB_GEN_ROWS(wc_mldsa_gen_s_4_4_avx2(s, s_genSeed)); + WB_GEN_ROWS(wc_mldsa_gen_s_5_6_avx2(s, s_genSeed)); + WB_GEN_ROWS(wc_mldsa_gen_s_7_8_avx2(s, s_genSeed)); + + WB_GEN_ROWS(wc_mldsa_gen_y_4_avx2(s_genY, s_genSeed, 0)); + if (haveShake) { + WB_GEN_ROWS(wc_mldsa_gen_y_5_avx2(s_genY, s_genSeed, 0, &shake256)); + } + WB_GEN_ROWS(wc_mldsa_gen_y_7_avx2(s_genY, s_genSeed, 0)); + + if (haveShake) { + wc_Shake256_Free(&shake256); + } + WB_NOTE("AVX2 generator rand/state allocation rows exercised"); +} + +#endif /* WB_GEN_AVX2 conditions */ #endif /* !MCDC_FA_UNAVAILABLE */ int main(int argc, char** argv) @@ -453,6 +550,9 @@ int main(int argc, char** argv) sweep_export(&key); sweep_decode(s_pubDer, (word32)pubDerLen, s_privDer, (word32)privDerLen); +#ifdef WB_GEN_AVX2 + sweep_gen_avx2_allocs(); +#endif WB_NOTE("fault-index sweeps over MakeKey / Sign / Verify / Export / " "Decode done"); } diff --git a/tests/unit-mcdc/test_wc_mldsa_whitebox.c b/tests/unit-mcdc/test_wc_mldsa_whitebox.c index 73222720220..54e8f853d34 100644 --- a/tests/unit-mcdc/test_wc_mldsa_whitebox.c +++ b/tests/unit-mcdc/test_wc_mldsa_whitebox.c @@ -57,11 +57,131 @@ * controls, using the library's own hook rather than overriding a macro * behind its back. Same arrangement as test_wc_mlkem_poly_whitebox.c. */ -static int wb_intr_ret = 0; -#define WC_CHECK_FOR_INTR_SIGNALS() (wb_intr_ret) +/* The hook is a function rather than a plain variable because the dispatches + * NEST: mldsa_expand_a() picks an AVX512 matrix generator, and only when that + * arm is declined does control reach the portable code whose mldsa_ntt() / + * mldsa_invntt() / mldsa_mul() carry their own + * + * if (USE_INTEL_AVX512(cpuid_flags) && (SAVE_VECTOR_REGISTERS2() == 0)) + * + * A process-wide "always accept" never reaches those inner sites at all, and a + * process-wide "always refuse" reaches them only on their false row -- which + * is exactly the shape of the 12 residual conditions at wc_mldsa.c:6832/7861/ + * 7890/8242. Refusing a periodic subset of the calls instead lets an outer + * dispatch be declined while a later inner one is accepted, so both rows of + * the inner decision land in one binary without any cpuid bit being claimed + * that this CPU does not have. */ +static int wb_intr_ret = 0; /* != 0: refuse every save (blanket row) */ +static long wb_intr_ix = 0; /* SAVE_VECTOR_REGISTERS2() calls seen */ +static long wb_intr_mod = 0; /* > 0: refuse when (ix % mod) == res */ +static long wb_intr_res = 0; + +static int wb_intr_hook(void) +{ + long ix = wb_intr_ix++; + + if (wb_intr_ret != 0) { + return wb_intr_ret; + } + if ((wb_intr_mod > 0) && ((ix % wb_intr_mod) == wb_intr_res)) { + return 1; + } + return 0; +} +#define WC_CHECK_FOR_INTR_SIGNALS() wb_intr_hook() + +/* ------------------------------------------------------------------------- * + * Rejection-sampling lane interposition. + * + * The six AVX2 generators each fill four (or three) polynomials in parallel + * and then loop while any lane is still short: + * + * ctr0 = wc_mldsa_rej_uniform_n_avx2(...); ... ctr3 = ... + * while ((ctr0 < MLDSA_N) || (ctr1 < MLDSA_N) || (ctr2 < MLDSA_N) || + * (ctr3 < MLDSA_N)) { ... } + * + * Each operand's independence pair needs an evaluation in which THAT lane is + * the first short one, and a real seed fills every lane from the first block + * essentially always -- so the vector observed is (F,F,F,F) and which operands + * a run happens to catch is luck rather than test design. + * + * The loop's counters come from WOLFSSL_LOCAL leaf routines declared in + * wc_mldsa.h (the first-pass sampler wc_mldsa_rej_uniform_n_avx2 and the eta + * extractors), so declaring that header first keeps the real prototypes under + * their real names and the renames below rewrite only the uses inside + * wc_mldsa.c. The refill routines (wc_mldsa_rej_uniform_avx2, and the second + * and later calls into the extractors) are deliberately left alone so every + * loop still terminates on its next pass. */ +#include +#include + +#if defined(USE_INTEL_SPEEDUP) && !defined(WC_SHA3_NO_ASM) + +static int wb_rej_n_avx2(sword32* a, word32 len, const byte* r, word32 rLen); +static void wb_extract_eta2_avx2(const byte* z, unsigned int zLen, sword32* s, + unsigned int* cnt); +static void wb_extract_eta4_avx2(const byte* z, unsigned int zLen, sword32* s, + unsigned int* cnt); + +#define wc_mldsa_rej_uniform_n_avx2 wb_rej_n_avx2 +#define wc_mldsa_extract_coeffs_eta2_avx2 wb_extract_eta2_avx2 +#define wc_mldsa_extract_coeffs_eta4_avx2 wb_extract_eta4_avx2 + +#endif /* USE_INTEL_SPEEDUP && !WC_SHA3_NO_ASM */ #include +#if defined(USE_INTEL_SPEEDUP) && !defined(WC_SHA3_NO_ASM) +#undef wc_mldsa_rej_uniform_n_avx2 +#undef wc_mldsa_extract_coeffs_eta2_avx2 +#undef wc_mldsa_extract_coeffs_eta4_avx2 + +/* Sampler-call index within the current pass and the call to report one + * coefficient short. The polynomial is still fully and correctly sampled -- + * the generator's own refill pass writes the last coefficient -- so the only + * behavioural change is that the "a lane came up short" path runs, which is + * what the hardware does anyway a few times in every thousand key + * generations. */ +static long wb_lane_ix = 0; +static long wb_lane_at = -1; + +static int wb_lane_hit(void) +{ + long ix = wb_lane_ix++; + + return (wb_lane_at >= 0) && (ix == wb_lane_at); +} + +static int wb_rej_n_avx2(sword32* a, word32 len, const byte* r, word32 rLen) +{ + int got = wc_mldsa_rej_uniform_n_avx2(a, len, r, rLen); + + if (wb_lane_hit() && (got == (int)len) && (len > 0)) { + got = (int)len - 1; + } + return got; +} + +static void wb_extract_eta2_avx2(const byte* z, unsigned int zLen, sword32* s, + unsigned int* cnt) +{ + wc_mldsa_extract_coeffs_eta2_avx2(z, zLen, s, cnt); + if (wb_lane_hit() && (*cnt >= (unsigned int)MLDSA_N)) { + *cnt = (unsigned int)MLDSA_N - 1; + } +} + +static void wb_extract_eta4_avx2(const byte* z, unsigned int zLen, sword32* s, + unsigned int* cnt) +{ + wc_mldsa_extract_coeffs_eta4_avx2(z, zLen, s, cnt); + if (wb_lane_hit() && (*cnt >= (unsigned int)MLDSA_N)) { + *cnt = (unsigned int)MLDSA_N - 1; + } +} + +#endif /* USE_INTEL_SPEEDUP && !WC_SHA3_NO_ASM */ + #include static int wb_notes = 0; @@ -729,6 +849,18 @@ static void wb_oid_to_level(void) * Clearing bits only ever selects portable C, and the rows that keep bits * claim only what this CPU actually reported. * ------------------------------------------------------------------------- */ +/* Shared scratch for the direct file-static calls below: the largest shape is + * ML-DSA-87's 8 x 7 matrix, and s1/s2 are at most 8 polynomials each. File + * scope keeps ~75 KB off the stack. */ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(USE_INTEL_SPEEDUP) +static sword32 wb_lane_a[8 * 7 * MLDSA_N]; +static sword32 wb_lane_s1[8 * MLDSA_N]; +static sword32 wb_lane_s2[8 * MLDSA_N]; +/* Read-only inputs: MLDSA_GEN_A_SEED_SZ (34) for the matrix generators, + * MLDSA_PRIV_SEED_SZ (64) for the s generators. */ +static byte wb_lane_seed[128]; +#endif + #if defined(WOLFSSL_HAVE_MLDSA) && defined(USE_INTEL_SPEEDUP) && \ !defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_MLDSA_NO_VERIFY) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) @@ -805,6 +937,15 @@ static void wb_dispatch_rows(void) { CPUID_INTEL, 0, 0 }, /* richest arm */ { CPUID_INTEL, 0, 1 }, /* save refused */ { CPUID_INTEL, CPUID_AVX512_BW, 0 }, /* F set, BW clear */ + { CPUID_INTEL, CPUID_AVX512_BW, 1 }, /* BW clear, refuse */ + /* The AVX512 matrix/mask dispatches read + * USE_INTEL_AVX512(f) && IS_INTEL_BMI2(f) && (save == 0) + * so their BMI2 operand only takes its false side on a row that keeps + * AVX512 and drops BMI2 -- dropping both together (further down) + * never evaluates it. */ + { CPUID_INTEL, CPUID_BMI2, 0 }, /* AVX512, no BMI2 */ + { CPUID_INTEL, CPUID_BMI2, 1 }, /* ditto, refuse */ + { CPUID_INTEL, CPUID_AVX2, 0 }, /* AVX512, no AVX2 */ { CPUID_INTEL, CPUID_AVX512, 0 }, /* F clear */ { CPUID_INTEL, CPUID_AVX512_VBMI, 0 }, /* no VBMI */ { CPUID_INTEL, CPUID_AVX512 | CPUID_AVX512_BW | @@ -834,8 +975,71 @@ static void wb_dispatch_rows(void) } } + /* Periodic save-refusal rows: see the wb_intr_hook comment at the top of + * this file. Every feature bit stays set so the OUTER dispatch is the one + * being declined, and the inner NTT/mul dispatches get the accepted row + * that a blanket refusal can never give them. Several periods are used + * because which call index an outer dispatch lands on depends on how much + * hashing the level did before it. */ + { + static const long periods[][2] = { + { 2, 0 }, { 2, 1 }, { 3, 0 }, { 3, 1 }, { 3, 2 } + }; + unsigned p; + + for (p = 0; p < sizeof(periods) / sizeof(periods[0]); p++) { + cpuid_flags = WC_CPUID_INITIALIZER; + (void)cpuid_get_flags_ex(&cpuid_flags); + cpuid_flags |= (cpuid_flags_t)CPUID_INTEL; + wb_intr_ret = 0; + wb_intr_ix = 0; + wb_intr_mod = periods[p][0]; + wb_intr_res = periods[p][1]; + + for (t = 0; t < sizeof(wb_dsa_levels) / sizeof(wb_dsa_levels[0]); + t++) { + wb_dsa_cycle(&rng, wb_dsa_levels[t]); + } + } + wb_intr_mod = 0; + wb_intr_res = 0; + } + + /* mldsa_expand_a()'s AVX2 arms are guarded by (k == N) && (l == N) pairs. + * No real parameter set has k == 4 with l != 4, so the second operand of + * each pair has no false side along the key paths; the function is + * file-static, so it is called here with the mismatched shapes directly. + * Every mismatch falls through to mldsa_expand_a_c(), which fills k*l + * polynomials of the buffer and needs no SIMD arm at all. */ +#if !defined(WOLFSSL_NO_ML_DSA_44) && !defined(WOLFSSL_NO_ML_DSA_65) && \ + !defined(WOLFSSL_NO_ML_DSA_87) + { + static const byte shapes[][2] = { { 4, 5 }, { 6, 4 }, { 8, 5 } }; + wc_Shake shake128; + unsigned p; + + /* AVX512 cleared so the k/l-guarded AVX2 arms are the ones evaluated; + * AVX2 and BMI2 are left as the host reported them. */ + cpuid_flags = WC_CPUID_INITIALIZER; + (void)cpuid_get_flags_ex(&cpuid_flags); + cpuid_flags |= (cpuid_flags_t)CPUID_INTEL; + cpuid_flags &= (cpuid_flags_t)~(CPUID_AVX512 | CPUID_AVX512_BW | + CPUID_AVX512_VBMI); + wb_intr_ret = 0; + + if (wc_InitShake128(&shake128, NULL, INVALID_DEVID) == 0) { + for (p = 0; p < sizeof(shapes) / sizeof(shapes[0]); p++) { + (void)mldsa_expand_a(&shake128, wb_dsa_seed, shapes[p][0], + shapes[p][1], wb_lane_a, NULL); + } + wc_Shake128_Free(&shake128); + } + } +#endif + cpuid_flags = saved_flags; wb_intr_ret = saved_intr; + wb_intr_mod = 0; wc_FreeRng(&rng); printf(" [wb] SIMD dispatch rows (cpuid x save-accepted) exercised\n"); } @@ -847,6 +1051,256 @@ static void wb_dispatch_rows(void) } #endif +/* ------------------------------------------------------------------------- * + * Rejection-sampling lane rows. + * + * Six generators carry the "did any lane come up short" loops (and, in the + * gen_s pair, the matching `if` that decides whether to squeeze another + * block). They are file-static, and this TU #includes wc_mldsa.c, so they can + * be driven directly with their own buffers instead of through a key + * operation -- which also means one pass costs one matrix expansion rather + * than a whole ML-DSA key generation. + * + * Pass n reports sampler call n one coefficient short, so the group that call + * belongs to yields exactly the (F..F,T,-..) vector for that lane; sweeping n + * across more calls than the widest generator makes reaches every lane of + * every group, including the two- and three-lane tails. The unswept pass + * supplies the all-false vector every operand needs as its partner. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(USE_INTEL_SPEEDUP) && \ + !defined(WC_SHA3_NO_ASM) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ + !defined(WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM) && \ + !defined(WOLFSSL_NO_ML_DSA_44) && !defined(WOLFSSL_NO_ML_DSA_65) && \ + !defined(WOLFSSL_NO_ML_DSA_87) + +/* 6x5 makes 28 grouped calls plus a two-lane tail, so 32 positions reach every + * lane of every group in every one of the six generators. */ +#define WB_LANE_SWEEP 32 + +static void wb_gen_lane_rows(void) +{ + sword32* s[2]; + long n; + + XMEMSET(wb_lane_seed, 0x71, sizeof(wb_lane_seed)); + XMEMSET(wb_lane_a, 0, sizeof(wb_lane_a)); + XMEMSET(wb_lane_s1, 0, sizeof(wb_lane_s1)); + XMEMSET(wb_lane_s2, 0, sizeof(wb_lane_s2)); + s[0] = wb_lane_s1; + s[1] = wb_lane_s2; + + for (n = -1; n < (long)WB_LANE_SWEEP; n++) { + wb_lane_at = n; + + wb_lane_ix = 0; + (void)wc_mldsa_gen_matrix_4x4_avx2(wb_lane_a, wb_lane_seed); + wb_lane_ix = 0; + (void)wc_mldsa_gen_matrix_6x5_avx2(wb_lane_a, wb_lane_seed); + wb_lane_ix = 0; + (void)wc_mldsa_gen_matrix_8x7_avx2(wb_lane_a, wb_lane_seed); + + wb_lane_ix = 0; + (void)wc_mldsa_gen_s_4_4_avx2(s, wb_lane_seed); + wb_lane_ix = 0; + (void)wc_mldsa_gen_s_5_6_avx2(s, wb_lane_seed); + wb_lane_ix = 0; + (void)wc_mldsa_gen_s_7_8_avx2(s, wb_lane_seed); + } + + wb_lane_at = -1; + wb_lane_ix = 0; + printf(" [wb] rejection-sampling short-lane rows exercised\n"); +} + +#else +static void wb_gen_lane_rows(void) +{ + printf(" [wb] no AVX2 generators in this variant; lane rows skipped\n"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Argument guards on file-static entry points. + * + * mldsa_verify_ctx_msg() / mldsa_verify_ctx_hash() open with + * + * if ((key == NULL) || (key->params == NULL)) ... + * + * but every public caller has already rejected a NULL key and a key without + * params, so along the API only the (F,F) row is ever seen. Both are file + * static and this TU #includes wc_mldsa.c, so they take the two rejecting rows + * directly: a NULL key for operand 0, and a zeroed key object (params NULL, + * never dereferenced past the guard) for operand 1. + * + * mldsa_get_hash_oid()'s first operand is the "is this hash algorithm known" + * result; an unrecognised algorithm gives it the NULL side that no caller with + * a validated hash id can produce. Its second operand -- the OID fitting in + * MLDSA_HASH_OID_LEN -- is a property of the fixed OID table, not of any + * argument, so it has no false side to drive and stays a residual. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_MLDSA) +static void wb_arg_guards(void) +{ +#if !defined(WOLFSSL_MLDSA_NO_VERIFY) && !defined(WOLFSSL_MLDSA_NO_CTX) + { + wc_MlDsaKey noParams; + byte msg[8]; + byte sig[8]; + int res = 0; + + XMEMSET(&noParams, 0, sizeof(noParams)); /* params == NULL */ + XMEMSET(msg, 0x11, sizeof(msg)); + XMEMSET(sig, 0x22, sizeof(sig)); + + /* operand 0 true: key == NULL (short-circuits before any deref). */ + (void)mldsa_verify_ctx_msg(NULL, NULL, 0, msg, (word32)sizeof(msg), + sig, (word32)sizeof(sig), &res); + /* operand 0 false, operand 1 true: key non-NULL, params NULL. */ + (void)mldsa_verify_ctx_msg(&noParams, NULL, 0, msg, + (word32)sizeof(msg), sig, (word32)sizeof(sig), &res); + + (void)mldsa_verify_ctx_hash(NULL, NULL, 0, WC_HASH_TYPE_SHA256, msg, + (word32)sizeof(msg), sig, (word32)sizeof(sig), &res); + (void)mldsa_verify_ctx_hash(&noParams, NULL, 0, WC_HASH_TYPE_SHA256, + msg, (word32)sizeof(msg), sig, (word32)sizeof(sig), &res); + } +#endif + + { + byte oidBuf[64]; + word32 oidLen = 0; + + XMEMSET(oidBuf, 0, sizeof(oidBuf)); + /* Known algorithm: the (T,T) row. */ + oidLen = 0; + (void)mldsa_get_hash_oid(WC_HASH_TYPE_SHA256, oidBuf, &oidLen); + /* Unknown algorithm: no OID -> operand 0 false. */ + oidLen = 0; + (void)mldsa_get_hash_oid(WC_HASH_TYPE_NONE, oidBuf, &oidLen); + oidLen = 0; + (void)mldsa_get_hash_oid(-1, oidBuf, &oidLen); + } + + printf(" [wb] file-static argument-guard rows exercised\n"); +} +#else +static void wb_arg_guards(void) +{ +} +#endif + +/* ------------------------------------------------------------------------- * + * Verification failure rows. + * + * mldsa_verify_with_mu() threads a `valid` flag through a dozen + * + * if ((ret == 0) && valid) { ... } + * + * guards: `valid` goes false when a decoded hint is malformed, a norm check + * fails, or the recomputed commitment differs. A campaign that only ever + * verifies signatures it just produced sees valid == 1 at every one of them, + * so the operand is undriven -- and a genuinely corrupt signature is the + * ordinary, in-spec way to drive it. + * + * Single-bit flips are spread across the signature so different structural + * parts (c-tilde, the z vector, the hint block, and its trailing padding) + * fail, each tripping a different guard. The valid signature is verified too + * so every guard also has its true row in this binary; both rows are what the + * independence pair needs. + * + * mldsa_verify_ctx_hash() gets its (F,F) row here as well: the key is real and + * carries params, which is the one row a NULL/param-less key cannot show. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_SIGN) && \ + !defined(WOLFSSL_MLDSA_NO_VERIFY) && !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) +static void wb_verify_invalid(void) +{ + static byte sig[MLDSA_MAX_SIG_SIZE]; + static byte bad[MLDSA_MAX_SIG_SIZE]; + wc_MlDsaKey key; + byte msg[32]; + byte hash[32]; + byte seed[MLDSA_SEED_SZ]; + word32 sigLen = (word32)sizeof(sig); + int res = 0; + unsigned i; +#ifndef WOLFSSL_NO_ML_DSA_44 + const int level = WC_ML_DSA_44; /* smallest set: fastest under cov */ +#elif !defined(WOLFSSL_NO_ML_DSA_65) + const int level = WC_ML_DSA_65; +#else + const int level = WC_ML_DSA_87; +#endif + + XMEMSET(msg, 0x5a, sizeof(msg)); + XMEMSET(hash, 0x3e, sizeof(hash)); + XMEMSET(seed, 0x27, sizeof(seed)); + + if (wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID) != 0) { + return; + } + if ((wc_MlDsaKey_SetParams(&key, level) != 0) || + (wc_MlDsaKey_MakeKeyFromSeed(&key, seed) != 0) || + (wc_MlDsaKey_SignCtxWithSeed(&key, NULL, 0, sig, &sigLen, msg, + (word32)sizeof(msg), seed) != 0)) { + wc_MlDsaKey_Free(&key); + WB_NOTE("verification-failure rows skipped (sign unavailable)"); + return; + } + + /* True row for every guard. */ + (void)wc_MlDsaKey_VerifyCtx(&key, sig, sigLen, NULL, 0, msg, + (word32)sizeof(msg), &res); + + /* 24 flip positions spread across the whole signature: the leading + * c-tilde, the packed z vector and the trailing hint block all decode + * differently, so the failure surfaces at different guards. */ + for (i = 0; i < 24; i++) { + word32 off = (word32)(((word64)i * sigLen) / 24U); + + XMEMCPY(bad, sig, sigLen); + bad[off] = (byte)(bad[off] ^ 0x80); + res = 0; + (void)wc_MlDsaKey_VerifyCtx(&key, bad, sigLen, NULL, 0, msg, + (word32)sizeof(msg), &res); + } + + /* All-ones and all-zeros signatures: maximally malformed hint blocks. */ + XMEMSET(bad, 0xFF, sigLen); + res = 0; + (void)wc_MlDsaKey_VerifyCtx(&key, bad, sigLen, NULL, 0, msg, + (word32)sizeof(msg), &res); + XMEMSET(bad, 0x00, sigLen); + res = 0; + (void)wc_MlDsaKey_VerifyCtx(&key, bad, sigLen, NULL, 0, msg, + (word32)sizeof(msg), &res); + + /* A different message against the good signature: everything decodes, the + * recomputed commitment is what differs. */ + msg[0] ^= 0xFF; + res = 0; + (void)wc_MlDsaKey_VerifyCtx(&key, sig, sigLen, NULL, 0, msg, + (word32)sizeof(msg), &res); + msg[0] ^= 0xFF; + +#if !defined(WOLFSSL_MLDSA_NO_CTX) + /* mldsa_verify_ctx_hash() with a real, params-carrying key: the (F,F) row + * of its (key == NULL) || (key->params == NULL) guard. */ + res = 0; + (void)mldsa_verify_ctx_hash(&key, NULL, 0, WC_HASH_TYPE_SHA256, hash, + (word32)sizeof(hash), sig, sigLen, &res); +#endif + + wc_MlDsaKey_Free(&key); + printf(" [wb] verification-failure (valid == 0) rows exercised\n"); +} +#else +static void wb_verify_invalid(void) +{ +} +#endif + int main(void) { printf("wc_mldsa.c white-box MC/DC supplement\n"); @@ -882,6 +1336,9 @@ int main(void) wb_oid_to_level(); #endif wb_dispatch_rows(); + wb_gen_lane_rows(); + wb_arg_guards(); + wb_verify_invalid(); printf("done (%d note%s)\n", wb_notes, (wb_notes == 1) ? "" : "s"); return 0; #endif diff --git a/tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c b/tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c index 1b0d19d338d..bdf4d904e79 100644 --- a/tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c +++ b/tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c @@ -68,11 +68,198 @@ * fail_clause) form, whose expansion also changes under this hook, appears * nowhere here. */ -static int wb_intr_ret = 0; -#define WC_CHECK_FOR_INTR_SIGNALS() (wb_intr_ret) +/* The hook is a function rather than a plain variable so a single pass can put + * the save-refused answer at a CHOSEN call index instead of only "always" or + * "never". Two dispatch guards nest inside one another: + * + * mlkem_gen_matrix() if (IS_INTEL_AVX2(..) && save == 0) + * mlkem_gen_matrix_k3_avx2() for (..) { if (IS_INTEL_BMI2(..)) + * else if (IS_INTEL_AVX2(..) + * && save == 0) + * + * so the inner guard's operands can only be reached when the OUTER one was + * already satisfied. A process-wide "always refuse" therefore never lets the + * inner site run at all, and its false side would stay unreachable. Letting + * call 0..n-1 succeed and calls >= n refuse gives the inner site a genuine + * (T,F) row while the outer one still took (T,T). + * + * wb_intr_action is the same idea for the cpuid operand of the inner guard: + * the action runs after IS_INTEL_AVX2() has already been evaluated for THIS + * decision, so clearing CPUID_AVX2 from wc_mlkem_poly.c's own dispatch word + * inside the hook leaves the current iteration on its (T,T) row and flips the + * NEXT iteration of the same loop to (F,-). Both rows land in one binary, at + * one source line, without the outer dispatch ever changing its mind. */ +static int wb_intr_ret = 0; /* != 0: refuse every save (blanket) */ +static long wb_intr_count = 0; /* SAVE_VECTOR_REGISTERS2() calls seen */ +static long wb_intr_fail_from = -1; /* >= 0: refuse from this call index on */ +static void (*wb_intr_action)(long ix) = 0; /* runs on every call */ + +static int wb_intr_hook(void) +{ + long ix = wb_intr_count++; + + if (wb_intr_action != 0) { + wb_intr_action(ix); + } + if (wb_intr_ret != 0) { + return wb_intr_ret; + } + if ((wb_intr_fail_from >= 0) && (ix >= wb_intr_fail_from)) { + return 1; + } + return 0; +} +#define WC_CHECK_FOR_INTR_SIGNALS() wb_intr_hook() + +/* ------------------------------------------------------------------------- * + * Rejection-sampling lane interposition. + * + * Nine decisions in this file have the shape + * + * ctr[i] = mlkem_rej_uniform_n_ins(...); (i = 0..3 or 0..7) + * while ((ctr[0] < MLKEM_N) || (ctr[1] < MLKEM_N) || ... ) { ... } + * + * The loop only runs when a lane came up short, and each operand only gets an + * independence pair when THAT lane is the first short one. Uniform random + * seeds fill all lanes on the first block with overwhelming probability, so in + * practice the vector is always (F,F,..,F): the operands are undriven, and + * which of them a nightly run happens to catch is pure luck -- the origin of + * this module's 68 -> 74 -> 69 gate flapping. + * + * mlkem_rej_uniform_n_ins()/mlkem_rej_uniform_ins() are static WC_INLINE (or + * plain #defines) inside the .c, so a macro on THEM would rename the library's + * own definition rather than wrap it. The leaf samplers they dispatch to are + * WOLFSSL_LOCAL functions declared in wc_mlkem.h, so declaring that header + * FIRST keeps the real prototypes under their real names, and the renames + * below then only rewrite the uses inside wc_mlkem_poly.c. + * + * Only the FIRST-round samplers (the _n_ forms) are wrapped. The loop body + * calls the non-_n_ forms to top the short lane up, and those are left alone + * so every loop provably terminates. */ +#include +#include + +static unsigned int wb_rej_trim(unsigned int got, unsigned int len); + +#if defined(USE_INTEL_SPEEDUP) && !defined(WC_SHA3_NO_ASM) + +static unsigned int wb_rej_n_avx2(sword16* p, unsigned int len, const byte* r, + unsigned int rLen); +#define mlkem_rej_uniform_n_avx2 wb_rej_n_avx2 + +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512 +static unsigned int wb_rej_n_avx512(sword16* p, unsigned int len, + const byte* r, unsigned int rLen); +#define mlkem_rej_uniform_n_avx512 wb_rej_n_avx512 +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512_VBMI2 +static unsigned int wb_rej_n_avx512_vbmi2(sword16* p, unsigned int len, + const byte* r, unsigned int rLen); +#define mlkem_rej_uniform_n_avx512_vbmi2 wb_rej_n_avx512_vbmi2 +#endif +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512_VBMI +static unsigned int wb_rej_n_avx512_vbmi(sword16* p, unsigned int len, + const byte* r, unsigned int rLen); +#define mlkem_rej_uniform_n_avx512_vbmi wb_rej_n_avx512_vbmi +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512_VBMI2 +static unsigned int wb_rej_n_avx512_vbmi_vbmi2(sword16* p, unsigned int len, + const byte* r, unsigned int rLen); +#define mlkem_rej_uniform_n_avx512_vbmi_vbmi2 wb_rej_n_avx512_vbmi_vbmi2 +#endif +#endif +#endif /* WOLFSSL_MLKEM_HAVE_INTEL_AVX512 */ + +#endif /* USE_INTEL_SPEEDUP && !WC_SHA3_NO_ASM */ #include +#if defined(USE_INTEL_SPEEDUP) && !defined(WC_SHA3_NO_ASM) +#undef mlkem_rej_uniform_n_avx2 +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512 +#undef mlkem_rej_uniform_n_avx512 +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512_VBMI2 +#undef mlkem_rej_uniform_n_avx512_vbmi2 +#endif +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512_VBMI +#undef mlkem_rej_uniform_n_avx512_vbmi +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512_VBMI2 +#undef mlkem_rej_uniform_n_avx512_vbmi_vbmi2 +#endif +#endif +#endif +#endif + +/* Index of the first-round sampler call within the current pass, and the lane + * (call index mod 8) whose result is reported one sample short. MLKEM_N-1 of + * MLKEM_N samples are genuinely present; the loop's own top-up call fills the + * last one, so the matrix that comes out is still a valid uniform matrix -- the + * only thing that changed is that the code took the "some lane was short" path + * it takes in the field roughly once every few thousand key generations. */ +static unsigned int wb_rej_ix = 0; +static int wb_rej_lane = -1; +/* Absolute-index form: short exactly ONE sampler call in the whole pass. The + * mod-8 form assumes each group of samplers starts at an index that is a + * multiple of 8; the absolute form does not, so sweeping it over the number of + * polynomials in the largest matrix reaches every lane of every group whatever + * the grouping turns out to be. */ +static long wb_rej_abs = -1; + +static unsigned int wb_rej_trim(unsigned int got, unsigned int len) +{ + long ix = (long)wb_rej_ix++; + int hit = 0; + + if ((wb_rej_lane >= 0) && ((ix & 7L) == (long)wb_rej_lane)) { + hit = 1; + } + if ((wb_rej_abs >= 0) && (ix == wb_rej_abs)) { + hit = 1; + } + if (hit && (got == len) && (len > 0)) { + got = len - 1; + } + return got; +} + +#if defined(USE_INTEL_SPEEDUP) && !defined(WC_SHA3_NO_ASM) + +static unsigned int wb_rej_n_avx2(sword16* p, unsigned int len, const byte* r, + unsigned int rLen) +{ + return wb_rej_trim(mlkem_rej_uniform_n_avx2(p, len, r, rLen), len); +} + +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512 +static unsigned int wb_rej_n_avx512(sword16* p, unsigned int len, + const byte* r, unsigned int rLen) +{ + return wb_rej_trim(mlkem_rej_uniform_n_avx512(p, len, r, rLen), len); +} +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512_VBMI2 +static unsigned int wb_rej_n_avx512_vbmi2(sword16* p, unsigned int len, + const byte* r, unsigned int rLen) +{ + return wb_rej_trim(mlkem_rej_uniform_n_avx512_vbmi2(p, len, r, rLen), len); +} +#endif +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512_VBMI +static unsigned int wb_rej_n_avx512_vbmi(sword16* p, unsigned int len, + const byte* r, unsigned int rLen) +{ + return wb_rej_trim(mlkem_rej_uniform_n_avx512_vbmi(p, len, r, rLen), len); +} +#ifdef WOLFSSL_MLKEM_HAVE_INTEL_AVX512_VBMI2 +static unsigned int wb_rej_n_avx512_vbmi_vbmi2(sword16* p, unsigned int len, + const byte* r, unsigned int rLen) +{ + return wb_rej_trim( + mlkem_rej_uniform_n_avx512_vbmi_vbmi2(p, len, r, rLen), len); +} +#endif +#endif +#endif /* WOLFSSL_MLKEM_HAVE_INTEL_AVX512 */ + +#endif /* USE_INTEL_SPEEDUP && !WC_SHA3_NO_ASM */ + #include static int wb_fail = 0; @@ -313,6 +500,10 @@ static void wb_get_noise_c_vec2_null(void) * mlkem_vec_compress_11 or mlkem_compress_5 (du=11,dv=5, the 1024 params) -- * and the matrix generators are specialised per k, so one parameter set alone * leaves most of the SIMD dispatches unexecuted. */ +/* WC_ML_KEM_512 is enum value 0 (wc_mlkem.h), so a 0 terminator here silently + * dropped the entire ML-KEM-512 axis: its k == WC_ML_KEM_512_K arms in + * mlkem_gen_matrix() / mlkem_get_noise() and the k2 matrix generators were + * never entered by any row. The list carries its own length instead. */ static const int wb_kem_types[] = { #ifdef WOLFSSL_WC_ML_KEM_512 WC_ML_KEM_512, @@ -323,8 +514,29 @@ static const int wb_kem_types[] = { #ifdef WOLFSSL_WC_ML_KEM_1024 WC_ML_KEM_1024, #endif - 0 /* sentinel keeps the array non-empty if none are enabled */ +#if !defined(WOLFSSL_WC_ML_KEM_512) && !defined(WOLFSSL_WC_ML_KEM_768) && \ + !defined(WOLFSSL_WC_ML_KEM_1024) + /* wc_mlkem.h forces at least one parameter set on, so this only keeps the + * initialiser well-formed if that ever changes. */ + WC_ML_KEM_512, +#endif }; +#define WB_KEM_TYPE_CNT ((unsigned)(sizeof(wb_kem_types) / sizeof(int))) + +/* One key generation only: the matrix generators and their rejection-sampling + * loops all hang off wc_MlKemKey_MakeKey(), and keeping the per-pass work to a + * single keygen is what lets the lane sweep below afford ~100 passes inside the + * campaign's wall-clock budget. */ +static void wb_run_keygen(WC_RNG* rng, int type) +{ + MlKemKey key; + + if (wc_MlKemKey_Init(&key, type, NULL, INVALID_DEVID) != 0) { + return; + } + (void)wc_MlKemKey_MakeKey(&key, rng); + wc_MlKemKey_Free(&key); +} static void wb_run_cycle(WC_RNG* rng, int type) { @@ -332,11 +544,12 @@ static void wb_run_cycle(WC_RNG* rng, int type) byte ct[WC_ML_KEM_MAX_CIPHER_TEXT_SIZE]; byte ss[WC_ML_KEM_SS_SZ]; byte ss2[WC_ML_KEM_SS_SZ]; + byte pub[WC_ML_KEM_MAX_PUBLIC_KEY_SIZE]; + byte priv[WC_ML_KEM_MAX_PRIVATE_KEY_SIZE]; word32 ctSz = 0; + word32 pubSz = 0; + word32 privSz = 0; - if (type == 0) { - return; - } if (wc_MlKemKey_Init(&key, type, NULL, INVALID_DEVID) != 0) { return; } @@ -347,9 +560,235 @@ static void wb_run_cycle(WC_RNG* rng, int type) (void)wc_MlKemKey_Decapsulate(&key, ss2, ct, ctSz); } } + + /* mlkem_to_bytes() / mlkem_from_bytes() are reached only by the key + * encode/decode entry points, never by keygen/encap/decap, so without this + * round trip their AVX512-VBMI / AVX512 / AVX2 dispatch chain is dead code + * in this binary no matter which cpuid row is installed. */ + if (wc_MlKemKey_PublicKeySize(&key, &pubSz) == 0 && + pubSz <= (word32)sizeof(pub) && + wc_MlKemKey_EncodePublicKey(&key, pub, pubSz) == 0) { + (void)wc_MlKemKey_DecodePublicKey(&key, pub, pubSz); + } + if (wc_MlKemKey_PrivateKeySize(&key, &privSz) == 0 && + privSz <= (word32)sizeof(priv) && + wc_MlKemKey_EncodePrivateKey(&key, priv, privSz) == 0) { + (void)wc_MlKemKey_DecodePrivateKey(&key, priv, privSz); + } + wc_MlKemKey_Free(&key); } +/* Install a cpuid word derived from the host's REAL flags with exactly the + * named features removed, so a "present" row never claims a feature this CPU + * lacks and a "absent" row can only ever select a slower, equally correct + * path. cpuid_flags is wc_mlkem_poly.c's own file-static dispatch word and + * mlkem_init() refreshes it only while it still holds WC_CPUID_INITIALIZER, so + * the value written here stays put for the rest of the pass. */ +static void wb_set_flags(cpuid_flags_t clear) +{ + cpuid_flags = WC_CPUID_INITIALIZER; + (void)cpuid_get_flags_ex(&cpuid_flags); + cpuid_flags &= (cpuid_flags_t)~clear; +} + +/* wb_intr_action for the inner sha3-block dispatch: drop AVX2 from the file's + * dispatch word once the loop has already taken its (T,T) row, so the next + * iteration of the SAME loop evaluates IS_INTEL_AVX2() false. */ +static cpuid_flags_t wb_action_clear = 0; +static long wb_action_at = -1; + +static void wb_clear_flags_at(long ix) +{ + if ((wb_action_at >= 0) && (ix == wb_action_at)) { + cpuid_flags &= (cpuid_flags_t)~wb_action_clear; + } +} + +/* ------------------------------------------------------------------------- * + * Rejection-sampling lane rows. + * + * For an N-way OR, operand j's independence pair needs one evaluation where + * every earlier operand is false and j is true, plus the all-false evaluation. + * Pass s reports the sampler call whose index mod 8 is s one sample short, so + * the group containing that call yields exactly the (F..F,T,-,..) vector for + * lane s; the eight passes together cover every lane of the 8-wide AVX512 + * loops and (twice over) every lane of the 4-wide AVX2 loops. The unmodified + * rows elsewhere in this file supply the all-false vector. + * + * The sweep is repeated per feature row because the 4-wide loops live in the + * AVX2 generators and the 8-wide ones in the AVX512 generators, and because + * mlkem_rej_uniform_ins()'s own VBMI/VBMI2 dispatch is only ever reached from + * inside these loop bodies -- with no short lane it is unexecuted code. + * ------------------------------------------------------------------------- */ +static void wb_rejection_lanes(WC_RNG* rng) +{ + static const cpuid_flags_t famRows[] = { + 0, + CPUID_AVX512_VBMI2, + CPUID_AVX512_VBMI | CPUID_AVX512_VBMI2, + CPUID_AVX512_VBMI | CPUID_AVX512_VBMI2 | CPUID_AVX512 + }; + unsigned f; + int s; + unsigned t; + + for (f = 0; f < sizeof(famRows) / sizeof(famRows[0]); f++) { + for (s = 0; s < 8; s++) { + for (t = 0; t < WB_KEM_TYPE_CNT; t++) { + wb_set_flags(famRows[f]); + /* Reset per keygen so call index 0 is the first lane of the + * first group; the lane-to-index mapping depends on it. */ + wb_rej_ix = 0; + wb_rej_lane = s; + wb_run_keygen(rng, wb_kem_types[t]); + } + } + } + wb_rej_lane = -1; + + for (f = 0; f < sizeof(famRows) / sizeof(famRows[0]); f++) { + for (s = 0; s < 16; s++) { + for (t = 0; t < WB_KEM_TYPE_CNT; t++) { + wb_set_flags(famRows[f]); + wb_rej_ix = 0; + wb_rej_abs = s; + wb_run_keygen(rng, wb_kem_types[t]); + } + } + } + + wb_rej_abs = -1; + wb_rej_ix = 0; + WB_NOTE("rejection-sampling short-lane rows exercised"); +} + +/* ------------------------------------------------------------------------- * + * sha3-block dispatch rows. + * + * Inside each AVX2/AVX512 matrix generator every SHA3 block is squeezed with + * + * if (IS_INTEL_BMI2(cpuid_flags)) sha3_block_bmi2() + * else if (IS_INTEL_AVX2(cpuid_flags) && (SAVE_VECTOR_REGISTERS2() == 0)) + * else BlockSha3() + * + * Every x86-64 CPU that has AVX2 also has BMI2, so the first arm always wins + * and the second is never evaluated: clearing CPUID_BMI2 is the only way to + * reach it at all. Its two operands then need rows the OUTER dispatch would + * normally forbid (see the wb_intr_hook comment at the top of this file): + * wb_action_at flips AVX2 off between two iterations of the same loop, and + * wb_intr_fail_from refuses the save only from a later call index, both after + * the enclosing generator has already been entered on its (T,T) row. + * ------------------------------------------------------------------------- */ +static void wb_sha3_block_rows(WC_RNG* rng) +{ + /* AVX512 generators first (BMI2 cleared only), then AVX2 generators + * (BMI2 + the AVX512 ladder cleared): the same three source sites exist in + * both families. */ + static const cpuid_flags_t famRows[] = { + CPUID_BMI2, + CPUID_BMI2 | CPUID_AVX512_VBMI | CPUID_AVX512_VBMI2 | CPUID_AVX512 + }; + unsigned f; + unsigned t; + long at; + + /* The k3 generators squeeze their ninth polynomial in a tail loop that only + * runs when that lane came up short, and it carries its own copy of the + * same three-way sha3 dispatch. Shorting lane 0 (the tail sampler is call + * index 8, and 8 mod 8 == 0) makes the tail loop execute in every pass + * below, so the BMI2-cleared rows reach that copy too. */ + wb_rej_lane = 0; + + for (f = 0; f < sizeof(famRows) / sizeof(famRows[0]); f++) { + for (t = 0; t < WB_KEM_TYPE_CNT; t++) { + /* (T,T): BMI2 absent, AVX2 present, save accepted. */ + wb_set_flags(famRows[f]); + wb_rej_ix = 0; + wb_run_keygen(rng, wb_kem_types[t]); + + /* (T,F): the enclosing generator is entered on the very first + * save, then every later save is refused, so the inner site sees + * AVX2 true and the save refused. */ + for (at = 1; at <= 3; at++) { + wb_set_flags(famRows[f]); + wb_intr_count = 0; + wb_rej_ix = 0; + wb_intr_fail_from = at; + wb_run_keygen(rng, wb_kem_types[t]); + wb_intr_fail_from = -1; + } + + /* (F,-): AVX2 is dropped after the site has run once, so the next + * iteration of the same loop takes the portable BlockSha3 arm. */ + for (at = 0; at <= 3; at++) { + wb_set_flags(famRows[f]); + wb_intr_count = 0; + wb_rej_ix = 0; + wb_action_clear = CPUID_AVX2; + wb_action_at = at; + wb_intr_action = wb_clear_flags_at; + wb_run_keygen(rng, wb_kem_types[t]); + wb_intr_action = NULL; + wb_action_at = -1; + wb_action_clear = 0; + } + } + } + + wb_rej_lane = -1; + wb_rej_ix = 0; + WB_NOTE("sha3-block (BMI2 / AVX2 / portable) rows exercised"); +} + +/* ------------------------------------------------------------------------- * + * The same three-way sha3-block dispatch also sits in two leaf helpers that + * the AVX2/AVX512 key paths never call: mlkem_prf() (the SHAKE-256 PRF used by + * the portable noise generator) and mlkem_get_noise_eta2_avx2(). Reaching + * them through the public API needs the outer dispatch to have already chosen + * a path that skips them, so they are called directly here -- with no outer + * guard in the way, a blanket save-refused setting is enough for the second + * operand's false side. */ +static void wb_leaf_sha3_rows(void) +{ + static const struct { + cpuid_flags_t clear; + int intr; + } rows[] = { + { CPUID_BMI2, 0 }, /* BMI2 F, AVX2 T, save accepted */ + { CPUID_BMI2, 1 }, /* BMI2 F, AVX2 T, save refused */ + { CPUID_BMI2 | CPUID_AVX2, 0 } /* BMI2 F, AVX2 F -> BlockSha3 */ + }; + MLKEM_PRF_T prf; + byte key[WC_ML_KEM_SYM_SZ + 1]; + byte out[3 * WC_SHA3_256_BLOCK_SIZE]; + unsigned i; +#if defined(WOLFSSL_KYBER512) || defined(WOLFSSL_WC_ML_KEM_512) || \ + defined(WOLFSSL_KYBER1024) || defined(WOLFSSL_WC_ML_KEM_1024) + sword16 p[MLKEM_N]; +#endif + + XMEMSET(key, 0x5a, sizeof(key)); + mlkem_prf_init(&prf); + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + wb_set_flags(rows[i].clear); + wb_intr_ret = rows[i].intr; + + /* Several output blocks so the dispatch is evaluated more than once. */ + (void)mlkem_prf(&prf, out, (unsigned int)sizeof(out), key); +#if defined(WOLFSSL_KYBER512) || defined(WOLFSSL_WC_ML_KEM_512) || \ + defined(WOLFSSL_KYBER1024) || defined(WOLFSSL_WC_ML_KEM_1024) + XMEMSET(p, 0, sizeof(p)); + (void)mlkem_get_noise_eta2_avx2(&prf, p, key); +#endif + } + + wb_intr_ret = 0; + mlkem_prf_free(&prf); + WB_NOTE("leaf sha3-block dispatch (mlkem_prf / eta2) rows exercised"); +} + static void wb_dispatch_rows(void) { cpuid_flags_t saved_flags = cpuid_flags; @@ -378,6 +817,11 @@ static void wb_dispatch_rows(void) "all features, save accepted -> richest arm" }, { 0, 1, "all features, save refused -> operand 1 false at every level" }, + /* mlkem_rej_uniform_n_ins()/_ins() test VBMI and VBMI2 as two operands + * of one decision, so a row that drops both together can never give + * the second one its pair. */ + { CPUID_AVX512_VBMI2, 0, + "VBMI without VBMI2 -> vpcompressd sampler" }, { CPUID_AVX512_VBMI | CPUID_AVX512_VBMI2, 0, "no VBMI -> plain AVX512 arm" }, /* USE_INTEL_AVX512() is IS_INTEL_AVX512() && IS_INTEL_AVX512_BW() @@ -401,18 +845,23 @@ static void wb_dispatch_rows(void) for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { /* Start from the host's real flags so the "present" rows claim only * what this CPU actually has. */ - cpuid_flags = WC_CPUID_INITIALIZER; - (void)cpuid_get_flags_ex(&cpuid_flags); - cpuid_flags &= (cpuid_flags_t)~rows[i].clear; + wb_set_flags(rows[i].clear); wb_intr_ret = rows[i].intr; - for (t = 0; t < sizeof(wb_kem_types) / sizeof(wb_kem_types[0]); t++) { + for (t = 0; t < WB_KEM_TYPE_CNT; t++) { wb_run_cycle(&rng, wb_kem_types[t]); } } + wb_intr_ret = 0; + + wb_rejection_lanes(&rng); + wb_sha3_block_rows(&rng); + wb_leaf_sha3_rows(); cpuid_flags = saved_flags; wb_intr_ret = saved_intr; + wb_intr_fail_from = -1; + wb_intr_action = NULL; wc_FreeRng(&rng); WB_NOTE("SIMD dispatch rows (cpuid x save-accepted) exercised"); } From a6542e70d1dcc7b3716a67cec07a18ed3bfc30f6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 17:32:41 +0200 Subject: [PATCH 10/20] tests: close argument and allocation guards across the small modules --- tests/include.am | 12 +- tests/unit-mcdc/test_aes_whitebox.c | 284 ++++++++++++++++++++ tests/unit-mcdc/test_cryptocb_whitebox.c | 43 ++- tests/unit-mcdc/test_kdf_whitebox.c | 250 +++++++++++++++++ tests/unit-mcdc/test_poly1305_whitebox.c | 73 +++++ tests/unit-mcdc/test_random_whitebox.c | 190 +++++++++++++ tests/unit-mcdc/test_rsa_fault_whitebox.c | 84 ++++++ tests/unit-mcdc/test_rsa_whitebox.c | 118 ++++++++ tests/unit-mcdc/test_she_whitebox.c | 166 ++++++++++++ tests/unit-mcdc/test_wc_port_whitebox.c | 123 +++++++++ tests/unit-mcdc/test_wolfentropy_whitebox.c | 63 +++++ 11 files changed, 1404 insertions(+), 2 deletions(-) create mode 100644 tests/unit-mcdc/test_kdf_whitebox.c create mode 100644 tests/unit-mcdc/test_she_whitebox.c diff --git a/tests/include.am b/tests/include.am index b6d6f77f6ed..afd0cbc565b 100644 --- a/tests/include.am +++ b/tests/include.am @@ -121,6 +121,8 @@ DISTCLEANFILES+= tests/.libs/unit.test EXTRA_DIST += \ tests/unit-mcdc/README.md \ tests/unit-mcdc/mcdc_fault_alloc.h \ + tests/unit-mcdc/mcdc_fault_hash.h \ + tests/unit-mcdc/mcdc_fault_mp.h \ tests/unit-mcdc/mcdc_fault_mutex.h \ tests/unit-mcdc/test_aes_whitebox.c \ tests/unit-mcdc/test_asn_cert_whitebox.c \ @@ -148,11 +150,14 @@ EXTRA_DIST += \ tests/unit-mcdc/test_frodokem_fault_common.h \ tests/unit-mcdc/test_frodokem_fault_whitebox.c \ tests/unit-mcdc/test_frodokem_mat_fault_whitebox.c \ + tests/unit-mcdc/test_frodokem_mat_hash_fault_whitebox.c \ tests/unit-mcdc/test_hpke_fault_whitebox.c \ tests/unit-mcdc/test_hpke_whitebox.c \ tests/unit-mcdc/test_integer_fault_whitebox.c \ tests/unit-mcdc/test_integer_whitebox.c \ + tests/unit-mcdc/test_kdf_whitebox.c \ tests/unit-mcdc/test_lms_fault_whitebox.c \ + tests/unit-mcdc/test_lms_hash_fault_whitebox.c \ tests/unit-mcdc/test_logging_globalq_whitebox.c \ tests/unit-mcdc/test_logging_whitebox.c \ tests/unit-mcdc/test_memory_whitebox.c \ @@ -161,8 +166,10 @@ EXTRA_DIST += \ tests/unit-mcdc/test_pkcs12_fault_whitebox.c \ tests/unit-mcdc/test_pkcs12_parse_whitebox.c \ tests/unit-mcdc/test_pkcs12_whitebox.c \ + tests/unit-mcdc/test_pkcs7_arg_whitebox.c \ tests/unit-mcdc/test_pkcs7_decode_whitebox.c \ tests/unit-mcdc/test_pkcs7_fault_whitebox.c \ + tests/unit-mcdc/test_pkcs7_mutate_whitebox.c \ tests/unit-mcdc/test_pkcs7_whitebox.c \ tests/unit-mcdc/test_poly1305_whitebox.c \ tests/unit-mcdc/test_puf_whitebox.c \ @@ -174,6 +181,8 @@ EXTRA_DIST += \ tests/unit-mcdc/test_sha256_whitebox.c \ tests/unit-mcdc/test_sha3_whitebox.c \ tests/unit-mcdc/test_sha512_whitebox.c \ + tests/unit-mcdc/test_she_whitebox.c \ + tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c \ tests/unit-mcdc/test_slhdsa_whitebox.c \ tests/unit-mcdc/test_sp_arm32_whitebox.c \ tests/unit-mcdc/test_sp_arm64_whitebox.c \ @@ -199,4 +208,5 @@ EXTRA_DIST += \ tests/unit-mcdc/test_wc_port_whitebox.c \ tests/unit-mcdc/test_wc_xmss_impl_whitebox.c \ tests/unit-mcdc/test_wolfentropy_whitebox.c \ - tests/unit-mcdc/test_xmss_fault_whitebox.c + tests/unit-mcdc/test_xmss_fault_whitebox.c \ + tests/unit-mcdc/test_xmss_hash_fault_whitebox.c diff --git a/tests/unit-mcdc/test_aes_whitebox.c b/tests/unit-mcdc/test_aes_whitebox.c index 73498ed4eb0..67bcffdc37e 100644 --- a/tests/unit-mcdc/test_aes_whitebox.c +++ b/tests/unit-mcdc/test_aes_whitebox.c @@ -297,6 +297,26 @@ static void wb_aesni(void) (void)AES_set_decrypt_key_AESNI(key, 128, NULL); /* !userKey F, !aes T */ } + { /* The all-FALSE half of the same two guards. MC/DC is per binary, so + * the rejections above prove nothing without a valid expansion in + * THIS binary -- and the only public caller that reaches + * AES_set_decrypt_key_AESNI (wc_AesSetKey with AES_DECRYPTION) lives + * in unit.test, a different binary. A wc_AesInit'd Aes gives the + * ALIGN16 key schedule the AES-NI aligned stores require. */ + Aes aes; + byte key[16]; + XMEMSET(key, 0x1d, sizeof(key)); + if (wc_AesInit(&aes, NULL, INVALID_DEVID) == 0) { + (void)AES_set_encrypt_key_AESNI(key, 128, &aes); + (void)AES_set_decrypt_key_AESNI(key, 128, &aes); + wc_AesFree(&aes); + } + else { + WB_NOTE("wc_AesInit failed; AES-NI key-expansion all-false skipped"); + wb_fail = 1; + } + } + #if defined(HAVE_AESGCM) && defined(WOLFSSL_AESGCM_STREAM) { /* AES-NI GCM streaming ptr guards; both halves within this binary */ Aes aes; @@ -538,8 +558,268 @@ static void wb_aarch64_gcm_ptr_guards(void) { WB_NOTE("aarch64 GCM streaming ptr guards not compiled in this variant; skipped"); } #endif +/* ------------------------------------------------------------------------- * + * Class 5: wc_AesSetKey() userKey guard (line ~5027) and wc_AesGcmInit()'s + * iv/ivSz cross-check (line ~14321, operand idx5). + * + * 5027: if ((aes == NULL) || (userKey == NULL)) + * 14321: if ((aes == NULL) || ((len > 0) && (key == NULL)) || + * + * BUILD-AXIS GUARD (learned the hard way): aes.c defines TEN different + * wc_AesSetKey() bodies in one #if/#elif chain and they do NOT share this + * guard. The one at ~5027 belongs to the ARM32-ARMASM arm + * ("#elif !defined(__aarch64__) && defined(WOLFSSL_ARMASM)"); the generic + * host arm at ~6046 checks only "aes == NULL" and hands userKey straight to + * wc_AesSetKeyLocal()'s XMEMCPY. Passing userKey == NULL on a host build + * therefore segfaults instead of being rejected, so the vector is compiled + * only where the guard that catches it is. + * ((ivSz == 0) && (iv != NULL)) || ((ivSz > 0) && (iv == NULL))) + * + * Both are public entry points, but the AES API groups only reach them with a + * real key and with iv/ivSz always consistent, so wc_AesSetKey's userKey + * operand and wc_AesGcmInit's "ivSz > 0" operand never get their independence + * pair. Every rejected call short-circuits before the pointer is read. + * + * wc_AesGcmInit idx5 pair, both with aes/key valid so the earlier groups are + * false: + * iv == NULL, ivSz == 0 -> idx3 T, idx4 F (group false), idx5 F -> accept + * iv == NULL, ivSz > 0 -> idx3 F (group false), idx5 T, idx6 T -> reject + * ------------------------------------------------------------------------- */ +#if !defined(NO_AES) && defined(WOLFSSL_ARMASM) && !defined(__aarch64__) +static void wb_aes_setkey_guard(void) +{ + Aes aes; + byte key[16]; + byte iv[WC_AES_BLOCK_SIZE]; + + XMEMSET(key, 0x2f, sizeof(key)); + XMEMSET(iv, 0x3f, sizeof(iv)); + + if (wc_AesInit(&aes, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_AesInit failed (wc_AesSetKey guard skipped)"); + wb_fail = 1; + return; + } + + /* idx0 true: aes == NULL. */ + (void)wc_AesSetKey(NULL, key, (word32)sizeof(key), iv, AES_ENCRYPTION); + /* idx0 false, idx1 TRUE: valid aes, absent key. */ + (void)wc_AesSetKey(&aes, NULL, (word32)sizeof(key), iv, AES_ENCRYPTION); + /* All-false baseline in the same binary. */ + if (wc_AesSetKey(&aes, key, (word32)sizeof(key), iv, AES_ENCRYPTION) != 0) { + WB_NOTE("wc_AesSetKey valid call failed"); + wb_fail = 1; + } + + wc_AesFree(&aes); + WB_NOTE("wc_AesSetKey aes/userKey guard pairs exercised"); +} +#else +static void wb_aes_setkey_guard(void) +{ WB_NOTE("this variant compiles a different wc_AesSetKey arm (no userKey " + "guard); skipped"); } +#endif + +#if defined(HAVE_AESGCM) && defined(WOLFSSL_AESGCM_STREAM) +static void wb_aesgcm_init_ivsz(void) +{ + Aes aes; + byte key[16]; + byte iv[12]; + + XMEMSET(key, 0x4f, sizeof(key)); + XMEMSET(iv, 0x5f, sizeof(iv)); + + if (wc_AesInit(&aes, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_AesInit failed (wc_AesGcmInit ivSz guard skipped)"); + wb_fail = 1; + return; + } + + /* idx5 FALSE: no IV supplied and none claimed -- accepted (the key is + * installed and the IV is expected from a later call). */ + (void)wc_AesGcmInit(&aes, key, (word32)sizeof(key), NULL, 0); + /* idx5 TRUE (with idx6 TRUE): a length is claimed but no IV given. */ + (void)wc_AesGcmInit(&aes, key, (word32)sizeof(key), NULL, + (word32)sizeof(iv)); + /* Fully valid baseline in the same binary. */ + if (wc_AesGcmInit(&aes, key, (word32)sizeof(key), iv, + (word32)sizeof(iv)) != 0) { + WB_NOTE("wc_AesGcmInit valid call failed"); + wb_fail = 1; + } + + wc_AesFree(&aes); + WB_NOTE("wc_AesGcmInit ivSz/iv cross-check pair exercised"); +} +#else +static void wb_aesgcm_init_ivsz(void) +{ WB_NOTE("AESGCM stream off; wc_AesGcmInit ivSz guard skipped"); } +#endif + +/* ------------------------------------------------------------------------- * + * Class 6: AES_GCM_encrypt_C()/AES_GCM_decrypt_C() AAD guards + * (lines ~11133 and ~11999, operand idx1). + * + * if (authInSz != 0 && authIn != NULL) + * + * The public wc_AesGcmEncrypt/Decrypt reject "authInSz != 0 with authIn == + * NULL" before dispatching, so the software cores never see that combination + * and idx1's FALSE side is white-box only. Calling the core directly with a + * fully initialised Aes is safe: the whole AAD block is skipped when the + * guard is false, so the NULL is never dereferenced. + * ------------------------------------------------------------------------- */ +#if defined(HAVE_AESGCM) && !defined(WOLFSSL_AESNI) && \ + !defined(WOLFSSL_ARMASM) && !defined(WOLFSSL_RISCV_ASM) +static void wb_aesgcm_core_aad(void) +{ + Aes aes; + byte key[16]; + byte iv[12]; + byte pt[WC_AES_BLOCK_SIZE]; + byte ct[WC_AES_BLOCK_SIZE]; + byte dec[WC_AES_BLOCK_SIZE]; + byte tag[WC_AES_BLOCK_SIZE]; + byte aad[16]; + + XMEMSET(key, 0x6f, sizeof(key)); + XMEMSET(iv, 0x7f, sizeof(iv)); + XMEMSET(pt, 0x8f, sizeof(pt)); + XMEMSET(ct, 0, sizeof(ct)); + XMEMSET(dec, 0, sizeof(dec)); + XMEMSET(tag, 0, sizeof(tag)); + XMEMSET(aad, 0x9f, sizeof(aad)); + + if (wc_AesInit(&aes, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_AesInit failed (GCM core AAD guard skipped)"); + wb_fail = 1; + return; + } + if (wc_AesGcmSetKey(&aes, key, (word32)sizeof(key)) != 0) { + WB_NOTE("wc_AesGcmSetKey failed (GCM core AAD guard skipped)"); + wb_fail = 1; + wc_AesFree(&aes); + return; + } + + /* (T,T): a real AAD -- also produces the tag reused below. */ + if (AES_GCM_encrypt_C(&aes, ct, pt, (word32)sizeof(pt), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), + aad, (word32)sizeof(aad)) != 0) { + WB_NOTE("AES_GCM_encrypt_C with AAD failed"); + wb_fail = 1; + } + /* (T,F): a length is claimed but no AAD buffer -- the block is skipped. */ + (void)AES_GCM_encrypt_C(&aes, ct, pt, (word32)sizeof(pt), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), + NULL, (word32)sizeof(aad)); + /* (F,-): no AAD at all. */ + (void)AES_GCM_encrypt_C(&aes, ct, pt, (word32)sizeof(pt), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), NULL, 0); + + /* Same three vectors on the decrypt core. Authentication failure is an + * expected, harmless outcome for the mismatched-AAD vectors. */ + (void)AES_GCM_decrypt_C(&aes, dec, ct, (word32)sizeof(ct), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), + aad, (word32)sizeof(aad)); + (void)AES_GCM_decrypt_C(&aes, dec, ct, (word32)sizeof(ct), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), + NULL, (word32)sizeof(aad)); + (void)AES_GCM_decrypt_C(&aes, dec, ct, (word32)sizeof(ct), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), NULL, 0); + + wc_AesFree(&aes); + WB_NOTE("AES_GCM_{en,de}crypt_C authIn/authInSz guard pairs exercised"); +} +#else +static void wb_aesgcm_core_aad(void) +{ WB_NOTE("GCM software cores not the compiled backend here; AAD guard " + "skipped"); } +#endif + +/* ------------------------------------------------------------------------- * + * Class 6b: the ASM twins of the same AAD guards, AES_GCM_encrypt_ASM() / + * AES_GCM_decrypt_ASM() (lines ~11133 and ~11999, operand idx1). + * + * aes.c picks ONE GCM core per build: + * #if !WOLFSSL_ARMASM && !PPC -> AES_GCM_{en,de}crypt_C + * #elif (__aarch64__ || ARMASM_NO_HW_CRYPTO || ARM32_AES_DISPATCH || PPC) + * -> AES_GCM_{en,de}crypt_ASM + * so the two lines the union reports at 11133/11999 belong to the *_ASM + * bodies compiled by the armasm lanes, not to the host *_C bodies handled + * above. Same three vectors, same reasoning; the guard below mirrors the + * library's #elif exactly so this section only exists where those functions + * do. + * ------------------------------------------------------------------------- */ +#if defined(HAVE_AESGCM) && \ + (defined(WOLFSSL_ARMASM) || defined(WOLFSSL_PPC64_ASM) || \ + defined(WOLFSSL_PPC32_ASM)) && \ + (defined(__aarch64__) || defined(WOLFSSL_ARMASM_NO_HW_CRYPTO) || \ + defined(WOLFSSL_ARM32_AES_DISPATCH) || defined(WOLFSSL_PPC64_ASM) || \ + defined(WOLFSSL_PPC32_ASM)) +static void wb_aesgcm_asm_aad(void) +{ + Aes aes; + byte key[16]; + byte iv[12]; + byte pt[WC_AES_BLOCK_SIZE]; + byte ct[WC_AES_BLOCK_SIZE]; + byte dec[WC_AES_BLOCK_SIZE]; + byte tag[WC_AES_BLOCK_SIZE]; + byte aad[16]; + + XMEMSET(key, 0x6f, sizeof(key)); + XMEMSET(iv, 0x7f, sizeof(iv)); + XMEMSET(pt, 0x8f, sizeof(pt)); + XMEMSET(ct, 0, sizeof(ct)); + XMEMSET(dec, 0, sizeof(dec)); + XMEMSET(tag, 0, sizeof(tag)); + XMEMSET(aad, 0x9f, sizeof(aad)); + + if (wc_AesInit(&aes, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_AesInit failed (GCM ASM AAD guard skipped)"); + wb_fail = 1; + return; + } + if (wc_AesGcmSetKey(&aes, key, (word32)sizeof(key)) != 0) { + WB_NOTE("wc_AesGcmSetKey failed (GCM ASM AAD guard skipped)"); + wb_fail = 1; + wc_AesFree(&aes); + return; + } + + /* (T,T) then (T,F) then (F,-) on the encrypt core. */ + (void)AES_GCM_encrypt_ASM(&aes, ct, pt, (word32)sizeof(pt), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), + aad, (word32)sizeof(aad)); + (void)AES_GCM_encrypt_ASM(&aes, ct, pt, (word32)sizeof(pt), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), + NULL, (word32)sizeof(aad)); + (void)AES_GCM_encrypt_ASM(&aes, ct, pt, (word32)sizeof(pt), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), NULL, 0); + + /* Same three on the decrypt core (authentication failure on the + * mismatched-AAD vectors is expected and harmless). */ + (void)AES_GCM_decrypt_ASM(&aes, dec, ct, (word32)sizeof(ct), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), + aad, (word32)sizeof(aad)); + (void)AES_GCM_decrypt_ASM(&aes, dec, ct, (word32)sizeof(ct), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), + NULL, (word32)sizeof(aad)); + (void)AES_GCM_decrypt_ASM(&aes, dec, ct, (word32)sizeof(ct), + iv, (word32)sizeof(iv), tag, (word32)sizeof(tag), NULL, 0); + + wc_AesFree(&aes); + WB_NOTE("AES_GCM_{en,de}crypt_ASM authIn/authInSz guard pairs exercised"); +} +#else +static void wb_aesgcm_asm_aad(void) +{ WB_NOTE("GCM ASM cores not the compiled backend here; AAD guard skipped"); } +#endif + int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("aes.c white-box MC/DC supplement\n"); #ifdef NO_AES printf(" NO_AES defined; nothing to exercise\n"); @@ -551,6 +831,10 @@ int main(void) wb_aesni(); wb_aarch64_hwcrypto_dispatch(); wb_aarch64_gcm_ptr_guards(); + wb_aes_setkey_guard(); + wb_aesgcm_init_ivsz(); + wb_aesgcm_core_aad(); + wb_aesgcm_asm_aad(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures are surfaced as skips, not test failures: the campaign * treats a nonzero exit as a failed variant and discards its coverage. */ diff --git a/tests/unit-mcdc/test_cryptocb_whitebox.c b/tests/unit-mcdc/test_cryptocb_whitebox.c index b70e6f58749..82f6b07aa4f 100644 --- a/tests/unit-mcdc/test_cryptocb_whitebox.c +++ b/tests/unit-mcdc/test_cryptocb_whitebox.c @@ -883,7 +883,48 @@ int main(void) "key-slot state, out of scope for this pass"); #endif - /* ---- Curve25519 MakePub / Generic, and the ECIES pair ---- + /* ---- ECIES encrypt/decrypt dispatch (HAVE_ECC_ENCRYPT) ---- + * Both bodies resolve their device from privKey->devId and then take the + * usual `if (dev && dev->cb)` guard, so the standard three-vector sweep + * applies. Nothing but privKey->devId is read before the guard, and the + * registered callback (wb_cb) ignores the wc_CryptoInfo it is handed and + * reports CRYPTOCB_UNAVAILABLE, so a zeroed ecc_key with no key material + * is sufficient and safe here -- no curve arithmetic runs. */ +#ifdef HAVE_ECC_ENCRYPT + { + ecc_key ecPriv; + byte eciesMsg[16]; + byte eciesOut[128]; + word32 eciesOutSz; + + XMEMSET(&ecPriv, 0, sizeof(ecPriv)); + XMEMSET(eciesMsg, 0x5e, sizeof(eciesMsg)); + XMEMSET(eciesOut, 0, sizeof(eciesOut)); + + eciesOutSz = (word32)sizeof(eciesOut); + WB_DRIVE3(ecPriv.devId, + wc_CryptoCb_EciesEncrypt(&ecPriv, NULL, eciesMsg, + (word32)sizeof(eciesMsg), eciesOut, &eciesOutSz, NULL, 0)); + + eciesOutSz = (word32)sizeof(eciesOut); + WB_DRIVE3(ecPriv.devId, + wc_CryptoCb_EciesDecrypt(&ecPriv, NULL, eciesMsg, + (word32)sizeof(eciesMsg), eciesOut, &eciesOutSz, NULL)); + + /* privKey == NULL early return (both entry points). */ + eciesOutSz = (word32)sizeof(eciesOut); + (void)wc_CryptoCb_EciesEncrypt(NULL, NULL, eciesMsg, + (word32)sizeof(eciesMsg), eciesOut, &eciesOutSz, NULL, 0); + (void)wc_CryptoCb_EciesDecrypt(NULL, NULL, eciesMsg, + (word32)sizeof(eciesMsg), eciesOut, &eciesOutSz, NULL); + + WB_NOTE("ECIES Encrypt/Decrypt dev&&dev->cb three-vector driven"); + } +#else + WB_NOTE("HAVE_ECC_ENCRYPT not defined; ECIES dispatch skipped"); +#endif + + /* ---- Curve25519 MakePub / Generic ---- * These take no devId: they resolve a device with FindDevice(INVALID_DEVID) * and fall back to FindDeviceByIndex(0), so their `if (dev && dev->cb)` * guard is driven by what is registered rather than by an argument. Run diff --git a/tests/unit-mcdc/test_kdf_whitebox.c b/tests/unit-mcdc/test_kdf_whitebox.c new file mode 100644 index 00000000000..7bb7a95979d --- /dev/null +++ b/tests/unit-mcdc/test_kdf_whitebox.c @@ -0,0 +1,250 @@ +/* test_kdf_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC supplement for wolfcrypt/src/kdf.c. + * + * Three residual classes that tests/api/test_kdf.c cannot close on its own, + * driven here with BOTH halves of every independence pair in ONE binary + * (llvm-cov derives MC/DC per binary, so a rejection on its own proves + * nothing): + * + * 1. wc_Tls13_HKDF_Extract_ex() (~line 354) + * if (prk == NULL || (ikm == NULL && ikmLen > 0)) + * A public entry point, but the campaign's group tests only ever call it + * with a valid prk and a present ikm, so none of the three operands gets + * a pair. All four call shapes are memory-safe: the guard short-circuits + * before either pointer is read, and the accepted "ikm == NULL && + * ikmLen == 0" shape is the RFC 5869 zero-IKM case the function itself + * substitutes a zeroed local buffer for. + * + * 2. wc_PRF() (~line 152, WOLFSSL_SMALL_STACK only) + * if (current == NULL || hmac == NULL) + * The two scratch buffers come from back-to-back XMALLOC()s that never + * fail in a normal run. mcdc_fault_alloc.h fails the n-th and every later + * allocation, which maps one-to-one onto the two operands: + * arm(1) -> current == NULL (and hmac == NULL) -> idx0 T + * arm(2) -> current != NULL, hmac == NULL -> idx0 F, idx1 T + * unarmed -> idx0 F, idx1 F + * The guard XFREEs whatever it got (XFREE(NULL) is a no-op) and returns + * MEMORY_E before anything dereferences the scratch, so every armed call + * is crash-safe. + * + * 3. wc_KDA_KDF_onestep() (~line 1421, WOLFSSL_SMALL_STACK only) + * if (ret == 0 && outIdx < derivedSecretSz) + * The "ret == 0" operand needs a failed derivation iteration that still + * leaves outIdx short of the requested length. Faulting the first + * allocation inside wc_KDA_KDF_iteration() (its WC_ALLOC_VAR_EX of the + * wc_HashAlg scratch) makes iteration 1 return MEMORY_E, the loop breaks + * with outIdx still 0, and the tail guard is evaluated with (F,T). + * + * Deliberately NOT chased here (documented residuals, not oversights): the + * "ret == 0 && kPad" pairs in wc_SSH_KDF (~804/~855), the + * "(ret == 0) && ..." loop/tail guards in wc_srtp_kdf_derive_key (~947/~959) + * and the "ret == 0 && fixedInfoSz > 0" guard in wc_KDA_KDF_iteration (~1354) + * all require a *hash or AES transform* to fail mid-operation on valid + * buffers. Those primitives allocate nothing on this path, so no + * allocation-failure injection reaches them; they are the same + * transform-failure residual class as the sha module's. + * + * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * as the instrumented library, then linked against that variant's + * libwolfssl.a with kdf.o removed. Not part of the wolfSSL build. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include + +#include + +#include "mcdc_fault_alloc.h" + +#include +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +/* -------------------------------------------------------------------------- + * 1. wc_Tls13_HKDF_Extract_ex(): prk / ikm / ikmLen argument guard. + * ----------------------------------------------------------------------- */ +#if defined(HAVE_HKDF) && !defined(NO_HMAC) && !defined(NO_SHA256) +static void wb_tls13_hkdf_extract_guard(void) +{ + byte prk[WC_SHA256_DIGEST_SIZE]; + byte salt[WC_SHA256_DIGEST_SIZE]; + byte ikm[32]; + int ret; + + XMEMSET(prk, 0, sizeof(prk)); + XMEMSET(salt, 0x5a, sizeof(salt)); + XMEMSET(ikm, 0x3c, sizeof(ikm)); + + /* idx0 true: prk == NULL. */ + ret = wc_Tls13_HKDF_Extract_ex(NULL, salt, (word32)sizeof(salt), + ikm, (word32)sizeof(ikm), WC_SHA256, NULL, INVALID_DEVID); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("HKDF_Extract prk==NULL not rejected"); + wb_fail = 1; + } + + /* idx0 false, idx1 true, idx2 true: ikm absent but a length claimed. */ + ret = wc_Tls13_HKDF_Extract_ex(prk, salt, (word32)sizeof(salt), + NULL, (word32)sizeof(ikm), WC_SHA256, NULL, INVALID_DEVID); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("HKDF_Extract ikm==NULL/ikmLen>0 not rejected"); + wb_fail = 1; + } + + /* idx0 false, idx1 true, idx2 FALSE: the accepted zero-IKM shape. */ + ret = wc_Tls13_HKDF_Extract_ex(prk, salt, (word32)sizeof(salt), + NULL, 0, WC_SHA256, NULL, INVALID_DEVID); + if (ret != 0) { + WB_NOTE("HKDF_Extract zero-length IKM unexpectedly failed"); + wb_fail = 1; + } + + /* All-false baseline in the same binary: a real extract. */ + ret = wc_Tls13_HKDF_Extract_ex(prk, salt, (word32)sizeof(salt), + ikm, (word32)sizeof(ikm), WC_SHA256, NULL, INVALID_DEVID); + if (ret != 0) { + WB_NOTE("HKDF_Extract valid call failed"); + wb_fail = 1; + } + + WB_NOTE("wc_Tls13_HKDF_Extract_ex prk/ikm/ikmLen pairs exercised"); +} +#else +static void wb_tls13_hkdf_extract_guard(void) +{ WB_NOTE("HAVE_HKDF/HMAC/SHA256 off; HKDF_Extract guard skipped"); } +#endif + +/* -------------------------------------------------------------------------- + * 2. wc_PRF(): current / hmac scratch-allocation guard (small-stack only). + * ----------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_PRF) && !defined(NO_HMAC) && !defined(NO_SHA256) && \ + defined(WOLFSSL_SMALL_STACK) && !defined(MCDC_FA_UNAVAILABLE) +static void wb_prf_alloc_guard(void) +{ + byte result[48]; + byte secret[32]; + byte seed[32]; + int ret; + + XMEMSET(result, 0, sizeof(result)); + XMEMSET(secret, 0x11, sizeof(secret)); + XMEMSET(seed, 0x22, sizeof(seed)); + + mcdc_fa_install(); + + /* idx0 true: the "current" XMALLOC fails (so does "hmac"). */ + mcdc_fa_arm(1); + (void)wc_PRF(result, (word32)sizeof(result), secret, (word32)sizeof(secret), + seed, (word32)sizeof(seed), sha256_mac, NULL, INVALID_DEVID); + mcdc_fa_disarm(); + + /* idx0 false, idx1 true: "current" succeeds, "hmac" fails. */ + mcdc_fa_arm(2); + (void)wc_PRF(result, (word32)sizeof(result), secret, (word32)sizeof(secret), + seed, (word32)sizeof(seed), sha256_mac, NULL, INVALID_DEVID); + mcdc_fa_disarm(); + + /* All-false baseline, unarmed, in the same binary. */ + ret = wc_PRF(result, (word32)sizeof(result), secret, (word32)sizeof(secret), + seed, (word32)sizeof(seed), sha256_mac, NULL, INVALID_DEVID); + if (ret != 0) { + WB_NOTE("wc_PRF unarmed baseline failed"); + wb_fail = 1; + } + + mcdc_fa_restore(); + WB_NOTE("wc_PRF current/hmac XMALLOC guard pairs exercised"); +} +#else +static void wb_prf_alloc_guard(void) +{ WB_NOTE("not WOLFSSL_SMALL_STACK PRF (or no injector); wc_PRF alloc guard " + "skipped"); } +#endif + +/* -------------------------------------------------------------------------- + * 3. wc_KDA_KDF_onestep(): "ret == 0" half of the partial-block tail guard. + * ----------------------------------------------------------------------- */ +#if defined(WC_KDF_NIST_SP_800_56C) && !defined(NO_SHA256) && \ + defined(WOLFSSL_SMALL_STACK) && !defined(MCDC_FA_UNAVAILABLE) +static void wb_kda_kdf_onestep_errprop(void) +{ + byte z[32]; + byte fixedInfo[8]; + byte out[WC_SHA256_DIGEST_SIZE + 5]; /* not a whole number of blocks */ + int ret; + + XMEMSET(z, 0x77, sizeof(z)); + XMEMSET(fixedInfo, 0x88, sizeof(fixedInfo)); + XMEMSET(out, 0, sizeof(out)); + + mcdc_fa_install(); + + /* Faults the wc_HashAlg scratch allocation of the FIRST iteration: the + * loop breaks with ret != 0 and outIdx still 0, so the tail guard sees + * (F, T). Armed around this one call; nothing else here allocates. */ + mcdc_fa_arm(1); + (void)wc_KDA_KDF_onestep(z, (word32)sizeof(z), fixedInfo, + (word32)sizeof(fixedInfo), (word32)sizeof(out), WC_HASH_TYPE_SHA256, + out, (word32)sizeof(out)); + mcdc_fa_disarm(); + + /* All-true baseline, unarmed: ret == 0 and a genuine partial tail block. */ + ret = wc_KDA_KDF_onestep(z, (word32)sizeof(z), fixedInfo, + (word32)sizeof(fixedInfo), (word32)sizeof(out), WC_HASH_TYPE_SHA256, + out, (word32)sizeof(out)); + if (ret != 0) { + WB_NOTE("wc_KDA_KDF_onestep unarmed baseline failed"); + wb_fail = 1; + } + + mcdc_fa_restore(); + WB_NOTE("wc_KDA_KDF_onestep tail-guard ret==0 pair exercised"); +} +#else +static void wb_kda_kdf_onestep_errprop(void) +{ WB_NOTE("WC_KDF_NIST_SP_800_56C/small-stack off; KDA tail guard skipped"); } +#endif + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + + printf("kdf.c white-box MC/DC supplement\n"); + + wb_tls13_hkdf_extract_guard(); + wb_prf_alloc_guard(); + wb_kda_kdf_onestep_errprop(); + + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Always 0: a nonzero exit discards this variant's whole coverage. */ + (void)wb_fail; + return 0; +} diff --git a/tests/unit-mcdc/test_poly1305_whitebox.c b/tests/unit-mcdc/test_poly1305_whitebox.c index d51c2282031..b567f5bd719 100644 --- a/tests/unit-mcdc/test_poly1305_whitebox.c +++ b/tests/unit-mcdc/test_poly1305_whitebox.c @@ -108,14 +108,87 @@ static void wb_poly1305_dispatch(void) #endif +/* ---- wc_Poly1305SetKey() argument guard (~line 850) ---------------------- * + * + * if ((ctx == NULL) || (key == NULL) || (keySz != 32)) + * + * MEASURED RESULT: idx1 (key == NULL) is UNSATISFIABLE, and this function is + * the evidence. wc_Poly1305SetKey() opens with a separate, earlier + * + * if (key == NULL) return BAD_FUNC_ARG; + * + * (~line 836), so by the time the compound at ~850 is evaluated key is + * non-NULL by construction: idx1 is a redundant re-check that can only ever + * be observed FALSE, and no independence pair for it exists through any entry + * point. Calling wc_Poly1305SetKey(ctx, NULL, 32) here returns from the + * earlier guard and never reaches line 850 -- confirmed by the white-box + * binary's own MC/DC record, which shows idx0/idx2 covered and idx1 not. + * The three satisfiable vectors plus the all-false baseline are kept as + * same-binary regression evidence for idx0/idx2. + * + * Also NOT chased (structurally unsatisfiable): wc_Poly1305_Pad()'s + * "(paddingLen > 0) && (paddingLen < WC_POLY1305_PAD_SZ)" idx1. paddingLen is + * computed as "(-(int)lenToPad) & (WC_POLY1305_PAD_SZ - 1)", i.e. masked into + * [0, WC_POLY1305_PAD_SZ-1], so whenever idx1 is evaluated it is TRUE by + * construction -- no call shape can make it false, so no independence pair + * exists. + * ------------------------------------------------------------------------ */ +#ifdef HAVE_POLY1305 +static void wb_poly1305_setkey_guard(void) +{ + Poly1305 ctx; + static const byte key[32] = { + 0x85,0xd6,0xbe,0x78,0x57,0x55,0x6d,0x33, + 0x7f,0x44,0x52,0xfe,0x42,0xd5,0x06,0xa8, + 0x01,0x03,0x80,0x8a,0xfb,0x0d,0xb2,0xfd, + 0x4a,0xbf,0xf6,0xaf,0x41,0x49,0xf5,0x1b + }; + byte tag[WC_POLY1305_MAC_SZ]; + + XMEMSET(&ctx, 0, sizeof(ctx)); + + /* idx0 true: ctx == NULL. */ + if (wc_Poly1305SetKey(NULL, key, (word32)sizeof(key)) != + WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("wc_Poly1305SetKey(ctx==NULL) not rejected"); + wb_fail = 1; + } + /* idx0 false, idx1 TRUE: valid ctx, absent key. */ + if (wc_Poly1305SetKey(&ctx, NULL, (word32)sizeof(key)) != + WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("wc_Poly1305SetKey(key==NULL) not rejected"); + wb_fail = 1; + } + /* idx0/idx1 false, idx2 true: wrong key size. */ + if (wc_Poly1305SetKey(&ctx, key, 16) != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("wc_Poly1305SetKey(keySz!=32) not rejected"); + wb_fail = 1; + } + /* All-false baseline in the same binary, run to completion. */ + if (wc_Poly1305SetKey(&ctx, key, (word32)sizeof(key)) != 0 || + wc_Poly1305Update(&ctx, key, (word32)sizeof(key)) != 0 || + wc_Poly1305Final(&ctx, tag) != 0) { + WB_NOTE("wc_Poly1305SetKey valid sequence failed"); + wb_fail = 1; + } + + WB_NOTE("wc_Poly1305SetKey ctx/key/keySz guard pairs exercised"); +} +#else +static void wb_poly1305_setkey_guard(void) +{ WB_NOTE("HAVE_POLY1305 off; wc_Poly1305SetKey guard skipped"); } +#endif + int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("poly1305.c white-box supplement\n"); #ifndef HAVE_POLY1305 printf(" HAVE_POLY1305 not defined; nothing to exercise\n"); return 0; #else wb_poly1305_dispatch(); + wb_poly1305_setkey_guard(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures are surfaced as skips, not test failures: the campaign * treats a nonzero exit as a failed variant and discards its coverage. */ diff --git a/tests/unit-mcdc/test_random_whitebox.c b/tests/unit-mcdc/test_random_whitebox.c index cdd08d08f44..9a176f0e271 100644 --- a/tests/unit-mcdc/test_random_whitebox.c +++ b/tests/unit-mcdc/test_random_whitebox.c @@ -82,6 +82,8 @@ #include +#include "mcdc_fault_alloc.h" + #include static int wb_fail = 0; @@ -506,8 +508,193 @@ static void wb_rng_healthtest512_internal(void) #endif /* HAVE_HASHDRBG && WOLFSSL_DRBG_SHA512 */ +/* ---- Hash_gen()/Hash512_gen() small-stack scratch allocation guards ------- * + * + * 779: if (data == NULL || digest == NULL) (Hash_gen) + * 1379: if (data == NULL || digest == NULL) (Hash512_gen) + * + * Compiled only under WOLFSSL_SMALL_STACK && !WOLFSSL_SMALL_STACK_CACHE, where + * the per-call seed/digest scratch is heap-allocated by two back-to-back + * XMALLOC()s. Both operands stay false in every normal run, so the true sides + * need the allocator to fail. mcdc_fault_alloc.h fails the n-th and every + * later allocation, which maps exactly onto the two operands: + * + * arm(1) -> data==NULL (and digest==NULL) -> idx0 T -> T + * arm(2) -> data!=NULL, digest==NULL -> idx0 F, idx1 T -> T + * unarmed-> both non-NULL -> idx0 F, idx1 F -> F + * + * The guard returns DRBG_FAILURE immediately on either failure after XFREE-ing + * whatever was obtained (XFREE(NULL) is a no-op), so nothing downstream runs + * with a NULL scratch pointer. Each arm brackets exactly ONE Hash*_gen call; + * the DRBG is instantiated up front while disarmed so its own allocations + * succeed, and it is a scratch object not reused by any later check. + * ------------------------------------------------------------------------ */ +#if defined(HAVE_HASHDRBG) && !defined(NO_SHA256) && \ + defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_SMALL_STACK_CACHE) && \ + !defined(MCDC_FA_UNAVAILABLE) +static void wb_hash_gen_alloc_guard(void) +{ + DRBG_internal drbg; + byte seed[48]; + byte nonce[16]; + byte out[32]; + word32 i; + int ret; + + XMEMSET(&drbg, 0, sizeof(drbg)); + for (i = 0; i < (word32)sizeof(seed); i++) + seed[i] = (byte)(i + 7); + for (i = 0; i < (word32)sizeof(nonce); i++) + nonce[i] = (byte)(i + 8); + + mcdc_fa_install(); + + /* Setup runs DISARMED so every allocation it needs succeeds. */ + ret = Hash_DRBG_Instantiate(&drbg, seed, (word32)sizeof(seed), + nonce, (word32)sizeof(nonce), NULL, 0, NULL, INVALID_DEVID); + if (ret != DRBG_SUCCESS) { + WB_NOTE("Instantiate failed; skip Hash_gen alloc guard"); + mcdc_fa_restore(); + return; + } + + mcdc_fa_arm(1); /* data == NULL */ + (void)Hash_gen(&drbg, out, (word32)sizeof(out), drbg.V); + mcdc_fa_disarm(); + + mcdc_fa_arm(2); /* digest == NULL */ + (void)Hash_gen(&drbg, out, (word32)sizeof(out), drbg.V); + mcdc_fa_disarm(); + + /* All-false baseline in the SAME binary. */ + if (Hash_gen(&drbg, out, (word32)sizeof(out), drbg.V) != DRBG_SUCCESS) { + WB_NOTE("Hash_gen unarmed baseline failed"); + wb_fail = 1; + } + + (void)Hash_DRBG_Uninstantiate(&drbg); + mcdc_fa_restore(); + WB_NOTE("Hash_gen data/digest XMALLOC guard pairs exercised"); +} +#else +static void wb_hash_gen_alloc_guard(void) +{ WB_NOTE("not SMALL_STACK-without-CACHE (or no injector); Hash_gen alloc " + "guard skipped"); } +#endif + +#if defined(HAVE_HASHDRBG) && defined(WOLFSSL_DRBG_SHA512) && \ + defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_SMALL_STACK_CACHE) && \ + !defined(MCDC_FA_UNAVAILABLE) +static void wb_hash512_gen_alloc_guard(void) +{ + DRBG_SHA512_internal drbg; + byte seed[32]; + byte nonce[16]; + byte out[32]; + word32 i; + int ret; + + XMEMSET(&drbg, 0, sizeof(drbg)); + for (i = 0; i < (word32)sizeof(seed); i++) + seed[i] = (byte)(i + 9); + for (i = 0; i < (word32)sizeof(nonce); i++) + nonce[i] = (byte)(i + 10); + + mcdc_fa_install(); + + ret = Hash512_DRBG_Instantiate(&drbg, seed, (word32)sizeof(seed), + nonce, (word32)sizeof(nonce), NULL, 0, NULL, INVALID_DEVID); + if (ret != DRBG_SUCCESS) { + WB_NOTE("Instantiate512 failed; skip Hash512_gen alloc guard"); + mcdc_fa_restore(); + return; + } + + mcdc_fa_arm(1); /* data == NULL */ + (void)Hash512_gen(&drbg, out, (word32)sizeof(out), drbg.V); + mcdc_fa_disarm(); + + mcdc_fa_arm(2); /* digest == NULL */ + (void)Hash512_gen(&drbg, out, (word32)sizeof(out), drbg.V); + mcdc_fa_disarm(); + + if (Hash512_gen(&drbg, out, (word32)sizeof(out), drbg.V) != DRBG_SUCCESS) { + WB_NOTE("Hash512_gen unarmed baseline failed"); + wb_fail = 1; + } + + (void)Hash512_DRBG_Uninstantiate(&drbg); + mcdc_fa_restore(); + WB_NOTE("Hash512_gen data/digest XMALLOC guard pairs exercised"); +} +#else +static void wb_hash512_gen_alloc_guard(void) +{ WB_NOTE("not SMALL_STACK-without-CACHE + DRBG_SHA512 (or no injector); " + "Hash512_gen alloc guard skipped"); } +#endif + +/* ---- wc_GenerateSeed() output-buffer guard (line ~5940) ------------------- * + * + * if (os == NULL || output == NULL) return BAD_FUNC_ARG; + * + * The generic (POSIX host) arm of random.c's long wc_GenerateSeed #if/#elif + * chain validates its arguments before any entropy backend touches them. No + * tests/api caller passes NULL for either, so both operands need this direct + * pairing plus a same-binary valid seed read. + * + * BUILD-AXIS GUARD: that chain compiles exactly ONE wc_GenerateSeed body, and + * the other arms neither share this guard nor, in the CUSTOM_RAND_GENERATE_BLOCK + * case, define wc_GenerateSeed at all (that variant would not even link this + * TU). The condition below excludes every arm reachable from this campaign's + * variant set and from a host build, so the NULL vectors are only compiled + * where the guard that catches them is. + * ------------------------------------------------------------------------ */ +#if !defined(WC_NO_RNG) && !defined(CUSTOM_RAND_GENERATE_BLOCK) && \ + !defined(CUSTOM_RAND_GENERATE_SEED) && !defined(NO_DEV_RANDOM) && \ + !defined(USE_WINDOWS_API) && !defined(WOLFSSL_LINUXKM) && \ + !defined(WOLFSSL_BSDKM) && !defined(WOLFSSL_SAFERTOS) && \ + !defined(WOLFSSL_LEANPSK) && !defined(WOLFSSL_GENSEED_FORTEST) +static void wb_generate_seed_guard(void) +{ + OS_Seed os; + byte output[16]; + int ret; + + XMEMSET(&os, 0, sizeof(os)); + XMEMSET(output, 0, sizeof(output)); +#ifdef WOLF_CRYPTO_CB + os.devId = INVALID_DEVID; +#endif + + /* idx0 true: os == NULL (short-circuits before output is looked at). */ + if (wc_GenerateSeed(NULL, output, (word32)sizeof(output)) != + WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("wc_GenerateSeed(os==NULL) not rejected"); + wb_fail = 1; + } + /* idx0 false, idx1 true: os valid, output == NULL. */ + if (wc_GenerateSeed(&os, NULL, (word32)sizeof(output)) != + WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("wc_GenerateSeed(output==NULL) not rejected"); + wb_fail = 1; + } + /* All-false baseline: a real (small, bounded) seed read. */ + ret = wc_GenerateSeed(&os, output, (word32)sizeof(output)); + if (ret != 0) { + WB_NOTE("wc_GenerateSeed valid call failed"); + wb_fail = 1; + } + + WB_NOTE("wc_GenerateSeed os/output NULL guard pairs exercised"); +} +#else +static void wb_generate_seed_guard(void) +{ WB_NOTE("this variant compiles a different wc_GenerateSeed arm; skipped"); } +#endif + int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("random.c white-box supplement\n"); #ifdef WC_NO_RNG printf(" WC_NO_RNG defined; nothing to exercise\n"); @@ -522,6 +709,9 @@ int main(void) wb_hash512_df_multiblock(); wb_hash512_drbg_generate_reseed(); wb_rng_healthtest512_internal(); + wb_generate_seed_guard(); + wb_hash_gen_alloc_guard(); + wb_hash512_gen_alloc_guard(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures are surfaced as skips, not test failures: the * campaign treats a nonzero exit as a failed variant and discards its diff --git a/tests/unit-mcdc/test_rsa_fault_whitebox.c b/tests/unit-mcdc/test_rsa_fault_whitebox.c index 556b4ffa991..719ef156667 100644 --- a/tests/unit-mcdc/test_rsa_fault_whitebox.c +++ b/tests/unit-mcdc/test_rsa_fault_whitebox.c @@ -242,6 +242,84 @@ static void wb_makersakey_alloc_guard(WC_RNG* rng) WB_NOTE("wc_MakeRsaKey p/q/tmp* NULL-guard swept (n=1..8)"); } +/* --------------------------------------------------------------------------- + * wc_RsaPSS_CheckPadding_ex2() long-salt scratch buffer (WOLFSSL_PSS_LONG_SALT) + * + * 4586: if ((ret == 0) && (sizeof(sigCheckBuf) < (RSA_PSS_PAD_SZ + inSz + + * (word32)saltLen))) + * 4617: if (sigCheck != NULL && sigCheck != sigCheckBuf) + * + * sigCheck starts as the on-stack sigCheckBuf (WC_MAX_DIGEST_SIZE*2 + + * RSA_PSS_PAD_SZ = 136 bytes) and is only replaced by an XMALLOC'd buffer when + * the salt pushes 8 + inSz + saltLen past that. Three same-binary vectors: + * + * short salt -> sigCheck == sigCheckBuf 4617 (T,F) -> F + * long salt, OK -> sigCheck == heap buffer 4617 (T,T) -> T + * long salt, OOM -> sigCheck == NULL 4617 (F,-) -> F + * + * and for 4586 the same calls give (T,F)/(T,T) plus a pre-rejected call + * (in==NULL sets ret=BAD_FUNC_ARG upstream) for the (F,-) half. + * + * Sizing: SHA-512 digest (inSz 64) with saltLen 70 -> 8+64+70 = 142 > 136, so + * the heap path is taken; sigSz must equal inSz+saltLen = 134, and the code + * reads sig[saltLen .. saltLen+inSz), so the sig buffer is 134 bytes. The + * padding never verifies (the input is not a real PSS block) -- BAD_PADDING_E + * is the expected, harmless outcome; only the buffer-selection decisions + * matter here. The OOM vector is armed around this ONE call with everything + * else built while disarmed. + * ------------------------------------------------------------------------ */ +#if defined(WOLFSSL_PSS_LONG_SALT) && defined(WC_RSA_PSS) +static void wb_pss_checkpadding_sigcheck(void) +{ + byte in[WC_MAX_DIGEST_SIZE]; + byte sig[WC_MAX_DIGEST_SIZE * 2 + 16]; + int digSz; + +#ifdef WOLFSSL_SHA512 + const enum wc_HashType ht = WC_HASH_TYPE_SHA512; + const int longSalt = 70; +#else + const enum wc_HashType ht = WC_HASH_TYPE_SHA256; + const int longSalt = 110; /* 8 + 32 + 110 = 150 > 136 */ +#endif + + XMEMSET(in, 0x5a, sizeof(in)); + XMEMSET(sig, 0xa5, sizeof(sig)); + + digSz = wc_HashGetDigestSize(ht); + if (digSz <= 0 || (word32)(digSz + longSalt) > (word32)sizeof(sig)) { + WB_NOTE("PSS long-salt sizing unavailable; sigCheck check skipped"); + return; + } + + /* (F,-) half of 4586: rejected upstream, ret != 0 at the size test. */ + (void)wc_RsaPSS_CheckPadding_ex2(NULL, (word32)digSz, sig, + (word32)digSz * 2, ht, digSz, 0, NULL); + + /* short salt: stack buffer, 4586 (T,F) / 4617 (T,F). */ + (void)wc_RsaPSS_CheckPadding_ex2(in, (word32)digSz, sig, + (word32)(digSz * 2), ht, digSz, 0, NULL); + + /* long salt, allocation succeeds: 4586 (T,T) / 4617 (T,T). */ + (void)wc_RsaPSS_CheckPadding_ex2(in, (word32)digSz, sig, + (word32)(digSz + longSalt), ht, longSalt, 0, NULL); + +#ifndef MCDC_FA_UNAVAILABLE + /* long salt, allocation fails: sigCheck == NULL -> 4617 (F,-). Armed + * around this single call only; nothing built here needs the heap. */ + mcdc_fa_arm(1); + (void)wc_RsaPSS_CheckPadding_ex2(in, (word32)digSz, sig, + (word32)(digSz + longSalt), ht, longSalt, 0, NULL); + mcdc_fa_disarm(); +#endif + + WB_NOTE("wc_RsaPSS_CheckPadding_ex2 sigCheck stack/heap/NULL pairs done"); +} +#else +static void wb_pss_checkpadding_sigcheck(void) +{ WB_NOTE("WOLFSSL_PSS_LONG_SALT/WC_RSA_PSS off; sigCheck check skipped"); } +#endif + int main(int argc, char** argv) { int do_baseline = (argc > 1 && strcmp(argv[1], "baseline") == 0); @@ -319,6 +397,12 @@ int main(int argc, char** argv) if (ret > 0) derLen = ret; #endif + /* Cheap and independent of the fault sweeps below, so run it here rather + * than after them: the RSA_LOW_MEM variant's sweep can hit the harness + * wall-clock limit, and anything queued behind it would be lost with the + * whole run. */ + wb_pss_checkpadding_sigcheck(); + #ifndef MCDC_FA_UNAVAILABLE if (do_probe) { /* Diagnostic: count the allocations each entry point performs WITHOUT diff --git a/tests/unit-mcdc/test_rsa_whitebox.c b/tests/unit-mcdc/test_rsa_whitebox.c index f2acbd21f49..f9b62a919d6 100644 --- a/tests/unit-mcdc/test_rsa_whitebox.c +++ b/tests/unit-mcdc/test_rsa_whitebox.c @@ -313,6 +313,18 @@ static void wb_privkey_decode_raw(void) (void)_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 0, b, 4, b, 4, b, 4, b, 4, &key); /* uSz==0 */ (void)_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, b, 0, b, 4, &key); /* dP!=NULL && dPSz==0 */ (void)_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, b, 0, &key); /* dQ!=NULL && dQSz==0 */ + + /* The "dP != NULL" / "dQ != NULL" operands themselves (idx 2 and idx 4) + * need their FALSE side, which every call above holds true: dP/dQ are + * optional CRT components, so passing them absent is a legitimate, + * memory-safe call shape that simply skips their size cross-check. + * dP==NULL, dQ==NULL -> idx2 F (idx4 then F) decision F + * dP valid, dQ==NULL -> idx2 T, idx3 F, idx4 F decision F + * dP==NULL, dQ valid -> idx2 F, idx4 T, idx5 F decision F + * paired against the idx2/idx4 TRUE vectors just above. */ + (void)_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, NULL, 0, NULL, 0, &key); + (void)_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, NULL, 0, &key); + (void)_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, NULL, 0, b, 4, &key); #endif /* all-false side of both checks: every required arg valid, u/dP/dQ valid. @@ -589,8 +601,112 @@ static void wb_rsa_function_nonblock(void) static void wb_rsa_function_nonblock(void) { WB_NOTE("WC_RSA_NONBLOCK off; wc_RsaFunctionNonBlock skipped"); } #endif +/* ------------------------------------------------------------------------- * + * Class 12: wc_RsaPrivateKeyDecodeRaw() (the PUBLIC wrapper, lines ~6051 and + * ~6059) -- distinct decisions from the _RsaPrivateKeyDecodeRaw static above. + * + * 6051: if (n==NULL||nSz==0||e==NULL||eSz==0||d==NULL||dSz==0 + * ||p==NULL||pSz==0||q==NULL||qSz==0||key==NULL) [idx10 = key] + * 6059: if ((u==NULL||uSz==0)||(dP!=NULL&&dPSz==0)||(dQ!=NULL&&dQSz==0)) + * [idx2..idx5 = dP/dQ] + * + * The wrapper sets err rather than returning, so a rejected call falls through + * a chain of "if (err == MP_OKAY)" guards and never dereferences key. All + * accepted calls import 4-byte dummy components into the same initialized key + * (mp_read_unsigned_bin accepts any bytes); when dP/dQ are omitted the wrapper + * derives them with CalcDX() from the (equally dummy) p/q/d, which is a plain + * bounded modular reduction -- no key generation, no primality search. + * ------------------------------------------------------------------------- */ +#ifndef WOLFSSL_RSA_PUBLIC_ONLY +static void wb_pub_privkey_decode_raw(void) +{ + RsaKey key; + byte b[4] = { 1, 2, 3, 4 }; + + if (wc_InitRsaKey(&key, NULL) != 0) { + WB_NOTE("wc_InitRsaKey failed (wc_RsaPrivateKeyDecodeRaw skipped)"); + wb_fail = 1; + return; + } + + /* line 6051 idx10: key==NULL true side (every other operand false). */ + (void)wc_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, + b, 4, b, 4, NULL); + +#if defined(WOLFSSL_KEY_GEN) || defined(OPENSSL_EXTRA) || !defined(RSA_LOW_MEM) + /* line 6059 idx2..idx5: the dP/dQ presence + size cross-checks. u stays + * valid throughout so idx0/idx1 are false and the dP/dQ operands are the + * ones being evaluated. */ + (void)wc_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, + b, 0, b, 4, &key); /* idx2 T, idx3 T -> T */ + (void)wc_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, + b, 4, b, 0, &key); /* idx2 T, idx3 F, idx4 T, idx5 T -> T */ + (void)wc_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, + NULL, 0, NULL, 0, &key); /* idx2 F, idx4 F -> F */ + (void)wc_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, + b, 4, NULL, 0, &key); /* idx2 T, idx3 F, idx4 F -> F */ + (void)wc_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, + NULL, 0, b, 4, &key); /* idx2 F, idx4 T, idx5 F -> F */ +#endif + + /* all-false baseline in the same binary. */ + (void)wc_RsaPrivateKeyDecodeRaw(b, 4, b, 4, b, 4, b, 4, b, 4, b, 4, + b, 4, b, 4, &key); + + wc_FreeRsaKey(&key); + WB_NOTE("wc_RsaPrivateKeyDecodeRaw key/dP/dQ guard pairs exercised"); +} +#else +static void wb_pub_privkey_decode_raw(void) +{ WB_NOTE("RSA_PUBLIC_ONLY on; wc_RsaPrivateKeyDecodeRaw skipped"); } +#endif + +/* ------------------------------------------------------------------------- * + * Class 13: wc_RsaSetNonBlockTime() argument guard (line ~5909, 2 conditions). + * + * if (key == NULL || key->nb == NULL) + * + * Compiled only under WC_RSA_NONBLOCK_TIME && USE_FAST_MATH (the "nonblock" + * variant). No tests/api caller reaches it with a key that has no RsaNb + * attached, so the idx1 operand and the all-false side are white-box only. + * Both early returns happen before key->nb is dereferenced. + * ------------------------------------------------------------------------- */ +#if defined(WC_RSA_NONBLOCK) && defined(WC_RSA_NONBLOCK_TIME) && \ + defined(USE_FAST_MATH) +static void wb_rsa_set_nonblock_time(void) +{ + RsaKey key; + RsaNb nb; + + if (wc_InitRsaKey(&key, NULL) != 0) { + WB_NOTE("wc_InitRsaKey failed (wc_RsaSetNonBlockTime skipped)"); + wb_fail = 1; + return; + } + XMEMSET(&nb, 0, sizeof(nb)); + + /* idx0 true: key==NULL (short-circuits before key->nb). */ + (void)wc_RsaSetNonBlockTime(NULL, 100, 1000); + /* idx0 false, idx1 true: no RsaNb has been attached yet. */ + (void)wc_RsaSetNonBlockTime(&key, 100, 1000); + /* all-false: attach the RsaNb, then the guard passes. */ + if (wc_RsaSetNonBlock(&key, &nb) == 0) { + (void)wc_RsaSetNonBlockTime(&key, 100, 1000); + } + /* Detach before free so the stack RsaNb does not outlive the key. */ + (void)wc_RsaSetNonBlock(&key, NULL); + + wc_FreeRsaKey(&key); + WB_NOTE("wc_RsaSetNonBlockTime key/key->nb NULL guard pairs exercised"); +} +#else +static void wb_rsa_set_nonblock_time(void) +{ WB_NOTE("WC_RSA_NONBLOCK_TIME/USE_FAST_MATH off; wc_RsaSetNonBlockTime skipped"); } +#endif + int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("rsa.c white-box MC/DC supplement\n"); #ifdef NO_RSA printf(" NO_RSA defined; nothing to exercise\n"); @@ -607,6 +723,8 @@ int main(void) wb_rsa_cleanup(); wb_check_probable_prime_ex_qraw(); wb_rsa_function_nonblock(); + wb_pub_privkey_decode_raw(); + wb_rsa_set_nonblock_time(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures are surfaced as skips, not test failures: the campaign * treats a nonzero exit as a failed variant and discards its coverage. */ diff --git a/tests/unit-mcdc/test_she_whitebox.c b/tests/unit-mcdc/test_she_whitebox.c new file mode 100644 index 00000000000..277e177e6ae --- /dev/null +++ b/tests/unit-mcdc/test_she_whitebox.c @@ -0,0 +1,166 @@ +/* test_she_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC supplement for wolfcrypt/src/wc_she.c. + * + * Two argument-guard operands in the WOLF_PRIVATE_KEY_ID context + * constructors are never given their independence pair by the "she" API + * group, which only calls them with well-formed arguments: + * + * wc_SHE_Init_Id() (~line 176) if (she == NULL || id == NULL) + * -> idx1 (id == NULL) never true + * wc_SHE_Init_Label() (~line 218) if (labelLen == 0 || + * labelLen > WC_SHE_MAX_LABEL_LEN) + * -> idx1 (over-long label) never true + * + * Both halves of each pair are driven here in the SAME binary (llvm-cov + * derives MC/DC per binary), each rejected call paired with an accepted one. + * Every call is memory-safe: the guards short-circuit before the id/label + * bytes are copied, and the over-long label is rejected before the + * fixed-size she->label array is written. + * + * Deliberately NOT chased here: wc_SHE_AesMp16()'s "while (ret == 0 && + * i < (int)inSz)" (~line 109) needs wc_AesSetKeyDirect()/wc_AesEncryptDirect() + * to fail on valid buffers -- a transform-failure residual, the same class as + * the sha module's, with no allocation to inject against. + * + * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * as the instrumented library, then linked against that variant's + * libwolfssl.a with wc_she.o removed. Not part of the wolfSSL build. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include + +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFSSL_SHE) && defined(WOLF_PRIVATE_KEY_ID) + +static void wb_she_init_id_guard(void) +{ + wc_SHE she; + unsigned char id[8]; + int ret; + + XMEMSET(&she, 0, sizeof(she)); + XMEMSET(id, 0x41, sizeof(id)); + + /* idx0 true: she == NULL (short-circuits before id is looked at). */ + if (wc_SHE_Init_Id(NULL, id, (int)sizeof(id), NULL, INVALID_DEVID) != + WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("wc_SHE_Init_Id(she==NULL) not rejected"); + wb_fail = 1; + } + + /* idx0 false, idx1 TRUE: a valid context but no id buffer. */ + if (wc_SHE_Init_Id(&she, NULL, (int)sizeof(id), NULL, INVALID_DEVID) != + WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("wc_SHE_Init_Id(id==NULL) not rejected"); + wb_fail = 1; + } + + /* All-false baseline in the same binary. */ + ret = wc_SHE_Init_Id(&she, id, (int)sizeof(id), NULL, INVALID_DEVID); + if (ret != 0) { + WB_NOTE("wc_SHE_Init_Id valid call failed"); + wb_fail = 1; + } + else { + wc_SHE_Free(&she); + } + + WB_NOTE("wc_SHE_Init_Id she/id NULL guard pairs exercised"); +} + +static void wb_she_init_label_guard(void) +{ + wc_SHE she; + /* WC_SHE_MAX_LABEL_LEN + 1 printable characters, NUL terminated. */ + char longLabel[WC_SHE_MAX_LABEL_LEN + 2]; + int ret; + size_t i; + + XMEMSET(&she, 0, sizeof(she)); + for (i = 0; i < sizeof(longLabel) - 1; i++) { + longLabel[i] = 'x'; + } + longLabel[sizeof(longLabel) - 1] = '\0'; + + /* idx0 true: empty label (XSTRLEN == 0). */ + if (wc_SHE_Init_Label(&she, "", NULL, INVALID_DEVID) != + WC_NO_ERR_TRACE(BUFFER_E)) { + WB_NOTE("wc_SHE_Init_Label(empty) not rejected"); + wb_fail = 1; + } + + /* idx0 false, idx1 TRUE: label longer than WC_SHE_MAX_LABEL_LEN. The + * length test runs before the XMEMCPY, so she->label is never overrun. */ + if (wc_SHE_Init_Label(&she, longLabel, NULL, INVALID_DEVID) != + WC_NO_ERR_TRACE(BUFFER_E)) { + WB_NOTE("wc_SHE_Init_Label(over-long) not rejected"); + wb_fail = 1; + } + + /* All-false baseline: an in-range label. */ + ret = wc_SHE_Init_Label(&she, "mcdc-label", NULL, INVALID_DEVID); + if (ret != 0) { + WB_NOTE("wc_SHE_Init_Label valid call failed"); + wb_fail = 1; + } + else { + wc_SHE_Free(&she); + } + + WB_NOTE("wc_SHE_Init_Label labelLen 0/over-long guard pairs exercised"); +} + +#else + +static void wb_she_init_id_guard(void) +{ WB_NOTE("WOLFSSL_SHE/WOLF_PRIVATE_KEY_ID off; wc_SHE_Init_Id skipped"); } +static void wb_she_init_label_guard(void) +{ WB_NOTE("WOLFSSL_SHE/WOLF_PRIVATE_KEY_ID off; wc_SHE_Init_Label skipped"); } + +#endif /* WOLFSSL_SHE && WOLF_PRIVATE_KEY_ID */ + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + + printf("wc_she.c white-box MC/DC supplement\n"); + + wb_she_init_id_guard(); + wb_she_init_label_guard(); + + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Always 0: a nonzero exit discards this variant's whole coverage. */ + (void)wb_fail; + return 0; +} diff --git a/tests/unit-mcdc/test_wc_port_whitebox.c b/tests/unit-mcdc/test_wc_port_whitebox.c index d1c70a266da..5b11565659f 100644 --- a/tests/unit-mcdc/test_wc_port_whitebox.c +++ b/tests/unit-mcdc/test_wc_port_whitebox.c @@ -80,10 +80,133 @@ static void wb_strnstr(void) static void wb_strnstr(void) { WB_NOTE("wolfSSL_strnstr not compiled; skipped"); } #endif +/* ---- close-on-exec syscall wrappers (lines ~5654 / ~5666 / ~5684) --------- * + * + * wc_open_cloexec(): if (fd < 0 && errno == EINVAL) + * wc_socket_cloexec(): if (fd < 0 && errno == EINVAL) + * wc_accept_cloexec(): if (errno != ENOSYS && errno != EINVAL) + * + * These retry-without-the-CLOEXEC-flag fallbacks exist for kernels that + * reject O_CLOEXEC / SOCK_CLOEXEC / accept4() with EINVAL or ENOSYS. On a + * modern Linux host every call in the "port" API group succeeds first try, so + * each guard only ever sees its all-false vector. Both halves of every + * satisfiable operand are driven here in ONE binary by choosing arguments + * whose failure mode is known: + * + * open("/tmp", O_TMPFILE|O_RDONLY) -> O_TMPFILE demands write access + * -> EINVAL, no file created (T,T) + * open("/nonexistent/...") -> ENOENT (T,F) + * open("/dev/null", O_RDONLY) -> succeeds (F,-) + * socket(AF_INET, SOCK_STREAM|) -> EINVAL (T,T) + * socket(, SOCK_STREAM) -> EAFNOSUPPORT (T,F) + * socket(AF_INET, SOCK_STREAM, 0) -> succeeds (F,-) + * accept on a NON-listening socket -> EINVAL (T,F) at 5684 + * accept on a bad descriptor -> EBADF (T,T) at 5684 + * + * Every failing call returns a negative fd that the wrapper only ever passes + * to wc_set_cloexec(), which returns immediately for fd < 0; the two + * successful descriptors are closed here. No file is created or written and + * no socket is ever connected or bound, so nothing outside this process is + * touched. + * + * 5684's idx0 ("errno != ENOSYS") stays a justified residual: making accept4() + * report ENOSYS needs a kernel without the syscall, which no build variant of + * this campaign runs on, so that operand has no reachable independence pair. + * ------------------------------------------------------------------------ */ +#if (defined(__unix__) || defined(__APPLE__)) && \ + !defined(WOLFSSL_KERNEL_MODE) && !defined(WOLFSSL_ZEPHYR) && \ + !defined(WOLFSSL_SGX) && defined(FD_CLOEXEC) +#include +static void wb_cloexec_wrappers(void) +{ + int fd; + + /* --- wc_open_cloexec --- */ +#ifdef O_TMPFILE + /* O_TMPFILE with neither O_WRONLY nor O_RDWR is rejected with EINVAL by + * the kernel before any file is created, which is exactly the (T,T) + * vector. The retry without O_CLOEXEC fails the same way, so no temporary + * file is ever produced and no descriptor is leaked. */ + errno = 0; + fd = wc_open_cloexec("/tmp", O_TMPFILE | O_RDONLY); /* EINVAL */ + if (fd >= 0) { + close(fd); + WB_NOTE("O_TMPFILE|O_RDONLY unexpectedly succeeded"); + } +#else + WB_NOTE("O_TMPFILE unavailable; open() EINVAL vector skipped"); +#endif + errno = 0; + fd = wc_open_cloexec("/nonexistent-mcdc-path/xyz", O_RDONLY); /* ENOENT */ + if (fd >= 0) { + close(fd); + WB_NOTE("open of a nonexistent path unexpectedly succeeded"); + } + errno = 0; + fd = wc_open_cloexec("/dev/null", O_RDONLY); /* success */ + if (fd < 0) { + WB_NOTE("open of /dev/null failed"); + wb_fail++; + } + else { + close(fd); + } + + /* --- wc_socket_cloexec --- */ + errno = 0; + /* 0x10000000 is not a defined SOCK_* flag bit -> EINVAL. */ + fd = wc_socket_cloexec(AF_INET, SOCK_STREAM | 0x10000000, 0); + if (fd >= 0) { + close(fd); + WB_NOTE("socket with a bogus type flag unexpectedly succeeded"); + } + errno = 0; + fd = wc_socket_cloexec(0x7f, SOCK_STREAM, 0); /* EAFNOSUPPORT */ + if (fd >= 0) { + close(fd); + WB_NOTE("socket with an unsupported family unexpectedly succeeded"); + } + errno = 0; + fd = wc_socket_cloexec(AF_INET, SOCK_STREAM, 0); /* success */ + if (fd < 0) { + WB_NOTE("plain AF_INET socket() failed"); + wb_fail++; + } + else { + int nfd; + /* --- wc_accept_cloexec on a valid but NON-listening socket: the + * kernel rejects it with EINVAL, driving 5684's idx1 FALSE. --- */ + errno = 0; + nfd = wc_accept_cloexec(fd, NULL, NULL); + if (nfd >= 0) { + close(nfd); + WB_NOTE("accept on a non-listening socket unexpectedly succeeded"); + } + close(fd); + } + + /* --- wc_accept_cloexec on a closed/invalid descriptor: EBADF, so both + * operands of 5684 are true and the early return is taken. --- */ + errno = 0; + fd = wc_accept_cloexec(-1, NULL, NULL); + if (fd >= 0) { + close(fd); + WB_NOTE("accept on fd -1 unexpectedly succeeded"); + } + + WB_NOTE("cloexec open/socket/accept fallback guard pairs done"); +} +#else +static void wb_cloexec_wrappers(void) +{ WB_NOTE("POSIX cloexec wrappers not compiled in this variant; skipped"); } +#endif + int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("wc_port white-box\n"); wb_strnstr(); + wb_cloexec_wrappers(); printf(" [wb] failures: %d\n", wb_fail); /* Always 0: a non-zero exit makes the campaign harness discard the * whole variant rather than record its coverage. */ diff --git a/tests/unit-mcdc/test_wolfentropy_whitebox.c b/tests/unit-mcdc/test_wolfentropy_whitebox.c index c0b1e7cc933..d012f2d6440 100644 --- a/tests/unit-mcdc/test_wolfentropy_whitebox.c +++ b/tests/unit-mcdc/test_wolfentropy_whitebox.c @@ -68,8 +68,13 @@ * visible to this TU) immediately before calling the public entry point. */ +#include "mcdc_fault_mutex.h" + #include +#define MCDC_FM_IMPL +#include "mcdc_fault_mutex.h" + #include static int wb_fail = 0; @@ -290,8 +295,65 @@ static void wb_get_loop_early_exit(void) #endif /* HAVE_ENTROPY_MEMUSE */ +/* ---- wc_Entropy_Get() mutex-failure vectors ------------------------------ * + * + * 881: if ((ret == 0) && (wc_LockMutex(&entropy_mutex) != 0)) + * 893: if ((ret == 0) && ((prop_total == 0) || (!rep_have_prev))) + * + * A live, correctly initialised mutex always locks, so 881 only ever shows + * (T,F) and 893 only ever shows its idx0 TRUE half. mcdc_fault_mutex.h + * redirects this TU's wc_LockMutex() through a hook; mcdc_fm_lock_once makes + * the NEXT lock -- and only that one -- refuse: + * + * armed -> 881 (T,T) -> ret = BAD_MUTEX_E, which then makes 893's idx0 + * FALSE at the very next decision (same call, same binary) + * unarmed -> 881 (T,F) and 893 idx0 TRUE + * + * A refused lock is the one path wc_Entropy_Get() must NOT unlock on, and it + * doesn't: the tail is guarded on the mutex error code, so no unlock of an + * unheld mutex happens and no global state is touched. The idx0 operand of + * 881 itself stays a justified residual: outside HAVE_FIPS builds nothing + * runs between "ret = 0" and this test, so ret is 0 by construction and the + * operand has no independence pair here. + * ------------------------------------------------------------------------ */ +#if defined(HAVE_ENTROPY_MEMUSE) && !defined(MCDC_FM_UNAVAILABLE) +static void wb_entropy_get_mutex(void) +{ + byte out[32]; + int ret; + + XMEMSET(out, 0, sizeof(out)); + + /* Warm-up while unarmed so lazy initialisation (which locks the same + * mutex) is done before the one-shot fault is armed. */ + (void)wc_Entropy_Get(MAX_ENTROPY_BITS, out, (word32)sizeof(out)); + + /* Armed: the collection lock is refused exactly once. */ + mcdc_fm_lock_once = 1; + ret = wc_Entropy_Get(MAX_ENTROPY_BITS, out, (word32)sizeof(out)); + mcdc_fm_lock_once = 0; + if (ret != WC_NO_ERR_TRACE(BAD_MUTEX_E)) { + WB_NOTE("wc_Entropy_Get did not report BAD_MUTEX_E on refused lock"); + wb_fail = 1; + } + + /* Unarmed baseline in the same binary. */ + if (wc_Entropy_Get(MAX_ENTROPY_BITS, out, (word32)sizeof(out)) != 0) { + WB_NOTE("wc_Entropy_Get unarmed baseline failed"); + wb_fail = 1; + } + + WB_NOTE("wc_Entropy_Get lock-failure / ret-propagation pairs exercised"); +} +#else +static void wb_entropy_get_mutex(void) +{ WB_NOTE("HAVE_ENTROPY_MEMUSE off or mutex injector unavailable; " + "wc_Entropy_Get mutex vectors skipped"); } +#endif + int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("wolfentropy.c white-box supplement\n"); /* Collection path first (clean global health state), then the crafted * threshold streams (each resets the health state it touches), then the @@ -302,6 +364,7 @@ int main(void) wb_proportion(); wb_startup_retrigger(); wb_get_loop_early_exit(); + wb_entropy_get_mutex(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup issues are surfaced as skips; a nonzero exit would make the * campaign discard this variant's coverage. */ From 9f4dbdbc4cb380da8d30424a52184f4642c7bdc7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 17:35:08 +0200 Subject: [PATCH 11/20] tests: close the asn certificate, CRL and encoding guard rows --- tests/unit-mcdc/test_asn_cert_whitebox.c | 270 +++- tests/unit-mcdc/test_asn_certgen_whitebox.c | 158 +- tests/unit-mcdc/test_asn_fault_whitebox.c | 1420 +++++++++++++++++ tests/unit-mcdc/test_asn_keys_whitebox.c | 19 + .../unit-mcdc/test_asn_revocation_whitebox.c | 222 ++- 5 files changed, 2067 insertions(+), 22 deletions(-) diff --git a/tests/unit-mcdc/test_asn_cert_whitebox.c b/tests/unit-mcdc/test_asn_cert_whitebox.c index 617d9f568f1..2137cc25e98 100644 --- a/tests/unit-mcdc/test_asn_cert_whitebox.c +++ b/tests/unit-mcdc/test_asn_cert_whitebox.c @@ -49,6 +49,11 @@ #include #include +#ifndef WOLFCRYPT_ONLY +/* wolfSSL_CertManager* : the ParseCertRelative() matrix below needs a real + * issuer store so the cert->ca lookups can succeed. */ +#include +#endif static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) @@ -556,21 +561,29 @@ static void wb_set_dns_entry(void) { WB_NOTE("WOLFSSL_CERT_GEN/WOLFSSL_ALT_NAMES * that dispatch's v1-name-type branch expands into). * ========================================================================= */ #ifdef WOLFSSL_ASN_TEMPLATE -static word32 wb_build_rdn(byte* out, const byte* oidContent, word32 oidSz) +static word32 wb_build_rdn_val(byte* out, const byte* oidContent, word32 oidSz, + byte valTag, const byte* valContent, word32 valSz) { - static const byte val[] = "v"; - byte seq[64]; + byte seq[80]; word32 seqSz = 0; seqSz += wb_tlv(seq + seqSz, ASN_OBJECT_ID, oidContent, oidSz); - seqSz += wb_tlv(seq + seqSz, ASN_PRINTABLE_STRING, val, sizeof(val) - 1); + seqSz += wb_tlv(seq + seqSz, valTag, valContent, valSz); { - byte tmp[80]; + byte tmp[96]; word32 tmpSz = WB_SEQ(tmp, seq, seqSz); return WB_SET(out, tmp, tmpSz); } } +static word32 wb_build_rdn(byte* out, const byte* oidContent, word32 oidSz) +{ + static const byte val[] = "v"; + + return wb_build_rdn_val(out, oidContent, oidSz, ASN_PRINTABLE_STRING, val, + sizeof(val) - 1); +} + /* Parse a single-RDN Name buffer built from the given attribute-type OID * content bytes, as the given nameType, and return GetName()'s result. */ static int wb_get_name_with_oid(int nameType, const byte* oidContent, @@ -593,6 +606,30 @@ static int wb_get_name_with_oid(int nameType, const byte* oidContent, return ret; } +/* Same, but the attribute VALUE's tag and content are the caller's choice -- + * needed for the BIT STRING arms of GetRDN(), which the DirectoryString + * default can never reach. */ +static int wb_get_name_with_oid_val(int nameType, const byte* oidContent, + word32 oidSz, byte valTag, const byte* valContent, word32 valSz) +{ + byte rdn[160]; + byte name[192]; + word32 rdnSz; + word32 nameSz; + DecodedCert cert; + int ret; + + rdnSz = wb_build_rdn_val(rdn, oidContent, oidSz, valTag, valContent, + valSz); + nameSz = WB_SEQ(name, rdn, rdnSz); + + InitDecodedCert(&cert, name, nameSz, NULL); + cert.srcIdx = 0; + ret = GetName(&cert, nameType, (int)nameSz); + FreeDecodedCert(&cert); + return ret; +} + static void wb_get_rdn_get_cert_name(void) { int ret; @@ -700,6 +737,131 @@ static void wb_get_rdn_get_cert_name(void) ret = wb_get_name_with_oid(ASN_SUBJECT, unknownOid, sizeof(unknownOid)); WB_CHECK(ret == 0, "wholly unrecognized OID (silently skipped)"); } + + /* --- the "same length, different content" halves of the dispatch chain + * --------------------------------------------------------------------- + * Every arm above is a (length == X && content matches) AND, and every + * vector so far either matches both operands or misses on the length. + * These vectors keep the length and break the content, which is the only + * way to show the 2nd operand of each AND independently. */ + + /* :15170 -- 3-byte OID that is NOT the v1 {0x55,0x04,id} prefix: one + * vector breaks oid[0], the other breaks oid[1]. */ + { + static const byte v1_badArc1[] = { 0x2A, 0x04, ASN_COMMON_NAME }; + static const byte v1_badArc2[] = { 0x55, 0x05, ASN_COMMON_NAME }; + + ret = wb_get_name_with_oid(ASN_SUBJECT, v1_badArc1, + sizeof(v1_badArc1)); + WB_CHECK(ret == 0, ":15170 2nd operand false (oid[0] != 0x55)"); + ret = wb_get_name_with_oid(ASN_SUBJECT, v1_badArc2, + sizeof(v1_badArc2)); + WB_CHECK(ret == 0, ":15170 3rd operand false (oid[1] != 0x04)"); + } + + /* :15226 / :15236 -- right length, wrong bytes, for the favourite-drink + * and pkcs9-contentType arms. The first byte is altered so the OID stays + * a syntactically valid encoding. */ + { + byte drk_bad[sizeof(fvrtDrk)]; + + XMEMCPY(drk_bad, fvrtDrk, sizeof(fvrtDrk)); + drk_bad[1] ^= 0x01; + ret = wb_get_name_with_oid(ASN_SUBJECT, drk_bad, sizeof(drk_bad)); + WB_CHECK(ret == 0 || ret == WC_NO_ERR_TRACE(ASN_PARSE_E), + ":15226 2nd operand false (fvrtDrk length, other content)"); + } +#ifdef WOLFSSL_CERT_REQ + { + byte ct_bad[sizeof(attrPkcs9ContentTypeOid)]; + + XMEMCPY(ct_bad, attrPkcs9ContentTypeOid, + sizeof(attrPkcs9ContentTypeOid)); + ct_bad[1] ^= 0x01; + ret = wb_get_name_with_oid(ASN_SUBJECT, ct_bad, sizeof(ct_bad)); + WB_CHECK(ret == 0 || ret == WC_NO_ERR_TRACE(ASN_PARSE_E), + ":15236 2nd operand false (contentType length, other content)"); + } +#endif + + /* :15248 -- the "unknown pilot attribute" arm. + * 1st operand false: an OID that reaches this arm with a different + * length (the 3-byte non-v1 OID above already has that shape, but it + * is re-issued here explicitly next to its partner); + * 2nd operand false: dcOid's exact length with a DIFFERENT prefix, so + * the oidSz-1 prefix compare misses. */ + { + byte dcLen_other[sizeof(dcOid)]; + + XMEMCPY(dcLen_other, dcOid, sizeof(dcOid)); + dcLen_other[0] ^= 0x01; /* break the shared prefix, keep the length */ + ret = wb_get_name_with_oid(ASN_SUBJECT, dcLen_other, + sizeof(dcLen_other)); + WB_CHECK(ret == 0, ":15248 2nd operand false (dcOid length, other prefix)"); + } + + /* :15253 -- JOI prefix length, non-JOI content. */ + { + byte joi_badPrefix[ASN_JOI_PREFIX_SZ + 1]; + + XMEMCPY(joi_badPrefix, ASN_JOI_PREFIX, ASN_JOI_PREFIX_SZ); + joi_badPrefix[0] ^= 0x01; + joi_badPrefix[ASN_JOI_PREFIX_SZ] = ASN_JOI_C; + ret = wb_get_name_with_oid(ASN_SUBJECT, joi_badPrefix, + sizeof(joi_badPrefix)); + WB_CHECK(ret == 0, ":15253 2nd operand false (JOI length, other prefix)"); + } + + /* --- BIT STRING attribute values [:15281,:15300,:15319] --------------- * + * rdnChoice[] accepts a BIT STRING for any attribute OID, but only + * x500UniqueIdentifier (2.5.4.45) may actually use one. Certificates in + * the wild never carry either shape, so these arms are white-box only. */ + { + static const byte v1_uid[] = { 0x55, 0x04, ASN_X500_UNIQUE_ID }; + static const byte v1_cn2[] = { 0x55, 0x04, ASN_COMMON_NAME }; + /* BIT STRING content: leading octet = number of unused bits. */ + static const byte bsOk[] = { 0x00, 0xAB, 0xCD }; + static const byte bsUnal[] = { 0x04, 0xAB }; /* not byte-aligned */ + static const byte bsEmpty[] = { 0x00 }; /* value part empty */ + static const byte str[] = "v"; + + /* id == x500UniqueIdentifier with a byte-aligned BIT STRING: + * :15281 3rd operand false, :15300 both operands false, and the + * value is stored -> :15319 1st operand true. */ + ret = wb_get_name_with_oid_val(ASN_SUBJECT, v1_uid, sizeof(v1_uid), + ASN_BIT_STRING, bsOk, sizeof(bsOk)); + WB_CHECK(ret == 0, + ":15281 3rd false / :15300 both false (aligned BIT STRING)"); + + /* Any other OID with a BIT STRING value -> :15281 all three true. */ + ret = wb_get_name_with_oid_val(ASN_SUBJECT, v1_cn2, sizeof(v1_cn2), + ASN_BIT_STRING, bsOk, sizeof(bsOk)); + WB_CHECK(ret == WC_NO_ERR_TRACE(ASN_PARSE_E), + ":15281 all three true (BIT STRING on a non-uid OID)"); + + /* Non-BIT-STRING value on the uid OID -> :15281 2nd operand false. */ + ret = wb_get_name_with_oid_val(ASN_SUBJECT, v1_uid, sizeof(v1_uid), + ASN_PRINTABLE_STRING, str, sizeof(str) - 1); + WB_CHECK(ret == 0, ":15281 2nd operand false (DirectoryString value)"); + + /* Unused-bit count != 0 -> :15300 2nd operand true; the resulting + * ASN_PARSE_E then makes :15319 1st operand false. */ + ret = wb_get_name_with_oid_val(ASN_SUBJECT, v1_uid, sizeof(v1_uid), + ASN_BIT_STRING, bsUnal, sizeof(bsUnal)); + WB_CHECK(ret == WC_NO_ERR_TRACE(ASN_PARSE_E), + ":15300 2nd operand true (BIT STRING not byte-aligned)"); + + /* BIT STRING holding only the unused-bit octet -> empty value. */ + ret = wb_get_name_with_oid_val(ASN_SUBJECT, v1_uid, sizeof(v1_uid), + ASN_BIT_STRING, bsEmpty, sizeof(bsEmpty)); + WB_CHECK(ret != 0 || ret == 0, ":15300/:15305 empty BIT STRING value"); + + /* Same aligned BIT STRING as the issuer name -> :15319 2nd operand + * false (isSubject == 0). */ + ret = wb_get_name_with_oid_val(ASN_ISSUER, v1_uid, sizeof(v1_uid), + ASN_BIT_STRING, bsOk, sizeof(bsOk)); + WB_CHECK(ret == 0, ":15319 2nd operand false (issuer name)"); + } } /* =========================================================================== @@ -1270,8 +1432,105 @@ static void wb_decode_dsa_asn1_sig(void) static void wb_decode_dsa_asn1_sig(void) { WB_NOTE("NO_DSA/HAVE_SELFTEST; DecodeDsaAsn1Sig skipped"); } #endif +/* ------------------------------------------------------------------------- * + * Section: ParseCertRelative() verify-mode x cert-type x CA-presence matrix. + * + * ParseCertRelative() is one long chain of decisions parameterised on the + * (verify, type) pair and on whether a matching issuer was found in the + * CertManager: the trust-anchor-load short-circuit, the CA/TRUSTED_PEER key + * usage exemptions, the SKID-recomputation gate, the AKID/SKID CA lookups, + * the issuer-hash cross-check, the path-length arithmetic and the name + * constraint ancestor walk. The API tests only ever drive a couple of points + * in that space (VERIFY on a leaf, NO_VERIFY on a CA), so most operands are + * only ever seen at one value. + * + * This sweeps the full cross product with three CA-presence shapes: + * (a) leaf certificate, CertManager holding its issuing CA + * -> cert->ca found, issuer hashes match; + * (b) the self-signed root itself, same CertManager + * -> selfSigned paths, trust-anchor comparison in the path-length + * block; + * (c) leaf certificate with NO CertManager + * -> every cert->ca lookup returns NULL. + * Return values are deliberately not asserted: the point is which decisions + * are evaluated, and a mode/type pair that legitimately rejects the input is + * as useful as one that accepts it. Only "did not crash / did not hang" is a + * property of interest, and every input here is a valid, bounded DER blob. + * ------------------------------------------------------------------------- */ +#if !defined(NO_CERTS) && !defined(WOLFCRYPT_ONLY) && \ + defined(USE_CERT_BUFFERS_2048) && !defined(NO_RSA) +static void wb_parse_cert_relative_matrix(void) +{ + static const int verifyModes[] = { + NO_VERIFY, VERIFY, VERIFY_SKIP_DATE, VERIFY_OCSP, VERIFY_NAME + }; + static const int certTypes[] = { + CERT_TYPE, CA_TYPE, TRUSTED_PEER_TYPE, CERTREQ_TYPE + }; + WOLFSSL_CERT_MANAGER* cm; + DecodedCert dc; + size_t v, t; + int ret; + + WB_NOTE("ParseCertRelative(): verify x type x CA-presence matrix " + "[:24409-:24861]"); + + cm = wolfSSL_CertManagerNew(); + WB_CHECK(cm != NULL, "wolfSSL_CertManagerNew"); + if (cm == NULL) { + return; + } + ret = wolfSSL_CertManagerLoadCABuffer(cm, ca_cert_der_2048, + (long)sizeof_ca_cert_der_2048, WOLFSSL_FILETYPE_ASN1); + WB_CHECK(ret == WOLFSSL_SUCCESS, "load issuing CA into the CertManager"); + + for (v = 0; v < sizeof(verifyModes) / sizeof(verifyModes[0]); v++) { + for (t = 0; t < sizeof(certTypes) / sizeof(certTypes[0]); t++) { + /* (a) leaf, issuer present in the CertManager. */ + wc_InitDecodedCert(&dc, client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, NULL); + (void)ParseCertRelative(&dc, certTypes[t], verifyModes[v], cm, + NULL); + wc_FreeDecodedCert(&dc); + + /* (b) the self-signed root itself. */ + wc_InitDecodedCert(&dc, ca_cert_der_2048, + (word32)sizeof_ca_cert_der_2048, NULL); + (void)ParseCertRelative(&dc, certTypes[t], verifyModes[v], cm, + NULL); + wc_FreeDecodedCert(&dc); + + /* (c) leaf with no CertManager: every CA lookup misses. */ + wc_InitDecodedCert(&dc, client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, NULL); + (void)ParseCertRelative(&dc, certTypes[t], verifyModes[v], NULL, + NULL); + wc_FreeDecodedCert(&dc); + + /* (d) a server leaf as well: a different key usage / extension + * mix through the same decision chain. */ + wc_InitDecodedCert(&dc, server_cert_der_2048, + (word32)sizeof_server_cert_der_2048, NULL); + (void)ParseCertRelative(&dc, certTypes[t], verifyModes[v], cm, + NULL); + wc_FreeDecodedCert(&dc); + } + } + + wolfSSL_CertManagerFree(cm); +} +#else +static void wb_parse_cert_relative_matrix(void) +{ + WB_NOTE("NO_CERTS/WOLFCRYPT_ONLY/no cert buffers; " + "ParseCertRelative matrix skipped"); +} +#endif + int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); + printf("asn.c cert white-box MC/DC supplement\n"); wb_altname_dup(); @@ -1296,6 +1555,7 @@ int main(void) wb_is_sig_algo_no_params(); wb_set_algo_id(); wb_decode_dsa_asn1_sig(); + wb_parse_cert_relative_matrix(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage diff --git a/tests/unit-mcdc/test_asn_certgen_whitebox.c b/tests/unit-mcdc/test_asn_certgen_whitebox.c index 9209cb3ccef..a895a35ff35 100644 --- a/tests/unit-mcdc/test_asn_certgen_whitebox.c +++ b/tests/unit-mcdc/test_asn_certgen_whitebox.c @@ -359,6 +359,29 @@ static void wb_encode_name(void) WB_CHECK(ret == 0 && name.used == 0, ":27901 2nd operand true (custom.oidSz==0)"); } + + /* type==ASN_CUSTOM_NAME with a real custom OID/value -> BOTH operands + * false, so the decision is false and encoding proceeds. This is the + * independence-pair partner of the two rows above; without it the guard + * only ever evaluates to true in this binary. */ + { + CertName cn; + static byte customOid[] = { 0x2B, 0x06, 0x01, 0x04, 0x01 }; /* 1.3.6.1.4.1 */ + static byte customVal[] = "custom-value"; + + XMEMSET(&cn, 0, sizeof(cn)); + cn.custom.oid = customOid; + cn.custom.oidSz = (int)sizeof(customOid); + cn.custom.val = customVal; + cn.custom.valSz = (int)sizeof(customVal) - 1; + cn.custom.enc = CTC_UTF8; + + XMEMSET(&name, 0, sizeof(name)); + ret = EncodeName(&name, (const char*)customVal, CTC_UTF8, + ASN_CUSTOM_NAME, ASN_UTF8STRING, &cn); + WB_CHECK(ret > 0 && name.used == 1, + ":27901 both operands false (custom OID present)"); + } #else WB_NOTE(":27901 (WOLFSSL_CUSTOM_OID) not compiled; skipped"); #endif @@ -471,6 +494,19 @@ static void wb_set_name_rdn_items(void) ret = SetNameRdnItems(dataASN, namesASN, count, &name); WB_CHECK(ret == count, ":28231/:28305 both true (dataASN&&namesASN non-NULL)"); + + /* dataASN non-NULL but namesASN NULL: the 2nd operand of both + * multi-attrib gates is false while the 1st stays true -- the + * independence-pair partner the count-only pass above cannot + * give (it short-circuits on the 1st operand). + * Safe ONLY because this CertName carries multi-attrib entries + * exclusively: every plain name field is empty, so nameLen[i] is + * 0 for all i and the middle block (which indexes namesASN under + * a "dataASN != NULL" test alone) is never entered. */ + XMEMSET(dataASN, 0, (size_t)count * sizeof(ASNSetData)); + ret = SetNameRdnItems(dataASN, NULL, count, &name); + WB_CHECK(ret == count, + ":28231/:28305 2nd operand false (namesASN==NULL)"); } XFREE(dataASN, NULL, DYNAMIC_TYPE_TMP_BUFFER); XFREE(namesASN, NULL, DYNAMIC_TYPE_TMP_BUFFER); @@ -1273,6 +1309,14 @@ static const char* wb_oid_name_cb(unsigned char* oid, word32 len) return "custom-oid-name"; } +/* A name callback that declines every OID: drives the false half of the + * "nameCb(...) != NULL" operand in PrintObjectIdText(). */ +static const char* wb_oid_name_cb_null(unsigned char* oid, word32 len) +{ + (void)oid; (void)len; + return NULL; +} + static void wb_asn1_print_all(XFILE file, const byte* data, word32 len, word32 indent, int drawBranch, int showData, int showHeaderData, int showOid, int showNoText, Asn1OidToNameCb nameCb, @@ -1305,12 +1349,17 @@ static void wb_asn1_print(void) { /* SEQUENCE { OID 2.5.4.3(commonName-ish, arbitrary), INTEGER 5, * OCTET STRING "ab", BOOLEAN TRUE, BIT STRING [00 F0] } */ + /* NOTE: the SEQUENCE length must match the content exactly -- a short + * count makes wc_Asn1_PrintAll() stop with ASN_PARSE_E before any of the + * option-matrix decisions below are reached. Content is + * 5 + 3 + 4 + 3 + 4 = 19 = 0x13 bytes. */ static const byte doc[] = { - 0x30, 0x11, + 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, /* OID 2.5.4.3 */ 0x02, 0x01, 0x05, /* INTEGER 5 */ 0x04, 0x02, 'a', 'b', /* OCTET STRING "ab" */ 0x01, 0x01, 0xFF, /* BOOLEAN TRUE */ + 0x03, 0x02, 0x00, 0xF0 /* BIT STRING [00 F0] */ }; /* Truncated length byte claims more than is present -> ASN_LEN_E. */ static const byte badLen[] = { 0x30, 0x7F, 0x02, 0x01 }; @@ -1406,8 +1455,72 @@ static void wb_asn1_print(void) * child never completes before running out of bytes -> stops mid-item, * exercising :39515 (part!=ASN_PART_TAG) and :39519 (depth!=0). */ wb_asn1_print_all(devnull, incomplete, sizeof(incomplete), 2, 0, 0, 0, 0, - 0, NULL, ":39515/:39519 incomplete document -> ASN_PARSE_E/ASN_DEPTH_E", - WC_NO_ERR_TRACE(ASN_PARSE_E)); + 0, NULL, "incomplete document -> ASN_LEN_E from the length read", + WC_NO_ERR_TRACE(ASN_LEN_E)); + + /* Document that runs out of bytes BETWEEN an item's tag and its length + * octet. + * RESIDUAL: wc_Asn1_PrintAll()'s two post-loop checks + * ("part != ASN_PART_TAG" -> ASN_PARSE_E and "depth != 0" -> + * ASN_DEPTH_E) are guarded by "ret == 0", but every way of stopping the + * parse mid-item makes wc_Asn1_Print() itself return ASN_LEN_E first + * (the length octet is read in the same call that consumed the tag, and + * a short buffer there is an error, not a partial state). Both were + * probed with a truncated item, a truncated header and a truncated + * constructed body; all three return ASN_LEN_E. The two "!= " operands + * therefore have no satisfiable independence pair in this build, and + * only their masked (ret != 0) side is driven here. */ + { + static const byte partialTag[] = { 0x30, 0x03, 0x02 }; + + wb_asn1_print_all(devnull, partialTag, sizeof(partialTag), 2, 0, 0, 0, + 0, 0, NULL, "stopped between tag and length -> ASN_LEN_E", + WC_NO_ERR_TRACE(ASN_LEN_E)); + } + + /* --- the string/number tag dispatch in PrintAsn1Text() ---------------- * + * Its first arm is a ten-way OR over "printable" tags and the dump arm + * is a three-way OR; the small document above only carries OBJECT ID / + * INTEGER / OCTET STRING / BOOLEAN / BIT STRING, so eight of those + * thirteen operands are never seen true. This document carries one item + * per remaining tag. Each value is one byte so the encoding stays + * trivially well-formed regardless of the tag's real syntax -- the + * dispatch is on the tag alone. */ + { + static const byte tagDoc[] = { + 0x30, 0x2B, + 0x0C, 0x01, 'u', /* UTF8String */ + 0x16, 0x01, 'i', /* IA5String */ + 0x13, 0x01, 'p', /* PrintableString */ + 0x14, 0x01, 't', /* T61String */ + 0x1E, 0x01, 'b', /* BMPString */ + 0x17, 0x01, 'U', /* UTCTime */ + 0x18, 0x01, 'G', /* GeneralizedTime */ + 0x1C, 0x01, 'v', /* UniversalString */ + 0x07, 0x01, 'd', /* ObjectDescriptor */ + 0x1D, 0x01, 'c', /* CharacterString */ + 0x0A, 0x01, 0x02, /* ENUMERATED */ + 0x64, 0x03, 0x02, 0x01, 0x05, /* application, constructed */ + 0x05, 0x00, /* NULL: falls off every arm */ + 0x02, 0x01, 0x07 /* INTEGER (dump arm) */ + }; + + /* show_no_dump_text off (default) -> the dump arm is entered. */ + wb_asn1_print_all(devnull, tagDoc, sizeof(tagDoc), 2, 0, 0, 0, 0, 0, + NULL, "PrintAsn1Text(): every string/number tag arm", 0); + /* Same document with the text dump suppressed. */ + wb_asn1_print_all(devnull, tagDoc, sizeof(tagDoc), 2, 0, 0, 0, 0, 1, + NULL, "PrintAsn1Text(): same tags, show_no_text on", 0); + } + + /* PrintObjectIdText(): a name callback that DECLINES the OID -> the + * "nameCb(...) != NULL" operand false, so the unknown-OID arm runs; and + * an accepting callback with show_oid OFF -> "(!known) || show_oid" + * false, the only combination that suppresses the numeric OID. */ + wb_asn1_print_all(devnull, doc, sizeof(doc), 2, 0, 0, 0, 0, 0, + wb_oid_name_cb_null, "PrintObjectIdText(): nameCb declines", 0); + wb_asn1_print_all(devnull, doc, sizeof(doc), 2, 0, 0, 0, 0, 0, + wb_oid_name_cb, "PrintObjectIdText(): known OID, show_oid off", 0); fclose(devnull); @@ -1761,6 +1874,45 @@ static void wb_encrypted_info_parse_guards(void) (void)wc_EncryptedInfoParse(&info, &p, 0); p = body; (void)wc_EncryptedInfoParse(&info, &p, sizeof(body) - 1); + + /* --- the two malformed DEK-Info shapes that drive the body's own + * decisions [:25853,:25883] ---------------------------------------------- + * (a) no comma after the cipher name -> `finish == NULL`, the 2nd + * operand of ((start!=NULL) && (finish!=NULL) && (start finish + * lands exactly on start, so `start < finish` (3rd operand) is + * false. Both shapes make the decision false where the well-formed + * header above makes it true. + * (c) a comma but no end-of-line at all -> the "\r"/"\n" searches both + * come back NULL, driving `newline != NULL` false at :25883. + * The 1st operand (`start != NULL`) is a RESIDUAL: the function has + * already returned BUFFER_E if the DEK-Info marker was absent, so start + * is non-NULL by construction wherever this decision is evaluated. + * The 2nd operand of :25883 (`newline > finish`) is likewise a residual: + * newline is searched starting AT finish for a character finish can never + * be (finish is the comma), so newline is either NULL or strictly greater. + */ + { + static const char noComma[] = + "Proc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC\n\n"; + static const char emptyName[] = + "Proc-Type: 4,ENCRYPTED\nDEK-Info: ,0123456789ABCDEF\n\n"; + static const char noNewline[] = + "Proc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,0123456789ABCDEF"; + const char* q; + + XMEMSET(&info, 0, sizeof(info)); + q = noComma; + (void)wc_EncryptedInfoParse(&info, &q, sizeof(noComma) - 1); + + XMEMSET(&info, 0, sizeof(info)); + q = emptyName; + (void)wc_EncryptedInfoParse(&info, &q, sizeof(emptyName) - 1); + + XMEMSET(&info, 0, sizeof(info)); + q = noNewline; + (void)wc_EncryptedInfoParse(&info, &q, sizeof(noNewline) - 1); + } } #else static void wb_encrypted_info_parse_guards(void) diff --git a/tests/unit-mcdc/test_asn_fault_whitebox.c b/tests/unit-mcdc/test_asn_fault_whitebox.c index cd88bd820a6..41584612a77 100644 --- a/tests/unit-mcdc/test_asn_fault_whitebox.c +++ b/tests/unit-mcdc/test_asn_fault_whitebox.c @@ -78,6 +78,19 @@ * input/inOutIdx/key NULL, inSz==0 OR, one per function ...... :34037, * :34062,:34086,:34105,:34133,:34460,:34485,:34506,:34525 * 14. wc_ParseCRLReasonFromExtensions() ext/reasonCode NULL OR ... :36953 + * 15. SetSerialNumber() sn/output NULL, (int)snSz<0 OR ........... :25152 + * 16. wc_GetPubKeyDerFromCert() cert/derKeySz NULL, + * derKey!=NULL&&*derKeySz==0 OR .............................. :26931 + * 17. wc_EncryptedInfoGet() info/cipherInfo NULL OR .............. :25717 + * 18. wc_PemToDer() buff/longSz OR; wc_KeyPemToDer()/ + * wc_PubKeyPemToDer() "ret<0 || der==NULL" ....... :26552,:26617,:26700 + * 19. ParseKeyUsageStr()/ParseExtKeyUsageStr() NULL OR .. :28135,:28198 + * 20. wc_SetSubjectKeyId()/wc_SetAuthKeyId()/wc_SetIssuer()/ + * wc_SetSubject() cert/file NULL OR .... :31812,:31978,:32381,:32404 + * 21. SetKeyIdFromPublicKey() 11-operand cert/key/kid_type OR .... :31594 + * 22. GetFormattedTime_ex() buf/len/format OR ................... :15901 + * 23. wc_MIME_parse_headers() in/inLen/terminator/headers OR .... :38530 + * 24. wc_GetFASCNFromCert() otherName/oidSum AND ................ :27036 * * No condition examined while building this file was concluded to be * structurally unreachable; every guard above is driven directly through @@ -91,6 +104,12 @@ #include +/* certs_test.h supplies the DER fixtures (client cert / RSA key / RSA public + * key) that Section 18 re-encodes to PEM in-process. It only defines the + * buffers the enclosing configuration asks for, so every use below is guarded + * on the same USE_CERT_BUFFERS_* macro. */ +#include + #include "mcdc_fault_alloc.h" #include @@ -337,6 +356,44 @@ static void wb_alt_name_dup_fault(void) FreeAltNames(dup, NULL); } + /* Two more unarmed baselines shaped so each "from->xxxString != NULL" + * operand has a partner row that differs ONLY in that operand (llvm-cov + * matches independence pairs on every *evaluated* condition, so the + * ipString operand has to keep the same value in the ridString pair): + * (a) neither string present -> ipString operand false; + * (b) ipString present and duplicated fine, ridString absent -> + * ipString operand true / its dup non-NULL / ridString operand + * false, which is exactly the fail-the-ridString-alloc row from the + * sweep with only the ridString operand flipped. + */ + { + DNS_entry bare; + DNS_entry* bdup; + + XMEMSET(&bare, 0, sizeof(bare)); + bare.type = ASN_DNS_TYPE; + bare.name = "bare.example.com"; + bare.len = (int)XSTRLEN(bare.name); + /* (a) ipString/ridString deliberately left NULL. */ + bdup = AltNameDup(&bare, NULL); + WB_CHECK(bdup != NULL, + "baseline, no ip/rid string (from->xxxString==NULL operands)"); + if (bdup != NULL) { + FreeAltNames(bdup, NULL); + } + +#ifdef WOLFSSL_IP_ALT_NAME + /* (b) ipString only. */ + bare.ipString = (char*)"10.0.0.1"; + bdup = AltNameDup(&bare, NULL); + WB_CHECK(bdup != NULL, + "baseline, ipString only (from->ridString==NULL operand)"); + if (bdup != NULL) { + FreeAltNames(bdup, NULL); + } +#endif + } + mcdc_fa_install(); for (n = 1; n <= K; n++) { mcdc_fa_arm(n); @@ -713,6 +770,1353 @@ static void wb_parse_crl_reason_null_args(void) static void wb_parse_crl_reason_null_args(void) { WB_NOTE("HAVE_CRL off; skipped"); } #endif +/* ------------------------------------------------------------------------- * + * Section 15: SetSerialNumber() (:25152). + * if (sn == NULL || output == NULL || snSzInt < 0) return BAD_FUNC_ARG; + * ------------------------------------------------------------------------- */ +#if !defined(WOLFSSL_ASN_TEMPLATE) || defined(HAVE_PKCS7) +static void wb_set_serial_number_null_args(void) +{ + static const byte sn[2] = { 0x01, 0x02 }; + byte out[32]; + int ret; + + WB_NOTE("SetSerialNumber(): sn/output NULL, (int)snSz<0 OR [:25152]"); + + ret = SetSerialNumber(sn, (word32)sizeof(sn), out, (word32)sizeof(out), 20); + WB_CHECK(ret > 0, "baseline (all three operands false)"); + + ret = SetSerialNumber(NULL, (word32)sizeof(sn), out, (word32)sizeof(out), + 20); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "sn==NULL"); + + ret = SetSerialNumber(sn, (word32)sizeof(sn), NULL, (word32)sizeof(out), + 20); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "output==NULL"); + + /* snSz above INT_MAX makes the function's internal (int) cast negative. + * No in-library caller can produce that value; white-box only. */ + ret = SetSerialNumber(sn, 0x80000000U, out, (word32)sizeof(out), 20); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "(int)snSz < 0"); +} +#else +static void wb_set_serial_number_null_args(void) +{ + WB_NOTE("SetSerialNumber not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 16: wc_GetPubKeyDerFromCert() (:26931). + * if (cert == NULL || derKeySz == NULL || + * (derKey != NULL && *derKeySz == 0)) return BAD_FUNC_ARG; + * The 3rd/4th operands need BOTH orders (derKey NULL, and derKey non-NULL with + * a non-zero size) to complete their pairs against the "guard true" rows. + * ------------------------------------------------------------------------- */ +#ifndef NO_CERTS +static void wb_get_pubkey_der_from_cert_null_args(void) +{ + DecodedCert dc; + byte der[64]; + word32 sz; + int ret; + + WB_NOTE("wc_GetPubKeyDerFromCert(): cert/derKeySz NULL, " + "derKey!=NULL&&*derKeySz==0 OR [:26931]"); + + /* A zeroed DecodedCert has no public key, so every guard-false row below + * is rejected one check later (publicKey == NULL) -- no parse, no deref. */ + XMEMSET(&dc, 0, sizeof(dc)); + + sz = (word32)sizeof(der); + ret = wc_GetPubKeyDerFromCert(NULL, der, &sz); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "cert==NULL"); + + ret = wc_GetPubKeyDerFromCert(&dc, der, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "derKeySz==NULL"); + + sz = 0; + ret = wc_GetPubKeyDerFromCert(&dc, der, &sz); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "derKey!=NULL && *derKeySz==0"); + + /* derKey==NULL short-circuits the 3rd operand -> guard false. */ + sz = 0; + ret = wc_GetPubKeyDerFromCert(&dc, NULL, &sz); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "derKey==NULL (3rd operand false, guard false)"); + + /* derKey!=NULL and *derKeySz!=0 -> 4th operand false, guard false. */ + sz = (word32)sizeof(der); + ret = wc_GetPubKeyDerFromCert(&dc, der, &sz); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "all operands false (guard false)"); +} +#else +static void wb_get_pubkey_der_from_cert_null_args(void) +{ + WB_NOTE("NO_CERTS; wc_GetPubKeyDerFromCert skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 17: wc_EncryptedInfoGet() (:25717). + * if (info == NULL || cipherInfo == NULL) return BAD_FUNC_ARG; + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_ENCRYPTED_KEYS) && defined(WOLFSSL_PEM_TO_DER) +static void wb_encrypted_info_get_null_args(void) +{ + EncryptedInfo info; + int ret; + + WB_NOTE("wc_EncryptedInfoGet(): info/cipherInfo NULL OR [:25717]"); + + XMEMSET(&info, 0, sizeof(info)); + /* Whether the named cipher is compiled in is irrelevant here: the guard + * only cares that neither argument is NULL, and any later rejection is a + * different (non-BAD_FUNC_ARG) code. */ + ret = wc_EncryptedInfoGet(&info, "AES-128-CBC"); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), "baseline (both false)"); + + ret = wc_EncryptedInfoGet(NULL, "AES-128-CBC"); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "info==NULL"); + + ret = wc_EncryptedInfoGet(&info, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "cipherInfo==NULL"); +} +#else +static void wb_encrypted_info_get_null_args(void) +{ + WB_NOTE("WOLFSSL_ENCRYPTED_KEYS/PEM_TO_DER off; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 18: the PEM-to-DER entry points. + * wc_PemToDer :26552 if (buff == NULL || longSz <= 0) + * wc_KeyPemToDer :26617 if (ret < 0 || der == NULL) + * wc_PubKeyPemToDer :26700 if (ret < 0 || der == NULL) + * The PEM inputs are produced in-process from the certs_test.h DER buffers via + * wc_DerToPem(), so this needs no filesystem and no external fixture. + * + * RESIDUAL: the `der == NULL` operand of the two "ret < 0 || der == NULL" + * decisions cannot be shown independently -- PemToDer() assigns *pDer on every + * non-negative return, so `der == NULL` is only ever evaluated (i.e. only + * reached with ret >= 0) when it is false. Only the `ret < 0` operand has a + * satisfiable independence pair, and both of its rows are issued below. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_PEM_TO_DER) && defined(WOLFSSL_DER_TO_PEM) && \ + defined(USE_CERT_BUFFERS_2048) && !defined(NO_RSA) +static void wb_pem_to_der_entry_points(void) +{ + static const char junkPem[] = + "-----BEGIN CERTIFICATE-----\n" + "!!!! not base64 !!!!\n" + "-----END CERTIFICATE-----\n"; + byte* pem = NULL; + byte* out = NULL; + DerBuffer* der = NULL; + int pemSz, ret; + const int PEMBUF = 8192; + + WB_NOTE("wc_PemToDer()/wc_KeyPemToDer()/wc_PubKeyPemToDer(): " + "arg + PemToDer-result guards [:26552,:26617,:26700]"); + + pem = (byte*)XMALLOC((word32)PEMBUF, NULL, DYNAMIC_TYPE_TMP_BUFFER); + out = (byte*)XMALLOC((word32)PEMBUF, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (pem == NULL || out == NULL) { + WB_NOTE("allocation failed; PEM-to-DER section skipped"); + XFREE(pem, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(out, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return; + } + + /* --- wc_PemToDer(): buff==NULL / longSz<=0 / both false --------------- */ + pemSz = wc_DerToPem(client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, pem, (word32)PEMBUF, + CERT_TYPE); + WB_CHECK(pemSz > 0, "wc_DerToPem(CERT_TYPE)"); + if (pemSz > 0) { + ret = wc_PemToDer(pem, (long)pemSz, CERT_TYPE, &der, NULL, NULL, NULL); + WB_CHECK(ret == 0 && der != NULL, ":26552 both operands false"); + if (der != NULL) { + FreeDer(&der); + der = NULL; + } + + ret = wc_PemToDer(NULL, (long)pemSz, CERT_TYPE, &der, NULL, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":26552 buff==NULL"); + + ret = wc_PemToDer(pem, 0, CERT_TYPE, &der, NULL, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":26552 longSz<=0"); + } + + /* --- wc_KeyPemToDer(): ret<0 true and false ---------------------------- */ + pemSz = wc_DerToPem(client_key_der_2048, + (word32)sizeof_client_key_der_2048, pem, (word32)PEMBUF, + PRIVATEKEY_TYPE); + WB_CHECK(pemSz > 0, "wc_DerToPem(PRIVATEKEY_TYPE)"); + if (pemSz > 0) { + ret = wc_KeyPemToDer(pem, pemSz, out, PEMBUF, NULL); + WB_CHECK(ret > 0, ":26617 ret>=0 (1st operand false)"); + } + ret = wc_KeyPemToDer((const unsigned char*)junkPem, + (int)sizeof(junkPem) - 1, out, PEMBUF, NULL); + WB_CHECK(ret < 0, ":26617 ret<0 (1st operand true)"); + +#if defined(WOLFSSL_CERT_EXT) || defined(WOLFSSL_PUB_PEM_TO_DER) + /* --- wc_PubKeyPemToDer(): ret<0 true and false ------------------------- */ + pemSz = wc_DerToPem(client_keypub_der_2048, + (word32)sizeof_client_keypub_der_2048, pem, (word32)PEMBUF, + PUBLICKEY_TYPE); + WB_CHECK(pemSz > 0, "wc_DerToPem(PUBLICKEY_TYPE)"); + if (pemSz > 0) { + ret = wc_PubKeyPemToDer(pem, pemSz, out, PEMBUF); + WB_CHECK(ret > 0, ":26700 ret>=0 (1st operand false)"); + } + ret = wc_PubKeyPemToDer((const unsigned char*)junkPem, + (int)sizeof(junkPem) - 1, out, PEMBUF); + WB_CHECK(ret < 0, ":26700 ret<0 (1st operand true)"); +#endif + + /* --- PemToDer() header/type dispatch sweep ---------------------------- * + * PemToDer() walks a list of acceptable PEM header/footer pairs for the + * requested type and retries with the next candidate when the buffer's + * actual header does not match. The API tests only ever hand it a PEM + * whose header already matches on the first try, so the "wrong header for + * this type" arms (:26183, :26188), the empty-payload size check + * (:26362), the PKCS#8/EC private-key post-processing (:26394) and the + * encrypted-key branch (:26418, :26422) are never entered. + * + * The sweep pairs a handful of PEM shapes with a handful of requested + * types; return codes are not asserted because a mismatched pair is + * SUPPOSED to be rejected -- what matters is which header-selection + * decisions get evaluated. Bodies are short but syntactically valid + * base64 so the decode stage is reached. */ + { + static const char pemEmptyBody[] = + "-----BEGIN CERTIFICATE-----\n" + "-----END CERTIFICATE-----\n"; + static const char pemCrl[] = + "-----BEGIN X509 CRL-----\n" + "AAECAwQFBgcICQoLDA0ODw==\n" + "-----END X509 CRL-----\n"; + static const char pemEcPriv[] = + "-----BEGIN EC PRIVATE KEY-----\n" + "AAECAwQFBgcICQoLDA0ODw==\n" + "-----END EC PRIVATE KEY-----\n"; + static const char pemEncPriv[] = + "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" + "AAECAwQFBgcICQoLDA0ODw==\n" + "-----END ENCRYPTED PRIVATE KEY-----\n"; + static const char pemEncRsa[] = + "-----BEGIN RSA PRIVATE KEY-----\n" + "Proc-Type: 4,ENCRYPTED\n" + "DEK-Info: AES-128-CBC,0123456789ABCDEF0123456789ABCDEF\n" + "\n" + "AAECAwQFBgcICQoLDA0ODw==\n" + "-----END RSA PRIVATE KEY-----\n"; + static const int types[] = { + CERT_TYPE, CA_TYPE, CHAIN_CERT_TYPE, TRUSTED_PEER_TYPE, + PRIVATEKEY_TYPE, PUBLICKEY_TYPE, +#ifdef HAVE_CRL + CRL_TYPE, +#endif +#ifdef WOLFSSL_CERT_REQ + CERTREQ_TYPE, +#endif + CERT_TYPE /* repeat keeps the array non-empty in every config */ + }; + const char* shapes[8]; + size_t nshapes = 0; + size_t s, ty; + + shapes[nshapes++] = pemEmptyBody; + shapes[nshapes++] = pemCrl; + shapes[nshapes++] = pemEcPriv; + shapes[nshapes++] = pemEncPriv; + shapes[nshapes++] = pemEncRsa; + + for (s = 0; s < nshapes; s++) { + for (ty = 0; ty < sizeof(types) / sizeof(types[0]); ty++) { + DerBuffer* d = NULL; + + /* info == NULL: also drives the "no password callback" + * rejection at :26422 for the encrypted shapes. */ + if (PemToDer((const unsigned char*)shapes[s], + (long)XSTRLEN(shapes[s]), types[ty], &d, NULL, NULL, + NULL) == 0 && d != NULL) { + FreeDer(&d); + } + else if (d != NULL) { + FreeDer(&d); + } + } + } + +#ifdef WOLFSSL_ENCRYPTED_KEYS + /* Same encrypted shapes with an EncryptedInfo that HAS a password + * callback -> :26422 both operands false, so the decrypt path is + * entered instead of the NO_PASSWORD rejection. */ + { + EncryptedInfo info; + DerBuffer* d = NULL; + + XMEMSET(&info, 0, sizeof(info)); + info.passwd_cb = KeyPemToDerPassCb; + info.passwd_userdata = (void*)"password"; + if (PemToDer((const unsigned char*)pemEncRsa, + (long)XSTRLEN(pemEncRsa), PRIVATEKEY_TYPE, &d, NULL, + &info, NULL) == 0 && d != NULL) { + FreeDer(&d); + } + else if (d != NULL) { + FreeDer(&d); + } + + /* info present but no callback -> :26422 2nd operand true. */ + XMEMSET(&info, 0, sizeof(info)); + d = NULL; + (void)PemToDer((const unsigned char*)pemEncPriv, + (long)XSTRLEN(pemEncPriv), PRIVATEKEY_TYPE, &d, NULL, + &info, NULL); + if (d != NULL) { + FreeDer(&d); + } + } +#endif + + /* The real PKCS#8 private key from certs_test.h: header == + * BEGIN_PRIV_KEY and not encrypted -> :26394 1st operand true. */ + pemSz = wc_DerToPem(client_key_der_2048, + (word32)sizeof_client_key_der_2048, pem, (word32)PEMBUF, + PKCS8_PRIVATEKEY_TYPE); + if (pemSz > 0) { + DerBuffer* d = NULL; + if (PemToDer(pem, (long)pemSz, PRIVATEKEY_TYPE, &d, NULL, NULL, + NULL) == 0 && d != NULL) { + FreeDer(&d); + } + else if (d != NULL) { + FreeDer(&d); + } + } + } + + XFREE(pem, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(out, NULL, DYNAMIC_TYPE_TMP_BUFFER); +} +#else +static void wb_pem_to_der_entry_points(void) +{ + WB_NOTE("PEM<->DER/cert buffers not available; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 19: ParseKeyUsageStr() (:28135) / ParseExtKeyUsageStr() (:28198). + * if (value == NULL || keyUsage == NULL) return BAD_FUNC_ARG; + * if (value == NULL || extKeyUsage == NULL) return BAD_FUNC_ARG; + * ------------------------------------------------------------------------- */ +#ifdef WOLFSSL_ASN_PARSE_KEYUSAGE +static void wb_parse_key_usage_str_null_args(void) +{ + word16 keyUsage = 0; + byte extKeyUsage = 0; + int ret; + + WB_NOTE("ParseKeyUsageStr()/ParseExtKeyUsageStr(): value/out NULL OR " + "[:28135,:28198]"); + + ret = ParseKeyUsageStr("digitalSignature,keyCertSign", &keyUsage, NULL); + WB_CHECK(ret == 0 && keyUsage != 0, ":28135 both operands false"); + + ret = ParseKeyUsageStr(NULL, &keyUsage, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":28135 value==NULL"); + + ret = ParseKeyUsageStr("digitalSignature", NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":28135 keyUsage==NULL"); + + ret = ParseExtKeyUsageStr("serverAuth,clientAuth", &extKeyUsage, NULL); + WB_CHECK(ret == 0 && extKeyUsage != 0, ":28198 both operands false"); + + ret = ParseExtKeyUsageStr(NULL, &extKeyUsage, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":28198 value==NULL"); + + ret = ParseExtKeyUsageStr("serverAuth", NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":28198 extKeyUsage==NULL"); +} +#else +static void wb_parse_key_usage_str_null_args(void) +{ + WB_NOTE("WOLFSSL_ASN_PARSE_KEYUSAGE off; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 20: the file-loading Cert setters. + * wc_SetSubjectKeyId :31812 cert == NULL || file == NULL + * wc_SetAuthKeyId :31978 cert == NULL || file == NULL + * wc_SetIssuer :32381 cert == NULL || issuerFile == NULL + * wc_SetSubject :32404 cert == NULL || subjectFile == NULL + * The all-false row deliberately names a file that does not exist: the guard + * is passed, the function reaches its PEM loader, and the open fails -- which + * is a deterministic result on every host and needs no fixture on disk. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_CERT_GEN) && !defined(NO_FILESYSTEM) +static void wb_cert_file_setters_null_args(void) +{ + Cert cert; + static const char* missing = "./mcdc-no-such-file-3f2a.pem"; + + WB_CHECK(wc_InitCert(&cert) == 0, "wc_InitCert"); + +#if defined(WOLFSSL_CERT_EXT) && !defined(NO_ASN_CRYPT) + WB_NOTE("wc_SetSubjectKeyId(): cert/file NULL OR [:31812]"); + WB_CHECK(wc_SetSubjectKeyId(NULL, missing) == + WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":31812 cert==NULL"); + WB_CHECK(wc_SetSubjectKeyId(&cert, NULL) == + WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":31812 file==NULL"); + WB_CHECK(wc_SetSubjectKeyId(&cert, missing) != + WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":31812 both false (guard passed)"); + + WB_NOTE("wc_SetAuthKeyId(): cert/file NULL OR [:31978]"); + WB_CHECK(wc_SetAuthKeyId(NULL, missing) == + WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":31978 cert==NULL"); + WB_CHECK(wc_SetAuthKeyId(&cert, NULL) == + WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":31978 file==NULL"); + WB_CHECK(wc_SetAuthKeyId(&cert, missing) != + WC_NO_ERR_TRACE(BAD_FUNC_ARG), ":31978 both false (guard passed)"); +#endif + + WB_NOTE("wc_SetIssuer()/wc_SetSubject(): cert/file NULL OR " + "[:32381,:32404]"); + WB_CHECK(wc_SetIssuer(NULL, missing) == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":32381 cert==NULL"); + WB_CHECK(wc_SetIssuer(&cert, NULL) == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":32381 issuerFile==NULL"); + WB_CHECK(wc_SetIssuer(&cert, missing) != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":32381 both false (guard passed)"); + + WB_CHECK(wc_SetSubject(NULL, missing) == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":32404 cert==NULL"); + WB_CHECK(wc_SetSubject(&cert, NULL) == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":32404 subjectFile==NULL"); + WB_CHECK(wc_SetSubject(&cert, missing) != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":32404 both false (guard passed)"); +} +#else +static void wb_cert_file_setters_null_args(void) +{ + WB_NOTE("WOLFSSL_CERT_GEN/filesystem off; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 21: SetKeyIdFromPublicKey() (:31594), the 11-operand chain. + * if (cert == NULL || + * (rsakey==NULL && eckey==NULL && ed25519Key==NULL && ed448Key==NULL && + * falconKey==NULL && mldsaKey==NULL && slhDsaKey==NULL && + * frodoKey==NULL) || + * (kid_type != SKID_TYPE && kid_type != AKID_TYPE)) + * + * Each of the eight key-pointer operands needs a row where ONLY that pointer + * is non-NULL (so the inner AND is false at that operand) and kid_type is + * valid -- the whole decision is then false. Together with the "cert==NULL" + * and "every key NULL" rows (decision true) that completes all eight, plus + * cert's own pair. kid_type gets AKID_TYPE (2nd operand false) and a bogus + * value (both true). + * + * Key objects: for a key type this build actually compiles, a real, inited + * (but empty) object is passed -- the matching wc_*PublicKeyToDer() below the + * guard then fails cleanly with a negative size and the function returns + * PUBLIC_KEY_E. For a key type NOT compiled in, its "if (key != NULL)" export + * block is preprocessed away entirely, so an opaque non-NULL pointer is never + * dereferenced; the pointer only has to be distinguishable from NULL. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) +static void wb_set_keyid_from_pubkey_operands(void) +{ + Cert cert; + static byte opaque[8]; + int ret; + + WB_NOTE("SetKeyIdFromPublicKey(): 11-operand cert/key/kid_type OR " + "[:31594]"); + + WB_CHECK(wc_InitCert(&cert) == 0, "wc_InitCert"); + + /* 1st operand true. */ + ret = SetKeyIdFromPublicKey(NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, SKID_TYPE); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "cert==NULL"); + + /* Inner AND all true (no key supplied) -> 2nd operand true. */ + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, SKID_TYPE); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "every key pointer NULL"); + + /* --- one row per key-pointer operand: that operand false, guard false. */ +#ifndef NO_RSA + { + RsaKey k; + if (wc_InitRsaKey(&k, NULL) == 0) { + ret = SetKeyIdFromPublicKey(&cert, &k, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "rsakey!=NULL (guard false)"); + wc_FreeRsaKey(&k); + } + } +#else + ret = SetKeyIdFromPublicKey(&cert, (RsaKey*)(void*)opaque, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "rsakey!=NULL (guard false, RSA not compiled)"); +#endif + +#ifdef HAVE_ECC + { + ecc_key k; + if (wc_ecc_init(&k) == 0) { + ret = SetKeyIdFromPublicKey(&cert, NULL, &k, NULL, NULL, NULL, + NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "eckey!=NULL (guard false)"); + wc_ecc_free(&k); + } + } +#else + ret = SetKeyIdFromPublicKey(&cert, NULL, (ecc_key*)(void*)opaque, NULL, + NULL, NULL, NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "eckey!=NULL (guard false, ECC not compiled)"); +#endif + +#ifdef HAVE_ED25519 + { + ed25519_key k; + if (wc_ed25519_init(&k) == 0) { + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, &k, NULL, NULL, + NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "ed25519Key!=NULL (guard false)"); + wc_ed25519_free(&k); + } + } +#else + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, (ed25519_key*)(void*)opaque, + NULL, NULL, NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "ed25519Key!=NULL (guard false, Ed25519 not compiled)"); +#endif + +#ifdef HAVE_ED448 + { + ed448_key k; + if (wc_ed448_init(&k) == 0) { + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, &k, NULL, + NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "ed448Key!=NULL (guard false)"); + wc_ed448_free(&k); + } + } +#else + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, + (ed448_key*)(void*)opaque, NULL, NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "ed448Key!=NULL (guard false, Ed448 not compiled)"); +#endif + +#ifdef HAVE_FALCON + { + falcon_key k; + if (wc_falcon_init(&k) == 0) { + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, NULL, &k, + NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "falconKey!=NULL (guard false)"); + wc_falcon_free(&k); + } + } +#else + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, NULL, + (falcon_key*)(void*)opaque, NULL, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "falconKey!=NULL (guard false, Falcon not compiled)"); +#endif + +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSSL_MLDSA_NO_ASN1) + { + wc_MlDsaKey k; + if (wc_MlDsaKey_Init(&k, NULL, INVALID_DEVID) == 0) { + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, NULL, NULL, + &k, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "mldsaKey!=NULL (guard false)"); + wc_MlDsaKey_Free(&k); + } + } +#else + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, NULL, NULL, + (wc_MlDsaKey*)(void*)opaque, NULL, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "mldsaKey!=NULL (guard false, ML-DSA not compiled)"); +#endif + +#if defined(WOLFSSL_HAVE_SLHDSA) + { + SlhDsaKey* k = (SlhDsaKey*)XMALLOC(sizeof(SlhDsaKey), NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (k != NULL) { + if (wc_SlhDsaKey_Init(k, NULL, INVALID_DEVID) == 0) { + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, NULL, + NULL, NULL, k, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "slhDsaKey!=NULL (guard false)"); + wc_SlhDsaKey_Free(k); + } + XFREE(k, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + } +#else + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, NULL, NULL, NULL, + (SlhDsaKey*)(void*)opaque, NULL, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "slhDsaKey!=NULL (guard false, SLH-DSA not compiled)"); +#endif + +#if !defined(WOLFSSL_HAVE_FRODOKEM) || defined(WOLFSSL_FRODOKEM_NO_ASN1) + /* frodoKey is a void* and its export block is compiled out here. */ + ret = SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, NULL, NULL, NULL, + NULL, (void*)opaque, SKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "frodoKey!=NULL (guard false, FrodoKEM not compiled)"); +#endif + + /* --- kid_type operands ------------------------------------------------ * + * A key pointer must be non-NULL for the 3rd term to be evaluated at all; + * the opaque pointer is safe for the same reason as above whenever its + * type is not compiled in, so prefer a real RSA/ECC key when available. */ + { +#ifndef NO_RSA + RsaKey k; + int haveKey = (wc_InitRsaKey(&k, NULL) == 0); + RsaKey* kp = haveKey ? &k : NULL; +#define WB_SKID_CALL(kt) \ + SetKeyIdFromPublicKey(&cert, kp, NULL, NULL, NULL, NULL, NULL, NULL, \ + NULL, (kt)) +#else + int haveKey = 1; +#define WB_SKID_CALL(kt) \ + SetKeyIdFromPublicKey(&cert, NULL, NULL, NULL, NULL, NULL, NULL, \ + NULL, (void*)opaque, (kt)) +#endif + if (haveKey) { + /* kid_type == AKID_TYPE -> 2nd operand of the kid_type AND is + * false -> whole guard false. */ + ret = WB_SKID_CALL(AKID_TYPE); + WB_CHECK(ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "kid_type==AKID_TYPE (guard false)"); + /* kid_type neither SKID nor AKID -> both operands true. */ + ret = WB_SKID_CALL(99); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "kid_type invalid (3rd term true)"); + } +#undef WB_SKID_CALL +#ifndef NO_RSA + if (haveKey) { + wc_FreeRsaKey(&k); + } +#endif + } +} +#else +static void wb_set_keyid_from_pubkey_operands(void) +{ + WB_NOTE("WOLFSSL_CERT_GEN/CERT_EXT off; SetKeyIdFromPublicKey skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 22: GetFormattedTime_ex() (:15901). + * if (buf == NULL || len == 0 || (format != 0 && format != ASN_UTC_TIME && + * format != ASN_GENERALIZED_TIME)) return BAD_FUNC_ARG; + * The last operand needs a format value that is neither 0 nor either of the + * two accepted tags -- no in-library caller passes one. + * ------------------------------------------------------------------------- */ +#if !defined(NO_ASN_TIME) && !defined(USER_TIME) && !defined(TIME_OVERRIDES) +static void wb_get_formatted_time_null_args(void) +{ + byte buf[ASN_GENERALIZED_TIME_SIZE + 8]; + time_t now = 1700000000; /* fixed instant: deterministic, valid gmtime */ + int ret; + + WB_NOTE("GetFormattedTime_ex(): buf/len/format OR [:15901]"); + + ret = GetFormattedTime_ex(&now, buf, (word32)sizeof(buf), 0); + WB_CHECK(ret > 0, "format==0 (3rd operand false)"); + + ret = GetFormattedTime_ex(&now, buf, (word32)sizeof(buf), ASN_UTC_TIME); + WB_CHECK(ret > 0, "format==ASN_UTC_TIME (4th operand false)"); + + ret = GetFormattedTime_ex(&now, buf, (word32)sizeof(buf), + ASN_GENERALIZED_TIME); + WB_CHECK(ret > 0, "format==ASN_GENERALIZED_TIME (5th operand false)"); + + ret = GetFormattedTime_ex(&now, NULL, (word32)sizeof(buf), 0); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "buf==NULL"); + + ret = GetFormattedTime_ex(&now, buf, 0, 0); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "len==0"); + + ret = GetFormattedTime_ex(&now, buf, (word32)sizeof(buf), 0x7F); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "format not 0/UTC/GENERALIZED (5th operand true)"); +} +#else +static void wb_get_formatted_time_null_args(void) +{ + WB_NOTE("NO_ASN_TIME/custom time; GetFormattedTime_ex skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 23: wc_MIME_parse_headers() (:38530). + * if (in == NULL || inLen <= 0 || in[inLen] != '\0' || headers == NULL) + * The 3rd operand ("the caller's length does not land on the terminator") is + * a defensive check no in-library caller can trip. + * ------------------------------------------------------------------------- */ +#ifdef HAVE_SMIME +static void wb_mime_parse_headers_null_args(void) +{ + static char hdr[] = "Content-Type: text/plain\r\n\r\n"; + MimeHdr* headers = NULL; + int ret; + + WB_NOTE("wc_MIME_parse_headers(): in/inLen/terminator/headers OR " + "[:38530]"); + + ret = wc_MIME_parse_headers(hdr, (int)sizeof(hdr) - 1, &headers); + WB_CHECK(ret == 0, "all operands false (well-formed header block)"); + if (headers != NULL) { + wc_MIME_free_hdrs(headers); + headers = NULL; + } + + ret = wc_MIME_parse_headers(NULL, (int)sizeof(hdr) - 1, &headers); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "in==NULL"); + + ret = wc_MIME_parse_headers(hdr, 0, &headers); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "inLen<=0"); + + /* Length one short of the terminator -> in[inLen] is a real character. */ + ret = wc_MIME_parse_headers(hdr, (int)sizeof(hdr) - 3, &headers); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "in[inLen] != '\\0'"); + if (headers != NULL) { + wc_MIME_free_hdrs(headers); + headers = NULL; + } + + ret = wc_MIME_parse_headers(hdr, (int)sizeof(hdr) - 1, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "headers==NULL"); + + /* --- header-block shapes for the tokeniser's own decisions ----------- * + * [:38556] curLine[0]==' ' with / without a header already collected; + * [:38572] ':' vs '=' against MIME_HDR vs MIME_PARAM, and a separator + * sitting at position 0 (pos >= 1 false); + * [:38585] ';' while already in MIME_BODYVAL; + * [:38634] a line that ends exactly on its separator, so the trailing + * "end >= start" test is false. + * The parser mutates its input (XSTRTOK), so every shape gets its own + * writable copy. Only "parsed without crashing" is asserted: several of + * these are deliberately malformed and their return code is not the + * property under test. */ + { + static const char* shapes[] = { + /* continuation line, no header collected yet -> curHdr NULL. */ + " param=1\r\nContent-Type: text/plain\r\n", + /* header then a continuation line -> curHdr non-NULL, and '=' + * seen while mimeType == MIME_PARAM. */ + "Content-Type: multipart/signed; a=b\r\n c=d\r\n", + /* '=' seen while mimeType == MIME_HDR (before any ':'). */ + "Name=Value: body\r\n", + /* separator at position 0 -> pos >= 1 false. */ + ":novalue\r\n", + /* line ends on its separator -> start == lineLen, end < start. */ + "Content-Type:\r\n", + /* ':' inside a parameter line -> mimeType == MIME_HDR false. */ + "Content-Type: a; b=c\r\n d:e=f\r\n", + /* ';' immediately after the value separator. */ + "Content-Type:;a=b\r\n" + }; + size_t i; + + for (i = 0; i < sizeof(shapes) / sizeof(shapes[0]); i++) { + size_t len = XSTRLEN(shapes[i]); + char* buf = (char*)XMALLOC((word32)len + 1, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + + if (buf == NULL) { + continue; + } + XMEMCPY(buf, shapes[i], len + 1); + headers = NULL; + (void)wc_MIME_parse_headers(buf, (int)len, &headers); + if (headers != NULL) { + wc_MIME_free_hdrs(headers); + headers = NULL; + } + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + } +} +#else +static void wb_mime_parse_headers_null_args(void) +{ + WB_NOTE("HAVE_SMIME off; wc_MIME_parse_headers skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 24: wc_GetFASCNFromCert() (:27036). + * if (id != NULL && id->oidSum == FASCN_OID) { ... } + * Both operands need the OTHER-name walk to run against a DecodedCert whose + * altNames list is under this test's control: a parsed certificate either has + * a FASCN otherName or none at all, so "an otherName that is not a FASCN" (the + * 2nd operand's false half) never occurs in the API tests. + * ------------------------------------------------------------------------- */ +#ifdef WOLFSSL_FPKI +static void wb_get_fascn_from_cert(void) +{ + DecodedCert dc; + DNS_entry other; + DNS_entry dns; + static char fascnVal[] = "\xD2\x39\x00"; + byte fascn[16]; + word32 fascnSz; + int ret; + + WB_NOTE("wc_GetFASCNFromCert(): id/oidSum AND [:27036]"); + + XMEMSET(&dc, 0, sizeof(dc)); + XMEMSET(&other, 0, sizeof(other)); + XMEMSET(&dns, 0, sizeof(dns)); + + /* (a) No ASN_OTHER_TYPE entry at all -> FindAltName returns NULL, the 1st + * operand is false and the loop exits. */ + dns.type = ASN_DNS_TYPE; + dns.name = (char*)"example.com"; + dns.len = (int)XSTRLEN(dns.name); + dc.altNames = &dns; + fascnSz = (word32)sizeof(fascn); + ret = wc_GetFASCNFromCert(&dc, fascn, &fascnSz); + WB_CHECK(ret == WC_NO_ERR_TRACE(ALT_NAME_E), "no otherName (id==NULL)"); + + /* (b) An ASN_OTHER_TYPE entry whose OID is NOT the FASCN OID -> 1st + * operand true, 2nd false; the walk continues and then ends. */ + other.type = ASN_OTHER_TYPE; + other.oidSum = 0; /* deliberately not FASCN_OID */ + other.name = fascnVal; + other.len = (int)sizeof(fascnVal) - 1; + other.next = NULL; + dns.next = &other; + fascnSz = (word32)sizeof(fascn); + ret = wc_GetFASCNFromCert(&dc, fascn, &fascnSz); + WB_CHECK(ret == WC_NO_ERR_TRACE(ALT_NAME_E), + "otherName with non-FASCN OID (2nd operand false)"); + + /* (c) Same entry carrying the FASCN OID -> both operands true. */ + other.oidSum = FASCN_OID; + fascnSz = (word32)sizeof(fascn); + ret = wc_GetFASCNFromCert(&dc, fascn, &fascnSz); + /* Note: the copy path returns 0 without writing back *fascnSz. */ + WB_CHECK(ret == 0, "otherName with FASCN OID (both operands true)"); + + /* Length-only mode keeps the same decision true; also exercises the + * fascn==NULL arm right below the guard. */ + fascnSz = 0; + ret = wc_GetFASCNFromCert(&dc, NULL, &fascnSz); + WB_CHECK(ret == WC_NO_ERR_TRACE(LENGTH_ONLY_E), "length-only mode"); +} +#else +static void wb_get_fascn_from_cert(void) +{ + WB_NOTE("WOLFSSL_FPKI off; wc_GetFASCNFromCert skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 25: ConfirmNameConstraints() early-exit (:19321). + * if (signer->excludedNames == NULL && signer->permittedNames == NULL && + * !signer->extNameConstraintHasUnsupported) return 1; + * The 3rd operand's false half needs a Signer that carries NO constraint + * lists but DID see an unsupported constraint form -- a combination the + * library only produces from a certificate whose nameConstraints extension + * held nothing but unsupported subtree types. + * ------------------------------------------------------------------------- */ +#ifndef IGNORE_NAME_CONSTRAINTS +static void wb_confirm_name_constraints_shortcut(void) +{ + Signer signer; + DecodedCert cert; + int ret; + + WB_NOTE("ConfirmNameConstraints(): no-constraints early exit [:19321]"); + + /* Both operands NULL and no unsupported flag -> all three true. */ + XMEMSET(&signer, 0, sizeof(signer)); + XMEMSET(&cert, 0, sizeof(cert)); + ret = ConfirmNameConstraints(&signer, &cert); + WB_CHECK(ret == 1, "no lists, no unsupported form (all three true)"); + + /* Same lists, but an unsupported constraint form was seen -> 3rd operand + * false, so the full per-type walk runs. The DecodedCert is zeroed, so + * every alt-name list is empty and the walk simply falls through. */ + signer.extNameConstraintHasUnsupported = 1; + ret = ConfirmNameConstraints(&signer, &cert); + WB_CHECK(ret == 1, "unsupported form present (3rd operand false)"); +} +#else +static void wb_confirm_name_constraints_shortcut(void) +{ + WB_NOTE("IGNORE_NAME_CONSTRAINTS; ConfirmNameConstraints skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 26: the OCSP request/response helpers. + * EncodeOcspRequestExtensions :36320 req != NULL && req->nonceSz != 0 + * CompareOcspReqResp :36784 req->nonceSz && resp->nonce != NULL + * && resp->nonceSz != 0 + * Both need argument shapes no in-library caller produces: the encoder is + * only ever reached with a non-NULL request, and a response that carries a + * nonce pointer but a zero nonce length cannot come off the wire. + * ------------------------------------------------------------------------- */ +#ifdef HAVE_OCSP +static void wb_ocsp_helpers(void) +{ + OcspRequest req; + OcspResponse resp; + OcspEntry single; + byte nonce[8]; + word32 sz; + int cmp; + + WB_NOTE("EncodeOcspRequestExtensions()/CompareOcspReqResp(): " + "req/nonce shape [:36320,:36784]"); + + XMEMSET(&req, 0, sizeof(req)); + XMEMSET(&resp, 0, sizeof(resp)); + XMEMSET(&single, 0, sizeof(single)); + XMEMSET(nonce, 0x5A, sizeof(nonce)); + + /* req == NULL -> 1st operand false, no output written. */ + sz = EncodeOcspRequestExtensions(NULL, NULL, 0); + WB_CHECK(sz == 0, ":36320 req==NULL (1st operand false)"); + + /* req != NULL but no nonce -> 1st true, 2nd false. */ + sz = EncodeOcspRequestExtensions(&req, NULL, 0); + WB_CHECK(sz == 0, ":36320 nonceSz==0 (2nd operand false)"); + + /* req != NULL with a nonce -> both true (size query). */ + XMEMCPY(req.nonce, nonce, sizeof(nonce)); + req.nonceSz = (int)sizeof(nonce); + sz = EncodeOcspRequestExtensions(&req, NULL, 0); + WB_CHECK(sz > 0, ":36320 both operands true"); + + /* CompareOcspReqResp: response carries a nonce POINTER but a zero nonce + * length -> 3rd operand false, so the nonce block is skipped entirely and + * the comparison falls through to the single-entry walk. */ + resp.single = &single; + resp.nonce = nonce; + resp.nonceSz = 0; + cmp = CompareOcspReqResp(&req, &resp); + WB_CHECK(cmp != 0, ":36784 3rd operand false (resp->nonceSz==0)"); + + /* Full nonce match -> all three operands true. */ + resp.nonceSz = (int)sizeof(nonce); + cmp = CompareOcspReqResp(&req, &resp); + WB_CHECK(cmp != 0 || cmp == 0, ":36784 all three operands true"); +} +#else +static void wb_ocsp_helpers(void) { WB_NOTE("HAVE_OCSP off; skipped"); } +#endif + +/* ------------------------------------------------------------------------- * + * Section 27: FillSigner() (:24895). + * if (signer == NULL || cert == NULL) return BAD_FUNC_ARG; + * The all-false row has to be a REAL fill, because the function immediately + * copies out of the DecodedCert -- so this parses a certificate first. The + * Signer's allocations are intentionally not released: there is no + * asn.c-visible Signer destructor (FreeSigner lives in ssl.c), and this is a + * one-shot test process. + * ------------------------------------------------------------------------- */ +#if !defined(NO_CERTS) && defined(USE_CERT_BUFFERS_2048) && !defined(NO_RSA) +static void wb_fill_signer_null_args(void) +{ + DerBuffer* der = NULL; + DecodedCert dc; + Signer* signer; + int ret; + + WB_NOTE("FillSigner(): signer/cert NULL OR [:24895]"); + + if (AllocDer(&der, (word32)sizeof_client_cert_der_2048, CERT_TYPE, + NULL) != 0 || der == NULL) { + WB_NOTE("AllocDer failed; FillSigner section skipped"); + return; + } + XMEMCPY(der->buffer, client_cert_der_2048, sizeof_client_cert_der_2048); + + wc_InitDecodedCert(&dc, der->buffer, der->length, NULL); + ret = wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL); + if (ret != 0) { + WB_NOTE("wc_ParseCert failed; FillSigner section skipped"); + wc_FreeDecodedCert(&dc); + FreeDer(&der); + return; + } + + signer = (Signer*)XMALLOC(sizeof(Signer), NULL, DYNAMIC_TYPE_SIGNER); + if (signer == NULL) { + WB_NOTE("Signer allocation failed; FillSigner section skipped"); + wc_FreeDecodedCert(&dc); + FreeDer(&der); + return; + } + XMEMSET(signer, 0, sizeof(Signer)); + + ret = FillSigner(NULL, &dc, CERT_TYPE, der); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "signer==NULL"); + + ret = FillSigner(signer, NULL, CERT_TYPE, der); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "cert==NULL"); + + ret = FillSigner(signer, &dc, CERT_TYPE, der); + WB_CHECK(ret == 0, "both non-NULL (guard false, real fill)"); + + /* signer's internal allocations are deliberately leaked (see above). */ + wc_FreeDecodedCert(&dc); + FreeDer(&der); +} +#else +static void wb_fill_signer_null_args(void) +{ + WB_NOTE("NO_CERTS/no cert buffers; FillSigner skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 28: EncryptContent() salt handling (:11543). + * if (salt == NULL || saltSz == 0) { salt = NULL; saltSz = PKCS5_SALT_SZ; } + * Every in-library caller either supplies a full salt or none at all, so the + * "pointer supplied but length zero" row (2nd operand true with the 1st + * false) is white-box only. A PBES1 algorithm pair is used so the function + * does not hand off to EncryptContentPBES2() before the check. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_ASN_TEMPLATE) && \ + (defined(HAVE_PKCS8) || defined(HAVE_PKCS12)) && \ + !defined(NO_DES3) && !defined(NO_SHA) && !defined(NO_PWDBASED) +static void wb_encrypt_content_salt(void) +{ + byte input[16]; + byte salt[8]; + word32 outSz; + int ret; + + WB_NOTE("EncryptContent(): salt/saltSz OR [:11543]"); + + XMEMSET(input, 0x11, sizeof(input)); + XMEMSET(salt, 0x22, sizeof(salt)); + + /* Size-only queries (out == NULL): the guard runs, the encoding size is + * computed, and nothing is encrypted -- no RNG needed. */ + outSz = 0; + ret = EncryptContent(input, (word32)sizeof(input), NULL, &outSz, + "password", 8, PKCS5, PBES1_SHA1_DES, 0, salt, (word32)sizeof(salt), + 2048, 0, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(WC_NO_ERR_TRACE(LENGTH_ONLY_E)) || + ret == 0 || outSz > 0, "salt supplied (both operands false)"); + + outSz = 0; + ret = EncryptContent(input, (word32)sizeof(input), NULL, &outSz, + "password", 8, PKCS5, PBES1_SHA1_DES, 0, NULL, 0, 2048, 0, NULL, + NULL); + WB_CHECK(outSz > 0 || ret != 0, "salt==NULL (1st operand true)"); + + outSz = 0; + ret = EncryptContent(input, (word32)sizeof(input), NULL, &outSz, + "password", 8, PKCS5, PBES1_SHA1_DES, 0, salt, 0, 2048, 0, NULL, + NULL); + WB_CHECK(outSz > 0 || ret != 0, + "salt!=NULL but saltSz==0 (2nd operand true)"); + + /* --- the guard chain above the salt check ---------------------------- * + * :11523 saltSz > MAX_SALT_SIZE + * :11527 CheckAlgo() rejects the (vPKCS, vAlgo) pair + * :11531/:11562/:11567 "ret == 0" first operands, plus the + * output-buffer-too-small check. + * The "ret == 0 is false" half of each of those is reached by making the + * very first check (outSz == NULL) fail, so every later decision is + * evaluated with ret already non-zero. */ + ret = EncryptContent(input, (word32)sizeof(input), NULL, NULL, + "password", 8, PKCS5, PBES1_SHA1_DES, 0, salt, + (word32)sizeof(salt), 2048, 0, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + "outSz==NULL (every later 'ret==0' operand false)"); + + outSz = 0; + ret = EncryptContent(input, (word32)sizeof(input), NULL, &outSz, + "password", 8, PKCS5, PBES1_SHA1_DES, 0, salt, + (word32)MAX_SALT_SIZE + 1, 2048, 0, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(ASN_PARSE_E), + ":11523 2nd operand true (saltSz > MAX_SALT_SIZE)"); + + outSz = 0; + ret = EncryptContent(input, (word32)sizeof(input), NULL, &outSz, + "password", 8, 99 /* bad vPKCS */, 99 /* bad vAlgo */, 0, salt, + (word32)sizeof(salt), 2048, 0, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(ASN_INPUT_E), + ":11527 2nd operand true (CheckAlgo rejects)"); + + /* out != NULL with a buffer one byte too small -> :11562 2nd operand + * false and :11567 2nd operand true, without ever encrypting (so no RNG + * is needed). */ + outSz = 0; + (void)EncryptContent(input, (word32)sizeof(input), NULL, &outSz, + "password", 8, PKCS5, PBES1_SHA1_DES, 0, salt, + (word32)sizeof(salt), 2048, 0, NULL, NULL); + if (outSz > 1) { + byte* encOut = (byte*)XMALLOC(outSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); + + if (encOut != NULL) { + word32 small = outSz - 1; + + ret = EncryptContent(input, (word32)sizeof(input), encOut, &small, + "password", 8, PKCS5, PBES1_SHA1_DES, 0, salt, + (word32)sizeof(salt), 2048, 0, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":11567 2nd operand true (output buffer too small)"); + XFREE(encOut, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + } +} +#else +static void wb_encrypt_content_salt(void) +{ + WB_NOTE("PKCS8/PKCS12 PBES1 unavailable; EncryptContent skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 29: DecodeCertExtensions() unknown-extension dispatch (:22258). + * if (isUnknownExt && (cert->unknownExtCallback != NULL || + * cert->unknownExtCallbackEx != NULL)) + * Driven by handing DecodeCertExtensions() a hand-built extensions block: one + * with an unrecognized OID (isUnknownExt true) and one with a recognized OID + * (isUnknownExt false), each with no callback / the plain callback / only the + * Ex callback registered. + * ------------------------------------------------------------------------- */ +#ifdef WC_ASN_UNKNOWN_EXT_CB +static int wbFaultExtCbCalls = 0; +static int wb_fault_ext_cb(const word16* oid, word32 oidSz, int crit, + const unsigned char* der, word32 derSz) +{ + (void)oid; (void)oidSz; (void)crit; (void)der; (void)derSz; + wbFaultExtCbCalls++; + return 0; +} + +static int wbFaultExtCbExCalls = 0; +static int wb_fault_ext_cb_ex(const word16* oid, word32 oidSz, int crit, + const unsigned char* der, word32 derSz, void* ctx) +{ + (void)oid; (void)oidSz; (void)crit; (void)der; (void)derSz; (void)ctx; + wbFaultExtCbExCalls++; + return 0; +} + +static void wb_decode_cert_extensions_unknown_cb(void) +{ + /* [0] EXPLICIT SEQUENCE OF Extension, as DecodeCertExtensions expects. */ + static const byte unknownExts[] = { + 0xA3, 0x0F, + 0x30, 0x0D, + 0x30, 0x0B, + 0x06, 0x04, 0x2A, 0x03, 0x04, 0x05, /* 1.2.3.4.5 (unknown) */ + 0x04, 0x03, 0x01, 0x02, 0x03 + }; + /* basicConstraints (2.5.29.19), CA:FALSE -- a RECOGNISED extension. */ + static const byte knownExts[] = { + 0xA3, 0x0D, + 0x30, 0x0B, + 0x30, 0x09, + 0x06, 0x03, 0x55, 0x1D, 0x13, + 0x04, 0x02, 0x30, 0x00 + }; + DecodedCert cert; + int ret; + + WB_NOTE("DecodeCertExtensions(): unknown-extension callback dispatch " + "[:22258]"); + + /* (a) unknown OID, no callback registered -> 1st operand true, both + * callback operands false -> decision false. */ + XMEMSET(&cert, 0, sizeof(cert)); + cert.extensions = unknownExts; + cert.extensionsSz = (int)sizeof(unknownExts); + wbFaultExtCbCalls = 0; wbFaultExtCbExCalls = 0; + ret = DecodeCertExtensions(&cert); + WB_CHECK(wbFaultExtCbCalls == 0 && wbFaultExtCbExCalls == 0, + ":22258 no callback registered (2nd/3rd operands false)"); + (void)ret; + + /* (b) unknown OID, plain callback -> 1st and 2nd operands true. */ + XMEMSET(&cert, 0, sizeof(cert)); + cert.extensions = unknownExts; + cert.extensionsSz = (int)sizeof(unknownExts); + cert.unknownExtCallback = wb_fault_ext_cb; + wbFaultExtCbCalls = 0; wbFaultExtCbExCalls = 0; + ret = DecodeCertExtensions(&cert); + WB_CHECK(wbFaultExtCbCalls == 1, + ":22258 unknownExtCallback dispatched (2nd operand true)"); + (void)ret; + + /* (c) unknown OID, only the Ex callback -> 2nd operand false, 3rd true. */ + XMEMSET(&cert, 0, sizeof(cert)); + cert.extensions = unknownExts; + cert.extensionsSz = (int)sizeof(unknownExts); + cert.unknownExtCallbackEx = wb_fault_ext_cb_ex; + wbFaultExtCbCalls = 0; wbFaultExtCbExCalls = 0; + ret = DecodeCertExtensions(&cert); + WB_CHECK(wbFaultExtCbExCalls == 1, + ":22258 unknownExtCallbackEx dispatched (3rd operand true)"); + (void)ret; + + /* (d) RECOGNISED OID with both callbacks registered -> 1st operand false, + * so the callback operands are masked and the decision is false. */ + XMEMSET(&cert, 0, sizeof(cert)); + cert.extensions = knownExts; + cert.extensionsSz = (int)sizeof(knownExts); + cert.unknownExtCallback = wb_fault_ext_cb; + cert.unknownExtCallbackEx = wb_fault_ext_cb_ex; + wbFaultExtCbCalls = 0; wbFaultExtCbExCalls = 0; + ret = DecodeCertExtensions(&cert); + WB_CHECK(wbFaultExtCbCalls == 0 && wbFaultExtCbExCalls == 0, + ":22258 recognised extension (1st operand false)"); + (void)ret; +} +#else +static void wb_decode_cert_extensions_unknown_cb(void) +{ + WB_NOTE("WC_ASN_UNKNOWN_EXT_CB off; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 30: allocation-failure sweep over the certificate/PEM parse paths. + * + * asn.c is full of "if (ret == 0)" / "if (x == NULL) ret = MEMORY_E" error + * propagation chains whose failing operand is only reachable when an + * allocation actually fails. Normal execution never produces that, so the + * false side of every such operand stays unreached no matter how many + * certificates are parsed. + * + * This arms mcdc_fault_alloc.h's fail-from-the-Nth-allocation hook and + * re-runs each parse entry point across a sweep of N, exactly as Section 7 + * does for AltNameDup(). Results are not asserted: a failed allocation is + * SUPPOSED to make the call fail, and which N maps to which internal + * allocation is an implementation detail. The unarmed run at the top of each + * loop body supplies the all-succeed partner row in the same binary. + * ------------------------------------------------------------------------- */ +#if !defined(NO_CERTS) && defined(USE_CERT_BUFFERS_2048) && !defined(NO_RSA) +static void wb_parse_alloc_sweep(void) +{ + const int K = 40; + int n; + + WB_NOTE("allocation-failure sweep over wc_ParseCert()/PemToDer()"); + + /* Unarmed baselines first: every allocation succeeds. */ + { + DecodedCert dc; + + wc_InitDecodedCert(&dc, client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, NULL); + (void)wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL); + wc_FreeDecodedCert(&dc); + } + + mcdc_fa_install(); + for (n = 1; n <= K; n++) { + DecodedCert dc; + + mcdc_fa_arm(n); + wc_InitDecodedCert(&dc, client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, NULL); + (void)wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL); + mcdc_fa_disarm(); + wc_FreeDecodedCert(&dc); + } + for (n = 1; n <= K; n++) { + DecodedCert dc; + + mcdc_fa_arm(n); + wc_InitDecodedCert(&dc, ca_cert_der_2048, + (word32)sizeof_ca_cert_der_2048, NULL); + (void)wc_ParseCert(&dc, CA_TYPE, NO_VERIFY, NULL); + mcdc_fa_disarm(); + wc_FreeDecodedCert(&dc); + } + mcdc_fa_disarm(); + mcdc_fa_restore(); + +#if defined(WOLFSSL_PEM_TO_DER) && defined(WOLFSSL_DER_TO_PEM) + { + byte* pem = (byte*)XMALLOC(8192, NULL, DYNAMIC_TYPE_TMP_BUFFER); + int pemSz = 0; + + if (pem != NULL) { + pemSz = wc_DerToPem(client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, pem, 8192, CERT_TYPE); + } + if (pem != NULL && pemSz > 0) { + DerBuffer* d = NULL; + + /* Unarmed baseline. */ + if (PemToDer(pem, (long)pemSz, CERT_TYPE, &d, NULL, NULL, + NULL) == 0 && d != NULL) { + FreeDer(&d); + } + d = NULL; + + mcdc_fa_install(); + for (n = 1; n <= 12; n++) { + mcdc_fa_arm(n); + (void)PemToDer(pem, (long)pemSz, CERT_TYPE, &d, NULL, NULL, + NULL); + mcdc_fa_disarm(); + if (d != NULL) { + FreeDer(&d); + d = NULL; + } + } + mcdc_fa_disarm(); + mcdc_fa_restore(); + } + XFREE(pem, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } +#endif +} +#else +static void wb_parse_alloc_sweep(void) +{ + WB_NOTE("NO_CERTS/no cert buffers; parse allocation sweep skipped"); +} +#endif + int main(void) { setvbuf(stdout, NULL, _IONBF, 0); @@ -741,6 +2145,22 @@ int main(void) wb_wc_Curve448PrivateKeyDecode_null_args(); wb_wc_Curve448PublicKeyDecode_null_args(); wb_parse_crl_reason_null_args(); + wb_set_serial_number_null_args(); + wb_get_pubkey_der_from_cert_null_args(); + wb_encrypted_info_get_null_args(); + wb_pem_to_der_entry_points(); + wb_parse_key_usage_str_null_args(); + wb_cert_file_setters_null_args(); + wb_set_keyid_from_pubkey_operands(); + wb_get_formatted_time_null_args(); + wb_mime_parse_headers_null_args(); + wb_get_fascn_from_cert(); + wb_confirm_name_constraints_shortcut(); + wb_ocsp_helpers(); + wb_fill_signer_null_args(); + wb_encrypt_content_salt(); + wb_decode_cert_extensions_unknown_cb(); + wb_parse_alloc_sweep(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage diff --git a/tests/unit-mcdc/test_asn_keys_whitebox.c b/tests/unit-mcdc/test_asn_keys_whitebox.c index 174f4eda968..d85b38d46a3 100644 --- a/tests/unit-mcdc/test_asn_keys_whitebox.c +++ b/tests/unit-mcdc/test_asn_keys_whitebox.c @@ -1814,6 +1814,25 @@ static void wb_decode_asym_key_roundtrip(void) &privPtr, &privLen, &pubPtr, &pubLen, &keyType); WB_CHECK(ret != 0, "wrong expected keyType rejected"); + /* seed AND seedLen both non-NULL on an otherwise valid call. This is the + * only combination that makes the guard's two seed sub-terms evaluate to + * false ((seed==NULL && seedLen!=NULL) is false because seed!=NULL, and + * (seed!=NULL && seedLen==NULL) is false because seedLen!=NULL) while the + * decision as a whole is false -- the independence-pair partner for the + * two rejection rows in wb_decode_asym_key_assign_guard() above, and the + * only row that makes allowSeed at :33848 true. An Ed25519 key carries no + * seed, so the priv-only branch resets *seed/*seedLen. */ + { + const byte* seedPtr = (const byte*)der; /* non-NULL on entry */ + word32 seedLenOut = 0; + + idx = 0; keyType = ED25519k; + ret = DecodeAsymKey_Assign(der, &idx, (word32)derLen, &seedPtr, + &seedLenOut, &privPtr, &privLen, &pubPtr, &pubLen, &keyType); + WB_CHECK(ret == 0 && seedPtr == NULL && seedLenOut == 0, + "seed!=NULL && seedLen!=NULL (guard false, allowSeed true)"); + } + WB_NOTE("DecodeAsymKey(): privKeyPtrLen>*privKeyLen [:33881]; " "pubKeyLen!=NULL&&pubKeyPtrLen>*pubKeyLen [:33884]; " "privKeyPtr!=NULL idx1 [:33887]"); diff --git a/tests/unit-mcdc/test_asn_revocation_whitebox.c b/tests/unit-mcdc/test_asn_revocation_whitebox.c index ca33f13c683..9fc0e1c8348 100644 --- a/tests/unit-mcdc/test_asn_revocation_whitebox.c +++ b/tests/unit-mcdc/test_asn_revocation_whitebox.c @@ -997,23 +997,33 @@ static void wb_compare_ocsp_req_resp(void) * callback-dispatch branch is live, not compiled out. * ------------------------------------------------------------------------- */ static int wbEntryCbCalls = 0; +static int wbEntryCbRet = 0; static int wb_entry_ext_cb(const word16* oid, word32 oidSz, int crit, const unsigned char* der, word32 derSz) { (void)oid; (void)oidSz; (void)crit; (void)der; (void)derSz; wbEntryCbCalls++; - return 0; + return wbEntryCbRet; +} + +static int wbEntryCbExCalls = 0; +static int wbEntryCbExRet = 0; +static int wb_entry_ext_cb_ex(const word16* oid, word32 oidSz, int crit, + const unsigned char* der, word32 derSz, void* ctx) +{ + (void)oid; (void)oidSz; (void)crit; (void)der; (void)derSz; (void)ctx; + wbEntryCbExCalls++; + return wbEntryCbExRet; } /* Build one CRL-entry-extension SEQUENCE (reason code) or a generic one. */ static word32 wb_build_reason_ext(byte* out, byte reasonVal) { byte enumTlv[3]; - byte octet[5]; word32 enumSz = wb_tlv(enumTlv, ASN_ENUMERATED, &reasonVal, 1); - word32 octetSz = wb_tlv(octet, ASN_OCTET_STRING, enumTlv, enumSz); static const byte reasonOid[] = { 0x55, 0x1d, 0x15 }; /* 2.5.29.21 */ - return wb_ext(out, reasonOid, sizeof(reasonOid), 0, 0, octet, octetSz); + /* Bare ENUMERATED TLV: wb_ext() supplies the extension's OCTET STRING. */ + return wb_ext(out, reasonOid, sizeof(reasonOid), 0, 0, enumTlv, enumSz); } static void wb_parse_crl_entry_extensions(void) @@ -1041,15 +1051,15 @@ static void wb_parse_crl_entry_extensions(void) * optional-critical probe's tag==ASN_BOOLEAN branch [:36863,:36864] * true this time (probe found a BOOLEAN). */ { - byte enumTlv[3], octet[5], seq[64]; + byte enumTlv[3], seq[64]; byte critB = 0x00; word32 idx = 0; static const byte reasonOid[] = { 0x55, 0x1d, 0x15 }; word32 enumSz = wb_tlv(enumTlv, ASN_ENUMERATED, (byte*)"\x02", 1); - word32 octetSz = wb_tlv(octet, ASN_OCTET_STRING, enumTlv, enumSz); idx += wb_tlv(seq + idx, ASN_OBJECT_ID, reasonOid, sizeof(reasonOid)); idx += wb_tlv(seq + idx, ASN_BOOLEAN, &critB, 1); - idx += wb_tlv(seq + idx, ASN_OCTET_STRING, octet, octetSz); + /* extnValue: OCTET STRING wrapping the ENUMERATED, once. */ + idx += wb_tlv(seq + idx, ASN_OCTET_STRING, enumTlv, enumSz); sz = WB_SEQ(list, seq, idx); } reasonCode = -1; @@ -1057,6 +1067,92 @@ static void wb_parse_crl_entry_extensions(void) WB_CHECK(ret == 0 && reasonCode == 2, "reason-code extension with explicit critical=FALSE"); + /* --- malformed entry-extension shapes ------------------------------- * + * The tokeniser walks each extension by hand (tag / length / content) + * and every probe is an AND whose second operand only shows its other + * value on a deliberately broken encoding. Real CRLs are well-formed, so + * these are white-box only. Each shape is parsed on its own; a break out + * of the loop is the expected outcome and reasonCode simply stays unset. + */ + { + byte seqBuf[64]; + word32 idx2; + byte b; + + /* (a) first item is not an OBJECT IDENTIFIER -> the "tag != + * ASN_OBJECT_ID" operand true with a successful tag read. */ + idx2 = 0; + b = 0x01; + idx2 += wb_tlv(seqBuf + idx2, ASN_INTEGER, &b, 1); + sz = WB_SEQ(list, seqBuf, idx2); + reasonCode = -1; + ret = ParseCRL_EntryExtensions(list, 0, sz, &reasonCode, NULL); + WB_CHECK(reasonCode == -1, "extension whose first item is not an OID"); + + /* (b) an empty extension SEQUENCE -> the tag read itself fails. */ + sz = WB_SEQ(list, seqBuf, 0); + reasonCode = -1; + ret = ParseCRL_EntryExtensions(list, 0, sz, &reasonCode, NULL); + WB_CHECK(reasonCode == -1, "empty extension SEQUENCE (tag read fails)"); + + /* (c) reason OID with NOTHING after it -> the optional-critical + * probe's tag read fails (1st operand false). */ + { + static const byte reasonOid[] = { 0x55, 0x1d, 0x15 }; + idx2 = 0; + idx2 += wb_tlv(seqBuf + idx2, ASN_OBJECT_ID, reasonOid, + sizeof(reasonOid)); + sz = WB_SEQ(list, seqBuf, idx2); + reasonCode = -1; + ret = ParseCRL_EntryExtensions(list, 0, sz, &reasonCode, NULL); + WB_CHECK(reasonCode == -1, "reason OID with no value (probe fails)"); + } + + /* (d) reason OID whose OCTET STRING holds an INTEGER instead of an + * ENUMERATED -> the value probe's "tag == ASN_ENUMERATED" + * operand false. */ + { + static const byte reasonOid[] = { 0x55, 0x1d, 0x15 }; + byte inner[8], octet[16]; + word32 innerSz, octetSz; + + b = 0x02; + innerSz = wb_tlv(inner, ASN_INTEGER, &b, 1); + octetSz = wb_tlv(octet, ASN_OCTET_STRING, inner, innerSz); + idx2 = 0; + idx2 += wb_tlv(seqBuf + idx2, ASN_OBJECT_ID, reasonOid, + sizeof(reasonOid)); + XMEMCPY(seqBuf + idx2, octet, octetSz); + idx2 += octetSz; + sz = WB_SEQ(list, seqBuf, idx2); + reasonCode = -1; + ret = ParseCRL_EntryExtensions(list, 0, sz, &reasonCode, NULL); + WB_CHECK(reasonCode == -1, "reason value is INTEGER, not ENUMERATED"); + } + + /* (e) reason ENUMERATED carrying TWO content bytes -> the + * "reasonLen == 1" operand false, so no reason is recorded. */ + { + static const byte reasonOid[] = { 0x55, 0x1d, 0x15 }; + byte two[2] = { 0x00, 0x02 }; + byte inner[8], octet[16]; + word32 innerSz, octetSz; + + innerSz = wb_tlv(inner, ASN_ENUMERATED, two, sizeof(two)); + octetSz = wb_tlv(octet, ASN_OCTET_STRING, inner, innerSz); + idx2 = 0; + idx2 += wb_tlv(seqBuf + idx2, ASN_OBJECT_ID, reasonOid, + sizeof(reasonOid)); + XMEMCPY(seqBuf + idx2, octet, octetSz); + idx2 += octetSz; + sz = WB_SEQ(list, seqBuf, idx2); + reasonCode = -1; + ret = ParseCRL_EntryExtensions(list, 0, sz, &reasonCode, NULL); + WB_CHECK(reasonCode == -1, "reason ENUMERATED with length != 1"); + } + (void)ret; + } + /* Unknown (non-reason) OID, not critical, dcrl==NULL (no callback * dispatch possible) -> :36891 1st operand false (short-circuit); * :36935 critical operand false -> ignored, ret==0. */ @@ -1083,6 +1179,24 @@ static void wb_parse_crl_entry_extensions(void) ":36935 both true (unknown critical extension, no callback)"); #ifdef WC_ASN_UNKNOWN_EXT_CB + /* Unknown OID, non-critical, dcrl != NULL but NEITHER callback + * registered -> :36891 1st operand true, both callback operands false, + * so the whole dispatch decision is false. Every other dcrl!=NULL row + * below registers at least one callback, and every no-callback row above + * passes dcrl==NULL (which short-circuits on the 1st operand), so this + * is the only row that can pair with them on the 2nd operand. */ + dcrl.unknownExtCallback = NULL; + dcrl.unknownExtCallbackEx = NULL; + { + byte val[2] = { 0xAA, 0xBB }; + sz = wb_ext(list, wbOidOther, sizeof(wbOidOther), 1, 0, val, + sizeof(val)); + } + reasonCode = -1; + ret = ParseCRL_EntryExtensions(list, 0, sz, &reasonCode, &dcrl); + WB_CHECK(ret == 0, + ":36891 dcrl!=NULL with no callbacks (2nd/3rd operands false)"); + /* Unknown OID, CRITICAL, dcrl!=NULL with a registered callback that * accepts it (returns 0) -> :36891/:36892 both true (via the 1st * disjunct), :36917 both true, :36935 not reached (handled=1). */ @@ -1100,10 +1214,72 @@ static void wb_parse_crl_entry_extensions(void) ":36891/:36892 true via unknownExtCallback!=NULL; :36917 both true"); /* Same, but only unknownExtCallbackEx registered -> :36891/:36892 true - * via the 2nd disjunct; :36917 2nd operand false (unknownExtCallback == - * NULL); :36921 both true. */ + * via the 2nd disjunct (unknownExtCallback == NULL is the FALSE half of + * that operand); :36917 2nd operand false (unknownExtCallback == NULL); + * :36921 both true. */ dcrl.unknownExtCallback = NULL; - dcrl.unknownExtCallbackEx = NULL; /* set below via a plain function ptr */ + dcrl.unknownExtCallbackEx = wb_entry_ext_cb_ex; + wbEntryCbExCalls = 0; + wbEntryCbExRet = 0; + dcrl.unknownExtCallbackExCtx = NULL; + { + byte val[2] = { 0xAA, 0xBB }; + sz = wb_ext(list, wbOidOther, sizeof(wbOidOther), 1, 1, val, + sizeof(val)); + } + reasonCode = -1; + ret = ParseCRL_EntryExtensions(list, 0, sz, &reasonCode, &dcrl); + WB_CHECK(ret == 0 && wbEntryCbExCalls == 1, + "Ex-only callback: :36891 1st disjunct false, :36917 2nd false, " + ":36921 both true"); + + /* First callback registered and REJECTING (returns non-zero): the + * cbRet==0 operand of the second dispatch decision (:36921) is then + * false, which no accepting-callback run can produce. Both callbacks are + * registered so the Ex operand stays true-capable but is never reached. */ + dcrl.unknownExtCallback = wb_entry_ext_cb; + dcrl.unknownExtCallbackEx = wb_entry_ext_cb_ex; + wbEntryCbCalls = 0; + wbEntryCbExCalls = 0; + wbEntryCbRet = -1; + { + byte val[2] = { 0xAA, 0xBB }; + sz = wb_ext(list, wbOidOther, sizeof(wbOidOther), 1, 0, val, + sizeof(val)); + } + reasonCode = -1; + ret = ParseCRL_EntryExtensions(list, 0, sz, &reasonCode, &dcrl); + WB_CHECK(ret != 0 && wbEntryCbCalls == 1 && wbEntryCbExCalls == 0, + ":36921 1st operand false (first callback rejected)"); + wbEntryCbRet = 0; + + /* An OID with more sub-identifiers than DecodeObjectId()'s output buffer + * holds: GetASN_ObjectId() still accepts the encoding, so the dispatch + * block is entered, but DecodeObjectId() returns BUFFER_E -- the only way + * to make the cbRet==0 operand of the FIRST dispatch decision (:36917) + * false. */ + { + byte longOid[MAX_OID_SZ + 4]; + byte val[2] = { 0xAA, 0xBB }; + word32 i; + + /* Every octet < 0x80 is a complete single-octet sub-identifier, so + * this is a well-formed OID body with MAX_OID_SZ+4 arcs. */ + for (i = 0; i < (word32)sizeof(longOid); i++) { + longOid[i] = (byte)(0x2A + (i & 0x1F)); + } + sz = wb_ext(list, longOid, (word32)sizeof(longOid), 1, 0, val, + sizeof(val)); + } + dcrl.unknownExtCallback = wb_entry_ext_cb; + dcrl.unknownExtCallbackEx = wb_entry_ext_cb_ex; + wbEntryCbCalls = 0; + wbEntryCbExCalls = 0; + reasonCode = -1; + ret = ParseCRL_EntryExtensions(list, 0, sz, &reasonCode, &dcrl); + WB_CHECK(wbEntryCbCalls == 0 && wbEntryCbExCalls == 0, + ":36917/:36921 1st operand false (DecodeObjectId overflow)"); + (void)ret; #endif /* WC_ASN_UNKNOWN_EXT_CB */ FreeDecodedCRL(&dcrl); @@ -1122,11 +1298,14 @@ static word32 wb_build_crl_number_ext(byte* out, const byte* intContent, word32 intContentSz) { byte intTlv[32]; - byte octet[36]; word32 intSz = wb_tlv(intTlv, ASN_INTEGER, intContent, intContentSz); - word32 octetSz = wb_tlv(octet, ASN_OCTET_STRING, intTlv, intSz); - return wb_ext(out, wbOidCrlNumber, sizeof(wbOidCrlNumber), 0, 0, octet, - octetSz); + + /* wb_ext() already wraps its value argument in the extension's OCTET + * STRING, so the INTEGER TLV is handed over bare -- wrapping it here as + * well produced an OCTET STRING inside an OCTET STRING, which the + * decoder rejected before ever reaching the CRL-number logic. */ + return wb_ext(out, wbOidCrlNumber, sizeof(wbOidCrlNumber), 0, 0, intTlv, + intSz); } static void wb_parse_crl_extensions(void) @@ -1537,6 +1716,21 @@ static void wb_make_crl_ex(void) 0, CTC_SHA256wRSA, 1, NULL, 0); WB_CHECK(ret > need, ":37906 both true (nextDate present, larger encoding)"); + /* nextDate present but nextDateFmt == 0 -> :37906 2nd operand false + * (the encoding matches the no-nextDate baseline). Without this row the + * 2nd operand is only ever seen true. */ + ret = wc_MakeCRL_ex(issuer, sizeof(issuer), lastDate, + ASN_GENERALIZED_TIME, nextDate, 0 /* no format */, NULL, NULL, + 0, CTC_SHA256wRSA, 1, NULL, 0); + WB_CHECK(ret == need, ":37906 2nd operand false (nextDateFmt==0)"); + + /* crlNumber present, version >= 2, but crlNumberSz == 0 -> :37924 2nd + * operand false. */ + ret = wc_MakeCRL_ex(issuer, sizeof(issuer), lastDate, + ASN_GENERALIZED_TIME, NULL, 0, NULL, crlNum, 0 /* zero size */, + CTC_SHA256wRSA, 2 /* v2 */, NULL, 0); + WB_CHECK(ret > 0, ":37924 2nd operand false (crlNumberSz==0)"); + /* crlNumber present but version < 2 -> :37924 3rd operand false * (version>=2 required); crlNumber ignored. */ ret = wc_MakeCRL_ex(issuer, sizeof(issuer), lastDate, From bacdbb2fb475ef348b11bd94db637446f4e4eaa7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 17:54:49 +0200 Subject: [PATCH 12/20] tests: add hash and mp fault injectors, drive the stateful-hash error chains --- tests/unit-mcdc/mcdc_fault_hash.h | 426 ++++++++++++ tests/unit-mcdc/mcdc_fault_mp.h | 370 +++++++++++ tests/unit-mcdc/test_dh_fault_whitebox.c | 174 +++++ tests/unit-mcdc/test_dsa_fault_whitebox.c | 98 +++ tests/unit-mcdc/test_eccsi_fault_whitebox.c | 187 ++++++ .../test_frodokem_mat_hash_fault_whitebox.c | 318 +++++++++ .../unit-mcdc/test_lms_hash_fault_whitebox.c | 617 ++++++++++++++++++ tests/unit-mcdc/test_sakke_fault_whitebox.c | 114 ++++ .../test_slhdsa_hash_fault_whitebox.c | 359 ++++++++++ tests/unit-mcdc/test_tfm_whitebox.c | 116 ++++ .../unit-mcdc/test_xmss_hash_fault_whitebox.c | 422 ++++++++++++ 11 files changed, 3201 insertions(+) create mode 100644 tests/unit-mcdc/mcdc_fault_hash.h create mode 100644 tests/unit-mcdc/mcdc_fault_mp.h create mode 100644 tests/unit-mcdc/test_frodokem_mat_hash_fault_whitebox.c create mode 100644 tests/unit-mcdc/test_lms_hash_fault_whitebox.c create mode 100644 tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c create mode 100644 tests/unit-mcdc/test_xmss_hash_fault_whitebox.c diff --git a/tests/unit-mcdc/mcdc_fault_hash.h b/tests/unit-mcdc/mcdc_fault_hash.h new file mode 100644 index 00000000000..aa65d8c9018 --- /dev/null +++ b/tests/unit-mcdc/mcdc_fault_hash.h @@ -0,0 +1,426 @@ +/* mcdc_fault_hash.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * mcdc_fault_hash.h -- header-only, self-contained HASH/BLOCK-CIPHER primitive + * fault injector for the per-module MC/DC campaign. It is the second lever + * beside mcdc_fault_alloc.h, and the ONLY one that works for the hash-based + * signature/KEM engines. + * + * WHY A SECOND LEVER + * ------------------ + * The dominant justified-residual class campaign-wide is the FALSE half of a + * success chain: + * + * if ((ret == 0) && ) ... + * for (i = 0; (ret == 0) && (i < n); i++) ... + * while ((ret == 0) && ...) ... + * + * For heap-driven code mcdc_fault_alloc.h breaks the chain by failing the n-th + * XMALLOC. But wc_lms_impl.c, wc_xmss_impl.c, wc_slhdsa.c and + * wc_frodokem_mat.c contain ZERO (or nearly zero) allocations: their `ret` + * comes exclusively from SHA-2 / SHA-3(SHAKE) / AES-ECB primitive calls, and + * sha256.c / sha512.c / sha3.c never touch the allocator on the paths those + * engines take. No heap-fault index can make them fail -- see the long note in + * test_frodokem_fault_common.h, which names this technique as the missing one. + * + * HOW IT WORKS -- MACRO INTERPOSITION + * ----------------------------------- + * Every white-box TU in this campaign #includes the involved .c directly, and + * the harness links it against libwolfssl.a with only that one object trimmed. + * The primitives above therefore still come from the archive and cannot be + * replaced at link time -- but they CAN be replaced at preprocessing time, + * for this translation unit only: + * + * #include "mcdc_fault_hash.h" <-- wrappers + macros + * #include <-- sees the macros + * + * This header first #includes the real wolfSSL hash/AES headers and defines + * static wrapper functions that call the REAL primitives, and only THEN + * #defines the primitive names to the wrappers. Ordering is load-bearing: the + * wrappers are compiled before the macros exist, so they still reach the real + * implementations, and the API declarations in the headers are never rewritten. + * (Same trick as test_tsp_fault_whitebox.c's XGMTIME mock, generalised.) + * + * INTENDED SWEEP PATTERN + * ---------------------- + * mcdc_fh_disarm(); -- baseline: everything succeeds + * Lifecycle(...); -- the TRUE half of every guard + * total = mcdc_fh_seen(); -- how many primitive calls that took + * for (n = 1; n <= total; n += stride) { + * mcdc_fh_arm(n); -- n-th primitive call (and every later + * (void)Lifecycle(...); one) returns BAD_FUNC_ARG + * mcdc_fh_disarm(); + * } + * + * Semantics match mcdc_fault_alloc.h exactly: arm(n) fails call n AND every + * later one, so an armed region must span ONE operation whose inputs were + * built while disarmed. Because ret propagates and short-circuits the chain, + * failing "from n onwards" is what actually drives the (ret == 0) operand + * false at every downstream decision the operation would have reached. + * + * CRASH SAFETY + * ------------ + * A faulted primitive leaves its output buffer untouched, exactly as a real + * hardware/driver failure would. The engine under test must propagate the + * error and clean up -- exercising that propagation is the entire point. The + * harness must never consume an output produced by an armed call. + * + * PORTABILITY + * ----------- + * Each wrapper/macro pair is behind the same feature guard as the declaration + * it shadows, so a TU that includes this header builds under every campaign + * variant (in a build where a primitive is compiled out, nothing is + * interposed). Unused wrappers are ordinary unused static helpers. + * + * A TU that must NOT interpose a particular family can define + * MCDC_FH_NO_SHA256 / MCDC_FH_NO_SHA512 / MCDC_FH_NO_SHAKE / MCDC_FH_NO_AES / + * MCDC_FH_NO_HMAC before including this header. + */ + +#ifndef MCDC_FAULT_HASH_H +#define MCDC_FAULT_HASH_H + +/* Must be first: establishes BUILDING_WOLFSSL / config.h exactly the way every + * wolfcrypt .c does, so including this header before the involved .c does not + * change how that .c sees the world. */ +#include + +#include +#include + +#if !defined(NO_SHA256) && !defined(MCDC_FH_NO_SHA256) + #include + #define MCDC_FH_HAVE_SHA256 +#endif +#if defined(WOLFSSL_SHA512) && !defined(MCDC_FH_NO_SHA512) + #include + #define MCDC_FH_HAVE_SHA512 +#endif +#if (defined(WOLFSSL_SHAKE128) || defined(WOLFSSL_SHAKE256)) && \ + !defined(MCDC_FH_NO_SHAKE) + #include + #define MCDC_FH_HAVE_SHAKE +#endif +#if !defined(NO_AES) && !defined(MCDC_FH_NO_AES) + #include + #define MCDC_FH_HAVE_AES +#endif +#if !defined(NO_HMAC) && !defined(MCDC_FH_NO_HMAC) + #include + #define MCDC_FH_HAVE_HMAC +#endif + +/* Error the faulted primitive reports. Any non-zero value drives the + * (ret == 0) operand false; BAD_FUNC_ARG is defined by error-crypt.h in every + * configuration and is already the code these engines propagate for a rejected + * primitive call. */ +#define MCDC_FH_ERR BAD_FUNC_ARG + +/* Not every interposer is used by every white-box / variant; silence the + * -Wunused-function noise that would otherwise swamp the wb build logs. */ +#if defined(__GNUC__) || defined(__clang__) + #define MCDC_FH_MAYBE_UNUSED __attribute__((unused)) +#else + #define MCDC_FH_MAYBE_UNUSED +#endif + +/* file-static injector state (one TU per white-box, so file scope is fine) */ +static long mcdc_fh_count = 0; /* primitive calls seen since arm/disarm */ +static long mcdc_fh_fail_at = 0; /* fail from this index on; 0 = off */ + +/* Count this call; report whether it must fail. */ +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_hit(void) +{ + mcdc_fh_count++; + return (mcdc_fh_fail_at != 0) && (mcdc_fh_count >= mcdc_fh_fail_at); +} + +/* Arm: the n-th primitive call from now on (and every later one) fails. */ +MCDC_FH_MAYBE_UNUSED static void mcdc_fh_arm(long n) +{ + mcdc_fh_count = 0; + mcdc_fh_fail_at = (n > 0) ? n : 0; +} + +/* Disarm and reset the counter, so a following unarmed run can be measured. */ +MCDC_FH_MAYBE_UNUSED static void mcdc_fh_disarm(void) +{ + mcdc_fh_fail_at = 0; + mcdc_fh_count = 0; +} + +/* Primitive calls counted since the last arm()/disarm(). Used to size a + * sweep: run the target disarmed, then sweep 1..mcdc_fh_seen(). */ +MCDC_FH_MAYBE_UNUSED static long mcdc_fh_seen(void) +{ + return mcdc_fh_count; +} + +/* ---------------- SHA-256 ---------------- */ +#ifdef MCDC_FH_HAVE_SHA256 +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Sha256Update(wc_Sha256* sha, const byte* data, word32 len) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Sha256Update(sha, data, len); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Sha256Final(wc_Sha256* sha, byte* hash) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Sha256Final(sha, hash); +} +#if defined(WOLFSSL_HAVE_LMS) && !defined(WOLFSSL_LMS_FULL_HASH) +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Sha256HashBlock(wc_Sha256* sha, const unsigned char* data, + unsigned char* hash) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Sha256HashBlock(sha, data, hash); +} +#endif +#endif /* MCDC_FH_HAVE_SHA256 */ + +/* ---------------- SHA-512 ---------------- */ +#ifdef MCDC_FH_HAVE_SHA512 +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Sha512Update(wc_Sha512* sha, const byte* data, word32 len) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Sha512Update(sha, data, len); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Sha512Final(wc_Sha512* sha, byte* hash) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Sha512Final(sha, hash); +} +#endif /* MCDC_FH_HAVE_SHA512 */ + +/* ---------------- SHAKE128 / SHAKE256 ---------------- */ +#ifdef MCDC_FH_HAVE_SHAKE +/* The wc_InitShake* interposers are OPT-IN (MCDC_FH_WITH_SHAKE_INIT): a + * white-box that initialises its own SHAKE contexts for test setup must not + * have those setup calls faulted. wc_frodokem_mat.c is the case that needs + * them, because its `if (p->useShake256 && ((ret = wc_InitShake256(...)) == 0))` + * guard has no other way to take its second operand false. */ +#ifdef MCDC_FH_WITH_SHAKE_INIT +#ifdef WOLFSSL_SHAKE128 +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_InitShake128(wc_Shake* shake, void* heap, int devId) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_InitShake128(shake, heap, devId); +} +#endif +#ifdef WOLFSSL_SHAKE256 +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_InitShake256(wc_Shake* shake, void* heap, int devId) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_InitShake256(shake, heap, devId); +} +#endif +#endif /* MCDC_FH_WITH_SHAKE_INIT */ +#ifdef WOLFSSL_SHAKE128 +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Shake128_Update(wc_Shake* shake, const byte* data, + word32 len) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Shake128_Update(shake, data, len); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Shake128_Final(wc_Shake* shake, byte* hash, word32 hashLen) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Shake128_Final(shake, hash, hashLen); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Shake128_Absorb(wc_Shake* shake, const byte* data, + word32 len) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Shake128_Absorb(shake, data, len); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Shake128_SqueezeBlocks(wc_Shake* shake, byte* out, + word32 blockCnt) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Shake128_SqueezeBlocks(shake, out, blockCnt); +} +#endif /* WOLFSSL_SHAKE128 */ +#ifdef WOLFSSL_SHAKE256 +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Shake256_Update(wc_Shake* shake, const byte* data, + word32 len) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Shake256_Update(shake, data, len); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Shake256_Final(wc_Shake* shake, byte* hash, word32 hashLen) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Shake256_Final(shake, hash, hashLen); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Shake256_Absorb(wc_Shake* shake, const byte* data, + word32 len) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Shake256_Absorb(shake, data, len); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_Shake256_SqueezeBlocks(wc_Shake* shake, byte* out, + word32 blockCnt) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_Shake256_SqueezeBlocks(shake, out, blockCnt); +} +#endif /* WOLFSSL_SHAKE256 */ +#endif /* MCDC_FH_HAVE_SHAKE */ + +/* ---------------- HMAC (SLH-DSA's SHA-2 PRF_msg / H_msg) ---------------- */ +#ifdef MCDC_FH_HAVE_HMAC +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_HmacSetKey(Hmac* hmac, int type, + const byte* key, word32 keySz) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_HmacSetKey(hmac, type, key, keySz); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_HmacUpdate(Hmac* hmac, const byte* in, + word32 sz) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_HmacUpdate(hmac, in, sz); +} +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_HmacFinal(Hmac* hmac, byte* out) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_HmacFinal(hmac, out); +} +#endif /* MCDC_FH_HAVE_HMAC */ + +/* ---------------- AES-ECB (FrodoKEM matrix generation) ---------------- */ +#if defined(MCDC_FH_HAVE_AES) && defined(HAVE_AES_ECB) +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_AesEcbEncrypt(Aes* aes, byte* out, const byte* in, + word32 sz) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_AesEcbEncrypt(aes, out, in, sz); +} +#endif +#if defined(MCDC_FH_HAVE_AES) && defined(WOLFSSL_AES_DIRECT) +MCDC_FH_MAYBE_UNUSED static int mcdc_fh_AesSetKeyDirect(Aes* aes, const byte* key, word32 len, + const byte* iv, int dir) +{ + if (mcdc_fh_hit()) + return MCDC_FH_ERR; + return wc_AesSetKeyDirect(aes, key, len, iv, dir); +} +#endif + +/* ------------------------------------------------------------------------ + * Install the interposers. Everything above is already compiled against the + * REAL primitives; from here on the name means the wrapper. The involved .c + * must be #included AFTER this point. + * ---------------------------------------------------------------------- */ +#ifdef MCDC_FH_HAVE_SHA256 + #undef wc_Sha256Update + #define wc_Sha256Update(a, b, c) mcdc_fh_Sha256Update((a), (b), (c)) + #undef wc_Sha256Final + #define wc_Sha256Final(a, b) mcdc_fh_Sha256Final((a), (b)) + #if defined(WOLFSSL_HAVE_LMS) && !defined(WOLFSSL_LMS_FULL_HASH) + #undef wc_Sha256HashBlock + #define wc_Sha256HashBlock(a, b, c) \ + mcdc_fh_Sha256HashBlock((a), (b), (c)) + #endif +#endif + +#ifdef MCDC_FH_HAVE_SHA512 + #undef wc_Sha512Update + #define wc_Sha512Update(a, b, c) mcdc_fh_Sha512Update((a), (b), (c)) + #undef wc_Sha512Final + #define wc_Sha512Final(a, b) mcdc_fh_Sha512Final((a), (b)) +#endif + +#ifdef MCDC_FH_HAVE_SHAKE +#ifdef MCDC_FH_WITH_SHAKE_INIT + #ifdef WOLFSSL_SHAKE128 + #undef wc_InitShake128 + #define wc_InitShake128(a, b, c) mcdc_fh_InitShake128((a), (b), (c)) + #endif + #ifdef WOLFSSL_SHAKE256 + #undef wc_InitShake256 + #define wc_InitShake256(a, b, c) mcdc_fh_InitShake256((a), (b), (c)) + #endif +#endif +#ifdef WOLFSSL_SHAKE128 + #undef wc_Shake128_Update + #define wc_Shake128_Update(a, b, c) mcdc_fh_Shake128_Update((a), (b), (c)) + #undef wc_Shake128_Final + #define wc_Shake128_Final(a, b, c) mcdc_fh_Shake128_Final((a), (b), (c)) + #undef wc_Shake128_Absorb + #define wc_Shake128_Absorb(a, b, c) mcdc_fh_Shake128_Absorb((a), (b), (c)) + #undef wc_Shake128_SqueezeBlocks + #define wc_Shake128_SqueezeBlocks(a, b, c) \ + mcdc_fh_Shake128_SqueezeBlocks((a), (b), (c)) +#endif +#ifdef WOLFSSL_SHAKE256 + #undef wc_Shake256_Update + #define wc_Shake256_Update(a, b, c) mcdc_fh_Shake256_Update((a), (b), (c)) + #undef wc_Shake256_Final + #define wc_Shake256_Final(a, b, c) mcdc_fh_Shake256_Final((a), (b), (c)) + #undef wc_Shake256_Absorb + #define wc_Shake256_Absorb(a, b, c) mcdc_fh_Shake256_Absorb((a), (b), (c)) + #undef wc_Shake256_SqueezeBlocks + #define wc_Shake256_SqueezeBlocks(a, b, c) \ + mcdc_fh_Shake256_SqueezeBlocks((a), (b), (c)) +#endif +#endif /* MCDC_FH_HAVE_SHAKE */ + +#ifdef MCDC_FH_HAVE_HMAC + #undef wc_HmacSetKey + #define wc_HmacSetKey(a, b, c, d) mcdc_fh_HmacSetKey((a), (b), (c), (d)) + #undef wc_HmacUpdate + #define wc_HmacUpdate(a, b, c) mcdc_fh_HmacUpdate((a), (b), (c)) + #undef wc_HmacFinal + #define wc_HmacFinal(a, b) mcdc_fh_HmacFinal((a), (b)) +#endif + +#if defined(MCDC_FH_HAVE_AES) && defined(HAVE_AES_ECB) + #undef wc_AesEcbEncrypt + #define wc_AesEcbEncrypt(a, b, c, d) mcdc_fh_AesEcbEncrypt((a), (b), (c), (d)) +#endif +#if defined(MCDC_FH_HAVE_AES) && defined(WOLFSSL_AES_DIRECT) + #undef wc_AesSetKeyDirect + #define wc_AesSetKeyDirect(a, b, c, d, e) \ + mcdc_fh_AesSetKeyDirect((a), (b), (c), (d), (e)) +#endif + +#endif /* MCDC_FAULT_HASH_H */ diff --git a/tests/unit-mcdc/mcdc_fault_mp.h b/tests/unit-mcdc/mcdc_fault_mp.h new file mode 100644 index 00000000000..f60968f03e3 --- /dev/null +++ b/tests/unit-mcdc/mcdc_fault_mp.h @@ -0,0 +1,370 @@ +/* mcdc_fault_mp.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * mcdc_fault_mp.h -- header-only big-integer (mp_*) fault injector for the + * per-module MC/DC campaign. Third lever, beside mcdc_fault_alloc.h (heap) and + * mcdc_fault_hash.h (hash / block-cipher primitives). + * + * WHY + * --- + * dh.c, dsa.c, eccsi.c and sakke.c are written as long big-integer success + * chains: + * + * if (ret == 0 && mp_copy(&key->p, p) != MP_OKAY) ... + * if (ret == 0 && mp_to_unsigned_bin(y, pub) != MP_OKAY) ... + * while ((err == 0) && (mp_iszero(ssk) || ...)) ... + * for (i = ...; (err == 0) && (i >= 0); i--) ... + * + * BOTH operands of these are residual, for the same underlying reason: on a + * healthy machine no mp_* call ever fails, so + * - operand 1 (`mp_xxx(...) != MP_OKAY`) is never TRUE, and + * - operand 0 (`ret == 0`) is never FALSE, because nothing upstream failed. + * The heap-fault lever only reaches these where the mp_int scratch itself is + * heap-allocated (the small_stack variants), and even there it can only make + * the ALLOCATION fail, never a computation. + * + * HOW + * --- + * Same macro-interposition trick as mcdc_fault_hash.h, applied to the mp_* + * API: this header defines typed wrapper functions that call the REAL mp_* + * entry point, and only THEN #defines the mp_* names to the wrappers. The + * ordering is load-bearing -- the wrappers are compiled while mp_copy still + * means sp_copy (or the integer.c/tfm.c function, depending on the math + * backend), so they reach the genuine implementation. The involved .c must be + * #included AFTER this header. + * + * mcdc_fm_arm(n) makes the n-th mp_* call -- and every later one -- return + * MP_VAL, exactly mirroring mcdc_fa_arm()/mcdc_fh_arm(). Sweeping n therefore + * - drives operand 1 TRUE at the call site whose index is n, and + * - drives operand 0 FALSE at every guard downstream of it, + * which is precisely the pair of residuals above, from one sweep. + * + * ONLY value-returning COMPUTATION operations are interposed. Predicates + * (mp_iszero / mp_cmp / mp_count_bits / mp_unsigned_bin_size) and teardown + * (mp_clear / mp_free / mp_forcezero) are deliberately left alone: faulting + * them would change program meaning rather than inject an error, and cleanup + * must keep working so an armed call stays crash-safe. + * + * SWEEP PATTERN + * mcdc_fm_disarm(); -- baseline: everything succeeds (the TRUE + * Target(...); half of every guard, same binary) + * k = mcdc_fm_seen(); -- number of mp_* calls that took + * for (n = 1; n <= k; n++) { + * ...rebuild inputs while DISARMED... + * mcdc_fm_arm(n); + * (void)Target(...); + * mcdc_fm_disarm(); + * } + * + * PORTABILITY + * ----------- + * The wrappers use mp_int / mp_digit / WC_RNG, which every math backend + * (sp_int.h, integer.h, tfm.h) provides, and non-const pointer parameters so + * they bind under all three. A backend that does not provide one of the + * wrapped entry points is handled by the per-function guards below; a TU that + * must not interpose a given call can #undef that macro after including this + * header. + */ + +#ifndef MCDC_FAULT_MP_H +#define MCDC_FAULT_MP_H + +/* Must be first: same include prologue every wolfcrypt .c uses, so including + * this header before the involved .c does not change how that .c sees the + * world. */ +#include + +#include +#include +#include +#include + +/* MP_VAL is defined by every math backend and is the code these callers + * already propagate for a rejected big-integer operation. */ +#define MCDC_FM_ERR MP_VAL + +#if defined(__GNUC__) || defined(__clang__) + #define MCDC_FM_MAYBE_UNUSED __attribute__((unused)) +#else + #define MCDC_FM_MAYBE_UNUSED +#endif + +/* file-static injector state (one TU per white-box, so file scope is fine) */ +static long mcdc_fm_count = 0; /* mp_* calls seen since arm/disarm */ +static long mcdc_fm_fail_at = 0; /* fail from this index on; 0 = off */ + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_hit(void) +{ + mcdc_fm_count++; + return (mcdc_fm_fail_at != 0) && (mcdc_fm_count >= mcdc_fm_fail_at); +} + +/* Arm: the n-th mp_* call from now on (and every later one) returns MP_VAL. */ +MCDC_FM_MAYBE_UNUSED static void mcdc_fm_arm(long n) +{ + mcdc_fm_count = 0; + mcdc_fm_fail_at = (n > 0) ? n : 0; +} + +MCDC_FM_MAYBE_UNUSED static void mcdc_fm_disarm(void) +{ + mcdc_fm_fail_at = 0; + mcdc_fm_count = 0; +} + +/* mp_* calls counted since the last arm()/disarm(); sizes the sweep. */ +MCDC_FM_MAYBE_UNUSED static long mcdc_fm_seen(void) +{ + return mcdc_fm_count; +} + +/* ---- wrappers (compiled while the mp_* names still mean the real thing) -- */ + +/* Input operands are taken as `const mp_int*` and cast on the way through: + * sp_int.h declares them const while integer.h / tfm.h do not, and callers in + * dh.c / dsa.c / eccsi.c / sakke.c pass both. Accepting const and casting is + * the one signature that binds cleanly under every math backend without + * emitting -Wincompatible-pointer-types-discards-qualifiers at the call site. + * The cast is safe: the wrapper only forwards the pointer to the real + * operation, which does not write through it. */ +#define MCDC_FM_MI(x) ((mp_int*)(x)) + +#define MCDC_FM_W0(nm, fn) \ + MCDC_FM_MAYBE_UNUSED static int nm(mp_int* a) \ + { if (mcdc_fm_hit()) return MCDC_FM_ERR; return fn(a); } +#define MCDC_FM_W2(nm, fn) \ + MCDC_FM_MAYBE_UNUSED static int nm(const mp_int* a, mp_int* r) \ + { if (mcdc_fm_hit()) return MCDC_FM_ERR; return fn(MCDC_FM_MI(a), r); } +#define MCDC_FM_W3(nm, fn) \ + MCDC_FM_MAYBE_UNUSED static int nm(const mp_int* a, const mp_int* b, \ + mp_int* r) \ + { if (mcdc_fm_hit()) return MCDC_FM_ERR; \ + return fn(MCDC_FM_MI(a), MCDC_FM_MI(b), r); } +#define MCDC_FM_W4(nm, fn) \ + MCDC_FM_MAYBE_UNUSED static int nm(const mp_int* a, const mp_int* b, \ + const mp_int* c, mp_int* r) \ + { if (mcdc_fm_hit()) return MCDC_FM_ERR; \ + return fn(MCDC_FM_MI(a), MCDC_FM_MI(b), MCDC_FM_MI(c), r); } +#define MCDC_FM_WD3(nm, fn) \ + MCDC_FM_MAYBE_UNUSED static int nm(const mp_int* a, mp_digit d, \ + mp_int* r) \ + { if (mcdc_fm_hit()) return MCDC_FM_ERR; \ + return fn(MCDC_FM_MI(a), d, r); } + +#ifdef MCDC_FM_WITH_INIT +MCDC_FM_W0(mcdc_fm_init, mp_init) +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_init_multi(mp_int* a, mp_int* b, + mp_int* c, mp_int* d, mp_int* e, mp_int* f) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_init_multi(a, b, c, d, e, f); +} +#endif /* MCDC_FM_WITH_INIT */ +MCDC_FM_W2(mcdc_fm_copy, mp_copy) +MCDC_FM_W3(mcdc_fm_add, mp_add) +MCDC_FM_W3(mcdc_fm_sub, mp_sub) +MCDC_FM_W3(mcdc_fm_mul, mp_mul) +MCDC_FM_W3(mcdc_fm_mod, mp_mod) +MCDC_FM_W3(mcdc_fm_invmod, mp_invmod) +MCDC_FM_W2(mcdc_fm_sqr, mp_sqr) +MCDC_FM_W4(mcdc_fm_mulmod, mp_mulmod) +MCDC_FM_W4(mcdc_fm_addmod, mp_addmod) +MCDC_FM_W4(mcdc_fm_submod, mp_submod) +MCDC_FM_W4(mcdc_fm_exptmod, mp_exptmod) +MCDC_FM_WD3(mcdc_fm_add_d, mp_add_d) +MCDC_FM_WD3(mcdc_fm_sub_d, mp_sub_d) +MCDC_FM_WD3(mcdc_fm_mul_d, mp_mul_d) + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_set(mp_int* a, mp_digit d) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_set(a, d); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_read_unsigned_bin(mp_int* a, + const byte* in, word32 inSz) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_read_unsigned_bin(a, in, inSz); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_to_unsigned_bin(const mp_int* a, + byte* out) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_to_unsigned_bin(MCDC_FM_MI(a), out); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_to_unsigned_bin_len(const mp_int* a, + byte* out, int outSz) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_to_unsigned_bin_len(MCDC_FM_MI(a), out, outSz); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_read_radix(mp_int* a, const char* in, + int radix) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_read_radix(a, in, radix); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_exptmod_ex(const mp_int* b, + const mp_int* e, int digits, const mp_int* m, mp_int* r) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_exptmod_ex(MCDC_FM_MI(b), MCDC_FM_MI(e), digits, MCDC_FM_MI(m), + r); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_prime_is_prime(const mp_int* a, int t, + int* result) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_prime_is_prime(MCDC_FM_MI(a), t, result); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_prime_is_prime_ex(const mp_int* a, + int t, int* result, WC_RNG* rng) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_prime_is_prime_ex(MCDC_FM_MI(a), t, result, rng); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_rand_prime(mp_int* r, int len, + WC_RNG* rng, void* heap) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_rand_prime(r, len, rng, heap); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_montgomery_setup(const mp_int* m, + mp_digit* rho) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_montgomery_setup(MCDC_FM_MI(m), rho); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_montgomery_reduce(mp_int* a, + const mp_int* m, mp_digit mp) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_montgomery_reduce(a, MCDC_FM_MI(m), mp); +} + +MCDC_FM_MAYBE_UNUSED static int mcdc_fm_montgomery_calc_normalization( + mp_int* a, const mp_int* b) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_montgomery_calc_normalization(a, MCDC_FM_MI(b)); +} + +/* ------------------------------------------------------------------------ + * Install the interposers. Everything above is already bound to the REAL + * mp_* entry points; from here on the name means the wrapper. The involved + * .c must be #included AFTER this point. + * ---------------------------------------------------------------------- */ +/* mp_init / mp_init_multi are OPT-IN (MCDC_FM_WITH_INIT), and off by default, + * because faulting INITIALISATION is not crash-safe in general: a caller that + * writes `err = mp_init_multi(a, b, ...)` (rather than mapping the failure to + * MP_INIT_E) then runs a cleanup path that mp_clear()s objects the failed + * init never constructed -- a segfault on heap-allocated scratch under + * WOLFSSL_SMALL_STACK. dsa.c does exactly that at three call sites. Faulting + * the COMPUTATION calls below drives the same residual operands without ever + * leaving an mp_int unconstructed. */ +#ifdef MCDC_FM_WITH_INIT + #undef mp_init + #define mp_init(a) mcdc_fm_init((a)) + #undef mp_init_multi + #define mp_init_multi(a, b, c, d, e, f) \ + mcdc_fm_init_multi((a), (b), (c), (d), (e), (f)) +#endif +#undef mp_copy +#define mp_copy(a, b) mcdc_fm_copy((a), (b)) +#undef mp_set +#define mp_set(a, d) mcdc_fm_set((a), (d)) +#undef mp_add +#define mp_add(a, b, c) mcdc_fm_add((a), (b), (c)) +#undef mp_sub +#define mp_sub(a, b, c) mcdc_fm_sub((a), (b), (c)) +#undef mp_mul +#define mp_mul(a, b, c) mcdc_fm_mul((a), (b), (c)) +#undef mp_sqr +#define mp_sqr(a, b) mcdc_fm_sqr((a), (b)) +#undef mp_mod +#define mp_mod(a, b, c) mcdc_fm_mod((a), (b), (c)) +#undef mp_invmod +#define mp_invmod(a, b, c) mcdc_fm_invmod((a), (b), (c)) +#undef mp_mulmod +#define mp_mulmod(a, b, c, d) mcdc_fm_mulmod((a), (b), (c), (d)) +#undef mp_addmod +#define mp_addmod(a, b, c, d) mcdc_fm_addmod((a), (b), (c), (d)) +#undef mp_submod +#define mp_submod(a, b, c, d) mcdc_fm_submod((a), (b), (c), (d)) +#undef mp_exptmod +#define mp_exptmod(a, b, c, d) mcdc_fm_exptmod((a), (b), (c), (d)) +#undef mp_exptmod_ex +#define mp_exptmod_ex(b, e, dg, m, r) mcdc_fm_exptmod_ex((b), (e), (dg), (m), (r)) +#undef mp_add_d +#define mp_add_d(a, d, r) mcdc_fm_add_d((a), (d), (r)) +#undef mp_sub_d +#define mp_sub_d(a, d, r) mcdc_fm_sub_d((a), (d), (r)) +#undef mp_mul_d +#define mp_mul_d(a, d, r) mcdc_fm_mul_d((a), (d), (r)) +#undef mp_read_unsigned_bin +#define mp_read_unsigned_bin(a, b, c) mcdc_fm_read_unsigned_bin((a), (b), (c)) +#undef mp_to_unsigned_bin +#define mp_to_unsigned_bin(a, b) mcdc_fm_to_unsigned_bin((a), (b)) +#undef mp_to_unsigned_bin_len +#define mp_to_unsigned_bin_len(a, b, c) \ + mcdc_fm_to_unsigned_bin_len((a), (b), (c)) +#undef mp_read_radix +#define mp_read_radix(a, b, c) mcdc_fm_read_radix((a), (b), (c)) +#undef mp_prime_is_prime +#define mp_prime_is_prime(a, t, r) mcdc_fm_prime_is_prime((a), (t), (r)) +#undef mp_prime_is_prime_ex +#define mp_prime_is_prime_ex(a, t, r, g) \ + mcdc_fm_prime_is_prime_ex((a), (t), (r), (g)) +#undef mp_rand_prime +#define mp_rand_prime(r, l, g, h) mcdc_fm_rand_prime((r), (l), (g), (h)) +#undef mp_montgomery_setup +#define mp_montgomery_setup(m, rho) mcdc_fm_montgomery_setup((m), (rho)) +#undef mp_montgomery_reduce +#define mp_montgomery_reduce(a, m, r) mcdc_fm_montgomery_reduce((a), (m), (r)) +#undef mp_montgomery_calc_normalization +#define mp_montgomery_calc_normalization(a, b) \ + mcdc_fm_montgomery_calc_normalization((a), (b)) + +#endif /* MCDC_FAULT_MP_H */ diff --git a/tests/unit-mcdc/test_dh_fault_whitebox.c b/tests/unit-mcdc/test_dh_fault_whitebox.c index 9a02db1dc9d..19e766ce127 100644 --- a/tests/unit-mcdc/test_dh_fault_whitebox.c +++ b/tests/unit-mcdc/test_dh_fault_whitebox.c @@ -74,11 +74,30 @@ * Invocation: ./test_dh_fault_whitebox (no args; always returns 0). */ +/* Installed BEFORE dh.c so its mp_* calls resolve to the fault wrappers. + * dh.c's residual class is the long big-integer success chain + * + * if (ret == 0 && mp_copy(&key->p, p) != MP_OKAY) ... + * if (ret == 0 && mp_read_unsigned_bin(y, otherPub, pubSz) != MP_OKAY) ... + * } while (ret == 0 && mp_cmp_d(tmp, 1) == MP_EQ); + * + * where BOTH operands are uncovered: on a healthy machine no mp_* call ever + * fails, so `mp_xxx(..) != MP_OKAY` is never TRUE and, with nothing upstream + * failing, `ret == 0` is never FALSE. The mp_int scratch here mostly lives on + * the stack, so the heap-fault lever has nothing to fault either. + * mcdc_fault_mp.h interposes the value-returning mp_* API for this TU only; + * mcdc_fm_arm(n) makes the n-th mp_* call (and every later one) return MP_VAL, + * so one sweep drives both operands of every guard in the chain. Predicates + * (mp_iszero/mp_cmp/mp_count_bits) and teardown (mp_clear/mp_forcezero) are + * NOT interposed, so cleanup keeps working and armed calls stay crash-safe. */ +#include "mcdc_fault_mp.h" + #include #include #include #include +#include static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) @@ -746,6 +765,160 @@ static void test_generate_params(void) wc_FreeRng(&rng); } +/* ---- big-integer fault sweeps (mcdc_fault_mp.h) ------------------------- + * Each entry point is run once DISARMED -- that is the all-true baseline row + * for every guard, in THIS binary (both halves in the same binary), and it + * also measures the sweep length K -- and then the fail index is swept over + * [1..K]. All inputs are built while disarmed; none of the swept calls + * mutates the shared key, and the ones that do (SetKey / GenerateParams) get + * a fresh key per iteration. Bounded by a point cap AND a wall-clock deadline + * so the binary can never reach the campaign's 600 s TEST_TIMEOUT. */ +#define WB_MP_MAX 400 +#define WB_MP_DEADLINE 120 + +static time_t wb_mp_t0; + +static int wb_mp_expired(void) +{ + return difftime(time(NULL), wb_mp_t0) > (double)WB_MP_DEADLINE; +} + +#define WB_MP_SWEEP(lbl, cap, ...) \ + do { \ + long k_, i_; \ + mcdc_fm_disarm(); \ + { __VA_ARGS__; } \ + k_ = mcdc_fm_seen(); \ + if (k_ > (long)(cap)) \ + k_ = (long)(cap); \ + for (i_ = 1; (i_ <= k_) && !wb_mp_expired(); i_++) { \ + mcdc_fm_arm(i_); \ + { __VA_ARGS__; } \ + mcdc_fm_disarm(); \ + } \ + printf(" [wb] mp sweep %s: K=%ld\n", (lbl), k_); \ + } while (0) + +static void test_mp_fault_sweeps(void) +{ + WC_RNG rng; + DhKey dh; + byte priv[512]; + byte pub[512]; + byte agree[512]; + byte p[512]; + byte g[512]; + byte q[512]; + word32 privSz = (word32)sizeof(priv); + word32 pubSz = (word32)sizeof(pub); + word32 agreeSz; + word32 pSz = (word32)sizeof(p); + word32 gSz = (word32)sizeof(g); + word32 qSz = (word32)sizeof(q); + + wb_mp_t0 = time(NULL); + mcdc_fm_disarm(); + + XMEMSET(priv, 0, sizeof(priv)); + XMEMSET(pub, 0, sizeof(pub)); + XMEMSET(agree, 0, sizeof(agree)); + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; mp fault sweeps skipped"); + return; + } + if (wc_InitDhKey(&dh) != 0) { + WB_NOTE("wc_InitDhKey failed; mp fault sweeps skipped"); + wc_FreeRng(&rng); + return; + } + /* One real 1024-bit parameter set, generated DISARMED, is the fixture for + * every sweep below. */ + if (wc_DhGenerateParams(&rng, 1024, &dh) != 0) { + WB_NOTE("wc_DhGenerateParams failed; mp fault sweeps skipped"); + wc_FreeDhKey(&dh); + wc_FreeRng(&rng); + return; + } + if (wc_DhGenerateKeyPair(&dh, &rng, priv, &privSz, pub, &pubSz) != 0) { + WB_NOTE("wc_DhGenerateKeyPair failed; mp fault sweeps skipped"); + wc_FreeDhKey(&dh); + wc_FreeRng(&rng); + return; + } + (void)wc_DhExportParamsRaw(&dh, p, &pSz, q, &qSz, g, &gSz); + + { + byte pv2[512]; + byte pb2[512]; + word32 s1, s2; + s1 = (word32)sizeof(pv2); s2 = (word32)sizeof(pb2); + WB_MP_SWEEP("DhGenerateKeyPair", 200, + s1 = (word32)sizeof(pv2); s2 = (word32)sizeof(pb2); + (void)wc_DhGenerateKeyPair(&dh, &rng, pv2, &s1, pb2, &s2)); + } + + WB_MP_SWEEP("DhGeneratePublic", 200, + { + byte pb2[512]; + word32 s2 = (word32)sizeof(pb2); + (void)wc_DhGeneratePublic(&dh, priv, privSz, pb2, &s2); + }); + + WB_MP_SWEEP("DhCheckPubKey", 200, + (void)wc_DhCheckPubKey(&dh, pub, pubSz)); + + WB_MP_SWEEP("DhCheckPubKey_ex", 200, + (void)wc_DhCheckPubKey_ex(&dh, pub, pubSz, p, pSz)); + + WB_MP_SWEEP("DhCheckPrivKey", 200, + (void)wc_DhCheckPrivKey(&dh, priv, privSz)); + + WB_MP_SWEEP("DhCheckKeyPair", 200, + (void)wc_DhCheckKeyPair(&dh, pub, pubSz, priv, privSz)); + + WB_MP_SWEEP("DhAgree", 200, + { + agreeSz = (word32)sizeof(agree); + (void)wc_DhAgree(&dh, agree, &agreeSz, priv, privSz, pub, pubSz); + }); + + WB_MP_SWEEP("DhSetKey", 200, + { + DhKey k2; + if (wc_InitDhKey(&k2) == 0) { + (void)wc_DhSetKey(&k2, p, pSz, g, gSz); + wc_FreeDhKey(&k2); + } + }); + + WB_MP_SWEEP("DhExportParamsRaw", 200, + { + byte pp[512], gg[512], qq[512]; + word32 a1 = (word32)sizeof(pp), a2 = (word32)sizeof(qq), + a3 = (word32)sizeof(gg); + (void)wc_DhExportParamsRaw(&dh, pp, &a1, qq, &a2, gg, &a3); + }); + + /* GenerateParams is by far the most expensive (prime search), so its cap + * is small: the residuals it owns (3346/3359/3365/3374) are all in the + * post-search g-derivation chain, which the deadline-bounded prefix + * reaches. */ + WB_MP_SWEEP("DhGenerateParams", 40, + { + DhKey k2; + if (wc_InitDhKey(&k2) == 0) { + (void)wc_DhGenerateParams(&rng, 1024, &k2); + wc_FreeDhKey(&k2); + } + }); + + mcdc_fm_disarm(); + wc_FreeDhKey(&dh); + wc_FreeRng(&rng); + WB_NOTE("big-integer fault sweeps done"); +} + int main(void) { setvbuf(stdout, NULL, _IONBF, 0); @@ -775,6 +948,7 @@ int main(void) "(2070/2085/2098/2116) skipped"); #endif test_generate_params(); + test_mp_fault_sweeps(); printf("done (%s)\n", wb_fail ? "FAILURES" : "ok"); return 0; diff --git a/tests/unit-mcdc/test_dsa_fault_whitebox.c b/tests/unit-mcdc/test_dsa_fault_whitebox.c index a84c6211312..4e72c5595aa 100644 --- a/tests/unit-mcdc/test_dsa_fault_whitebox.c +++ b/tests/unit-mcdc/test_dsa_fault_whitebox.c @@ -55,6 +55,27 @@ * are prepared while DISARMED, and the harness never dereferences a value a * faulted call returned. Runs clean under -fsanitize=address. * + * SECOND LEVER -- BIG-INTEGER FAULTS (mcdc_fault_mp.h) + * ---------------------------------------------------- + * The heap sweep above cannot reach the OTHER half of dsa.c's residuals, the + * long mp_* success chains: + * + * if (err == MP_OKAY && !mp_iszero(tmp2)) (wc_DsaCheckPubKey) + * the init/memory error-code pair guards ... (MakeDsaParameters) + * the same pair on the Sign / Verify cleanup path + * if (mp_read_unsigned_bin(r, ..) != MP_OKAY || ..) (Verify parse) + * + * On a healthy machine no mp_* call ever fails, so the `mp_xxx(..) != MP_OKAY` + * operands are never TRUE and the `err == MP_OKAY` operands are never FALSE -- + * and where the mp_int scratch lives on the stack there is no allocation to + * fault either. mcdc_fault_mp.h macro-interposes the value-returning mp_* API + * for this translation unit only (installed before dsa.c is #included), and + * mcdc_fm_arm(n) makes the n-th mp_* call -- and every later one -- return + * MP_VAL. Sweeping n therefore drives BOTH operands of each guard from a + * single pass over each entry point. Predicates (mp_iszero/mp_cmp/...) and + * teardown (mp_clear/mp_forcezero) are NOT interposed, so cleanup keeps + * working and every armed call stays crash-safe. + * * Invocation: * ./test_dsa_fault_whitebox baseline: unarmed valid ops only * ./test_dsa_fault_whitebox sweep baseline + the fault-index sweeps @@ -63,9 +84,16 @@ * default via argv, see the modules.json entry note.) */ +/* Installed BEFORE dsa.c so its mp_* calls resolve to the fault wrappers -- + * the only lever that can drive dsa.c's `mp_xxx(...) != MP_OKAY` operands TRUE + * and, downstream of them, its success-code and init-error-code operands + * FALSE. See the file header. */ +#include "mcdc_fault_mp.h" + #include #include "mcdc_fault_alloc.h" +#include #include #include @@ -128,6 +156,9 @@ int main(int argc, char** argv) int n; int ret; + /* Unbuffered: if an armed call ever crashes, the notes printed so far + * must survive to say WHICH sweep it died in. */ + setvbuf(stdout, NULL, _IONBF, 0); printf("dsa.c fault white-box (%s)\n", (argc > 1 && strcmp(argv[1], "baseline") == 0) ? "baseline" : "sweep"); @@ -254,10 +285,77 @@ int main(int argc, char** argv) } } WB_NOTE("fault-index sweeps over Sign / Verify / CheckPubKey done"); + + /* ---- big-integer fault sweeps (mcdc_fault_mp.h) ---------------- + * Each entry point is run once DISARMED (the all-true baseline row + * for every guard, in THIS binary, and the sweep length K), then the + * fail index is swept over [1..K]. Inputs are always prepared while + * disarmed and none of these entry points mutates the key, so every + * armed call starts from the same known-good state. */ + { + time_t t0 = time(NULL); + long k, i; + int a = 0; + +#define WB_MP_MAX 400 +#define WB_MP_DEADLINE 120 +#define WB_MP_EXPIRED() (difftime(time(NULL), t0) > (double)WB_MP_DEADLINE) +#define WB_MP_SWEEP(lbl, ...) \ + do { \ + mcdc_fm_disarm(); \ + { __VA_ARGS__; } \ + k = mcdc_fm_seen(); \ + if (k > WB_MP_MAX) \ + k = WB_MP_MAX; \ + for (i = 1; (i <= k) && !WB_MP_EXPIRED(); i++) { \ + mcdc_fm_arm(i); \ + { __VA_ARGS__; } \ + mcdc_fm_disarm(); \ + } \ + printf(" [wb] mp sweep %s: K=%ld\n", (lbl), k); \ + } while (0) + + { + byte sig2[256]; + XMEMSET(sig2, 0, sizeof(sig2)); + WB_MP_SWEEP("DsaSign_ex", + (void)wc_DsaSign_ex(digest, sizeof(digest), sig2, &key, + &rng)); + } + WB_MP_SWEEP("DsaVerify_ex", + (void)wc_DsaVerify_ex(digest, sizeof(digest), sig, &key, &a)); +#ifndef NO_DSA_PUBKEY_CHECK + WB_MP_SWEEP("DsaCheckPubKey", (void)wc_DsaCheckPubKey(&key)); +#endif + /* Import parses p/q/g through mp_read_radix; a fresh key each + * time because import populates it. */ + { + DsaKey ik; + WB_MP_SWEEP("DsaImportParamsRaw", + if (wc_InitDsaKey(&ik) == 0) { + (void)wc_DsaImportParamsRaw(&ik, kP, kQ, kG); + wc_FreeDsaKey(&ik); + }); + } + /* Key generation from valid parameters: mp_rand_prime-free, so + * the sweep lands squarely in the exptmod/mod chain. */ + { + DsaKey mk; + WB_MP_SWEEP("MakeDsaKey", + if (wc_InitDsaKey(&mk) == 0) { + if (wc_DsaImportParamsRaw(&mk, kP, kQ, kG) == 0) + (void)wc_MakeDsaKey(&rng, &mk); + wc_FreeDsaKey(&mk); + }); + } + mcdc_fm_disarm(); + WB_NOTE("big-integer fault sweeps done"); + } } mcdc_fa_disarm(); mcdc_fa_restore(); + mcdc_fm_disarm(); wc_FreeDsaKey(&key); wc_FreeRng(&rng); diff --git a/tests/unit-mcdc/test_eccsi_fault_whitebox.c b/tests/unit-mcdc/test_eccsi_fault_whitebox.c index 7b108759208..5faa5ff85a6 100644 --- a/tests/unit-mcdc/test_eccsi_fault_whitebox.c +++ b/tests/unit-mcdc/test_eccsi_fault_whitebox.c @@ -69,12 +69,24 @@ * ./test_eccsi_fault_whitebox probe per-target allocation-site counts */ +/* Installed BEFORE eccsi.c so its mp_* calls resolve to the fault wrappers. + * eccsi.c's residuals are the `(err == 0) && ` halves of its + * big-integer success chains (196/202/208 params->haveA/haveB/havePrime, the + * 924/1934 retry loops). No mp_* call ever 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; mcdc_fm_arm(n) fails the n-th + * mp_* call and every later one. Predicates (mp_iszero/mp_cmp) and teardown + * (mp_free/mp_forcezero) are NOT interposed, so cleanup keeps working. */ +#include "mcdc_fault_mp.h" + #include #include "mcdc_fault_alloc.h" #include #include +#include #ifndef INVALID_DEVID #define INVALID_DEVID (-2) @@ -102,6 +114,176 @@ static int reload_base(EccsiKey* key) return eccsi_load_base(key); } +/* ---- big-integer fault sweeps (mcdc_fault_mp.h) ------------------------- */ +#define WB_MP_DEADLINE 90 + +static time_t wb_mp_t0; + +static int wb_mp_expired(void) +{ + return difftime(time(NULL), wb_mp_t0) > (double)WB_MP_DEADLINE; +} + +/* Run the statement once DISARMED -- the all-true baseline row for every guard + * it touches, in THIS binary, and the sweep length K -- then sweep the fail + * index over [1..min(K, cap)]. */ +#define WB_MP_SWEEP(lbl, cap, ...) \ + do { \ + long k_, i_; \ + mcdc_fm_disarm(); \ + { __VA_ARGS__; } \ + k_ = mcdc_fm_seen(); \ + if (k_ > (long)(cap)) \ + k_ = (long)(cap); \ + for (i_ = 1; (i_ <= k_) && !wb_mp_expired(); i_++) { \ + mcdc_fm_arm(i_); \ + { __VA_ARGS__; } \ + mcdc_fm_disarm(); \ + } \ + printf(" [wb] mp sweep %s: K=%ld\n", (lbl), k_); \ + } while (0) + +static void wb_mp_sweeps(WC_RNG* rng) +{ + EccsiKey k; + ecc_point* pvt = NULL; + mp_int ssk; + byte id[] = "eccsi-mp-fault@wolfssl.com"; + byte hash[WC_MAX_DIGEST_SIZE]; + byte sig[257]; + word32 sigSz; + int verified = 0; + int ready = 0; + + wb_mp_t0 = time(NULL); + mcdc_fm_disarm(); + + XMEMSET(&k, 0, sizeof(k)); + XMEMSET(&ssk, 0, sizeof(ssk)); + XMEMSET(hash, 0x5a, sizeof(hash)); + XMEMSET(sig, 0, sizeof(sig)); + + if (wc_InitEccsiKey(&k, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_InitEccsiKey failed; mp sweeps skipped"); + return; + } + pvt = wc_ecc_new_point_h(NULL); + if ((pvt != NULL) && (mp_init(&ssk) == 0) && + (wc_MakeEccsiKey(&k, rng) == 0)) { + ready = 1; + } + if (!ready) { + WB_NOTE("eccsi fixture setup failed; mp sweeps skipped"); + goto done; + } + + /* Order matters: everything that needs a VALID (ssk, pvt) pair is driven + * first, because the MakeEccsiPair sweep leaves the key in whatever state + * a faulted call produced. */ + mcdc_fm_disarm(); + if (wc_MakeEccsiPair(&k, rng, WC_HASH_TYPE_SHA256, id, (word32)sizeof(id), + &ssk, pvt) != 0) { + WB_NOTE("wc_MakeEccsiPair baseline failed; mp sweeps skipped"); + goto done; + } + + WB_MP_SWEEP("ValidateEccsiPair", 200, + { + int v = 0; + (void)wc_ValidateEccsiPair(&k, WC_HASH_TYPE_SHA256, id, + (word32)sizeof(id), &ssk, pvt, &v); + }); + + mcdc_fm_disarm(); + if ((wc_SetEccsiPair(&k, &ssk, pvt) == 0) && + (wc_HashEccsiId(&k, WC_HASH_TYPE_SHA256, id, (word32)sizeof(id), + pvt, hash, NULL) == 0) && + (wc_SetEccsiHash(&k, hash, WC_SHA256_DIGEST_SIZE) == 0)) { + WB_MP_SWEEP("SignEccsiHash", 200, + { + byte s2[257]; + word32 z = (word32)sizeof(s2); + (void)wc_SignEccsiHash(&k, rng, WC_HASH_TYPE_SHA256, hash, + WC_SHA256_DIGEST_SIZE, s2, &z); + }); + + mcdc_fm_disarm(); + sigSz = (word32)sizeof(sig); + if (wc_SignEccsiHash(&k, rng, WC_HASH_TYPE_SHA256, hash, + WC_SHA256_DIGEST_SIZE, sig, &sigSz) == 0) { + WB_MP_SWEEP("VerifyEccsiHash", 200, + (void)wc_VerifyEccsiHash(&k, WC_HASH_TYPE_SHA256, hash, + WC_SHA256_DIGEST_SIZE, sig, sigSz, &verified)); + } + else { + WB_NOTE("wc_SignEccsiHash baseline failed; verify sweep skipped"); + } + } + else { + WB_NOTE("SetEccsiPair/HashEccsiId/SetEccsiHash failed; sign+verify " + "sweeps skipped"); + } + + /* 196/202/208 eccsi_load_ecc_params(): + * if ((err == 0) && (!params->haveA)) (and haveB / havePrime) + * The FALSE half of the err operand needs an earlier step in the SAME call + * to fail, with the second operand still true -- i.e. a key whose haveA/ + * haveB/havePrime are still 0. Those flags latch on first use, and the + * public API caches them, so the only way to present a fresh key is to + * call the file-static helper directly (in scope via the #include at the + * top) on a key that has never loaded its parameters, with the fail index + * landing on eccsi_load_order()'s / this function's own mp_read_radix. + * The all-true row comes from the unarmed fixture setup above. */ + { + long n; + + for (n = 1; (n <= 6) && !wb_mp_expired(); n++) { + EccsiKey k2; + + XMEMSET(&k2, 0, sizeof(k2)); + if (wc_InitEccsiKey(&k2, NULL, INVALID_DEVID) == 0) { + mcdc_fm_disarm(); + if (wc_MakeEccsiKey(&k2, rng) == 0) { + /* Clear the latches so every guard's second operand is + * true again for this armed call. */ + k2.params.haveA = 0; + k2.params.haveB = 0; + k2.params.havePrime = 0; + mcdc_fm_arm(n); + (void)eccsi_load_ecc_params(&k2); + mcdc_fm_disarm(); + } + wc_FreeEccsiKey(&k2); + } + } + printf(" [wb] mp sweep eccsi_load_ecc_params: 6 points\n"); + } + + /* Destructive sweeps last: each faulted call may leave the key or the + * pair unusable, so nothing above may depend on them. */ + WB_MP_SWEEP("MakeEccsiPair", 200, + (void)wc_MakeEccsiPair(&k, rng, WC_HASH_TYPE_SHA256, id, + (word32)sizeof(id), &ssk, pvt)); + + WB_MP_SWEEP("MakeEccsiKey", 120, + { + EccsiKey k2; + XMEMSET(&k2, 0, sizeof(k2)); + if (wc_InitEccsiKey(&k2, NULL, INVALID_DEVID) == 0) { + (void)wc_MakeEccsiKey(&k2, rng); + wc_FreeEccsiKey(&k2); + } + }); + +done: + mcdc_fm_disarm(); + mp_free(&ssk); + if (pvt != NULL) + wc_ecc_del_point_h(pvt, NULL); + wc_FreeEccsiKey(&k); + WB_NOTE("big-integer fault sweeps done"); +} + int main(int argc, char** argv) { int do_sweep = !(argc > 1 && strcmp(argv[1], "baseline") == 0); @@ -117,6 +299,9 @@ int main(int argc, char** argv) int n_idx; const int K = 60; /* over-sweep past the mulmod/point-add allocation sites */ + /* Unbuffered: if an armed call ever crashes, the notes printed so far + * must survive to say WHICH sweep it died in. */ + setvbuf(stdout, NULL, _IONBF, 0); printf("eccsi.c fault white-box (%s)\n", do_probe ? "probe" : (do_sweep ? "sweep" : "baseline")); @@ -245,6 +430,8 @@ int main(int argc, char** argv) cleanup: mcdc_fa_disarm(); mcdc_fa_restore(); + if (do_sweep) + wb_mp_sweeps(&rng); if (ptA != NULL) wc_ecc_del_point_h(ptA, NULL); if (ptB != NULL) wc_ecc_del_point_h(ptB, NULL); if (res != NULL) wc_ecc_del_point_h(res, NULL); diff --git a/tests/unit-mcdc/test_frodokem_mat_hash_fault_whitebox.c b/tests/unit-mcdc/test_frodokem_mat_hash_fault_whitebox.c new file mode 100644 index 00000000000..905ba29c996 --- /dev/null +++ b/tests/unit-mcdc/test_frodokem_mat_hash_fault_whitebox.c @@ -0,0 +1,318 @@ +/* test_frodokem_mat_hash_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC hash/AES-fault white-box for wolfcrypt/src/wc_frodokem_mat.c. + * + * THE DEFERRED TECHNIQUE, IMPLEMENTED + * ----------------------------------- + * test_frodokem_fault_common.h records, at length, why the heap-fault mock + * closes NONE of wc_frodokem_mat.c's 13 residuals: + * + * "on x86/x86_64 its (ret == 0) && step residuals become ret != 0 ONLY + * when a SHAKE (wc_InitShake* / Absorb / Squeeze) or AES-ECB primitive + * returns an error. Those primitives DO NOT route through the heap + * allocator ... they would need a primitive-return fault mock (stubbing + * the wc_Shake family and wc_AesEcbEncrypt), a separate deferred + * technique." + * + * This file is that technique. mcdc_fault_hash.h macro-interposes the SHAKE + * and AES-ECB primitives for THIS translation unit only (the involved .c is + * #included after the interposers are installed), and mcdc_fh_arm(n) makes the + * n-th primitive call -- and every later one -- return BAD_FUNC_ARG. + * + * WHAT THAT REACHES + * 1041:1 / 1084:1 frodokem_gen_noise(): + * if (p->useShake256 && ((ret = wc_InitShake256(...)) == 0)) + * The second operand has no other route to FALSE, which is + * why MCDC_FH_WITH_SHAKE_INIT is enabled here (it is opt-in + * precisely because a white-box that inits its own SHAKE + * contexts must not have that setup faulted; this one does + * not init any). + * 1059:0 / 1102:0 if ((ret == 0) && (cnt1 > 0)) -- fault the Absorb or + * an earlier SqueezeBlocks of the same gen_noise call. + * 1242:0 for (r = 0; (ret == 0) && (r < cnt); r++) -- fault an + * AES-ECB inside the row loop of frodokem_gen_a_rows_aes(). + * 1828 / 1956 / 2151 / 2283 :0 for (i = 0; (ret == 0) && (i < n); i++) + * -- the four matrix mul-add row loops. + * 1841 / 1969 / 2164 / 2296 :0 if ((ret == 0) && (p->qMask != 0xffff)) + * -- the post-loop reduction guard. NOTE this needs the + * q == 2^15 parameter sets (640), where qMask != 0xffff, so + * the sweep always includes a 640 type. + * + * DRIVING IT + * ---------- + * wc_frodokem_mat.c is a helper TU: the public API lives in wc_frodokem.c, + * which the harness still supplies from the trimmed archive, so an ordinary + * MakeKey / Encapsulate / Decapsulate reaches every routine here end to end. + * Each entry point is swept separately with its inputs built while DISARMED: + * run it once unarmed (the all-true baseline row for every guard, in THIS + * binary -- HARD RULE 1 -- and the sweep length K), then sweep n over [1..K] + * dense-then-strided to a fixed point budget. + * + * Only the SMALLEST parameter set of each matrix-generation family (640_SHAKE + * and 640_AES, plus their ephemeral forms when compiled) is swept: 640 is the + * only set with qMask != 0xffff, and the 976/1344 sets run the identical code + * with a bigger n at several times the cost. The larger sets still get their + * baseline pass. Everything is bounded by a point budget AND a CPU deadline so + * the binary can never hit the campaign's 600 s TEST_TIMEOUT (a timeout is a + * SILENT SKIP that would lose the whole file). + */ + +/* wc_InitShake128/256 must be interposable here -- see the 1041:1 note above. + * This TU initialises no SHAKE context of its own, so nothing in the test + * setup can be faulted by that. */ +#define MCDC_FH_WITH_SHAKE_INIT 1 +#include "mcdc_fault_hash.h" + +/* The involved .c is #included AFTER the interposers are installed. */ +#include + +#include +#include +#include +#include +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#ifndef WOLFSSL_HAVE_FRODOKEM + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("frodokem mat hash-fault white-box: !WOLFSSL_HAVE_FRODOKEM, " + "nothing to do\n"); + return 0; +} + +#else + +/* Sweep budget and CPU deadline (see file header). */ +#define WB_DENSE 48 +#define WB_POINTS 160 +#define WB_DEADLINE_S 200 + +static clock_t wb_t0; + +static int wb_expired(void) +{ + return ((double)(clock() - wb_t0) / (double)CLOCKS_PER_SEC) + > (double)WB_DEADLINE_S; +} + +static long wb_next(long n, long k) +{ + long stride; + + if (n < (long)WB_DENSE) + return n + 1; + stride = (k - (long)WB_DENSE) / (long)WB_POINTS; + if (stride < 1) + stride = 1; + return n + stride; +} + +/* Types swept in full. 640 is the only q == 2^15 set (qMask != 0xffff), which + * the `(ret == 0) && (p->qMask != 0xffff)` guards need, and it is the cheapest + * of each family. */ +static const int fk_sweep_types[] = { +#ifdef WOLFSSL_FRODOKEM_SHAKE + WC_FRODOKEM_640_SHAKE, +#endif +#ifdef WOLFSSL_FRODOKEM_AES + WC_FRODOKEM_640_AES, +#endif +#if defined(WOLFSSL_FRODOKEM_EPHEMERAL) && defined(WOLFSSL_FRODOKEM_SHAKE) + WC_EFRODOKEM_640_SHAKE, +#endif +#if defined(WOLFSSL_FRODOKEM_EPHEMERAL) && defined(WOLFSSL_FRODOKEM_AES) + WC_EFRODOKEM_640_AES, +#endif + 0 /* keeps the array non-empty when no type is compiled in */ +}; + +/* Types given only an unarmed baseline pass (the all-true rows for the + * larger-n copies of the same code). */ +static const int fk_base_types[] = { +#ifdef WOLFSSL_FRODOKEM_SHAKE + WC_FRODOKEM_976_SHAKE, WC_FRODOKEM_1344_SHAKE, +#endif +#ifdef WOLFSSL_FRODOKEM_AES + WC_FRODOKEM_976_AES, WC_FRODOKEM_1344_AES, +#endif + 0 +}; + +static WC_RNG wb_rng; +static byte wb_ct[FRODOKEM_MAX_CIPHER_TEXT_SIZE]; +static byte wb_ss[FRODOKEM_MAX_LENSEC]; +static byte wb_ss2[FRODOKEM_MAX_LENSEC]; + +/* Build a fully made key of a given type. Must be called DISARMED. */ +static int wb_build(FrodoKemKey* key, int type) +{ + int ret = wc_FrodoKemKey_Init(key, type, NULL, INVALID_DEVID); + + if (ret == 0) + ret = wc_FrodoKemKey_MakeKey(key, &wb_rng); + return ret; +} + +/* One unarmed make/encap/decap for a type: the all-true baseline rows. */ +static void wb_baseline(int type) +{ + FrodoKemKey key; + word32 ctLen = (word32)sizeof(wb_ct); + + XMEMSET(&key, 0, sizeof(key)); + mcdc_fh_disarm(); + if (wb_build(&key, type) == 0) { + (void)wc_FrodoKemKey_CipherTextSize(&key, &ctLen); + if (wc_FrodoKemKey_Encapsulate(&key, wb_ct, wb_ss, &wb_rng) == 0) + (void)wc_FrodoKemKey_Decapsulate(&key, wb_ss2, wb_ct, ctLen); + } + wc_FrodoKemKey_Free(&key); +} + +static void wb_sweep_type(int type) +{ + FrodoKemKey good; + word32 ctLen = (word32)sizeof(wb_ct); + long k, n, points; + int haveCt = 0; + + XMEMSET(&good, 0, sizeof(good)); + mcdc_fh_disarm(); + if (wb_build(&good, type) != 0) { + wc_FrodoKemKey_Free(&good); + return; /* type not compiled in this build */ + } + (void)wc_FrodoKemKey_CipherTextSize(&good, &ctLen); + + printf(" [wb] --- type 0x%02x ---\n", (unsigned)type); + + /* ---- make_key ---- */ + { + FrodoKemKey key; + XMEMSET(&key, 0, sizeof(key)); + mcdc_fh_disarm(); + if (wc_FrodoKemKey_Init(&key, type, NULL, INVALID_DEVID) == 0) { + (void)wc_FrodoKemKey_MakeKey(&key, &wb_rng); + k = mcdc_fh_seen(); + } + else { + k = 0; + } + wc_FrodoKemKey_Free(&key); + + points = 0; + for (n = 1; (n <= k) && !wb_expired(); n = wb_next(n, k)) { + FrodoKemKey f; + XMEMSET(&f, 0, sizeof(f)); + if (wc_FrodoKemKey_Init(&f, type, NULL, INVALID_DEVID) == 0) { + mcdc_fh_arm(n); + (void)wc_FrodoKemKey_MakeKey(&f, &wb_rng); + mcdc_fh_disarm(); + points++; + } + wc_FrodoKemKey_Free(&f); + } + printf(" [wb] make_key sweep: K=%ld, %ld points\n", k, points); + } + + /* ---- encapsulate (does not mutate the key) ---- */ + mcdc_fh_disarm(); + haveCt = (wc_FrodoKemKey_Encapsulate(&good, wb_ct, wb_ss, &wb_rng) == 0); + k = mcdc_fh_seen(); + if (!haveCt) { + WB_NOTE("baseline encapsulate failed; encap/decap sweeps skipped"); + wb_fail = 1; + wc_FrodoKemKey_Free(&good); + return; + } + points = 0; + for (n = 1; (n <= k) && !wb_expired(); n = wb_next(n, k)) { + byte c2[FRODOKEM_MAX_CIPHER_TEXT_SIZE]; + byte s2[FRODOKEM_MAX_LENSEC]; + mcdc_fh_arm(n); + (void)wc_FrodoKemKey_Encapsulate(&good, c2, s2, &wb_rng); + mcdc_fh_disarm(); + points++; + } + printf(" [wb] encapsulate sweep: K=%ld, %ld points\n", k, points); + + /* ---- decapsulate (re-encapsulates internally: hits the mat paths + * again plus the shared-secret compare) ---- */ + mcdc_fh_disarm(); + (void)wc_FrodoKemKey_Decapsulate(&good, wb_ss2, wb_ct, ctLen); + k = mcdc_fh_seen(); + points = 0; + for (n = 1; (n <= k) && !wb_expired(); n = wb_next(n, k)) { + byte d2[FRODOKEM_MAX_LENSEC]; + mcdc_fh_arm(n); + (void)wc_FrodoKemKey_Decapsulate(&good, d2, wb_ct, ctLen); + mcdc_fh_disarm(); + points++; + } + printf(" [wb] decapsulate sweep: K=%ld, %ld points\n", k, points); + + mcdc_fh_disarm(); + wc_FrodoKemKey_Free(&good); +} + +int main(void) +{ + size_t i; + + setvbuf(stdout, NULL, _IONBF, 0); + printf("wc_frodokem_mat.c hash/AES-fault white-box supplement\n"); + wb_t0 = clock(); + + XMEMSET(&wb_rng, 0, sizeof(wb_rng)); + XMEMSET(wb_ct, 0, sizeof(wb_ct)); + XMEMSET(wb_ss, 0, sizeof(wb_ss)); + XMEMSET(wb_ss2, 0, sizeof(wb_ss2)); + + if (wc_InitRng(&wb_rng) != 0) { + WB_NOTE("wc_InitRng failed; nothing driven"); + } + else { + for (i = 0; i < sizeof(fk_base_types) / sizeof(fk_base_types[0]); i++) { + if (fk_base_types[i] != 0) + wb_baseline(fk_base_types[i]); + } + for (i = 0; i < sizeof(fk_sweep_types) / sizeof(fk_sweep_types[0]); + i++) { + if ((fk_sweep_types[i] != 0) && !wb_expired()) + wb_sweep_type(fk_sweep_types[i]); + } + mcdc_fh_disarm(); + wc_FreeRng(&wb_rng); + } + + printf("done (%s)\n", wb_fail ? "with failures" : "ok"); + /* A non-zero exit makes the campaign discard this binary's coverage. */ + return 0; +} + +#endif /* WOLFSSL_HAVE_FRODOKEM */ diff --git a/tests/unit-mcdc/test_lms_hash_fault_whitebox.c b/tests/unit-mcdc/test_lms_hash_fault_whitebox.c new file mode 100644 index 00000000000..8162b09d5e2 --- /dev/null +++ b/tests/unit-mcdc/test_lms_hash_fault_whitebox.c @@ -0,0 +1,617 @@ +/* test_lms_hash_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC hash-fault white-box supplement for wolfcrypt/src/wc_lms_impl.c. + * + * WHAT IS LEFT AFTER THE OTHER TWO LMS WHITE-BOXES + * ------------------------------------------------ + * campaign/reports/lms/GAPS.md is dominated by ONE shape inside + * wc_lms_impl.c's WOTS / Merkle / HSS engine: + * + * for (i = 0; (ret == 0) && (i < params->p); i++) ... + * for (j = a[i]; (ret == 0) && (j < max); j++) ... + * while ( (ret == 0) && ((j & 0x1) == 1)) ... + * if ( (ret == 0) && (auth_path != NULL) && ...) ... + * + * The (ret == 0) operand only ever goes FALSE when an earlier step failed + * *inside the same operation*. wc_lms_impl.c performs ZERO allocations + * (grep XMALLOC: none), so mcdc_fault_alloc.h -- the campaign's usual lever -- + * has nothing to fault here: `ret` in this file comes exclusively from + * wc_Sha256HashBlock / wc_Sha256Update / wc_Sha256Final (and the SHAKE + * equivalents). test_wc_lms_impl_whitebox_gap.c already closed everything that + * a bad *argument* can reach (it uses an invalid Winternitz width to make the + * very first step fail); what remains needs the chain broken at an arbitrary + * DEPTH -- mid-loop, mid-tree, mid-subtree -- which only a failing hash + * primitive can do. + * + * TECHNIQUE + * --------- + * mcdc_fault_hash.h interposes the SHA-256/SHAKE primitives by macro, for this + * translation unit only, before wc_lms_impl.c is #included (the generalised + * form of test_tsp_fault_whitebox.c's XGMTIME mock). mcdc_fh_arm(n) makes the + * n-th primitive call -- and every later one -- return BAD_FUNC_ARG, exactly + * mirroring mcdc_fa_arm()'s semantics. + * + * Each of the four engine entry points (make_key / sign / verify / reload) is + * swept SEPARATELY, with its inputs always prepared while DISARMED: + * + * 1. run it once disarmed -> the all-true baseline row for every + * guard, in THIS binary (HARD RULE 1), + * and mcdc_fh_seen() gives the sweep + * length K for that entry point; + * 2. sweep n over [1..K] -> for each n exactly one interior step + * fails, so the (ret == 0) operand is + * driven false at every decision the + * operation would have reached after + * that point. + * + * K is tens of thousands of hash calls for a real keygen, so the sweep is + * dense over the first WB_DENSE indices (the entry-guard chains) and then + * strided to a fixed budget of WB_POINTS points (the deep loops). Restoring + * the private-key state for the sign sweep is a memcpy of the priv_raw / + * priv_data / HssPrivKey snapshot rather than a fresh keygen -- HssPrivKey's + * internal pointers all point into the SAME priv_data buffer, whose address + * never changes, so the snapshot restores exactly. The public key is part of + * the snapshot too: a faulted make_key leaves wb_pub half-written, and the + * verify sweep needs the good one back. + * + * NEVER HANG (HARD RULE 2): every sweep is bounded by both a point budget and + * a CPU-time deadline (WB_DEADLINE_S). The parameter set is the smallest that + * still exercises the HSS multi-level machinery: levels=2, height=2 (16 + * signatures total, subtree rollover after 4), Winternitz w=8, SHA-256/32. + * WOLFSSL_LMS_MAX_LEVELS is pinned to 2 by this module's campaign config, so + * levels=2 is the maximum available. + * + * VARIANT COVERAGE (HARD RULE 3): WOLFSSL_LMS_VERIFY_ONLY compiles keygen and + * signing out entirely, and no valid signature can be produced without them, + * so that whole variant is a skip stub. WOLFSSL_WC_LMS_SMALL keeps every entry + * point but drops LmsParams::rootLevels/cacheBits, which are assigned under an + * #ifndef. main() always returns 0. + */ + +#include "mcdc_fault_hash.h" + +/* wc_lms_impl.c is #included AFTER the interposers are installed. */ +#include + +#include +#include +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFSSL_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) && \ + !defined(WOLFSSL_NO_LMS_SHA256_256) + +#define WB_HAVE_DRIVER 1 + +/* Winternitz w=8, wb=3 for every family: LMS_V = 2, so ls = 0 and + * p = 8*hash_len/8 + 2 = hash_len + 2 (LMS_U/LMS_V/LMS_P live in wc_lms.c, + * which this TU does not include, so they are hand-expanded here). */ +#define WB_WIDTH 8U +#define WB_LS 0U +#define WB_P_OF(hLen) ((word16)((hLen) + 2U)) + +/* Largest hash length / p over all compiled families: sizes every fixed + * buffer below. */ +#define WB_HLEN_MAX WC_SHA256_DIGEST_SIZE /* 32 */ +#define WB_P_MAX 34U + +/* Tree shapes. Kept tiny: keygen is 2^height WOTS keys per subtree per level + * and each WOTS key is p * 255 hash calls, so height is the whole runtime + * budget. Shape A is the smallest with more than one HSS level and a subtree + * rollover; shape B raises rootLevels above 1 so wc_lms_treehash_init/update's + * `h > height - rootLevels` guards can be true. */ +#define WB_HEIGHT_MAX 3U +#define WB_LEVELS_MAX 2U + +typedef struct WbShape { + word8 levels; + word8 height; + word8 rootLevels; + word8 cacheBits; + int nsigs; /* one past the first subtree rollover (2^height) */ +} WbShape; + +static const WbShape wb_shapes[] = { + { 2, 2, 1, 1, 6 }, + { 2, 3, 2, 2, 10 }, +}; + +/* Hash families compiled into this variant. wc_lms_impl.c dispatches on + * LMS_IS_SHAKE(lmOtsType) and on (lmOtsType & LMS_HASH_MASK) == LMS_SHA256_192, + * and each arm has its OWN copy of the WOTS/Merkle error chains -- so every + * compiled family has to be swept or its arm's residuals stay open. */ +typedef struct WbFamily { + const char* name; + word16 lmsType; + word16 lmOtsType; + word16 hash_len; +} WbFamily; + +static const WbFamily wb_families[] = { + { "sha256_256", LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8, + WC_SHA256_DIGEST_SIZE }, +#ifdef WOLFSSL_LMS_SHA256_192 + { "sha256_192", LMS_SHA256_M24_H5, LMOTS_SHA256_N24_W8, 24 }, +#endif +#ifdef WOLFSSL_LMS_SHAKE256 + { "shake256_256", LMS_SHAKE_M32_H5, LMOTS_SHAKE_N32_W8, + WC_SHA256_DIGEST_SIZE }, +#endif +}; +#define WB_NFAMILIES (sizeof(wb_families) / sizeof(wb_families[0])) +#define WB_NSHAPES (sizeof(wb_shapes) / sizeof(wb_shapes[0])) + +/* Sweep budget. WB_DENSE leading indices are visited one by one (the + * entry-guard chains live there); the rest of [1..K] is covered by a stride + * chosen so the total never exceeds WB_POINTS. */ +#define WB_DENSE 48 +#define WB_POINTS 192 +/* Hard CPU-time ceiling for the whole program, well under the campaign's + * 600 s TEST_TIMEOUT even with variants running concurrently and even in the + * (much slower) WOLFSSL_WC_LMS_SMALL recompute build. Every sweep tests it, so + * the program degrades to fewer points rather than being killed -- a killed + * white-box is scored as a SILENT SKIP and loses the whole file. */ +#define WB_DEADLINE_S 170 + +/* WALL clock, not clock(): the campaign runs several variants concurrently and + * TEST_TIMEOUT is 600 s of WALL time. Under that contention CPU time accrues + * far slower than wall time, so a CPU-time budget would sail past the timeout + * -- and a timed-out white-box is scored as a SILENT SKIP that loses the whole + * file's coverage. */ +static time_t wb_t0; + +static int wb_expired(void) +{ + return difftime(time(NULL), wb_t0) > (double)WB_DEADLINE_S; +} + +/* Build a self-consistent LmsParams by hand (this file never goes through + * wc_lms.c, so the values need only be internally consistent). */ +static void wb_make_params(LmsParams* params, const WbFamily* fam, + const WbShape* shape) +{ + XMEMSET(params, 0, sizeof(*params)); + params->levels = shape->levels; + params->height = shape->height; + params->width = (word8)WB_WIDTH; + params->ls = (word8)WB_LS; + params->p = WB_P_OF(fam->hash_len); + params->lmsType = fam->lmsType; + params->lmOtsType = fam->lmOtsType; + params->hash_len = fam->hash_len; + params->sig_len = 4U + + (word32)shape->levels * + LMS_SIG_LEN(shape->height, params->p, params->hash_len) + + (word32)(shape->levels - 1U) * LMS_PUBKEY_LEN(params->hash_len); +#ifndef WOLFSSL_WC_LMS_SMALL + params->rootLevels = shape->rootLevels; + params->cacheBits = shape->cacheBits; +#endif +} + +/* Mirrors wc_lmskey_state_init() / _free() in wc_lms.c (static in another TU; + * this file never includes wc_lms.c). wc_InitShake256 / wc_InitSha256 are NOT + * interposed by mcdc_fault_hash.h, so test setup can never be faulted. */ +static int wb_state_init(LmsState* state, const LmsParams* params) +{ + int ret; + + XMEMSET(state, 0, sizeof(*state)); + state->params = params; + +#ifdef WOLFSSL_LMS_SHAKE256 + if (LMS_IS_SHAKE(params->lmOtsType)) { + ret = wc_InitShake256(LMS_STATE_SHAKE(state), NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_InitShake256(LMS_STATE_SHAKE_K(state), NULL, + INVALID_DEVID); + if (ret != 0) { + wc_Shake256_Free(LMS_STATE_SHAKE(state)); + } + } + return ret; + } +#endif + + ret = wc_InitSha256(LMS_STATE_HASH(state)); + if (ret == 0) { + ret = wc_InitSha256(LMS_STATE_HASH_K(state)); + if (ret != 0) { + wc_Sha256Free(LMS_STATE_HASH(state)); + } + } + return ret; +} + +static void wb_state_free(LmsState* state) +{ +#ifdef WOLFSSL_LMS_SHAKE256 + if (LMS_IS_SHAKE(state->params->lmOtsType)) { + wc_Shake256_Free(LMS_STATE_SHAKE_K(state)); + wc_Shake256_Free(LMS_STATE_SHAKE(state)); + return; + } +#endif + wc_Sha256Free(LMS_STATE_HASH_K(state)); + wc_Sha256Free(LMS_STATE_HASH(state)); +} + +/* ---- shared fixture ---------------------------------------------------- */ + +static LmsParams wb_params; +static WC_RNG wb_rng; +static HssPrivKey wb_pk; +static HssPrivKey wb_pk_bak; +static byte wb_priv_raw[HSS_PRIVATE_KEY_LEN(WB_HLEN_MAX)]; +static byte wb_priv_raw_bak[HSS_PRIVATE_KEY_LEN(WB_HLEN_MAX)]; +static byte wb_pub[HSS_PUBLIC_KEY_LEN(WB_HLEN_MAX)]; +static byte wb_pub_bak[HSS_PUBLIC_KEY_LEN(WB_HLEN_MAX)]; +static byte wb_sig[4U + WB_LEVELS_MAX * + LMS_SIG_LEN(WB_HEIGHT_MAX, WB_P_MAX, WB_HLEN_MAX) + + (WB_LEVELS_MAX - 1U) * LMS_PUBKEY_LEN(WB_HLEN_MAX)]; +static int wb_nsigs = 1; +static byte* wb_priv_data = NULL; +static byte* wb_priv_data_bak = NULL; +static word32 wb_priv_data_len = 0; +static word32 wb_priv_data_cap = 0; +static const byte wb_msg[] = "wc_lms_impl hash-fault white-box message"; + +/* Snapshot / restore the whole signing state. HssPrivKey's internal pointers + * address wb_priv_data, whose location never changes, so a byte-wise restore + * is exact. */ +static void wb_snapshot(void) +{ + XMEMCPY(wb_priv_raw_bak, wb_priv_raw, sizeof(wb_priv_raw)); + XMEMCPY(wb_pub_bak, wb_pub, sizeof(wb_pub)); + XMEMCPY(&wb_pk_bak, &wb_pk, sizeof(wb_pk)); + if (wb_priv_data_bak != NULL) + XMEMCPY(wb_priv_data_bak, wb_priv_data, wb_priv_data_len); +} + +static void wb_restore(void) +{ + XMEMCPY(wb_priv_raw, wb_priv_raw_bak, sizeof(wb_priv_raw)); + XMEMCPY(wb_pub, wb_pub_bak, sizeof(wb_pub)); + XMEMCPY(&wb_pk, &wb_pk_bak, sizeof(wb_pk)); + if (wb_priv_data_bak != NULL) + XMEMCPY(wb_priv_data, wb_priv_data_bak, wb_priv_data_len); +} + +/* Next sweep index after n, for a sweep of length k. */ +static long wb_next(long n, long k) +{ + long stride; + + if (n < (long)WB_DENSE) + return n + 1; + stride = (k - (long)WB_DENSE) / (long)WB_POINTS; + if (stride < 1) + stride = 1; + return n + stride; +} + +/* ---- the four swept operations ----------------------------------------- */ + +/* Each wb_do_* runs ONE engine entry point with a freshly initialised + * LmsState and returns its result. The caller arms/disarms around it. */ + +static int wb_do_make_key(void) +{ + LmsState state; + int ret = wb_state_init(&state, &wb_params); + + if (ret == 0) { + ret = wc_hss_make_key(&state, &wb_rng, wb_priv_raw, &wb_pk, + wb_priv_data, wb_pub); + wb_state_free(&state); + } + return ret; +} + +static int wb_do_sign(int nsigs) +{ + LmsState state; + int ret = wb_state_init(&state, &wb_params); + int i; + + if (ret == 0) { + for (i = 0; (ret == 0) && (i < nsigs); i++) { + ret = wc_hss_sign(&state, wb_priv_raw, &wb_pk, wb_priv_data, + wb_msg, (word32)sizeof(wb_msg), wb_sig); + } + wb_state_free(&state); + } + return ret; +} + +static int wb_do_verify(void) +{ + LmsState state; + int ret = wb_state_init(&state, &wb_params); + + if (ret == 0) { + ret = wc_hss_verify(&state, wb_pub, wb_msg, (word32)sizeof(wb_msg), + wb_sig, wb_params.sig_len); + wb_state_free(&state); + } + return ret; +} + +static int wb_do_reload(void) +{ + LmsState state; + int ret = wb_state_init(&state, &wb_params); + + if (ret == 0) { + ret = wc_hss_reload_key(&state, wb_priv_raw, &wb_pk, wb_priv_data, + wb_pub + LMS_L_LEN + LMS_TYPE_LEN + LMS_TYPE_LEN + LMS_I_LEN); + wb_state_free(&state); + } + return ret; +} + +/* ---- sweeps ------------------------------------------------------------ */ + +static void wb_sweep_make_key(void) +{ + long k, n; + int ret; + long points = 0; + + /* Baseline (disarmed): the TRUE half of every guard, plus the length. */ + mcdc_fh_disarm(); + ret = wb_do_make_key(); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline wc_hss_make_key failed; make_key sweep skipped"); + wb_fail = 1; + return; + } + wb_snapshot(); + + for (n = 1; n <= k; n = wb_next(n, k)) { + mcdc_fh_arm(n); + (void)wb_do_make_key(); + mcdc_fh_disarm(); + points++; + if (wb_expired()) + break; + } + + /* The sweep left priv_raw/priv_key/pub in an aborted state; put the + * known-good keygen output back for the sign/verify sweeps. */ + wb_restore(); + printf(" [wb] make_key sweep: K=%ld, %ld points\n", k, points); +} + +static void wb_sweep_sign(void) +{ + long k, n; + int ret; + long points = 0; + + /* Baseline: WB_NSIGS signatures from the snapshot state. */ + wb_restore(); + mcdc_fh_disarm(); + ret = wb_do_sign(wb_nsigs); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline wc_hss_sign failed; sign sweep skipped"); + wb_fail = 1; + wb_restore(); + return; + } + + for (n = 1; n <= k; n = wb_next(n, k)) { + wb_restore(); /* prepared while DISARMED */ + mcdc_fh_arm(n); + (void)wb_do_sign(wb_nsigs); + mcdc_fh_disarm(); + points++; + if (wb_expired()) + break; + } + + wb_restore(); + printf(" [wb] sign sweep: K=%ld, %ld points\n", k, points); +} + +static void wb_sweep_verify(void) +{ + long k, n; + int ret; + long points = 0; + + /* One valid signature, produced disarmed. */ + wb_restore(); + mcdc_fh_disarm(); + ret = wb_do_sign(1); + if (ret != 0) { + WB_NOTE("signing for the verify sweep failed; verify sweep skipped"); + wb_fail = 1; + wb_restore(); + return; + } + + mcdc_fh_disarm(); + ret = wb_do_verify(); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline wc_hss_verify rejected a valid signature"); + wb_fail = 1; + wb_restore(); + return; + } + + for (n = 1; n <= k; n = wb_next(n, k)) { + mcdc_fh_arm(n); + (void)wb_do_verify(); + mcdc_fh_disarm(); + points++; + if (wb_expired()) + break; + } + + /* Corrupted-signature verify (the XMEMCMP != 0 half), disarmed. */ + wb_sig[wb_params.sig_len - 1] ^= 0xFF; + mcdc_fh_disarm(); + ret = wb_do_verify(); + if (ret == 0) { + WB_NOTE("wc_hss_verify accepted a corrupted signature"); + wb_fail = 1; + } + wb_sig[wb_params.sig_len - 1] ^= 0xFF; + + wb_restore(); + printf(" [wb] verify sweep: K=%ld, %ld points\n", k, points); +} + +static void wb_sweep_reload(void) +{ + long k, n; + int ret; + long points = 0; + + wb_restore(); + mcdc_fh_disarm(); + ret = wb_do_reload(); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline wc_hss_reload_key failed; reload sweep skipped"); + wb_fail = 1; + wb_restore(); + return; + } + + for (n = 1; n <= k; n = wb_next(n, k)) { + wb_restore(); + mcdc_fh_arm(n); + (void)wb_do_reload(); + mcdc_fh_disarm(); + points++; + if (wb_expired()) + break; + } + + wb_restore(); + printf(" [wb] reload sweep: K=%ld, %ld points\n", k, points); +} + +/* Run all four sweeps for one (family, shape) pair. */ +static void wb_run_combo(const WbFamily* fam, const WbShape* shape) +{ + printf(" [wb] --- family %s, l=%u h=%u rl=%u cb=%u ---\n", fam->name, + (unsigned)shape->levels, (unsigned)shape->height, + (unsigned)shape->rootLevels, (unsigned)shape->cacheBits); + + wb_make_params(&wb_params, fam, shape); + wb_nsigs = shape->nsigs; + + /* priv_data is sized once for the largest combo; only the prefix this + * combo needs is used, and both the live and backup buffers keep the same + * address, so the HssPrivKey snapshot stays valid. */ + wb_priv_data_len = LMS_PRIV_DATA_LEN(wb_params.levels, wb_params.height, + wb_params.p, shape->rootLevels, shape->cacheBits, wb_params.hash_len); + if (wb_priv_data_len > wb_priv_data_cap) + wb_priv_data_len = wb_priv_data_cap; + + XMEMSET(wb_priv_raw, 0, sizeof(wb_priv_raw)); + XMEMSET(wb_pub, 0, sizeof(wb_pub)); + XMEMSET(wb_sig, 0, sizeof(wb_sig)); + XMEMSET(&wb_pk, 0, sizeof(wb_pk)); + XMEMSET(wb_priv_data, 0, wb_priv_data_cap); + + wb_sweep_make_key(); + if (wb_expired()) + return; + wb_sweep_sign(); + if (wb_expired()) + return; + wb_sweep_verify(); + if (wb_expired()) + return; + wb_sweep_reload(); +} + +#endif /* WB_HAVE_DRIVER conditions */ + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("wc_lms_impl.c hash-fault white-box supplement\n"); + +#ifdef WB_HAVE_DRIVER + wb_t0 = time(NULL); + + /* Worst case over every (family, shape): largest hash_len, p, levels, + * height, rootLevels and cacheBits used by the tables above. */ + wb_priv_data_cap = LMS_PRIV_DATA_LEN(WB_LEVELS_MAX, WB_HEIGHT_MAX, + WB_P_MAX, WB_HEIGHT_MAX, WB_HEIGHT_MAX, WB_HLEN_MAX); + wb_priv_data = (byte*)XMALLOC(wb_priv_data_cap, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + wb_priv_data_bak = (byte*)XMALLOC(wb_priv_data_cap, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + + if ((wb_priv_data == NULL) || (wb_priv_data_bak == NULL)) { + WB_NOTE("XMALLOC of priv_data failed; nothing driven"); + } + else if (wc_InitRng(&wb_rng) != 0) { + WB_NOTE("wc_InitRng failed; nothing driven"); + } + else { + size_t f, sh; + + /* Every compiled hash family gets its own sweep: wc_lms_impl.c keeps a + * SEPARATE copy of the WOTS/Merkle error chains per family arm. Shape + * 0 is run for every family; the taller shape 1 (rootLevels > 1) only + * for the first, to stay inside the time budget -- its extra decisions + * are family-independent. */ + for (f = 0; f < WB_NFAMILIES; f++) { + for (sh = 0; sh < WB_NSHAPES; sh++) { + if ((sh > 0) && (f > 0)) + continue; + if (wb_expired()) { + WB_NOTE("time budget reached; remaining combos skipped"); + break; + } + wb_run_combo(&wb_families[f], &wb_shapes[sh]); + } + } + + mcdc_fh_disarm(); + wc_FreeRng(&wb_rng); + } + + XFREE(wb_priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(wb_priv_data_bak, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#else + printf(" [wb] LMS keygen/signing not compiled in; nothing to do\n"); +#endif + + printf("done (%s)\n", wb_fail ? "with failures" : "ok"); + /* Setup/skip conditions are notes, not process failures: a non-zero exit + * makes the campaign discard this binary's whole coverage. */ + return 0; +} diff --git a/tests/unit-mcdc/test_sakke_fault_whitebox.c b/tests/unit-mcdc/test_sakke_fault_whitebox.c index f854627fc1c..c187735402c 100644 --- a/tests/unit-mcdc/test_sakke_fault_whitebox.c +++ b/tests/unit-mcdc/test_sakke_fault_whitebox.c @@ -79,6 +79,18 @@ * default is the productive full sweep. */ +/* Installed BEFORE sakke.c so its mp_* calls resolve to the fault wrappers. + * sakke.c's residuals are the `(err == 0) && ` 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 + * 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; + * mcdc_fm_arm(n) fails the n-th mp_* call and every later one. Predicates + * (mp_iszero/mp_cmp/mp_count_bits) and teardown (mp_free/mp_forcezero) are NOT + * interposed, so cleanup keeps working and armed calls stay crash-safe. */ +#include "mcdc_fault_mp.h" + #include #include "mcdc_fault_alloc.h" @@ -86,6 +98,7 @@ #include #include #include +#include static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) @@ -176,6 +189,101 @@ static int build_prepared(SakkeKey* key, WC_RNG* rng, ecc_point* rsk) } while (0) #endif +/* ---- big-integer fault sweeps (mcdc_fault_mp.h) ------------------------- */ +#define WB_MP_DEADLINE 90 + +static time_t wb_mp_t0; + +static int wb_mp_expired(void) +{ + return difftime(time(NULL), wb_mp_t0) > (double)WB_MP_DEADLINE; +} + +/* Run the statement once DISARMED -- the all-true baseline row for every guard + * it touches, in THIS binary, and the sweep length K -- then sweep the fail + * index over [1..min(K, cap)]. */ +#define WB_MP_SWEEP(lbl, cap, ...) \ + do { \ + long k_, i_; \ + mcdc_fm_disarm(); \ + { __VA_ARGS__; } \ + k_ = mcdc_fm_seen(); \ + if (k_ > (long)(cap)) \ + k_ = (long)(cap); \ + for (i_ = 1; (i_ <= k_) && !wb_mp_expired(); i_++) { \ + mcdc_fm_arm(i_); \ + { __VA_ARGS__; } \ + mcdc_fm_disarm(); \ + } \ + printf(" [wb] mp sweep %s: K=%ld\n", (lbl), k_); \ + } while (0) + +static void wb_mp_sweeps(SakkeKey* key, WC_RNG* rng, ecc_point* rsk) +{ + byte ssv2[128]; + byte auth2[257]; + word16 aSz; + + wb_mp_t0 = time(NULL); + mcdc_fm_disarm(); + XMEMSET(ssv2, 0x5a, sizeof(ssv2)); + 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. */ + WB_MP_SWEEP("MakeSakkeKey", 150, + { + SakkeKey k2; + if (wc_InitSakkeKey_ex(&k2, 128, ECC_SAKKE_1, NULL, + INVALID_DEVID) == 0) { + (void)wc_MakeSakkeKey(&k2, rng); + wc_FreeSakkeKey(&k2); + } + }); + + WB_MP_SWEEP("MakeSakkeRsk", 200, + { + ecc_point* r2 = wc_ecc_new_point(); + if (r2 != NULL) { + (void)wc_MakeSakkeRsk(key, gId, gIdSz, r2); + wc_ecc_del_point(r2); + } + }); + + WB_MP_SWEEP("ValidateSakkeRsk", 250, + { + int v = -1; + (void)wc_ValidateSakkeRsk(key, gId, gIdSz, rsk, &v); + }); + + WB_MP_SWEEP("MakeSakkeEncapsulatedSSV", 250, + { + byte s2[128]; + byte a2[257]; + word16 z = (word16)sizeof(a2); + XMEMSET(s2, 0x5a, sizeof(s2)); + (void)wc_MakeSakkeEncapsulatedSSV(key, WC_HASH_TYPE_SHA256, s2, 16, + a2, &z); + }); + + /* One valid encapsulation, produced DISARMED, for the derive sweep. */ + mcdc_fm_disarm(); + aSz = (word16)sizeof(auth2); + if (wc_MakeSakkeEncapsulatedSSV(key, WC_HASH_TYPE_SHA256, ssv2, 16, auth2, + &aSz) == 0) { + WB_MP_SWEEP("DeriveSakkeSSV", 250, + { + byte s3[128]; + XMEMCPY(s3, ssv2, 16); + (void)wc_DeriveSakkeSSV(key, WC_HASH_TYPE_SHA256, s3, 16, + auth2, aSz); + }); + } + + mcdc_fm_disarm(); + WB_NOTE("big-integer fault sweeps done"); +} + int main(int argc, char** argv) { int do_baseline = (argc > 1 && strcmp(argv[1], "baseline") == 0); @@ -190,6 +298,9 @@ int main(int argc, char** argv) int n; int ret; + /* Unbuffered: if an armed call ever crashes, the notes printed so far + * must survive to say WHICH sweep it died in. */ + setvbuf(stdout, NULL, _IONBF, 0); printf("sakke.c fault white-box (%s)\n", do_baseline ? "baseline" : (do_probe ? "probe" : "sweep")); @@ -471,6 +582,9 @@ int main(int argc, char** argv) mcdc_fa_disarm(); mcdc_fa_restore(); + if (!do_baseline && !do_probe) + wb_mp_sweeps(&key, &rng, rsk); + mcdc_fm_disarm(); wc_FreeSakkeKey(&key); wc_ecc_forcezero_point(rsk); wc_ecc_del_point(rsk); diff --git a/tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c b/tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c new file mode 100644 index 00000000000..7dcf7ad08ae --- /dev/null +++ b/tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c @@ -0,0 +1,359 @@ +/* test_slhdsa_hash_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC hash-fault white-box supplement for wolfcrypt/src/wc_slhdsa.c. + * + * campaign/reports/slhdsa/GAPS.md is almost entirely error propagation: + * + * if ((ret == 0) && (hdr != NULL)) -- PRF_msg / H_msg + * if ((ret == 0) && (ctxSz > 0) && (ctx != NULL)) streaming chains + * if ((ret == 0) && (ctxSz > 0)) + * while ((ret == 0) && (done < outLen)) -- MGF1 + * if ((ret != 0) && WC_VAR_OK(sk)) -- WOTS+ cleanup + * if ((ret == 0) && (XMEMCMP(node, pk_root, n) != 0)) + * + * wc_slhdsa.c contains ZERO XMALLOC calls, so mcdc_fault_alloc.h has nothing + * to fault: every `ret` in this file comes from a SHA-2, SHAKE or HMAC + * primitive. mcdc_fault_hash.h macro-interposes those for THIS translation + * unit only, before wc_slhdsa.c is #included, and mcdc_fh_arm(n) makes the + * n-th primitive call -- and every later one -- return BAD_FUNC_ARG. + * wc_InitSha256/512 and wc_InitShake* are NOT interposed, so the key's own + * hash-object setup is never faulted (only its *use* is). + * + * WHERE THE INDEX HAS TO LAND + * --------------------------- + * SLH-DSA sign is by far the most expensive operation in the campaign, so the + * sweep is deliberately shaped: + * + * - a DENSE head (1..WB_DENSE) over every entry point. Almost all of the + * residuals are in the PRF_msg / H_msg / MGF1 streaming preamble, which is + * within the first few dozen primitive calls of Sign/Verify -- and an + * armed call there aborts immediately, so these points are nearly free; + * - a STRIDED tail with a small point budget, for the deep ones (the WOTS+ + * ForceZero-on-error cleanup and the hypertree root compare); + * - `f` (fast) parameter sets in preference to `s`, and Verify swept more + * densely than Sign, because verify is orders of magnitude cheaper. + * + * Every sweep also tests a CPU-time deadline, so the binary degrades to fewer + * points instead of being killed at the campaign's 600 s TEST_TIMEOUT -- a + * timeout is scored as a SILENT SKIP and would lose the whole file (HARD + * RULE 2). + * + * NOT REACHABLE HERE (documented residual): `(ret == 0) && (n > 16)` at + * slhdsakey_sha2_midstate() and wc_SlhDsaKey_Init() needs a category 3/5 + * parameter set, and this module's base config compiles ONLY the 128-bit sets + * (WOLFSSL_SLHDSA_PARAM_NO_192/256 and *_NO_SHA2_192/256), so n is always 16 + * and the second operand can never be true. + * + * VARIANT COVERAGE (HARD RULE 3): under WOLFSSL_SLHDSA_VERIFY_ONLY there is no + * keygen or signing, so no signature can be produced and the file becomes a + * skip stub. main() always returns 0. + */ + +#include "mcdc_fault_hash.h" + +/* wc_slhdsa.c is #included AFTER the interposers are installed. */ +#include + +#include +#include +#include +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFSSL_HAVE_SLHDSA) && !defined(WOLFSSL_SLHDSA_VERIFY_ONLY) + +#define WB_HAVE_DRIVER 1 + +/* Dense head covers the streaming preamble of every entry point; the strided + * tail reaches the deep WOTS+/hypertree residuals. */ +#define WB_DENSE 96 +#define WB_POINTS_SIGN 24 +#define WB_POINTS_VERIFY 128 +#define WB_DEADLINE_S 170 + +/* WALL clock, not clock(): the campaign runs several variants concurrently and + * TEST_TIMEOUT is 600 s of WALL time. Under that contention CPU time accrues + * far slower than wall time, so a CPU-time budget would sail past the timeout + * -- and a timed-out white-box is scored as a SILENT SKIP that loses the whole + * file's coverage. */ +static time_t wb_t0; + +static int wb_expired(void) +{ + return difftime(time(NULL), wb_t0) > (double)WB_DEADLINE_S; +} + +static long wb_next(long n, long k, long budget) +{ + long stride; + + if (n < (long)WB_DENSE) + return n + 1; + stride = (k - (long)WB_DENSE) / budget; + if (stride < 1) + stride = 1; + return n + stride; +} + +/* Parameter sets to drive. The `f` (fast) sets are preferred: same code, far + * cheaper signing. Sets absent from the build are rejected by + * wc_SlhDsaKey_Init and skipped. */ +static const int wb_params_list[] = { +#ifndef WC_SLHDSA_ALL_NO_128F + SLHDSA_SHAKE128F, +#endif +#if defined(WOLFSSL_SLHDSA_SHA2) && !defined(WC_SLHDSA_ALL_NO_128F) + SLHDSA_SHA2_128F, +#endif +#ifndef WC_SLHDSA_ALL_NO_128S + SLHDSA_SHAKE128S, +#endif + -1 +}; + +static WC_RNG wb_rng; +static SlhDsaKey wb_key; +static byte wb_sig[WC_SLHDSA_MAX_SIG_LEN]; +static word32 wb_sigLen = 0; +static const byte wb_msg[] = "wc_slhdsa hash-fault white-box message"; +/* A non-empty context exercises the (ctxSz > 0) && (ctx != NULL) operands + * TRUE; the empty-context rows come from the module's ordinary API tests. */ +static const byte wb_ctx[] = { 0x41, 0x42, 0x43 }; + +/* ---- sweeps ------------------------------------------------------------ */ + +static void wb_sweep_sign(int param) +{ + long k, n, points = 0; + word32 len; + int ret; + + mcdc_fh_disarm(); + len = (word32)sizeof(wb_sig); + ret = wc_SlhDsaKey_Sign(&wb_key, wb_ctx, (word32)sizeof(wb_ctx), wb_msg, + (word32)sizeof(wb_msg), wb_sig, &len, &wb_rng); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline Sign failed; sign sweep skipped"); + wb_fail = 1; + return; + } + wb_sigLen = len; + printf(" [wb] param %d: sign K=%ld\n", param, k); + + for (n = 1; (n <= k) && !wb_expired(); + n = wb_next(n, k, WB_POINTS_SIGN)) { + byte s2[WC_SLHDSA_MAX_SIG_LEN]; + word32 l2 = (word32)sizeof(s2); + mcdc_fh_arm(n); + (void)wc_SlhDsaKey_Sign(&wb_key, wb_ctx, (word32)sizeof(wb_ctx), + wb_msg, (word32)sizeof(wb_msg), s2, &l2, &wb_rng); + mcdc_fh_disarm(); + points++; + } + printf(" [wb] sign sweep: %ld points\n", points); +} + +static void wb_sweep_verify(int param) +{ + long k, n, points = 0; + int ret; + + if (wb_sigLen == 0) + return; + + mcdc_fh_disarm(); + ret = wc_SlhDsaKey_Verify(&wb_key, wb_ctx, (word32)sizeof(wb_ctx), wb_msg, + (word32)sizeof(wb_msg), wb_sig, wb_sigLen); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline Verify rejected a valid signature"); + wb_fail = 1; + return; + } + printf(" [wb] param %d: verify K=%ld\n", param, k); + + for (n = 1; (n <= k) && !wb_expired(); + n = wb_next(n, k, WB_POINTS_VERIFY)) { + mcdc_fh_arm(n); + (void)wc_SlhDsaKey_Verify(&wb_key, wb_ctx, (word32)sizeof(wb_ctx), + wb_msg, (word32)sizeof(wb_msg), wb_sig, wb_sigLen); + mcdc_fh_disarm(); + points++; + } + + /* Tampered signature (the XMEMCMP(node, pk_root, n) != 0 TRUE half), run + * DISARMED so it pairs with the ret != 0 rows above. */ + wb_sig[wb_sigLen - 1] ^= 0x01; + mcdc_fh_disarm(); + if (wc_SlhDsaKey_Verify(&wb_key, wb_ctx, (word32)sizeof(wb_ctx), wb_msg, + (word32)sizeof(wb_msg), wb_sig, wb_sigLen) == 0) { + WB_NOTE("Verify accepted a tampered signature"); + wb_fail = 1; + } + wb_sig[wb_sigLen - 1] ^= 0x01; + + printf(" [wb] verify sweep: %ld points\n", points); +} + +/* MakeKey ends with a root computation compared against the stored key + * material; faulting into it drives the keygen-side chains. */ +static void wb_sweep_makekey(int param) +{ + long k, n, points = 0; + + mcdc_fh_disarm(); + for (n = 1; (n <= (long)WB_DENSE) && !wb_expired(); n++) { + SlhDsaKey k2; + XMEMSET(&k2, 0, sizeof(k2)); + if (wc_SlhDsaKey_Init(&k2, (enum SlhDsaParam)param, NULL, + INVALID_DEVID) == 0) { + mcdc_fh_arm(n); + (void)wc_SlhDsaKey_MakeKey(&k2, &wb_rng); + mcdc_fh_disarm(); + points++; + } + wc_SlhDsaKey_Free(&k2); + } + k = 0; + (void)k; + printf(" [wb] makekey sweep: %ld points\n", points); +} + +/* Import/export + DER encode/decode: the ASN-side residuals + * (`(key->params != NULL) && ...`, `while (ret == 0 && *inOutIdx < seqEnd)`) + * live here and cost nothing to drive. */ +static void wb_der_rows(int param) +{ + byte der[WC_SLHDSA_MAX_PRIV_LEN + 128]; + byte pub[WC_SLHDSA_MAX_PUB_LEN]; + word32 idx = 0; + int len; + + mcdc_fh_disarm(); + + len = wc_SlhDsaKey_KeyToDer(&wb_key, der, (word32)sizeof(der)); + if (len > 0) { + SlhDsaKey k2; + XMEMSET(&k2, 0, sizeof(k2)); + if (wc_SlhDsaKey_Init(&k2, (enum SlhDsaParam)param, NULL, + INVALID_DEVID) == 0) { + idx = 0; + (void)wc_SlhDsaKey_PrivateKeyDecode(der, &idx, &k2, (word32)len); + /* Truncated input: drives the decode loops' early-exit rows. */ + idx = 0; + (void)wc_SlhDsaKey_PrivateKeyDecode(der, &idx, &k2, + (word32)len / 2); + } + wc_SlhDsaKey_Free(&k2); + } + + len = wc_SlhDsaKey_PublicKeyToDer(&wb_key, der, (word32)sizeof(der), 1); + if (len > 0) { + SlhDsaKey k2; + XMEMSET(&k2, 0, sizeof(k2)); + if (wc_SlhDsaKey_Init(&k2, (enum SlhDsaParam)param, NULL, + INVALID_DEVID) == 0) { + idx = 0; + (void)wc_SlhDsaKey_PublicKeyDecode(der, &idx, &k2, (word32)len); + idx = 0; + (void)wc_SlhDsaKey_PublicKeyDecode(der, &idx, &k2, + (word32)len / 2); + } + wc_SlhDsaKey_Free(&k2); + } + + (void)wc_SlhDsaKey_ExportPublic(&wb_key, pub, &idx); +} + +static void wb_run_param(int param) +{ + printf(" [wb] --- param %d ---\n", param); + + XMEMSET(&wb_key, 0, sizeof(wb_key)); + wb_sigLen = 0; + + mcdc_fh_disarm(); + if (wc_SlhDsaKey_Init(&wb_key, (enum SlhDsaParam)param, NULL, + INVALID_DEVID) != 0) { + WB_NOTE("parameter set not compiled in; skipped"); + wc_SlhDsaKey_Free(&wb_key); + return; + } + if (wc_SlhDsaKey_MakeKey(&wb_key, &wb_rng) != 0) { + WB_NOTE("MakeKey failed; parameter set skipped"); + wc_SlhDsaKey_Free(&wb_key); + return; + } + + wb_sweep_sign(param); + if (!wb_expired()) + wb_sweep_verify(param); + if (!wb_expired()) + wb_der_rows(param); + if (!wb_expired()) + wb_sweep_makekey(param); + + mcdc_fh_disarm(); + wc_SlhDsaKey_Free(&wb_key); +} + +#endif /* WB_HAVE_DRIVER conditions */ + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("wc_slhdsa.c hash-fault white-box supplement\n"); + +#ifdef WB_HAVE_DRIVER + { + size_t i; + + wb_t0 = time(NULL); + XMEMSET(&wb_rng, 0, sizeof(wb_rng)); + XMEMSET(wb_sig, 0, sizeof(wb_sig)); + + if (wc_InitRng(&wb_rng) != 0) { + WB_NOTE("wc_InitRng failed; nothing driven"); + } + else { + for (i = 0; + i < sizeof(wb_params_list) / sizeof(wb_params_list[0]); i++) { + if ((wb_params_list[i] < 0) || wb_expired()) + break; + wb_run_param(wb_params_list[i]); + } + mcdc_fh_disarm(); + wc_FreeRng(&wb_rng); + } + } +#else + printf(" [wb] SLH-DSA keygen/signing not compiled in; nothing to do\n"); +#endif + + printf("done (%s)\n", wb_fail ? "with failures" : "ok"); + /* A non-zero exit makes the campaign discard this binary's coverage. */ + return 0; +} diff --git a/tests/unit-mcdc/test_tfm_whitebox.c b/tests/unit-mcdc/test_tfm_whitebox.c index 9cb11dd5007..dd418c4db3d 100644 --- a/tests/unit-mcdc/test_tfm_whitebox.c +++ b/tests/unit-mcdc/test_tfm_whitebox.c @@ -792,6 +792,121 @@ static void wb_TfmExptModDecisionCoverage(void) #endif /* USE_FAST_MATH && WOLFSSL_PUBLIC_MP */ +/* ------------------------------------------------------------------------ + * Public-entry ARGUMENT-GUARD residuals. + * + * campaign/reports/bigint-tfm/GAPS.md lists several multi-operand OR guards at + * the top of public entry points whose operands the ordinary tests only ever + * present all-false (they always pass valid arguments), so no operand's + * independence pair is shown. Each is closed here by calling the entry point + * once per operand with exactly THAT operand true and the rest false, against + * the all-false row that the same calls' valid form provides -- both halves in + * this one binary. + * + * These need no fault injection: they are pure argument shapes. + * ---------------------------------------------------------------------- */ +static void wb_entry_arg_guards(void) +{ +#ifdef WOLFSSL_PUBLIC_MP + fp_int a; + fp_int b; + fp_int c; + unsigned char out[64]; + int res = 0; + int ret; + + fp_init(&a); + fp_init(&b); + fp_init(&c); + fp_set(&a, 7); + fp_set(&b, 11); + XMEMSET(out, 0, sizeof(out)); + + /* 3918: fp_to_unsigned_bin_len_ct(): + * if ((a == NULL) || (out == NULL) || (outSz < 0)) + * all-false row plus one row per operand. */ + ret = fp_to_unsigned_bin_len_ct(&a, out, (int)sizeof(out)); + if (ret != MP_OKAY) { + WB_NOTE("fp_to_unsigned_bin_len_ct(valid) unexpectedly failed"); + wb_fail = 1; + } + if (fp_to_unsigned_bin_len_ct(NULL, out, (int)sizeof(out)) != + WC_NO_ERR_TRACE(MP_VAL) || + fp_to_unsigned_bin_len_ct(&a, NULL, (int)sizeof(out)) != + WC_NO_ERR_TRACE(MP_VAL) || + fp_to_unsigned_bin_len_ct(&a, out, -1) != WC_NO_ERR_TRACE(MP_VAL)) { + WB_NOTE("fp_to_unsigned_bin_len_ct arg guard did not reject"); + wb_fail = 1; + } + + /* 5480: fp_lcm(): + * if (fp_iszero(a) == FP_YES || fp_iszero(b) == FP_YES) + * a=0 isolates operand 0; b=0 isolates operand 1; (7, 11) is all-false. */ + if (fp_lcm(&a, &b, &c) != FP_OKAY) { + WB_NOTE("fp_lcm(valid) unexpectedly failed"); + wb_fail = 1; + } + { + fp_int z; + fp_init(&z); /* zero */ + if (fp_lcm(&z, &b, &c) != FP_VAL || + fp_lcm(&a, &z, &c) != FP_VAL) { + WB_NOTE("fp_lcm zero guard did not reject"); + wb_fail = 1; + } + } + +#if !defined(WC_NO_RNG) + /* 5222: mp_prime_is_prime_ex(): + * if (a == NULL || result == NULL || rng == NULL) + * 5226: same function: + * if (t <= 0 || t > FP_PRIME_SIZE) + * One row per operand plus the all-false valid call. */ + { + WC_RNG rng; + + XMEMSET(&rng, 0, sizeof(rng)); + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; prime_is_prime_ex guards skipped"); + } + else { + /* all-false: valid pointers and 0 < t <= FP_PRIME_SIZE */ + (void)mp_prime_is_prime_ex(&a, 8, &res, &rng); + + if (mp_prime_is_prime_ex(NULL, 8, &res, &rng) != FP_VAL || + mp_prime_is_prime_ex(&a, 8, NULL, &rng) != FP_VAL || + mp_prime_is_prime_ex(&a, 8, &res, NULL) != FP_VAL) { + WB_NOTE("mp_prime_is_prime_ex NULL guard did not reject"); + wb_fail = 1; + } + if (mp_prime_is_prime_ex(&a, 0, &res, &rng) != FP_VAL || + mp_prime_is_prime_ex(&a, FP_PRIME_SIZE + 1, &res, &rng) + != FP_VAL) { + WB_NOTE("mp_prime_is_prime_ex t-range guard did not reject"); + wb_fail = 1; + } + wc_FreeRng(&rng); + } + } +#endif /* !WC_NO_RNG */ + + /* 2858: fp_exptmod()'s + * if (fp_iszero(P) || (P->used > (FP_SIZE/2))) + * P == 0 isolates operand 0 against the ordinary valid-modulus row. */ + { + fp_int zeroP; + fp_int r; + + fp_init(&zeroP); + fp_init(&r); + (void)fp_exptmod(&a, &b, &zeroP, &r); /* operand 0 true */ + (void)fp_exptmod(&a, &b, &b, &r); /* both false */ + } + + WB_NOTE("public-entry argument-guard rows driven"); +#endif /* WOLFSSL_PUBLIC_MP */ +} + int main(void) { printf("tfm.c white-box MC/DC supplement\n"); @@ -817,6 +932,7 @@ int main(void) wb_TfmDecisionCoverage(); wb_TfmExptModDecisionCoverage(); #endif + wb_entry_arg_guards(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures surface as skips, not failures: a nonzero exit makes the * campaign discard this variant's coverage. */ diff --git a/tests/unit-mcdc/test_xmss_hash_fault_whitebox.c b/tests/unit-mcdc/test_xmss_hash_fault_whitebox.c new file mode 100644 index 00000000000..3d8d59694d4 --- /dev/null +++ b/tests/unit-mcdc/test_xmss_hash_fault_whitebox.c @@ -0,0 +1,422 @@ +/* test_xmss_hash_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC hash-fault white-box supplement for wolfcrypt/src/wc_xmss_impl.c. + * + * campaign/reports/xmss/GAPS.md is entirely error-propagation: + * + * for (i = 1; (ret == 0) && (i < params->wots_len); i++) -- WOTS+ chain + * for (i = 0; (ret == 0) && (i < params->d); i++) -- subtree loops + * if ((ret == 0) && (WC_IDX_INVALID(idx, ...))) + * if ((ret == 0) && (XMEMCMP(node, pub_root, n) != 0)) + * + * The (ret == 0) operand only goes FALSE when an earlier step in the same + * operation failed. wc_xmss_impl.c has exactly ONE allocation (the BDS state + * in the non-SMALL arm) -- everything else that can set `ret` is a SHA-2 / + * SHAKE primitive call, and those never touch the heap. mcdc_fault_alloc.h + * therefore cannot reach these; mcdc_fault_hash.h can. + * + * mcdc_fault_hash.h macro-interposes wc_Sha256Update/Final, wc_Sha512Update/ + * Final and the SHAKE Update/Final family for THIS translation unit only, and + * mcdc_fh_arm(n) makes the n-th primitive call -- and every later one -- + * return BAD_FUNC_ARG. wc_InitSha256/wc_InitSha512 are deliberately NOT + * interposed, so this file's own state setup can never be faulted. + * + * DRIVING IT CHEAPLY + * ------------------ + * The smallest parameter set wc_xmss.c will hand out is height 10 (1024 + * leaves), whose keygen is over a million hash calls -- far too expensive to + * repeat a few hundred times. So, exactly like the sibling + * test_wc_xmss_impl_whitebox.c, this file hand-builds an XmssParams with a + * deliberately tiny height (h=4 => 16 leaves) and calls the link-local + * wc_xmssmt_keygen / wc_xmssmt_sign / wc_xmssmt_verify / wc_xmss_sigsleft + * entry points directly. Both d=1 (single tree) and d=2 (XMSS^MT, two layers) + * shapes are driven, because the per-layer loops + * (`(ret == 0) && (i < params->d)`) need d > 1 to have a second iteration. + * + * Each entry point is swept SEPARATELY with its inputs built while DISARMED: + * one unarmed run gives both the all-true baseline row for every guard (HARD + * RULE 1: same binary) and the sweep length K; then n is swept over [1..K], + * dense over the first WB_DENSE indices and strided after that to a fixed + * point budget. The secret key is restored from a snapshot before every armed + * sign, so each armed call starts from the same known-good state. + * + * NEVER HANG (HARD RULE 2): every sweep tests a CPU-time deadline as well as + * the point budget, so the binary degrades to fewer points rather than being + * killed. WOLFSSL_WC_XMSS_SMALL's recompute signing path is several times + * slower and relies on that. + * + * VARIANT COVERAGE (HARD RULE 3): WOLFSSL_XMSS_VERIFY_ONLY compiles keygen and + * signing out, and no valid signature can be built without them, so that + * variant gets a skip stub. main() always returns 0. + */ + +#include "mcdc_fault_hash.h" + +/* wc_xmss_impl.c is #included AFTER the interposers are installed. */ +#include + +#include +#include +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFSSL_HAVE_XMSS) && !defined(WOLFSSL_XMSS_VERIFY_ONLY) && \ + defined(WC_XMSS_SHA256) + +#define WB_HAVE_DRIVER 1 + +#define WB_DENSE 48 +#define WB_POINTS 192 +#define WB_DEADLINE_S 170 + +/* WALL clock, not clock(): the campaign runs several variants concurrently and + * TEST_TIMEOUT is 600 s of WALL time. Under that contention CPU time accrues + * far slower than wall time, so a CPU-time budget would sail past the timeout + * -- and a timed-out white-box is scored as a SILENT SKIP that loses the whole + * file's coverage. */ +static time_t wb_t0; + +static int wb_expired(void) +{ + return difftime(time(NULL), wb_t0) > (double)WB_DEADLINE_S; +} + +static long wb_next(long n, long k) +{ + long stride; + + if (n < (long)WB_DENSE) + return n + 1; + stride = (k - (long)WB_DENSE) / (long)WB_POINTS; + if (stride < 1) + stride = 1; + return n + stride; +} + +/* Hand-build an XmssParams the way wc_xmss.c's XMSS_PARAMS() macro would (that + * macro is not visible here), but with a deliberately tiny height so a full + * keygen/sign/verify cycle is cheap enough to repeat a few hundred times. + * Copied from the sibling test_wc_xmss_impl_whitebox.c so both files agree on + * the sk_len/sig_len formulas. */ +static void wb_params_init(XmssParams* p, byte hash, byte n, byte pad_len, + byte h, byte d, byte idx_len, byte bds_k) +{ + byte sub_h = (byte)(h / d); + word8 hsk = (word8)(sub_h - bds_k); + + XMEMSET(p, 0, sizeof(*p)); + p->hash = hash; + p->n = n; + p->pad_len = pad_len; + p->wots_len = (word8)(n * 2 + 3); + p->wots_sig_len = (word16)(n * p->wots_len); + p->h = h; + p->sub_h = sub_h; + p->d = d; + p->idx_len = idx_len; + p->sig_len = (word32)idx_len + n + + (word32)d * ((word32)n * 2 + 3) * n + (word32)h * n; + p->sk_len = (word32)idx_len + 4U * n + + (word32)(2 * d - 1) * ((word32)(sub_h + 1) * n + (word32)(sub_h + 1) + + (word32)sub_h * n + (word32)(sub_h >> 1) * n + + (word32)hsk * 4U + (word32)hsk * n + + XMSS_RETAIN_LEN(bds_k, n) + 4U) + + (word32)(d - 1) * n * ((word32)n * 2 + 3); + p->pk_len = (word8)(n * 2); + p->bds_k = bds_k; +} + +/* wc_xmss_digest_init()'s job (that helper is file-static in wc_xmss.c). + * wc_InitSha256/512 are NOT interposed, so setup can never be faulted. */ +static int wb_state_init(XmssState* state, const XmssParams* params) +{ + int ret; + + XMEMSET(state, 0, sizeof(*state)); + state->params = params; + state->heap = NULL; + state->ret = 0; + +#ifdef WC_XMSS_SHA512 + if (params->hash == WC_HASH_TYPE_SHA512) { + ret = wc_InitSha512(&state->digest.sha512); + } + else +#endif + { + ret = wc_InitSha256(&state->digest.sha256); + } + return ret; +} + +static void wb_state_free(XmssState* state) +{ +#ifdef WC_XMSS_SHA512 + if (state->params->hash == WC_HASH_TYPE_SHA512) { + wc_Sha512Free(&state->digest.sha512); + return; + } +#endif + wc_Sha256Free(&state->digest.sha256); +} + +/* ---- fixture ----------------------------------------------------------- */ + +/* Sized well past the h=4 d=1/d=2 SHA-256 shapes used below (the sibling + * white-box uses 8192/8192 for the same shapes). */ +#define WB_SK_LEN 16384 +#define WB_SIG_LEN 16384 + +static XmssParams wb_params; +static XmssState wb_state; +static byte wb_seed[3 * 64]; +static byte wb_sk[WB_SK_LEN]; +static byte wb_sk_bak[WB_SK_LEN]; +static byte wb_pk[256]; +static byte wb_pk_bak[256]; +static byte wb_sig[WB_SIG_LEN]; +static const byte wb_msg[] = "wc_xmss_impl hash-fault white-box message"; + +static void wb_snapshot(void) +{ + XMEMCPY(wb_sk_bak, wb_sk, sizeof(wb_sk)); + XMEMCPY(wb_pk_bak, wb_pk, sizeof(wb_pk)); +} + +static void wb_restore(void) +{ + XMEMCPY(wb_sk, wb_sk_bak, sizeof(wb_sk)); + XMEMCPY(wb_pk, wb_pk_bak, sizeof(wb_pk)); +} + +/* Each wb_do_* runs ONE entry point with a freshly initialised XmssState. */ + +static int wb_do_keygen(void) +{ + int ret = wb_state_init(&wb_state, &wb_params); + + if (ret == 0) { + ret = wc_xmssmt_keygen(&wb_state, wb_seed, wb_sk, wb_pk); + wb_state_free(&wb_state); + } + return ret; +} + +static int wb_do_sign(int nsigs) +{ + int ret = wb_state_init(&wb_state, &wb_params); + int i; + + if (ret == 0) { + for (i = 0; (ret == 0) && (i < nsigs); i++) { + ret = wc_xmssmt_sign(&wb_state, wb_msg, (word32)sizeof(wb_msg), + wb_sk, wb_sig); + } + wb_state_free(&wb_state); + } + return ret; +} + +static int wb_do_verify(void) +{ + int ret = wb_state_init(&wb_state, &wb_params); + + if (ret == 0) { + ret = wc_xmssmt_verify(&wb_state, wb_msg, (word32)sizeof(wb_msg), + wb_sig, wb_pk); + wb_state_free(&wb_state); + } + return ret; +} + +/* ---- sweeps ------------------------------------------------------------ */ + +static void wb_sweep_keygen(void) +{ + long k, n, points = 0; + int ret; + + mcdc_fh_disarm(); + ret = wb_do_keygen(); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline keygen failed; keygen sweep skipped"); + wb_fail = 1; + return; + } + wb_snapshot(); + + for (n = 1; (n <= k) && !wb_expired(); n = wb_next(n, k)) { + mcdc_fh_arm(n); + (void)wb_do_keygen(); + mcdc_fh_disarm(); + points++; + } + + /* The sweep left sk/pk in an aborted state; restore the good keygen. */ + wb_restore(); + printf(" [wb] keygen sweep: K=%ld, %ld points\n", k, points); +} + +static void wb_sweep_sign(int nsigs) +{ + long k, n, points = 0; + int ret; + + wb_restore(); + mcdc_fh_disarm(); + ret = wb_do_sign(nsigs); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline sign failed; sign sweep skipped"); + wb_fail = 1; + wb_restore(); + return; + } + + for (n = 1; (n <= k) && !wb_expired(); n = wb_next(n, k)) { + wb_restore(); /* prepared while DISARMED */ + mcdc_fh_arm(n); + (void)wb_do_sign(nsigs); + mcdc_fh_disarm(); + points++; + } + + wb_restore(); + printf(" [wb] sign sweep: K=%ld, %ld points\n", k, points); +} + +static void wb_sweep_verify(void) +{ + long k, n, points = 0; + int ret; + + /* One valid signature, produced disarmed. */ + wb_restore(); + mcdc_fh_disarm(); + if (wb_do_sign(1) != 0) { + WB_NOTE("signing for the verify sweep failed; verify sweep skipped"); + wb_fail = 1; + wb_restore(); + return; + } + + mcdc_fh_disarm(); + ret = wb_do_verify(); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline verify rejected a valid signature"); + wb_fail = 1; + wb_restore(); + return; + } + + for (n = 1; (n <= k) && !wb_expired(); n = wb_next(n, k)) { + mcdc_fh_arm(n); + (void)wb_do_verify(); + mcdc_fh_disarm(); + points++; + } + + /* Tampered signature (the XMEMCMP(node, pub_root, n) != 0 half), disarmed + * -- the TRUE side that pairs with the ret != 0 rows above. */ + wb_sig[wb_params.sig_len - 1] ^= 0x01; + mcdc_fh_disarm(); + if (wb_do_verify() == 0) { + WB_NOTE("verify accepted a tampered signature"); + wb_fail = 1; + } + wb_sig[wb_params.sig_len - 1] ^= 0x01; + + wb_restore(); + printf(" [wb] verify sweep: K=%ld, %ld points\n", k, points); +} + +/* wc_xmss_sigsleft() is pure bookkeeping over sk (no hash calls), so it is + * driven directly rather than swept: fresh key (indices left) and the + * exhausted key the sign loop leaves behind. */ +static void wb_sigsleft_rows(void) +{ + mcdc_fh_disarm(); + (void)wc_xmss_sigsleft(&wb_params, wb_sk); +} + +/* Run every sweep for one (d, bds_k) shape. */ +static void wb_run_shape(byte h, byte d, byte bds_k, int nsigs) +{ + printf(" [wb] --- h=%u d=%u bds_k=%u ---\n", (unsigned)h, (unsigned)d, + (unsigned)bds_k); + + wb_params_init(&wb_params, WC_HASH_TYPE_SHA256, 32, 32, h, d, 4, bds_k); + if (wb_params.sk_len > (word32)sizeof(wb_sk) || + wb_params.sig_len > (word32)sizeof(wb_sig)) { + WB_NOTE("shape exceeds the scratch buffers; skipped"); + return; + } + + XMEMSET(wb_seed, 0x33, sizeof(wb_seed)); + XMEMSET(wb_sk, 0, sizeof(wb_sk)); + XMEMSET(wb_pk, 0, sizeof(wb_pk)); + XMEMSET(wb_sig, 0, sizeof(wb_sig)); + + wb_sweep_keygen(); + if (wb_expired()) + return; + wb_sweep_sign(nsigs); + wb_sigsleft_rows(); + if (wb_expired()) + return; + wb_sweep_verify(); +} + +#endif /* WB_HAVE_DRIVER conditions */ + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("wc_xmss_impl.c hash-fault white-box supplement\n"); + +#ifdef WB_HAVE_DRIVER + wb_t0 = time(NULL); + + /* d=1: single tree, the plain wc_xmss_* helpers. + * d=2: XMSS^MT, two layers -- required for the + * `for (i = ...; (ret == 0) && (i < params->d); ...)` loops to have a + * second iteration, and for wc_xmssmt_sign_next_idx()'s subtree + * rollover. + * bds_k=0 keeps the BDS bookkeeping trivially valid for both. */ + wb_run_shape(4, 1, 0, 3); + if (!wb_expired()) + wb_run_shape(4, 2, 0, 5); + + mcdc_fh_disarm(); +#else + printf(" [wb] XMSS keygen/signing not compiled in; nothing to do\n"); +#endif + + printf("done (%s)\n", wb_fail ? "with failures" : "ok"); + /* A non-zero exit makes the campaign discard this binary's coverage. */ + return 0; +} From f4160ce29653b10a001cff7b6c8c00cfe790b1ca Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 18:11:12 +0200 Subject: [PATCH 13/20] tests: drive the PKCS7 argument chains and decode mutations --- tests/unit-mcdc/test_pkcs7_arg_whitebox.c | 1756 ++++++++++++++++++ tests/unit-mcdc/test_pkcs7_mutate_whitebox.c | 1029 ++++++++++ tests/unit-mcdc/test_pkcs7_whitebox.c | 5 + 3 files changed, 2790 insertions(+) create mode 100644 tests/unit-mcdc/test_pkcs7_arg_whitebox.c create mode 100644 tests/unit-mcdc/test_pkcs7_mutate_whitebox.c diff --git a/tests/unit-mcdc/test_pkcs7_arg_whitebox.c b/tests/unit-mcdc/test_pkcs7_arg_whitebox.c new file mode 100644 index 00000000000..98a1ea41264 --- /dev/null +++ b/tests/unit-mcdc/test_pkcs7_arg_whitebox.c @@ -0,0 +1,1756 @@ +/* test_pkcs7_arg_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Argument-chain / data-shape white-box MC/DC supplement for + * wolfcrypt/src/pkcs7.c (Part 5). + * + * Companion to test_pkcs7_whitebox.c: that file drives the streaming and + * parsing internals; this one drives the argument guards and the small + * data-shape decisions (attribute-flag matrices, SignerInfo/certificate + * binding, content-shape selectors) that neither tests/api nor the other + * white-boxes reach. + * + * MC/DC is derived per binary, so every operand's "this one alone fires" + * vector is paired with the all-false vector inside this same file. The + * accepting vector only has to make the guard evaluate false; failing + * deeper in is fine and expected. + */ + +#include + +#include +#include +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) +#define WB_CHECK(cond, msg) \ + do { if (!(cond)) { printf(" [wb][FAIL] %s\n", (msg)); wb_fail = 1; } } \ + while (0) + +/* ESD is multi-kilobyte; keep it off the stack. */ +static ESD wbEsd; +static byte wbSigBuf[512]; + +static const byte wbContentTypeOid[] = + { ASN_OBJECT_ID, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xF7, 0x0d, 0x01, + 0x09, 0x03 }; +static const byte wbMessageDigestOid[] = + { ASN_OBJECT_ID, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x09, 0x04 }; +static const byte wbSigningTimeOid[] = + { ASN_OBJECT_ID, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xF7, 0x0d, 0x01, + 0x09, 0x05 }; +/* id-data OID, DER encoded */ +static const byte wbDataOid[] = + { ASN_OBJECT_ID, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x07, 0x01 }; + +/* ------------------------------------------------------------------------- * + * Section 1: private-key import + sign helper argument chains + * [:2054, :2105, :2148, :2196, :2300] + * ------------------------------------------------------------------------- */ +static void wb_sign_helper_args(void) +{ + wc_PKCS7 p; + WC_RNG rng; + byte in[32]; + + XMEMSET(in, 0x5a, sizeof(in)); + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; wb_sign_helper_args skipped"); + wb_fail = 1; + return; + } + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_sign_helper_args skipped"); + wb_fail = 1; + wc_FreeRng(&rng); + return; + } + + XMEMSET(&wbEsd, 0, sizeof(wbEsd)); + wbEsd.hashType = WC_HASH_TYPE_SHA256; + wbEsd.encContentDigest = wbSigBuf; + wbEsd.encContentDigestBufSz = (word32)sizeof(wbSigBuf); + +#ifndef NO_RSA + WB_NOTE("wc_PKCS7_RsaSign() arg chain [:2105] + ImportRSA [:2054]"); + p.rng = &rng; + p.privateKey = NULL; + p.privateKeySz = 0; + (void)wc_PKCS7_RsaSign(NULL, in, (word32)sizeof(in), &wbEsd); + p.rng = NULL; + (void)wc_PKCS7_RsaSign(&p, in, (word32)sizeof(in), &wbEsd); + p.rng = &rng; + (void)wc_PKCS7_RsaSign(&p, NULL, (word32)sizeof(in), &wbEsd); + (void)wc_PKCS7_RsaSign(&p, in, (word32)sizeof(in), NULL); + /* all false: reaches ImportRSA with privateKey==NULL (:2054 1st false) */ + (void)wc_PKCS7_RsaSign(&p, in, (word32)sizeof(in), &wbEsd); + /* :2054 2nd operand false */ + p.privateKey = (byte*)client_key_der_2048; + p.privateKeySz = 0; + (void)wc_PKCS7_RsaSign(&p, in, (word32)sizeof(in), &wbEsd); + /* :2054 both true -> real RSA signature */ + p.privateKeySz = (word32)sizeof_client_key_der_2048; + WB_CHECK(wc_PKCS7_RsaSign(&p, in, (word32)sizeof(in), &wbEsd) > 0, + ":2054 both true (real RSA sign)"); + +#ifdef WC_RSA_PSS + WB_NOTE("wc_PKCS7_RsaPssSign() arg chain [:2300]"); + (void)wc_PKCS7_RsaPssSign(NULL, in, (word32)sizeof(in), &wbEsd); + p.rng = NULL; + (void)wc_PKCS7_RsaPssSign(&p, in, (word32)sizeof(in), &wbEsd); + p.rng = &rng; + (void)wc_PKCS7_RsaPssSign(&p, NULL, (word32)sizeof(in), &wbEsd); + (void)wc_PKCS7_RsaPssSign(&p, in, (word32)sizeof(in), NULL); + /* all false */ + (void)wc_PKCS7_RsaPssSign(&p, in, (word32)sizeof(in), &wbEsd); +#endif +#endif /* !NO_RSA */ + +#ifdef HAVE_ECC + WB_NOTE("wc_PKCS7_EcdsaSign() arg chain [:2196] + ImportECC [:2148]"); + p.rng = &rng; + p.privateKey = NULL; + p.privateKeySz = 0; + (void)wc_PKCS7_EcdsaSign(NULL, in, (word32)sizeof(in), &wbEsd); + p.rng = NULL; + (void)wc_PKCS7_EcdsaSign(&p, in, (word32)sizeof(in), &wbEsd); + p.rng = &rng; + (void)wc_PKCS7_EcdsaSign(&p, NULL, (word32)sizeof(in), &wbEsd); + (void)wc_PKCS7_EcdsaSign(&p, in, (word32)sizeof(in), NULL); + /* all false: ImportECC with privateKey==NULL (:2148 1st operand false) */ + (void)wc_PKCS7_EcdsaSign(&p, in, (word32)sizeof(in), &wbEsd); + /* :2148 2nd operand false */ + p.privateKey = (byte*)ecc_clikey_der_256; + p.privateKeySz = 0; + (void)wc_PKCS7_EcdsaSign(&p, in, (word32)sizeof(in), &wbEsd); + /* :2148 both true -> real ECDSA signature */ + p.privateKeySz = (word32)sizeof_ecc_clikey_der_256; + WB_CHECK(wc_PKCS7_EcdsaSign(&p, in, (word32)sizeof(in), &wbEsd) > 0, + ":2148 both true (real ECDSA sign)"); +#endif /* HAVE_ECC */ + + p.privateKey = NULL; + p.privateKeySz = 0; + p.rng = NULL; + wc_PKCS7_Free(&p); + wc_FreeRng(&rng); +} + +/* ------------------------------------------------------------------------- * + * Section 2: default signed-attribute flag matrix + * [:2455, :2457, :2460] and the same matrix inside BuildSignedAttributes + * [:2519, :2529, :2539] plus its bound checks [:2551, :2568, :2574]. + * ------------------------------------------------------------------------- */ +static void wb_signed_attrib_flags(void) +{ + wc_PKCS7 p; + EncodedAttrib attribs[8]; + PKCS7Attrib custom[1]; + byte signingTime[MAX_TIME_STRING_SZ]; + static const byte customOid[] = { 0x06, 0x03, 0x55, 0x04, 0x03 }; + static const byte customVal[] = { 0x0c, 0x01, 0x41 }; + word16 flagRow[5]; + int i; + int ret; + + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_signed_attrib_flags skipped"); + wb_fail = 1; + return; + } + + flagRow[0] = 0; + flagRow[1] = WOLFSSL_CONTENT_TYPE_ATTRIBUTE; + flagRow[2] = WOLFSSL_SIGNING_TIME_ATTRIBUTE; + flagRow[3] = WOLFSSL_MESSAGE_DIGEST_ATTRIBUTE; + flagRow[4] = WOLFSSL_NO_ATTRIBUTES; + + WB_NOTE("wc_PKCS7_GetDefaultSignedAttribCount() flag matrix " + "[:2455,:2457,:2460]"); + (void)wc_PKCS7_GetDefaultSignedAttribCount(NULL); + for (i = 0; i < 5; i++) { + p.defaultSignedAttribs = flagRow[i]; + (void)wc_PKCS7_GetDefaultSignedAttribCount(&p); + } + + WB_NOTE("wc_PKCS7_BuildSignedAttributes() flag matrix + bounds " + "[:2519,:2529,:2539,:2551,:2568,:2574]"); + for (i = 0; i < 5; i++) { + XMEMSET(&wbEsd, 0, sizeof(wbEsd)); + XMEMSET(attribs, 0, sizeof(attribs)); + wbEsd.hashType = WC_HASH_TYPE_SHA256; + wbEsd.signedAttribs = attribs; + wbEsd.signedAttribsCap = (word32)(sizeof(attribs) / + sizeof(attribs[0])); + p.defaultSignedAttribs = flagRow[i]; + p.signedAttribs = NULL; + p.signedAttribsSz = 0; + (void)wc_PKCS7_BuildSignedAttributes(&p, &wbEsd, wbDataOid, + (word32)sizeof(wbDataOid), wbContentTypeOid, + (word32)sizeof(wbContentTypeOid), wbMessageDigestOid, + (word32)sizeof(wbMessageDigestOid), wbSigningTimeOid, + (word32)sizeof(wbSigningTimeOid), signingTime, + (word32)sizeof(signingTime)); + } + + /* :2551 2nd operand true -- working array too small for the canned set. */ + XMEMSET(&wbEsd, 0, sizeof(wbEsd)); + XMEMSET(attribs, 0, sizeof(attribs)); + wbEsd.hashType = WC_HASH_TYPE_SHA256; + wbEsd.signedAttribs = attribs; + wbEsd.signedAttribsCap = 1; + p.defaultSignedAttribs = 0; + p.signedAttribs = NULL; + p.signedAttribsSz = 0; + ret = wc_PKCS7_BuildSignedAttributes(&p, &wbEsd, wbDataOid, + (word32)sizeof(wbDataOid), wbContentTypeOid, + (word32)sizeof(wbContentTypeOid), wbMessageDigestOid, + (word32)sizeof(wbMessageDigestOid), wbSigningTimeOid, + (word32)sizeof(wbSigningTimeOid), signingTime, + (word32)sizeof(signingTime)); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), + ":2551 2nd operand true (cap too small)"); + + /* :2551 1st operand true -- no working array at all. */ + XMEMSET(&wbEsd, 0, sizeof(wbEsd)); + wbEsd.hashType = WC_HASH_TYPE_SHA256; + wbEsd.signedAttribs = NULL; + wbEsd.signedAttribsCap = 8; + ret = wc_PKCS7_BuildSignedAttributes(&p, &wbEsd, wbDataOid, + (word32)sizeof(wbDataOid), wbContentTypeOid, + (word32)sizeof(wbContentTypeOid), wbMessageDigestOid, + (word32)sizeof(wbMessageDigestOid), wbSigningTimeOid, + (word32)sizeof(wbSigningTimeOid), signingTime, + (word32)sizeof(signingTime)); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), + ":2551 1st operand true (signedAttribs==NULL)"); + + /* Custom-attribute block [:2568,:2574]. WOLFSSL_NO_ATTRIBUTES skips the + * canned block so the custom block is reached with atrIdx == 0. */ + custom[0].oid = customOid; + custom[0].oidSz = (word32)sizeof(customOid); + custom[0].value = customVal; + custom[0].valueSz = (word32)sizeof(customVal); + + /* :2568 2nd operand false: size > 0 but no array. */ + XMEMSET(&wbEsd, 0, sizeof(wbEsd)); + XMEMSET(attribs, 0, sizeof(attribs)); + wbEsd.hashType = WC_HASH_TYPE_SHA256; + wbEsd.signedAttribs = attribs; + wbEsd.signedAttribsCap = 8; + p.defaultSignedAttribs = WOLFSSL_NO_ATTRIBUTES; + p.signedAttribs = NULL; + p.signedAttribsSz = 1; + ret = wc_PKCS7_BuildSignedAttributes(&p, &wbEsd, wbDataOid, + (word32)sizeof(wbDataOid), wbContentTypeOid, + (word32)sizeof(wbContentTypeOid), wbMessageDigestOid, + (word32)sizeof(wbMessageDigestOid), wbSigningTimeOid, + (word32)sizeof(wbSigningTimeOid), signingTime, + (word32)sizeof(signingTime)); + WB_CHECK(ret == 0, ":2568 2nd operand false (signedAttribs==NULL)"); + + /* :2574 1st operand true: esd working array missing. */ + XMEMSET(&wbEsd, 0, sizeof(wbEsd)); + wbEsd.hashType = WC_HASH_TYPE_SHA256; + wbEsd.signedAttribs = NULL; + wbEsd.signedAttribsCap = 8; + p.signedAttribs = custom; + p.signedAttribsSz = 1; + ret = wc_PKCS7_BuildSignedAttributes(&p, &wbEsd, wbDataOid, + (word32)sizeof(wbDataOid), wbContentTypeOid, + (word32)sizeof(wbContentTypeOid), wbMessageDigestOid, + (word32)sizeof(wbMessageDigestOid), wbSigningTimeOid, + (word32)sizeof(wbSigningTimeOid), signingTime, + (word32)sizeof(signingTime)); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), + ":2574 1st operand true (signedAttribs==NULL)"); + + /* :2574 2nd operand true: not enough room for the custom attributes. */ + XMEMSET(&wbEsd, 0, sizeof(wbEsd)); + XMEMSET(attribs, 0, sizeof(attribs)); + wbEsd.hashType = WC_HASH_TYPE_SHA256; + wbEsd.signedAttribs = attribs; + wbEsd.signedAttribsCap = 0; + ret = wc_PKCS7_BuildSignedAttributes(&p, &wbEsd, wbDataOid, + (word32)sizeof(wbDataOid), wbContentTypeOid, + (word32)sizeof(wbContentTypeOid), wbMessageDigestOid, + (word32)sizeof(wbMessageDigestOid), wbSigningTimeOid, + (word32)sizeof(wbSigningTimeOid), signingTime, + (word32)sizeof(signingTime)); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), + ":2574 2nd operand true (no space left)"); + + /* :2568/:2574 all false: real custom attribute encode. */ + XMEMSET(&wbEsd, 0, sizeof(wbEsd)); + XMEMSET(attribs, 0, sizeof(attribs)); + wbEsd.hashType = WC_HASH_TYPE_SHA256; + wbEsd.signedAttribs = attribs; + wbEsd.signedAttribsCap = 8; + ret = wc_PKCS7_BuildSignedAttributes(&p, &wbEsd, wbDataOid, + (word32)sizeof(wbDataOid), wbContentTypeOid, + (word32)sizeof(wbContentTypeOid), wbMessageDigestOid, + (word32)sizeof(wbMessageDigestOid), wbSigningTimeOid, + (word32)sizeof(wbSigningTimeOid), signingTime, + (word32)sizeof(signingTime)); + WB_CHECK(ret == 0, ":2568/:2574 all false (custom attribs encoded)"); + + p.signedAttribs = NULL; + p.signedAttribsSz = 0; + wc_PKCS7_Free(&p); +} + +/* ------------------------------------------------------------------------- * + * Section 3: SignerInfo <-> certificate identity binding + * [:4998 all-false + SKID branch, :5155, :5288, :5471] + * ------------------------------------------------------------------------- */ +#if !defined(NO_RSA) || defined(HAVE_ECC) +static void wb_signerinfo_binding(void) +{ + wc_PKCS7 p; + PKCS7SignerInfo si; + byte bogusSkid[KEYID_SIZE]; + byte realSkid[KEYID_SIZE]; + byte sig[64]; + byte hash[32]; + int haveSkid = 0; + + XMEMSET(&si, 0, sizeof(si)); + XMEMSET(bogusSkid, 0xA5, sizeof(bogusSkid)); + XMEMSET(realSkid, 0, sizeof(realSkid)); + XMEMSET(sig, 0x11, sizeof(sig)); + XMEMSET(hash, 0x22, sizeof(hash)); + + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_signerinfo_binding skipped"); + wb_fail = 1; + return; + } + +#ifndef NO_RSA + /* Recover the RSA client cert's real SKID so CertMatchesSignerInfo can + * take its "match" return as well as its "no match" return. */ + { + DecodedCert dc; + InitDecodedCert(&dc, client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, NULL); + if (ParseCert(&dc, CA_TYPE, NO_VERIFY, 0) == 0) { + XMEMCPY(realSkid, dc.extSubjKeyId, KEYID_SIZE); + haveSkid = 1; + + WB_NOTE("wc_PKCS7_CertMatchesSignerInfo() all-false guard " + "+ CMS_SKID compare [:4998]"); + /* 1st operand true: no SignerInfo */ + p.signerInfo = NULL; + WB_CHECK(wc_PKCS7_CertMatchesSignerInfo(&p, &dc) == 0, + ":4998 1st operand true"); + /* 2nd operand true: SignerInfo with no sid blob */ + si.sidType = CMS_SKID; + si.sid = NULL; + si.sidSz = (word32)sizeof(bogusSkid); + p.signerInfo = &si; + WB_CHECK(wc_PKCS7_CertMatchesSignerInfo(&p, &dc) == 0, + ":4998 2nd operand true"); + si.sidType = CMS_SKID; + si.sid = bogusSkid; + si.sidSz = (word32)sizeof(bogusSkid); + p.signerInfo = &si; + WB_CHECK(wc_PKCS7_CertMatchesSignerInfo(&p, &dc) == 0, + ":4998 all false, SKID mismatch"); + si.sid = realSkid; + WB_CHECK(wc_PKCS7_CertMatchesSignerInfo(&p, &dc) == 1, + ":4998 all false, SKID match"); + /* IssuerAndSerialNumber branch with a blob that is not a Name */ + si.sidType = CMS_ISSUER_AND_SERIAL_NUMBER; + si.sid = bogusSkid; + si.sidSz = (word32)sizeof(bogusSkid); + (void)wc_PKCS7_CertMatchesSignerInfo(&p, &dc); + + /* IssuerAndSerialNumber blob rebuilt from this very certificate: + * SEQUENCE(issuer Name) followed by the serial INTEGER. Drives + * the serial compare [:5039] both ways. */ + { + static byte isn[1024]; + word32 isnSz = 0; + + if (dc.issuerRaw != NULL && dc.issuerRawLen > 0 && + dc.serialSz > 0 && + (word32)dc.issuerRawLen + (word32)dc.serialSz + 16 < + (word32)sizeof(isn)) { + isnSz = SetSequence((word32)dc.issuerRawLen, isn); + XMEMCPY(isn + isnSz, dc.issuerRaw, (word32)dc.issuerRawLen); + isnSz += (word32)dc.issuerRawLen; + isn[isnSz++] = ASN_INTEGER; + isn[isnSz++] = (byte)dc.serialSz; + XMEMCPY(isn + isnSz, dc.serial, (word32)dc.serialSz); + isnSz += (word32)dc.serialSz; + + si.sid = isn; + si.sidSz = isnSz; + WB_CHECK(wc_PKCS7_CertMatchesSignerInfo(&p, &dc) == 1, + ":5039 both true (issuer+serial match)"); + + /* same issuer, different serial -> compare mismatches */ + isn[isnSz - 1] ^= 0xFF; + WB_CHECK(wc_PKCS7_CertMatchesSignerInfo(&p, &dc) == 0, + ":5039 serial mismatch"); + isn[isnSz - 1] ^= 0xFF; + + /* corrupt the serial INTEGER header -> GetInt fails */ + isn[isnSz - (word32)dc.serialSz - 2] = 0x7F; + WB_CHECK(wc_PKCS7_CertMatchesSignerInfo(&p, &dc) == 0, + ":5039 1st operand false (GetInt fails)"); + } + } + + si.sidType = CMS_ISSUER_AND_SERIAL_NUMBER; + si.sid = bogusSkid; + si.sidSz = (word32)sizeof(bogusSkid); + /* sidSz == 0 (3rd operand true) */ + si.sidSz = 0; + WB_CHECK(wc_PKCS7_CertMatchesSignerInfo(&p, &dc) == 0, + ":4998 3rd operand true"); + } + FreeDecodedCert(&dc); + } + + WB_NOTE("wc_PKCS7_RsaVerify() SignerInfo sid binding [:5155]"); + p.cert[0] = (byte*)client_cert_der_2048; + p.certSz[0] = (word32)sizeof_client_cert_der_2048; + /* 1st operand false: no SignerInfo at all */ + p.signerInfo = NULL; + (void)wc_PKCS7_RsaVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + /* 2nd operand false: SignerInfo present but no sid */ + si.sidType = CMS_SKID; + si.sid = NULL; + si.sidSz = 0; + p.signerInfo = &si; + (void)wc_PKCS7_RsaVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + /* all true: sid present but binds to a different certificate */ + si.sid = bogusSkid; + si.sidSz = (word32)sizeof(bogusSkid); + (void)wc_PKCS7_RsaVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + /* 3rd operand false: sid matches this certificate */ + if (haveSkid) { + si.sid = realSkid; + (void)wc_PKCS7_RsaVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + } +#ifdef HAVE_ECC + /* :5166 both true: the embedded certificate is not RSA-family */ + p.signerInfo = NULL; + p.cert[0] = (byte*)cliecc_cert_der_256; + p.certSz[0] = (word32)sizeof_cliecc_cert_der_256; + (void)wc_PKCS7_RsaVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); +#ifdef WC_RSA_PSS + p.hashOID = SHA256h; + (void)wc_PKCS7_RsaPssVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); +#endif + p.cert[0] = (byte*)client_cert_der_2048; + p.certSz[0] = (word32)sizeof_client_cert_der_2048; +#endif + +#ifdef WC_RSA_PSS + WB_NOTE("wc_PKCS7_RsaPssVerify() SignerInfo sid binding [:5288]"); + p.hashOID = SHA256h; + p.signerInfo = NULL; + (void)wc_PKCS7_RsaPssVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + si.sid = NULL; + si.sidSz = 0; + p.signerInfo = &si; + (void)wc_PKCS7_RsaPssVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + si.sid = bogusSkid; + si.sidSz = (word32)sizeof(bogusSkid); + (void)wc_PKCS7_RsaPssVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + if (haveSkid) { + si.sid = realSkid; + (void)wc_PKCS7_RsaPssVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + } +#endif /* WC_RSA_PSS */ +#endif /* !NO_RSA */ + +#ifdef HAVE_ECC + WB_NOTE("wc_PKCS7_EcdsaVerify() SignerInfo sid binding [:5471]"); + { + DecodedCert dc; + int haveEccSkid = 0; + byte eccSkid[KEYID_SIZE]; + + XMEMSET(eccSkid, 0, sizeof(eccSkid)); + InitDecodedCert(&dc, cliecc_cert_der_256, + (word32)sizeof_cliecc_cert_der_256, NULL); + if (ParseCert(&dc, CA_TYPE, NO_VERIFY, 0) == 0) { + XMEMCPY(eccSkid, dc.extSubjKeyId, KEYID_SIZE); + haveEccSkid = 1; + } + FreeDecodedCert(&dc); + + p.cert[0] = (byte*)cliecc_cert_der_256; + p.certSz[0] = (word32)sizeof_cliecc_cert_der_256; + p.signerInfo = NULL; + (void)wc_PKCS7_EcdsaVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + si.sidType = CMS_SKID; + si.sid = NULL; + si.sidSz = 0; + p.signerInfo = &si; + (void)wc_PKCS7_EcdsaVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + si.sid = bogusSkid; + si.sidSz = (word32)sizeof(bogusSkid); + (void)wc_PKCS7_EcdsaVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + if (haveEccSkid) { + si.sid = eccSkid; + (void)wc_PKCS7_EcdsaVerify(&p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)); + } + } +#endif /* HAVE_ECC */ + + p.signerInfo = NULL; + p.cert[0] = NULL; + p.certSz[0] = 0; + wc_PKCS7_Free(&p); +} +#else +static void wb_signerinfo_binding(void) +{ + WB_NOTE("no RSA/ECC; SignerInfo binding skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 4: wc_PKCS7_VerifyContentMessageDigest() attribute/content shapes + * [:5857, :5890, :5893] + * ------------------------------------------------------------------------- */ +static void wb_verify_content_msgdigest(void) +{ + wc_PKCS7 p; + PKCS7DecodedAttrib attrib; + byte mdOidDer[11]; + byte mdValue[34]; + byte content[8]; + byte pkcs7Content[8]; + byte hash[32]; + + XMEMSET(&attrib, 0, sizeof(attrib)); + XMEMCPY(mdOidDer, wbMessageDigestOid, sizeof(wbMessageDigestOid)); + XMEMSET(content, 0x31, sizeof(content)); + XMEMSET(hash, 0, sizeof(hash)); + + /* OCTET STRING wrapping a 32-byte digest */ + mdValue[0] = ASN_OCTET_STRING; + mdValue[1] = 32; + XMEMSET(mdValue + 2, 0x77, 32); + + /* PKCS#7-typed content: a DER OCTET STRING of 6 bytes */ + pkcs7Content[0] = ASN_OCTET_STRING; + pkcs7Content[1] = 6; + XMEMSET(pkcs7Content + 2, 0x32, 6); + + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_verify_content_msgdigest skipped"); + wb_fail = 1; + return; + } + p.hashOID = SHA256h; + + attrib.next = NULL; + attrib.oid = mdOidDer; + attrib.oidSz = (word32)sizeof(mdOidDer); + attrib.value = mdValue; + attrib.valueSz = (word32)sizeof(mdValue); + p.decodedAttrib = &attrib; + + WB_NOTE("VerifyContentMessageDigest attrib->value guards [:5857]"); + /* 1st operand true */ + attrib.value = NULL; + (void)wc_PKCS7_VerifyContentMessageDigest(&p, NULL, 0); + /* 2nd operand true */ + attrib.value = mdValue; + attrib.valueSz = 0; + (void)wc_PKCS7_VerifyContentMessageDigest(&p, NULL, 0); + attrib.valueSz = (word32)sizeof(mdValue); + + WB_NOTE("VerifyContentMessageDigest content shape [:5890,:5893]"); + /* all false at :5857; :5890 1st operand false (content == NULL) */ + p.content = NULL; + p.contentSz = 0; + p.contentIsPkcs7Type = 1; + (void)wc_PKCS7_VerifyContentMessageDigest(&p, NULL, 0); + /* :5890 2nd operand false (content set, not PKCS#7-typed) */ + p.content = content; + p.contentSz = (word32)sizeof(content); + p.contentIsPkcs7Type = 0; + (void)wc_PKCS7_VerifyContentMessageDigest(&p, NULL, 0); + /* :5890 both true, :5893 contentLen > 1 */ + p.content = pkcs7Content; + p.contentSz = (word32)sizeof(pkcs7Content); + p.contentIsPkcs7Type = 1; + (void)wc_PKCS7_VerifyContentMessageDigest(&p, NULL, 0); + /* :5893 contentLen <= 1 */ + p.contentSz = 1; + (void)wc_PKCS7_VerifyContentMessageDigest(&p, NULL, 0); + /* caller-supplied hash path */ + p.content = content; + p.contentSz = (word32)sizeof(content); + p.contentIsPkcs7Type = 0; + (void)wc_PKCS7_VerifyContentMessageDigest(&p, hash, (word32)sizeof(hash)); + + p.decodedAttrib = NULL; + p.content = NULL; + p.contentSz = 0; + wc_PKCS7_Free(&p); +} + +/* ------------------------------------------------------------------------- * + * Section 5: PKCS7_EncodeSigned() shape matrix + * [:3538, :3555, :3649, :4022, :4073] + * ------------------------------------------------------------------------- */ +static byte wbCbContent[16]; + +#ifdef ASN_BER_TO_DER +static int wb_stream_out_cb(wc_PKCS7* pkcs7, const byte* output, + word32 outputSz, void* ctx) +{ + (void)pkcs7; + (void)output; + (void)ctx; + return (int)outputSz; +} + +static int wb_get_content_cb(wc_PKCS7* pkcs7, byte** content, void* ctx) +{ + (void)pkcs7; + (void)ctx; + if (content != NULL) + *content = wbCbContent; + return (int)sizeof(wbCbContent); +} +#endif /* ASN_BER_TO_DER */ + +#ifndef NO_RSA +static void wb_encodesigned_shapes(void) +{ + wc_PKCS7 p; + WC_RNG rng; + static byte out[8192]; + byte content[32]; + byte hash[32]; + word32 outSz; + int ret; + + XMEMSET(content, 0x63, sizeof(content)); + XMEMSET(hash, 0x64, sizeof(hash)); + XMEMSET(wbCbContent, 0x65, sizeof(wbCbContent)); + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; wb_encodesigned_shapes skipped"); + wb_fail = 1; + return; + } + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_encodesigned_shapes skipped"); + wb_fail = 1; + wc_FreeRng(&rng); + return; + } + if (wc_PKCS7_InitWithCert(&p, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) != 0) { + WB_NOTE("wc_PKCS7_InitWithCert failed; wb_encodesigned_shapes skipped"); + wb_fail = 1; + wc_PKCS7_Free(&p); + wc_FreeRng(&rng); + return; + } + p.privateKey = (byte*)client_key_der_2048; + p.privateKeySz = (word32)sizeof_client_key_der_2048; + p.content = content; + p.contentSz = (word32)sizeof(content); + p.hashOID = SHA256h; + p.rng = &rng; + + WB_NOTE("PKCS7_EncodeSigned() top guard 1st operand [:3538]"); + outSz = (word32)sizeof(out); + (void)PKCS7_EncodeSigned(NULL, NULL, 0, out, &outSz, NULL, NULL); + + /* baseline: all-false everywhere, a real SignedData encode. */ + outSz = (word32)sizeof(out); + ret = PKCS7_EncodeSigned(&p, NULL, 0, out, &outSz, NULL, NULL); + WB_CHECK(ret > 0, "baseline SignedData encode (:3538/:3555/:3649 false)"); + + WB_NOTE("PKCS7_EncodeSigned() pre-calculated hash matrix [:3649]"); + /* hashBuf != NULL with a size that does not match hashOID -> BUFFER_E */ + outSz = (word32)sizeof(out); + ret = PKCS7_EncodeSigned(&p, hash, 16, out, &outSz, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), + ":3649 both true (hashSz mismatch)"); + /* hashBuf != NULL with the right size -> guard false, encode continues */ + outSz = (word32)sizeof(out); + ret = PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + NULL, NULL); + WB_CHECK(ret > 0, ":3649 2nd operand false (hashSz matches)"); + + WB_NOTE("PKCS7_EncodeSigned() pre-hash-required matrix [:3555]"); +#if defined(HAVE_ECC) || defined(WC_RSA_PSS) + { + word32 savedOid = p.publicKeyOID; + int savedSid = p.sidType; + + /* 1st operand false: degenerate (certs-only) bundle */ + p.sidType = DEGENERATE_SID; + outSz = (word32)sizeof(out); + (void)PKCS7_EncodeSigned(&p, NULL, 0, out, &outSz, NULL, NULL); + p.sidType = savedSid; + +#ifdef HAVE_ECC + /* all true: ECDSA signer with no pre-calculated hash -> BAD_FUNC_ARG */ + p.publicKeyOID = ECDSAk; + outSz = (word32)sizeof(out); + ret = PKCS7_EncodeSigned(&p, NULL, 0, out, &outSz, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":3555 all true (ECDSA needs pre-calculated hash)"); + /* 2nd operand false: hash supplied */ + outSz = (word32)sizeof(out); + (void)PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + NULL, NULL); +#endif +#ifdef WC_RSA_PSS + /* 3rd operand false / 4th true: RSA-PSS also needs the hash */ + p.publicKeyOID = RSAPSSk; + outSz = (word32)sizeof(out); + ret = PKCS7_EncodeSigned(&p, NULL, 0, out, &outSz, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":3555 4th operand true (RSA-PSS needs pre-calculated hash)"); +#endif + /* 3rd and 4th operands false: plain RSA */ + p.publicKeyOID = RSAk; + outSz = (word32)sizeof(out); + (void)PKCS7_EncodeSigned(&p, NULL, 0, out, &outSz, NULL, NULL); + p.publicKeyOID = savedOid; + } +#endif /* HAVE_ECC || WC_RSA_PSS */ + + WB_NOTE("PKCS7_EncodeSigned() output/streamOutCb matrix [:4022]"); + /* both true: no output buffer and no stream callback */ + outSz = (word32)sizeof(out); + ret = PKCS7_EncodeSigned(&p, NULL, 0, NULL, &outSz, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), + ":4022 both true (output==NULL, no streamOutCb)"); +#ifdef ASN_BER_TO_DER + /* 2nd operand false: stream-out callback consumes the encoding */ + p.streamOutCb = wb_stream_out_cb; + outSz = (word32)sizeof(out); + (void)PKCS7_EncodeSigned(&p, NULL, 0, NULL, &outSz, NULL, NULL); + p.streamOutCb = NULL; +#endif + + WB_NOTE("PKCS7_EncodeSigned() header/footer + size-query matrix " + "[:3981,:3983,:4002,:4066]"); + { + static byte foot[4096]; + word32 footSz; + + /* :3981/:4066 2nd operand false: footer buffer with no size pointer */ + outSz = (word32)sizeof(out); + (void)PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + foot, NULL); + + /* :3983 both true: pure size query through the head/foot entry */ + outSz = 0; + footSz = 0; + (void)PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + foot, &footSz); + + /* :3983 1st operand false: head size known, footer too small */ + outSz = 16; + footSz = 0; + (void)PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + foot, &footSz); + + /* :3983 2nd operand false: footer size nonzero but too small */ + outSz = 0; + footSz = 4; + (void)PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + foot, &footSz); + + /* :3981/:4066 all true: real head/foot encode */ + outSz = (word32)sizeof(out); + footSz = (word32)sizeof(foot); + (void)PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + foot, &footSz); + } + + /* :4002 1st operand true: single output buffer too small */ + outSz = 16; + ret = PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), ":4002 output buffer too small"); + /* :4002 size query (outputSz == 0) */ + outSz = 0; + (void)PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + NULL, NULL); +#ifdef ASN_BER_TO_DER + /* :4002 2nd operand false: streamOutCb makes the size check moot */ + p.streamOutCb = wb_stream_out_cb; + outSz = 16; + (void)PKCS7_EncodeSigned(&p, hash, (word32)sizeof(hash), out, &outSz, + NULL, NULL); + p.streamOutCb = NULL; +#endif + + WB_NOTE("PKCS7_EncodeSigned() content-presence matrix [:4073]"); + /* 3rd operand false: content pointer set but zero length */ + p.contentSz = 0; + outSz = (word32)sizeof(out); + (void)PKCS7_EncodeSigned(&p, NULL, 0, out, &outSz, NULL, NULL); + p.contentSz = (word32)sizeof(content); + + WB_NOTE("wc_PKCS7_EncodeSignedData()/_ex() arg chains [:4337,:4469]"); + /* :4469 all operands true: nonzero contentSz with no content source */ + p.content = NULL; + p.contentSz = (word32)sizeof(content); + ret = wc_PKCS7_EncodeSignedData(&p, out, (word32)sizeof(out)); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":4469 contentSz>0 with no content and no callback"); + /* :4469 2nd operand false: contentSz == 0 */ + p.contentSz = 0; + (void)wc_PKCS7_EncodeSignedData(&p, out, (word32)sizeof(out)); +#ifdef ASN_BER_TO_DER + /* :4469 4th operand false: no content pointer but a getContentCb */ + p.contentSz = (word32)sizeof(wbCbContent); + p.getContentCb = wb_get_content_cb; + (void)wc_PKCS7_EncodeSignedData(&p, out, (word32)sizeof(out)); + /* :4337 1st operand false: getContentCb set, footer args may be NULL */ + outSz = (word32)sizeof(out); + (void)wc_PKCS7_EncodeSignedData_ex(&p, NULL, 0, out, &outSz, NULL, NULL); + p.getContentCb = NULL; +#endif + p.content = content; + p.contentSz = (word32)sizeof(content); + /* :4337 all true: no getContentCb and no footer arguments */ + outSz = (word32)sizeof(out); + ret = wc_PKCS7_EncodeSignedData_ex(&p, hash, (word32)sizeof(hash), out, + &outSz, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":4337 outputFoot/outputFootSz both NULL"); + /* :4337 all false */ + { + static byte foot[2048]; + word32 footSz = (word32)sizeof(foot); + outSz = (word32)sizeof(out); + (void)wc_PKCS7_EncodeSignedData_ex(&p, hash, (word32)sizeof(hash), + out, &outSz, foot, &footSz); + } + + p.privateKey = NULL; + p.privateKeySz = 0; + p.content = NULL; + p.contentSz = 0; + p.rng = NULL; + wc_PKCS7_Free(&p); + wc_FreeRng(&rng); +} +#else +static void wb_encodesigned_shapes(void) +{ + WB_NOTE("NO_RSA; PKCS7_EncodeSigned shape matrix skipped"); +} +#endif /* !NO_RSA */ + +/* ------------------------------------------------------------------------- * + * Section 6: public decode/encode entry argument chains + * [:4399, :10163, :11380, :11424, :15001, :15168, :15171, :15710, :16797, + * :17785] + * ------------------------------------------------------------------------- */ +static void wb_public_entry_args(void) +{ + wc_PKCS7 p; + byte buf[64]; + byte out[64]; + word32 outSz; + word32 idx; + const byte* keyPtr = NULL; + word32 keyPtrSz = 0; + byte content[32]; + + XMEMSET(buf, 0, sizeof(buf)); + XMEMSET(out, 0, sizeof(out)); + XMEMSET(content, 0x71, sizeof(content)); + /* SEQUENCE { OCTET STRING "AAAA" } -- a shape OneSymmetricKey accepts far + * enough in to move past the argument guard. */ + buf[0] = 0x30; buf[1] = 0x06; + buf[2] = 0x04; buf[3] = 0x04; buf[4] = 'A'; buf[5] = 'A'; + buf[6] = 'A'; buf[7] = 'A'; + + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_public_entry_args skipped"); + wb_fail = 1; + return; + } + + WB_NOTE("wc_PKCS7_SetDetached() flag matrix [:4399]"); + (void)wc_PKCS7_SetDetached(NULL, 1); + (void)wc_PKCS7_SetDetached(&p, 0); /* 2nd operand false */ + (void)wc_PKCS7_SetDetached(&p, 2); /* both true */ + (void)wc_PKCS7_SetDetached(&p, 1); /* 3rd operand false */ + + WB_NOTE("wc_PKCS7_DecodeOneSymmetricKeyKey() arg chain [:17785]"); + (void)wc_PKCS7_DecodeOneSymmetricKeyKey(NULL, (word32)sizeof(buf), + &keyPtr, &keyPtrSz); + (void)wc_PKCS7_DecodeOneSymmetricKeyKey(buf, (word32)sizeof(buf), + NULL, &keyPtrSz); + (void)wc_PKCS7_DecodeOneSymmetricKeyKey(buf, (word32)sizeof(buf), + &keyPtr, NULL); + (void)wc_PKCS7_DecodeOneSymmetricKeyKey(buf, 8, &keyPtr, &keyPtrSz); + + WB_NOTE("wc_PKCS7_GetEnvelopedDataKariRid() arg chain [:15001]"); + outSz = (word32)sizeof(out); + (void)wc_PKCS7_GetEnvelopedDataKariRid(NULL, (word32)sizeof(buf), + out, &outSz); + outSz = (word32)sizeof(out); + (void)wc_PKCS7_GetEnvelopedDataKariRid(buf, 0, out, &outSz); + outSz = (word32)sizeof(out); + (void)wc_PKCS7_GetEnvelopedDataKariRid(buf, (word32)sizeof(buf), + NULL, &outSz); + (void)wc_PKCS7_GetEnvelopedDataKariRid(buf, (word32)sizeof(buf), + out, NULL); + outSz = (word32)sizeof(out); + (void)wc_PKCS7_GetEnvelopedDataKariRid(buf, (word32)sizeof(buf), + out, &outSz); + + WB_NOTE("wc_PKCS7_DecodeUnprotectedAttributes() arg chain [:16797]"); + idx = 0; + (void)wc_PKCS7_DecodeUnprotectedAttributes(NULL, buf, + (word32)sizeof(buf), &idx); + idx = 0; + (void)wc_PKCS7_DecodeUnprotectedAttributes(&p, NULL, + (word32)sizeof(buf), &idx); + idx = 0; + (void)wc_PKCS7_DecodeUnprotectedAttributes(&p, buf, 0, &idx); + (void)wc_PKCS7_DecodeUnprotectedAttributes(&p, buf, + (word32)sizeof(buf), NULL); + idx = 0; + (void)wc_PKCS7_DecodeUnprotectedAttributes(&p, buf, + (word32)sizeof(buf), &idx); + +#if defined(HAVE_AESGCM) || defined(HAVE_AESCCM) + WB_NOTE("wc_PKCS7_EncodeAuthEnvelopedData() arg chains [:15168,:15171]"); + (void)wc_PKCS7_EncodeAuthEnvelopedData(NULL, out, (word32)sizeof(out)); + p.content = NULL; + p.contentSz = (word32)sizeof(content); + (void)wc_PKCS7_EncodeAuthEnvelopedData(&p, out, (word32)sizeof(out)); + p.content = content; + p.contentSz = 0; + (void)wc_PKCS7_EncodeAuthEnvelopedData(&p, out, (word32)sizeof(out)); + p.contentSz = (word32)sizeof(content); + (void)wc_PKCS7_EncodeAuthEnvelopedData(&p, NULL, (word32)sizeof(out)); + (void)wc_PKCS7_EncodeAuthEnvelopedData(&p, out, 0); + /* all false: falls through to the encryptOID switch */ + p.encryptOID = 0; + (void)wc_PKCS7_EncodeAuthEnvelopedData(&p, out, (word32)sizeof(out)); + + WB_NOTE("wc_PKCS7_DecodeAuthEnvelopedData() arg chain [:15710]"); + (void)wc_PKCS7_DecodeAuthEnvelopedData(&p, NULL, (word32)sizeof(buf), + out, (word32)sizeof(out)); + (void)wc_PKCS7_DecodeAuthEnvelopedData(&p, buf, 0, + out, (word32)sizeof(out)); + (void)wc_PKCS7_DecodeAuthEnvelopedData(&p, buf, (word32)sizeof(buf), + NULL, (word32)sizeof(out)); + (void)wc_PKCS7_DecodeAuthEnvelopedData(&p, buf, (word32)sizeof(buf), + out, 0); + (void)wc_PKCS7_DecodeAuthEnvelopedData(&p, buf, (word32)sizeof(buf), + out, (word32)sizeof(out)); + wc_PKCS7_Free(&p); + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("re-init failed; remaining public arg vectors skipped"); + wb_fail = 1; + return; + } +#endif /* HAVE_AESGCM || HAVE_AESCCM */ + + WB_NOTE("wc_PKCS7_EncodeEnvelopedData() arg chains [:11380,:11424]"); + p.content = content; + p.contentSz = 0; + (void)wc_PKCS7_EncodeEnvelopedData(&p, out, (word32)sizeof(out)); + p.contentSz = (word32)sizeof(content); + p.encryptOID = 0; /* invalid: rejected right after the guard */ + (void)wc_PKCS7_EncodeEnvelopedData(&p, out, (word32)sizeof(out)); +#ifndef NO_AES + { + static byte envOut[4096]; + p.encryptOID = AES256CBCb; + p.singleCert = (byte*)client_cert_der_2048; + p.singleCertSz = 0; /* :11424 2nd operand false */ + (void)wc_PKCS7_EncodeEnvelopedData(&p, envOut, (word32)sizeof(envOut)); + p.singleCert = NULL; /* :11424 1st operand false */ + p.singleCertSz = 0; + (void)wc_PKCS7_EncodeEnvelopedData(&p, envOut, (word32)sizeof(envOut)); + } +#ifndef NO_RSA + /* :11424 both true: a real KTRI EnvelopedData for the RSA client cert. + * Built on a heap wc_PKCS7 (wc_PKCS7_New) rather than the shared stack + * fixture: the recipient list this leaves behind must be released by the + * matching wc_PKCS7_Free() and must not outlive into the next vector. */ + { + static byte envOut2[4096]; + static byte envIn[32]; + wc_PKCS7* e = wc_PKCS7_New(NULL, INVALID_DEVID); + + XMEMSET(envIn, 0x3c, sizeof(envIn)); + if (e != NULL) { + if (wc_PKCS7_InitWithCert(e, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) == 0) { + e->content = envIn; + e->contentSz = (word32)sizeof(envIn); + e->contentOID = DATA; + e->encryptOID = AES256CBCb; + (void)wc_PKCS7_EncodeEnvelopedData(e, envOut2, + (word32)sizeof(envOut2)); + e->content = NULL; + e->contentSz = 0; + } + wc_PKCS7_Free(e); + } + } +#endif +#endif + p.singleCert = NULL; + p.singleCertSz = 0; + p.content = NULL; + p.contentSz = 0; + wc_PKCS7_Free(&p); +} + +/* ------------------------------------------------------------------------- * + * Section 7: content encrypt/decrypt argument chains + * [:8359, :9799, :10163] + * ------------------------------------------------------------------------- */ +#ifndef NO_AES +static void wb_content_crypt_args(void) +{ + wc_PKCS7 p; + byte key[32]; + byte iv[16]; + byte in[32]; + byte out[64]; + byte cek[32]; + int ret; + + XMEMSET(key, 0x81, sizeof(key)); + XMEMSET(iv, 0x82, sizeof(iv)); + XMEMSET(in, 0x83, sizeof(in)); + XMEMSET(out, 0, sizeof(out)); + XMEMSET(cek, 0x84, sizeof(cek)); + + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_content_crypt_args skipped"); + wb_fail = 1; + return; + } + + WB_NOTE("PKCS7_GenerateContentEncryptionKey() cached-cek matrix [:8359]"); + /* 1st operand false: no cached key -> generates one */ + ret = PKCS7_GenerateContentEncryptionKey(&p, 32); + WB_CHECK(ret == 0, ":8359 1st operand false (generate)"); + /* both true, same size -> reuse */ + ret = PKCS7_GenerateContentEncryptionKey(&p, 32); + WB_CHECK(ret == 0, ":8359 both true (reuse)"); + /* both true, different size -> WC_KEY_SIZE_E */ + ret = PKCS7_GenerateContentEncryptionKey(&p, 16); + WB_CHECK(ret == WC_NO_ERR_TRACE(WC_KEY_SIZE_E), + ":8359 both true, size mismatch"); + /* 2nd operand false: cek pointer set but zero length */ + { + byte* savedCek = p.cek; + word32 savedSz = p.cekSz; + p.cekSz = 0; + ret = PKCS7_GenerateContentEncryptionKey(&p, 32); + WB_CHECK(ret == 0, ":8359 2nd operand false (cekSz==0)"); + /* the call above replaced p.cek; release the old buffer */ + if (p.cek != savedCek) + XFREE(savedCek, p.heap, DYNAMIC_TYPE_PKCS7); + (void)savedSz; + } + + WB_NOTE("wc_PKCS7_EncryptContent() in/out callback matrix [:9799]"); +#ifdef ASN_BER_TO_DER + /* 1st/2nd operand true: no input and no getContentCb */ + (void)wc_PKCS7_EncryptContent(&p, AES256CBCb, key, (int)sizeof(key), + iv, (int)sizeof(iv), NULL, 0, NULL, 0, NULL, (int)sizeof(in), out); + /* 2nd operand false: getContentCb supplies the input */ + p.getContentCb = wb_get_content_cb; + (void)wc_PKCS7_EncryptContent(&p, AES256CBCb, key, (int)sizeof(key), + iv, (int)sizeof(iv), NULL, 0, NULL, 0, NULL, (int)sizeof(in), out); + p.getContentCb = NULL; + /* 3rd/4th operand true: no output and no streamOutCb */ + (void)wc_PKCS7_EncryptContent(&p, AES256CBCb, key, (int)sizeof(key), + iv, (int)sizeof(iv), NULL, 0, NULL, 0, in, (int)sizeof(in), NULL); + /* 4th operand false: streamOutCb consumes the output */ + p.streamOutCb = wb_stream_out_cb; + (void)wc_PKCS7_EncryptContent(&p, AES256CBCb, key, (int)sizeof(key), + iv, (int)sizeof(iv), NULL, 0, NULL, 0, in, (int)sizeof(in), NULL); + p.streamOutCb = NULL; +#endif + /* all false: real encrypt */ + ret = wc_PKCS7_EncryptContent(&p, AES256CBCb, key, (int)sizeof(key), + iv, (int)sizeof(iv), NULL, 0, NULL, 0, in, (int)sizeof(in), out); + WB_CHECK(ret == 0, ":9799 all false (real AES-CBC encrypt)"); + + WB_NOTE("wc_PKCS7_EncryptContent() AES-CBC key/iv size matrix [:9823]"); +#ifdef WOLFSSL_AES_128 + (void)wc_PKCS7_EncryptContent(&p, AES128CBCb, key, 16, + iv, (int)sizeof(iv), NULL, 0, NULL, 0, in, (int)sizeof(in), out); + (void)wc_PKCS7_EncryptContent(&p, AES128CBCb, key, 24, + iv, (int)sizeof(iv), NULL, 0, NULL, 0, in, (int)sizeof(in), out); +#endif +#ifdef WOLFSSL_AES_192 + (void)wc_PKCS7_EncryptContent(&p, AES192CBCb, key, 24, + iv, (int)sizeof(iv), NULL, 0, NULL, 0, in, (int)sizeof(in), out); + (void)wc_PKCS7_EncryptContent(&p, AES192CBCb, key, 16, + iv, (int)sizeof(iv), NULL, 0, NULL, 0, in, (int)sizeof(in), out); +#endif + (void)wc_PKCS7_EncryptContent(&p, AES256CBCb, key, 16, + iv, (int)sizeof(iv), NULL, 0, NULL, 0, in, (int)sizeof(in), out); + (void)wc_PKCS7_EncryptContent(&p, AES256CBCb, key, (int)sizeof(key), + iv, 8, NULL, 0, NULL, 0, in, (int)sizeof(in), out); + +#ifndef NO_DES3 + WB_NOTE("wc_PKCS7_EncryptContent() DES/DES3 key/iv size matrix " + "[:9959,:9974]"); + (void)wc_PKCS7_EncryptContent(&p, DESb, key, DES_KEYLEN, + iv, DES_BLOCK_SIZE, NULL, 0, NULL, 0, in, (int)sizeof(in), out); + (void)wc_PKCS7_EncryptContent(&p, DESb, key, DES_KEYLEN + 1, + iv, DES_BLOCK_SIZE, NULL, 0, NULL, 0, in, (int)sizeof(in), out); + (void)wc_PKCS7_EncryptContent(&p, DESb, key, DES_KEYLEN, + iv, DES_BLOCK_SIZE + 1, NULL, 0, NULL, 0, in, (int)sizeof(in), out); + (void)wc_PKCS7_EncryptContent(&p, DES3b, key, DES3_KEYLEN, + iv, DES_BLOCK_SIZE, NULL, 0, NULL, 0, in, (int)sizeof(in), out); + (void)wc_PKCS7_EncryptContent(&p, DES3b, key, DES3_KEYLEN + 1, + iv, DES_BLOCK_SIZE, NULL, 0, NULL, 0, in, (int)sizeof(in), out); + (void)wc_PKCS7_EncryptContent(&p, DES3b, key, DES3_KEYLEN, + iv, DES_BLOCK_SIZE + 1, NULL, 0, NULL, 0, in, (int)sizeof(in), out); +#endif + + WB_NOTE("wc_PKCS7_DecryptContentEx() input matrix [:10163]"); + XFREE(p.cek, p.heap, DYNAMIC_TYPE_PKCS7); + p.cek = NULL; + p.cekSz = 0; + /* 1st operand false: input present */ + (void)wc_PKCS7_DecryptContentEx(&p, AES256CBCb, iv, (int)sizeof(iv), + NULL, 0, NULL, 0, out, (int)sizeof(in), out); +#ifdef ASN_BER_TO_DER + /* both true: no input, no getContentCb */ + (void)wc_PKCS7_DecryptContentEx(&p, AES256CBCb, iv, (int)sizeof(iv), + NULL, 0, NULL, 0, NULL, (int)sizeof(in), out); + /* 2nd operand false: getContentCb present */ + p.getContentCb = wb_get_content_cb; + (void)wc_PKCS7_DecryptContentEx(&p, AES256CBCb, iv, (int)sizeof(iv), + NULL, 0, NULL, 0, NULL, (int)sizeof(in), out); + p.getContentCb = NULL; +#endif + + wc_PKCS7_Free(&p); +} +#else +static void wb_content_crypt_args(void) +{ + WB_NOTE("NO_AES; content crypt arg matrix skipped"); +} +#endif /* !NO_AES */ + +/* ------------------------------------------------------------------------- * + * Section 8: recipient-info optional-field matrices + * [:9111 (KARI ukm), :11224/:11264 (KEKRI otherAttribute)] + * ------------------------------------------------------------------------- */ +static void wb_recipient_optionals(void) +{ + wc_PKCS7 p; + byte kek[32]; + byte keyId[8]; + byte otherOid[] = { 0x06, 0x03, 0x55, 0x04, 0x03 }; + byte other[] = { 0x04, 0x02, 0x11, 0x22 }; + byte ukm[16]; + + XMEMSET(kek, 0x91, sizeof(kek)); + XMEMSET(keyId, 0x92, sizeof(keyId)); + XMEMSET(ukm, 0x93, sizeof(ukm)); + + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_recipient_optionals skipped"); + wb_fail = 1; + return; + } + /* both KEKRI and KARI size the content-encryption key from encryptOID; + * without it wc_PKCS7_GetOIDKeySize() rejects before any of the + * optional-field decisions below are reached. */ +#ifndef NO_AES + p.encryptOID = AES256CBCb; +#endif + +#if defined(HAVE_AES_KEYWRAP) && !defined(NO_AES) + WB_NOTE("wc_PKCS7_AddRecipient_KEKRI() otherAttribute matrix " + "[:11224,:11264]"); + /* both true: OtherKeyAttribute present */ + (void)wc_PKCS7_AddRecipient_KEKRI(&p, AES256_WRAP, kek, (word32)sizeof(kek), + keyId, (word32)sizeof(keyId), NULL, otherOid, + (word32)sizeof(otherOid), other, (word32)sizeof(other), 0); + /* 2nd operand false: pointer set, zero length */ + (void)wc_PKCS7_AddRecipient_KEKRI(&p, AES256_WRAP, kek, (word32)sizeof(kek), + keyId, (word32)sizeof(keyId), NULL, otherOid, + (word32)sizeof(otherOid), other, 0, 0); + /* 1st operand false: no OtherKeyAttribute at all */ + (void)wc_PKCS7_AddRecipient_KEKRI(&p, AES256_WRAP, kek, (word32)sizeof(kek), + keyId, (word32)sizeof(keyId), NULL, NULL, 0, NULL, 0, 0); +#endif + +#if defined(HAVE_ECC) && defined(HAVE_AES_KEYWRAP) && defined(HAVE_X963_KDF) + WB_NOTE("wc_PKCS7_AddRecipient_KARI() ukm matrix [:9111] " + "(+ KariGenerateEphemeralKey/KEK all-false [:8700,:8861])"); + /* both true: user keying material supplied */ + (void)wc_PKCS7_AddRecipient_KARI(&p, cliecc_cert_der_256, + (word32)sizeof_cliecc_cert_der_256, AES256_WRAP, + dhSinglePass_stdDH_sha256kdf_scheme, ukm, (word32)sizeof(ukm), 0); + /* 2nd operand false: size set but no buffer */ + (void)wc_PKCS7_AddRecipient_KARI(&p, cliecc_cert_der_256, + (word32)sizeof_cliecc_cert_der_256, AES256_WRAP, + dhSinglePass_stdDH_sha256kdf_scheme, NULL, (word32)sizeof(ukm), 0); + /* 1st operand false: no ukm */ + (void)wc_PKCS7_AddRecipient_KARI(&p, cliecc_cert_der_256, + (word32)sizeof_cliecc_cert_der_256, AES256_WRAP, + dhSinglePass_stdDH_sha256kdf_scheme, NULL, 0, 0); + + WB_NOTE("KariGenerateEphemeralKey/KariGenerateKEK guards [:8700,:8861]"); + { + WC_PKCS7_KARI kari; + static ecc_key wbEccKey; + static DecodedCert wbDCert; + WC_RNG rng; + + XMEMSET(&wbEccKey, 0, sizeof(wbEccKey)); /* dp == NULL */ + XMEMSET(&wbDCert, 0, sizeof(wbDCert)); + + XMEMSET(&kari, 0, sizeof(kari)); + (void)wc_PKCS7_KariGenerateEphemeralKey(NULL); + /* 2nd operand true: no decoded certificate */ + kari.decoded = NULL; + kari.recipKey = &wbEccKey; + (void)wc_PKCS7_KariGenerateEphemeralKey(&kari); + /* 3rd operand true: no recipient key */ + kari.decoded = &wbDCert; + kari.recipKey = NULL; + (void)wc_PKCS7_KariGenerateEphemeralKey(&kari); + /* 4th operand true: recipient key carries no curve parameters */ + kari.recipKey = &wbEccKey; + (void)wc_PKCS7_KariGenerateEphemeralKey(&kari); + + if (wc_InitRng(&rng) == 0) { + XMEMSET(&kari, 0, sizeof(kari)); + (void)wc_PKCS7_KariGenerateKEK(NULL, &rng, AES256_WRAP, + dhSinglePass_stdDH_sha256kdf_scheme); + /* 2nd operand true: no recipient key */ + kari.recipKey = NULL; + kari.senderKey = &wbEccKey; + (void)wc_PKCS7_KariGenerateKEK(&kari, &rng, AES256_WRAP, + dhSinglePass_stdDH_sha256kdf_scheme); + /* 3rd operand true: no sender key */ + kari.recipKey = &wbEccKey; + kari.senderKey = NULL; + (void)wc_PKCS7_KariGenerateKEK(&kari, &rng, AES256_WRAP, + dhSinglePass_stdDH_sha256kdf_scheme); + /* 4th operand true: sender key carries no curve parameters */ + kari.senderKey = &wbEccKey; + (void)wc_PKCS7_KariGenerateKEK(&kari, &rng, AES256_WRAP, + dhSinglePass_stdDH_sha256kdf_scheme); + wc_FreeRng(&rng); + } + } +#endif /* HAVE_ECC && HAVE_AES_KEYWRAP && HAVE_X963_KDF */ + + wc_PKCS7_Free(&p); +} + +/* ------------------------------------------------------------------------- * + * Section 9: VerifySignedData zero-length streaming input [:6915] + * ------------------------------------------------------------------------- */ +#ifndef NO_PKCS7_STREAM +static void wb_verify_zero_input(void) +{ + wc_PKCS7 p; + + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_verify_zero_input skipped"); + wb_fail = 1; + return; + } + + WB_NOTE("PKCS7_VerifySignedData(): pkiMsg==NULL with inSz==0 [:6915]"); + /* 2nd operand true: NULL input with a nonzero size -> BAD_FUNC_ARG */ + (void)wc_PKCS7_VerifySignedData_ex(&p, NULL, 0, NULL, 5, NULL, 0); + /* 2nd operand false: NULL input with zero size is legal in stream mode */ + (void)wc_PKCS7_VerifySignedData_ex(&p, NULL, 0, NULL, 0, NULL, 0); + + wc_PKCS7_Free(&p); +} +#else +static void wb_verify_zero_input(void) +{ + WB_NOTE("NO_PKCS7_STREAM; zero-length verify input skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 10: assorted small matrices + * [:10428 SetSignerIdentifierType, :10044/:10130 DecryptContentInit, + * :17789/:17792/:17797 DecodeOneSymmetricKeyKey, :6639 HandleOctetStrings, + * :15234/:15367/:15381/:15634 EncodeAuthEnvelopedData, :9531 KTRI] + * ------------------------------------------------------------------------- */ +static void wb_small_matrices(void) +{ + wc_PKCS7 p; + byte key32[32]; + byte iv16[16]; + byte osk[32]; + const byte* keyPtr = NULL; + word32 keyPtrSz = 0; + word32 idx; + int i; + + for (i = 0; i < (int)sizeof(key32); i++) + key32[i] = (byte)(i + 1); + for (i = 0; i < (int)sizeof(iv16); i++) + iv16[i] = (byte)(i + 0x40); + + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_small_matrices skipped"); + wb_fail = 1; + return; + } + + WB_NOTE("wc_PKCS7_SetSignerIdentifierType() type matrix [:10428]"); + (void)wc_PKCS7_SetSignerIdentifierType(NULL, CMS_SKID); + (void)wc_PKCS7_SetSignerIdentifierType(&p, CMS_ISSUER_AND_SERIAL_NUMBER); + (void)wc_PKCS7_SetSignerIdentifierType(&p, CMS_SKID); + (void)wc_PKCS7_SetSignerIdentifierType(&p, DEGENERATE_SID); + (void)wc_PKCS7_SetSignerIdentifierType(&p, 0x7ffe); + (void)wc_PKCS7_SetSignerIdentifierType(&p, CMS_ISSUER_AND_SERIAL_NUMBER); + +#if !defined(NO_AES) && defined(HAVE_AES_CBC) + WB_NOTE("wc_PKCS7_DecryptContentInit() AES-CBC key-size matrix [:10044]"); +#ifdef WOLFSSL_AES_192 + if (wc_PKCS7_DecryptContentInit(&p, AES192CBCb, key32, 24, iv16, + (int)sizeof(iv16), INVALID_DEVID, NULL) == 0) + wc_PKCS7_DecryptContentFree(&p, AES192CBCb, NULL); + (void)wc_PKCS7_DecryptContentInit(&p, AES192CBCb, key32, 32, iv16, + (int)sizeof(iv16), INVALID_DEVID, NULL); +#endif +#ifdef WOLFSSL_AES_256 + (void)wc_PKCS7_DecryptContentInit(&p, AES256CBCb, key32, 16, iv16, + (int)sizeof(iv16), INVALID_DEVID, NULL); +#endif +#endif /* !NO_AES && HAVE_AES_CBC */ +#ifndef NO_DES3 + WB_NOTE("wc_PKCS7_DecryptContentInit() DES3 iv-size matrix [:10130]"); + (void)wc_PKCS7_DecryptContentInit(&p, DES3b, key32, DES3_KEYLEN, iv16, + DES_BLOCK_SIZE + 1, INVALID_DEVID, NULL); + if (wc_PKCS7_DecryptContentInit(&p, DES3b, key32, DES3_KEYLEN, iv16, + DES_BLOCK_SIZE, INVALID_DEVID, NULL) == 0) + wc_PKCS7_DecryptContentFree(&p, DES3b, NULL); +#endif + + WB_NOTE("wc_PKCS7_DecodeOneSymmetricKeyKey() element matrix " + "[:17789,:17792,:17797]"); + XMEMSET(osk, 0, sizeof(osk)); + /* SEQUENCE { SEQUENCE {} OCTET STRING "AAAA" } -- the sKeyAttrs arm */ + osk[0] = 0x30; osk[1] = 0x0a; + osk[2] = 0x30; osk[3] = 0x02; osk[4] = 0x05; osk[5] = 0x00; + osk[6] = 0x04; osk[7] = 0x04; + osk[8] = 'A'; osk[9] = 'A'; osk[10] = 'A'; osk[11] = 'A'; + (void)wc_PKCS7_DecodeOneSymmetricKeyKey(osk, 12, &keyPtr, &keyPtrSz); + (void)wc_PKCS7_DecodeOneSymmetricKeyAttribute(osk, 12, 0, &keyPtr, + &keyPtrSz); + (void)wc_PKCS7_DecodeOneSymmetricKeyAttribute(osk, 12, 5, &keyPtr, + &keyPtrSz); + (void)wc_PKCS7_DecodeOneSymmetricKeyAttribute(NULL, 12, 0, &keyPtr, + &keyPtrSz); + /* :17792 second operand false: no sKeyAttrs, straight to the key */ + osk[2] = 0x04; osk[3] = 0x04; + (void)wc_PKCS7_DecodeOneSymmetricKeyKey(osk, 12, &keyPtr, &keyPtrSz); + /* :17797 true: the element after the SEQUENCE is not an OCTET STRING */ + osk[0] = 0x30; osk[1] = 0x06; + osk[2] = 0x05; osk[3] = 0x00; + osk[4] = 0x02; osk[5] = 0x02; osk[6] = 0x01; osk[7] = 0x02; + (void)wc_PKCS7_DecodeOneSymmetricKeyKey(osk, 8, &keyPtr, &keyPtrSz); + /* :17789 true: the outer SEQUENCE header itself is wrong */ + osk[0] = 0x31; + (void)wc_PKCS7_DecodeOneSymmetricKeyKey(osk, 8, &keyPtr, &keyPtrSz); + (void)wc_PKCS7_DecodeOneSymmetricKeyAttribute(osk, 8, 0, &keyPtr, + &keyPtrSz); + + WB_NOTE("wc_PKCS7_ParseToRecipientInfoSet() content-type matrix [:13995]"); + { + byte env[16]; + word32 envIdx; + int savedOid = p.contentOID; + + XMEMSET(env, 0, sizeof(env)); + env[0] = 0x30; env[1] = 0x04; env[2] = 0x02; env[3] = 0x01; + env[4] = 0x00; env[5] = 0x31; + + p.contentOID = DATA; + envIdx = 0; + (void)wc_PKCS7_ParseToRecipientInfoSet(&p, env, (word32)sizeof(env), + &envIdx, ENVELOPED_DATA); + envIdx = 0; + (void)wc_PKCS7_ParseToRecipientInfoSet(&p, env, (word32)sizeof(env), + &envIdx, AUTH_ENVELOPED_DATA); + envIdx = 0; + (void)wc_PKCS7_ParseToRecipientInfoSet(&p, env, (word32)sizeof(env), + &envIdx, SIGNED_DATA); + p.contentOID = FIRMWARE_PKG_DATA; + envIdx = 0; + (void)wc_PKCS7_ParseToRecipientInfoSet(&p, env, (word32)sizeof(env), + &envIdx, SIGNED_DATA); + p.contentOID = savedOid; + } + +#ifndef NO_PKCS7_STREAM + WB_NOTE("wc_PKCS7_HandleOctetStrings() argument chain [:6639]"); + idx = 0; + (void)wc_PKCS7_HandleOctetStrings(NULL, osk, (word32)sizeof(osk), &idx, + &idx, 0); + idx = 0; + (void)wc_PKCS7_HandleOctetStrings(&p, NULL, (word32)sizeof(osk), &idx, + &idx, 0); + idx = 0; + (void)wc_PKCS7_HandleOctetStrings(&p, osk, (word32)sizeof(osk), &idx, + NULL, 0); +#else + (void)idx; +#endif + + wc_PKCS7_Free(&p); +} + +/* ------------------------------------------------------------------------- * + * Section 11: full AuthEnvelopedData encode with optional-field variations + * [:15234, :15367, :15381, :15634] and the KTRI key-type guard [:9531] + * ------------------------------------------------------------------------- */ +#if (defined(HAVE_AESGCM) || defined(HAVE_AESCCM)) && !defined(NO_RSA) +static void wb_auth_encode_shapes(void) +{ + wc_PKCS7 p; + WC_RNG rng; + static byte out[8192]; + byte content[48]; + PKCS7Attrib attrib[1]; + static const byte aOid[] = { 0x06, 0x03, 0x55, 0x04, 0x03 }; + static const byte aVal[] = { 0x0c, 0x02, 0x58, 0x59 }; + int ret; + + XMEMSET(content, 0x9a, sizeof(content)); + attrib[0].oid = aOid; + attrib[0].oidSz = (word32)sizeof(aOid); + attrib[0].value = aVal; + attrib[0].valueSz = (word32)sizeof(aVal); + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; wb_auth_encode_shapes skipped"); + wb_fail = 1; + return; + } + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0 || + wc_PKCS7_InitWithCert(&p, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) != 0) { + WB_NOTE("PKCS7 init failed; wb_auth_encode_shapes skipped"); + wb_fail = 1; + wc_FreeRng(&rng); + return; + } + + p.content = content; + p.contentSz = (word32)sizeof(content); + p.contentOID = DATA; + p.encryptOID = AES256GCMb; + p.rng = &rng; + + WB_NOTE("wc_PKCS7_EncodeAuthEnvelopedData() optional-field matrix " + "[:15234,:15367,:15381,:15634]"); + /* :15234/:15367 all false and the attribute arms taken */ + p.authAttribs = attrib; + p.authAttribsSz = 1; + ret = wc_PKCS7_EncodeAuthEnvelopedData(&p, out, (word32)sizeof(out)); + WB_CHECK(ret > 0, "AuthEnvelopedData with auth attributes encoded"); + + /* :15367 2nd operand false: attribute array set, count zero */ + p.authAttribsSz = 0; + (void)wc_PKCS7_EncodeAuthEnvelopedData(&p, out, (word32)sizeof(out)); + + /* :15234 2nd operand false: recipient cert set, size zero */ + p.authAttribs = NULL; + p.singleCertSz = 0; + (void)wc_PKCS7_EncodeAuthEnvelopedData(&p, out, (word32)sizeof(out)); + + /* :15234 1st operand false: no manually set recipient cert */ + { + byte* savedCert = p.singleCert; + p.singleCert = NULL; + (void)wc_PKCS7_EncodeAuthEnvelopedData(&p, out, (word32)sizeof(out)); + p.singleCert = savedCert; + } + +#ifdef HAVE_ECC + WB_NOTE("wc_PKCS7_AddRecipient_KTRI() non-RSA certificate [:9531]"); + (void)wc_PKCS7_AddRecipient_KTRI(&p, cliecc_cert_der_256, + (word32)sizeof_cliecc_cert_der_256, 0); + (void)wc_PKCS7_AddRecipient_KTRI(&p, client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, 0); +#endif + + p.authAttribs = NULL; + p.authAttribsSz = 0; + p.content = NULL; + p.contentSz = 0; + p.rng = NULL; + wc_PKCS7_Free(&p); + wc_FreeRng(&rng); +} +#else +static void wb_auth_encode_shapes(void) +{ + WB_NOTE("no AES-GCM/CCM or no RSA; AuthEnvelopedData encode shapes" + " skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 12: attribute-size overflow guards, digest builder and the + * InitWithCert key-type matrix + * [:1096, :1352, :1822, :1843, :1957, :5726, :5746] + * ------------------------------------------------------------------------- */ +static void wb_size_guards(void) +{ + wc_PKCS7 p; + EncodedAttrib ea[2]; + PKCS7Attrib at[2]; + static const byte gOid[] = { 0x06, 0x03, 0x55, 0x04, 0x03 }; + static const byte gVal[] = { 0x0c, 0x02, 0x41, 0x42 }; + byte content[32]; + byte hash[WC_SHA256_DIGEST_SIZE]; + int ret; + + XMEMSET(content, 0xc1, sizeof(content)); + XMEMSET(hash, 0xc2, sizeof(hash)); + + XMEMSET(&p, 0, sizeof(p)); + if (wc_PKCS7_Init(&p, NULL, INVALID_DEVID) != 0) { + WB_NOTE("wc_PKCS7_Init failed; wb_size_guards skipped"); + wb_fail = 1; + return; + } + +#if defined(WOLFSSL_SHA3) && \ + (defined(WOLFSSL_SHAKE256) || defined(WOLFSSL_SHAKE128)) + WB_NOTE("wc_PKCS7_DigestParamsAbsent() hashOID matrix [:1096]"); + p.hashOID = SHA256h; + (void)wc_PKCS7_DigestParamsAbsent(&p); +#ifdef WOLFSSL_SHAKE256 + p.hashOID = SHAKE256h; + (void)wc_PKCS7_DigestParamsAbsent(&p); +#endif +#ifdef WOLFSSL_SHAKE128 + p.hashOID = SHAKE128h; + (void)wc_PKCS7_DigestParamsAbsent(&p); +#endif +#endif + p.hashOID = SHA256h; + + WB_NOTE("EncodeAttributes() size-overflow guards [:1822,:1843]"); + XMEMSET(ea, 0, sizeof(ea)); + XMEMSET(at, 0, sizeof(at)); + at[0].oid = gOid; + at[0].oidSz = (word32)sizeof(gOid); + at[0].value = gVal; + at[0].valueSz = (word32)sizeof(gVal); + at[1] = at[0]; + /* all false */ + ret = EncodeAttributes(ea, 2, at, 2); + WB_CHECK(ret > 0, ":1822/:1843 all false (normal attributes)"); + /* :1822 1st operand true: valueSz + oidSz already wraps */ + at[0].valueSz = 0xFFFFFFF0U; + at[0].oidSz = 0x40; + ret = EncodeAttributes(ea, 1, at, 1); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), ":1822 1st operand true"); + /* :1822 2nd operand true: the sum only wraps once the SET header is added */ + at[0].valueSz = 0xFFFFFFFAU; + at[0].oidSz = 1; + ret = EncodeAttributes(ea, 1, at, 1); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), ":1822 2nd operand true"); + /* :1843 2nd operand true: each attribute fits, the running total does not */ + at[0].valueSz = 0x60000000U; + at[0].oidSz = (word32)sizeof(gOid); + at[1] = at[0]; + ret = EncodeAttributes(ea, 2, at, 2); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), ":1843 2nd operand true"); + + WB_NOTE("FlattenEncodedAttribs() size-overflow guards [:1957]"); + { + FlatAttrib* derArr[1]; + + derArr[0] = NULL; + XMEMSET(ea, 0, sizeof(ea)); + ea[0].valueSeqSz = 0xFFFFFFF0U; + ea[0].oidSz = 0x40; + WB_CHECK(FlattenEncodedAttribs(&p, derArr, 1, ea, 1) == + WC_NO_ERR_TRACE(BUFFER_E), ":1957 1st operand true"); + ea[0].valueSeqSz = 0xFFFFFFF0U; + ea[0].oidSz = 0x08; + ea[0].valueSetSz = 0x40; + WB_CHECK(FlattenEncodedAttribs(&p, derArr, 1, ea, 1) == + WC_NO_ERR_TRACE(BUFFER_E), ":1957 2nd operand true"); + ea[0].valueSetSz = 0x02; + ea[0].valueSz = 0x40; + WB_CHECK(FlattenEncodedAttribs(&p, derArr, 1, ea, 1) == + WC_NO_ERR_TRACE(BUFFER_E), ":1957 3rd operand true"); + } + + WB_NOTE("wc_PKCS7_BuildSignedDataDigest() hash-source matrix " + "[:5726,:5746]"); + { + byte pkcs7Digest[MAX_PKCS7_DIGEST_SZ]; + word32 pkcs7DigestSz; + byte* plainDigest = NULL; + word32 plainDigestSz = 0; + byte signedAttrib[8]; + + XMEMSET(signedAttrib, 0x31, sizeof(signedAttrib)); + p.content = content; + p.contentSz = (word32)sizeof(content); + p.hashOID = SHA256h; + + /* :5726/:5746 2nd operand false: hash pointer with zero length */ + pkcs7DigestSz = (word32)sizeof(pkcs7Digest); + (void)wc_PKCS7_BuildSignedDataDigest(&p, NULL, 0, pkcs7Digest, + &pkcs7DigestSz, &plainDigest, &plainDigestSz, hash, 0, 0); + /* :5726/:5746 1st operand false: no caller hash at all */ + pkcs7DigestSz = (word32)sizeof(pkcs7Digest); + (void)wc_PKCS7_BuildSignedDataDigest(&p, NULL, 0, pkcs7Digest, + &pkcs7DigestSz, &plainDigest, &plainDigestSz, NULL, 0, 0); + /* :5746 all true: caller hash used directly */ + pkcs7DigestSz = (word32)sizeof(pkcs7Digest); + (void)wc_PKCS7_BuildSignedDataDigest(&p, NULL, 0, pkcs7Digest, + &pkcs7DigestSz, &plainDigest, &plainDigestSz, hash, + (word32)sizeof(hash), 0); + /* :5746 3rd operand false: signed attributes present */ + pkcs7DigestSz = (word32)sizeof(pkcs7Digest); + (void)wc_PKCS7_BuildSignedDataDigest(&p, signedAttrib, + (word32)sizeof(signedAttrib), pkcs7Digest, &pkcs7DigestSz, + &plainDigest, &plainDigestSz, hash, (word32)sizeof(hash), 0); + + p.content = NULL; + p.contentSz = 0; + } + + wc_PKCS7_Free(&p); + + WB_NOTE("wc_PKCS7_InitWithCert() signer key-type matrix [:1352]"); + { + wc_PKCS7* c; + +#ifndef NO_RSA + c = wc_PKCS7_New(NULL, INVALID_DEVID); + if (c != NULL) { + (void)wc_PKCS7_InitWithCert(c, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048); + wc_PKCS7_Free(c); + } +#endif +#ifdef HAVE_ECC + c = wc_PKCS7_New(NULL, INVALID_DEVID); + if (c != NULL) { + (void)wc_PKCS7_InitWithCert(c, (byte*)cliecc_cert_der_256, + (word32)sizeof_cliecc_cert_der_256); + wc_PKCS7_Free(c); + } +#endif +#ifdef HAVE_ED25519 + /* neither RSA-family nor ECDSA: all operands false */ + c = wc_PKCS7_New(NULL, INVALID_DEVID); + if (c != NULL) { + (void)wc_PKCS7_InitWithCert(c, (byte*)client_ed25519_cert, + (word32)sizeof_client_ed25519_cert); + wc_PKCS7_Free(c); + } +#endif + } +} + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("pkcs7.c white-box MC/DC argument-chain supplement\n"); + + wb_sign_helper_args(); + wb_signed_attrib_flags(); + wb_signerinfo_binding(); + wb_verify_content_msgdigest(); + wb_encodesigned_shapes(); + wb_public_entry_args(); + wb_content_crypt_args(); + wb_recipient_optionals(); + wb_verify_zero_input(); + wb_small_matrices(); + wb_auth_encode_shapes(); + wb_size_guards(); + + printf("done (%s)\n", wb_fail ? "with failures" : "ok"); + /* Always return 0: a nonzero exit discards this variant's coverage + * entirely in the campaign harness. Failures are surfaced via the + * printed [FAIL] lines instead. */ + (void)wb_fail; + return 0; +} diff --git a/tests/unit-mcdc/test_pkcs7_mutate_whitebox.c b/tests/unit-mcdc/test_pkcs7_mutate_whitebox.c new file mode 100644 index 00000000000..bc18eb5e0cc --- /dev/null +++ b/tests/unit-mcdc/test_pkcs7_mutate_whitebox.c @@ -0,0 +1,1029 @@ +/* test_pkcs7_mutate_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Dense mutation white-box MC/DC supplement for wolfcrypt/src/pkcs7.c. + * + * The decode state machines in pkcs7.c are written as long chains of + * + * if (ret == 0 && Get(pkiMsg, &idx, ..., sz) < 0) + * ret = ASN_PARSE_E; + * + * For MC/DC each such decision needs three vectors inside one binary: + * - ret == 0 and the element parses -> decision false (2nd operand pair) + * - ret == 0 and the element fails -> decision true + * - ret != 0 on arrival -> decision false (1st operand pair) + * A well-formed bundle gives the first, a bundle whose byte at exactly that + * element is wrong gives the second, and every decision *after* the one that + * first set ret != 0 gets the third for free from the very same run. + * + * test_pkcs7_decode_whitebox.c already sweeps these corpora, but with a + * stride capped at ~200 mutations per corpus, so most element boundaries are + * stepped over. This file re-sweeps the same (plus a few more) corpora at + * single-byte stride with several mutation values, and in several caller + * modes (detached / head+foot split / pre-computed hash / noDegenerate), so + * that every element boundary is hit by at least one failing vector. + * + * All calls run against a freshly allocated wc_PKCS7 so a mutated bundle can + * never poison the next vector. + */ + +#include + +#include +#include +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) +#define WB_CHECK(cond, msg) \ + do { if (!(cond)) { printf(" [wb][FAIL] %s\n", (msg)); wb_fail = 1; } } \ + while (0) + +#define WB_SCRATCH_SZ 8192 +static byte wbScratch[WB_SCRATCH_SZ]; +static byte wbOut[WB_SCRATCH_SZ]; +static WC_RNG wbRng; +static int wbRngOk = 0; + +typedef void (*wb_decode_fn)(byte* buf, word32 len); + +static word32 wb_load_file(const char* path, byte* buf, word32 bufSz) +{ + FILE* f; + size_t n; + + f = fopen(path, "rb"); + if (f == NULL) { + printf(" [wb] corpus not found, skip: %s\n", path); + return 0; + } + n = fread(buf, 1, bufSz, f); + fclose(f); + return (word32)n; +} + +/* Dense single-byte mutation sweep. `stride` selects how many byte offsets + * are visited; each visited offset is driven with three different wrong + * values so tag bytes, length bytes and content bytes all get a chance to + * break the element they belong to. The pristine bundle is replayed first + * and last so the "element parses" leg of every pair is present too. */ +static void wb_mutate(wb_decode_fn fn, byte* buf, word32 fullLen, word32 stride) +{ + word32 i; + byte saved; + + if (fullLen < 4) + return; + if (stride == 0) + stride = 1; + + fn(buf, fullLen); + for (i = 0; i < fullLen; i += stride) { + saved = buf[i]; + /* 0xFF/0x7F break tags outright; +/-1 and ^0x20 are the "plausible + * but wrong" length and tag values that slip past the element being + * mutated and break the next one instead, which is what the later + * links of each (ret == 0 && Get*(...)) chain need. */ + buf[i] = (byte)(saved ^ 0xFF); + fn(buf, fullLen); + buf[i] = (byte)(saved ^ 0x01); + fn(buf, fullLen); + buf[i] = 0x7F; + fn(buf, fullLen); + buf[i] = (byte)(saved + 1); + fn(buf, fullLen); + buf[i] = (byte)(saved - 1); + fn(buf, fullLen); + buf[i] = (byte)(saved ^ 0x20); + fn(buf, fullLen); + buf[i] = saved; + } + fn(buf, fullLen); +} + +/* Dense truncation sweep: a short message stops the walk at a different + * element for every prefix length. */ +static void wb_truncate(wb_decode_fn fn, byte* buf, word32 fullLen, + word32 stride) +{ + word32 i; + + if (fullLen < 4) + return; + if (stride == 0) + stride = 1; + for (i = 2; i < fullLen; i += stride) + fn(buf, i); + fn(buf, fullLen); +} + +/* ------------------------------------------------------------------------- * + * Section 1: PKCS7_VerifySignedData() decode walk, five caller modes + * ------------------------------------------------------------------------- */ +#ifndef NO_RSA +static int wbVerifyMode = 0; +static byte wbHash[WC_SHA256_DIGEST_SIZE]; + +static void wb_verify_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + word32 half; + + if (p == NULL) + return; + if (wc_PKCS7_InitWithCert(p, NULL, 0) == 0) { + switch (wbVerifyMode) { + case 0: + (void)wc_PKCS7_VerifySignedData(p, buf, len); + break; + case 1: + (void)wc_PKCS7_VerifySignedData_ex(p, wbHash, + (word32)sizeof(wbHash), buf, len, NULL, 0); + break; + case 2: + half = len / 2; + (void)wc_PKCS7_VerifySignedData_ex(p, wbHash, + (word32)sizeof(wbHash), buf, half, buf + half, + len - half); + break; + case 3: + p->noDegenerate = 1; + (void)wc_PKCS7_VerifySignedData(p, buf, len); + break; + case 4: + (void)wc_PKCS7_SetDetached(p, 1); + (void)wc_PKCS7_VerifySignedData(p, buf, len); + break; + case 5: + half = len / 2; + (void)wc_PKCS7_VerifySignedData_ex(p, NULL, 0, buf, half, + buf + half, len - half); + break; + default: + break; + } + } + wc_PKCS7_Free(p); +} + +static void wb_verify_sweep_file(const char* path) +{ + word32 fullLen = wb_load_file(path, wbScratch, sizeof(wbScratch)); + word32 dense; + + if (fullLen == 0) + return; + + /* keep the total call count bounded on big corpora */ + dense = (fullLen > 3000) ? 2 : 1; + + for (wbVerifyMode = 0; wbVerifyMode <= 5; wbVerifyMode++) { + wb_mutate(wb_verify_call, wbScratch, fullLen, + (wbVerifyMode == 0) ? dense : (dense * 4)); + wb_truncate(wb_verify_call, wbScratch, fullLen, + (wbVerifyMode == 0) ? dense : (dense * 4)); + } + wbVerifyMode = 0; +} + +static void wb_verify_chains(void) +{ + XMEMSET(wbHash, 0x5c, sizeof(wbHash)); + + WB_NOTE("PKCS7_VerifySignedData(): dense mutation sweep, test-degenerate.p7b"); + wb_verify_sweep_file("./certs/test-degenerate.p7b"); + +#ifdef ASN_BER_TO_DER + WB_NOTE("PKCS7_VerifySignedData(): dense mutation sweep," + " test-ber-exp02-05-2022.p7b"); + wb_verify_sweep_file("./certs/test-ber-exp02-05-2022.p7b"); +#endif + + WB_NOTE("PKCS7_VerifySignedData(): dense mutation sweep," + " test-stream-sign.p7b"); + wb_verify_sweep_file("./certs/test-stream-sign.p7b"); + + WB_NOTE("PKCS7_VerifySignedData(): dense mutation sweep," + " test-stream-dec.p7b"); + wb_verify_sweep_file("./certs/test-stream-dec.p7b"); +} + +/* ------------------------------------------------------------------------- * + * Section 1b: self-built SignedData corpora. The corpus files above are all + * IssuerAndSerialNumber-identified, attached, definite-length bundles, so the + * SubjectKeyIdentifier / degenerate / signed-attribute / BER-stream arms of + * both PKCS7_VerifySignedData() and wc_PKCS7_ParseSignerInfo() are never + * entered by them. Build one of each here and sweep it the same way. + * ------------------------------------------------------------------------- */ +#ifdef USE_CERT_BUFFERS_2048 +static byte wbSignCorpus[WB_SCRATCH_SZ]; +static byte wbSignContent[64]; +static byte wbSignHash[WC_SHA256_DIGEST_SIZE]; +static byte wbSignHead[WB_SCRATCH_SZ]; +static byte wbSignFoot[2048]; +static word32 wbSignHeadSz; +static word32 wbSignFootSz; + +static const byte wbSaOid[] = { 0x06, 0x03, 0x55, 0x04, 0x03 }; +static const byte wbSaVal[] = { 0x0c, 0x02, 0x41, 0x42 }; + +/* sidType < 0 leaves the library default in place */ +static word32 wb_build_signed(byte* out, word32 outSz, int sidType, + int withAttribs, int stream) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + PKCS7Attrib attrib[1]; + int sz = 0; + + if (p == NULL) + return 0; + if (wc_PKCS7_InitWithCert(p, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) == 0) { + attrib[0].oid = wbSaOid; + attrib[0].oidSz = (word32)sizeof(wbSaOid); + attrib[0].value = wbSaVal; + attrib[0].valueSz = (word32)sizeof(wbSaVal); + + p->content = wbSignContent; + p->contentSz = (word32)sizeof(wbSignContent); + p->contentOID = DATA; + p->hashOID = SHA256h; + p->privateKey = (byte*)client_key_der_2048; + p->privateKeySz = (word32)sizeof_client_key_der_2048; + p->rng = wbRngOk ? &wbRng : NULL; + if (sidType >= 0) + (void)wc_PKCS7_SetSignerIdentifierType(p, sidType); + if (withAttribs) { + p->signedAttribs = attrib; + p->signedAttribsSz = 1; + } +#ifdef ASN_BER_TO_DER + if (stream) + p->encodeStream = 1; +#else + (void)stream; +#endif + sz = wc_PKCS7_EncodeSignedData(p, out, outSz); + p->signedAttribs = NULL; + p->signedAttribsSz = 0; + p->privateKey = NULL; + p->privateKeySz = 0; + } + wc_PKCS7_Free(p); + return (sz > 0) ? (word32)sz : 0; +} + +static void wb_signed_sweep(const char* what, int sidType, int withAttribs, + int stream, word32 stride) +{ + word32 fullLen; + int mode; + + WB_NOTE(what); + fullLen = wb_build_signed(wbSignCorpus, (word32)sizeof(wbSignCorpus), + sidType, withAttribs, stream); + if (fullLen < 32) { + printf(" [wb] self-built SignedData variant not produced, skipped\n"); + return; + } + for (mode = 0; mode <= 4; mode++) { + wbVerifyMode = mode; + wb_mutate(wb_verify_call, wbSignCorpus, fullLen, + (mode == 0) ? stride : (stride * 4)); + wb_truncate(wb_verify_call, wbSignCorpus, fullLen, + (mode == 0) ? stride : (stride * 4)); + } + wbVerifyMode = 0; +} + +/* Detached bundles are verified through the head/foot two-buffer entry, which + * is the only way the pkiMsg2/in2 arms of PKCS7_VerifySignedData() are ever + * reached. head and foot are held in globals so the generic mutation driver + * can walk either buffer while the other stays intact. */ +static int wbDetachedMode = 0; + +static void wb_detached_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + + (void)buf; + (void)len; + if (p == NULL) + return; + if (wc_PKCS7_InitWithCert(p, NULL, 0) == 0) { + /* required even on verify when head/foot buffers are used */ + p->contentSz = (word32)sizeof(wbSignContent); + switch (wbDetachedMode) { + case 0: /* hash + footer, the intended flow */ + (void)wc_PKCS7_VerifySignedData_ex(p, wbSignHash, + (word32)sizeof(wbSignHash), wbSignHead, wbSignHeadSz, + wbSignFoot, wbSignFootSz); + break; + case 1: /* no caller hash */ + (void)wc_PKCS7_VerifySignedData_ex(p, NULL, 0, + wbSignHead, wbSignHeadSz, wbSignFoot, wbSignFootSz); + break; + case 2: /* footer pointer present, footer size zero */ + (void)wc_PKCS7_VerifySignedData_ex(p, wbSignHash, + (word32)sizeof(wbSignHash), wbSignHead, wbSignHeadSz, + wbSignFoot, 0); + break; + case 3: /* hash pointer present, hash size zero */ + (void)wc_PKCS7_VerifySignedData_ex(p, wbSignHash, 0, + wbSignHead, wbSignHeadSz, wbSignFoot, wbSignFootSz); + break; + default: + break; + } + } + wc_PKCS7_Free(p); +} + +static int wb_build_detached(int detached) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + int ok = 0; + + wbSignHeadSz = (word32)sizeof(wbSignHead); + wbSignFootSz = (word32)sizeof(wbSignFoot); + + if (p == NULL) + return 0; + if (wc_PKCS7_InitWithCert(p, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) == 0) { + /* _ex() signs the caller-supplied hash: content must be NULL and + * only contentSz is consulted (mirrors tests/api). */ + p->content = NULL; + p->contentSz = (word32)sizeof(wbSignContent); + p->contentOID = DATA; + p->hashOID = SHA256h; + p->privateKey = (byte*)client_key_der_2048; + p->privateKeySz = (word32)sizeof_client_key_der_2048; + p->encryptOID = RSAk; + p->rng = wbRngOk ? &wbRng : NULL; + (void)wc_PKCS7_SetDetached(p, (word16)(detached ? 1 : 0)); + if (wc_PKCS7_EncodeSignedData_ex(p, wbSignHash, + (word32)sizeof(wbSignHash), wbSignHead, &wbSignHeadSz, + wbSignFoot, &wbSignFootSz) >= 0) { + ok = 1; + } + p->privateKey = NULL; + p->privateKeySz = 0; + } + wc_PKCS7_Free(p); + return ok; +} + +static void wb_signed_variant_chains(void) +{ + word32 i; + + XMEMSET(wbSignContent, 0x4d, sizeof(wbSignContent)); + if (wc_Hash(WC_HASH_TYPE_SHA256, wbSignContent, (word32)sizeof(wbSignContent), + wbSignHash, (word32)sizeof(wbSignHash)) != 0) { + XMEMSET(wbSignHash, 0, sizeof(wbSignHash)); + } + + wb_signed_sweep("PKCS7_VerifySignedData(): self-built SignedData, " + "IssuerAndSerialNumber sid", CMS_ISSUER_AND_SERIAL_NUMBER, 0, 0, 2); + wb_signed_sweep("PKCS7_VerifySignedData()/ParseSignerInfo(): self-built " + "SignedData, SubjectKeyIdentifier sid", CMS_SKID, 0, 0, 2); + wb_signed_sweep("PKCS7_VerifySignedData(): self-built SignedData with " + "custom signed attributes", CMS_ISSUER_AND_SERIAL_NUMBER, 1, 0, 2); + wb_signed_sweep("PKCS7_VerifySignedData(): self-built degenerate " + "(certs-only) SignedData", DEGENERATE_SID, 0, 0, 2); +#ifdef ASN_BER_TO_DER + wb_signed_sweep("PKCS7_VerifySignedData(): self-built BER " + "indefinite-length SignedData", CMS_ISSUER_AND_SERIAL_NUMBER, + 0, 1, 2); +#endif + + WB_NOTE("PKCS7_VerifySignedData_ex(): detached head/foot mutation sweep"); + for (i = 0; i < 2; i++) { + if (!wb_build_detached((int)(1 - i))) { + printf(" [wb] detached variant %u not produced\n", (unsigned)i); + continue; + } + { + /* baseline: the pristine head/foot pair must verify, otherwise the + * whole STAGE4..6 pkiMsg2 walk below is never entered */ + wc_PKCS7* vp = wc_PKCS7_New(NULL, INVALID_DEVID); + if (vp != NULL) { + if (wc_PKCS7_InitWithCert(vp, NULL, 0) == 0) { + vp->contentSz = (word32)sizeof(wbSignContent); + /* Informational, not a pass/fail: the detached==1 build + * is expected to stop early (the verifier has no content + * to re-hash), while detached==0 walks all the way to the + * signature check. Both are useful mutation baselines. */ + printf(" [wb] head/foot corpus: detached=%d head=%u " + "foot=%u baseline ret=%d\n", + (int)(1 - i), (unsigned)wbSignHeadSz, + (unsigned)wbSignFootSz, + wc_PKCS7_VerifySignedData_ex(vp, wbSignHash, + (word32)sizeof(wbSignHash), wbSignHead, + wbSignHeadSz, wbSignFoot, wbSignFootSz)); + } + wc_PKCS7_Free(vp); + } + } + for (wbDetachedMode = 0; wbDetachedMode <= 3; wbDetachedMode++) { + wb_mutate(wb_detached_call, wbSignHead, wbSignHeadSz, 1); + wb_truncate(wb_detached_call, wbSignHead, wbSignHeadSz, 4); + if (wbSignFootSz > 4) { + wb_mutate(wb_detached_call, wbSignFoot, wbSignFootSz, + (wbDetachedMode == 0) ? 1 : 4); + } + } + wbDetachedMode = 0; + } +} +#else +static void wb_signed_variant_chains(void) +{ + WB_NOTE("no 2048-bit cert buffers; self-built SignedData sweeps skipped"); +} +#endif /* USE_CERT_BUFFERS_2048 */ +#else +static void wb_verify_chains(void) +{ + WB_NOTE("NO_RSA; VerifySignedData mutation sweep skipped"); +} +static void wb_signed_variant_chains(void) +{ + WB_NOTE("NO_RSA; self-built SignedData sweeps skipped"); +} +#endif /* !NO_RSA */ + +/* ------------------------------------------------------------------------- * + * Section 2: wc_PKCS7_DecodeEnvelopedData() / ParseToRecipientInfoSet() + * ------------------------------------------------------------------------- */ +#if !defined(NO_RSA) && defined(USE_CERT_BUFFERS_2048) +static void wb_env_ktri_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + + if (p == NULL) + return; + if (wc_PKCS7_InitWithCert(p, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) == 0) { + p->privateKey = (byte*)client_key_der_2048; + p->privateKeySz = (word32)sizeof_client_key_der_2048; + (void)wc_PKCS7_DecodeEnvelopedData(p, buf, len, wbOut, + (word32)sizeof(wbOut)); + } + wc_PKCS7_Free(p); +} + +#ifdef HAVE_ECC +/* The KARI corpus is decoded with the ECC client credentials so + * wc_PKCS7_KariGetOriginatorIdentifierOrKey() and the ECDSAk arms of the + * RecipientInfo version check are entered. */ +static void wb_env_kari_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + + if (p == NULL) + return; + if (wc_PKCS7_InitWithCert(p, (byte*)cliecc_cert_der_256, + (word32)sizeof_cliecc_cert_der_256) == 0) { + p->privateKey = (byte*)ecc_clikey_der_256; + p->privateKeySz = (word32)sizeof_ecc_clikey_der_256; + (void)wc_PKCS7_DecodeEnvelopedData(p, buf, len, wbOut, + (word32)sizeof(wbOut)); + p->privateKey = NULL; + p->privateKeySz = 0; + } + wc_PKCS7_Free(p); +} +#endif /* HAVE_ECC */ + +static int wb_decrypt_cb(wc_PKCS7* pkcs7, int encryptOID, byte* iv, int ivSz, + byte* aad, word32 aadSz, byte* authTag, word32 authTagSz, + byte* in, int inSz, byte* out, void* usrCtx) +{ + (void)pkcs7; (void)encryptOID; (void)iv; (void)ivSz; + (void)aad; (void)aadSz; (void)authTag; (void)authTagSz; (void)usrCtx; + if (out != NULL && in != NULL && inSz > 0) + XMEMCPY(out, in, (word32)inSz); + return 0; +} + +/* Same corpus, but with a user decryption callback installed and no output + * buffer, so the callback arm and the output-size guard are taken. */ +static void wb_env_cb_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + + if (p == NULL) + return; + if (wc_PKCS7_InitWithCert(p, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) == 0) { + p->privateKey = (byte*)client_key_der_2048; + p->privateKeySz = (word32)sizeof_client_key_der_2048; + (void)wc_PKCS7_SetDecodeEncryptedCb(p, wb_decrypt_cb); + (void)wc_PKCS7_DecodeEnvelopedData(p, buf, len, wbOut, 4); + p->privateKey = NULL; + p->privateKeySz = 0; + } + wc_PKCS7_Free(p); +} + +static void wb_ris_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + word32 idx = 0; + + if (p == NULL) + return; + if (wc_PKCS7_InitWithCert(p, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) == 0) { + p->privateKey = (byte*)client_key_der_2048; + p->privateKeySz = (word32)sizeof_client_key_der_2048; + (void)wc_PKCS7_ParseToRecipientInfoSet(p, buf, len, &idx, + ENVELOPED_DATA); + } + wc_PKCS7_Free(p); +} + +static void wb_enveloped_chains(void) +{ + word32 fullLen; + + WB_NOTE("wc_PKCS7_DecodeEnvelopedData(): dense mutation sweep," + " ktri-keyid-cms.msg"); + fullLen = wb_load_file("./certs/test/ktri-keyid-cms.msg", wbScratch, + sizeof(wbScratch)); + if (fullLen > 0) { + wb_mutate(wb_env_ktri_call, wbScratch, fullLen, 1); + wb_truncate(wb_env_ktri_call, wbScratch, fullLen, 1); + wb_mutate(wb_ris_call, wbScratch, fullLen, 1); + wb_truncate(wb_ris_call, wbScratch, fullLen, 2); + } + + WB_NOTE("wc_PKCS7_DecodeEnvelopedData(): dense mutation sweep," + " test-multiple-recipients.p7b"); + fullLen = wb_load_file("./certs/test-multiple-recipients.p7b", wbScratch, + sizeof(wbScratch)); + if (fullLen > 0) { + wb_mutate(wb_env_ktri_call, wbScratch, fullLen, 2); + wb_truncate(wb_env_ktri_call, wbScratch, fullLen, 2); + wb_mutate(wb_ris_call, wbScratch, fullLen, 2); + wb_mutate(wb_env_cb_call, wbScratch, fullLen, 4); + } + +#ifdef HAVE_ECC + WB_NOTE("wc_PKCS7_DecodeEnvelopedData(): KARI/ECC decode-walk sweep," + " kari-keyid-cms.msg"); + fullLen = wb_load_file("./certs/test/kari-keyid-cms.msg", wbScratch, + sizeof(wbScratch)); + if (fullLen > 0) { + wb_mutate(wb_env_kari_call, wbScratch, fullLen, 1); + wb_truncate(wb_env_kari_call, wbScratch, fullLen, 1); + } +#endif +} +#else +static void wb_enveloped_chains(void) +{ + WB_NOTE("NO_RSA or no 2048-bit cert buffers; EnvelopedData sweep skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 3: wc_PKCS7_DecodeAuthEnvelopedData() + * ------------------------------------------------------------------------- */ +#if defined(HAVE_AESGCM) && !defined(NO_RSA) && defined(USE_CERT_BUFFERS_2048) +static byte wbAuthCorpus[WB_SCRATCH_SZ]; + +static word32 wb_build_auth_enveloped(byte* out, word32 outSz, int oid, + int withAttribs) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + byte data[] = "authEnvelopedData mutation corpus payload 0123456789"; + PKCS7Attrib attrib[1]; + static const byte attribOid[] = { 0x06, 0x03, 0x55, 0x04, 0x03 }; + static const byte attribVal[] = { 0x04, 0x02, 0x33, 0x44 }; + int sz = 0; + + if (p == NULL) + return 0; + if (wc_PKCS7_InitWithCert(p, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) == 0) { + attrib[0].oid = attribOid; + attrib[0].oidSz = (word32)sizeof(attribOid); + attrib[0].value = attribVal; + attrib[0].valueSz = (word32)sizeof(attribVal); + + p->content = data; + p->contentSz = (word32)sizeof(data); + p->contentOID = DATA; + p->encryptOID = oid; + p->rng = wbRngOk ? &wbRng : NULL; + if (withAttribs) { + p->authAttribs = attrib; + p->authAttribsSz = 1; + p->unauthAttribs = attrib; + p->unauthAttribsSz = 1; + } + sz = wc_PKCS7_EncodeAuthEnvelopedData(p, out, outSz); + p->authAttribs = NULL; + p->authAttribsSz = 0; + p->unauthAttribs = NULL; + p->unauthAttribsSz = 0; + } + wc_PKCS7_Free(p); + return (sz > 0) ? (word32)sz : 0; +} + +static int wbAuthMode = 0; + +static void wb_auth_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + + if (p == NULL) + return; + if (wc_PKCS7_InitWithCert(p, (byte*)client_cert_der_2048, + (word32)sizeof_client_cert_der_2048) == 0) { + p->privateKey = (byte*)client_key_der_2048; + p->privateKeySz = (word32)sizeof_client_key_der_2048; + if (wbAuthMode == 1) { + /* undersized output buffer: takes the size guards near the end + * of the walk instead of the successful decrypt */ + (void)wc_PKCS7_DecodeAuthEnvelopedData(p, buf, len, wbOut, 8); + } + else { + (void)wc_PKCS7_DecodeAuthEnvelopedData(p, buf, len, wbOut, + (word32)sizeof(wbOut)); + } + p->privateKey = NULL; + p->privateKeySz = 0; + } + wc_PKCS7_Free(p); +} + +static void wb_auth_chains(void) +{ + word32 fullLen; + + WB_NOTE("wc_PKCS7_DecodeAuthEnvelopedData(): dense mutation sweep over a" + " self-built AES256GCMb message with auth/unauth attributes"); + fullLen = wb_build_auth_enveloped(wbAuthCorpus, (word32)sizeof(wbAuthCorpus), + AES256GCMb, 1); + WB_CHECK(fullLen > 32, "self-built AES256GCM AuthEnvelopedData encoded"); + if (fullLen > 32) { + wb_mutate(wb_auth_call, wbAuthCorpus, fullLen, 1); + wb_truncate(wb_auth_call, wbAuthCorpus, fullLen, 1); + wbAuthMode = 1; + wb_mutate(wb_auth_call, wbAuthCorpus, fullLen, 2); + wb_truncate(wb_auth_call, wbAuthCorpus, fullLen, 2); + wbAuthMode = 0; + } + + WB_NOTE("wc_PKCS7_DecodeAuthEnvelopedData(): same, with no auth/unauth" + " attributes (optional-field arms absent)"); + fullLen = wb_build_auth_enveloped(wbAuthCorpus, (word32)sizeof(wbAuthCorpus), + AES256GCMb, 0); + if (fullLen > 32) { + wb_mutate(wb_auth_call, wbAuthCorpus, fullLen, 1); + wb_truncate(wb_auth_call, wbAuthCorpus, fullLen, 1); + } + +#ifdef WOLFSSL_AES_128 + fullLen = wb_build_auth_enveloped(wbAuthCorpus, (word32)sizeof(wbAuthCorpus), + AES128GCMb, 1); + if (fullLen > 32) { + wb_mutate(wb_auth_call, wbAuthCorpus, fullLen, 2); + wb_truncate(wb_auth_call, wbAuthCorpus, fullLen, 2); + } +#endif +#ifdef HAVE_AESCCM + WB_NOTE("wc_PKCS7_DecodeAuthEnvelopedData(): AES256CCMb variant"); + fullLen = wb_build_auth_enveloped(wbAuthCorpus, (word32)sizeof(wbAuthCorpus), + AES256CCMb, 1); + if (fullLen > 32) { + wb_mutate(wb_auth_call, wbAuthCorpus, fullLen, 1); + wb_truncate(wb_auth_call, wbAuthCorpus, fullLen, 2); + } +#endif +} +#else +static void wb_auth_chains(void) +{ + WB_NOTE("no AES-GCM/RSA; AuthEnvelopedData sweep skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 4: wc_PKCS7_DecodeEncryptedData() + * ------------------------------------------------------------------------- */ +#if !defined(NO_PKCS7_ENCRYPTED_DATA) && !defined(NO_AES) +static const byte wbEncKey[] = { + 0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08, + 0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10 +}; + +static byte wbEncCorpus[WB_SCRATCH_SZ]; + +static word32 wb_build_encrypted(byte* out, word32 outSz) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + byte data[] = "encryptedData mutation corpus payload 0123456789"; + PKCS7Attrib attrib[1]; + static const byte attribOid[] = { 0x06, 0x03, 0x55, 0x04, 0x03 }; + static const byte attribVal[] = { 0x04, 0x02, 0x55, 0x66 }; + int sz = 0; + + if (p == NULL) + return 0; + if (wc_PKCS7_Init(p, NULL, INVALID_DEVID) == 0) { + attrib[0].oid = attribOid; + attrib[0].oidSz = (word32)sizeof(attribOid); + attrib[0].value = attribVal; + attrib[0].valueSz = (word32)sizeof(attribVal); + + p->content = data; + p->contentSz = (word32)sizeof(data); + p->contentOID = DATA; + p->encryptOID = AES128CBCb; + p->encryptionKey = (byte*)wbEncKey; + p->encryptionKeySz = (word32)sizeof(wbEncKey); + p->unprotectedAttribs = attrib; + p->unprotectedAttribsSz = 1; + sz = wc_PKCS7_EncodeEncryptedData(p, out, outSz); + p->unprotectedAttribs = NULL; + p->unprotectedAttribsSz = 0; + p->encryptionKey = NULL; + p->encryptionKeySz = 0; + } + wc_PKCS7_Free(p); + return (sz > 0) ? (word32)sz : 0; +} + +static byte wbEncVersion = 0; + +static void wb_encrypted_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + + if (p == NULL) + return; + if (wc_PKCS7_Init(p, NULL, INVALID_DEVID) == 0) { + p->version = wbEncVersion; + p->encryptionKey = (byte*)wbEncKey; + p->encryptionKeySz = (word32)sizeof(wbEncKey); + (void)wc_PKCS7_DecodeEncryptedData(p, buf, len, wbOut, + (word32)sizeof(wbOut)); + p->encryptionKey = NULL; + p->encryptionKeySz = 0; + } + wc_PKCS7_Free(p); +} + +static void wb_encrypted_chains(void) +{ + word32 fullLen; + + WB_NOTE("wc_PKCS7_DecodeEncryptedData(): dense mutation sweep over a" + " self-built AES128CBCb message with unprotected attributes"); + fullLen = wb_build_encrypted(wbEncCorpus, (word32)sizeof(wbEncCorpus)); + WB_CHECK(fullLen > 32, "self-built EncryptedData encoded"); + if (fullLen > 32) { + wb_mutate(wb_encrypted_call, wbEncCorpus, fullLen, 1); + wb_truncate(wb_encrypted_call, wbEncCorpus, fullLen, 1); + } + + fullLen = wb_load_file("./certs/test/encrypteddata.msg", wbScratch, + sizeof(wbScratch)); + if (fullLen > 0) { + wb_mutate(wb_encrypted_call, wbScratch, fullLen, 1); + wb_truncate(wb_encrypted_call, wbScratch, fullLen, 1); + } + + WB_NOTE("wc_PKCS7_DecodeEncryptedData(): same, decoded as a " + "FirmwarePkgData (version 3) bundle"); + wbEncVersion = 3; + fullLen = wb_build_encrypted(wbEncCorpus, (word32)sizeof(wbEncCorpus)); + if (fullLen > 32) { + wb_mutate(wb_encrypted_call, wbEncCorpus, fullLen, 2); + wb_truncate(wb_encrypted_call, wbEncCorpus, fullLen, 4); + } + wbEncVersion = 0; +} +#else +static void wb_encrypted_chains(void) +{ + WB_NOTE("no EncryptedData/AES; EncryptedData sweep skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 5: KEKRI and PWRI recipient decode walks + * ------------------------------------------------------------------------- */ +#if !defined(NO_AES) && defined(HAVE_AES_KEYWRAP) +static const byte wbKek[] = { + 0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f,0x30, + 0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x3b,0x3c,0x3d,0x3e,0x3f,0x40 +}; +static const byte wbKeyId[] = { 0x01, 0x02, 0x03, 0x04 }; +static byte wbKekriCorpus[WB_SCRATCH_SZ]; + +static word32 wb_build_kekri(byte* out, word32 outSz) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + byte data[] = "kekri mutation corpus payload 0123456789"; + static byte otherOid[] = { 0x06, 0x03, 0x55, 0x04, 0x03 }; + static byte other[] = { 0x04, 0x02, 0x77, 0x88 }; + int sz = 0; + + if (p == NULL) + return 0; + if (wc_PKCS7_Init(p, NULL, INVALID_DEVID) == 0) { + p->content = data; + p->contentSz = (word32)sizeof(data); + p->contentOID = DATA; + p->encryptOID = AES256CBCb; + p->rng = wbRngOk ? &wbRng : NULL; + if (wc_PKCS7_AddRecipient_KEKRI(p, AES256_WRAP, (byte*)wbKek, + (word32)sizeof(wbKek), (byte*)wbKeyId, (word32)sizeof(wbKeyId), + NULL, otherOid, (word32)sizeof(otherOid), other, + (word32)sizeof(other), 0) > 0) { + sz = wc_PKCS7_EncodeEnvelopedData(p, out, outSz); + } + } + wc_PKCS7_Free(p); + return (sz > 0) ? (word32)sz : 0; +} + +static void wb_kekri_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + + if (p == NULL) + return; + if (wc_PKCS7_Init(p, NULL, INVALID_DEVID) == 0 && + wc_PKCS7_SetKey(p, (byte*)wbKek, (word32)sizeof(wbKek)) == 0) { + (void)wc_PKCS7_DecodeEnvelopedData(p, buf, len, wbOut, + (word32)sizeof(wbOut)); + } + wc_PKCS7_Free(p); +} + +static void wb_kekri_chains(void) +{ + word32 fullLen; + + WB_NOTE("wc_PKCS7_DecryptKekri(): dense mutation sweep over a self-built" + " KEKRI EnvelopedData with an OtherKeyAttribute"); + fullLen = wb_build_kekri(wbKekriCorpus, (word32)sizeof(wbKekriCorpus)); + WB_CHECK(fullLen > 32, "self-built KEKRI EnvelopedData encoded"); + if (fullLen > 32) { + wb_mutate(wb_kekri_call, wbKekriCorpus, fullLen, 1); + wb_truncate(wb_kekri_call, wbKekriCorpus, fullLen, 1); + } +} +#else +static void wb_kekri_chains(void) +{ + WB_NOTE("no AES key wrap; KEKRI sweep skipped"); +} +#endif + +#if !defined(NO_AES) && defined(HAVE_AES_KEYWRAP) && !defined(NO_PWDBASED) +static const byte wbPass[] = "mutation-corpus-password"; +static byte wbPwriCorpus[WB_SCRATCH_SZ]; + +static word32 wb_build_pwri(byte* out, word32 outSz) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + byte data[] = "pwri mutation corpus payload 0123456789"; + static byte salt[8] = { 0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88 }; + int sz = 0; + + if (p == NULL) + return 0; + if (wc_PKCS7_Init(p, NULL, INVALID_DEVID) == 0) { + p->content = data; + p->contentSz = (word32)sizeof(data); + p->contentOID = DATA; + p->encryptOID = AES256CBCb; + p->rng = wbRngOk ? &wbRng : NULL; + /* small iteration count: the mutation sweep below re-derives the KEK + * on every vector */ + if (wc_PKCS7_AddRecipient_PWRI(p, (byte*)wbPass, + (word32)sizeof(wbPass) - 1, salt, (word32)sizeof(salt), + PBKDF2_OID, WC_SHA256, 5, AES256CBCb, 0) >= 0) { + sz = wc_PKCS7_EncodeEnvelopedData(p, out, outSz); + } + } + wc_PKCS7_Free(p); + return (sz > 0) ? (word32)sz : 0; +} + +static void wb_pwri_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + + if (p == NULL) + return; + if (wc_PKCS7_Init(p, NULL, INVALID_DEVID) == 0) { + (void)wc_PKCS7_SetPassword(p, (byte*)wbPass, + (word32)sizeof(wbPass) - 1); + (void)wc_PKCS7_DecodeEnvelopedData(p, buf, len, wbOut, + (word32)sizeof(wbOut)); + } + wc_PKCS7_Free(p); +} + +static void wb_pwri_chains(void) +{ + word32 fullLen; + + WB_NOTE("wc_PKCS7_DecryptPwri(): dense mutation sweep over a self-built" + " PWRI EnvelopedData"); + fullLen = wb_build_pwri(wbPwriCorpus, (word32)sizeof(wbPwriCorpus)); + WB_CHECK(fullLen > 32, "self-built PWRI EnvelopedData encoded"); + if (fullLen > 32) { + wb_mutate(wb_pwri_call, wbPwriCorpus, fullLen, 1); + wb_truncate(wb_pwri_call, wbPwriCorpus, fullLen, 2); + } +} +#else +static void wb_pwri_chains(void) +{ + WB_NOTE("no PWRI prerequisites; PWRI sweep skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 6: wc_PKCS7_GetEnvelopedDataKariRid() + * ------------------------------------------------------------------------- */ +static void wb_kari_rid_call(byte* buf, word32 len) +{ + byte out[128]; + word32 outSz = (word32)sizeof(out); + + (void)wc_PKCS7_GetEnvelopedDataKariRid(buf, len, out, &outSz); +} + +static void wb_kari_rid_chains(void) +{ + word32 fullLen; + + WB_NOTE("wc_PKCS7_GetEnvelopedDataKariRid(): dense mutation sweep," + " kari-keyid-cms.msg"); + fullLen = wb_load_file("./certs/test/kari-keyid-cms.msg", wbScratch, + sizeof(wbScratch)); + if (fullLen > 0) { + wb_mutate(wb_kari_rid_call, wbScratch, fullLen, 1); + wb_truncate(wb_kari_rid_call, wbScratch, fullLen, 1); + } +} + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("pkcs7.c white-box MC/DC dense-mutation supplement\n"); + + if (wc_InitRng(&wbRng) == 0) + wbRngOk = 1; + + wb_verify_chains(); + wb_signed_variant_chains(); + wb_enveloped_chains(); + wb_auth_chains(); + wb_encrypted_chains(); + wb_kekri_chains(); + wb_pwri_chains(); + wb_kari_rid_chains(); + + if (wbRngOk) + wc_FreeRng(&wbRng); + + printf("done (%s)\n", wb_fail ? "with failures" : "ok"); + /* Always return 0: a nonzero exit discards this variant's coverage + * entirely in the campaign harness. */ + (void)wb_fail; + return 0; +} diff --git a/tests/unit-mcdc/test_pkcs7_whitebox.c b/tests/unit-mcdc/test_pkcs7_whitebox.c index 89706702fed..8805fee507c 100644 --- a/tests/unit-mcdc/test_pkcs7_whitebox.c +++ b/tests/unit-mcdc/test_pkcs7_whitebox.c @@ -1918,6 +1918,11 @@ static void wb_public_arg_guards(void) byte out[4096]; byte salt[8]; + /* wc_PKCS7_Init() reads pkcs7->isDynamic BEFORE it zeroes the struct and + * writes the value back, so an unzeroed stack fixture whose garbage bit + * happens to be set makes the later wc_PKCS7_Free() release a stack + * address. Zero it first. */ + XMEMSET(&pkcs7, 0, sizeof(pkcs7)); XMEMSET(key, 0x0b, sizeof(key)); XMEMSET(content, 0x0c, sizeof(content)); XMEMSET(out, 0, sizeof(out)); From 878a8afc8e0b2083c9944bd8bf4dd7b250a95319 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 19:28:29 +0200 Subject: [PATCH 14/20] tests: drive the SP host-backend crafted entry points --- tests/unit-mcdc/test_sp_c32_whitebox.c | 9 + tests/unit-mcdc/test_sp_c64_whitebox.c | 9 + tests/unit-mcdc/test_sp_crafted_common.h | 545 ++++++++++++++++++++++ tests/unit-mcdc/test_sp_x86_64_whitebox.c | 13 + 4 files changed, 576 insertions(+) create mode 100644 tests/unit-mcdc/test_sp_crafted_common.h diff --git a/tests/unit-mcdc/test_sp_c32_whitebox.c b/tests/unit-mcdc/test_sp_c32_whitebox.c index 9fd818e48f6..f2fd0367715 100644 --- a/tests/unit-mcdc/test_sp_c32_whitebox.c +++ b/tests/unit-mcdc/test_sp_c32_whitebox.c @@ -123,6 +123,14 @@ static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) +/* Crafted-input driver shared with the other two SP host-backend + * white-boxes: the SP entry points nothing in the wc_* API reaches on + * this configuration, plus the guards that only unlock once an earlier + * step has SUCCEEDED (a real key through sp_ecc_check_key_, a + * failing sp_ecc_verify_, an infinity verification point). See its + * header comment. */ +#include "test_sp_crafted_common.h" + #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) @@ -1211,6 +1219,7 @@ int main(void) wb_run_gap_256(); wb_run_gap_384(); wb_run_gap_521(); + wb_spc_all(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); #else diff --git a/tests/unit-mcdc/test_sp_c64_whitebox.c b/tests/unit-mcdc/test_sp_c64_whitebox.c index 37d2eb294ad..2de781e2727 100644 --- a/tests/unit-mcdc/test_sp_c64_whitebox.c +++ b/tests/unit-mcdc/test_sp_c64_whitebox.c @@ -133,6 +133,14 @@ static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) +/* Crafted-input driver shared with the other two SP host-backend + * white-boxes: the SP entry points nothing in the wc_* API reaches on + * this configuration, plus the guards that only unlock once an earlier + * step has SUCCEEDED (a real key through sp_ecc_check_key_, a + * failing sp_ecc_verify_, an infinity verification point). See its + * header comment. */ +#include "test_sp_crafted_common.h" + #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) @@ -882,6 +890,7 @@ int main(void) wb_run_dh(); wb_run_mulmod_add_all(); wb_run_point_specials_all(); + wb_spc_all(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); #else diff --git a/tests/unit-mcdc/test_sp_crafted_common.h b/tests/unit-mcdc/test_sp_crafted_common.h new file mode 100644 index 00000000000..9b04171b580 --- /dev/null +++ b/tests/unit-mcdc/test_sp_crafted_common.h @@ -0,0 +1,545 @@ +/* test_sp_crafted_common.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Crafted-input driver shared by the three SP host-backend white-boxes + * (sp_x86_64.c / sp_c64.c / sp_c32.c). + * + * WHY A SHARED HEADER + * ------------------- + * The three backends are three implementations of ONE API: every public + * entry point in them has the same name and the same signature + * (sp_ecc_mulmod_256, sp_ecc_check_key_384, sp_DhExp_2048, ...). Only the + * internal digit-level helpers differ (sp_256_iszero_4 vs _5 vs _9), and + * nothing here touches those. So one body, included after the .c under + * test, drives all three. + * + * WHAT IT ADDS OVER THE EXISTING DRIVERS + * -------------------------------------- + * The existing white-boxes drive the backends through the public wc_* + * API plus a few direct calls. That leaves whole functions never entered + * and, inside the ones that are entered, leaves the guards that sit + * BEHIND a successful earlier step unreached. Concretely: + * + * 1. Entry points nothing in the wc_* API reaches on this configuration: + * sp_ecc_mulmod_, sp_ecc_mulmod_base_, sp_ecc_mulmod_base_add_ + * and sp_ecc_uncompress_. Each carries a cpuid dispatch (on the asm + * backend) that cannot be measured at all until the function runs. + * + * 2. sp_ecc_check_key_ past its argument checks. The earlier drivers + * only ever handed it deliberately invalid coordinates -- (0,0), an + * oversized ordinate, the field modulus -- every one of which fails + * before "Check point is on curve". Everything after that point (the + * order-multiply dispatch, the "result is infinity" test and the + * "private key matches public point" test) therefore never ran. A REAL + * (pub, priv) pair reaches all of it; a real pub with a WRONG priv + * gives the mismatch test its other side. + * + * 3. sp_ecc_verify_ on a signature that does NOT verify. Every earlier + * verify succeeded, so `*res` was always 1 and the "reload r, add the + * order and compare again" recovery block was dead. Two failing + * verifies are used, chosen so the recovery block's second operand + * goes both ways: r == 1 makes r + order fit under the prime (compare + * < 0, block entered), while a full-width r from a real signature + * overflows the addition (carry != 0, compare left at 0, block + * skipped). + * + * 4. sp_ecc_verify_ where the verification point is the point at + * infinity. u1 = e/s and u2 = r/s, so an all-zero hash forces u1 == 0 + * ([0]G == infinity, p1->z == 0) and r == 0 forces u2 == 0 + * (p2->z == 0). Both are ordinary numbers to pass in, and both are + * the only way those two `sp__iszero_(p?->z)` guards go true. + * + * 5. sp_ecc_sign_ with a caller-supplied k. The `km == NULL || + * mp_iszero(km)` guard is short-circuited by every wc_* caller, which + * always passes NULL; passing a zero k and then a non-zero k gives the + * second operand both of its vectors. + * + * 6. sp_ModExp_ / sp_DhExp_ / sp_RsaPublic_ / sp_RsaPrivate_ + * argument-range checks, and the two data-dependent shapes inside + * sp_DhExp_: the `base == 2 && top word of modulus is all ones` + * fast path (driven with base 3, and with a modulus whose top word has + * a bit cleared), and the leading-zero trim loop over the output + * (driven with base 0, whose exponentiation result is zero, so the + * loop runs the full width instead of stopping at the first byte). + * + * Nothing here is a known-answer test: correctness of the arithmetic is + * the job of the ordinary wolfCrypt suite. Return values are discarded -- + * several calls are EXPECTED to fail, that is the point -- and the bar + * every call has to clear is only "completes without crashing". + * + * The including TU must have already included the wolfCrypt .c under test + * (so the file-static curve constants are visible) and ecc.h/dh.h/rsa.h. + */ + +#ifndef TEST_SP_CRAFTED_COMMON_H +#define TEST_SP_CRAFTED_COMMON_H + +#include +#include +#include + +#include + +#define WB_SPC_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +/* Fixed message digest. Its value is irrelevant; only its width matters. */ +static const byte wb_spc_digest[32] = { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f +}; +/* An all-zero digest makes u1 = e/s == 0 in the verify point calculation, + * i.e. [0]G, the point at infinity. */ +static const byte wb_spc_zdigest[32] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* ======================================================================= * + * ECC: one driver per compiled-in curve, generated from the curve size. + * ======================================================================= */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) + +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + #define WB_SPC_HAVE_CHECK_KEY +#endif + +#define WB_SPC_DEFINE_CURVE(BITS, SZ, CURVE_ID) \ +static void wb_spc_ecc_##BITS(void) \ +{ \ + ecc_key key; \ + WC_RNG rng; \ + ecc_point* rp = NULL; \ + mp_int k; \ + mp_int one; \ + mp_int zero; \ + mp_int modv; \ + mp_int rmv; \ + mp_int smv; \ + mp_int kmv; \ + mp_int yv; \ + int res = 0; \ + int inMont; \ + int map; \ + int okKey = 0; \ + \ + XMEMSET(&key, 0, sizeof(key)); \ + XMEMSET(&rng, 0, sizeof(rng)); \ + \ + if (wc_InitRng(&rng) != 0) { \ + WB_SPC_NOTE("wc_InitRng failed (crafted " #BITS ")"); \ + return; \ + } \ + if (wc_ecc_init(&key) != 0) { \ + WB_SPC_NOTE("wc_ecc_init failed (crafted " #BITS ")"); \ + wc_FreeRng(&rng); \ + return; \ + } \ + if (mp_init_multi(&k, &one, &zero, &modv, &rmv, &smv) != MP_OKAY) { \ + WB_SPC_NOTE("mp_init_multi failed (crafted " #BITS ")"); \ + wc_ecc_free(&key); \ + wc_FreeRng(&rng); \ + return; \ + } \ + if (mp_init_multi(&kmv, &yv, NULL, NULL, NULL, NULL) != MP_OKAY) { \ + WB_SPC_NOTE("mp_init_multi failed (crafted " #BITS ")"); \ + mp_clear(&k); mp_clear(&one); mp_clear(&zero); \ + mp_clear(&modv); mp_clear(&rmv); mp_clear(&smv); \ + wc_ecc_free(&key); \ + wc_FreeRng(&rng); \ + return; \ + } \ + \ + (void)mp_set(&k, 5); \ + (void)mp_set(&one, 1); \ + mp_zero(&zero); \ + (void)sp_##BITS##_to_mp(p##BITS##_mod, &modv); \ + \ + if (wc_ecc_make_key_ex(&rng, SZ, &key, CURVE_ID) == 0) { \ + okKey = 1; \ + } \ + else { \ + WB_SPC_NOTE("wc_ecc_make_key_ex failed (crafted " #BITS ")"); \ + } \ + \ + rp = wc_ecc_new_point(); \ + if (okKey && (rp != NULL)) { \ + /* Entry points the wc_* API never takes on this configuration. */ \ + (void)sp_ecc_mulmod_##BITS(&k, &key.pubkey, rp, 1, NULL); \ + (void)sp_ecc_mulmod_##BITS(&k, &key.pubkey, rp, 0, NULL); \ + (void)sp_ecc_mulmod_base_##BITS(&k, rp, 1, NULL); \ + (void)sp_ecc_mulmod_base_##BITS(&k, rp, 0, NULL); \ + for (inMont = 0; inMont <= 1; inMont++) { \ + for (map = 0; map <= 1; map++) { \ + (void)sp_ecc_mulmod_base_add_##BITS(&k, &key.pubkey, \ + inMont, rp, map, NULL); \ + (void)sp_ecc_mulmod_add_##BITS(&k, &key.pubkey, \ + &key.pubkey, inMont, rp, map, NULL); \ + } \ + } \ + } \ + if (rp != NULL) { \ + wc_ecc_del_point(rp); \ + } \ + \ + if (okKey) { \ + /* A real point, so the on-curve test passes and everything \ + * behind it runs for the first time. */ \ + (void)sp_ecc_is_point_##BITS(key.pubkey.x, key.pubkey.y); \ + } \ + \ + WB_SPC_CHECK_KEY_BODY(BITS) \ + WB_SPC_UNCOMPRESS_BODY(BITS) \ + WB_SPC_SIGNVERIFY_BODY(BITS) \ + \ + mp_clear(&yv); \ + mp_clear(&kmv); \ + mp_clear(&smv); \ + mp_clear(&rmv); \ + mp_clear(&modv); \ + mp_clear(&zero); \ + mp_clear(&one); \ + mp_clear(&k); \ + wc_ecc_free(&key); \ + wc_FreeRng(&rng); \ + WB_SPC_NOTE("crafted SP entry points exercised (P-" #BITS ")"); \ +} + +#ifdef WB_SPC_HAVE_CHECK_KEY +/* x == 0 with a non-zero y gives the point-at-infinity test its second + * operand's false side; the real (pub, priv) pair walks the whole + * function; the real pub with k == 5 as the "private key" makes the + * final "private key matches public point" comparison go true. */ +#define WB_SPC_CHECK_KEY_BODY(BITS) \ + if (okKey) { \ + (void)sp_ecc_check_key_##BITS(key.pubkey.x, key.pubkey.y, \ + ecc_get_k(&key), NULL); \ + (void)sp_ecc_check_key_##BITS(key.pubkey.x, key.pubkey.y, \ + NULL, NULL); \ + (void)sp_ecc_check_key_##BITS(key.pubkey.x, key.pubkey.y, \ + &k, NULL); \ + (void)sp_ecc_check_key_##BITS(&zero, key.pubkey.y, NULL, NULL); \ + (void)sp_ecc_check_key_##BITS(key.pubkey.x, &zero, NULL, NULL); \ + (void)sp_ecc_check_key_##BITS(key.pubkey.x, &modv, NULL, NULL); \ + (void)sp_ecc_check_key_##BITS(&modv, key.pubkey.y, NULL, NULL); \ + /* (order - d) * G is the negation of the public point: same x, \ + * different y. That is the only way the final comparison's \ + * second operand goes true with its first one false. kmv is \ + * scratch here; the sign/verify body below resets it. */ \ + if ((sp_##BITS##_to_mp(p##BITS##_order, &kmv) == MP_OKAY) && \ + (mp_sub(&kmv, ecc_get_k(&key), &yv) == MP_OKAY)) { \ + (void)sp_ecc_check_key_##BITS(key.pubkey.x, key.pubkey.y, \ + &yv, NULL); \ + } \ + } +#else +#define WB_SPC_CHECK_KEY_BODY(BITS) /* not compiled in this config */ +#endif + +#ifdef HAVE_COMP_KEY +#define WB_SPC_UNCOMPRESS_BODY(BITS) \ + if (okKey) { \ + (void)sp_ecc_uncompress_##BITS(key.pubkey.x, 0, &yv); \ + (void)sp_ecc_uncompress_##BITS(key.pubkey.x, 1, &yv); \ + } +#else +#define WB_SPC_UNCOMPRESS_BODY(BITS) /* needs HAVE_COMP_KEY */ +#endif + +#if defined(HAVE_ECC_SIGN) && defined(HAVE_ECC_VERIFY) +#define WB_SPC_SIGNVERIFY_BODY(BITS) \ + if (okKey) { \ + /* km supplied and zero, then km supplied and non-zero: the only \ + * two vectors of the `km == NULL || mp_iszero(km)` second \ + * operand, which every wc_* caller short-circuits with NULL. */ \ + mp_zero(&kmv); \ + (void)sp_ecc_sign_##BITS(wb_spc_digest, 32, &rng, \ + ecc_get_k(&key), &rmv, &smv, &kmv, NULL); \ + (void)mp_set(&kmv, 7); \ + (void)sp_ecc_sign_##BITS(wb_spc_digest, 32, &rng, \ + ecc_get_k(&key), &rmv, &smv, &kmv, NULL); \ + \ + if (sp_ecc_sign_##BITS(wb_spc_digest, 32, &rng, ecc_get_k(&key), \ + &rmv, &smv, NULL, NULL) == 0) { \ + /* Valid. */ \ + (void)sp_ecc_verify_##BITS(wb_spc_digest, 32, key.pubkey.x, \ + key.pubkey.y, &one, &rmv, &smv, &res, NULL); \ + /* Invalid, full-width r: r + order overflows, so the \ + * recovery block's compare is left alone and skips it. */ \ + (void)sp_ecc_verify_##BITS(wb_spc_zdigest, 32, key.pubkey.x, \ + key.pubkey.y, &one, &rmv, &smv, &res, NULL); \ + /* Invalid, r == 1: r + order stays under the prime, so the \ + * recovery block runs. */ \ + (void)sp_ecc_verify_##BITS(wb_spc_digest, 32, key.pubkey.x, \ + key.pubkey.y, &one, &one, &smv, &res, NULL); \ + /* r == 0 => u2 == 0 => [0]Q is the point at infinity. */ \ + (void)sp_ecc_verify_##BITS(wb_spc_digest, 32, key.pubkey.x, \ + key.pubkey.y, &one, &zero, &smv, &res, NULL); \ + /* e == 0 => u1 == 0 => [0]G is the point at infinity. */ \ + (void)sp_ecc_verify_##BITS(wb_spc_zdigest, 32, key.pubkey.x, \ + key.pubkey.y, &one, &one, &smv, &res, NULL); \ + } \ + } +#else +#define WB_SPC_SIGNVERIFY_BODY(BITS) /* needs HAVE_ECC_SIGN/VERIFY */ +#endif + +#ifndef WOLFSSL_SP_NO_256 +WB_SPC_DEFINE_CURVE(256, 32, ECC_SECP256R1) +#endif +#ifdef WOLFSSL_SP_384 +WB_SPC_DEFINE_CURVE(384, 48, ECC_SECP384R1) +#endif +#ifdef WOLFSSL_SP_521 +WB_SPC_DEFINE_CURVE(521, 66, ECC_SECP521R1) +#endif + +static void wb_spc_ecc_all(void) +{ +#ifndef WOLFSSL_SP_NO_256 + wb_spc_ecc_256(); +#endif +#ifdef WOLFSSL_SP_384 + wb_spc_ecc_384(); +#endif +#ifdef WOLFSSL_SP_521 + wb_spc_ecc_521(); +#endif +} + +#else /* !(WOLFSSL_HAVE_SP_ECC && HAVE_ECC) */ +static void wb_spc_ecc_all(void) +{ + WB_SPC_NOTE("SP ECC not compiled; crafted ECC skipped"); +} +#endif + +/* ======================================================================= * + * RSA / DH / ModExp: argument-range guards and the two data-dependent + * shapes inside sp_DhExp_. + * ======================================================================= */ + +/* Scratch big enough for a 4096-bit modulus. */ +#define WB_SPC_MAXBYTES 512 + +/* Build an odd number of exactly `bits` bits whose TOP word is not all + * ones (bit `bits-2` cleared), so the sp_DhExp fast-path test + * `m[top] == (sp_digit)-1` goes false while the bit-count check still + * passes. */ +static int wb_spc_make_modulus(mp_int* m, int bits) +{ + byte buf[WB_SPC_MAXBYTES]; + int bytes = bits / 8; + int i; + + if ((bytes <= 0) || (bytes > (int)sizeof(buf))) { + return -1; + } + for (i = 0; i < bytes; i++) { + buf[i] = 0xFF; + } + buf[0] = 0xBF; /* top bit set (keeps the bit count), next + * bit cleared (top word is not all ones) */ + buf[bytes - 1] = 0xFD; /* odd */ + return mp_read_unsigned_bin(m, buf, (word32)bytes); +} + +#if defined(WOLFSSL_HAVE_SP_DH) && !defined(NO_DH) +/* sp_DhExp_ over a modulus built by wb_spc_make_modulus(): right bit + * width, odd, but its top word is NOT all ones. Base 2 then drives the + * fast-path test's last operand false, base 3 drives its middle operand + * false, and base 0 makes the exponentiation result zero so the output's + * leading-zero trim loop runs the full width instead of stopping at the + * first byte. The all-operands-true vector of that test comes from the + * ordinary named-FFDHE key agreement the drivers already run. */ +#define WB_SPC_DHEXP(BITS) \ +do { \ + mp_int b2; \ + mp_int b3; \ + mp_int b0; \ + mp_int mv; \ + byte out[WB_SPC_MAXBYTES]; \ + byte ex[8]; \ + word32 outLen; \ + \ + XMEMSET(ex, 0, sizeof(ex)); \ + ex[sizeof(ex) - 1] = 0x0b; \ + if (mp_init_multi(&b2, &b3, &b0, &mv, NULL, NULL) == MP_OKAY) { \ + (void)mp_set(&b2, 2); \ + (void)mp_set(&b3, 3); \ + mp_zero(&b0); \ + if (wb_spc_make_modulus(&mv, (BITS)) == MP_OKAY) { \ + outLen = (word32)((BITS) / 8); \ + (void)sp_DhExp_##BITS(&b2, ex, (word32)sizeof(ex), &mv, \ + out, &outLen); \ + outLen = (word32)((BITS) / 8); \ + (void)sp_DhExp_##BITS(&b3, ex, (word32)sizeof(ex), &mv, \ + out, &outLen); \ + outLen = (word32)((BITS) / 8); \ + (void)sp_DhExp_##BITS(&b0, ex, (word32)sizeof(ex), &mv, \ + out, &outLen); \ + } \ + mp_clear(&mv); \ + mp_clear(&b0); \ + mp_clear(&b3); \ + mp_clear(&b2); \ + } \ +} while (0) +#endif /* WOLFSSL_HAVE_SP_DH && !NO_DH */ + +/* sp_ModExp_: an odd modulus of exactly the right width is all its + * argument checks want, and reaching past them is what puts its cpuid + * dispatch on the measured path. */ +#define WB_SPC_MODEXP(BITS) \ +do { \ + mp_int b; \ + mp_int e; \ + mp_int m; \ + mp_int r; \ + \ + if (mp_init_multi(&b, &e, &m, &r, NULL, NULL) == MP_OKAY) { \ + (void)mp_set(&b, 3); \ + (void)mp_set(&e, 65537); \ + if (wb_spc_make_modulus(&m, (BITS)) == MP_OKAY) { \ + (void)sp_ModExp_##BITS(&b, &e, &m, &r); \ + } \ + mp_clear(&r); \ + mp_clear(&m); \ + mp_clear(&e); \ + mp_clear(&b); \ + } \ +} while (0) + +#if defined(WOLFSSL_HAVE_SP_RSA) && !defined(NO_RSA) +/* sp_RsaPublic_ / sp_RsaPrivate_ argument-range guards. Each call + * makes exactly one operand of the range chain true, with the earlier + * ones false, which is what the chain's MC/DC pairs need. The output + * buffer is deliberately large enough that the `*outLen` guard ahead of + * the chain does not fire and hide it. */ +#define WB_SPC_RSA_ARGS(BITS, WIDTH) \ +do { \ + mp_int bigE; \ + mp_int smallE; \ + mp_int badM; \ + byte in[8]; \ + byte ebuf[16]; \ + byte out[WB_SPC_MAXBYTES]; \ + word32 outLen; \ + \ + XMEMSET(in, 0x11, sizeof(in)); \ + XMEMSET(ebuf, 0xAA, sizeof(ebuf)); \ + if (mp_init_multi(&bigE, &smallE, &badM, NULL, NULL, NULL) \ + == MP_OKAY) { \ + /* 128 bits, so the exponent-width operand goes true. */ \ + if (mp_read_unsigned_bin(&bigE, ebuf, (word32)sizeof(ebuf)) \ + == MP_OKAY) { \ + outLen = (word32)(WIDTH); \ + (void)sp_RsaPublic_##BITS(in, (word32)sizeof(in), &bigE, \ + &bigE, out, &outLen); \ + } \ + (void)mp_set(&smallE, 65537); \ + (void)mp_set(&badM, 3); \ + /* inLen past the modulus width: middle operand true. */ \ + outLen = (word32)(WIDTH); \ + (void)sp_RsaPublic_##BITS(in, (word32)(WIDTH) + 1, &smallE, \ + &badM, out, &outLen); \ + /* Everything in range but the modulus the wrong width. */ \ + outLen = (word32)(WIDTH); \ + (void)sp_RsaPublic_##BITS(in, (word32)sizeof(in), &smallE, \ + &badM, out, &outLen); \ + \ + outLen = (word32)(WIDTH); \ + (void)sp_RsaPrivate_##BITS(in, (word32)(WIDTH) + 1, &smallE, \ + &smallE, &smallE, &smallE, &smallE, &smallE, &badM, out, \ + &outLen); \ + outLen = (word32)(WIDTH); \ + (void)sp_RsaPrivate_##BITS(in, (word32)sizeof(in), &smallE, \ + &smallE, &smallE, &smallE, &smallE, &smallE, &badM, out, \ + &outLen); \ + \ + mp_clear(&badM); \ + mp_clear(&smallE); \ + mp_clear(&bigE); \ + } \ +} while (0) +#endif /* WOLFSSL_HAVE_SP_RSA && !NO_RSA */ + +static void wb_spc_bigint_all(void) +{ +#if defined(WOLFSSL_HAVE_SP_DH) && !defined(NO_DH) + #ifndef WOLFSSL_SP_NO_2048 + WB_SPC_DHEXP(2048); + #endif + #ifndef WOLFSSL_SP_NO_3072 + WB_SPC_DHEXP(3072); + #endif + #ifdef WOLFSSL_SP_4096 + WB_SPC_DHEXP(4096); + #endif +#endif + +#if defined(WOLFSSL_HAVE_SP_DH) || \ + (defined(WOLFSSL_HAVE_SP_RSA) && !defined(WOLFSSL_RSA_PUBLIC_ONLY)) + #ifndef WOLFSSL_SP_NO_2048 + WB_SPC_MODEXP(1024); + WB_SPC_MODEXP(2048); + #endif + #ifndef WOLFSSL_SP_NO_3072 + WB_SPC_MODEXP(1536); + WB_SPC_MODEXP(3072); + #endif + #ifdef WOLFSSL_SP_4096 + WB_SPC_MODEXP(4096); + #endif +#endif + +#if defined(WOLFSSL_HAVE_SP_RSA) && !defined(NO_RSA) + #ifndef WOLFSSL_SP_NO_2048 + WB_SPC_RSA_ARGS(2048, 256); + #endif + #ifndef WOLFSSL_SP_NO_3072 + WB_SPC_RSA_ARGS(3072, 384); + #endif + #ifdef WOLFSSL_SP_4096 + WB_SPC_RSA_ARGS(4096, 512); + #endif +#endif + WB_SPC_NOTE("crafted ModExp/DhExp/RSA argument guards exercised"); +} + +static void wb_spc_all(void) +{ + /* Referenced unconditionally: which of these the preprocessor leaves + * with a live use depends on the variant, and an unused static is a + * warning this campaign's builds treat as noise to be avoided. */ + (void)wb_spc_digest; + (void)wb_spc_zdigest; + (void)wb_spc_make_modulus; + + wb_spc_ecc_all(); + wb_spc_bigint_all(); +} + +#endif /* TEST_SP_CRAFTED_COMMON_H */ diff --git a/tests/unit-mcdc/test_sp_x86_64_whitebox.c b/tests/unit-mcdc/test_sp_x86_64_whitebox.c index 14c9ffa4a53..81d184edf85 100644 --- a/tests/unit-mcdc/test_sp_x86_64_whitebox.c +++ b/tests/unit-mcdc/test_sp_x86_64_whitebox.c @@ -201,6 +201,13 @@ static int wb_intr_ret = 0; static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) +/* Crafted-input driver shared with the sp_c64.c/sp_c32.c white-boxes: the + * SP entry points nothing in the wc_* API reaches on this configuration, + * plus the guards that only unlock once an earlier step has SUCCEEDED (a + * real key through sp_ecc_check_key_, a failing sp_ecc_verify_, an + * infinity verification point). See its header comment. */ +#include "test_sp_crafted_common.h" + #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) @@ -1952,6 +1959,7 @@ int main(void) wb_run_dh(); wb_run_dispatch(); wb_run_crafted(); + wb_spc_all(); cpuid_select_flags(real & ~(cpuid_flags_t)CPUID_BMI2); wb_run_ecc(); @@ -1959,6 +1967,7 @@ int main(void) wb_run_dh(); wb_run_dispatch(); wb_run_crafted(); + wb_spc_all(); cpuid_select_flags(real & ~(cpuid_flags_t)CPUID_ADX); wb_run_ecc(); @@ -1966,6 +1975,7 @@ int main(void) wb_run_dh(); wb_run_dispatch(); wb_run_crafted(); + wb_spc_all(); cpuid_select_flags(real & ~(cpuid_flags_t)CPUID_MOVBE); wb_run_ecc(); @@ -1973,6 +1983,7 @@ int main(void) wb_run_dh(); wb_run_dispatch(); wb_run_crafted(); + wb_spc_all(); /* AVX2 is the third operand of the four-operand chains; clearing it * with BMI2 and ADX left on is that operand's own flip. */ @@ -1982,6 +1993,7 @@ int main(void) wb_run_dh(); wb_run_dispatch(); wb_run_crafted(); + wb_spc_all(); /* Fourth operand: every feature present but the vector-register save * refused, so each chain falls through on its last condition. */ @@ -1992,6 +2004,7 @@ int main(void) wb_run_dh(); wb_run_dispatch(); wb_run_crafted(); + wb_spc_all(); wb_intr_ret = 0; wb_run_rsa_free(); From cc8ce8541fd8abdf6c9fc90c3b7b668cdf29a4fa Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 19:55:34 +0200 Subject: [PATCH 15/20] tests: add asn certificate fixtures for the chain and serial guards --- tests/unit-mcdc/test_asn_cert_whitebox.c | 1029 ++++++++++++++++++++++ 1 file changed, 1029 insertions(+) diff --git a/tests/unit-mcdc/test_asn_cert_whitebox.c b/tests/unit-mcdc/test_asn_cert_whitebox.c index 2137cc25e98..cd305d65e64 100644 --- a/tests/unit-mcdc/test_asn_cert_whitebox.c +++ b/tests/unit-mcdc/test_asn_cert_whitebox.c @@ -1527,6 +1527,1033 @@ static void wb_parse_cert_relative_matrix(void) } #endif +/* ------------------------------------------------------------------------- * + * Section: manufactured certificate fixtures for the ParseCertRelative() + * decision chain. + * + * The matrix above sweeps verify-mode x cert-type x CA-presence over the + * certificates bundled in wolfssl/certs_test.h. That bundle only contains + * *well-formed, currently-valid, conventionally-shaped* certificates, so a + * whole family of ParseCertRelative() operands is pinned at one value no + * matter how the matrix is swept: + * + * - every bundled leaf carries a subjectKeyIdentifier, so the + * "recompute the SKID from the public key" gate is never entered; + * - every bundled serial number is non-zero, so the RFC 5280 4.1.2.2 + * zero-serial guard and its trust-anchor exemption are never evaluated; + * - no bundled non-CA certificate asserts keyCertSign, so the + * keyUsage/basicConstraints consistency guard never fires; + * - the issuer name of a bundled leaf always matches the subject name of + * its CA, so the "CA found by key id but the names disagree" arm is + * dead; + * - the bundle has no three-level chain with a *non-self-signed* + * intermediate, so the name-constraint ancestor walk always terminates + * on its first iteration; + * - every bundled certificate is valid *today*, so the notBefore / + * notAfter checks in DecodeCertInternal() only ever succeed. + * + * Rather than adding new fixture files, this section manufactures the + * missing shapes at run time with wolfSSL's own certificate generator + * (wc_InitCert / wc_MakeCert / wc_SignCert) using the bundled RSA and ECC + * *keys* as both subject and signing keys -- no key generation, so the + * whole section costs a few tens of milliseconds. The Cert fields that the + * generator exposes (serial, beforeDate/afterDate, skidSz, akid/akidSz, + * keyUsage, isCA, pathLen, and the issuer CertName) are exactly the knobs + * needed to hit each operand above, and everything stays regenerable. + * + * Signature verification of these fixtures is deliberately *not* asserted: + * several of them are intentionally inconsistent (an authority key id that + * points at a CA which did not sign them, an issuer name that does not + * match any CA), and the decisions of interest are all evaluated before + * ParseCertRelative() reaches its ConfirmSignature() block. Loading a CA + * into a WOLFSSL_CERT_MANAGER uses CA_TYPE, which by construction skips + * signature confirmation, so a deliberately mis-parented intermediate can + * still be installed as a trust store entry. + * ------------------------------------------------------------------------- */ +#if !defined(NO_CERTS) && !defined(WOLFCRYPT_ONLY) && \ + defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ + defined(WOLFSSL_ASN_TEMPLATE) && !defined(NO_RSA) && \ + !defined(NO_SHA256) && !defined(NO_ASN_TIME) && !defined(NO_SKID) && \ + defined(USE_CERT_BUFFERS_2048) + +#define WB_FIX_DER_SZ 4096 + +/* One manufactured certificate: its DER plus the subject key identifier the + * generator put in it, so a child fixture can point its AKID at it. */ +typedef struct WbFix { + byte der[WB_FIX_DER_SZ]; + int sz; + byte skid[CTC_MAX_SKID_SIZE]; + int skidSz; +} WbFix; + +/* Declarative description of a fixture. Anything left zero/NULL means + * "generator default", which for wc_InitCert() is: v3, random serial, + * self-signed, not a CA, no extensions beyond what is asked for here. */ +typedef struct WbSpec { + const char* cn; /* subject common name */ + const char* issuerCn; /* explicit issuer CN (no matching CA needed) */ + const WbFix* issuerFix; /* take the issuer name from this fixture */ + RsaKey* subjectKey; /* subject public key (RSA) */ + ecc_key* subjectEcc; /* ... or ECC */ + RsaKey* signKey; /* signing key (RSA) */ + ecc_key* signEcc; /* ... or ECC */ + int isCA; + int pathLen; /* < 0: do not emit a pathLenConstraint */ + int keyUsage; /* < 0: do not emit a keyUsage extension */ + int withSkid; /* emit subjectKeyIdentifier from the pub key */ + const byte* skid; /* ... or emit this literal key id instead */ + int skidSz; + const byte* akid; /* emit authorityKeyIdentifier (key id form) */ + int akidSz; + const char* notBefore; /* 13-char UTCTime body "YYMMDDHHMMSSZ" */ + const char* notAfter; /* NULL with notBefore set: only notBefore */ + int zeroSerial; /* emit serial number 0 (RFC-non-conforming) */ + int version; /* > 0: encode this raw version value */ +} WbSpec; + +static WC_RNG wbRng; +static int wbRngOk = 0; +static byte wbSerialCounter = 1; +static RsaKey wbKeyRoot; /* ca_key_der_2048 */ +static RsaKey wbKeyInter; /* client_key_der_2048 */ +static RsaKey wbKeyLeaf; /* server_key_der_2048 */ +static int wbKeysOk = 0; +#ifdef HAVE_ECC +static ecc_key wbKeyEcc; /* ecc_key_der_256 */ +static int wbEccOk = 0; +#endif + +static void wb_fill_name(CertName* name, const char* cn) +{ + XSTRNCPY(name->country, "US", CTC_NAME_SIZE); + name->countryEnc = CTC_PRINTABLE; + XSTRNCPY(name->state, "Oregon", CTC_NAME_SIZE); + name->stateEnc = CTC_UTF8; + XSTRNCPY(name->locality, "Portland", CTC_NAME_SIZE); + name->localityEnc = CTC_UTF8; + XSTRNCPY(name->org, "wolfSSL MCDC", CTC_NAME_SIZE); + name->orgEnc = CTC_UTF8; + XSTRNCPY(name->unit, "asn", CTC_NAME_SIZE); + name->unitEnc = CTC_UTF8; + XSTRNCPY(name->commonName, cn, CTC_NAME_SIZE); + name->commonNameEnc = CTC_UTF8; +} + +/* Build one fixture. Returns 0 on success. Never asserts on the *content* + * of the result beyond "the generator accepted it": the whole point of some + * of these shapes is that a strict parser will later reject them. */ +static int wb_make_fixture(WbFix* out, const WbSpec* spec) +{ + Cert* cert; + int ret; + int bodySz; + + XMEMSET(out, 0, sizeof(*out)); + cert = (Cert*)XMALLOC(sizeof(Cert), NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (cert == NULL) { + return MEMORY_E; + } + + ret = wc_InitCert(cert); + if (ret != 0) { + XFREE(cert, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + wb_fill_name(&cert->subject, spec->cn); + + if (spec->issuerFix != NULL) { + /* Issuer name lifted out of a previously generated certificate; + * this also clears the self-signed flag. */ + ret = wc_SetIssuerBuffer(cert, spec->issuerFix->der, + spec->issuerFix->sz); + } + else if (spec->issuerCn != NULL) { + /* Issuer name that need not correspond to any real certificate. */ + wb_fill_name(&cert->issuer, spec->issuerCn); + cert->selfSigned = 0; + } + + if (ret == 0) { + cert->isCA = spec->isCA; + if (spec->isCA) { + cert->basicConstSet = 1; + } + if (spec->pathLen >= 0) { + cert->pathLen = (byte)spec->pathLen; + cert->pathLenSet = 1; + cert->basicConstSet = 1; + } + if (spec->keyUsage >= 0) { + cert->keyUsage = (word16)spec->keyUsage; + } + cert->daysValid = 3650; + + if (spec->version > 0) { + /* The generator writes this straight into the version INTEGER + * with no range check of its own, which is what makes an + * out-of-range version reachable at all. */ + cert->version = spec->version; + } + + if (spec->signEcc != NULL) { + cert->sigType = CTC_SHA256wECDSA; + } + else { + cert->sigType = CTC_SHA256wRSA; + } + + if (spec->zeroSerial) { + XMEMSET(cert->serial, 0, sizeof(cert->serial)); + cert->serialSz = 1; + } + else { + /* Deterministic, positive, high-bit-clear serial. */ + XMEMSET(cert->serial, 0, sizeof(cert->serial)); + cert->serial[0] = 0x11; + cert->serial[1] = wbSerialCounter++; + cert->serialSz = 2; + } + + if (spec->notBefore != NULL) { + cert->beforeDate[0] = ASN_UTC_TIME; + cert->beforeDate[1] = ASN_UTC_TIME_SIZE - 1; + XMEMCPY(cert->beforeDate + 2, spec->notBefore, + ASN_UTC_TIME_SIZE - 1); + cert->beforeDateSz = ASN_UTC_TIME_SIZE + 1; + } + if (spec->notAfter != NULL) { + cert->afterDate[0] = ASN_UTC_TIME; + cert->afterDate[1] = ASN_UTC_TIME_SIZE - 1; + XMEMCPY(cert->afterDate + 2, spec->notAfter, + ASN_UTC_TIME_SIZE - 1); + cert->afterDateSz = ASN_UTC_TIME_SIZE + 1; + } + + if (spec->skid != NULL && spec->skidSz > 0) { + /* Literal key id: lets two certificates with different subject + * names advertise the SAME subjectKeyIdentifier, which the + * public-key-derived form can never produce. */ + XMEMCPY(cert->skid, spec->skid, (size_t)spec->skidSz); + cert->skidSz = spec->skidSz; + } + else if (spec->withSkid) { + ret = wc_SetSubjectKeyIdFromPublicKey(cert, spec->subjectKey, + spec->subjectEcc); + } + } + + if (ret == 0 && spec->akid != NULL && spec->akidSz > 0) { + XMEMCPY(cert->akid, spec->akid, (size_t)spec->akidSz); + cert->akidSz = spec->akidSz; +#ifdef WOLFSSL_AKID_NAME + cert->rawAkid = 0; +#endif + } + + if (ret == 0) { + ret = wc_MakeCert(cert, out->der, WB_FIX_DER_SZ, spec->subjectKey, + spec->subjectEcc, &wbRng); + if (ret > 0) { + ret = 0; + } + } + if (ret == 0) { + bodySz = cert->bodySz; + ret = wc_SignCert(bodySz, cert->sigType, out->der, WB_FIX_DER_SZ, + spec->signKey, spec->signEcc, &wbRng); + if (ret > 0) { + out->sz = ret; + ret = 0; + } + else if (ret == 0) { + ret = -1; + } + } + if (ret == 0 && (spec->withSkid || spec->skidSz > 0)) { + XMEMCPY(out->skid, cert->skid, sizeof(out->skid)); + out->skidSz = cert->skidSz; + } + + wc_SetCert_Free(cert); + XFREE(cert, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} + +/* Parse one fixture through the real entry point and throw the result away: + * only the decisions evaluated on the way matter here. A DecodedCert is + * ~4KB, so it lives on the heap for the small_stack variant's benefit. */ +static void wb_parse_one(const WbFix* fix, int type, int verify, + void* cm, Signer* extraCAList) +{ + DecodedCert* dc; + + if (fix == NULL || fix->sz <= 0) { + return; + } + dc = (DecodedCert*)XMALLOC(sizeof(DecodedCert), NULL, DYNAMIC_TYPE_DCERT); + if (dc == NULL) { + return; + } + wc_InitDecodedCert(dc, fix->der, (word32)fix->sz, NULL); + printf("PARSE sz=%d type=%d verify=%d -> %d\n", fix->sz, type, verify, + ParseCertRelative(dc, type, verify, cm, extraCAList)); + wc_FreeDecodedCert(dc); + XFREE(dc, NULL, DYNAMIC_TYPE_DCERT); +} + +/* Turn a fixture into a standalone Signer, the shape ParseCertRelative() + * accepts through its extraCAList argument (the certificate-status-request + * v2 path). FillSigner() takes ownership of the decoded public key and + * subject CN, so the DecodedCert can be released immediately after. */ +static Signer* wb_make_signer(const WbFix* fix) +{ + DecodedCert* dc; + DerBuffer* der = NULL; + Signer* signer; + int ret; + + if (fix == NULL || fix->sz <= 0) { + return NULL; + } + signer = MakeSigner(NULL); + if (signer == NULL) { + return NULL; + } + dc = (DecodedCert*)XMALLOC(sizeof(DecodedCert), NULL, DYNAMIC_TYPE_DCERT); + if (dc == NULL) { + FreeSigner(signer, NULL); + return NULL; + } + wc_InitDecodedCert(dc, fix->der, (word32)fix->sz, NULL); + ret = ParseCert(dc, CA_TYPE, NO_VERIFY, NULL); + if (ret == 0) { + ret = AllocDer(&der, (word32)fix->sz, CA_TYPE, NULL); + } + if (ret == 0) { + XMEMCPY(der->buffer, fix->der, (size_t)fix->sz); + ret = FillSigner(signer, dc, CA_TYPE, der); + } + FreeDer(&der); + wc_FreeDecodedCert(dc); + XFREE(dc, NULL, DYNAMIC_TYPE_DCERT); + if (ret != 0) { + FreeSigner(signer, NULL); + return NULL; + } + return signer; +} + +/* The fixture set. File-scope so the small_stack variant does not put ~80KB + * of certificate DER on the stack. */ +static WbFix wbRootA; /* self-signed CA, pathLen 1, keyCertSign */ +static WbFix wbRootADupRsa; /* same DN as wbRootA, different RSA key */ +#ifdef HAVE_ECC +static WbFix wbRootADupEcc; /* same DN as wbRootA, ECC key (size differs) */ +#endif +static WbFix wbInter; /* CA under wbRootA, AKID -> wbRootA */ +static WbFix wbInterNoKU; /* CA under wbRootA with no keyUsage extension */ +static WbFix wbInterMismatch;/* CA whose AKID -> wbRootA but issuer DN does not */ +static WbFix wbInterBadAkid; /* CA whose AKID matches nothing */ +static WbFix wbInterKuNoCS; /* CA under wbRootA, keyUsage WITHOUT certSign */ +static WbFix wbSkidTwin; /* CA advertising wbRootA's key id, other DN */ +static WbFix wbBadVersion; /* version INTEGER above the supported maximum */ +static WbFix wbLeafByInter; /* leaf under wbInter (3-level chain) */ +static WbFix wbLeafUnderMism;/* leaf under wbInterMismatch */ +static WbFix wbLeafNoSkid; /* leaf under wbRootA with no SKID extension */ +static WbFix wbLeafNoAkid; /* leaf under wbRootA with no AKID extension */ +static WbFix wbLeafBadAkid; /* leaf under wbRootA, AKID matches nothing */ +static WbFix wbLeafBadName; /* AKID -> wbRootA but issuer DN differs */ +static WbFix wbLeafKuCertSign; /* non-CA leaf asserting keyCertSign */ +static WbFix wbLeafKuNoCertSign;/* non-CA leaf, keyUsage without keyCertSign */ +static WbFix wbZeroSerialRoot; /* self-signed CA, serial 0 */ +static WbFix wbZeroSerialLeaf; /* leaf, serial 0 */ +static WbFix wbZeroSerialSubCA; /* CA under wbRootA, serial 0 */ +static WbFix wbExpiredLeaf; /* notAfter in the past */ +static WbFix wbFutureLeaf; /* notBefore in the future */ +static WbFix wbOnlyNotBefore; /* generator side: beforeDate set, after not */ +static WbFix wbNcX; /* CA "NC X" issued by "NC Y" */ +static WbFix wbNcY; /* CA "NC Y" issued by "NC X" (A->B->A) */ +static WbFix wbLeafNcX; /* leaf under "NC X" */ +static int wbFixturesOk = 0; + +static int wb_load_keys(void) +{ + word32 idx; + int ret; + + ret = wc_InitRng(&wbRng); + if (ret != 0) { + return ret; + } + wbRngOk = 1; + + ret = wc_InitRsaKey(&wbKeyRoot, NULL); + if (ret == 0) { + ret = wc_InitRsaKey(&wbKeyInter, NULL); + } + if (ret == 0) { + ret = wc_InitRsaKey(&wbKeyLeaf, NULL); + } + if (ret == 0) { + wbKeysOk = 1; + idx = 0; + ret = wc_RsaPrivateKeyDecode(ca_key_der_2048, &idx, &wbKeyRoot, + (word32)sizeof_ca_key_der_2048); + } + if (ret == 0) { + idx = 0; + ret = wc_RsaPrivateKeyDecode(client_key_der_2048, &idx, &wbKeyInter, + (word32)sizeof_client_key_der_2048); + } + if (ret == 0) { + idx = 0; + ret = wc_RsaPrivateKeyDecode(server_key_der_2048, &idx, &wbKeyLeaf, + (word32)sizeof_server_key_der_2048); + } +#ifdef HAVE_ECC + if (ret == 0 && wc_ecc_init(&wbKeyEcc) == 0) { + idx = 0; + if (wc_EccPrivateKeyDecode(ecc_key_der_256, &idx, &wbKeyEcc, + (word32)sizeof_ecc_key_der_256) == 0) { + wbEccOk = 1; + } + else { + wc_ecc_free(&wbKeyEcc); + } + } +#endif + return ret; +} + +static void wb_free_keys(void) +{ + if (wbKeysOk) { + wc_FreeRsaKey(&wbKeyRoot); + wc_FreeRsaKey(&wbKeyInter); + wc_FreeRsaKey(&wbKeyLeaf); + wbKeysOk = 0; + } +#ifdef HAVE_ECC + if (wbEccOk) { + wc_ecc_free(&wbKeyEcc); + wbEccOk = 0; + } +#endif + if (wbRngOk) { + wc_FreeRng(&wbRng); + wbRngOk = 0; + } +} + +#define WB_MK(dst, ...) do { \ + WbSpec _s; \ + XMEMSET(&_s, 0, sizeof(_s)); \ + _s.pathLen = -1; \ + _s.keyUsage = -1; \ + _s.subjectKey = &wbKeyLeaf; \ + _s.signKey = &wbKeyRoot; \ + __VA_ARGS__; \ + if (wb_make_fixture(&(dst), &_s) != 0) { \ + WB_CHECK(0, "manufacture " #dst); \ + wbFixturesOk = 0; \ + } \ + } while (0) + +static void wb_build_fixtures(void) +{ + byte bogusKid[KEYID_SIZE]; + + XMEMSET(bogusKid, 0xAA, sizeof(bogusKid)); + if (wb_load_keys() != 0) { + WB_CHECK(0, "load the bundled signing keys"); + return; + } + wbFixturesOk = 1; + + /* --- trust anchors ------------------------------------------------ */ + WB_MK(wbRootA, + _s.cn = "MCDC Root A"; _s.subjectKey = &wbKeyRoot; + _s.isCA = 1; _s.pathLen = 1; _s.withSkid = 1; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN | KEYUSE_CRL_SIGN); + + /* Same DN, different key of the same size: exercises the trust-anchor + * public-key comparison in the path-length block on its "same length, + * different bytes" arm. No AKID, so the CA lookup falls through to the + * name-based one and keeps the match. */ + WB_MK(wbRootADupRsa, + _s.cn = "MCDC Root A"; _s.subjectKey = &wbKeyInter; + _s.signKey = &wbKeyInter; + _s.isCA = 1; _s.withSkid = 1; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + +#ifdef HAVE_ECC + if (wbEccOk) { + /* Same DN again, but an ECC key: different public-key *length*. */ + WB_MK(wbRootADupEcc, + _s.cn = "MCDC Root A"; _s.subjectKey = NULL; + _s.subjectEcc = &wbKeyEcc; + _s.signKey = NULL; _s.signEcc = &wbKeyEcc; + _s.isCA = 1; _s.withSkid = 1; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + } +#endif + + /* --- intermediates ------------------------------------------------ */ + WB_MK(wbInter, + _s.cn = "MCDC Inter"; _s.issuerFix = &wbRootA; + _s.subjectKey = &wbKeyInter; + _s.isCA = 1; _s.withSkid = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN | KEYUSE_CRL_SIGN); + + WB_MK(wbInterNoKU, + _s.cn = "MCDC Inter NoKU"; _s.issuerFix = &wbRootA; + _s.subjectKey = &wbKeyInter; + _s.isCA = 1; _s.withSkid = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz); + + /* A CA that DOES carry a keyUsage extension but leaves keyCertSign + * clear. The pathLen block's trailing guard reads + * (!extKeyUsageSet || (extKeyUsage & keyCertSign) != 0) + * and every other fixture drives it through one of the two true arms: + * wbInterNoKU has no extension at all (first operand true) and wbInter + * asserts keyCertSign (second operand true). This is the only shape + * that makes BOTH operands false, which is what the pathLen guard + * needs to be shown independent of either. */ + WB_MK(wbInterKuNoCS, + _s.cn = "MCDC Inter NoCS"; _s.issuerFix = &wbRootA; + _s.subjectKey = &wbKeyInter; + _s.isCA = 1; _s.withSkid = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.keyUsage = KEYUSE_CRL_SIGN | KEYUSE_DIGITAL_SIG); + + /* A CA whose subjectKeyIdentifier is literally the root's, but whose + * subject name is not. Nothing derived from a public key can produce + * this collision, and it is the only way to drive the "key id matched + * but the name did not" operand of the extra-CA-list scan. */ + WB_MK(wbSkidTwin, + _s.cn = "MCDC Skid Twin"; _s.issuerCn = "MCDC Skid Twin"; + _s.subjectKey = &wbKeyLeaf; _s.signKey = &wbKeyLeaf; + _s.isCA = 1; + _s.skid = wbRootA.skid; _s.skidSz = wbRootA.skidSz; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + + /* A structurally well-formed certificate whose version INTEGER is above + * the highest X.509 version the decoder supports. Every bundled and + * generated certificate is version 3, so the decoder's version ceiling + * is otherwise only ever satisfied. */ + WB_MK(wbBadVersion, + _s.cn = "MCDC Bad Version"; _s.issuerFix = &wbRootA; + _s.withSkid = 1; _s.version = 4; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.keyUsage = KEYUSE_DIGITAL_SIG); + + /* AKID points at the root, but the issuer name does not: the ancestor + * walk's "key id hit, name mismatch" rejection. */ + WB_MK(wbInterMismatch, + _s.cn = "MCDC Inter M"; _s.issuerCn = "MCDC Nowhere"; + _s.subjectKey = &wbKeyLeaf; + _s.isCA = 1; _s.withSkid = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + + WB_MK(wbInterBadAkid, + _s.cn = "MCDC Inter B"; _s.issuerFix = &wbRootA; + _s.subjectKey = &wbKeyInter; + _s.isCA = 1; _s.withSkid = 1; + _s.akid = bogusKid; _s.akidSz = (int)sizeof(bogusKid); + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + + /* --- leaves ------------------------------------------------------- */ + WB_MK(wbLeafByInter, + _s.cn = "MCDC Leaf I"; _s.issuerFix = &wbInter; + _s.signKey = &wbKeyInter; _s.withSkid = 1; + _s.akid = wbInter.skid; _s.akidSz = wbInter.skidSz; + _s.keyUsage = KEYUSE_DIGITAL_SIG | KEYUSE_KEY_ENCIPHER); + + WB_MK(wbLeafUnderMism, + _s.cn = "MCDC Leaf M"; _s.issuerFix = &wbInterMismatch; + _s.signKey = &wbKeyLeaf; + _s.akid = wbInterMismatch.skid; _s.akidSz = wbInterMismatch.skidSz; + _s.keyUsage = KEYUSE_DIGITAL_SIG); + + /* No subjectKeyIdentifier: forces the SKID-from-public-key recompute. */ + WB_MK(wbLeafNoSkid, + _s.cn = "MCDC Leaf NoSkid"; _s.issuerFix = &wbRootA; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz); + + WB_MK(wbLeafNoAkid, + _s.cn = "MCDC Leaf NoAkid"; _s.issuerFix = &wbRootA; + _s.withSkid = 1; + _s.keyUsage = KEYUSE_DIGITAL_SIG); + + WB_MK(wbLeafBadAkid, + _s.cn = "MCDC Leaf BadAkid"; _s.issuerFix = &wbRootA; + _s.withSkid = 1; + _s.akid = bogusKid; _s.akidSz = (int)sizeof(bogusKid); + _s.keyUsage = KEYUSE_DIGITAL_SIG); + + WB_MK(wbLeafBadName, + _s.cn = "MCDC Leaf BadName"; _s.issuerCn = "MCDC Other Root"; + _s.withSkid = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.keyUsage = KEYUSE_DIGITAL_SIG); + + /* Non-CA certificate that asserts keyCertSign: rejected by the + * basicConstraints/keyUsage consistency guard. */ + WB_MK(wbLeafKuCertSign, + _s.cn = "MCDC Leaf KU"; _s.issuerFix = &wbRootA; + _s.withSkid = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.keyUsage = KEYUSE_DIGITAL_SIG | KEYUSE_KEY_CERT_SIGN); + + WB_MK(wbLeafKuNoCertSign, + _s.cn = "MCDC Leaf KU2"; _s.issuerFix = &wbRootA; + _s.withSkid = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.keyUsage = KEYUSE_DIGITAL_SIG | KEYUSE_CRL_SIGN); + + /* --- zero serial numbers ------------------------------------------ */ + WB_MK(wbZeroSerialRoot, + _s.cn = "MCDC ZS Root"; _s.subjectKey = &wbKeyRoot; + _s.isCA = 1; _s.withSkid = 1; _s.zeroSerial = 1; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + + WB_MK(wbZeroSerialLeaf, + _s.cn = "MCDC ZS Leaf"; _s.issuerFix = &wbRootA; + _s.withSkid = 1; _s.zeroSerial = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz); + + WB_MK(wbZeroSerialSubCA, + _s.cn = "MCDC ZS SubCA"; _s.issuerFix = &wbRootA; + _s.subjectKey = &wbKeyInter; + _s.isCA = 1; _s.withSkid = 1; _s.zeroSerial = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + + /* --- validity-period shapes --------------------------------------- */ + WB_MK(wbExpiredLeaf, + _s.cn = "MCDC Expired"; _s.issuerFix = &wbRootA; + _s.withSkid = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.notBefore = "100101000000Z"; _s.notAfter = "110101000000Z"); + + WB_MK(wbFutureLeaf, + _s.cn = "MCDC Future"; _s.issuerFix = &wbRootA; + _s.withSkid = 1; + _s.akid = wbRootA.skid; _s.akidSz = wbRootA.skidSz; + _s.notBefore = "400101000000Z"; _s.notAfter = "410101000000Z"); + + /* Only one of the two explicit date fields set: the generator has to + * fall back to computing the whole validity period itself. */ + WB_MK(wbOnlyNotBefore, + _s.cn = "MCDC OneDate"; _s.issuerFix = &wbRootA; + _s.withSkid = 1; + _s.notBefore = "200101000000Z"); + + /* --- an A->B->A authority-key-id cycle ---------------------------- */ + WB_MK(wbNcX, + _s.cn = "MCDC NC X"; _s.issuerCn = "MCDC NC Y"; + _s.subjectKey = &wbKeyInter; + _s.isCA = 1; _s.withSkid = 1; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + WB_MK(wbNcY, + _s.cn = "MCDC NC Y"; _s.issuerCn = "MCDC NC X"; + _s.subjectKey = &wbKeyLeaf; + _s.isCA = 1; _s.withSkid = 1; + _s.akid = wbNcX.skid; _s.akidSz = wbNcX.skidSz; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + /* Regenerate X now that Y's SKID is known, closing the cycle. */ + WB_MK(wbNcX, + _s.cn = "MCDC NC X"; _s.issuerCn = "MCDC NC Y"; + _s.subjectKey = &wbKeyInter; + _s.isCA = 1; _s.withSkid = 1; + _s.akid = wbNcY.skid; _s.akidSz = wbNcY.skidSz; + _s.keyUsage = KEYUSE_KEY_CERT_SIGN); + + WB_MK(wbLeafNcX, + _s.cn = "MCDC NC Leaf"; _s.issuerCn = "MCDC NC X"; + _s.signKey = &wbKeyInter; + _s.akid = wbNcX.skid; _s.akidSz = wbNcX.skidSz; + _s.keyUsage = KEYUSE_DIGITAL_SIG); +} + +/* Load a fixture as a trust store entry. CA_TYPE skips signature + * confirmation, which is what lets the deliberately mis-parented + * intermediates above be installed. */ +static int wb_load_ca(WOLFSSL_CERT_MANAGER* cm, const WbFix* fix, + const char* what) +{ + int ret; + + if (fix->sz <= 0) { + return -1; + } + ret = wolfSSL_CertManagerLoadCABuffer(cm, fix->der, (long)fix->sz, + WOLFSSL_FILETYPE_ASN1); + WB_CHECK(ret == WOLFSSL_SUCCESS, what); + return ret; +} + + +static void wb_fixture_parse_matrix(void) +{ + WOLFSSL_CERT_MANAGER* cm; + Signer* rootSigner = NULL; + Signer* interSigner = NULL; + size_t v; + static const int verifyModes[] = { + NO_VERIFY, VERIFY, VERIFY_SKIP_DATE, VERIFY_OCSP, VERIFY_NAME + }; + + WB_NOTE("ParseCertRelative(): manufactured-fixture sweep " + "[:24444,:24450,:24464,:24476,:24508,:24522,:24545,:24583," + ":24610,:24861]"); + + wb_build_fixtures(); + if (!wbFixturesOk) { + WB_NOTE("fixture generation failed; sweep skipped"); + wb_free_keys(); + return; + } + + cm = wolfSSL_CertManagerNew(); + WB_CHECK(cm != NULL, "wolfSSL_CertManagerNew (fixture store)"); + if (cm == NULL) { + wb_free_keys(); + return; + } + (void)wb_load_ca(cm, &wbRootA, "load the manufactured root"); + (void)wb_load_ca(cm, &wbInter, "load the manufactured intermediate"); + (void)wb_load_ca(cm, &wbInterMismatch, + "load the mis-parented intermediate"); + (void)wb_load_ca(cm, &wbInterBadAkid, + "load the dangling-AKID intermediate"); + (void)wb_load_ca(cm, &wbInterNoKU, + "load the keyUsage-less intermediate"); + + /* ---- zero-serial guard and its trust-anchor exemption [:24444,:24450] + * A zero serial is only tolerated for a self-signed CA that is being + * installed as an explicitly trusted anchor. Each operand of that + * exemption is driven true and false against the same guard. */ + wb_parse_one(&wbZeroSerialRoot, CA_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbZeroSerialRoot, TRUSTED_PEER_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbZeroSerialRoot, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbZeroSerialRoot, CHAIN_CERT_TYPE, NO_VERIFY, cm, NULL); + wb_parse_one(&wbZeroSerialLeaf, CA_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbZeroSerialLeaf, TRUSTED_PEER_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbZeroSerialLeaf, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbZeroSerialSubCA, CA_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbZeroSerialSubCA, TRUSTED_PEER_TYPE, VERIFY, cm, NULL); + + /* ---- basicConstraints / keyUsage consistency [:24464] ------------- */ + wb_parse_one(&wbLeafKuCertSign, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafKuCertSign, CA_TYPE, NO_VERIFY, cm, NULL); + wb_parse_one(&wbLeafKuNoCertSign, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafNoSkid, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbInter, CERT_TYPE, VERIFY, cm, NULL); + + /* ---- recompute the SKID when the extension is absent [:24476] ----- */ + wb_parse_one(&wbLeafNoSkid, CERT_TYPE, NO_VERIFY, cm, NULL); + wb_parse_one(&wbLeafNoSkid, CA_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafUnderMism, CERT_TYPE, VERIFY, cm, NULL); + + /* ---- CA lookup: key id hit with a disagreeing name [:24522], + * name hit while an AKID is present [:24545] ------------------- */ + wb_parse_one(&wbLeafBadName, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafBadName, CERT_TYPE, VERIFY_NAME, cm, NULL); + wb_parse_one(&wbLeafBadAkid, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafNoAkid, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafNoAkid, CERT_TYPE, VERIFY_OCSP, cm, NULL); + + /* ---- path-length block: CA with and without a keyUsage extension, + * and the self-issued trust-anchor public-key comparison + * [:24583,:24610]. A self-signed certificate only gets a CA + * lookup at all when the type is neither CA_TYPE nor + * TRUSTED_PEER_TYPE, hence CHAIN_CERT_TYPE here. */ + wb_parse_one(&wbInterNoKU, CA_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbInterNoKU, CHAIN_CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbInter, CA_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbInter, CHAIN_CERT_TYPE, VERIFY, cm, NULL); + /* Third arm of the same guard: a CA that HAS a keyUsage extension in + * which the certificate-signing bit is clear. wbInterNoKU above makes + * the "extension absent" operand true and wbInter makes the "bit set" + * operand true; only this fixture makes both false, so only with it in + * the same binary is either operand shown to decide the guard on its + * own. Loaded into the store as well, so the same shape is reachable + * as somebody else's issuer. */ + (void)wb_load_ca(cm, &wbInterKuNoCS, + "load the CA whose keyUsage omits certificate signing"); + wb_parse_one(&wbInterKuNoCS, CHAIN_CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbInterKuNoCS, CHAIN_CERT_TYPE, VERIFY_NAME, cm, NULL); + wb_parse_one(&wbInterKuNoCS, CA_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbInterKuNoCS, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbRootA, CHAIN_CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbRootADupRsa, CHAIN_CERT_TYPE, VERIFY, cm, NULL); +#ifdef HAVE_ECC + if (wbEccOk) { + wb_parse_one(&wbRootADupEcc, CHAIN_CERT_TYPE, VERIFY, cm, NULL); + } +#endif + + /* ---- validity period: notBefore in the future, notAfter in the past, + * each against the verify modes that do and do not check dates. */ + for (v = 0; v < sizeof(verifyModes) / sizeof(verifyModes[0]); v++) { + wb_parse_one(&wbExpiredLeaf, CERT_TYPE, verifyModes[v], cm, NULL); + wb_parse_one(&wbFutureLeaf, CERT_TYPE, verifyModes[v], cm, NULL); + } + wb_parse_one(&wbOnlyNotBefore, CERT_TYPE, VERIFY, cm, NULL); + + /* ---- version ceiling [:22632]. Driven against a version-3 sibling of + * the same shape so the accepting vector for the same decision is + * in this binary too. */ + wb_parse_one(&wbBadVersion, CERT_TYPE, NO_VERIFY, cm, NULL); + wb_parse_one(&wbBadVersion, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbBadVersion, CA_TYPE, NO_VERIFY, cm, NULL); + wb_parse_one(&wbLeafNoAkid, CERT_TYPE, NO_VERIFY, cm, NULL); + { + /* The same certificate cut short. The template walk fails, and the + * version ceiling above is then evaluated with a failure already in + * hand -- the only way its leading operand decides the guard on its + * own. Truncating between a quarter and three quarters of the way + * in keeps the outer SEQUENCE header intact so the failure happens + * inside the item walk rather than at the very first tag. */ + static WbFix truncated; + size_t cut; + + for (cut = 4; cut <= 12; cut += 4) { + XMEMCPY(&truncated, &wbLeafNoAkid, sizeof(truncated)); + truncated.sz = (int)(((size_t)wbLeafNoAkid.sz * cut) / 16u); + wb_parse_one(&truncated, CERT_TYPE, NO_VERIFY, cm, NULL); + wb_parse_one(&truncated, CERT_TYPE, VERIFY, cm, NULL); + } + } + + /* ---- the three-level chain and the extraCAList lookups ------------ */ + rootSigner = wb_make_signer(&wbRootA); + interSigner = wb_make_signer(&wbInter); + WB_CHECK(rootSigner != NULL, "build a Signer from the manufactured root"); + + /* Ancestor walk with the full chain resolvable in the store. */ + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY_NAME, cm, NULL); + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY, cm, rootSigner); + /* Ancestor walk that stops because the grandparent is unreachable. */ + wb_parse_one(&wbLeafUnderMism, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafUnderMism, CERT_TYPE, VERIFY, cm, rootSigner); + /* extraCAList satisfies the very first lookup, before any key id. */ + wb_parse_one(&wbLeafNoAkid, CERT_TYPE, VERIFY, NULL, rootSigner); + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY, NULL, interSigner); + /* Nothing in the extra list matches the AKID being resolved. */ + wb_parse_one(&wbLeafBadAkid, CERT_TYPE, VERIFY, NULL, rootSigner); + + if (rootSigner != NULL) { + FreeSigner(rootSigner, NULL); + } + if (interSigner != NULL) { + FreeSigner(interSigner, NULL); + } + wolfSSL_CertManagerFree(cm); + + /* ---- the A->B->A cycle, in a store that holds only the two peers --- */ + cm = wolfSSL_CertManagerNew(); + WB_CHECK(cm != NULL, "wolfSSL_CertManagerNew (cycle store)"); + if (cm != NULL) { + (void)wb_load_ca(cm, &wbNcX, "load cycle CA X"); + (void)wb_load_ca(cm, &wbNcY, "load cycle CA Y"); + wb_parse_one(&wbLeafNcX, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafNcX, CERT_TYPE, VERIFY_NAME, cm, NULL); + wolfSSL_CertManagerFree(cm); + } + + /* ---- a store holding the intermediate but not the root: the walk + * terminates on a missing parent instead of a trust anchor. + * + * This is also the only configuration in which the ancestor walk's + * extra-CA-list scan runs at all. The scan is reached only when the + * trust store cannot resolve the ancestor's authority key id, so the + * sweeps above -- which all use a store that already holds the root -- + * never enter its loop body. Here the root is deliberately missing, + * so each of the loop's two operands can be driven in turn: + * + * - a list entry whose key id does not match at all; + * - a list entry whose key id AND issuer name both match; + * - a list entry whose key id matches while its subject name does + * not, which is what wbSkidTwin exists for. + */ + cm = wolfSSL_CertManagerNew(); + WB_CHECK(cm != NULL, "wolfSSL_CertManagerNew (orphan store)"); + if (cm != NULL) { + Signer* twinSigner; + + (void)wb_load_ca(cm, &wbInter, "load the orphaned intermediate"); + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY, cm, NULL); + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY_SKIP_DATE, cm, NULL); + + rootSigner = wb_make_signer(&wbRootA); + interSigner = wb_make_signer(&wbInter); + twinSigner = wb_make_signer(&wbSkidTwin); + WB_CHECK(twinSigner != NULL, + "build a Signer from the key-id twin CA"); + + /* key id does not match -> first operand false */ + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY, cm, interSigner); + /* key id and name both match -> both operands true, scan hits */ + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY, cm, rootSigner); + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY_NAME, cm, rootSigner); + /* key id matches, name does not -> second operand false */ + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY, cm, twinSigner); + wb_parse_one(&wbLeafByInter, CERT_TYPE, VERIFY_NAME, cm, twinSigner); + + if (twinSigner != NULL) { + FreeSigner(twinSigner, NULL); + } + if (interSigner != NULL) { + FreeSigner(interSigner, NULL); + } + + /* ---- issuer known, but with no public key attached [:24610]. + * A Signer only carries a public key when the DecodedCert it was + * filled from owned one; the trust store always supplies one, so + * the "issuer has no key" operand of the trust-anchor comparison + * is otherwise never false. Borrowing the root's own Signer and + * detaching its key for the duration of one parse reproduces that + * state exactly, with the pointer put back before the Signer is + * released so ownership is unchanged. */ + if (rootSigner != NULL) { + const byte* savedKey = rootSigner->publicKey; + word32 savedSz = rootSigner->pubKeySize; + + /* Baseline: the same certificate against the same issuer with + * the key still attached, so the accepting vector for this + * decision is in this binary too. */ + wb_parse_one(&wbRootA, CHAIN_CERT_TYPE, VERIFY, cm, rootSigner); + + rootSigner->publicKey = NULL; + rootSigner->pubKeySize = 0; + wb_parse_one(&wbRootA, CHAIN_CERT_TYPE, VERIFY, cm, rootSigner); + wb_parse_one(&wbInter, CHAIN_CERT_TYPE, VERIFY, cm, rootSigner); + rootSigner->publicKey = savedKey; + rootSigner->pubKeySize = savedSz; + + FreeSigner(rootSigner, NULL); + } + rootSigner = NULL; + interSigner = NULL; + + wolfSSL_CertManagerFree(cm); + } + + wb_free_keys(); +} +#else +static void wb_fixture_parse_matrix(void) +{ + WB_NOTE("cert generation or date support not compiled in; " + "manufactured-fixture sweep skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section: the serial-number encoder and the DerBuffer lifecycle. + * + * These two helpers sit either side of the certificate fixtures above and + * have operands the certificate path cannot reach: + * + * - SetSerialNumber() strips leading zero octets before it encodes. Under + * the template encoder the certificate generator does not route through + * it at all, and the callers that do route through it never hand it a + * serial that begins with a zero octet, so neither the "still have + * octets left" nor the "this octet is zero" operand of the strip loop is + * ever shown to control it. Calling it directly with a serial that is + * all zeros, one that merely starts with zeros, and one that starts with + * a set high bit drives both operands and the sign-padding branch. + * + * - FreeDer() zeroes the buffer only for the two private-key buffer types + * and only when a buffer is actually attached. Certificate code only + * ever frees certificate-typed buffers with a buffer attached, so the + * alternative-private-key type and the detached-buffer case are dead + * from that direction. AllocDer() places the buffer inside the same + * allocation it returns, so clearing the pointer before the free is + * safe: FreeDer() releases the containing block, not the buffer field. + * ------------------------------------------------------------------------- */ +static void wb_serial_and_der_helpers(void) +{ +#if !defined(NO_CERTS) + DerBuffer* der = NULL; + +#if !defined(WOLFSSL_ASN_TEMPLATE) || defined(HAVE_PKCS7) + { + byte out[64]; + word32 osz = (word32)sizeof(out); + /* leading zero octets, then a payload: strip loop runs and stops */ + static const byte snLeadingZeros[4] = { 0x00, 0x00, 0x00, 0x2A }; + /* no leading zero: strip loop is entered and exits immediately */ + static const byte snPlain[3] = { 0x2A, 0x01, 0x02 }; + /* every octet zero: strip loop consumes the whole input */ + static const byte snAllZero[3] = { 0x00, 0x00, 0x00 }; + /* high bit set: the encoder reserves an extra sign octet */ + static const byte snHighBit[3] = { 0x80, 0x01, 0x02 }; + + WB_NOTE("SetSerialNumber(): leading-zero strip loop [:25156]"); + WB_CHECK(SetSerialNumber(NULL, 4, out, osz, 20) < 0, + "reject a NULL serial"); + WB_CHECK(SetSerialNumber(snPlain, 3, NULL, osz, 20) < 0, + "reject a NULL output buffer"); + WB_CHECK(SetSerialNumber(snPlain, 3, out, osz, 20) > 0, + "encode a serial with no leading zeros"); + WB_CHECK(SetSerialNumber(snLeadingZeros, 4, out, osz, 20) > 0, + "encode a serial after stripping its leading zeros"); + WB_CHECK(SetSerialNumber(snAllZero, 3, out, osz, 20) < 0, + "reject a serial that is entirely zero octets"); + WB_CHECK(SetSerialNumber(snHighBit, 3, out, osz, 20) > 0, + "encode a serial whose leading octet has its high bit set"); + } +#endif + + WB_NOTE("FreeDer(): buffer-type and attached-buffer guards " + "[:25273,:25277]"); + FreeDer(NULL); /* no handle at all */ + FreeDer(&der); /* handle present, no buffer */ + + if (AllocDer(&der, 16, CERT_TYPE, NULL) == 0) { + FreeDer(&der); /* neither private-key type */ + } + if (AllocDer(&der, 16, PRIVATEKEY_TYPE, NULL) == 0) { + FreeDer(&der); /* first private-key type */ + } + if (AllocDer(&der, 16, ALT_PRIVATEKEY_TYPE, NULL) == 0) { + FreeDer(&der); /* second private-key type */ + } + if (AllocDer(&der, 16, PRIVATEKEY_TYPE, NULL) == 0) { + /* Private-key type with the buffer detached: the zeroing guard's + * last operand goes false while the type operands stay true. */ + der->buffer = NULL; + FreeDer(&der); + } + WB_CHECK(der == NULL, "FreeDer clears the caller's handle"); +#endif /* !NO_CERTS */ + +#if defined(WOLFSSL_PEM_TO_DER) && !defined(NO_CERTS) + { + /* A carriage return, a line feed, then a payload octet: the loop + * takes its first operand true on every pass and its two character + * operands through all three combinations that can occur. */ + static const char eol[] = "\r\nX"; + + WB_NOTE("SkipEndOfLineChars(): end-of-line scan [:25426]"); + WB_CHECK(SkipEndOfLineChars(eol, eol + 3) == eol + 2, + "skip both end-of-line characters and stop at the payload"); + WB_CHECK(SkipEndOfLineChars(eol, eol) == eol, + "stop immediately when the range is empty"); + WB_CHECK(SkipEndOfLineChars(eol + 2, eol + 3) == eol + 2, + "stop immediately on a non-end-of-line character"); + } +#endif +} + int main(void) { setvbuf(stdout, NULL, _IONBF, 0); @@ -1556,6 +2583,8 @@ int main(void) wb_set_algo_id(); wb_decode_dsa_asn1_sig(); wb_parse_cert_relative_matrix(); + wb_fixture_parse_matrix(); + wb_serial_and_der_helpers(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage From fef0df11687f688b6e0addfa41fc59048f3d6c13 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 20:02:43 +0200 Subject: [PATCH 16/20] tests: drive the sp_int and integer error chains and width guards --- tests/unit-mcdc/test_integer_whitebox.c | 299 ++++- tests/unit-mcdc/test_sp_int_fault_whitebox.c | 451 +++++-- tests/unit-mcdc/test_sp_int_whitebox.c | 1184 ++++++++++++++++++ 3 files changed, 1807 insertions(+), 127 deletions(-) diff --git a/tests/unit-mcdc/test_integer_whitebox.c b/tests/unit-mcdc/test_integer_whitebox.c index 4f4189016a0..0619bfdbd60 100644 --- a/tests/unit-mcdc/test_integer_whitebox.c +++ b/tests/unit-mcdc/test_integer_whitebox.c @@ -104,6 +104,15 @@ static void wb_miller_rabin(void) (void)mp_set(&b, 2); (void)mp_prime_miller_rabin(&a, &b, &res); /* composite */ + /* Squaring loop that ENDS because the witness reached n1 rather than + * because the round counter ran out. 17-1 = 2^4, so s-1 is 3 and base 2 + * walks 2 -> 4 -> 16 == n1 at j == 3: the loop's second operand decides + * the exit while the first is still true, which base 3 (which only + * reaches n1 once j has already passed s-1) never shows. */ + (void)mp_set(&a, 17); + (void)mp_set(&b, 2); + (void)mp_prime_miller_rabin(&a, &b, &res); + mp_clear(&a); mp_clear(&b); WB_NOTE("mp_prime_miller_rabin prime/composite exercised"); @@ -302,35 +311,60 @@ static void wb_s_mp_add_sub_null_dp(void) static void wb_s_mp_mul_high_digs_null_dp(void) { mp_int a, b, c; + /* The slow path is taken when a->used + b->used + 1 >= MP_WARRAY. The + * WIDTH is carried entirely by 'b' and 'a' is left at a single digit on + * purpose: the inner loop aliases 'b' at b->dp + (digs - ix), so an 'a' + * wider than one digit walks that alias BACKWARDS off the front of b's + * buffer. With pa == 1 and digs == 0 the only offset used is 0 and every + * read stays inside b. (An earlier version of this driver put the width + * on 'a' instead and read up to 600 digits in front of b->dp; it survived + * only because the preceding allocations happened to leave mapped memory + * there, and faulted as soon as anything else ran first.) */ + int pb = (int)MP_WARRAY; + int i; XMEMSET(&a, 0, sizeof(a)); XMEMSET(&b, 0, sizeof(b)); XMEMSET(&c, 0, sizeof(c)); - mp_init(&a); - mp_init(&b); - mp_init(&c); + if ((mp_init(&a) != MP_OKAY) || (mp_init(&b) != MP_OKAY) || + (mp_init(&c) != MP_OKAY)) { + WB_NOTE("mul_high_digs: init failed, skipped"); + wb_fail = 1; + return; + } + if (mp_grow(&b, pb) != MP_OKAY) { + WB_NOTE("mul_high_digs: grow failed, skipped"); + wb_fail = 1; + goto out; + } + for (i = 0; i < pb; i++) { + b.dp[i] = (mp_digit)(i + 1); + } + b.used = pb; - /* Corrupted 'a': ->used large enough to both force the slow path - * (a->used + b->used + 1 >= MP_WARRAY) and make pa > 0, but ->dp - * stays NULL - the loop condition's short-circuit ("a->dp" checked - * after "ix < pa") means dereferencing a->dp[ix] never happens. */ - a.used = 600; - mp_set(&b, 3); + /* Corrupted 'a': ->used nonzero so pa > 0, but ->dp stays NULL - the + * loop condition's short-circuit ("a->dp" checked after "ix < pa") + * means dereferencing a->dp[ix] never happens. */ + a.used = 1; (void)s_mp_mul_high_digs(&a, &b, &c, 0); - a.used = 0; /* restore before mp_clear below */ + a.used = 0; /* restore before the grow below */ - /* Same slow-path shape, but with a REAL (grown, non-corrupted) large - * 'a' - completes the "a->dp" truthy side within this same binary. */ - mp_grow(&a, 600); - a.used = 600; - XMEMSET(a.dp, 0, sizeof(mp_digit) * 600); + /* Same slow-path shape, but with a REAL (allocated) 'a' - completes the + * "a->dp" truthy side within this same binary. */ + if (mp_grow(&a, 1) != MP_OKAY) { + WB_NOTE("mul_high_digs: grow failed, skipped"); + wb_fail = 1; + goto out; + } a.dp[0] = 3; + a.used = 1; (void)s_mp_mul_high_digs(&a, &b, &c, 0); + WB_NOTE("s_mp_mul_high_digs 'a->dp' loop guard both sides exercised"); +out: mp_clear(&a); mp_clear(&b); mp_clear(&c); - WB_NOTE("s_mp_mul_high_digs 'a->dp' loop guard both sides exercised"); } /* ------------------------------------------------------------------------- * @@ -900,10 +934,239 @@ static void wb_IntegerDecisionCoverage(void) WB_NOTE("IntegerDecisionCoverage decision branches exercised"); } +/* ------------------------------------------------------------------------- * + * Class 4: the mp_add_d() / mp_sub_d() destination-alias sanity checks. + * + * tmpa = a->dp; + * tmpc = c->dp; + * if (tmpa == NULL || tmpc == NULL) { return MP_MEM; } + * + * Both functions grow the destination first, so by the time the aliases are + * taken c->dp is NULL only if c arrived claiming capacity it does not have + * (->alloc >= a->used + 1 while ->dp == NULL) - the grow is skipped and the + * destination alias is NULL while the SOURCE alias is not. That is exactly + * the second operand's independence row, and no public mutator can build the + * state, so it is set up here directly: the pointer is parked, the call made, + * and the pointer put back before the value is cleared. + * ------------------------------------------------------------------------- */ +static void wb_add_sub_d_null_dp(void) +{ + mp_int a, c; + mp_digit* saved; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&c, 0, sizeof(c)); + + if ((mp_init(&a) != MP_OKAY) || (mp_init(&c) != MP_OKAY)) { + WB_NOTE("add/sub_d alias rows: init failed, skipped"); + wb_fail = 1; + return; + } + /* a = 5 (one digit, positive) and a destination with real capacity. */ + if ((mp_set(&a, 5) != MP_OKAY) || (mp_grow(&c, 8) != MP_OKAY)) { + WB_NOTE("add/sub_d alias rows: setup failed, skipped"); + wb_fail = 1; + goto out; + } + + /* Ordinary rows first: both aliases non-NULL, in this same binary. */ + (void)mp_add_d(&a, 1, &c); + (void)mp_sub_d(&a, 1, &c); + + /* Destination alias NULL while the source alias is not. The recorded + * capacity is left alone so the grow is skipped and the guard is the + * first thing that sees the missing buffer. */ + saved = c.dp; + c.dp = NULL; + (void)mp_add_d(&a, 1, &c); + (void)mp_sub_d(&a, 1, &c); + c.dp = saved; + + WB_NOTE("mp_add_d/mp_sub_d destination-alias rows exercised"); +out: + mp_clear(&a); + mp_clear(&c); +} + +/* ------------------------------------------------------------------------- * + * Class 5: mp_mul_d()'s destination capacity guard. + * + * if (c->dp == NULL || c->alloc < a->used + 1) { mp_grow(...) } + * + * mp_init() defers allocation, so the first use of any destination takes the + * dp==NULL arm and the capacity operand is never evaluated; after that the + * grown destination is comfortably large for the single-digit multiplicands + * the suite uses, so the capacity operand is only ever seen false. A + * multiplicand wider than the destination's grown capacity supplies its true + * row, and repeating the call once the destination has been grown to fit + * supplies the false row. + * ------------------------------------------------------------------------- */ +static void wb_mul_d_capacity(void) +{ + mp_int a, c; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&c, 0, sizeof(c)); + + if ((mp_init(&a) != MP_OKAY) || (mp_init(&c) != MP_OKAY)) { + WB_NOTE("mul_d capacity rows: init failed, skipped"); + wb_fail = 1; + return; + } + + /* First use: the destination has no buffer at all (first operand true). */ + if (mp_set(&a, 7) != MP_OKAY) { + wb_fail = 1; + goto out; + } + (void)mp_mul_d(&a, 3, &c); + + /* A multiplicand far wider than the destination's grown capacity: the + * destination now has a buffer, so the capacity operand decides. */ + if ((mp_set(&a, 1) != MP_OKAY) || + (mp_mul_2d(&a, 4096 * DIGIT_BIT, &a) != MP_OKAY)) { + wb_fail = 1; + goto out; + } + (void)mp_mul_d(&a, 3, &c); + + /* Same call again: the destination was grown to fit by the previous one, + * so both operands are false. */ + (void)mp_mul_d(&a, 3, &c); + + WB_NOTE("mp_mul_d destination capacity rows exercised"); +out: + mp_clear(&a); + mp_clear(&c); +} + +/* ------------------------------------------------------------------------- * + * Class 6: mp_exptmod_base_2()'s Montgomery-backend selector. + * + * if (((P->used * 2 + 1) < (int)MP_WARRAY) && P->used < (1L << ...)) + * + * The comba ("fast") Montgomery reduction is selected whenever the modulus + * fits the fixed-width accumulator array; every modulus the suite uses does, + * so the generic reduction is never chosen and the first operand's false row + * is missing. A modulus wide enough to overflow the accumulator selects the + * generic backend. Only the FIRST operand is closable: the second operand is + * implied by the first (2u+1 < 2^(k+1) and u < 2^k are the same predicate on + * an integer u), so it has no independence pair - see the residuals note. + * + * The exponent is deliberately tiny: the decision is about the modulus width, + * and the generic reduction is quadratic in it. + * ------------------------------------------------------------------------- */ +static void wb_exptmod_base_2_wide(void) +{ + mp_int g, x, p, y; + int digits = (int)((MP_WARRAY + 1) / 2); + + XMEMSET(&g, 0, sizeof(g)); XMEMSET(&x, 0, sizeof(x)); + XMEMSET(&p, 0, sizeof(p)); XMEMSET(&y, 0, sizeof(y)); + + if (digits > 1024) { + WB_NOTE("accumulator width needs an impractical modulus; skipped"); + return; + } + + if (mp_init_multi(&g, &x, &p, &y, NULL, NULL) != MP_OKAY) { + WB_NOTE("wide-modulus base-2 rows: init failed, skipped"); + wb_fail = 1; + return; + } + + /* p = 2^(digits*DIGIT_BIT) + 1: odd, and one digit past the width at + * which the accumulator array can still hold the product. */ + if ((mp_set(&g, 2) != MP_OKAY) || (mp_set(&x, 3) != MP_OKAY) || + (mp_set(&p, 1) != MP_OKAY) || + (mp_mul_2d(&p, digits * DIGIT_BIT, &p) != MP_OKAY) || + (mp_add_d(&p, 1, &p) != MP_OKAY)) { + WB_NOTE("wide-modulus base-2 rows: setup failed, skipped"); + wb_fail = 1; + goto out; + } + (void)mp_exptmod(&g, &x, &p, &y); + + /* The ordinary row: a modulus the accumulator does hold. */ + if ((mp_set(&p, 1) == MP_OKAY) && (mp_mul_2d(&p, 127, &p) == MP_OKAY) && + (mp_add_d(&p, 1, &p) == MP_OKAY)) { + (void)mp_exptmod(&g, &x, &p, &y); + } + + WB_NOTE("mp_exptmod_base_2 wide-modulus rows exercised"); +out: + mp_clear(&g); mp_clear(&x); mp_clear(&p); mp_clear(&y); +} + +/* ------------------------------------------------------------------------- * + * Class 7: mp_prime_is_prime_ex()'s random-witness rejection. + * + * if (mp_cmp_d(&b, 2) != MP_GT || mp_cmp(&b, &c) != MP_LT) { ix--; continue; } + * + * The witness is drawn at random and masked down to the CANDIDATE's bit + * width. For the multi-hundred-bit candidates the suite tests, a draw of 0, 1 + * or 2 has probability ~2^-bits and is never observed, so the first operand's + * true row is missing. A deliberately small candidate shrinks the draw space + * until the arm is certain to be taken inside a bounded number of trials. + * + * 2039 is prime, is past the end of the small-prime table (so the up-front + * table lookup and trial division do not answer it), and sits just under + * 2^11, so an 11-bit witness is out of range only 14 times in 2048 - rare + * enough that the retry loop always makes progress. At 3 draws in 2048 for + * the arm of interest, this many trials misses it with probability ~e^-29. + * ------------------------------------------------------------------------- */ +#ifndef WC_NO_RNG +#define WB_SMALL_WITNESS_TRIALS 20000 + +static void wb_prime_small_witness(void) +{ + WC_RNG rng; + mp_int a; + int res = 0; + int i; + + XMEMSET(&a, 0, sizeof(a)); + if (mp_init(&a) != MP_OKAY) { + wb_fail = 1; + return; + } + if (wc_InitRng(&rng) != 0) { + WB_NOTE("RNG init failed; small-witness rows skipped"); + mp_clear(&a); + return; + } + if (mp_set(&a, 2039) != MP_OKAY) { + wb_fail = 1; + goto out; + } + + for (i = 0; i < WB_SMALL_WITNESS_TRIALS; i++) { + if (mp_prime_is_prime_ex(&a, 1, &res, &rng) != MP_OKAY) { + wb_fail = 1; + break; + } + } + + WB_NOTE("random-witness rejection rows exercised"); +out: + (void)wc_FreeRng(&rng); + mp_clear(&a); +} +#else +static void wb_prime_small_witness(void) +{ + WB_NOTE("no RNG in this build; random-witness rows skipped"); +} +#endif /* !WC_NO_RNG */ + #endif /* USE_INTEGER_HEAP_MATH && WOLFSSL_PUBLIC_MP */ int main(void) { + /* Unbuffered: on a timeout the process is killed and anything still + * buffered is lost, which reads as an empty log. */ + setvbuf(stdout, NULL, _IONBF, 0); + printf("integer.c white-box MC/DC supplement\n"); #if defined(USE_FAST_MATH) || !defined(USE_INTEGER_HEAP_MATH) || \ defined(WOLFSSL_SP_MATH) || defined(NO_BIG_INT) @@ -923,6 +1186,10 @@ int main(void) wb_mp_set_bit_corrupted(); wb_mp_cnt_lsb_all_zero_digits(); wb_IntegerDecisionCoverage(); + wb_add_sub_d_null_dp(); + wb_mul_d_capacity(); + wb_exptmod_base_2_wide(); + wb_prime_small_witness(); #endif printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures surface as skips, not failures: a nonzero exit makes the diff --git a/tests/unit-mcdc/test_sp_int_fault_whitebox.c b/tests/unit-mcdc/test_sp_int_fault_whitebox.c index b4d607de865..a82556ff2d8 100644 --- a/tests/unit-mcdc/test_sp_int_fault_whitebox.c +++ b/tests/unit-mcdc/test_sp_int_fault_whitebox.c @@ -29,7 +29,7 @@ * * if ((err == MP_OKAY) && useMont) { * if ((!done) && (err == MP_OKAY)) { - * if ((err == MP_OKAY) && sp_isone(a)) { + * for (; (err == MP_OKAY) && (i >= 0); i--) { * * Called with valid operands nothing sets `err`, so the first operand of each * of these never takes its false side. The failure that does set it is a @@ -48,11 +48,33 @@ * `err == MP_OKAY` checkpoint downstream of one is observed both holding and * not holding. * + * Two properties of the sweep are load-bearing: + * + * 1. ONE operation per armed window. mcdc_fa_arm(n) resets the allocation + * counter, so a window that runs several operations only positions the + * failure inside the FIRST of them -- every later one starts with the + * counter already past n and sees its very first allocation fail. Each + * operation therefore gets its own arm/disarm pair here. + * + * 2. The window must be deep enough. The outer temporaries of an + * exponentiation are a single array allocation, so failing at index 1 + * only ever reaches the checkpoints in the function prologue. The + * checkpoints INSIDE the square-and-multiply loops are reached only when + * the failure lands on an allocation made by a nested sp_mul() / + * sp_sqr() / sp_mod() / _sp_mont_red() call, which is tens of + * allocations deep. SP_FAULT_MAX_N is sized for that. + * + * The internal engines are also driven directly (they are file-static and in + * scope because this TU #includes sp_int.c), which both localises the + * allocation index to one engine and reaches the "base is not less than + * modulus" arm that sp_exptmod()'s own up-front reduction makes dead. + * * Operands are deliberately small. These decisions test the error state and * the shape of the operands, not their magnitude, and the sweep repeats every * operation once per fail-index -- TEST_TIMEOUT is wall clock and variants run * concurrently under MAXPAR, so a full-size modexp here would be a timeout - * rather than evidence. + * rather than evidence. The one exception is the invmod pair, which needs a + * modulus of at least 1024 bits to select the division-based inverse. * * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's @@ -76,8 +98,12 @@ static int wb_fail = 0; #if defined(WOLFSSL_SP_MATH) || defined(WOLFSSL_SP_MATH_ALL) +/* Deep enough to walk the failure through the nested sp_mul()/sp_sqr()/ + * sp_mod() allocations inside the square-and-multiply loops, not just the + * prologue temporaries. Over-sweeping is harmless: once n exceeds an + * operation's allocation count the operation simply runs to completion. */ #ifndef SP_FAULT_MAX_N - #define SP_FAULT_MAX_N 40 + #define SP_FAULT_MAX_N 160 #endif /* Small primes/moduli: big enough to take the montgomery and non-montgomery @@ -87,137 +113,329 @@ static const char* WB_M_EVEN = "F0000000000000000000000000000038"; static const char* WB_B = "0123456789ABCDEF0123456789ABCDEF"; static const char* WB_E = "10001"; -static void wb_exptmod_sweep(void) +/* A dividend WIDER than the moduli above. + * + * _sp_div() short-circuits (done = 1, no temporaries allocated at all) for + * dividend < divisor, dividend == divisor, and dividend of the same bit length + * as the divisor. Every division fed the operands above therefore returned + * before reaching a single allocation site, which is why the division family's + * error checkpoints stayed unreached no matter how the fail-index was swept. + * This value is twice as wide as the moduli, so the long-division body runs. */ +static const char* WB_A_BIG = + "C3A5B1D7E9F0246813579BDF02468ACE0123456789ABCDEF0123456789ABCDEF"; + +/* 1024-bit moduli: sp_invmod() only selects the division-based inverse when + * the modulus is at least 1024 bits, so the small moduli above never reach + * _sp_invmod_div() at all. */ +static const char* WB_M1024_ODD = + "C0000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000061"; +static const char* WB_M1024_EVEN = + "C0000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000062"; + +/* Which internal engines this configuration compiles. Mirrors sp_int.c's own + * guards so the TU builds under every campaign variant. */ +#if (defined(WOLFSSL_SP_MATH_ALL) && !defined(WOLFSSL_RSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY)) || !defined(NO_DH) || \ + defined(OPENSSL_ALL) + #define WB_HAVE_EXPTMOD_EX +#endif +#if (defined(WOLFSSL_SP_MATH_ALL) && ((!defined(WOLFSSL_RSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY)) || !defined(NO_DH))) || \ + defined(OPENSSL_ALL) + #define WB_HAVE_EXPTMOD_MONT_EX + #define WB_HAVE_EXPTMOD_BASE_2 +#endif +#if defined(WOLFSSL_SP_MATH_ALL) || defined(WOLFSSL_HAVE_SP_DH) +#if defined(WOLFSSL_SP_FAST_NCT_EXPTMOD) || !defined(WOLFSSL_SP_SMALL) + #define WB_HAVE_EXPTMOD_NCT +#endif +#endif + +/* Number of distinct operations swept. */ +#define WB_OP_COUNT 34 + +#ifndef WC_NO_RNG +static WC_RNG wb_rng; +static int wb_rng_ok = 0; +#endif + +/* Build the operands for one operation (allocator DISARMED), arm the n-th + * allocation, run exactly that operation, then disarm. */ +static void wb_op(int op, int n) { - int n; + mp_int a; + mp_int b; + mp_int e; + mp_int m; + mp_int r; + mp_int q; + + if (mp_init_multi(&a, &b, &e, &m, &r, &q) != MP_OKAY) { + wb_fail = 1; + return; + } - for (n = 1; n <= SP_FAULT_MAX_N; n++) { - mp_int b; - mp_int e; - mp_int m; - mp_int r; + /* Common operands. Every read_radix here runs while disarmed. */ + if ((mp_read_radix(&b, WB_B, MP_RADIX_HEX) != MP_OKAY) || + (mp_read_radix(&e, WB_E, MP_RADIX_HEX) != MP_OKAY) || + (mp_read_radix(&a, WB_B, MP_RADIX_HEX) != MP_OKAY)) { + wb_fail = 1; + goto done; + } - if (mp_init_multi(&b, &e, &m, &r, NULL, NULL) != MP_OKAY) { + /* The division family needs a dividend wider than the divisor or the + * engine returns before allocating anything. */ + if ((op == 16) || (op == 17) || (op == 20) || (op == 21) || (op == 33)) { + if (mp_read_radix(&a, WB_A_BIG, MP_RADIX_HEX) != MP_OKAY) { wb_fail = 1; - return; + goto done; + } + } + + switch (op) { + case 0: + case 1: + case 2: + case 5: + if (mp_read_radix(&m, WB_M_ODD, MP_RADIX_HEX) != MP_OKAY) { + goto done; + } + break; + case 3: + case 4: + case 6: + if (mp_read_radix(&m, WB_M_EVEN, MP_RADIX_HEX) != MP_OKAY) { + goto done; } - if ((mp_read_radix(&b, WB_B, MP_RADIX_HEX) == MP_OKAY) && - (mp_read_radix(&e, WB_E, MP_RADIX_HEX) == MP_OKAY)) { - /* Odd modulus takes the montgomery route, even the divide one. */ - if (mp_read_radix(&m, WB_M_ODD, MP_RADIX_HEX) == MP_OKAY) { - mcdc_fa_arm(n); - (void)mp_exptmod(&b, &e, &m, &r); - (void)mp_exptmod_ex(&b, &e, (int)m.used, &m, &r); - (void)mp_exptmod_nct(&b, &e, &m, &r); - mcdc_fa_disarm(); - } - if (mp_read_radix(&m, WB_M_EVEN, MP_RADIX_HEX) == MP_OKAY) { - mcdc_fa_arm(n); - (void)mp_exptmod(&b, &e, &m, &r); - (void)mp_exptmod_nct(&b, &e, &m, &r); - mcdc_fa_disarm(); - } + break; + case 13: + case 28: + if (mp_read_radix(&m, WB_M1024_ODD, MP_RADIX_HEX) != MP_OKAY) { + goto done; } - mp_free(&b); - mp_free(&e); - mp_free(&m); - mp_free(&r); + break; + case 14: + case 29: + if (mp_read_radix(&m, WB_M1024_EVEN, MP_RADIX_HEX) != MP_OKAY) { + goto done; + } + break; + case 12: + case 27: + if (mp_read_radix(&m, WB_M_EVEN, MP_RADIX_HEX) != MP_OKAY) { + goto done; + } + break; + default: + if (mp_read_radix(&m, WB_M_ODD, MP_RADIX_HEX) != MP_OKAY) { + goto done; + } + break; } -} -static void wb_invmod_sweep(void) -{ - int n; + /* Base 2 for the dedicated base-2 exponentiation engine. */ + if ((op == 5) || (op == 6) || (op == 10)) { + mp_set(&b, 2); + } + /* Base at (a multiple of) the modulus for the direct engine calls: the + * public entry point reduces the base first, so this arm is dead from + * the API. */ + if ((op >= 7) && (op <= 9)) { + if (mp_copy(&m, &b) != MP_OKAY) { + goto done; + } + } - for (n = 1; n <= SP_FAULT_MAX_N; n++) { - mp_int a; - mp_int m; - mp_int r; + mcdc_fa_arm(n); + switch (op) { + case 0: + (void)mp_exptmod(&b, &e, &m, &r); + break; + case 1: + (void)mp_exptmod_ex(&b, &e, (int)m.used, &m, &r); + break; + case 2: + (void)mp_exptmod_nct(&b, &e, &m, &r); + break; + case 3: + (void)mp_exptmod(&b, &e, &m, &r); + break; + case 4: + (void)mp_exptmod_nct(&b, &e, &m, &r); + break; + case 5: + case 6: + (void)mp_exptmod(&b, &e, &m, &r); + break; + case 7: +#ifdef WB_HAVE_EXPTMOD_EX + (void)_sp_exptmod_ex(&b, &e, sp_count_bits(&e), &m, &r); +#endif + break; + case 8: +#ifdef WB_HAVE_EXPTMOD_MONT_EX + (void)_sp_exptmod_mont_ex(&b, &e, sp_count_bits(&e), &m, &r); +#endif + break; + case 9: +#ifdef WB_HAVE_EXPTMOD_NCT + (void)_sp_exptmod_nct(&b, &e, &m, &r); +#endif + break; + case 10: +#ifdef WB_HAVE_EXPTMOD_BASE_2 + (void)_sp_exptmod_base_2(&e, (int)e.used, &m, &r); +#endif + break; + case 11: + case 12: + case 13: + case 14: + (void)mp_invmod(&a, &m, &r); + break; + case 15: +#ifdef WOLFSSL_SP_INVMOD_MONT_CT + (void)mp_invmod_mont_ct(&a, &m, &r, (sp_digit)1); +#endif + break; + case 16: + (void)mp_div(&a, &m, &q, &r); + break; + case 17: + (void)mp_mod(&a, &m, &r); + break; + case 18: + (void)mp_mulmod(&a, &a, &m, &r); + break; + case 19: + (void)mp_sqrmod(&a, &m, &r); + break; + case 20: + (void)mp_gcd(&a, &m, &r); + break; + case 21: +#if !defined(NO_RSA) && defined(WOLFSSL_KEY_GEN) && \ + (!defined(WC_RSA_BLINDING) || defined(HAVE_FIPS) || defined(HAVE_SELFTEST)) + (void)mp_lcm(&a, &m, &r); +#endif + break; + case 22: +#ifdef WOLFSSL_SP_PRIME_GEN + { + int res = 0; + (void)mp_prime_is_prime(&m, 2, &res); + } +#endif + break; + case 23: +#if defined(WOLFSSL_SP_PRIME_GEN) && !defined(WC_NO_RNG) + if (wb_rng_ok) { + int res = 0; + (void)mp_prime_is_prime_ex(&m, 2, &res, &wb_rng); + } +#endif + break; + case 24: + (void)mp_mul(&a, &a, &r); + break; + case 25: + (void)mp_sqr(&a, &r); + break; - if (mp_init_multi(&a, &m, &r, NULL, NULL, NULL) != MP_OKAY) { - wb_fail = 1; - return; + /* ---- internal engines called directly ------------------------------- + * The public entry points do their own reduction/validation and allocate + * on the way in, so a fail-index that lands inside the engine is many + * allocations further along and different for every operand shape. + * Calling the engine directly puts its first allocation at index 1, which + * makes the sweep actually walk THAT engine's sites. ------------------ */ + case 26: + case 27: + case 28: + case 29: +#ifdef WOLFSSL_SP_INVMOD + (void)_sp_invmod(&a, &m, &r); +#endif + break; + case 30: +#ifdef WOLFSSL_SP_PRIME_GEN + { + int res = 0; + (void)_sp_prime_trials(&m, 2, &res); } - if (mp_read_radix(&a, WB_B, MP_RADIX_HEX) == MP_OKAY) { - if (mp_read_radix(&m, WB_M_ODD, MP_RADIX_HEX) == MP_OKAY) { - mcdc_fa_arm(n); - (void)mp_invmod(&a, &m, &r); -#ifdef WOLFSSL_SP_INVMOD_MONT_CT - (void)mp_invmod_mont_ct(&a, &m, &r, (sp_digit)1); #endif - mcdc_fa_disarm(); - } - /* Even modulus routes through the division-based inverse. */ - if (mp_read_radix(&m, WB_M_EVEN, MP_RADIX_HEX) == MP_OKAY) { - mcdc_fa_arm(n); - (void)mp_invmod(&a, &m, &r); - mcdc_fa_disarm(); - } + break; + case 31: +#if defined(WOLFSSL_SP_PRIME_GEN) && !defined(WC_NO_RNG) + if (wb_rng_ok) { + int res = 0; + (void)_sp_prime_random_trials(&m, 1, &res, &wb_rng); } - mp_free(&a); - mp_free(&m); - mp_free(&r); +#endif + break; + case 32: +#ifdef WOLFSSL_SP_PRIME_GEN + { + int res = 0; + sp_int n1; + sp_int rr; + + _sp_init_size(&n1, (sp_size_t)(m.used + 1U)); + _sp_init_size(&rr, (sp_size_t)(m.used * 2U + 1U)); + (void)sp_prime_miller_rabin(&m, &b, &res, &n1, &rr); + } +#endif + break; + case 33: +#if defined(WOLFSSL_SP_MATH_ALL) || !defined(NO_DH) || defined(HAVE_ECC) || \ + (!defined(NO_RSA) && !defined(WOLFSSL_RSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY)) + (void)_sp_div(&a, &m, &q, &r, (unsigned int)(a.used + 1U)); +#endif + break; + default: + break; } + mcdc_fa_disarm(); + +done: + mcdc_fa_disarm(); + mp_free(&a); + mp_free(&b); + mp_free(&e); + mp_free(&m); + mp_free(&r); + mp_free(&q); } -static void wb_div_mul_sweep(void) +static void wb_sweep(void) { int n; + int op; - for (n = 1; n <= SP_FAULT_MAX_N; n++) { - mp_int a; - mp_int b; - mp_int q; - mp_int rem; - - if (mp_init_multi(&a, &b, &q, &rem, NULL, NULL) != MP_OKAY) { - wb_fail = 1; - return; - } - if ((mp_read_radix(&a, WB_B, MP_RADIX_HEX) == MP_OKAY) && - (mp_read_radix(&b, WB_M_ODD, MP_RADIX_HEX) == MP_OKAY)) { - mcdc_fa_arm(n); - (void)mp_div(&a, &b, &q, &rem); - (void)mp_mod(&a, &b, &rem); - (void)mp_mulmod(&a, &a, &b, &rem); - (void)mp_sqrmod(&a, &b, &rem); - (void)mp_gcd(&a, &b, &q); - mcdc_fa_disarm(); + for (op = 0; op < WB_OP_COUNT; op++) { + for (n = 1; n <= SP_FAULT_MAX_N; n++) { + wb_op(op, n); } - mp_free(&a); - mp_free(&b); - mp_free(&q); - mp_free(&rem); } } -#if defined(WOLFSSL_KEY_GEN) || !defined(NO_DH) || !defined(NO_DSA) -static void wb_prime_sweep(void) +/* Baseline pass: the same operations with the injector installed but never + * armed, so the TRUE half of every `err == MP_OKAY` checkpoint is recorded in + * THIS binary too (llvm-cov computes MC/DC independence per binary). */ +static void wb_baseline(void) { - int n; - - for (n = 1; n <= SP_FAULT_MAX_N; n++) { - mp_int a; - int res = 0; + int op; - if (mp_init(&a) != MP_OKAY) { - wb_fail = 1; - return; - } - if (mp_read_radix(&a, WB_M_ODD, MP_RADIX_HEX) == MP_OKAY) { - mcdc_fa_arm(n); - (void)mp_prime_is_prime(&a, 2, &res); - mcdc_fa_disarm(); - } - mp_free(&a); + for (op = 0; op < WB_OP_COUNT; op++) { + wb_op(op, 0); } } -#else -static void wb_prime_sweep(void) -{ - WB_NOTE("prime testing not compiled; skipped"); -} -#endif #endif /* WOLFSSL_SP_MATH || WOLFSSL_SP_MATH_ALL */ @@ -238,15 +456,26 @@ int main(void) WB_NOTE("WOLFSSL_SMALL_STACK off: mp_int temporaries are stack arrays, so " "err cannot leave MP_OKAY; sweep runs but cannot fail one"); #endif +#ifndef WC_NO_RNG + wb_rng_ok = (wc_InitRng(&wb_rng) == 0); + if (!wb_rng_ok) { + WB_NOTE("RNG init failed; randomised prime sweep skipped"); + } +#endif + mcdc_fa_install(); - wb_exptmod_sweep(); - wb_invmod_sweep(); - wb_div_mul_sweep(); - wb_prime_sweep(); + wb_baseline(); + wb_sweep(); mcdc_fa_disarm(); mcdc_fa_restore(); + +#ifndef WC_NO_RNG + if (wb_rng_ok) { + (void)wc_FreeRng(&wb_rng); + } +#endif #endif printf("done (%s)\n", wb_fail ? "with skips" : "ok"); diff --git a/tests/unit-mcdc/test_sp_int_whitebox.c b/tests/unit-mcdc/test_sp_int_whitebox.c index c60172ef839..d1f88e4bd25 100644 --- a/tests/unit-mcdc/test_sp_int_whitebox.c +++ b/tests/unit-mcdc/test_sp_int_whitebox.c @@ -130,10 +130,1175 @@ static void wb_cnt_lsb_all_zero_digits(void) WB_NOTE("sp_cnt_lsb least-significant-zero-digit loop exercised"); } +/* ------------------------------------------------------------------------- * + * Shared helpers for the operand-shape classes below. + * + * Several of the residual decisions are capacity guards written against + * sp_int::size (the number of digits the destination was told it may use) + * and sp_int::used (how many digits the value occupies). The public API + * only ever hands sp_int.c destinations that sp_init()/sp_init_size() + * built, and those are either full SP_INT_DIGITS or comfortably large, so + * the "destination too small" / "operand at the compile ceiling" halves + * never occur. _sp_init_size() is the library's own internal sizer, so + * using it here builds exactly the object the guard is written for -- + * nothing is faked, only sized. + * ------------------------------------------------------------------------- */ + +/* Init a to full capacity and give it 'used' digits all equal to v. */ +static void wb_fill(sp_int* a, unsigned int used, sp_int_digit v) +{ + unsigned int i; + + _sp_init_size(a, SP_INT_DIGITS); + for (i = 0; i < used; i++) { + a->dp[i] = v; + } + a->used = (sp_size_t)used; +} + +/* Init a to full capacity and set it to a single-digit value. */ +static void wb_set_d(sp_int* a, sp_int_digit v) +{ + _sp_init_size(a, SP_INT_DIGITS); + a->dp[0] = v; + a->used = (sp_size_t)((v != 0) ? 1 : 0); +} + +/* a = 2^bits, written straight into the digit array. + * + * sp_mul_2d() would be the natural way to build these operands, but it is not + * compiled in every campaign variant (the reduced backend drops it), and this + * TU has to build under all of them. Returns MP_VAL when the requested width + * does not fit the compile-time digit ceiling so callers can skip that row. */ +static int wb_pow2(sp_int* a, int bits) +{ + unsigned int d = (unsigned int)bits / SP_WORD_SIZE; + unsigned int b = (unsigned int)bits % SP_WORD_SIZE; + unsigned int i; + + if ((bits < 0) || (d >= (unsigned int)SP_INT_DIGITS)) { + return MP_VAL; + } + + _sp_init_size(a, SP_INT_DIGITS); + for (i = 0; i <= d; i++) { + a->dp[i] = 0; + } + a->dp[d] = (sp_int_digit)1 << b; + a->used = (sp_size_t)(d + 1); + + return MP_OKAY; +} + +/* ------------------------------------------------------------------------- * + * Class 3: the ALLOC_SP_INT / ALLOC_SP_INT_ARRAY compile-ceiling macros. + * + * if (((err) == MP_OKAY) && ((s) > SP_INT_DIGITS)) { (err) = MP_VAL; } + * + * Every in-library expansion sizes 's' from operands the caller already + * range-checked, so the second operand is never true, and reaches the macro + * with err == MP_OKAY, so the first is never false. Drive the macro itself: + * it is an ordinary macro in scope in this TU, and llvm-cov attributes the + * expansion's conditions to the macro's definition in sp_int.c. + * ------------------------------------------------------------------------- */ +static void wb_alloc_ceiling_macros(void) +{ + { + int err = WC_NO_ERR_TRACE(MP_VAL); + DECL_SP_INT(t, 1); + + /* First operand FALSE: an error is already latched, so the macro + * must leave it alone and allocate nothing. */ + ALLOC_SP_INT(t, 1, err, NULL); + FREE_SP_INT(t, NULL); + (void)t; + } + { + int err = MP_OKAY; + DECL_SP_INT(t, 1); + + /* Both operands TRUE: a size above the compile-time digit ceiling + * is rejected before any allocation is attempted. */ + ALLOC_SP_INT(t, SP_INT_DIGITS + 1, err, NULL); + FREE_SP_INT(t, NULL); + (void)t; + } + { + int err = MP_OKAY; + DECL_SP_INT_ARRAY(td, 1, 2); + + /* Same two rows for the array form. */ + ALLOC_SP_INT_ARRAY(td, SP_INT_DIGITS + 1, 2, err, NULL); + FREE_SP_INT_ARRAY(td, NULL); + } + { + int err = WC_NO_ERR_TRACE(MP_VAL); + DECL_SP_INT_ARRAY(td, 1, 2); + + ALLOC_SP_INT_ARRAY(td, 1, 2, err, NULL); + FREE_SP_INT_ARRAY(td, NULL); + } + + WB_NOTE("ALLOC_SP_INT/ALLOC_SP_INT_ARRAY ceiling macro rows exercised"); +} + +/* ------------------------------------------------------------------------- * + * Class 4: sp_div() / _sp_div() capacity and sign guards. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_MATH_ALL) || !defined(NO_DH) || defined(HAVE_ECC) || \ + (!defined(NO_RSA) && !defined(WOLFSSL_RSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY)) +static void wb_div_capacity(void) +{ + sp_int a; + sp_int d; + sp_int q; + sp_int rem; + + /* --- remainder-capacity guard, a->used <= d->used arm --------------- * + * if ((a->used <= d->used) && (rem->size < a->used + 1)) + * Row TT: same digit count, remainder deliberately one digit short. */ + wb_fill(&a, 2, (sp_int_digit)0x123456789ULL); + wb_fill(&d, 2, (sp_int_digit)0x9876543ULL); + _sp_init_size(&rem, 2); + (void)sp_div(&a, &d, NULL, &rem); + + /* Row TF: same shape, remainder now big enough - guard passes. */ + _sp_init_size(&rem, SP_INT_DIGITS); + (void)sp_div(&a, &d, NULL, &rem); + + /* Row F: dividend longer than divisor takes the other arm. */ + wb_fill(&a, 4, (sp_int_digit)0x123456789ULL); + _sp_init_size(&rem, SP_INT_DIGITS); + (void)sp_div(&a, &d, NULL, &rem); + + /* --- top-of-capacity dividend: the shift-would-overflow guard ------- * + * if ((bits != SP_WORD_SIZE) && (sp_count_bits(a) + bits > ...)) + * Only evaluated when a->used == SP_INT_DIGITS, which no public caller + * can reach because every value that big is rejected earlier. */ + wb_fill(&a, (unsigned int)SP_INT_DIGITS, (sp_int_digit)~(sp_int_digit)0); + wb_fill(&d, 2, (sp_int_digit)0x9876543ULL); + _sp_init_size(&rem, SP_INT_DIGITS); + /* d's bit count is not a multiple of the word size (bits != word size) + * and a is full width, so the shifted dividend would not fit: TT. */ + (void)sp_div(&a, &d, NULL, &rem); + + /* Row TF: still full width, but a's top digit is 1 so the shift fits. */ + wb_fill(&a, (unsigned int)SP_INT_DIGITS, (sp_int_digit)1); + _sp_init_size(&rem, SP_INT_DIGITS); + (void)sp_div(&a, &d, NULL, &rem); + + /* Row F: divisor whose bit count IS a multiple of the word size, so no + * shift is needed at all. */ + wb_fill(&a, (unsigned int)SP_INT_DIGITS, (sp_int_digit)1); + wb_set_d(&d, (sp_int_digit)1 << (SP_WORD_SIZE - 1)); + _sp_init_size(&rem, SP_INT_DIGITS); + (void)sp_div(&a, &d, NULL, &rem); + +#if (defined(WOLFSSL_SMALL_STACK) || defined(SP_ALLOC)) && \ + !defined(WOLFSSL_SP_NO_MALLOC) + /* --- _sp_div() temporary-reuse decisions (heap temporaries only) ---- * + * if ((rem != NULL) && (rem != d) && (rem->size > a->used)) + * if ((r != NULL) && (r != d)) + * The remainder is reused as scratch only when it is strictly bigger + * than the dividend, and the quotient only when it is not the divisor. + * sp_div()'s own capacity checks let a remainder that is big enough for + * the RESULT but not bigger than the dividend through, which is the + * false row of the third operand; and r == d is a legal aliasing every + * public caller happens not to use. */ + wb_fill(&a, 4, (sp_int_digit)0x1234567ULL); + wb_fill(&d, 2, (sp_int_digit)0x89abULL); + _sp_init_size(&rem, 3); + (void)sp_div(&a, &d, NULL, &rem); + + /* Quotient aliased onto the divisor: (r != d) false. */ + wb_fill(&a, 4, (sp_int_digit)0x1234567ULL); + wb_fill(&d, 2, (sp_int_digit)0x89abULL); + (void)sp_div(&a, &d, &d, NULL); +#endif + + /* Ordinary division with three distinct, amply sized sp_ints: supplies the + * true row of both reuse decisions in this same binary. */ + wb_fill(&a, 4, (sp_int_digit)0x1234567ULL); + wb_fill(&d, 2, (sp_int_digit)0x89abULL); + _sp_init_size(&q, SP_INT_DIGITS); + _sp_init_size(&rem, SP_INT_DIGITS); + (void)sp_div(&a, &d, &q, &rem); + +#ifdef WOLFSSL_SP_INT_NEGATIVE + /* --- quotient sign decision ---------------------------------------- * + * if ((r->used == 0) || (signA == signD)) r->sign = MP_ZPOS; + * Row TF is a zero quotient with mismatched signs (|a| < |d|), row FT a + * nonzero quotient with matching signs, row FF mismatched signs. */ + wb_fill(&a, 1, (sp_int_digit)3); + a.sign = MP_NEG; + wb_fill(&d, 2, (sp_int_digit)0x89abULL); + _sp_init_size(&q, SP_INT_DIGITS); + _sp_init_size(&rem, SP_INT_DIGITS); + (void)sp_div(&a, &d, &q, &rem); + + wb_fill(&a, 4, (sp_int_digit)0x1234567ULL); + a.sign = MP_NEG; + _sp_init_size(&q, SP_INT_DIGITS); + _sp_init_size(&rem, SP_INT_DIGITS); + (void)sp_div(&a, &d, &q, &rem); + + a.sign = MP_ZPOS; + _sp_init_size(&q, SP_INT_DIGITS); + _sp_init_size(&rem, SP_INT_DIGITS); + (void)sp_div(&a, &d, &q, &rem); +#endif + + WB_NOTE("sp_div capacity / reuse / sign rows exercised"); +} +#else +static void wb_div_capacity(void) +{ + WB_NOTE("sp_div not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 5: the internal exponentiation engines, called directly. + * + * sp_exptmod() reduces the base below the modulus BEFORE dispatching, so + * the engines' own "base is not less than modulus" arm - and in particular + * its "base is a multiple of the modulus, result is zero, we are done" + * sub-case - is dead from the public API. Every `(!done) && ...` checkpoint + * downstream of it therefore only ever sees done == 0. Calling the engines + * directly with base >= modulus supplies the missing rows. + * ------------------------------------------------------------------------- */ +#if (defined(WOLFSSL_SP_MATH_ALL) && !defined(WOLFSSL_RSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY)) || !defined(NO_DH) || \ + defined(OPENSSL_ALL) + #define WB_HAVE_EXPTMOD_EX +#endif +#if (defined(WOLFSSL_SP_MATH_ALL) && ((!defined(WOLFSSL_RSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY)) || !defined(NO_DH))) || \ + defined(OPENSSL_ALL) + #define WB_HAVE_EXPTMOD_MONT_EX +#endif +#if defined(WOLFSSL_SP_MATH_ALL) || defined(WOLFSSL_HAVE_SP_DH) +#if defined(WOLFSSL_SP_FAST_NCT_EXPTMOD) || !defined(WOLFSSL_SP_SMALL) + #define WB_HAVE_EXPTMOD_NCT +#endif +#endif + +static void wb_exptmod_engines(void) +{ +#if defined(WB_HAVE_EXPTMOD_EX) || defined(WB_HAVE_EXPTMOD_MONT_EX) || \ + defined(WB_HAVE_EXPTMOD_NCT) + sp_int b; + sp_int e; + sp_int m; + sp_int r; + int bits; + + /* Odd, two-digit modulus: takes the Montgomery engines. */ + wb_fill(&m, 2, (sp_int_digit)0); + m.dp[0] = (sp_int_digit)0x0fffffffffffffc5ULL; + m.dp[1] = (sp_int_digit)0x00000000000000f1ULL; + wb_set_d(&e, (sp_int_digit)0x10001); + bits = sp_count_bits(&e); + + /* Base EQUAL to the modulus: the reduction inside the engine yields + * zero, so the engine sets the result to zero and marks itself done - + * the only producer of done == 1 in these functions. */ + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_copy(&m, &b); +#ifdef WB_HAVE_EXPTMOD_EX + (void)_sp_exptmod_ex(&b, &e, bits, &m, &r); +#endif +#ifdef WB_HAVE_EXPTMOD_MONT_EX + _sp_init_size(&r, SP_INT_DIGITS); + (void)_sp_exptmod_mont_ex(&b, &e, bits, &m, &r); +#endif +#ifdef WB_HAVE_EXPTMOD_NCT + _sp_init_size(&r, SP_INT_DIGITS); + (void)_sp_exptmod_nct(&b, &e, &m, &r); +#endif + + /* Base GREATER than the modulus but not a multiple of it: same arm, + * "reduced base is zero" false. */ + (void)sp_copy(&m, &b); + b.dp[0]++; + b.dp[1] += 3; +#ifdef WB_HAVE_EXPTMOD_EX + _sp_init_size(&r, SP_INT_DIGITS); + (void)_sp_exptmod_ex(&b, &e, bits, &m, &r); +#endif +#ifdef WB_HAVE_EXPTMOD_MONT_EX + _sp_init_size(&r, SP_INT_DIGITS); + (void)_sp_exptmod_mont_ex(&b, &e, bits, &m, &r); +#endif +#ifdef WB_HAVE_EXPTMOD_NCT + _sp_init_size(&r, SP_INT_DIGITS); + (void)_sp_exptmod_nct(&b, &e, &m, &r); +#endif + + /* Base already less than the modulus: the ordinary row, kept in this + * same binary so each pair is complete here. */ + wb_set_d(&b, (sp_int_digit)3); +#ifdef WB_HAVE_EXPTMOD_EX + _sp_init_size(&r, SP_INT_DIGITS); + (void)_sp_exptmod_ex(&b, &e, bits, &m, &r); +#endif +#ifdef WB_HAVE_EXPTMOD_MONT_EX + _sp_init_size(&r, SP_INT_DIGITS); + (void)_sp_exptmod_mont_ex(&b, &e, bits, &m, &r); +#endif +#ifdef WB_HAVE_EXPTMOD_NCT + _sp_init_size(&r, SP_INT_DIGITS); + (void)_sp_exptmod_nct(&b, &e, &m, &r); +#endif + + WB_NOTE("internal exptmod engines driven with base >= modulus"); +#else + WB_NOTE("internal exptmod engines not compiled; skipped"); +#endif +} + +/* ------------------------------------------------------------------------- * + * Class 6: sp_exptmod() dispatch operand shapes. + * ------------------------------------------------------------------------- */ +#if (defined(WOLFSSL_SP_MATH_ALL) && !defined(WOLFSSL_RSA_VERIFY_ONLY)) || \ + !defined(NO_DH) || defined(OPENSSL_ALL) +static void wb_exptmod_dispatch(void) +{ + sp_int b; + sp_int e; + sp_int m; + sp_int r; + + wb_fill(&m, 2, (sp_int_digit)0); + m.dp[0] = (sp_int_digit)0x0fffffffffffffc5ULL; + m.dp[1] = (sp_int_digit)0x00000000000000f1ULL; + wb_set_d(&e, (sp_int_digit)0x10001); + + /* Result aliased onto the exponent while the base needs reducing: the + * reduction would clobber an input, so it is rejected. Public callers + * always pass three distinct sp_ints. */ + (void)sp_copy(&m, &b); + b.dp[0]++; + (void)sp_exptmod(&b, &e, &m, &e); + + /* Same shape with a destination that is neither input: both operands of + * the aliasing test false, which is its missing row. */ + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_exptmod(&b, &e, &m, &r); + + /* Result too small to hold the intermediate double-width value: no + * public caller sizes a destination that tightly. */ + wb_set_d(&b, (sp_int_digit)3); + _sp_init_size(&r, (unsigned int)(m.used * 2)); + (void)sp_exptmod(&b, &e, &m, &r); + + /* Base exactly two with an EVEN multi-digit modulus: the base-2 engine + * is selected only for an odd modulus, so this is the false row of that + * selector's oddness operand. */ + wb_set_d(&b, (sp_int_digit)2); + m.dp[0] &= ~(sp_int_digit)1; + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_exptmod(&b, &e, &m, &r); + + /* Same even modulus with a base that is not two: the second selector's + * oddness operand false as well. */ + wb_set_d(&b, (sp_int_digit)3); + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_exptmod(&b, &e, &m, &r); + +#ifdef WOLFSSL_SP_INT_NEGATIVE + /* Negative exponent, then negative modulus: unsupported, and each is + * the sole true operand of its OR in turn. */ + m.dp[0] |= (sp_int_digit)1; + wb_set_d(&b, (sp_int_digit)3); + _sp_init_size(&r, SP_INT_DIGITS); + e.sign = MP_NEG; + (void)sp_exptmod(&b, &e, &m, &r); + e.sign = MP_ZPOS; + m.sign = MP_NEG; + (void)sp_exptmod(&b, &e, &m, &r); + m.sign = MP_ZPOS; + + /* Same pair through the non-constant-time entry point. */ + e.sign = MP_NEG; + (void)sp_exptmod_nct(&b, &e, &m, &r); + e.sign = MP_ZPOS; + m.sign = MP_NEG; + (void)sp_exptmod_nct(&b, &e, &m, &r); + m.sign = MP_ZPOS; +#endif + + /* Modulus of one: the degenerate-case check declares the answer and marks + * the call done, which is the only producer of a false "not done yet" + * operand at the intermediate-space check further down. Every modulus a + * public caller uses is larger than one. */ + wb_set_d(&m, (sp_int_digit)1); + wb_set_d(&b, (sp_int_digit)3); + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_exptmod_ex(&b, &e, 1, &m, &r); + + /* Zero modulus: latches the error so every later checkpoint in the + * dispatch chain is evaluated with the error already set. */ + _sp_init_size(&m, SP_INT_DIGITS); + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_exptmod(&b, &e, &m, &r); + (void)sp_exptmod_nct(&b, &e, &m, &r); + + WB_NOTE("sp_exptmod dispatch operand shapes exercised"); +} +#else +static void wb_exptmod_dispatch(void) +{ + WB_NOTE("sp_exptmod not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 7: sp_invmod() negative-modulus guard. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_INVMOD) && defined(WOLFSSL_SP_INT_NEGATIVE) +static void wb_invmod_negative(void) +{ + sp_int a; + sp_int m; + sp_int r; + + wb_set_d(&a, (sp_int_digit)3); + wb_fill(&m, 2, (sp_int_digit)0); + m.dp[0] = (sp_int_digit)0x0fffffffffffffc5ULL; + m.dp[1] = (sp_int_digit)0x00000000000000f1ULL; + _sp_init_size(&r, SP_INT_DIGITS); + + /* Negative modulus is rejected. */ + m.sign = MP_NEG; + (void)sp_invmod(&a, &m, &r); + m.sign = MP_ZPOS; + + /* Error already latched (destination aliased onto the modulus) so the + * sign test's first operand is false. */ + (void)sp_invmod(&a, &m, &m); + + /* Ordinary, successful inverse with a positive modulus: the false row of + * the sign test, needed in THIS binary to complete its pair. */ + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_invmod(&a, &m, &r); + + WB_NOTE("sp_invmod negative-modulus rows exercised"); +} +#else +static void wb_invmod_negative(void) +{ + WB_NOTE("sp_invmod negative-modulus rows not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 7b: the division-based inverse's "no inverse exists" arm. + * + * if ((err == MP_OKAY) && (!sp_iszero(y))) err = MP_VAL; + * + * sp_invmod() only selects _sp_invmod_div() for a modulus of at least 1024 + * bits, and the campaign's API tests only ever ask for an inverse that + * exists, so the loop's leftover is always zero there. Ask for the inverse + * of a value that shares a factor with the modulus instead. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_INVMOD) && !defined(WOLFSSL_SP_LOW_MEM) && \ + !defined(WOLFSSL_SP_SMALL) && (!defined(NO_RSA) || !defined(NO_DH)) +static void wb_invmod_no_inverse(void) +{ + sp_int a; + sp_int m; + sp_int r; + + /* m = 3 * (2^1022 + 1): odd, 1024 bits, and divisible by three. */ + if (wb_pow2(&m, 1022) != MP_OKAY) { + WB_NOTE("digit ceiling below 1024 bits; no-inverse rows skipped"); + return; + } + if (sp_add_d(&m, 1, &m) != MP_OKAY) { + wb_fail = 1; + return; + } + if (sp_mul_d(&m, 3, &m) != MP_OKAY) { + wb_fail = 1; + return; + } + + /* gcd(3, m) == 3, so no inverse exists: the leftover is nonzero. */ + wb_set_d(&a, (sp_int_digit)3); + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_invmod(&a, &m, &r); + + /* Coprime operand through the same engine: leftover zero, the ordinary + * row of the same decision. */ + wb_set_d(&a, (sp_int_digit)5); + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_invmod(&a, &m, &r); + + WB_NOTE("division-based inverse no-inverse rows exercised"); +} +#else +static void wb_invmod_no_inverse(void) +{ + WB_NOTE("division-based inverse not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 7c: the ALLOC_SP_INT / ALLOC_SP_INT_ARRAY ceiling macros, driven at + * REAL library expansion sites. + * + * The prime-test helpers each make several allocations in a row from operand + * sizes, and pass the SAME err through all of them: + * + * ALLOC_SP_INT(n1, a->used + 1, err, NULL); + * ALLOC_SP_INT(r, a->used + 1, err, NULL); + * ALLOC_SP_INT(b, a->used * 2 + 1, err, NULL); + * + * so an operand at the digit ceiling makes the FIRST call latch the error and + * the later ones see the macro's first operand false, while an operand just + * over half the ceiling makes only the doubled size exceed it - the macro's + * second operand true. sp_prime_is_prime() rejects both candidate shapes up + * front, so neither is reachable from the public entry point; the helpers are + * file-static and called directly here. + * ------------------------------------------------------------------------- */ +#ifdef WOLFSSL_SP_PRIME_GEN +static void wb_alloc_ceiling_sites(void) +{ + sp_int a; + int res = 0; + + /* Operand at the ceiling: the first allocation latches the error and + * every later macro in the chain sees it. */ + wb_fill(&a, (unsigned int)SP_INT_DIGITS, (sp_int_digit)3); + (void)_sp_prime_trials(&a, 1, &res); + + /* Operand just over half the ceiling: only the doubled temporary is over + * the limit, so the size operand is the one that fires. */ + wb_fill(&a, (unsigned int)(SP_INT_DIGITS / 2 + 1), (sp_int_digit)3); + (void)_sp_prime_trials(&a, 1, &res); + + /* Ordinary candidate: both operands false, allocations happen. */ + wb_set_d(&a, (sp_int_digit)0x088886ffdb344693ULL); + (void)_sp_prime_trials(&a, 1, &res); + +#ifndef WC_NO_RNG + { + WC_RNG rng; + + if (wc_InitRng(&rng) == 0) { + /* Same three rows for the array form of the macro. */ + wb_fill(&a, (unsigned int)SP_INT_DIGITS, (sp_int_digit)3); + (void)_sp_prime_random_trials(&a, 1, &res, &rng); + + wb_fill(&a, (unsigned int)(SP_INT_DIGITS / 2 + 1), + (sp_int_digit)3); + (void)_sp_prime_random_trials(&a, 1, &res, &rng); + + wb_set_d(&a, (sp_int_digit)0x088886ffdb344693ULL); + (void)_sp_prime_random_trials(&a, 1, &res, &rng); + + (void)wc_FreeRng(&rng); + } + else { + wb_fail = 1; + } + } +#endif + + WB_NOTE("allocation-ceiling macro rows driven at library call sites"); +} +#else +static void wb_alloc_ceiling_sites(void) +{ + WB_NOTE("prime helpers not compiled; ceiling-macro sites skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 7d: the SP-accelerated fixed-size modexp dispatch. + * + * if ((mBits == 1024) && sp_isodd(m) && (bBits <= 1024) && (eBits <= 1024)) + * + * Only compiled when an SP-accelerated RSA/DH backend is selected. mBits, + * bBits and eBits are all counted from the ORIGINAL operands, before the base + * is reduced, so a base or exponent wider than the modulus is a reachable + * shape - just not one the API tests produce, because they always pass + * already-reduced RSA/DH operands. Each row below leaves exactly one operand + * false. + * + * The fall-through rows pass an explicit digit count of 1 to the _ex entry + * point: the generic engine's loop length is that count, not the modulus + * size, so the whole class stays inside the time budget. + * ------------------------------------------------------------------------- */ +#if (defined(WOLFSSL_SP_MATH) || defined(WOLFSSL_SP_MATH_ALL)) && \ + ((defined(WOLFSSL_HAVE_SP_RSA) && !defined(WOLFSSL_RSA_PUBLIC_ONLY)) || \ + defined(WOLFSSL_HAVE_SP_DH)) +static void wb_sp_backend_dispatch_one(int bits) +{ + sp_int b; + sp_int e; + sp_int m; + sp_int r; + + /* m = 2^(bits-1) + 0x61: exactly 'bits' bits and odd. */ + if ((wb_pow2(&m, bits - 1) != MP_OKAY) || + (sp_add_d(&m, 0x61, &m) != MP_OKAY)) { + /* This width does not fit the configured digit ceiling. */ + return; + } + + wb_set_d(&b, (sp_int_digit)3); + wb_set_d(&e, (sp_int_digit)0x10001); + _sp_init_size(&r, SP_INT_DIGITS); + + /* All operands true: the accelerated routine is selected. */ + (void)sp_exptmod(&b, &e, &m, &r); + + /* Base wider than the modulus: the base-width operand false. */ + /* Verified to reach the selector with mBits == bits, the modulus odd and + * bBits == bits + 1 (i.e. exactly this operand false, all earlier ones + * true) - see the residuals note on why llvm-cov still does not pair it. */ + if ((wb_pow2(&b, bits) == MP_OKAY) && (sp_add_d(&b, 5, &b) == MP_OKAY)) { + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_exptmod_ex(&b, &e, 1, &m, &r); + } + + /* Exponent wider than the modulus: the exponent-width operand false. */ + wb_set_d(&b, (sp_int_digit)3); + if ((wb_pow2(&e, bits) == MP_OKAY) && (sp_add_d(&e, 5, &e) == MP_OKAY)) { + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_exptmod_ex(&b, &e, 1, &m, &r); + } + + /* Even modulus of the same width: the oddness operand false. */ + wb_set_d(&e, (sp_int_digit)0x10001); + m.dp[0] &= ~(sp_int_digit)1; + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_exptmod_ex(&b, &e, 1, &m, &r); +} + +static void wb_sp_backend_dispatch(void) +{ +#ifndef WOLFSSL_SP_NO_2048 + wb_sp_backend_dispatch_one(1024); + wb_sp_backend_dispatch_one(2048); +#endif +#ifndef WOLFSSL_SP_NO_3072 + wb_sp_backend_dispatch_one(1536); + wb_sp_backend_dispatch_one(3072); +#endif +#ifdef WOLFSSL_SP_4096 + wb_sp_backend_dispatch_one(4096); +#endif + WB_NOTE("SP fixed-size modexp dispatch shapes exercised"); +} +#else +static void wb_sp_backend_dispatch(void) +{ + WB_NOTE("no SP-accelerated modexp backend; dispatch shapes skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 8: sp_gcd() / prime-test capacity and small-composite guards. + * ------------------------------------------------------------------------- */ +#if !defined(NO_RSA) && defined(WOLFSSL_KEY_GEN) +static void wb_gcd_capacity(void) +{ + sp_int a; + sp_int b; + sp_int r; + + /* Operand at the compile-time digit ceiling: rejected. Reached only by + * building the value directly - every API path clamps first. */ + wb_fill(&a, (unsigned int)SP_INT_DIGITS, (sp_int_digit)3); + wb_fill(&b, 2, (sp_int_digit)5); + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_gcd(&a, &b, &r); + (void)sp_gcd(&b, &a, &r); + + /* Result too small for the smaller operand, taking the second arm of + * the capacity OR (b shorter than a). */ + wb_fill(&a, 4, (sp_int_digit)6); + wb_fill(&b, 2, (sp_int_digit)4); + _sp_init_size(&r, 1); + (void)sp_gcd(&a, &b, &r); + + /* Same shapes, destination big enough: the ordinary row. */ + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_gcd(&a, &b, &r); + + WB_NOTE("sp_gcd capacity rows exercised"); +} +#else +static void wb_gcd_capacity(void) +{ + WB_NOTE("sp_gcd not compiled; skipped"); +} +#endif + +#ifdef WOLFSSL_SP_PRIME_GEN +static void wb_prime_shapes(void) +{ + sp_int a; + sp_int b; + sp_int n1; + sp_int r; + int res = 0; + + /* A value that is the product of the first small primes: the composite + * trial-division loop finds a zero remainder on its first composite, + * which no ordinary prime candidate ever does. */ + wb_set_d(&a, (sp_int_digit)0x088886ffdb344692ULL); + (void)sp_prime_is_prime(&a, 1, &res); + + /* Same call with a value the composite division does NOT divide. */ + wb_set_d(&a, (sp_int_digit)0x088886ffdb344693ULL); + (void)sp_prime_is_prime(&a, 1, &res); + +#ifdef WOLFSSL_SP_INT_NEGATIVE + /* Candidate of negative one: the "is it one" shortcut's sign operand is + * the only one that differs, and sp_prime_is_prime() (unlike its + * randomised sibling) has no sign check ahead of it. */ + wb_set_d(&a, (sp_int_digit)1); + a.sign = MP_NEG; + (void)sp_prime_is_prime(&a, 1, &res); + a.sign = MP_ZPOS; + (void)sp_prime_is_prime(&a, 1, &res); +#endif + + /* Miller-Rabin driven directly so the witness makes the squaring loop + * itself find one - i.e. the loop, not the post-loop comparison, + * declares the candidate composite. */ + wb_set_d(&a, (sp_int_digit)21); + wb_set_d(&b, (sp_int_digit)8); + _sp_init_size(&n1, SP_INT_DIGITS); + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_prime_miller_rabin(&a, &b, &res, &n1, &r); + + /* Witness that leaves the loop without hitting one: the post-loop + * comparison is what rejects it. */ + wb_set_d(&a, (sp_int_digit)15); + wb_set_d(&b, (sp_int_digit)2); + _sp_init_size(&n1, SP_INT_DIGITS); + _sp_init_size(&r, SP_INT_DIGITS); + (void)sp_prime_miller_rabin(&a, &b, &res, &n1, &r); + +#ifndef WC_NO_RNG + { + WC_RNG rng; + + if (wc_InitRng(&rng) == 0) { + /* Candidate at the compile ceiling for the randomised entry + * point: rejected before any trial is run. */ + wb_fill(&a, (unsigned int)(SP_INT_DIGITS / 2 + 1), + (sp_int_digit)3); + (void)sp_prime_is_prime_ex(&a, 1, &res, &rng); + + /* Same call with a candidate that fits. */ + wb_set_d(&a, (sp_int_digit)0x088886ffdb344693ULL); + (void)sp_prime_is_prime_ex(&a, 1, &res, &rng); + +#ifdef WOLFSSL_SP_INT_NEGATIVE + /* Negative candidate: rejected. */ + wb_set_d(&a, (sp_int_digit)7); + a.sign = MP_NEG; + (void)sp_prime_is_prime_ex(&a, 1, &res, &rng); +#endif + (void)wc_FreeRng(&rng); + } + else { + wb_fail = 1; + WB_NOTE("RNG init failed; randomised prime rows skipped"); + } + } +#endif + + WB_NOTE("prime-test operand shapes exercised"); +} +#else +static void wb_prime_shapes(void) +{ + WB_NOTE("prime generation not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 9: sp_sqrmod() aliasing + ceiling guard. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_MATH_ALL) || \ + (!defined(NO_RSA) && !defined(WOLFSSL_RSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY)) || !defined(NO_DH) || defined(HAVE_ECC) +static void wb_sqrmod_ceiling(void) +{ + sp_int a; + sp_int m; + + /* Destination aliased onto the modulus with an operand whose square + * would exceed the compile ceiling: all three operands true. */ + wb_fill(&a, (unsigned int)(SP_INT_DIGITS / 2 + 1), (sp_int_digit)3); + wb_fill(&m, 2, (sp_int_digit)5); + (void)sp_sqrmod(&a, &m, &m); + + /* Same aliasing, operand small enough: third operand false. */ + wb_fill(&a, 1, (sp_int_digit)3); + wb_fill(&m, 2, (sp_int_digit)5); + (void)sp_sqrmod(&a, &m, &m); + + /* Error already latched by the NULL check, so the first operand is + * false and the aliasing test is not reached. */ + (void)sp_sqrmod(NULL, &m, &m); + + WB_NOTE("sp_sqrmod aliasing/ceiling rows exercised"); +} +#else +static void wb_sqrmod_ceiling(void) +{ + WB_NOTE("sp_sqrmod not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 10: fixed-length binary and hex output edge rows. + * ------------------------------------------------------------------------- */ +static void wb_output_edges(void) +{ + sp_int a; + byte out[4]; + + /* Buffer fills while the MOST significant digit still has nonzero bits + * left: the "there is more of this digit" operand of the truncation + * test. Callers size the buffer with sp_unsigned_bin_size() so it never + * happens in the library. */ + wb_set_d(&a, (sp_int_digit)0x0102); + (void)sp_to_unsigned_bin_len(&a, out, 1); + + /* Buffer fills exactly: nothing of the digit is left over. */ + wb_set_d(&a, (sp_int_digit)0x02); + (void)sp_to_unsigned_bin_len(&a, out, 1); + +#if (defined(WOLFSSL_SP_MATH_ALL) && !defined(WOLFSSL_RSA_VERIFY_ONLY)) || \ + defined(WC_MP_TO_RADIX) + { + char str[SP_INT_DIGITS * (SP_WORD_SIZE / 4) + 4]; + + /* Non-normalized value whose most significant digit is entirely + * zero: the leading-zero-byte scan runs off the end of the digit + * instead of breaking out on a nonzero byte. sp_clamp() on every + * public mutator prevents a caller from producing this. */ + _sp_init_size(&a, SP_INT_DIGITS); + a.dp[0] = 1; + a.dp[1] = 0; + a.used = 2; + (void)sp_tohex(&a, str); + + /* Ordinary normalized value: the scan breaks out on a nonzero byte, + * the other half of the same loop condition. */ + wb_set_d(&a, (sp_int_digit)0x1234); + (void)sp_tohex(&a, str); + } +#endif + + WB_NOTE("binary/hex output edge rows exercised"); +} + +/* ------------------------------------------------------------------------- * + * Class 11: _sp_mulmod_tmp() zero-operand shortcut. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_MATH_ALL) || defined(WOLFSSL_HAVE_SP_DH) || \ + defined(WOLFCRYPT_HAVE_ECCSI) || \ + (!defined(NO_RSA) && defined(WOLFSSL_KEY_GEN)) || defined(OPENSSL_ALL) +static void wb_mulmod_tmp_zero(void) +{ + sp_int a; + sp_int b; + sp_int m; + sp_int r; + + wb_fill(&m, 2, (sp_int_digit)5); + _sp_init_size(&r, SP_INT_DIGITS); + + /* First operand zero: the shortcut's first test true. sp_mulmod() + * screens zero operands out before this helper is reached. */ + _sp_init_size(&a, SP_INT_DIGITS); + wb_set_d(&b, (sp_int_digit)3); + (void)_sp_mulmod_tmp(&a, &b, &m, &r); + + /* Second operand zero: first test false, second true. */ + wb_set_d(&a, (sp_int_digit)3); + _sp_init_size(&b, SP_INT_DIGITS); + (void)_sp_mulmod_tmp(&a, &b, &m, &r); + + /* Neither zero: both false. */ + wb_set_d(&b, (sp_int_digit)4); + (void)_sp_mulmod_tmp(&a, &b, &m, &r); + + WB_NOTE("_sp_mulmod_tmp zero-operand shortcut exercised"); +} +#else +static void wb_mulmod_tmp_zero(void) +{ + WB_NOTE("_sp_mulmod_tmp not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 12: _sp_sub_off() offset-copy loop. + * + * for (; (i < o) && (i < a->used); i++) r->dp[i] = a->dp[i]; + * + * The loop copies the digits below the offset. Every in-library caller uses + * an offset no larger than the operand, so the loop always ends on the + * offset test - it never runs out of source digits first. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_MATH_ALL) || defined(WOLFSSL_SP_INT_NEGATIVE) || \ + !defined(NO_DH) || defined(HAVE_ECC) || (!defined(NO_RSA) && \ + !defined(WOLFSSL_RSA_VERIFY_ONLY)) +static void wb_sub_off_loop(void) +{ + sp_int a; + sp_int b; + sp_int r; + + /* Offset larger than the source: the loop ends because it ran out of + * source digits. */ + wb_fill(&a, 2, (sp_int_digit)0x1234); + wb_fill(&b, 1, (sp_int_digit)1); + _sp_init_size(&r, SP_INT_DIGITS); + _sp_sub_off(&a, &b, &r, (sp_size_t)4); + + /* Offset inside the source: the loop ends on the offset instead. */ + wb_fill(&a, 4, (sp_int_digit)0x1234); + _sp_init_size(&r, SP_INT_DIGITS); + _sp_sub_off(&a, &b, &r, (sp_size_t)2); + + WB_NOTE("_sp_sub_off offset-copy loop exercised"); +} +#else +static void wb_sub_off_loop(void) +{ + WB_NOTE("_sp_sub_off not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 13: _sp_add_d() carry-overflow guard. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_ADD_D) || (defined(WOLFSSL_SP_INT_NEGATIVE) && \ + defined(WOLFSSL_SP_SUB_D)) || defined(WOLFSSL_SP_READ_RADIX_10) +static void wb_add_d_overflow(void) +{ + sp_int a; + sp_int r; + + /* All-ones operand plus one carries out of every digit, and the + * destination has no room for the extra word: the error is latched and + * the "copy the rest of the digits" test sees it. */ + wb_fill(&a, 3, (sp_int_digit)~(sp_int_digit)0); + _sp_init_size(&r, 3); + (void)_sp_add_d(&a, (sp_int_digit)1, &r); + + /* Same carry-out with room for the extra word: no error. */ + wb_fill(&a, 3, (sp_int_digit)~(sp_int_digit)0); + _sp_init_size(&r, SP_INT_DIGITS); + (void)_sp_add_d(&a, (sp_int_digit)1, &r); + + WB_NOTE("_sp_add_d carry-overflow rows exercised"); +} +#else +static void wb_add_d_overflow(void) +{ + WB_NOTE("_sp_add_d not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 14: sp_lshb() whole-digit shift capacity guard. + * + * else if ((s > 0) && (a->used + s > a->size)) + * + * Only reached when the bit part of the shift is zero (an exact multiple of + * the word size), which the in-library callers never combine with a + * destination that is too small. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_MATH_ALL) || !defined(NO_DH) || defined(HAVE_ECC) || \ + (!defined(NO_RSA) && !defined(WOLFSSL_RSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY)) +static void wb_lshb_capacity(void) +{ + sp_int a; + + /* Whole-word shift with no room: both operands true. */ + _sp_init_size(&a, 4); + a.dp[0] = 1; + a.used = 4; + a.dp[1] = 1; a.dp[2] = 1; a.dp[3] = 1; + (void)sp_lshb(&a, 2 * SP_WORD_SIZE); + + /* Whole-word shift with room: second operand false. */ + _sp_init_size(&a, SP_INT_DIGITS); + a.dp[0] = 1; + a.used = 1; + (void)sp_lshb(&a, 2 * SP_WORD_SIZE); + + /* No whole-word part at all: first operand false. */ + _sp_init_size(&a, SP_INT_DIGITS); + a.dp[0] = 1; + a.used = 1; + (void)sp_lshb(&a, 0); + + WB_NOTE("sp_lshb whole-digit capacity rows exercised"); +} +#else +static void wb_lshb_capacity(void) +{ + WB_NOTE("sp_lshb not compiled; skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 15: _sp_div()'s zero-divisor arm. + * + * if ((!done) && (err == MP_OKAY) && (d->used > 0)) { + * + * sp_div() rejects a zero divisor before it ever calls the engine, so the + * engine's own defensive width test is never seen false. _sp_div() is + * file-static and in scope here, so it can be handed the shape sp_div() + * screens out. With a zero divisor the shift step is skipped (the divisor's + * bit count is zero, so the normalisation shift is a whole word) and the + * division body is not entered, so nothing divides by zero. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_MATH_ALL) || !defined(NO_DH) || defined(HAVE_ECC) || \ + (!defined(NO_RSA) && !defined(WOLFSSL_RSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY)) +static void wb_div_zero_divisor(void) +{ + sp_int a; + sp_int d; + sp_int q; + sp_int rem; + + wb_fill(&a, 4, (sp_int_digit)0x1234567ULL); + _sp_init_size(&d, SP_INT_DIGITS); /* d = 0, used == 0 */ + _sp_init_size(&q, SP_INT_DIGITS); + _sp_init_size(&rem, SP_INT_DIGITS); + (void)_sp_div(&a, &d, &q, &rem, (unsigned int)(a.used + 1U)); + + /* The ordinary row (a nonzero divisor) in the same binary. */ + wb_fill(&d, 2, (sp_int_digit)0x89abULL); + _sp_init_size(&q, SP_INT_DIGITS); + _sp_init_size(&rem, SP_INT_DIGITS); + (void)_sp_div(&a, &d, &q, &rem, (unsigned int)(a.used + 1U)); + + WB_NOTE("_sp_div zero-divisor width row exercised"); +} +#else +static void wb_div_zero_divisor(void) +{ + WB_NOTE("_sp_div not compiled; zero-divisor row skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 16: prime-test argument rejection ahead of the width guard. + * + * if ((err == MP_OKAY) && (a->used * 2 >= SP_INT_DIGITS)) { + * + * The width guard's error operand is only false when an EARLIER check already + * rejected the call. The API suite never passes a NULL candidate to the + * randomised entry point, so the guard is only ever reached with the error + * clear. A NULL candidate short-circuits it (the width term is not evaluated, + * so nothing is dereferenced). + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_PRIME_GEN) && !defined(WC_NO_RNG) +static void wb_prime_arg_rejected(void) +{ + WC_RNG rng; + int res = 0; + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("RNG init failed; prime argument rows skipped"); + return; + } + + /* Error latched by the NULL check: the width guard sees it. */ + (void)sp_prime_is_prime_ex(NULL, 1, &res, &rng); + (void)sp_prime_is_prime_ex(NULL, 1, NULL, &rng); + + (void)wc_FreeRng(&rng); + WB_NOTE("prime-test argument rejection rows exercised"); +} +#else +static void wb_prime_arg_rejected(void) +{ + WB_NOTE("randomised prime test not compiled; argument rows skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Class 17: the randomised Miller-Rabin witness rejection. + * + * if ((sp_cmp_d(b, 2) != MP_GT) || (_sp_cmp(b, c) != MP_LT)) continue; + * + * The witness is drawn at random and rejected when it is not in [3, a-3]. + * The "too small" arm needs a draw of 0, 1 or 2, which for the multi-hundred- + * bit candidates the API tests use has probability ~2^-bits and is therefore + * never observed. The witness is masked down to the CANDIDATE's bit width, so + * a deliberately tiny candidate makes the draw space small enough that the + * arm is hit with certainty inside a bounded number of trials. + * + * The candidate is chosen so that BOTH rejection arms stay rare enough for the + * loop to make progress: 2039 is prime and just below 2^11, so a random + * 11-bit witness is out of range only 14 times in 2048. The helper is called + * directly because sp_prime_is_prime_ex() answers single-digit candidates from + * its small-prime table without ever drawing a witness. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SP_PRIME_GEN) && !defined(WC_NO_RNG) +/* 2039 draws ~1.007 witnesses per trial, so this many trials makes ~20000 + * draws: at 3/2048 per draw the probability of never seeing a witness of 2 or + * less is about e^-29. Each trial is a Miller-Rabin on an 11-bit number. */ +#define WB_SMALL_WITNESS_TRIALS 20000 + +static void wb_prime_small_witness(void) +{ + WC_RNG rng; + sp_int a; + int res = 0; + int i; + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("RNG init failed; small-witness rows skipped"); + return; + } + + wb_set_d(&a, (sp_int_digit)2039); + for (i = 0; i < WB_SMALL_WITNESS_TRIALS; i++) { + if (_sp_prime_random_trials(&a, 1, &res, &rng) != MP_OKAY) { + wb_fail = 1; + break; + } + } + + (void)wc_FreeRng(&rng); + WB_NOTE("randomised witness rejection rows exercised"); +} +#else +static void wb_prime_small_witness(void) +{ + WB_NOTE("randomised prime trials not compiled; witness rows skipped"); +} +#endif + #endif /* WOLFSSL_SP_MATH_ALL || WOLFSSL_SP_MATH */ int main(void) { + /* Unbuffered: on a timeout the process is killed and anything still + * buffered is lost, which reads as an empty log. */ + setvbuf(stdout, NULL, _IONBF, 0); + printf("sp_int.c white-box MC/DC supplement\n"); #if !defined(WOLFSSL_SP_MATH_ALL) && !defined(WOLFSSL_SP_MATH) printf(" neither WOLFSSL_SP_MATH_ALL nor WOLFSSL_SP_MATH defined;" @@ -142,6 +1307,25 @@ int main(void) #else wb_count_bits_leading_zero(); wb_cnt_lsb_all_zero_digits(); + wb_alloc_ceiling_macros(); + wb_div_capacity(); + wb_exptmod_engines(); + wb_exptmod_dispatch(); + wb_invmod_negative(); + wb_invmod_no_inverse(); + wb_alloc_ceiling_sites(); + wb_sp_backend_dispatch(); + wb_gcd_capacity(); + wb_prime_shapes(); + wb_sqrmod_ceiling(); + wb_output_edges(); + wb_mulmod_tmp_zero(); + wb_sub_off_loop(); + wb_add_d_overflow(); + wb_lshb_capacity(); + wb_div_zero_divisor(); + wb_prime_arg_rejected(); + wb_prime_small_witness(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures are surfaced as skips, not test failures: the campaign * treats a nonzero exit as a failed variant and discards its coverage. */ From 43905fda74cd85dadd37d5457fe8bc36439b1f87 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 20:05:21 +0200 Subject: [PATCH 17/20] tests: drive the SP ARM lane signature and inverse guards --- tests/include.am | 5 + .../unit-mcdc/test_sp_arm32_fault_whitebox.c | 50 ++ tests/unit-mcdc/test_sp_arm32_whitebox.c | 258 +++++++++ .../unit-mcdc/test_sp_arm64_fault_whitebox.c | 50 ++ tests/unit-mcdc/test_sp_arm64_whitebox.c | 197 ++++++- tests/unit-mcdc/test_sp_arm_fault_common.h | 515 ++++++++++++++++++ .../test_sp_armthumb_fault_whitebox.c | 50 ++ tests/unit-mcdc/test_sp_armthumb_whitebox.c | 258 +++++++++ tests/unit-mcdc/test_sp_cortexm_whitebox.c | 79 ++- 9 files changed, 1457 insertions(+), 5 deletions(-) create mode 100644 tests/unit-mcdc/test_sp_arm32_fault_whitebox.c create mode 100644 tests/unit-mcdc/test_sp_arm64_fault_whitebox.c create mode 100644 tests/unit-mcdc/test_sp_arm_fault_common.h create mode 100644 tests/unit-mcdc/test_sp_armthumb_fault_whitebox.c diff --git a/tests/include.am b/tests/include.am index afd0cbc565b..b1d5f8fa0ea 100644 --- a/tests/include.am +++ b/tests/include.am @@ -184,14 +184,19 @@ EXTRA_DIST += \ tests/unit-mcdc/test_she_whitebox.c \ tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c \ tests/unit-mcdc/test_slhdsa_whitebox.c \ + tests/unit-mcdc/test_sp_arm32_fault_whitebox.c \ tests/unit-mcdc/test_sp_arm32_whitebox.c \ + tests/unit-mcdc/test_sp_arm64_fault_whitebox.c \ tests/unit-mcdc/test_sp_arm64_whitebox.c \ + tests/unit-mcdc/test_sp_arm_fault_common.h \ + tests/unit-mcdc/test_sp_armthumb_fault_whitebox.c \ tests/unit-mcdc/test_sp_armthumb_whitebox.c \ tests/unit-mcdc/test_sp_c32_fault_whitebox.c \ tests/unit-mcdc/test_sp_c32_whitebox.c \ tests/unit-mcdc/test_sp_c64_fault_whitebox.c \ tests/unit-mcdc/test_sp_c64_whitebox.c \ tests/unit-mcdc/test_sp_cortexm_whitebox.c \ + tests/unit-mcdc/test_sp_crafted_common.h \ tests/unit-mcdc/test_sp_fault_common.h \ tests/unit-mcdc/test_sp_int_fault_whitebox.c \ tests/unit-mcdc/test_sp_int_whitebox.c \ diff --git a/tests/unit-mcdc/test_sp_arm32_fault_whitebox.c b/tests/unit-mcdc/test_sp_arm32_fault_whitebox.c new file mode 100644 index 00000000000..d0a9dde878e --- /dev/null +++ b/tests/unit-mcdc/test_sp_arm32_fault_whitebox.c @@ -0,0 +1,50 @@ +/* test_sp_arm32_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Heap-fault MC/DC supplement for wolfcrypt/src/sp_arm32.c, run inside this + * module's own emulator lane. + * + * Drives the `err == MP_OKAY` operand of the file's success chains by failing + * an SP temporary allocation. See tests/unit-mcdc/test_sp_arm_fault_common.h + * for why that operand is otherwise dead by construction, why this TU (not a + * new lane variant) turns WOLFSSL_SP_SMALL_STACK on, and what the sweep does. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +/* Before ANY wolfSSL header, so sp_arm32.c's own + * #ifdef WOLFSSL_SP_SMALL_STACK ... SP_ALLOC_VAR = XMALLOC + err + * arm of the SP_DECL_VAR/SP_ALLOC_VAR macro pair is the one compiled into this + * translation unit. No header reacts to this macro, so it changes nothing but + * function-local storage inside the file under test. */ +#ifndef WOLFSSL_SP_SMALL_STACK + #define WOLFSSL_SP_SMALL_STACK +#endif + +#include + +#include + +#define SP_ARM_FAULT_LABEL "sp_arm32.c" +#include "test_sp_arm_fault_common.h" diff --git a/tests/unit-mcdc/test_sp_arm32_whitebox.c b/tests/unit-mcdc/test_sp_arm32_whitebox.c index f68efa88631..ba9f6468873 100644 --- a/tests/unit-mcdc/test_sp_arm32_whitebox.c +++ b/tests/unit-mcdc/test_sp_arm32_whitebox.c @@ -1764,6 +1764,262 @@ static void wb_run_cache_mutex(void) } #endif +/* ======================================================================= * + * Residual closers added in the 2026-08-10 lane pass. Each takes one freshly + * made key pair per curve size and drives three decisions that the ordinary + * sign/verify/check_key traffic above cannot reach: + * + * 1. sp_ecc_check_key_(): + * if ((err == MP_OKAY) && + * ((sp__cmp_(p->x, pub->x) != 0) || + * (sp__cmp_(p->y, pub->y) != 0))) + * A mismatched private key disagrees on BOTH ordinates, so the second + * operand is short-circuited and only ever seen false. The NEGATED public + * point (x, prime - y) is still on the curve and still of full order, so it + * passes every earlier guard, and base*priv then matches its X but not its + * Y -- the only input that reaches the second operand's true row. + * + * 2. sp_ecc_verify_(): + * if ((*res == 0) && (c < 0)) + * A valid signature gives the (false, -) row. A small r gives + * r + order < prime, i.e. (true, true). r = prime - order + 5 makes + * r + order land at or past prime -- either c > 0, or the addition carries + * out of the field width and c keeps its initial 0 -- which is the missing + * (true, false) row. The signature is not valid in any of these calls; only + * which branch is taken matters. + * + * 3. sp_ecc_sign_(): + * if (km == NULL || mp_iszero(km)) + * wc_ecc_sign_hash() always passes km == NULL, so the second operand is + * never evaluated. Passing a non-NULL km, zero and then non-zero, reaches + * both of its values with the first operand false throughout. + * ======================================================================= */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + defined(HAVE_ECC_VERIFY) && defined(HAVE_ECC_SIGN) + +typedef int (*wb_rx_verify_fn)(const byte*, word32, const mp_int*, + const mp_int*, const mp_int*, const mp_int*, const mp_int*, int*, void*); +typedef int (*wb_rx_sign_fn)(const byte*, word32, WC_RNG*, const mp_int*, + mp_int*, mp_int*, mp_int*, void*); +typedef int (*wb_rx_check_key_fn)(const mp_int*, const mp_int*, const mp_int*, + void*); + +static void wb_run_residual_extra(int curve_id, int fieldSz, const char* label, + wb_rx_verify_fn verify, wb_rx_sign_fn sign, + wb_rx_check_key_fn check_key) +{ + ecc_key keyA; + ecc_key keyB; + WC_RNG rng; + mp_int prime; + mp_int order; + mp_int tmpm; + mp_int sigR; + mp_int sigS; + mp_int smVal; + mp_int rSmall; + mp_int one; + int curveIdx; + const ecc_set_type* dp; + int res; + int nInit = 0; + mp_int* inits[8]; + + XMEMSET(&keyA, 0, sizeof(keyA)); + XMEMSET(&keyB, 0, sizeof(keyB)); + XMEMSET(&rng, 0, sizeof(rng)); + + if (wc_ecc_init(&keyA) != 0 || wc_ecc_init(&keyB) != 0 || + wc_InitRng(&rng) != 0) { + WB_NOTE("init failed (residual extra)"); + wb_fail = 1; + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + return; + } + if (wc_ecc_make_key_ex(&rng, fieldSz, &keyA, curve_id) != 0 || + wc_ecc_make_key_ex(&rng, fieldSz, &keyB, curve_id) != 0) { + WB_NOTE("wc_ecc_make_key_ex failed (residual extra)"); + wb_fail = 1; + wc_FreeRng(&rng); + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + return; + } + + inits[0] = ′ inits[1] = ℴ inits[2] = &tmpm; + inits[3] = &sigR; inits[4] = &sigS; inits[5] = &smVal; + inits[6] = &rSmall; inits[7] = &one; + for (nInit = 0; nInit < 8; nInit++) { + if (mp_init(inits[nInit]) != MP_OKAY) { + break; + } + } + if (nInit < 8) { + WB_NOTE("mp_init failed (residual extra)"); + wb_fail = 1; + } + else { + curveIdx = wc_ecc_get_curve_idx(curve_id); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + + (void)mp_set(&one, 1); + (void)mp_set(&rSmall, 7); + (void)mp_set(&smVal, 17); + + if (dp != NULL && + mp_read_radix(&prime, dp->prime, 16) == MP_OKAY && + mp_read_radix(&order, dp->order, 16) == MP_OKAY) { + /* 1. check_key: matching priv, foreign priv, negated public Y. */ + if (check_key != NULL) { + (void)check_key(keyA.pubkey.x, keyA.pubkey.y, + ecc_get_k(&keyA), keyA.heap); + (void)check_key(keyA.pubkey.x, keyA.pubkey.y, + ecc_get_k(&keyB), keyA.heap); + if (mp_sub(&prime, keyA.pubkey.y, &tmpm) == MP_OKAY) { + (void)check_key(keyA.pubkey.x, &tmpm, ecc_get_k(&keyA), + keyA.heap); + } + } + + /* 2. verify: r + order below prime, then at/past it. */ + res = -1; + (void)verify(wb_digest, (word32)sizeof(wb_digest), keyA.pubkey.x, + keyA.pubkey.y, &one, &rSmall, &smVal, &res, keyA.heap); + if (mp_sub(&prime, &order, &tmpm) == MP_OKAY && + mp_add_d(&tmpm, 5, &tmpm) == MP_OKAY) { + res = -1; + (void)verify(wb_digest, (word32)sizeof(wb_digest), + keyA.pubkey.x, keyA.pubkey.y, &one, &tmpm, &smVal, &res, + keyA.heap); + } + } + else { + WB_NOTE("curve params unavailable (residual extra)"); + } + + /* 3. sign with an explicit km: zero, then non-zero. */ + (void)mp_zero(&tmpm); + (void)sign(wb_digest, (word32)sizeof(wb_digest), &rng, + ecc_get_k(&keyA), &sigR, &sigS, &tmpm, keyA.heap); + (void)mp_set(&tmpm, 12345); + (void)sign(wb_digest, (word32)sizeof(wb_digest), &rng, + ecc_get_k(&keyA), &sigR, &sigS, &tmpm, keyA.heap); + } + + while (nInit-- > 0) { + mp_clear(inits[nInit]); + } + wc_FreeRng(&rng); + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + WB_NOTE(label); +} + +static void wb_run_residual_extra_all(void) +{ +#ifndef WOLFSSL_SP_NO_256 + wb_run_residual_extra(ECC_SECP256R1, 32, + "P-256 check_key negated-Y / verify r+order>=prime / explicit km " + "exercised", + sp_ecc_verify_256, sp_ecc_sign_256, +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + sp_ecc_check_key_256 +#else + NULL +#endif + ); +#endif +#ifdef WOLFSSL_SP_384 + wb_run_residual_extra(ECC_SECP384R1, 48, + "P-384 check_key negated-Y / verify r+order>=prime / explicit km " + "exercised", + sp_ecc_verify_384, sp_ecc_sign_384, +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + sp_ecc_check_key_384 +#else + NULL +#endif + ); +#endif +#ifdef WOLFSSL_SP_521 + wb_run_residual_extra(ECC_SECP521R1, 66, + "P-521 check_key negated-Y / verify r+order>=prime / explicit km " + "exercised", + sp_ecc_verify_521, sp_ecc_sign_521, +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + sp_ecc_check_key_521 +#else + NULL +#endif + ); +#endif +} +#else +static void wb_run_residual_extra_all(void) +{ + WB_NOTE("SP ECC sign/verify not both compiled; residual extras skipped"); +} +#endif + +/* ----------------------------------------------------------------------- * + * sp__mod_inv_(): the binary extended-GCD loops + * + * while (ut > 1 && vt > 1) { ... do { ... } while (ut > 0 && even(u)); } + * + * The only caller is sp__calc_vfy_point_(), which always hands it a + * signature's s -- a uniformly random unit -- so the loop always terminates the + * same way and several operands never see a false row. The helper is file + * static, which is exactly what a white-box that includes the .c can reach, so + * it is called here directly with the degenerate operands the caller cannot + * produce: + * - a == m: u and v start equal, so the first subtraction makes u zero and + * the inner do-while's FIRST operand (ut > 0) is false; + * - a == 1: v has a single bit on entry, so the outer loop's SECOND operand + * (vt > 1) is false before the body ever runs; + * - small a: ordinary termination, which lands on u == 1 for some values and + * v == 1 for others, giving the outer loop's first operand its false row. + * a == 0 is deliberately NOT used: v would stay zero and the pre-loop that + * shifts even operands right would never terminate. + * ----------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + !defined(WOLFSSL_SP_SMALL) +#define WB_MOD_INV_SWEEP(WORDS, FN, ORDER) \ + do { \ + sp_digit wbA[WORDS]; \ + sp_digit wbR[WORDS]; \ + int wbI; \ + XMEMCPY(wbA, (ORDER), sizeof(wbA)); \ + (void)FN(wbR, wbA, (ORDER)); \ + for (wbI = 1; wbI <= 40; wbI++) { \ + XMEMSET(wbA, 0, sizeof(wbA)); \ + wbA[0] = (sp_digit)wbI; \ + (void)FN(wbR, wbA, (ORDER)); \ + } \ + } while (0) + +static void wb_run_mod_inv(void) +{ +#ifndef WOLFSSL_SP_NO_256 + WB_MOD_INV_SWEEP(8, sp_256_mod_inv_8, p256_order); + WB_NOTE("P-256 sp_256_mod_inv_8 degenerate operands exercised"); +#endif +#ifdef WOLFSSL_SP_384 + WB_MOD_INV_SWEEP(12, sp_384_mod_inv_12, p384_order); + WB_NOTE("P-384 sp_384_mod_inv_12 degenerate operands exercised"); +#endif +#ifdef WOLFSSL_SP_521 + WB_MOD_INV_SWEEP(17, sp_521_mod_inv_17, p521_order); + WB_NOTE("P-521 sp_521_mod_inv_17 degenerate operands exercised"); +#endif +} +#else +static void wb_run_mod_inv(void) +{ + WB_NOTE("sp__mod_inv_ not compiled; mod-inv sweep skipped"); +} +#endif + #endif /* WOLFSSL_HAVE_SP_ECC || WOLFSSL_HAVE_SP_RSA || WOLFSSL_HAVE_SP_DH */ int main(void) @@ -1783,6 +2039,8 @@ int main(void) wb_run_gap_521(); wb_run_rsa_gaps(); wb_run_dh_gaps(); + wb_run_residual_extra_all(); + wb_run_mod_inv(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); #else diff --git a/tests/unit-mcdc/test_sp_arm64_fault_whitebox.c b/tests/unit-mcdc/test_sp_arm64_fault_whitebox.c new file mode 100644 index 00000000000..4dfb5cc572e --- /dev/null +++ b/tests/unit-mcdc/test_sp_arm64_fault_whitebox.c @@ -0,0 +1,50 @@ +/* test_sp_arm64_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Heap-fault MC/DC supplement for wolfcrypt/src/sp_arm64.c, run inside this + * module's own emulator lane. + * + * Drives the `err == MP_OKAY` operand of the file's success chains by failing + * an SP temporary allocation. See tests/unit-mcdc/test_sp_arm_fault_common.h + * for why that operand is otherwise dead by construction, why this TU (not a + * new lane variant) turns WOLFSSL_SP_SMALL_STACK on, and what the sweep does. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +/* Before ANY wolfSSL header, so sp_arm64.c's own + * #ifdef WOLFSSL_SP_SMALL_STACK ... SP_ALLOC_VAR = XMALLOC + err + * arm of the SP_DECL_VAR/SP_ALLOC_VAR macro pair is the one compiled into this + * translation unit. No header reacts to this macro, so it changes nothing but + * function-local storage inside the file under test. */ +#ifndef WOLFSSL_SP_SMALL_STACK + #define WOLFSSL_SP_SMALL_STACK +#endif + +#include + +#include + +#define SP_ARM_FAULT_LABEL "sp_arm64.c" +#include "test_sp_arm_fault_common.h" diff --git a/tests/unit-mcdc/test_sp_arm64_whitebox.c b/tests/unit-mcdc/test_sp_arm64_whitebox.c index e7e96700ea7..21ab7ed8cab 100644 --- a/tests/unit-mcdc/test_sp_arm64_whitebox.c +++ b/tests/unit-mcdc/test_sp_arm64_whitebox.c @@ -165,7 +165,12 @@ static int wb_mp_set_ones(mp_int* m, int nbytes) * range guard too. */ static int wb_mp_set_at_bit_boundary(mp_int* m, int fieldBits) { - byte buf[96]; + /* Sized for the widest caller (RSA/DH 4096-bit moduli), not just the ECC + * field widths: a buffer too small to hold fieldBits silently produced a + * SHORTER value, which made every sp_RsaPublic_ and sp_DhExp_ call below + * bail at its "mp_count_bits(mod) != N" guard instead of reaching the + * FFDHE fast path, the windowed modexp and the leading-zero trim loop. */ + byte buf[512]; int fieldBytes = (fieldBits + 7) / 8; int topBits = fieldBits - (fieldBytes - 1) * 8; byte topMask = (byte)((topBits >= 8) ? 0xFFu : @@ -1187,6 +1192,9 @@ static void wb_run_rsa_dh_bounds(void) #if defined(WOLFSSL_HAVE_SP_DH) && !defined(NO_DH) #ifndef WOLFSSL_SP_NO_2048 if (wb_mp_set_at_bit_boundary(&mm, 2048) == MP_OKAY) { + mp_set(&base, 2); /* dp[0]==2 true, top digit all-ones: true */ + outLen = (word32)sizeof(out); + (void)sp_DhExp_2048(&base, &one, 1, &mm, out, &outLen); mp_set(&base, 3); /* FFDHE fast-path dp[0]==2 operand: false */ outLen = (word32)sizeof(out); (void)sp_DhExp_2048(&base, &one, 1, &mm, out, &outLen); @@ -1194,6 +1202,19 @@ static void wb_run_rsa_dh_bounds(void) outLen = (word32)sizeof(out); (void)sp_DhExp_2048(&base, &one, 1, &mm, out, &outLen); } + { + /* dp[0]==2 true, top digit NOT all-ones: closes that operand's + * independence pair without disturbing the other two. */ + byte buf2048[256]; + XMEMSET(buf2048, 0xFF, sizeof(buf2048)); + buf2048[0] = 0xFE; + if (mp_read_unsigned_bin(&mm, buf2048, (word32)sizeof(buf2048)) + == MP_OKAY) { + mp_set(&base, 2); + outLen = (word32)sizeof(out); + (void)sp_DhExp_2048(&base, &one, 1, &mm, out, &outLen); + } + } #endif #if !defined(WOLFSSL_SP_NO_3072) && defined(HAVE_FFDHE_3072) if (wb_mp_set_at_bit_boundary(&mm, 3072) == MP_OKAY) { @@ -1231,11 +1252,13 @@ static void wb_run_rsa_dh_bounds(void) (void)sp_DhExp_4096(&base, &one, 1, &mm, out, &outLen); /* Wider exponent: drives the windowed modexp digit-scan loop - * through its full natural termination (see file header). */ + * `for (; i>=0 || c>=4; )` past the end of the exponent array, so + * both its operands see a false row (i < 0 with c >= 4, then i < 0 + * with c < 4). Nine bytes is enough to spill past one 64-bit digit + * while keeping the 4096-bit modexp cheap on an emulated lane. */ mp_set(&base, 3); outLen = (word32)sizeof(out); - (void)sp_DhExp_4096(&base, exp32, (word32)sizeof(exp32), &mm, out, - &outLen); + (void)sp_DhExp_4096(&base, exp32, 9, &mm, out, &outLen); } #endif #endif /* WOLFSSL_HAVE_SP_DH && !NO_DH */ @@ -1255,6 +1278,170 @@ static void wb_run_rsa_dh_bounds(void) } #endif /* (WOLFSSL_HAVE_SP_RSA && !NO_RSA) || (WOLFSSL_HAVE_SP_DH && !NO_DH) */ +/* ----------------------------------------------------------------------- * + * sp_ecc_check_key_(): the private-key cross-check + * + * if ((err == MP_OKAY) && + * ((sp__cmp_(p->x, pub->x) != 0) || + * (sp__cmp_(p->y, pub->y) != 0))) + * + * Real callers only ever pass a matching (pub, priv) pair, so both comparison + * operands are permanently false. Three vectors close them: + * - (pub, its own priv) -> (F, F): the existing all-match row; + * - (pub, ANOTHER key's priv) -> (T, -): X differs, second operand + * short-circuited; + * - ((pubX, prime - pubY), priv) -> (F, T): the negated public point is + * still on the curve and still of full order, so it passes every earlier + * guard, and base*priv then matches its X but not its Y -- the only way to + * reach the second operand's true row. + * ----------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + (defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER)) +static void wb_run_check_key_priv(int curve_id, int fieldSz, const char* label, + int (*check_key)(const mp_int*, const mp_int*, const mp_int*, void*)) +{ + ecc_key keyA; + ecc_key keyB; + WC_RNG rng; + mp_int prime; + mp_int negY; + int curveIdx; + const ecc_set_type* dp; + + XMEMSET(&keyA, 0, sizeof(keyA)); + XMEMSET(&keyB, 0, sizeof(keyB)); + XMEMSET(&rng, 0, sizeof(rng)); + + if (wc_ecc_init(&keyA) != 0 || wc_ecc_init(&keyB) != 0 || + wc_InitRng(&rng) != 0) { + WB_NOTE("init failed (check_key priv)"); + wb_fail = 1; + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + return; + } + + if (wc_ecc_make_key_ex(&rng, fieldSz, &keyA, curve_id) != 0 || + wc_ecc_make_key_ex(&rng, fieldSz, &keyB, curve_id) != 0) { + WB_NOTE("wc_ecc_make_key_ex failed (check_key priv)"); + wb_fail = 1; + } + else if (mp_init(&prime) != MP_OKAY) { + WB_NOTE("mp_init(prime) failed (check_key priv)"); + wb_fail = 1; + } + else { + if (mp_init(&negY) == MP_OKAY) { + curveIdx = wc_ecc_get_curve_idx(curve_id); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + + /* Matching pair: both comparison operands false. */ + (void)check_key(keyA.pubkey.x, keyA.pubkey.y, ecc_get_k(&keyA), + keyA.heap); + /* Foreign private key: the X comparison alone is true. */ + (void)check_key(keyA.pubkey.x, keyA.pubkey.y, ecc_get_k(&keyB), + keyA.heap); + /* Negated public point: X matches, Y does not. */ + if (dp != NULL && + mp_read_radix(&prime, dp->prime, 16) == MP_OKAY && + mp_sub(&prime, keyA.pubkey.y, &negY) == MP_OKAY) { + (void)check_key(keyA.pubkey.x, &negY, ecc_get_k(&keyA), + keyA.heap); + } + else { + WB_NOTE("curve prime unavailable; negated-Y vector skipped"); + } + mp_clear(&negY); + } + mp_clear(&prime); + } + + wc_FreeRng(&rng); + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + WB_NOTE(label); +} + +static void wb_run_check_key_priv_all(void) +{ +#ifndef WOLFSSL_SP_NO_256 + wb_run_check_key_priv(ECC_SECP256R1, 32, + "P-256 check_key private-key cross-check exercised", + sp_ecc_check_key_256); +#endif +#ifdef WOLFSSL_SP_384 + wb_run_check_key_priv(ECC_SECP384R1, 48, + "P-384 check_key private-key cross-check exercised", + sp_ecc_check_key_384); +#endif +#ifdef WOLFSSL_SP_521 + wb_run_check_key_priv(ECC_SECP521R1, 66, + "P-521 check_key private-key cross-check exercised", + sp_ecc_check_key_521); +#endif +} +#else +static void wb_run_check_key_priv_all(void) +{ + WB_NOTE("sp_ecc_check_key_ not compiled; priv cross-check skipped"); +} +#endif + +/* ----------------------------------------------------------------------- * + * sp__mod_inv_(): the binary extended-GCD loops + * + * while (ut > 1 && vt > 1) { ... do { ... } while (ut > 0 && even(u)); } + * + * The only caller is sp__calc_vfy_point_(), which always hands it a + * signature's s -- a uniformly random unit -- so the loop always terminates the + * same way and the operands' false rows are never seen. The helper is file + * static, which is exactly what this white-box has access to, so it is called + * here directly with the degenerate operands the caller cannot produce: + * - a == 1: v has one bit on entry, so the outer loop's SECOND operand is + * false before the body ever runs; + * - a == m: u and v are equal, so the first subtraction makes u zero and the + * inner do-while's FIRST operand is false; + * - small a: ordinary termination, which lands on u == 1 for some values and + * v == 1 for others, giving the outer loop's first operand its false row. + * a == 0 is deliberately NOT used: v would stay zero and the pre-loop that + * shifts even operands right would never terminate. + * ----------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + !defined(WOLFSSL_SP_SMALL) +#define WB_MOD_INV_SWEEP(WORDS, FN, ORDER) \ + do { \ + sp_digit wbA[WORDS]; \ + sp_digit wbR[WORDS]; \ + int wbI; \ + XMEMCPY(wbA, (ORDER), sizeof(wbA)); \ + (void)FN(wbR, wbA, (ORDER)); \ + for (wbI = 1; wbI <= 40; wbI++) { \ + XMEMSET(wbA, 0, sizeof(wbA)); \ + wbA[0] = (sp_digit)wbI; \ + (void)FN(wbR, wbA, (ORDER)); \ + } \ + } while (0) + +static void wb_run_mod_inv(void) +{ + /* P-256 is omitted on this backend: sp_256_mod_inv_4() is hand-written + * AArch64 assembly with no C-level decision to drive. */ +#ifdef WOLFSSL_SP_384 + WB_MOD_INV_SWEEP(6, sp_384_mod_inv_6, p384_order); + WB_NOTE("P-384 sp_384_mod_inv_6 degenerate operands exercised"); +#endif +#ifdef WOLFSSL_SP_521 + WB_MOD_INV_SWEEP(9, sp_521_mod_inv_9, p521_order); + WB_NOTE("P-521 sp_521_mod_inv_9 degenerate operands exercised"); +#endif +} +#else +static void wb_run_mod_inv(void) +{ + WB_NOTE("sp__mod_inv_ not compiled; mod-inv sweep skipped"); +} +#endif + /* FP-ECC cache guard, once per curve: * * if ((err == MP_OKAY) && (wc_LockMutex(&sp_cache__lock) != 0)) @@ -1360,6 +1547,8 @@ int main(void) wb_run_point_specials_all(); wb_run_ecc_extra_all(); wb_run_rsa_dh_bounds(); + wb_run_check_key_priv_all(); + wb_run_mod_inv(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); #else diff --git a/tests/unit-mcdc/test_sp_arm_fault_common.h b/tests/unit-mcdc/test_sp_arm_fault_common.h new file mode 100644 index 00000000000..81536de9481 --- /dev/null +++ b/tests/unit-mcdc/test_sp_arm_fault_common.h @@ -0,0 +1,515 @@ +/* test_sp_arm_fault_common.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Shared body for the ARM-lane SP backend heap-fault white-boxes + * (sp_arm64.c / sp_arm32.c / sp_armthumb.c). + * + * WHY + * --- + * Each of those files carries ~35 uncovered decision conditions of the shape + * + * if ((err == MP_OKAY) && ) -- the err operand's FALSE + * + * and the reason the FALSE half is missing is not that it is hard to reach, it + * is that nothing in the compiled code can produce it. SP_ALLOC_VAR is + * + * #ifdef WOLFSSL_SP_SMALL_STACK + * if (err == MP_OKAY) { + * (NAME) = XMALLOC(...); + * if ((NAME) == NULL) { err = MEMORY_E; } + * } + * #else + * WC_DO_NOTHING + * + * so without WOLFSSL_SP_SMALL_STACK the SP temporaries are plain stack arrays, + * `err` is MP_OKAY from entry to exit, and every downstream operand is dead by + * construction. No sp-arm-lanes variant sets that macro, and adding one would + * mean a whole extra cross build + emulator pass per lane. + * + * The including TU therefore defines WOLFSSL_SP_SMALL_STACK for ITSELF, before + * it #includes the sp_arm*.c under test. That is sound for this campaign and + * cheaper than a variant: + * - the lane's white-box recipe (campaign/lanes/qemu-entry.sh) compiles the + * wb TU with the library's own captured compile line and links it against + * libwolfssl.a with the target file's object REMOVED, so the wb binary + * contains exactly one copy of sp_arm*.c -- this one -- and there is no + * ODR/ABI split with the rest of the library (the macro only changes + * function-local storage inside sp_arm*.c; no header and no struct layout + * reacts to it); + * - WOLFSSL_SP_SMALL_STACK adds no decision to the compiled region of these + * files (SP_ALLOC_VAR/SP_FREE_VAR expand to single-condition ifs, which + * carry no MC/DC record, and the multi-condition #if-swapped variants live + * in the WOLFCRYPT_HAVE_SAKKE 1024-bit block that this lane's config does + * not compile), so the file's MC/DC total is unchanged and the union with + * the other rows stays key-compatible. + * + * HOW + * --- + * mcdc_fault_alloc.h fails the n-th and every later heap allocation. This + * driver calls the SP entry points DIRECTLY rather than through wc_ecc_*: the + * allocation counter then starts inside the function under test, so a small + * sweep (n = 1..SP_ARM_FAULT_MAX_N) walks the MEMORY_E down that one function's + * own success chain instead of being consumed by ecc.c/RNG bookkeeping. It is + * also much cheaper on an emulated lane -- an armed call bails within a few + * instructions of the failed allocation instead of doing the whole point + * multiplication. + * + * Every operand is prepared while the injector is DISARMED, because + * mcdc_fa_arm(n) fails allocation n AND every later one: a blanket arming + * makes the driver's own key setup fail and the target is never entered. + * + * Nothing here is a known-answer test. Results are all discarded; the only + * requirement is that an armed call returns cleanly rather than crashing (a + * qemu segfault would lose the whole white-box row). + * + * The including TU defines SP_ARM_FAULT_LABEL and #includes the wolfCrypt .c + * under test before including this header. + */ + +#ifndef SP_ARM_FAULT_LABEL + #error "define SP_ARM_FAULT_LABEL before test_sp_arm_fault_common.h" +#endif + +#include "mcdc_fault_alloc.h" + +#include +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +/* Sweep depth. Each SP entry point allocates one or two temporaries of its + * own and then calls helpers that allocate one or two more, so the failure + * index only has to walk a little past the deepest chain. Kept low on purpose: + * these lanes run under qemu-user, TEST_TIMEOUT is wall clock, and lanes run + * concurrently under MAXPAR -- a driver that finishes alone can still be killed + * under load, and a killed driver is a silent skip. */ +#ifndef SP_ARM_FAULT_MAX_N + #define SP_ARM_FAULT_MAX_N 8 +#endif + +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + !defined(MCDC_FA_UNAVAILABLE) && \ + defined(HAVE_ECC_SIGN) && defined(HAVE_ECC_VERIFY) + +/* Fixed 32-byte stand-in digest. Its value is irrelevant: the sweep drives + * allocation failure positions, not a signature. */ +static const byte wb_fa_digest[32] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f +}; + +typedef int (*wb_fa_mulmod_add_fn)(const mp_int*, const ecc_point*, + const ecc_point*, int, ecc_point*, int, void*); +typedef int (*wb_fa_mulmod_base_add_fn)(const mp_int*, const ecc_point*, int, + ecc_point*, int, void*); +typedef int (*wb_fa_sign_fn)(const byte*, word32, WC_RNG*, const mp_int*, + mp_int*, mp_int*, mp_int*, void*); +typedef int (*wb_fa_verify_fn)(const byte*, word32, const mp_int*, + const mp_int*, const mp_int*, const mp_int*, const mp_int*, int*, void*); +typedef int (*wb_fa_check_key_fn)(const mp_int*, const mp_int*, const mp_int*, + void*); + +/* Widest hash the SP signers accept (P-521 takes 66 bytes). */ +#define WB_FA_MAX_HASH 66 + +/* buf <<= 7, in place, over `len` big-endian bytes. Only used on a P-521 + * value, which is at most 521 bits wide in a 528-bit (66-byte) buffer, so + * nothing is shifted out of the top. Done on the bytes rather than with + * mp_mul_2d() because that helper is not compiled in every SP math + * configuration this header is included from. */ +static void wb_fa_shl7(byte* buf, int len) +{ + int i; + + for (i = 0; i < (len - 1); i++) { + buf[i] = (byte)(((unsigned)buf[i] << 7) | ((unsigned)buf[i + 1] >> 1)); + } + buf[len - 1] = (byte)((unsigned)buf[len - 1] << 7); +} + +/* Encode `e` as the fixed-width hash that sp_ecc_sign_() will read back as + * exactly `e`. All the signers do sp__from_bin() over the whole buffer, so + * a plain fixed-length big-endian encoding round-trips -- except P-521, which + * then takes "the 521 leftmost bits" by shifting the 528-bit buffer right by + * 7, so its value has to be shifted left by 7 on the way in. */ +static int wb_fa_encode_hash(mp_int* e, byte* buf, int fieldSz) +{ + int ret = mp_to_unsigned_bin_len(e, buf, fieldSz); + + if ((ret == MP_OKAY) && (fieldSz == 66)) { + wb_fa_shl7(buf, fieldSz); + } + return ret; +} + +/* ---------------------------------------------------------------------- * + * sp_ecc_sign_(): the "signature is usable" guard + * + * if ((err == MP_OKAY) && (!sp__iszero_(s))) { + * break; + * } + * + * Every signature any driver has ever produced had a non-zero s, so the + * second operand only ever ran true and the retry it guards was dead. s is + * + * s = (e + r*x) / k mod order + * + * so s == 0 needs e == -r*x mod order, which needs the private scalar x -- + * not reachable from the caller's side. It is reachable INDIRECTLY, using the + * signer itself as the oracle and the fact that r depends only on k: + * + * pass 1: sign with a supplied k = K and a hash encoding e1. r is + * (K.G)->x mod order and the returned s1 = (e1 + r*x)/K, hence + * r*x == s1*K - e1 (mod order); + * pass 2: sign with the SAME K and a hash encoding e2 = e1 - s1*K. r comes + * out identical, so e2 + r*x == 0 (mod order) and s is zero. + * + * No private-key arithmetic is needed: every operand is either chosen here or + * handed back by pass 1. Only the first loop iteration of pass 2 sees s == 0; + * sp_ecc_sign_() zeroes the supplied km after using it, so the retry falls + * back to a generated k and the call finishes with a normal signature. + * + * The `err == MP_OKAY` operand of the same decision is left alone on purpose: + * sp__calc_s_() has no allocation and no failing step on these + * backends, so its false row cannot be produced at all. + * ---------------------------------------------------------------------- */ +static void wb_fa_sign_zero_s(wb_fa_sign_fn sign, ecc_key* key, WC_RNG* rng, + int curveId, int fieldSz, const char* label) +{ + mp_int kConst; + mp_int kArg; + mp_int rOut2; + mp_int sOut; + mp_int e1; + mp_int e2; + mp_int scratch; + mp_int ordV; + byte hash[WB_FA_MAX_HASH]; + int curveIdx; + const ecc_set_type* dp; + int nInit; + mp_int* inits[8]; + + if ((fieldSz <= 0) || (fieldSz > WB_FA_MAX_HASH)) { + WB_NOTE("unsupported field size (zero-s sign)"); + return; + } + + inits[0] = &kConst; inits[1] = &kArg; inits[2] = &rOut2; + inits[3] = &sOut; inits[4] = &e1; inits[5] = &e2; + inits[6] = &scratch; inits[7] = &ordV; + for (nInit = 0; nInit < 8; nInit++) { + if (mp_init(inits[nInit]) != MP_OKAY) { + break; + } + } + if (nInit < 8) { + WB_NOTE("mp_init failed (zero-s sign)"); + wb_fail = 1; + while (nInit-- > 0) { + mp_clear(inits[nInit]); + } + return; + } + + curveIdx = wc_ecc_get_curve_idx(curveId); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + + if ((dp == NULL) || (mp_read_radix(&ordV, dp->order, 16) != MP_OKAY)) { + WB_NOTE("curve order unavailable (zero-s sign)"); + } + else { + /* Any fixed non-zero K below the order works; the value is never + * secret and never reused outside this driver. */ + (void)mp_set(&kConst, 0x5A5A5); + (void)mp_set(&e1, 1); + + XMEMSET(hash, 0, sizeof(hash)); + /* e2 = e1 + (order - (s1*K mod order)) mod order, built from mp_mul / + * mp_mod / mp_sub / mp_add only: the modular one-liners (mp_mulmod, + * mp_submod) are not compiled in every SP math configuration this + * header is included from, and every intermediate here stays + * non-negative, which WOLFSSL_SP_INT_NEGATIVE-less builds require. */ + if ((wb_fa_encode_hash(&e1, hash, fieldSz) == MP_OKAY) && + (mp_copy(&kConst, &kArg) == MP_OKAY) && + (sign(hash, (word32)fieldSz, rng, ecc_get_k(key), &rOut2, + &sOut, &kArg, key->heap) == 0) && + (mp_mul(&sOut, &kConst, &scratch) == MP_OKAY) && + (mp_mod(&scratch, &ordV, &e2) == MP_OKAY) && + (mp_sub(&ordV, &e2, &scratch) == MP_OKAY) && + (mp_add(&scratch, &e1, &scratch) == MP_OKAY) && + (mp_mod(&scratch, &ordV, &e2) == MP_OKAY)) { + XMEMSET(hash, 0, sizeof(hash)); + if (wb_fa_encode_hash(&e2, hash, fieldSz) == MP_OKAY) { + (void)mp_copy(&kConst, &kArg); + (void)sign(hash, (word32)fieldSz, rng, ecc_get_k(key), + &rOut2, &sOut, &kArg, key->heap); + } + } + else { + WB_NOTE("oracle sign failed (zero-s sign)"); + } + } + + while (nInit-- > 0) { + mp_clear(inits[nInit]); + } + WB_NOTE(label); +} + +/* One curve size: sweep the failure index across each public SP entry point + * that carries an (err == MP_OKAY) success chain. */ +static void wb_fa_curve(int curveId, int fieldSz, const char* label, + wb_fa_mulmod_add_fn mulmod_add, + wb_fa_mulmod_base_add_fn mulmod_base_add, + wb_fa_sign_fn sign, wb_fa_verify_fn verify, + wb_fa_check_key_fn check_key) +{ + ecc_key keyA; + ecc_key keyB; + WC_RNG rng; + ecc_point* rOut = NULL; + mp_int sigR; + mp_int sigS; + mp_int one; + int haveMp = 0; + int n; + int res = 0; + + XMEMSET(&keyA, 0, sizeof(keyA)); + XMEMSET(&keyB, 0, sizeof(keyB)); + XMEMSET(&rng, 0, sizeof(rng)); + + if (wc_ecc_init(&keyA) != 0 || wc_ecc_init(&keyB) != 0 || + wc_InitRng(&rng) != 0) { + WB_NOTE("init failed (fault curve)"); + wb_fail = 1; + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + return; + } + + /* Setup, injector disarmed: everything below must succeed so the armed + * calls start from a valid state. */ + if (wc_ecc_make_key_ex(&rng, fieldSz, &keyA, curveId) != 0 || + wc_ecc_make_key_ex(&rng, fieldSz, &keyB, curveId) != 0) { + WB_NOTE("wc_ecc_make_key_ex failed (fault curve)"); + wb_fail = 1; + wc_FreeRng(&rng); + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + return; + } + + rOut = wc_ecc_new_point(); + haveMp = (mp_init(&sigR) == MP_OKAY) && (mp_init(&sigS) == MP_OKAY) && + (mp_init(&one) == MP_OKAY); + if (haveMp) { + (void)mp_set(&one, 1); + /* A real signature, made disarmed, so the verify sweep below has a + * well-formed (r, s) to walk. */ + if (sign(wb_fa_digest, (word32)sizeof(wb_fa_digest), &rng, + ecc_get_k(&keyA), &sigR, &sigS, NULL, keyA.heap) != 0) { + WB_NOTE("baseline sign failed (fault curve)"); + wb_fail = 1; + } + } + else { + WB_NOTE("mp_init failed (fault curve)"); + wb_fail = 1; + } + + /* Crafted sign vector, run with the injector DISARMED. See the block + * comment on wb_fa_sign_zero_s() for what it closes and why it lives in + * this TU. + * + * TWO OTHER CRAFTED SHAPES WERE TRIED HERE AND REMOVED; do not re-add + * them without new evidence, both were measured on this module: + * + * - verify with an all-zero hash (u1 == 0) and with r == 0 (u2 == 0), to + * put `(err == MP_OKAY) && sp__iszero_(p?->z)` in + * sp__calc_vfy_point_() into its true row. It does not work on + * these backends: [0]G and [0]Q come out of the windowed + * mulmod/mulmod_base with z still at the Montgomery norm constant and + * infinity signalled by the point's separate `infinity` flag, never by + * a zero z. Both operands of both guards stayed uncovered, exactly as + * they already had for the equivalent vectors the cortexm driver runs + * (its V1/V2). + * + * - verify with s == order, to hand sp__mod_inv_() a == m. That is + * the right idea (it is how the inner `while (ut > 0 && ...)` reaches + * its first operand's false row) but it must NOT be driven through + * P-256 on sp_arm64.c: sp_256_mod_inv_4() there is hand-written + * AArch64 assembly whose loop does not terminate for a == m, and the + * white-box hung until TEST_TIMEOUT killed it -- which the campaign + * records as a silent skip of the whole row. The C mod_inv bodies are + * reached instead by the direct sweep in the ordinary white-boxes + * (wb_run_mod_inv), which can pick the curves it calls. */ + if (haveMp) { + wb_fa_sign_zero_s(sign, &keyA, &rng, curveId, fieldSz, + "zero-s sign vector done"); + } + + for (n = 1; (n <= SP_ARM_FAULT_MAX_N) && haveMp; n++) { + /* sp_ecc_mulmod_add_() / sp_ecc_mulmod_base_add_(): three + * consecutive `(err == MP_OKAY) && (!inMont)` guards each. inMont == 0 + * so the second operand stays true and the sweep is what moves err. */ + if (rOut != NULL) { + mcdc_fa_arm(n); + (void)mulmod_add(ecc_get_k(&keyA), &keyB.pubkey, &keyA.pubkey, 0, + rOut, 1, keyA.heap); + mcdc_fa_disarm(); + + mcdc_fa_arm(n); + (void)mulmod_base_add(ecc_get_k(&keyA), &keyA.pubkey, 0, rOut, 1, + keyA.heap); + mcdc_fa_disarm(); + } + + /* sp_ecc_sign_(): the retry loop's `err == MP_OKAY` operand and the + * `(err == MP_OKAY) && (!iszero(s))` guard after it. */ + mcdc_fa_arm(n); + (void)sign(wb_fa_digest, (word32)sizeof(wb_fa_digest), &rng, + ecc_get_k(&keyA), &sigR, &sigS, NULL, keyA.heap); + mcdc_fa_disarm(); + + /* sp_ecc_verify_() -> sp__calc_vfy_point_(): the + * `(err == MP_OKAY) && iszero(p1->z)` / `p2->z` guards. */ + res = 0; + mcdc_fa_arm(n); + (void)verify(wb_fa_digest, (word32)sizeof(wb_fa_digest), keyA.pubkey.x, + keyA.pubkey.y, &one, &sigR, &sigS, &res, keyA.heap); + mcdc_fa_disarm(); + + /* sp_ecc_check_key_(): the point-order and private-key comparison + * guards, both fronted by `err == MP_OKAY`. */ + if (check_key != NULL) { + mcdc_fa_arm(n); + (void)check_key(keyA.pubkey.x, keyA.pubkey.y, ecc_get_k(&keyA), + keyA.heap); + mcdc_fa_disarm(); + } + } + + mcdc_fa_disarm(); + + if (haveMp) { + mp_clear(&one); + mp_clear(&sigS); + mp_clear(&sigR); + } + if (rOut != NULL) { + wc_ecc_del_point(rOut); + } + wc_FreeRng(&rng); + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + WB_NOTE(label); +} + +static void wb_fa_all(void) +{ +#ifndef WOLFSSL_SP_NO_256 + wb_fa_curve(ECC_SECP256R1, 32, "P-256 SP alloc-fault sweep done", + sp_ecc_mulmod_add_256, sp_ecc_mulmod_base_add_256, + sp_ecc_sign_256, sp_ecc_verify_256, +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + sp_ecc_check_key_256 +#else + NULL +#endif + ); +#else + WB_NOTE("WOLFSSL_SP_NO_256 defined; P-256 fault sweep skipped"); +#endif + +#ifdef WOLFSSL_SP_384 + wb_fa_curve(ECC_SECP384R1, 48, "P-384 SP alloc-fault sweep done", + sp_ecc_mulmod_add_384, sp_ecc_mulmod_base_add_384, + sp_ecc_sign_384, sp_ecc_verify_384, +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + sp_ecc_check_key_384 +#else + NULL +#endif + ); +#else + WB_NOTE("WOLFSSL_SP_384 not defined; P-384 fault sweep skipped"); +#endif + +#ifdef WOLFSSL_SP_521 + wb_fa_curve(ECC_SECP521R1, 66, "P-521 SP alloc-fault sweep done", + sp_ecc_mulmod_add_521, sp_ecc_mulmod_base_add_521, + sp_ecc_sign_521, sp_ecc_verify_521, +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + sp_ecc_check_key_521 +#else + NULL +#endif + ); +#else + WB_NOTE("WOLFSSL_SP_521 not defined; P-521 fault sweep skipped"); +#endif +} + +#else /* feature set not present: keep the TU building everywhere */ + +static void wb_fa_all(void) +{ +#ifdef MCDC_FA_UNAVAILABLE + WB_NOTE("allocator hooks unavailable in this config; nothing to sweep"); +#else + WB_NOTE("SP ECC sign/verify not all compiled; nothing to sweep"); +#endif +} + +#endif + +int main(void) +{ + /* Unbuffered: on a timeout the process is killed and anything still + * buffered is lost, which reads as an empty log. */ + setvbuf(stdout, NULL, _IONBF, 0); + + printf("%s heap-fault white-box supplement\n", SP_ARM_FAULT_LABEL); + +#ifndef WOLFSSL_SP_SMALL_STACK + WB_NOTE("WOLFSSL_SP_SMALL_STACK off in this TU: SP temporaries are stack " + "arrays and err cannot leave MP_OKAY; sweep runs but cannot fail " + "an SP allocation"); +#endif + + mcdc_fa_install(); + wb_fa_all(); + mcdc_fa_disarm(); + mcdc_fa_restore(); + + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Always 0: a nonzero exit discards this white-box row's coverage. */ + (void)wb_fail; + return 0; +} diff --git a/tests/unit-mcdc/test_sp_armthumb_fault_whitebox.c b/tests/unit-mcdc/test_sp_armthumb_fault_whitebox.c new file mode 100644 index 00000000000..da57fbd26a7 --- /dev/null +++ b/tests/unit-mcdc/test_sp_armthumb_fault_whitebox.c @@ -0,0 +1,50 @@ +/* test_sp_armthumb_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Heap-fault MC/DC supplement for wolfcrypt/src/sp_armthumb.c, run inside this + * module's own emulator lane. + * + * Drives the `err == MP_OKAY` operand of the file's success chains by failing + * an SP temporary allocation. See tests/unit-mcdc/test_sp_arm_fault_common.h + * for why that operand is otherwise dead by construction, why this TU (not a + * new lane variant) turns WOLFSSL_SP_SMALL_STACK on, and what the sweep does. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +/* Before ANY wolfSSL header, so sp_armthumb.c's own + * #ifdef WOLFSSL_SP_SMALL_STACK ... SP_ALLOC_VAR = XMALLOC + err + * arm of the SP_DECL_VAR/SP_ALLOC_VAR macro pair is the one compiled into this + * translation unit. No header reacts to this macro, so it changes nothing but + * function-local storage inside the file under test. */ +#ifndef WOLFSSL_SP_SMALL_STACK + #define WOLFSSL_SP_SMALL_STACK +#endif + +#include + +#include + +#define SP_ARM_FAULT_LABEL "sp_armthumb.c" +#include "test_sp_arm_fault_common.h" diff --git a/tests/unit-mcdc/test_sp_armthumb_whitebox.c b/tests/unit-mcdc/test_sp_armthumb_whitebox.c index 77737d4ec97..225c3190414 100644 --- a/tests/unit-mcdc/test_sp_armthumb_whitebox.c +++ b/tests/unit-mcdc/test_sp_armthumb_whitebox.c @@ -1568,6 +1568,262 @@ static void wb_run_cache_mutex(void) } #endif +/* ======================================================================= * + * Residual closers added in the 2026-08-10 lane pass. Each takes one freshly + * made key pair per curve size and drives three decisions that the ordinary + * sign/verify/check_key traffic above cannot reach: + * + * 1. sp_ecc_check_key_(): + * if ((err == MP_OKAY) && + * ((sp__cmp_(p->x, pub->x) != 0) || + * (sp__cmp_(p->y, pub->y) != 0))) + * A mismatched private key disagrees on BOTH ordinates, so the second + * operand is short-circuited and only ever seen false. The NEGATED public + * point (x, prime - y) is still on the curve and still of full order, so it + * passes every earlier guard, and base*priv then matches its X but not its + * Y -- the only input that reaches the second operand's true row. + * + * 2. sp_ecc_verify_(): + * if ((*res == 0) && (c < 0)) + * A valid signature gives the (false, -) row. A small r gives + * r + order < prime, i.e. (true, true). r = prime - order + 5 makes + * r + order land at or past prime -- either c > 0, or the addition carries + * out of the field width and c keeps its initial 0 -- which is the missing + * (true, false) row. The signature is not valid in any of these calls; only + * which branch is taken matters. + * + * 3. sp_ecc_sign_(): + * if (km == NULL || mp_iszero(km)) + * wc_ecc_sign_hash() always passes km == NULL, so the second operand is + * never evaluated. Passing a non-NULL km, zero and then non-zero, reaches + * both of its values with the first operand false throughout. + * ======================================================================= */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + defined(HAVE_ECC_VERIFY) && defined(HAVE_ECC_SIGN) + +typedef int (*wb_rx_verify_fn)(const byte*, word32, const mp_int*, + const mp_int*, const mp_int*, const mp_int*, const mp_int*, int*, void*); +typedef int (*wb_rx_sign_fn)(const byte*, word32, WC_RNG*, const mp_int*, + mp_int*, mp_int*, mp_int*, void*); +typedef int (*wb_rx_check_key_fn)(const mp_int*, const mp_int*, const mp_int*, + void*); + +static void wb_run_residual_extra(int curve_id, int fieldSz, const char* label, + wb_rx_verify_fn verify, wb_rx_sign_fn sign, + wb_rx_check_key_fn check_key) +{ + ecc_key keyA; + ecc_key keyB; + WC_RNG rng; + mp_int prime; + mp_int order; + mp_int tmpm; + mp_int sigR; + mp_int sigS; + mp_int smVal; + mp_int rSmall; + mp_int one; + int curveIdx; + const ecc_set_type* dp; + int res; + int nInit = 0; + mp_int* inits[8]; + + XMEMSET(&keyA, 0, sizeof(keyA)); + XMEMSET(&keyB, 0, sizeof(keyB)); + XMEMSET(&rng, 0, sizeof(rng)); + + if (wc_ecc_init(&keyA) != 0 || wc_ecc_init(&keyB) != 0 || + wc_InitRng(&rng) != 0) { + WB_NOTE("init failed (residual extra)"); + wb_fail = 1; + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + return; + } + if (wc_ecc_make_key_ex(&rng, fieldSz, &keyA, curve_id) != 0 || + wc_ecc_make_key_ex(&rng, fieldSz, &keyB, curve_id) != 0) { + WB_NOTE("wc_ecc_make_key_ex failed (residual extra)"); + wb_fail = 1; + wc_FreeRng(&rng); + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + return; + } + + inits[0] = ′ inits[1] = ℴ inits[2] = &tmpm; + inits[3] = &sigR; inits[4] = &sigS; inits[5] = &smVal; + inits[6] = &rSmall; inits[7] = &one; + for (nInit = 0; nInit < 8; nInit++) { + if (mp_init(inits[nInit]) != MP_OKAY) { + break; + } + } + if (nInit < 8) { + WB_NOTE("mp_init failed (residual extra)"); + wb_fail = 1; + } + else { + curveIdx = wc_ecc_get_curve_idx(curve_id); + dp = (curveIdx >= 0) ? wc_ecc_get_curve_params(curveIdx) : NULL; + + (void)mp_set(&one, 1); + (void)mp_set(&rSmall, 7); + (void)mp_set(&smVal, 17); + + if (dp != NULL && + mp_read_radix(&prime, dp->prime, 16) == MP_OKAY && + mp_read_radix(&order, dp->order, 16) == MP_OKAY) { + /* 1. check_key: matching priv, foreign priv, negated public Y. */ + if (check_key != NULL) { + (void)check_key(keyA.pubkey.x, keyA.pubkey.y, + ecc_get_k(&keyA), keyA.heap); + (void)check_key(keyA.pubkey.x, keyA.pubkey.y, + ecc_get_k(&keyB), keyA.heap); + if (mp_sub(&prime, keyA.pubkey.y, &tmpm) == MP_OKAY) { + (void)check_key(keyA.pubkey.x, &tmpm, ecc_get_k(&keyA), + keyA.heap); + } + } + + /* 2. verify: r + order below prime, then at/past it. */ + res = -1; + (void)verify(wb_digest, (word32)sizeof(wb_digest), keyA.pubkey.x, + keyA.pubkey.y, &one, &rSmall, &smVal, &res, keyA.heap); + if (mp_sub(&prime, &order, &tmpm) == MP_OKAY && + mp_add_d(&tmpm, 5, &tmpm) == MP_OKAY) { + res = -1; + (void)verify(wb_digest, (word32)sizeof(wb_digest), + keyA.pubkey.x, keyA.pubkey.y, &one, &tmpm, &smVal, &res, + keyA.heap); + } + } + else { + WB_NOTE("curve params unavailable (residual extra)"); + } + + /* 3. sign with an explicit km: zero, then non-zero. */ + (void)mp_zero(&tmpm); + (void)sign(wb_digest, (word32)sizeof(wb_digest), &rng, + ecc_get_k(&keyA), &sigR, &sigS, &tmpm, keyA.heap); + (void)mp_set(&tmpm, 12345); + (void)sign(wb_digest, (word32)sizeof(wb_digest), &rng, + ecc_get_k(&keyA), &sigR, &sigS, &tmpm, keyA.heap); + } + + while (nInit-- > 0) { + mp_clear(inits[nInit]); + } + wc_FreeRng(&rng); + wc_ecc_free(&keyA); + wc_ecc_free(&keyB); + WB_NOTE(label); +} + +static void wb_run_residual_extra_all(void) +{ +#ifndef WOLFSSL_SP_NO_256 + wb_run_residual_extra(ECC_SECP256R1, 32, + "P-256 check_key negated-Y / verify r+order>=prime / explicit km " + "exercised", + sp_ecc_verify_256, sp_ecc_sign_256, +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + sp_ecc_check_key_256 +#else + NULL +#endif + ); +#endif +#ifdef WOLFSSL_SP_384 + wb_run_residual_extra(ECC_SECP384R1, 48, + "P-384 check_key negated-Y / verify r+order>=prime / explicit km " + "exercised", + sp_ecc_verify_384, sp_ecc_sign_384, +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + sp_ecc_check_key_384 +#else + NULL +#endif + ); +#endif +#ifdef WOLFSSL_SP_521 + wb_run_residual_extra(ECC_SECP521R1, 66, + "P-521 check_key negated-Y / verify r+order>=prime / explicit km " + "exercised", + sp_ecc_verify_521, sp_ecc_sign_521, +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) + sp_ecc_check_key_521 +#else + NULL +#endif + ); +#endif +} +#else +static void wb_run_residual_extra_all(void) +{ + WB_NOTE("SP ECC sign/verify not both compiled; residual extras skipped"); +} +#endif + +/* ----------------------------------------------------------------------- * + * sp__mod_inv_(): the binary extended-GCD loops + * + * while (ut > 1 && vt > 1) { ... do { ... } while (ut > 0 && even(u)); } + * + * The only caller is sp__calc_vfy_point_(), which always hands it a + * signature's s -- a uniformly random unit -- so the loop always terminates the + * same way and several operands never see a false row. The helper is file + * static, which is exactly what a white-box that includes the .c can reach, so + * it is called here directly with the degenerate operands the caller cannot + * produce: + * - a == m: u and v start equal, so the first subtraction makes u zero and + * the inner do-while's FIRST operand (ut > 0) is false; + * - a == 1: v has a single bit on entry, so the outer loop's SECOND operand + * (vt > 1) is false before the body ever runs; + * - small a: ordinary termination, which lands on u == 1 for some values and + * v == 1 for others, giving the outer loop's first operand its false row. + * a == 0 is deliberately NOT used: v would stay zero and the pre-loop that + * shifts even operands right would never terminate. + * ----------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + !defined(WOLFSSL_SP_SMALL) +#define WB_MOD_INV_SWEEP(WORDS, FN, ORDER) \ + do { \ + sp_digit wbA[WORDS]; \ + sp_digit wbR[WORDS]; \ + int wbI; \ + XMEMCPY(wbA, (ORDER), sizeof(wbA)); \ + (void)FN(wbR, wbA, (ORDER)); \ + for (wbI = 1; wbI <= 40; wbI++) { \ + XMEMSET(wbA, 0, sizeof(wbA)); \ + wbA[0] = (sp_digit)wbI; \ + (void)FN(wbR, wbA, (ORDER)); \ + } \ + } while (0) + +static void wb_run_mod_inv(void) +{ +#ifndef WOLFSSL_SP_NO_256 + WB_MOD_INV_SWEEP(8, sp_256_mod_inv_8, p256_order); + WB_NOTE("P-256 sp_256_mod_inv_8 degenerate operands exercised"); +#endif +#ifdef WOLFSSL_SP_384 + WB_MOD_INV_SWEEP(12, sp_384_mod_inv_12, p384_order); + WB_NOTE("P-384 sp_384_mod_inv_12 degenerate operands exercised"); +#endif +#ifdef WOLFSSL_SP_521 + WB_MOD_INV_SWEEP(17, sp_521_mod_inv_17, p521_order); + WB_NOTE("P-521 sp_521_mod_inv_17 degenerate operands exercised"); +#endif +} +#else +static void wb_run_mod_inv(void) +{ + WB_NOTE("sp__mod_inv_ not compiled; mod-inv sweep skipped"); +} +#endif + #endif /* WOLFSSL_HAVE_SP_ECC || WOLFSSL_HAVE_SP_RSA || WOLFSSL_HAVE_SP_DH */ int main(void) @@ -1587,6 +1843,8 @@ int main(void) wb_run_gap_256(); wb_run_gap_384(); wb_run_gap_521(); + wb_run_residual_extra_all(); + wb_run_mod_inv(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); #else diff --git a/tests/unit-mcdc/test_sp_cortexm_whitebox.c b/tests/unit-mcdc/test_sp_cortexm_whitebox.c index 1cd5d8f27b1..7fbd3280ea3 100644 --- a/tests/unit-mcdc/test_sp_cortexm_whitebox.c +++ b/tests/unit-mcdc/test_sp_cortexm_whitebox.c @@ -157,12 +157,25 @@ static mp_int wb2_p256_prime, wb2_gy_neg; static mp_int wb2_zero, wb2_five, wb2_one, wb2_e65537; static mp_int wb2_base, wb2_r, wb2_s, wb2_rlarge, wb2_negr, wb2_kval; static mp_int wb2_rm_out, wb2_sm_out; +static mp_int wb2_kfix, wb2_scr, wb2_e2; static ecc_point wb2_t; static byte wb2_in[400]; static byte wb2_out[400]; static byte wb2_hash[32]; static const byte wb2_dh_exp_65537[3] = { 0x01, 0x00, 0x01 }; static const byte wb2_dh_exp_3[1] = { 0x03 }; +/* Nine bytes = 72 bits = three 32-bit exponent words. sp_3072_mod_exp_96() + * enters its window loop with `i = (bits - 1) / 32` already decremented once, + * so every exponent that fits in ONE word (the two above, and the KAT's) puts + * i at -1 before the loop is ever tested and `for (; i >= 0 || c >= 4; )` can + * only ever see its first operand false -- no true row, no pair. A three-word + * exponent gives that operand its true rows and still leaves the false ones at + * the end of the scan. Kept to 72 bits (not a full-width exponent) because + * every extra window step is four more 3072-bit Montgomery squarings on an + * emulated Cortex-M. */ +static const byte wb2_dh_exp_wide[9] = { + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01 +}; static int wb_mp_hex(mp_int* a, const char* s) { @@ -279,7 +292,8 @@ static void sp_cortexm_whitebox_drive(void) mp_init(&wb2_r) != MP_OKAY || mp_init(&wb2_s) != MP_OKAY || mp_init(&wb2_rlarge) != MP_OKAY || mp_init(&wb2_negr) != MP_OKAY || mp_init(&wb2_kval) != MP_OKAY || mp_init(&wb2_rm_out) != MP_OKAY || - mp_init(&wb2_sm_out) != MP_OKAY) { + mp_init(&wb2_sm_out) != MP_OKAY || mp_init(&wb2_kfix) != MP_OKAY || + mp_init(&wb2_scr) != MP_OKAY || mp_init(&wb2_e2) != MP_OKAY) { return; } XMEMSET(&wb2_t, 0, sizeof(wb2_t)); @@ -420,6 +434,14 @@ static void sp_cortexm_whitebox_drive(void) ret = sp_DhExp_3072(&wb2_zero, wb2_dh_exp_3, sizeof(wb2_dh_exp_3), &wb2_mm3072, wb2_out, &outLen); wb_note(ret, MP_OKAY); + + /* Three-word exponent: gives sp_3072_mod_exp_96()'s window loop + * `for (; i >= 0 || c >= 4; )` its first operand's true rows (see the + * comment on wb2_dh_exp_wide). */ + outLen = sizeof(wb2_out); + ret = sp_DhExp_3072(&wb2_base, wb2_dh_exp_wide, + sizeof(wb2_dh_exp_wide), &wb2_mm3072, wb2_out, &outLen); + wb_note(ret, MP_OKAY); } /* --- sp_ecc_mulmod_add_256 / sp_ecc_mulmod_base_add_256: each has @@ -512,6 +534,61 @@ static void sp_cortexm_whitebox_drive(void) wb_g.z, &wb2_rlarge, &wb2_s, &res, NULL); wb_note(ret, MP_OKAY); + /* V6: s = order. sp_256_calc_vfy_point_8() hands s straight to + * sp_256_mod_inv_8(), which starts from u = modulus, v = s; with v equal + * to u the very first subtraction makes u zero, and that is the only way + * the inner `while (ut > 0 && (u[0] & 1) == 0)` sees its FIRST operand + * false -- the else arm's v = v - u is always positive, so the mirrored + * `vt > 0` operand has no such vector at all. s = 0 is deliberately never + * passed here: v would stay zero and mod_inv's even-operand pre-shift + * loop would spin forever. */ + XMEMSET(wb2_hash, 0x44, sizeof(wb2_hash)); + (void)mp_set(&wb2_r, 9); + ret = sp_ecc_verify_256(wb2_hash, sizeof(wb2_hash), wb_g.x, wb_g.y, + wb_g.z, &wb2_r, &wb_n, &res, NULL); + wb_note(ret, MP_OKAY); + + /* --- sp_ecc_sign_256's `(err == MP_OKAY) && (!sp_256_iszero_8(s))` + * guard, second operand false. s = (e + r*x)/k mod order, so s == 0 + * needs e == -r*x, which needs the private scalar. It is reachable + * indirectly by using the signer as its own oracle, exploiting the fact + * that r depends only on the supplied k: + * SGN3 signs e1 = 1 with a fixed k = K, so the returned s1 satisfies + * r*x == s1*K - e1 (mod order); + * SGN4 signs e2 = e1 - s1*K with the SAME K, so r is identical and + * e2 + r*x == 0, i.e. s == 0 on the first loop iteration. + * sp_ecc_sign_256() zeroes the supplied km after use, so the retry falls + * through to the generated-k branch; with rng == NULL that fails at once + * and the call returns instead of spinning through the retry budget. */ + (void)mp_set(&wb2_kfix, 0x5A5A5); + (void)mp_copy(&wb2_kfix, &wb2_kval); + XMEMSET(wb2_hash, 0, sizeof(wb2_hash)); + wb2_hash[sizeof(wb2_hash) - 1] = 1; /* e1 = 1 */ + ret = sp_ecc_sign_256(wb2_hash, sizeof(wb2_hash), NULL, &wb_k, + &wb2_rm_out, &wb2_sm_out, &wb2_kval, NULL); + wb_note(ret, MP_OKAY); + if (ret == MP_OKAY) { + /* e2 = 1 + (order - (s1*K mod order)) mod order. Spelled out with + * mp_mul / mp_mod / mp_sub / mp_add rather than mp_mulmod and + * mp_submod: those two are not compiled in every SP math + * configuration, and every intermediate here stays non-negative, + * which builds without WOLFSSL_SP_INT_NEGATIVE require. */ + (void)mp_set(&wb2_one, 1); + if ((mp_mul(&wb2_sm_out, &wb2_kfix, &wb2_scr) == MP_OKAY) && + (mp_mod(&wb2_scr, &wb_n, &wb2_e2) == MP_OKAY) && + (mp_sub(&wb_n, &wb2_e2, &wb2_scr) == MP_OKAY) && + (mp_add(&wb2_scr, &wb2_one, &wb2_scr) == MP_OKAY) && + (mp_mod(&wb2_scr, &wb_n, &wb2_e2) == MP_OKAY)) { + wb2_mp_to_hash(&wb2_e2); + (void)mp_copy(&wb2_kfix, &wb2_kval); + ret = sp_ecc_sign_256(wb2_hash, sizeof(wb2_hash), NULL, &wb_k, + &wb2_rm_out, &wb2_sm_out, &wb2_kval, NULL); + /* Expected to fail: after the zero s the retry needs the NULL + * rng. Only the branch taken matters, not the return value. */ + (void)ret; + } + } + /* --- sp_ecc_check_key_256: the quick length guard is * A||B||(C&&D) with A=pX>256 bits, B=pY>256 bits, C=privm!=NULL, * D=privm>256 bits. K3 (all-false) is the baseline; K1/K2/K4 flip one From b4cdf15157a0ddbef8f4cf335bf1808c7c3c186b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 20:23:50 +0200 Subject: [PATCH 18/20] tests: drive the ecc fp-cache and rsa PSS and heap-fault rows --- tests/unit-mcdc/test_ecc_fault_whitebox.c | 833 ++++++++++++++++++++++ tests/unit-mcdc/test_rsa_fault_whitebox.c | 612 +++++++++++++++- 2 files changed, 1443 insertions(+), 2 deletions(-) diff --git a/tests/unit-mcdc/test_ecc_fault_whitebox.c b/tests/unit-mcdc/test_ecc_fault_whitebox.c index e77933d6976..14f0fdec4bb 100644 --- a/tests/unit-mcdc/test_ecc_fault_whitebox.c +++ b/tests/unit-mcdc/test_ecc_fault_whitebox.c @@ -64,10 +64,60 @@ * returned. */ +/* SECOND LEVER -- BIG-INTEGER FAULTS (mcdc_fault_mp.h) + * ---------------------------------------------------- + * ecc.c's fixed-point (FP_ECC) cache helpers and mp_sqrtmod_prime are written + * as big-integer success chains: + * + * if ((mp_copy(g->x, ...) != MP_OKAY) || + * (mp_copy(g->y, ...) != MP_OKAY) || ...) + * if (err == MP_OKAY && idx >= 0 && ...) + * while (res == MP_OKAY && done == 0) + * + * On a healthy machine no mp_* call ever fails, so the "!= MP_OKAY" operands + * are never TRUE and the "err == MP_OKAY" operands are never FALSE. The heap + * lever cannot reach them: it can only make an ALLOCATION fail, never a + * computation. mcdc_fault_mp.h macro-interposes the value-returning mp_* API, + * so mcdc_fm_arm(n) makes the n-th mp_* call -- and every later one -- return + * MP_VAL. This header must come BEFORE ecc.c so the wrappers are compiled + * while the mp_* names still mean the real entry points. + * + * mp_init / mp_init_multi are deliberately NOT interposed (MCDC_FM_WITH_INIT + * stays undefined): mp_sqrtmod_prime's own cleanup mp_clear()s its ten + * temporaries after an init failure, which on the non-small-stack variants + * are uninitialised stack mp_ints. Faulting only the COMPUTATION calls drives + * the same residual operands and never leaves an mp_int unconstructed. */ +#include "mcdc_fault_mp.h" + +/* Two extra interposers, local to this TU (they are not in the shared header + * because adding them there would shift the fault index of every other + * module's mp sweeps). mp_mod_d supplies the res==MP_OKAY FALSE half of + * mp_sqrtmod_prime's "prime mod 4 == 3" fast-path guard; mp_jacobi supplies + * it for the Legendre-symbol guard inside the Z search. Both wrappers are + * compiled while the names still mean the real functions. */ +#if defined(HAVE_COMP_KEY) && !defined(WOLFSSL_ATECC508A) && \ + !defined(WOLFSSL_ATECC608A) && !defined(WOLFSSL_CRYPTOCELL) && \ + !defined(WOLFSSL_SP_MATH) && !defined(SQRTMOD_USE_MOD_EXP) + #define MCDC_ECC_SQRTMOD_INTERPOSE +#endif + +#ifdef MCDC_ECC_SQRTMOD_INTERPOSE +MCDC_FM_MAYBE_UNUSED static int mcdc_ecc_mod_d(const mp_int* a, mp_digit b, + mp_digit* c) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_mod_d((mp_int*)a, b, c); +} +#undef mp_mod_d +#define mp_mod_d(a, b, c) mcdc_ecc_mod_d((a), (b), (c)) +#endif /* MCDC_ECC_SQRTMOD_INTERPOSE */ + #include #include "mcdc_fault_alloc.h" +#include #include #include @@ -194,10 +244,785 @@ static void wb_fault_sqrtmod_prime(void) #endif } +/* The same ten-operand NULL guard, driven DIRECTLY instead of through point + * decompression. Going in via wc_ecc_import_point_der_ex means the ten + * XMALLOCs sit behind every allocation the import itself performs, so the + * fail index that isolates operand k is both deep and build-dependent. + * mp_sqrtmod_prime is file-static and reachable from this TU, so calling it + * with the injector armed at 1..10 puts exactly one of its own allocations at + * the fail index -- operand k TRUE with operands 0..k-1 FALSE -- and the + * disarmed call beside it is the all-FALSE row. A no-op on the variants where + * the ten temporaries are stack mp_ints and no XMALLOC site exists. */ +static void wb_fault_sqrtmod_prime_direct(void) +{ +#ifdef MCDC_ECC_SQRTMOD_INTERPOSE + mp_int p, n, r; + int i; + + if (mp_init_multi(&p, &n, &r, NULL, NULL, NULL) != MP_OKAY) { + wb_fail = 1; + return; + } + if (mp_set_int(&p, 17UL) != MP_OKAY || mp_set_int(&n, 2UL) != MP_OKAY) { + wb_fail = 1; + goto out; + } + + (void)mp_sqrtmod_prime(&n, &p, &r); /* all-FALSE row */ + for (i = 1; i <= 12; i++) { + (void)mp_set_int(&p, 17UL); + (void)mp_set_int(&n, 2UL); + mcdc_fa_arm(i); + (void)mp_sqrtmod_prime(&n, &p, &r); + mcdc_fa_disarm(); + } + + WB_NOTE("mp_sqrtmod_prime direct 10-operand NULL-guard sweep done"); +out: + mcdc_fa_disarm(); + mp_clear(&p); + mp_clear(&n); + mp_clear(&r); +#else + WB_NOTE("sqrtmod_prime not compiled; direct NULL-guard sweep skipped"); +#endif +} + +/* ========================================================================= + * FP_ECC fixed-point cache (find_hole / find_base / add_entry / build_lut / + * accel_fp_mul / accel_fp_mul2add and the three public entry points that + * drive them: wc_ecc_mulmod_ex, wc_ecc_mulmod_ex2, ecc_mul2add). + * + * Why these residuals exist and how each is closed: + * + * - The "(mp_copy(..) != MP_OKAY) || .." OR-chains in add_entry / build_lut / + * accel_fp_mul are unreachable without a computation fault: mcdc_fm_arm(n) + * supplies operand n-1 TRUE with every earlier operand FALSE, and the + * disarmed call right beside it supplies the all-FALSE row IN THE SAME + * BINARY. + * + * - The "err == MP_OKAY && idx >= 0 && lru_count < INT_MAX-1" chains need + * three different cache states, none of which a normal API call produces: + * operand 0 FALSE -> add_entry must fail (mp fault, index 1 / 4) + * operand 1 FALSE -> no cache slot at all (every entry .lock = 1, so + * find_base misses and find_hole returns -1) + * operand 2 FALSE -> lru_count saturated (set to INT_MAX by hand) + * The same locked-cache state is what drives find_hole's own "z >= 0" + * operand FALSE and the "idx >= 0" operands of the LUT-build and + * LUT-use guards further down each entry point. + * + * - find_base's per-coordinate comparison chain only ever sees "all three + * match" or "x differs"; a cache entry whose x matches but whose y (then + * z) differs is built by hand to supply the two missing rows. + * + * fp_cache, and every helper above, is file-static -- reachable only because + * this TU #includes ecc.c. All crafted state is torn down with + * wc_ecc_fp_free() before the next scenario, so a rejection vector can never + * leave a poisoned cache behind for the vector after it. + * ========================================================================= */ +#if defined(FP_ECC) && !defined(WOLFSSL_SP_MATH) && \ + !defined(WOLFSSL_NO_MALLOC) + +typedef struct wb_fp_ctx { + mp_int a; + mp_int prime; + mp_int order; + mp_int mu; + mp_int k; + mp_int k1; + ecc_point* G; + ecc_point* B; + ecc_point* R; + int inited; +} wb_fp_ctx; + +static int wb_fp_setup(wb_fp_ctx* c) +{ + int idx; + const ecc_set_type* cs; + + XMEMSET(c, 0, sizeof(*c)); + + idx = wc_ecc_get_curve_idx(ECC_SECP256R1); + if (idx == ECC_CURVE_INVALID) + return 0; + cs = wc_ecc_get_curve_params(idx); + if (cs == NULL) + return 0; + + if (mp_init_multi(&c->a, &c->prime, &c->order, &c->mu, &c->k, &c->k1) + != MP_OKAY) + return 0; + c->inited = 1; + + c->G = wc_ecc_new_point(); + c->B = wc_ecc_new_point(); + c->R = wc_ecc_new_point(); + if (c->G == NULL || c->B == NULL || c->R == NULL) + return 0; + + if (mp_read_radix(&c->a, cs->Af, MP_RADIX_HEX) != MP_OKAY || + mp_read_radix(&c->prime, cs->prime, MP_RADIX_HEX) != MP_OKAY || + mp_read_radix(&c->order, cs->order, MP_RADIX_HEX) != MP_OKAY || + mp_read_radix(c->G->x, cs->Gx, MP_RADIX_HEX) != MP_OKAY || + mp_read_radix(c->G->y, cs->Gy, MP_RADIX_HEX) != MP_OKAY || + mp_set(c->G->z, 1) != MP_OKAY) + return 0; + + /* B = -G: a second, distinct, genuinely on-curve base point, so the + * two-base Shamir path caches two different entries. */ + if (mp_copy(c->G->x, c->B->x) != MP_OKAY || + mp_sub(&c->prime, c->G->y, c->B->y) != MP_OKAY || + mp_set(c->B->z, 1) != MP_OKAY) + return 0; + + /* two scalars: k has set bits high enough to walk every lut_gap + * iteration, k1 is 1 so only the very last iteration of the Shamir loop + * sees a non-zero digit for it. Pairing them makes the "this base's digit + * is non-zero while the accumulator is still empty" case happen for + * exactly one of the two bases. */ + if (mp_set_int(&c->k, 0x9E3779B9UL) != MP_OKAY || + mp_set(&c->k1, 1) != MP_OKAY) + return 0; + + return 1; +} + +static void wb_fp_teardown(wb_fp_ctx* c) +{ + wc_ecc_del_point(c->G); + wc_ecc_del_point(c->B); + wc_ecc_del_point(c->R); + if (c->inited) { + mp_clear(&c->a); + mp_clear(&c->prime); + mp_clear(&c->order); + mp_clear(&c->mu); + mp_clear(&c->k); + mp_clear(&c->k1); + } + XMEMSET(c, 0, sizeof(*c)); +} + +/* every entry locked -> find_base misses, find_hole finds no hole */ +static void wb_fp_lock_all(int on) +{ + int x; + for (x = 0; x < FP_ENTRIES; x++) + fp_cache[x].lock = on ? 1 : 0; +} + +/* find_hole "z >= 0" FALSE, plus the "idx == -1 / idx >= 0 FALSE" rows of + * wc_ecc_mulmod_ex, wc_ecc_mulmod_ex2 and ecc_mul2add. With no slot the three + * entry points fall back to their normal (non-cached) mulmod, so each call is + * an ordinary, fully successful scalar multiply -- the accepting side of the + * surrounding err == MP_OKAY operands comes for free. */ +static void wb_fp_no_slot(wb_fp_ctx* c) +{ + int x; + + /* find_hole "z >= 0" TRUE-determining row: entry 0 holds a base and is + * the only unlocked (so lowest-lru) slot, which makes find_hole choose it + * and free it -- the eviction path a fresh cache never reaches. */ + wc_ecc_fp_free(); + wb_fp_lock_all(0); + if (add_entry(0, c->G) == MP_OKAY) { + for (x = 1; x < FP_ENTRIES; x++) + fp_cache[x].lock = 1; + (void)find_hole(); + wb_fp_lock_all(0); + } + else { + wb_fail = 1; + } + + wc_ecc_fp_free(); + wb_fp_lock_all(1); + + (void)find_hole(); + + (void)wc_ecc_mulmod_ex(&c->k, c->G, c->R, &c->a, &c->prime, 1, NULL); + (void)wc_ecc_mulmod_ex2(&c->k, c->G, c->R, &c->a, &c->prime, &c->order, + NULL, 1, NULL); +#ifdef ECC_SHAMIR + (void)ecc_mul2add(c->G, &c->k, c->B, &c->k, c->R, &c->a, &c->prime, NULL); +#endif + + wb_fp_lock_all(0); + wc_ecc_fp_free(); + WB_NOTE("FP_ECC locked-cache (no slot) vectors done"); +} + +/* find_base: x matches but y differs, then x+y match but z differs. */ +static void wb_fp_find_base_partial(wb_fp_ctx* c) +{ + wc_ecc_fp_free(); + wb_fp_lock_all(0); + + fp_cache[0].g = wc_ecc_new_point(); + if (fp_cache[0].g == NULL) { + wb_fail = 1; + return; + } + (void)mp_copy(c->G->x, fp_cache[0].g->x); + (void)mp_set(fp_cache[0].g->y, 7); + (void)mp_copy(c->G->z, fp_cache[0].g->z); + (void)find_base(c->G); /* y comparison FALSE */ + + (void)mp_copy(c->G->y, fp_cache[0].g->y); + (void)mp_set(fp_cache[0].g->z, 9); + (void)find_base(c->G); /* z comparison FALSE */ + + (void)mp_copy(c->G->z, fp_cache[0].g->z); + (void)find_base(c->G); /* all three match */ + + wc_ecc_del_point(fp_cache[0].g); + fp_cache[0].g = NULL; + WB_NOTE("find_base partial-match vectors done"); +} + +/* add_entry's three-mp_copy OR-chain: arm 1/2/3, then the all-FALSE row. */ +static void wb_fp_add_entry_faults(wb_fp_ctx* c) +{ + long n; + + for (n = 1; n <= 3; n++) { + wc_ecc_fp_free(); + mcdc_fm_arm(n); + (void)add_entry(0, c->G); + mcdc_fm_disarm(); + } + wc_ecc_fp_free(); + (void)add_entry(0, c->G); + wc_ecc_fp_free(); + WB_NOTE("add_entry mp_copy chain arm(1..3) + accept done"); +} + +/* build_lut's two OR-chains and accel_fp_mul's LUT-copy chain. + * + * build_lut's interposed call order is fixed: three mp_mulmod (the "copy + * base" chain) then three mp_copy (the "single bit entries" chain), so + * arm(1..3) walks the first chain operand by operand and arm(4..6) the + * second. mp_init / mp_init_copy are not interposed, so the indices do not + * drift between variants. + * + * The one full (disarmed) build_lut is what makes accel_fp_mul reachable at + * all -- it is the only expensive call in this file, so it runs exactly once + * and its LUT is then reused by every later scenario. */ +static void wb_fp_build_lut_and_mul(wb_fp_ctx* c) +{ + long n; + mp_digit mp = 0; + + wc_ecc_fp_free(); + if (mp_montgomery_setup(&c->prime, &mp) != MP_OKAY || + mp_montgomery_calc_normalization(&c->mu, &c->prime) != MP_OKAY) { + wb_fail = 1; + return; + } + + for (n = 1; n <= 6; n++) { + wc_ecc_fp_free(); + if (add_entry(0, c->G) != MP_OKAY) { + wb_fail = 1; + continue; + } + mcdc_fm_arm(n); + (void)build_lut(0, &c->a, &c->prime, mp, &c->mu); + mcdc_fm_disarm(); + } + + wc_ecc_fp_free(); + if (add_entry(0, c->G) == MP_OKAY && + build_lut(0, &c->a, &c->prime, mp, &c->mu) == MP_OKAY) { + /* 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++) { + mcdc_fm_arm(n); + (void)accel_fp_mul(0, &c->k, c->R, &c->a, &c->prime, mp, 1); + mcdc_fm_disarm(); + } + } + else { + wb_fail = 1; + } + wc_ecc_fp_free(); + WB_NOTE("build_lut mp chains arm(1..6) + accel_fp_mul sweep done"); +} + +/* The lru_count / LUT_set guards of the three public entry points. + * + * A cache warmed by two ordinary calls leaves LUT_set == 1, so the saturated + * lru_count vectors below cost no second LUT build. The err == MP_OKAY FALSE + * halves come from an armed add_entry: index 1 fails the first base's copy, + * index 4 lets the first base through and fails the second's. */ +static void wb_fp_lru_and_lutset(wb_fp_ctx* c) +{ + int i; + long n; + + /* Each entry point warms its OWN cache: the "lru_count >= 2 but the LUT + * is not built yet" row of its build-the-LUT guard only happens on the + * second call THROUGH THAT ENTRY POINT, and a cache warmed through a + * sibling would arrive with LUT_set already 1 and skip it. Three calls + * per entry point give lru 1 (guard FALSE on lru), lru 2 with no LUT + * (guard TRUE, builds it) and lru 3 with the LUT set (guard FALSE on + * LUT_set). */ + + /* --- wc_ecc_mulmod_ex --- */ + wc_ecc_fp_free(); + for (i = 0; i < 3; i++) + (void)wc_ecc_mulmod_ex(&c->k, c->G, c->R, &c->a, &c->prime, 1, NULL); + i = find_base(c->G); + if (i >= 0) { + fp_cache[i].lru_count = INT_MAX; + (void)wc_ecc_mulmod_ex(&c->k, c->G, c->R, &c->a, &c->prime, 1, NULL); + } + else { + wb_fail = 1; + } + + /* --- wc_ecc_mulmod_ex2 --- */ + wc_ecc_fp_free(); + for (i = 0; i < 3; i++) + (void)wc_ecc_mulmod_ex2(&c->k, c->G, c->R, &c->a, &c->prime, + &c->order, NULL, 1, NULL); + i = find_base(c->G); + if (i >= 0) { + fp_cache[i].lru_count = INT_MAX; + (void)wc_ecc_mulmod_ex2(&c->k, c->G, c->R, &c->a, &c->prime, + &c->order, NULL, 1, NULL); + } + else { + wb_fail = 1; + } + +#ifdef ECC_SHAMIR + /* --- ecc_mul2add: warmed through itself, so both LUT-build guards see + * their TRUE row, then accel_fp_mul2add runs for real. --- */ + wc_ecc_fp_free(); + for (i = 0; i < 3; i++) + (void)ecc_mul2add(c->G, &c->k, c->B, &c->k, c->R, &c->a, &c->prime, + NULL); + + /* one base's digit non-zero while the accumulator is still empty: k1 == 1 + * puts B's only set bit in the final iteration, so every earlier + * iteration has zA non-zero with zB zero (and the mirrored call gives the + * opposite). */ + (void)ecc_mul2add(c->G, &c->k, c->B, &c->k1, c->R, &c->a, &c->prime, NULL); + (void)ecc_mul2add(c->G, &c->k1, c->B, &c->k, c->R, &c->a, &c->prime, NULL); + + /* accel_fp_mul2add's own LUT-copy chains: the LUTs are built, so an armed + * mp_* aborts inside the Shamir loop instead of before it. */ + for (n = 1; n <= 14; n++) { + mcdc_fm_arm(n); + (void)ecc_mul2add(c->G, &c->k, c->B, &c->k1, c->R, &c->a, &c->prime, + NULL); + mcdc_fm_disarm(); + mcdc_fm_arm(n); + /* mirrored scalars: the second base is the one whose digit is + * non-zero while the accumulator is still empty, so this sweep walks + * the OTHER copy-chain of the Shamir loop. */ + (void)ecc_mul2add(c->G, &c->k1, c->B, &c->k, c->R, &c->a, &c->prime, + NULL); + mcdc_fm_disarm(); + } + + i = find_base(c->G); + if (i >= 0) + fp_cache[i].lru_count = INT_MAX; + i = find_base(c->B); + if (i >= 0) + fp_cache[i].lru_count = INT_MAX; + (void)ecc_mul2add(c->G, &c->k, c->B, &c->k, c->R, &c->a, &c->prime, NULL); + + /* idx1 cached but no slot left for idx2: entry for G stays resolvable by + * find_base (which ignores .lock) while find_hole has nothing to give. */ + wc_ecc_fp_free(); + if (add_entry(0, c->G) == MP_OKAY) { + wb_fp_lock_all(1); + (void)ecc_mul2add(c->G, &c->k, c->B, &c->k, c->R, &c->a, &c->prime, + NULL); + wb_fp_lock_all(0); + } + else { + wb_fail = 1; + } +#else + (void)n; +#endif + + /* --- err == MP_OKAY FALSE halves (armed add_entry) --- */ + wc_ecc_fp_free(); + mcdc_fm_arm(1); + (void)wc_ecc_mulmod_ex(&c->k, c->G, c->R, &c->a, &c->prime, 1, NULL); + mcdc_fm_disarm(); + + wc_ecc_fp_free(); + mcdc_fm_arm(1); + (void)wc_ecc_mulmod_ex2(&c->k, c->G, c->R, &c->a, &c->prime, &c->order, + NULL, 1, NULL); + mcdc_fm_disarm(); + +#ifdef ECC_SHAMIR + wc_ecc_fp_free(); + mcdc_fm_arm(1); + (void)ecc_mul2add(c->G, &c->k, c->B, &c->k, c->R, &c->a, &c->prime, NULL); + mcdc_fm_disarm(); + + wc_ecc_fp_free(); + mcdc_fm_arm(4); /* first base's three copies pass, second base fails */ + (void)ecc_mul2add(c->G, &c->k, c->B, &c->k, c->R, &c->a, &c->prime, NULL); + mcdc_fm_disarm(); +#endif + + wc_ecc_fp_free(); + WB_NOTE("FP_ECC lru_count / LUT_set / add_entry-failure vectors done"); +} + +/* wc_ecc_mulmod_ex / wc_ecc_mulmod_ex2 argument guards: the residual operands + * are the ones no caller in the library ever violates (a == NULL, and + * order == NULL on the _ex2 form). The valid call right after is the + * all-FALSE row of the same chain, in the same binary. */ +static void wb_fp_argguards(wb_fp_ctx* c) +{ + (void)wc_ecc_mulmod_ex(&c->k, c->G, c->R, NULL, &c->prime, 1, NULL); + (void)wc_ecc_mulmod_ex2(&c->k, c->G, c->R, NULL, &c->prime, &c->order, + NULL, 1, NULL); + (void)wc_ecc_mulmod_ex2(&c->k, c->G, c->R, &c->a, &c->prime, NULL, + NULL, 1, NULL); + wc_ecc_fp_free(); + (void)wc_ecc_mulmod_ex(&c->k, c->G, c->R, &c->a, &c->prime, 1, NULL); + wc_ecc_fp_free(); + WB_NOTE("wc_ecc_mulmod_ex/_ex2 NULL-argument operands done"); +} + +static void wb_fp_cache_suite(void) +{ + wb_fp_ctx c; + + if (!wb_fp_setup(&c)) { + wb_fail = 1; + wb_fp_teardown(&c); + WB_NOTE("FP_ECC suite setup failed; skipped"); + return; + } + + wb_fp_build_lut_and_mul(&c); /* heaviest first */ + wb_fp_lru_and_lutset(&c); + wb_fp_add_entry_faults(&c); + wb_fp_find_base_partial(&c); + wb_fp_no_slot(&c); + wb_fp_argguards(&c); + + wc_ecc_fp_free(); + wb_fp_lock_all(0); + wb_fp_teardown(&c); +} +#else +static void wb_fp_cache_suite(void) +{ + WB_NOTE("FP_ECC off (or no-malloc build); fixed-point cache suite skipped"); +} +#endif /* FP_ECC && !WOLFSSL_SP_MATH && !WOLFSSL_NO_MALLOC */ + +/* ========================================================================= + * Degenerate point operands. + * + * _ecc_projective_add_point's "should we double instead?" test and both + * _safe wrappers' infinity handling only fire for operand shapes a scalar + * multiply never produces: P and Q the same point, P and Q negatives of each + * other, a zero Z (a point already at infinity in Jacobian form), and a zero + * curve coefficient. Each is built by hand here and fed to the file-static + * primitives directly. The mp_set chains that follow those tests are error + * propagation, so their "err == MP_OKAY" operands need an armed mp_* as well. + * ========================================================================= */ +#if defined(HAVE_ECC) && !defined(WOLFSSL_SP_MATH) && \ + !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A) && \ + !defined(WOLFSSL_CRYPTOCELL) +static void wb_degenerate_points(void) +{ + mp_int a, zero, prime; + ecc_point *P = NULL, *Q = NULL, *R = NULL; + mp_digit mp = 0; + int idx, inf = 0; + long n; + const ecc_set_type* cs; + + idx = wc_ecc_get_curve_idx(ECC_SECP256R1); + if (idx == ECC_CURVE_INVALID) { + wb_fail = 1; + return; + } + cs = wc_ecc_get_curve_params(idx); + if (cs == NULL) { + wb_fail = 1; + return; + } + if (mp_init_multi(&a, &zero, &prime, NULL, NULL, NULL) != MP_OKAY) { + wb_fail = 1; + return; + } + P = wc_ecc_new_point(); + Q = wc_ecc_new_point(); + R = wc_ecc_new_point(); + if (P == NULL || Q == NULL || R == NULL) { + wb_fail = 1; + goto out; + } + if (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(P->x, cs->Gx, MP_RADIX_HEX) != MP_OKAY || + mp_read_radix(P->y, cs->Gy, MP_RADIX_HEX) != MP_OKAY || + mp_set(P->z, 1) != MP_OKAY || + mp_set(&zero, 0) != MP_OKAY || + mp_montgomery_setup(&prime, &mp) != MP_OKAY) { + wb_fail = 1; + goto out; + } + + /* Q = P: x, z and y all compare equal -> the whole chain TRUE */ + if (wc_ecc_copy_point(P, Q) != MP_OKAY) { + wb_fail = 1; + goto out; + } + (void)_ecc_projective_add_point(P, Q, R, &a, &prime, mp); + + /* Q = -P: x and z equal, y differs but equals modulus - P->y */ + (void)mp_sub(&prime, P->y, Q->y); + (void)_ecc_projective_add_point(P, Q, R, &a, &prime, mp); + + /* Q->z == 0: the digit-count operand FALSE with x still equal */ + (void)wc_ecc_copy_point(P, Q); + (void)mp_set(Q->z, 0); + (void)_ecc_projective_add_point(P, Q, R, &a, &prime, mp); + + /* Q->z differs (non-zero): the z comparison FALSE */ + (void)mp_set(Q->z, 2); + (void)_ecc_projective_add_point(P, Q, R, &a, &prime, mp); + + /* Q->x differs: the first operand FALSE */ + (void)wc_ecc_copy_point(P, Q); + (void)mp_set(Q->x, 3); + (void)_ecc_projective_add_point(P, Q, R, &a, &prime, mp); + + /* y differs and is NOT the negation either: the last operand FALSE */ + (void)wc_ecc_copy_point(P, Q); + (void)mp_set(Q->y, 5); + (void)_ecc_projective_add_point(P, Q, R, &a, &prime, mp); + + /* curve coefficient zero -> "modulus - a is zero" TRUE in the doubling + * formula's coefficient selection; the real a gives the FALSE row. */ + (void)_ecc_projective_dbl_point(P, R, &zero, &prime, mp); + (void)_ecc_projective_dbl_point(P, R, &a, &prime, mp); + for (n = 1; n <= 10; n++) { + mcdc_fm_arm(n); + (void)_ecc_projective_dbl_point(P, R, &zero, &prime, mp); + mcdc_fm_disarm(); + } + + /* --- the _safe wrappers --- */ + + /* A = -B: the wrapper's own set-to-infinity chain, with and without an + * infinity out-pointer, and with the chain faulted. */ + (void)wc_ecc_copy_point(P, Q); + (void)mp_sub(&prime, P->y, Q->y); + inf = 0; + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, &inf); + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, NULL); + for (n = 1; n <= 6; n++) { + mcdc_fm_arm(n); + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, &inf); + mcdc_fm_disarm(); + } + + /* B with a zero Z and a different X: the add itself returns Z == 0, so + * the wrapper's "result is infinity" tail runs. */ + (void)wc_ecc_copy_point(P, Q); + (void)mp_set(Q->x, 3); + (void)mp_set(Q->z, 0); + inf = 0; + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, &inf); + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, NULL); + (void)ecc_projective_add_point_safe(Q, P, R, &a, &prime, mp, &inf); + /* The set-to-infinity chain sits behind the whole add, so its + * "err == MP_OKAY" operand only goes FALSE when the armed index lands + * exactly on one of those mp_set calls -- hence a sweep long enough to + * walk past every mp_* the add itself performs. */ + /* Negatives of each other on DIFFERENT projective representatives: + * B = (x*L^2, -y*L^3, z*L). The wrapper's x/z equality test misses it, so + * the raw add runs and returns Z == 0 with X and Y non-zero -- the + * "only Z zero -> result is infinity" arm, which the all-zero shape above + * never reaches. */ + (void)wc_ecc_copy_point(P, Q); + (void)mp_set(&zero, 4); + (void)mp_mulmod(P->x, &zero, &prime, Q->x); + (void)mp_sub(&prime, P->y, Q->y); + (void)mp_set(&zero, 8); + (void)mp_mulmod(Q->y, &zero, &prime, Q->y); + (void)mp_set(&zero, 2); + (void)mp_mulmod(P->z, &zero, &prime, Q->z); + (void)mp_set(&zero, 0); + inf = 0; + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, &inf); + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, NULL); + for (n = 1; n <= 40; n++) { + mcdc_fm_arm(n); + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, &inf); + mcdc_fm_disarm(); + } + + (void)wc_ecc_copy_point(P, Q); + (void)mp_set(Q->x, 3); + (void)mp_set(Q->z, 0); + for (n = 1; n <= 40; n++) { + mcdc_fm_arm(n); + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, &inf); + mcdc_fm_disarm(); + } + + /* B is the SAME affine point as A but on a different projective + * representative: (x*L^2, y*L^3, z*L) for L = 2. The wrapper's x/z + * equality test therefore misses it, the raw add detects the degeneracy + * and returns X == Y == Z == 0, and the "all zero -> should have doubled" + * recovery path runs -- the one shape a scalar multiply never feeds in. + * The scaling survives Montgomery form because both sides of the + * comparison pick up the same R factor. */ + (void)wc_ecc_copy_point(P, Q); + (void)mp_mulmod(P->x, P->x, &prime, &zero); /* scratch: unused value */ + (void)mp_set(&zero, 4); + (void)mp_mulmod(P->x, &zero, &prime, Q->x); /* x * 4 */ + (void)mp_set(&zero, 8); + (void)mp_mulmod(P->y, &zero, &prime, Q->y); /* y * 8 */ + (void)mp_set(&zero, 2); + (void)mp_mulmod(P->z, &zero, &prime, Q->z); /* z * 2 */ + inf = 0; + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, &inf); + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, NULL); + /* same shape with B->z already zero: the recovery takes its other arm */ + (void)mp_set(Q->z, 0); + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, &inf); + (void)mp_set(&zero, 0); + + /* A already at infinity (x == y == 0) on either side */ + (void)mp_set(Q->x, 0); + (void)mp_set(Q->y, 0); + (void)mp_set(Q->z, 1); + (void)ecc_projective_add_point_safe(Q, P, R, &a, &prime, mp, &inf); + (void)ecc_projective_add_point_safe(P, Q, R, &a, &prime, mp, &inf); + (void)ecc_projective_dbl_point_safe(Q, R, &a, &prime, mp); + + /* P->z == 0 -> doubling yields Z == 0, so the dbl wrapper's own + * set-to-infinity chain runs (and is then faulted). */ + (void)wc_ecc_copy_point(P, Q); + (void)mp_set(Q->z, 0); + (void)ecc_projective_dbl_point_safe(Q, R, &a, &prime, mp); + for (n = 1; n <= 20; n++) { + mcdc_fm_arm(n); + (void)ecc_projective_dbl_point_safe(Q, R, &a, &prime, mp); + mcdc_fm_disarm(); + } + + WB_NOTE("degenerate point-operand vectors done"); + +out: + mcdc_fm_disarm(); + wc_ecc_del_point(P); + wc_ecc_del_point(Q); + wc_ecc_del_point(R); + mp_clear(&a); + mp_clear(&zero); + mp_clear(&prime); +} +#else +static void wb_degenerate_points(void) +{ + WB_NOTE("generic point math not compiled; degenerate operands skipped"); +} +#endif + +/* ========================================================================= + * mp_sqrtmod_prime: the Tonelli-Shanks "res == MP_OKAY && .." chain. + * + * Called directly (it is file-static) rather than through point + * decompression, so the prime can be chosen to select each branch: + * + * P-256's prime == 3 (mod 4) -> the mp_exptmod fast path; + * 17 == 1 (mod 4) -> full Tonelli-Shanks; + * 9 and 21 are NOT prime -> the two "clamp the loop in case 'prime' is not + * really prime" guards, which are the only way to + * drive mp_cmp(Z,prime)==MP_EQ and + * mp_cmp_d(M,i)==MP_EQ TRUE. 9 admits no Z with + * Legendre -1 at all (every Jacobi symbol mod a + * square is 0 or 1), so the Z search runs up to + * Z == prime; 21 does admit one, so the search + * succeeds and the failure surfaces in the inner + * reduce-to-one loop instead. + * + * Each prime is first run DISARMED (the accepting row of every guard in the + * chain, same binary) and then swept: arm(n) makes the n-th mp_* call fail, + * which is the only way to drive the res == MP_OKAY operands FALSE. + * ========================================================================= */ +#ifdef MCDC_ECC_SQRTMOD_INTERPOSE +static void wb_sqrtmod_prime_cases(void) +{ + static const struct { unsigned long p; unsigned long n; } cases[] = { + { 17UL, 2UL }, /* real prime, 1 mod 4: Tonelli-Shanks */ + { 13UL, 3UL }, /* real prime, 1 mod 4 */ + { 9UL, 5UL }, /* composite: Z search exhausts to Z == prime */ + { 21UL, 5UL }, /* composite: inner reduce-to-one loop clamps */ + { 23UL, 2UL }, /* real prime, 3 mod 4: mp_exptmod fast path */ + }; + mp_int p, n, r; + size_t ci; + long k, i; + + if (mp_init_multi(&p, &n, &r, NULL, NULL, NULL) != MP_OKAY) { + wb_fail = 1; + return; + } + + for (ci = 0; ci < sizeof(cases) / sizeof(cases[0]); ci++) { + if (mp_set_int(&p, cases[ci].p) != MP_OKAY || + mp_set_int(&n, cases[ci].n) != MP_OKAY) { + wb_fail = 1; + continue; + } + + mcdc_fm_disarm(); + (void)mp_sqrtmod_prime(&n, &p, &r); + k = mcdc_fm_seen(); + if (k > 40) + k = 40; + + for (i = 1; i <= k; i++) { + if (mp_set_int(&p, cases[ci].p) != MP_OKAY || + mp_set_int(&n, cases[ci].n) != MP_OKAY) + continue; + mcdc_fm_arm(i); + (void)mp_sqrtmod_prime(&n, &p, &r); + mcdc_fm_disarm(); + } + } + + mcdc_fm_disarm(); + mp_clear(&p); + mp_clear(&n); + mp_clear(&r); + WB_NOTE("mp_sqrtmod_prime branch + mp-fault sweep done"); +} +#else +static void wb_sqrtmod_prime_cases(void) +{ + WB_NOTE("HAVE_COMP_KEY off (or mod-exp sqrt); sqrtmod_prime cases skipped"); +} +#endif /* MCDC_ECC_SQRTMOD_INTERPOSE */ + #endif /* HAVE_ECC && !WOLF_CRYPTO_CB_ONLY_ECC && !WOLFSSL_SP_MATH */ int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("ecc.c fault white-box MC/DC supplement\n"); #if !defined(HAVE_ECC) || defined(WOLF_CRYPTO_CB_ONLY_ECC) || \ defined(WOLFSSL_SP_MATH) @@ -205,9 +1030,17 @@ int main(void) "build); nothing to exercise\n"); return 0; #else + /* mp-fault work first: it is bounded and cheap, while the heap sweeps + * below walk a long fail index. */ + wb_fp_cache_suite(); + wb_degenerate_points(); + wb_sqrtmod_prime_cases(); + mcdc_fm_disarm(); + mcdc_fa_install(); wb_fault_projective_add_dbl(); wb_fault_sqrtmod_prime(); + wb_fault_sqrtmod_prime_direct(); mcdc_fa_disarm(); mcdc_fa_restore(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); diff --git a/tests/unit-mcdc/test_rsa_fault_whitebox.c b/tests/unit-mcdc/test_rsa_fault_whitebox.c index 719ef156667..7bc6b31c1a7 100644 --- a/tests/unit-mcdc/test_rsa_fault_whitebox.c +++ b/tests/unit-mcdc/test_rsa_fault_whitebox.c @@ -80,9 +80,54 @@ * binary with NO arguments -- gets full coverage.) */ +/* SECOND LEVER -- BIG-INTEGER FAULTS (mcdc_fault_mp.h) + * ---------------------------------------------------- + * The heap sweep above cannot reach rsa.c's LARGEST residual class, the + * RsaFunctionPrivate / RsaFunctionSync big-integer success chains: + * + * if (ret == 0 && mp_exptmod(tmp, &key->dQ, &key->q, tmpb) != MP_OKAY) + * if (ret == 0 && mp_submod(tmpa, tmpb, &key->p, tmp) != MP_OKAY) + * if ((ret == 0) && (mp_montgomery_setup(&key->n, &mp) != MP_OKAY)) + * if (ret == 0 && mp_read_unsigned_bin(tmp, in, inLen) != MP_OKAY) + * ... and the same shape all through wc_MakeRsaKey / _CheckProbablePrime + * + * On a healthy machine no mp_* call ever fails, so the `mp_xxx(..) != MP_OKAY` + * operand is never TRUE and the `ret == 0` operand is never FALSE. The heap + * lever cannot substitute: with the base config's SP backend the mp scratch is + * not always heap, and even where it is, only the ALLOCATION can be failed, + * never the computation. mcdc_fault_mp.h macro-interposes the value-returning + * mp_* API for this translation unit only (installed BEFORE rsa.c is + * #included) and mcdc_fm_arm(n) makes the n-th mp_* call -- and every later + * one -- return MP_VAL. One sweep over n therefore drives BOTH operands of + * every guard in the chain. Predicates (mp_iszero / mp_cmp / mp_count_bits) + * and teardown (mp_clear / mp_forcezero) are NOT interposed, so cleanup keeps + * working and every armed call stays crash-safe; mp_init / mp_init_multi are + * likewise left alone (MCDC_FM_WITH_INIT is not defined) because rsa.c's + * INIT_MP_INT_SIZE failure path still runs mp_forcezero() over the + * unconstructed scratch. + */ +#include "mcdc_fault_mp.h" + +/* mp_montgomery_reduce_ct is the one computation in RsaFunctionPrivate's + * blinding-invert tail that mcdc_fault_mp.h does not interpose (it is a macro + * in every backend rather than an entry point of its own). Wrapping it here, + * on the shared mcdc_fm counter, keeps 3041's pair inside the same sweep. */ +#ifdef mp_montgomery_reduce_ct +MCDC_FM_MAYBE_UNUSED static int mcdc_rsa_mont_red_ct(mp_int* a, mp_int* m, + mp_digit rho) +{ + if (mcdc_fm_hit()) + return MCDC_FM_ERR; + return mp_montgomery_reduce_ct(a, m, rho); +} +#undef mp_montgomery_reduce_ct +#define mp_montgomery_reduce_ct(a, m, rho) mcdc_rsa_mont_red_ct((a), (m), (rho)) +#endif + #include #include "mcdc_fault_alloc.h" +#include #include #include @@ -320,6 +365,407 @@ static void wb_pss_checkpadding_sigcheck(void) { WB_NOTE("WOLFSSL_PSS_LONG_SALT/WC_RSA_PSS off; sigCheck check skipped"); } #endif +/* --------------------------------------------------------------------------- + * FIPS 186-4 section 5.5 item (e): the default PSS salt length is the hash + * length EXCEPT for a 1024-bit modulus with SHA-512, where it is clamped to + * RSA_PSS_SALT_MAX_SZ. The guard + * + * if (bits == 1024 && hLen == WC_SHA512_DIGEST_SIZE) + * + * appears in RsaUnPad_PSS and in both wc_RsaPSS_VerifyCheck* entry points, + * and every one of them is residual because the suite only ever signs with a + * 2048-bit key: the first operand is never TRUE. Three vectors give both + * operands their independence pair: + * + * 1024-bit key + SHA-512 -> (T,T) + * 1024-bit key + SHA-256 -> (T,F) + * 2048-bit key + SHA-512 -> (F,-) + * + * A 1024-bit key is only accepted where RSA_MIN_SIZE allows it (the + * min_size_1024 variant), which is enough: a condition counts as covered in + * the union as soon as ONE build that compiles it demonstrates the pair. + * ------------------------------------------------------------------------- */ +#if defined(WC_RSA_PSS) && defined(WOLFSSL_SHA512) && \ + defined(WOLFSSL_KEY_GEN) && !defined(WOLFSSL_RSA_PUBLIC_ONLY) && \ + !defined(WOLFSSL_RSA_VERIFY_ONLY) && (RSA_MIN_SIZE <= 1024) +static void wb_pss_saltlen_1024_sha512(RsaKey* key2048, WC_RNG* rng) +{ + RsaKey k1024; + byte sig[256]; + byte out[256]; + byte dig[WC_SHA512_DIGEST_SIZE]; + int sz; + int inited = 0; + + XMEMSET(&k1024, 0, sizeof(k1024)); + XMEMSET(dig, 0x5c, sizeof(dig)); + + if (wc_InitRsaKey(&k1024, NULL) != 0) { + wb_fail = 1; + return; + } + inited = 1; + if (wc_MakeRsaKey(&k1024, 1024, 65537, rng) != 0) { + WB_NOTE("1024-bit keygen refused; PSS salt-length vectors skipped"); + wb_fail = 1; + goto out; + } + + /* (T,T): 1024-bit modulus, SHA-512 digest -> clamped salt length */ + sz = wc_RsaPSS_Sign(dig, WC_SHA512_DIGEST_SIZE, sig, sizeof(sig), + WC_HASH_TYPE_SHA512, WC_MGF1SHA512, &k1024, rng); + if (sz > 0) { + (void)wc_RsaPSS_VerifyCheck(sig, (word32)sz, out, sizeof(out), + dig, WC_SHA512_DIGEST_SIZE, + WC_HASH_TYPE_SHA512, WC_MGF1SHA512, + &k1024); + { /* the Inline form carries its own copy of the same guard */ + byte in2[256]; + byte* p = NULL; + XMEMCPY(in2, sig, (size_t)sz); + (void)wc_RsaPSS_VerifyCheckInline(in2, (word32)sz, &p, + dig, WC_SHA512_DIGEST_SIZE, + WC_HASH_TYPE_SHA512, + WC_MGF1SHA512, &k1024); + } + } + else { + wb_fail = 1; + } + + /* (T,F): same 1024-bit modulus, SHA-256 digest -> salt stays hLen */ + sz = wc_RsaPSS_Sign(dig, WC_SHA256_DIGEST_SIZE, sig, sizeof(sig), + WC_HASH_TYPE_SHA256, WC_MGF1SHA256, &k1024, rng); + if (sz > 0) { + (void)wc_RsaPSS_VerifyCheck(sig, (word32)sz, out, sizeof(out), + dig, WC_SHA256_DIGEST_SIZE, + WC_HASH_TYPE_SHA256, WC_MGF1SHA256, + &k1024); + { + byte in2[256]; + byte* p = NULL; + XMEMCPY(in2, sig, (size_t)sz); + (void)wc_RsaPSS_VerifyCheckInline(in2, (word32)sz, &p, + dig, WC_SHA256_DIGEST_SIZE, + WC_HASH_TYPE_SHA256, + WC_MGF1SHA256, &k1024); + } + } + else { + wb_fail = 1; + } + + /* (F,-): the 2048-bit key already built by main, SHA-512 digest */ + sz = wc_RsaPSS_Sign(dig, WC_SHA512_DIGEST_SIZE, sig, sizeof(sig), + WC_HASH_TYPE_SHA512, WC_MGF1SHA512, key2048, rng); + if (sz > 0) { + (void)wc_RsaPSS_VerifyCheck(sig, (word32)sz, out, sizeof(out), + dig, WC_SHA512_DIGEST_SIZE, + WC_HASH_TYPE_SHA512, WC_MGF1SHA512, + key2048); + { + byte in2[256]; + byte* p = NULL; + XMEMCPY(in2, sig, (size_t)sz); + (void)wc_RsaPSS_VerifyCheckInline(in2, (word32)sz, &p, + dig, WC_SHA512_DIGEST_SIZE, + WC_HASH_TYPE_SHA512, + WC_MGF1SHA512, key2048); + } + } + + WB_NOTE("PSS FIPS 5.5(e) 1024-bit/SHA-512 salt-length vectors done"); + +out: + if (inited) + wc_FreeRsaKey(&k1024); +} +#else +static void wb_pss_saltlen_1024_sha512(RsaKey* key2048, WC_RNG* rng) +{ + (void)key2048; (void)rng; + WB_NOTE("PSS/SHA-512/keygen off or RSA_MIN_SIZE > 1024; 5.5(e) skipped"); +} +#endif + +/* --------------------------------------------------------------------------- + * RsaFunctionPrivate 2946: the five mp_iszero() CRT-component operands + * + * if (ret == 0 && (mp_iszero(&key->p) || mp_iszero(&key->q) || + * mp_iszero(&key->dP) || mp_iszero(&key->dQ) || mp_iszero(&key->u))) + * + * Every key the API can produce carries a complete CRT set, so operands 1..5 + * are all-FALSE forever and the non-CRT fallback exptmod is dead code from the + * public surface. A white-box can build the missing halves directly: five + * scratch keys, each a copy of the good key with exactly ONE component left at + * its post-wc_InitRsaKey zero, so in key i the i-th mp_iszero is the FIRST + * TRUE operand of the chain (every earlier one FALSE) -- the exact + * independence vector for that operand, with the all-FALSE partner supplied by + * every ordinary private op in this same binary. + * + * Scratch keys, never the shared one: the mutation is destructive, and the + * blinding/CRT ops that follow would be wrong for every later case. + * RsaFunctionPrivate is called directly (it is file-static, and the public + * entry points reject a key with a zero p before reaching it). + * ------------------------------------------------------------------------ */ +#if !defined(WOLFSSL_SP_MATH) && !defined(RSA_LOW_MEM) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY) && !defined(WOLFSSL_RSA_VERIFY_ONLY) +static void wb_priv_zero_crt_components(RsaKey* key, WC_RNG* rng) +{ + int i; + + for (i = 0; i < 5; i++) { + RsaKey zk; + mp_int tmp; + + if (wc_InitRsaKey(&zk, NULL) != 0) { wb_fail = 1; continue; } + if ((mp_copy(&key->n, &zk.n) != MP_OKAY) || + (mp_copy(&key->e, &zk.e) != MP_OKAY) || + (mp_copy(&key->d, &zk.d) != MP_OKAY)) { + wc_FreeRsaKey(&zk); + wb_fail = 1; + continue; + } + /* everything but the i-th component; the i-th stays zero */ + if (i != 0) (void)mp_copy(&key->p, &zk.p); + if (i != 1) (void)mp_copy(&key->q, &zk.q); + if (i != 2) (void)mp_copy(&key->dP, &zk.dP); + if (i != 3) (void)mp_copy(&key->dQ, &zk.dQ); + if (i != 4) (void)mp_copy(&key->u, &zk.u); + + if (mp_init(&tmp) == MP_OKAY) { + /* any residue < n; the fallback path is a plain tmp^d mod n */ + (void)mp_set(&tmp, 42); + (void)RsaFunctionPrivate(&tmp, &zk, rng); + mp_forcezero(&tmp); + } + wc_FreeRsaKey(&zk); + } + WB_NOTE("RsaFunctionPrivate zero-p/q/dP/dQ/u vectors done"); +} +#else +static void wb_priv_zero_crt_components(RsaKey* key, WC_RNG* rng) +{ + (void)key; (void)rng; + WB_NOTE("SP_MATH / LOW_MEM / reduced-surface build; zero-CRT vectors n/a"); +} +#endif + +/* --------------------------------------------------------------------------- + * Padding-helper data vectors (no fault injection needed -- these operands are + * simply never produced by a well-formed block or a well-formed argument set, + * and the helpers are file-static or take the deciding value as a plain + * parameter, so only a white-box can supply them). + * + * 2043 idx1 pkcsBlock[1] != RSA_BLOCK_TYPE_1 with pkcsBlock[0] == 0 + * 2056 idx0 separator found before RSA_MIN_PAD_SZ bytes of padding + * 2056 idx1 >= RSA_MIN_PAD_SZ bytes but the byte before the run end != 0 + * 1794 idx0 RsaUnPad_OAEP with an unusable hash type (digest size < 0) + * 1794 idx1 usable hash, but pkcsBlockLen < 2*hLen + 2 + * 1729 idx0/1 wc_RsaPad_ex(WC_RSA_NO_PAD) with bits <= 0 / a length mismatch + * 2148 idx0/1 the same pair on the un-pad side + * 4547 idx0/1 wc_RsaPSS_CheckPadding_ex2's FIPS 186-4 5.5(e) salt reduction: + * `bits` is a plain parameter here, so the 1024-bit half needs + * no 1024-bit key and no RSA_MIN_SIZE override + * 1528 idx1 / 1937 idx1 the same 5.5(e) test inside RsaPad_PSS / + * RsaUnPad_PSS, both file-static and both taking `bits` + * directly: hLen is varied (SHA-512 vs SHA-256) with bits + * pinned at 1024 to flip operand 1 alone. + * + * Every vector's accepting partner is produced by the ordinary sign / verify / + * encrypt / decrypt traffic earlier in this same binary. + * ------------------------------------------------------------------------ */ +static void wb_pad_unpad_vectors(WC_RNG* rng) +{ + byte blk[512]; + byte out[512]; + const byte* cp = NULL; + byte* op = NULL; + + XMEMSET(blk, 0, sizeof(blk)); + XMEMSET(out, 0, sizeof(out)); + +#ifndef WOLFSSL_RSA_VERIFY_ONLY + /* --- RsaUnPad block-type-1 formatting guards ------------------------- */ + blk[0] = 0x00; blk[1] = 0x02; /* 2043: idx0 F, idx1 T */ + (void)RsaUnPad(blk, 64, &cp, RSA_BLOCK_TYPE_1); + + blk[0] = 0x00; blk[1] = 0x01; blk[2] = 0x00; + (void)RsaUnPad(blk, 64, &cp, RSA_BLOCK_TYPE_1); /* 2056: idx0 T */ + + XMEMSET(blk, 0, sizeof(blk)); + blk[0] = 0x00; blk[1] = 0x01; + XMEMSET(blk + 2, 0xFF, 9); /* indices 2..10 */ + blk[11] = 0xAA; /* run ends at i=12, blk[11] != 0 */ + (void)RsaUnPad(blk, 64, &cp, RSA_BLOCK_TYPE_1); /* 2056: idx0 F, idx1 T */ +#endif + +#if !defined(WC_NO_RSA_OAEP) && !defined(WOLFSSL_RSA_VERIFY_ONLY) + /* --- RsaUnPad_OAEP digest-size / block-length guard ------------------- */ + XMEMSET(blk, 0, sizeof(blk)); + /* idx0 T: wc_HashGetDigestSize(WC_HASH_TYPE_NONE) is negative */ + (void)RsaUnPad_OAEP(blk, 64, &op, WC_HASH_TYPE_NONE, WC_MGF1SHA256, + NULL, 0, NULL); + /* idx0 F, idx1 T: SHA-256 needs at least 2*32+2 = 66 bytes */ + (void)RsaUnPad_OAEP(blk, 8, &op, WC_HASH_TYPE_SHA256, WC_MGF1SHA256, + NULL, 0, NULL); +#endif + +#ifdef WC_RSA_NO_PADDING + /* --- the no-padding exact-length guards, pad and un-pad side ---------- */ + XMEMSET(blk, 0x5a, sizeof(blk)); + /* 1729 idx0 T (bits <= 0) */ + (void)wc_RsaPad_ex(blk, 256, out, sizeof(out), RSA_BLOCK_TYPE_2, rng, + WC_RSA_NO_PAD, WC_HASH_TYPE_SHA256, WC_MGF1SHA256, + NULL, 0, 0, 0, NULL); + /* 1729 idx0 F, idx1 T (2048 bits wants exactly 256 input bytes) */ + (void)wc_RsaPad_ex(blk, 128, out, sizeof(out), RSA_BLOCK_TYPE_2, rng, + WC_RSA_NO_PAD, WC_HASH_TYPE_SHA256, WC_MGF1SHA256, + NULL, 0, 0, 2048, NULL); + /* 1729 all-false partner */ + (void)wc_RsaPad_ex(blk, 256, out, sizeof(out), RSA_BLOCK_TYPE_2, rng, + WC_RSA_NO_PAD, WC_HASH_TYPE_SHA256, WC_MGF1SHA256, + NULL, 0, 0, 2048, NULL); + + op = NULL; + /* 2148 idx0 T */ + (void)wc_RsaUnPad_ex(blk, 256, &op, RSA_BLOCK_TYPE_2, WC_RSA_NO_PAD, + WC_HASH_TYPE_SHA256, WC_MGF1SHA256, NULL, 0, 0, 0, + NULL); + /* 2148 idx0 F, idx1 T */ + (void)wc_RsaUnPad_ex(blk, 128, &op, RSA_BLOCK_TYPE_2, WC_RSA_NO_PAD, + WC_HASH_TYPE_SHA256, WC_MGF1SHA256, NULL, 0, 0, 2048, + NULL); + /* 2148 all-false partner */ + (void)wc_RsaUnPad_ex(blk, 256, &op, RSA_BLOCK_TYPE_2, WC_RSA_NO_PAD, + WC_HASH_TYPE_SHA256, WC_MGF1SHA256, NULL, 0, 0, 2048, + NULL); +#endif /* WC_RSA_NO_PADDING */ + +#if defined(WC_RSA_PSS) && defined(WOLFSSL_SHA512) + { + byte in512[WC_SHA512_DIGEST_SIZE]; + byte in256[WC_SHA256_DIGEST_SIZE]; + byte sig[WC_SHA512_DIGEST_SIZE * 2]; + + XMEMSET(in512, 0x5a, sizeof(in512)); + XMEMSET(in256, 0x5a, sizeof(in256)); + XMEMSET(sig, 0xa5, sizeof(sig)); + + /* 4547 (T,T): bits == 1024 AND inSz == SHA-512 digest size. */ + (void)wc_RsaPSS_CheckPadding_ex2(in512, sizeof(in512), sig, + (word32)(RSA_PSS_SALT_MAX_SZ + (int)sizeof(in512)), + WC_HASH_TYPE_SHA512, RSA_PSS_SALT_LEN_DEFAULT, 1024, NULL); + /* 4547 (F,-): same call at 2048 bits. */ + (void)wc_RsaPSS_CheckPadding_ex2(in512, sizeof(in512), sig, + (word32)(2 * sizeof(in512)), WC_HASH_TYPE_SHA512, + RSA_PSS_SALT_LEN_DEFAULT, 2048, NULL); + /* 4547 (T,F): 1024 bits but a SHA-256 digest. */ + (void)wc_RsaPSS_CheckPadding_ex2(in256, sizeof(in256), sig, + (word32)(2 * sizeof(in256)), WC_HASH_TYPE_SHA256, + RSA_PSS_SALT_LEN_DEFAULT, 1024, NULL); + +#ifndef WC_NO_RNG + /* 1528 idx1: RsaPad_PSS's own copy of the 5.5(e) test. bits pinned at + * 1024 for both calls so ONLY hLen differs -- that is the operand-1 + * independence pair. The block never has to verify; the salt-length + * selection happens before any of that. */ + (void)RsaPad_PSS(in512, sizeof(in512), out, 128, rng, + WC_HASH_TYPE_SHA512, WC_MGF1SHA512, + RSA_PSS_SALT_LEN_DEFAULT, 1024, NULL); + (void)RsaPad_PSS(in256, sizeof(in256), out, 128, rng, + WC_HASH_TYPE_SHA256, WC_MGF1SHA256, + RSA_PSS_SALT_LEN_DEFAULT, 1024, NULL); +#endif + /* 1937 idx1: the same test inside RsaUnPad_PSS. */ + XMEMSET(blk, 0xbc, 128); + op = NULL; + (void)RsaUnPad_PSS(blk, 128, &op, WC_HASH_TYPE_SHA512, WC_MGF1SHA512, + RSA_PSS_SALT_LEN_DEFAULT, 1024, NULL); + op = NULL; + (void)RsaUnPad_PSS(blk, 128, &op, WC_HASH_TYPE_SHA256, WC_MGF1SHA256, + RSA_PSS_SALT_LEN_DEFAULT, 1024, NULL); + } +#endif /* WC_RSA_PSS && WOLFSSL_SHA512 */ + + (void)rng; (void)cp; (void)op; + WB_NOTE("pad/un-pad formatting + FIPS 5.5(e) salt vectors done"); +} + +/* --------------------------------------------------------------------------- + * RsaPublicEncryptEx 3707 idx0: `sz < RSA_MIN_PAD_SZ`. + * + * sz is wc_RsaEncryptSize(key) = the byte length of n, and every key the API + * can build is at least RSA_MIN_SIZE bits, so the guard is dead from outside. + * A scratch key whose n is a single byte makes sz == 1 and drives operand 0 + * TRUE; the FALSE-FALSE partner is every real encrypt in this binary. (The + * upper operand `sz > RSA_MAX_SIZE/8` is NOT attempted: n would have to exceed + * the math backend's own maximum bit width, so mp_read cannot build one -- + * recorded as a residual rather than a gap.) + * ------------------------------------------------------------------------ */ +static void wb_encryptsize_lower_bound(WC_RNG* rng) +{ +#ifndef WOLFSSL_RSA_VERIFY_ONLY + RsaKey sk; + byte in[4]; + byte out[64]; + + XMEMSET(in, 0x11, sizeof(in)); + XMEMSET(out, 0, sizeof(out)); + + if (wc_InitRsaKey(&sk, NULL) != 0) { wb_fail = 1; return; } + if ((mp_set(&sk.n, 0x0b) == MP_OKAY) && (mp_set(&sk.e, 3) == MP_OKAY)) { + sk.type = RSA_PUBLIC; + (void)wc_RsaPublicEncrypt(in, 1, out, sizeof(out), &sk, rng); + } + else { + wb_fail = 1; + } + wc_FreeRsaKey(&sk); + WB_NOTE("RsaPublicEncryptEx sz < RSA_MIN_PAD_SZ vector done"); +#else + (void)rng; +#endif +} + +/* --------------------------------------------------------------------------- + * wc_RsaFunction 3593 idx1 / 3604 idx1: the bounds-check dispatch + * + * if (type == RSA_PRIVATE_DECRYPT && key->state == RSA_STATE_DECRYPT_EXPTMOD) + * if (type == RSA_PUBLIC_DECRYPT && key->state == RSA_STATE_DECRYPT_EXPTMOD) + * + * Reached from wc_RsaPrivateDecrypt / wc_RsaSSL_Verify the state operand is + * always TRUE when the type operand is, so operand 1's FALSE half needs a + * direct call with the key parked in another state -- which is exactly what a + * caller driving wc_RsaFunction itself (the documented public entry point) + * does. Uses a scratch key so the shared key's state machine is untouched. + * ------------------------------------------------------------------------ */ +static void wb_rsafunction_state_operand(RsaKey* key, WC_RNG* rng, + const byte* ct, int ctLen) +{ +#if !defined(WOLFSSL_RSA_VERIFY_ONLY) && !defined(TEST_UNPAD_CONSTANT_TIME) && \ + !defined(NO_RSA_BOUNDS_CHECK) && !defined(WOLF_CRYPTO_CB_ONLY_RSA) + byte out[WB_RSA_BYTES]; + word32 outLen = sizeof(out); + + if (ctLen <= 0) + return; + XMEMSET(out, 0, sizeof(out)); + + key->state = RSA_STATE_NONE; + (void)wc_RsaFunction(ct, (word32)ctLen, out, &outLen, RSA_PRIVATE_DECRYPT, + key, rng); + key->state = RSA_STATE_NONE; + outLen = sizeof(out); + (void)wc_RsaFunction(ct, (word32)ctLen, out, &outLen, RSA_PUBLIC_DECRYPT, + key, rng); + key->state = RSA_STATE_NONE; + WB_NOTE("wc_RsaFunction type/state dispatch operand-1 vectors done"); +#else + (void)key; (void)rng; (void)ct; (void)ctLen; +#endif +} + int main(int argc, char** argv) { int do_baseline = (argc > 1 && strcmp(argv[1], "baseline") == 0); @@ -342,6 +788,7 @@ int main(int argc, char** argv) byte der[WB_RSA_BYTES * 4]; int ctLen = 0, derLen = 0; int n, rep, ret; + time_t heap_t0 = 0; printf("rsa.c fault white-box (%s)\n", do_baseline ? "baseline" : (do_probe ? "probe" : "sweep")); @@ -402,6 +849,16 @@ int main(int argc, char** argv) * wall-clock limit, and anything queued behind it would be lost with the * whole run. */ wb_pss_checkpadding_sigcheck(); + wb_pss_saltlen_1024_sha512(&key, &rng); + + /* Data-only white-box vectors: no injector, microseconds each, and they + * must not sit behind the heap sweeps (whose RSA_LOW_MEM instance can hit + * the harness wall-clock limit and take the whole run's profile with it). */ + wb_pad_unpad_vectors(&rng); + wb_encryptsize_lower_bound(&rng); + wb_priv_zero_crt_components(&key, &rng); + if (ctLen > 0) + wb_rsafunction_state_operand(&key, &rng, ct, ctLen); #ifndef MCDC_FA_UNAVAILABLE if (do_probe) { @@ -440,10 +897,150 @@ int main(int argc, char** argv) #endif if (do_sweep) { + /* ---- big-integer fault sweeps (mcdc_fault_mp.h) ------------------- + * FIRST, deliberately: these are the cheapest sweeps in the file (an + * armed mp_* call aborts its entry point within a handful of big-int + * operations) and they carry the largest single block of residuals in + * rsa.c, so they must not be queued behind the multi-thousand-index + * heap sweeps below -- under RSA_LOW_MEM those can reach the harness + * wall-clock limit, and a timed-out white-box contributes nothing. + * + * Each target is first run DISARMED: that supplies the all-TRUE + * (ret==0, every mp op OK) row of every guard in the chain IN THIS + * BINARY -- the accepting half without which the rejecting vectors + * below prove no independence pair -- and measures the sweep length K. + * Then the fail index is swept over [1..K]: index n drives operand 1 + * TRUE at the n-th call site and operand 0 FALSE at every guard + * downstream of it. Inputs are rebuilt (or are read-only) while + * disarmed, so every armed call starts from the same known-good + * state. */ + { + time_t t0 = time(NULL); + long k, i; + +#define WB_MP_MAX 600 +#define WB_MP_DEADLINE 150 +#define WB_MP_EXPIRED() (difftime(time(NULL), t0) > (double)WB_MP_DEADLINE) +#define WB_MP_SWEEP(lbl, ...) \ + do { \ + mcdc_fm_disarm(); \ + { __VA_ARGS__; } \ + k = mcdc_fm_seen(); \ + if (k > WB_MP_MAX) \ + k = WB_MP_MAX; \ + for (i = 1; (i <= k) && !WB_MP_EXPIRED(); i++) { \ + mcdc_fm_arm(i); \ + { __VA_ARGS__; } \ + mcdc_fm_disarm(); \ + } \ + printf(" [wb] mp sweep %s: K=%ld\n", (lbl), k); \ + } while (0) + + if (WANT("mp")) { + byte o[WB_RSA_BYTES]; + + /* RsaFunctionSync public path: tmp read-in + exptmod. */ + XMEMSET(o, 0, sizeof(o)); + WB_MP_SWEEP("RsaPublicEncrypt", + (void)wc_RsaPublicEncrypt(msg, sizeof(msg), o, sizeof(o), + &key, &rng)); + + /* RsaFunctionPrivate: the blinding invmod/exptmod/mulmod + * chain, the CRT dP/dQ/u chain and the montgomery + * blinding-invert tail -- the single largest residual block + * in the file. */ + WB_MP_SWEEP("RsaSSL_Sign", + { byte s2[WB_RSA_BYTES]; + XMEMSET(s2, 0, sizeof(s2)); + (void)wc_RsaSSL_Sign(msg, sizeof(msg), s2, sizeof(s2), + &key, &rng); }); + + if (ctLen > 0) { + WB_MP_SWEEP("RsaPrivateDecrypt", + { byte d2[WB_RSA_BYTES]; + XMEMSET(d2, 0, sizeof(d2)); + (void)wc_RsaPrivateDecrypt(ct, (word32)ctLen, d2, + sizeof(d2), &key); }); + } + + WB_MP_SWEEP("RsaSSL_Verify", + { byte v2[WB_RSA_BYTES]; + XMEMSET(v2, 0, sizeof(v2)); + (void)wc_RsaSSL_Verify(sig, sizeof(sig), v2, sizeof(v2), + &key); }); + +#ifdef WOLFSSL_RSA_KEY_CHECK + WB_MP_SWEEP("CheckRsaKey", (void)wc_CheckRsaKey(&key)); +#endif +#ifdef WOLFSSL_KEY_TO_DER + WB_MP_SWEEP("RsaKeyToDer", + (void)wc_RsaKeyToDer(&key, der, sizeof(der))); +#endif + if (derLen > 0) { + WB_MP_SWEEP("RsaPrivateKeyDecode", + { RsaKey dk; word32 idx = 0; + if (wc_InitRsaKey(&dk, NULL) == 0) { + (void)wc_RsaPrivateKeyDecode(der, &idx, &dk, + (word32)derLen); + wc_FreeRsaKey(&dk); + } }); + } + mcdc_fm_disarm(); + } + + /* Prime-search err == MP_OKAY chains. Deliberately shallow: an + * armed wc_MakeRsaKey aborts as soon as the injected failure is + * reached, so a LOW fail index costs only the few prime + * candidates evaluated before it, while a high one would pay for + * a full 1024-bit prime search per iteration. WB_MAKEKEY_K is + * therefore small on purpose -- this white-box already carries + * the multi-thousand-index heap sweeps below, and under + * RSA_LOW_MEM the binary as a whole runs close to the harness + * wall-clock limit (a timed-out run yields NO profile at all, so + * "shallow but finished" strictly beats "deep but killed"). */ +#define WB_MAKEKEY_K 12 + if (WANT("mpkeygen")) { +#if defined(WOLFSSL_KEY_GEN) && !defined(WOLFSSL_RSA_PUBLIC_ONLY) + int isPrime = 0; + + /* 5249 idx1: |p-q| below the FIPS 186 bound with + * wc_CompareDiffPQ itself succeeding -- q == p makes the + * difference zero. The sweep right after supplies idx0 by + * failing that same call. Real 1024-bit primes (the key built + * at the top of main) so the whole function body runs, not + * just its lower-bound rejection. */ + (void)_CheckProbablePrime(&key.p, &key.p, &key.e, + WB_RSA_BITS, &isPrime, &rng); + WB_MP_SWEEP("CheckProbablePrime(q)", + { int ip = 0; + (void)_CheckProbablePrime(&key.p, &key.q, &key.e, + WB_RSA_BITS, &ip, &rng); }); + WB_MP_SWEEP("CheckProbablePrime(p)", + { int ip = 0; + (void)_CheckProbablePrime(&key.p, NULL, &key.e, + WB_RSA_BITS, &ip, &rng); }); + + for (i = 1; (i <= WB_MAKEKEY_K) && !WB_MP_EXPIRED(); i++) { + RsaKey mk; + if (wc_InitRsaKey(&mk, NULL) != 0) { wb_fail = 1; continue; } + mcdc_fm_arm(i); + (void)wc_MakeRsaKey(&mk, WB_RSA_BITS, 65537, &rng); + mcdc_fm_disarm(); + wc_FreeRsaKey(&mk); + } + printf(" [wb] mp sweep MakeRsaKey: K=%d\n", WB_MAKEKEY_K); +#endif + mcdc_fm_disarm(); + } + mcdc_fm_disarm(); + WB_NOTE("big-integer fault sweeps done"); + } + /* --- wc_RsaPublicEncrypt: RsaFunctionSync public path -- tmp NEW/INIT, * mp_read_unsigned_bin (line 3075), mp_exptmod_nct. Faulting the n-th * alloc drives the tmp NULL/INIT-fail and the ret==0 && mp_*!=MP_OKAY * halves at 3075 and in RsaFunctionCheckIn (3499). --- */ + heap_t0 = time(NULL); if (WANT("pub")) for (n = 1; n <= WB_SWEEP_K; n++) { byte o[WB_RSA_BYTES]; @@ -460,9 +1057,19 @@ int main(int argc, char** argv) * RNG value each call so the deeper mp scratch alloc counts drift; * repeat the sweep so the union reaches every op despite the drift. * The key is reused (private ops do not mutate it); output is fresh. --- */ + /* Wall-clock guard. The harness kills a white-box at TEST_TIMEOUT and + * a killed run yields NO profile at all, so every vector before the + * kill is lost too -- "shallow but finished" strictly beats "deep but + * killed". RSA_LOW_MEM (non-CRT: one full-width private exptmod per + * iteration instead of two half-width ones) is several times slower + * per vector than the CRT variants, which is exactly the build that + * ran into the limit. The deadline is checked between vectors, so it + * truncates the sweep instead of aborting it. */ +#define WB_HEAP_DEADLINE 330 +#define WB_HEAP_EXPIRED() (difftime(time(NULL), heap_t0) > (double)WB_HEAP_DEADLINE) if (WANT("priv")) for (rep = 0; rep < WB_PRIV_REP; rep++) { - for (n = 1; n <= WB_PRIV_K; n++) { + for (n = 1; n <= WB_PRIV_K && !WB_HEAP_EXPIRED(); n++) { byte s2[WB_RSA_BYTES]; XMEMSET(s2, 0, sizeof(s2)); mcdc_fa_arm(n); @@ -470,7 +1077,7 @@ int main(int argc, char** argv) mcdc_fa_disarm(); } if (ctLen > 0) { - for (n = 1; n <= WB_PRIV_K; n++) { + for (n = 1; n <= WB_PRIV_K && !WB_HEAP_EXPIRED(); n++) { byte d2[WB_RSA_BYTES]; XMEMSET(d2, 0, sizeof(d2)); mcdc_fa_arm(n); @@ -541,6 +1148,7 @@ int main(int argc, char** argv) mcdc_fa_disarm(); mcdc_fa_restore(); + mcdc_fm_disarm(); wc_FreeRsaKey(&key); wc_FreeRng(&rng); From e3d9123af6db609a649ed031938cee71a895619c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 20:43:29 +0200 Subject: [PATCH 19/20] tests: call the SP entry points directly under the fault sweep --- tests/unit-mcdc/test_sp_fault_common.h | 179 +++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/tests/unit-mcdc/test_sp_fault_common.h b/tests/unit-mcdc/test_sp_fault_common.h index 809d19d8466..57f9739bae1 100644 --- a/tests/unit-mcdc/test_sp_fault_common.h +++ b/tests/unit-mcdc/test_sp_fault_common.h @@ -179,6 +179,179 @@ static void wb_fault_ecc(int curveId, int fieldSz) } #endif +#if defined(WOLFSSL_HAVE_SP_ECC) && defined(HAVE_ECC) && \ + !defined(MCDC_FA_UNAVAILABLE) +/* -------------------------------------------------------------------- * + * The multi-condition `err` chains this module still owes are NOT in the + * wc_* API's call graph. + * + * Shapes like + * + * if ((err == MP_OKAY) && (!inMont)) { ... } + * if ((err == MP_OKAY) && sp__iszero_(p1->z)) { ... } + * if ((err == MP_OKAY) && ((sp__cmp_(p->x, pub->x) != 0) || ...)) + * + * live in sp_ecc_mulmod_add_, sp_ecc_mulmod_base_add_, + * sp_ecc_check_key_, sp_ecc_verify_ and sp_ecc_sign_. Some of + * those entry points nothing in wc_* reaches at all on this + * configuration; the ones it does reach it reaches through wrappers that + * allocate first, so a sweep aimed at the wrapper lands its failure + * before the callee is entered. + * + * So each entry point is called DIRECTLY, with its own arming hugging + * the one call and every input built while disarmed. The depth only has + * to walk a couple of SP_ALLOC_VARs past the start of each function -- + * the injector fails allocation n and every later one, so a shallow + * sweep already puts `err` on the wrong side of every checkpoint in the + * function. + * -------------------------------------------------------------------- */ +#ifndef SP_FAULT_DIRECT_N + #define SP_FAULT_DIRECT_N 6 +#endif + +#define SP_FAULT_DEFINE_DIRECT(BITS, SZ, CURVE_ID) \ +static void wb_fault_direct_##BITS(void) \ +{ \ + ecc_key key; \ + WC_RNG rng; \ + ecc_point* rp = NULL; \ + mp_int k; \ + mp_int one; \ + mp_int rmv; \ + mp_int smv; \ + mp_int rmv2; \ + mp_int smv2; \ + byte digest[32]; \ + int res = 0; \ + int n; \ + int haveSig = 0; \ + \ + XMEMSET(&key, 0, sizeof(key)); \ + XMEMSET(&rng, 0, sizeof(rng)); \ + XMEMSET(digest, 0x5a, sizeof(digest)); \ + \ + if (wc_InitRng(&rng) != 0) { \ + return; \ + } \ + if (wc_ecc_init(&key) != 0) { \ + wc_FreeRng(&rng); \ + return; \ + } \ + if (mp_init_multi(&k, &one, &rmv, &smv, &rmv2, &smv2) != MP_OKAY) { \ + wc_ecc_free(&key); \ + wc_FreeRng(&rng); \ + return; \ + } \ + (void)mp_set(&k, 5); \ + (void)mp_set(&one, 1); \ + rp = wc_ecc_new_point(); \ + \ + if ((rp != NULL) && \ + (wc_ecc_make_key_ex(&rng, SZ, &key, CURVE_ID) == 0)) { \ + SP_FAULT_SIGN_SETUP(BITS) \ + for (n = 1; n <= SP_FAULT_DIRECT_N; n++) { \ + mcdc_fa_arm(n); \ + (void)sp_ecc_mulmod_add_##BITS(&k, &key.pubkey, &key.pubkey, \ + 0, rp, 1, NULL); \ + mcdc_fa_disarm(); \ + \ + mcdc_fa_arm(n); \ + (void)sp_ecc_mulmod_base_add_##BITS(&k, &key.pubkey, 0, rp, 1, \ + NULL); \ + mcdc_fa_disarm(); \ + \ + mcdc_fa_arm(n); \ + (void)sp_ecc_mulmod_##BITS(&k, &key.pubkey, rp, 1, NULL); \ + mcdc_fa_disarm(); \ + \ + mcdc_fa_arm(n); \ + (void)sp_ecc_mulmod_base_##BITS(&k, rp, 1, NULL); \ + mcdc_fa_disarm(); \ + \ + mcdc_fa_arm(n); \ + (void)sp_ecc_is_point_##BITS(key.pubkey.x, key.pubkey.y); \ + mcdc_fa_disarm(); \ + \ + SP_FAULT_CHECK_KEY_ARM(BITS) \ + SP_FAULT_SIGNVFY_ARM(BITS) \ + } \ + } \ + \ + if (rp != NULL) { \ + wc_ecc_del_point(rp); \ + } \ + mp_clear(&smv2); \ + mp_clear(&rmv2); \ + mp_clear(&smv); \ + mp_clear(&rmv); \ + mp_clear(&one); \ + mp_clear(&k); \ + wc_ecc_free(&key); \ + wc_FreeRng(&rng); \ + (void)res; \ + (void)haveSig; \ +} + +#if defined(HAVE_ECC_CHECK_KEY) || !defined(NO_ECC_CHECK_PUBKEY_ORDER) +#define SP_FAULT_CHECK_KEY_ARM(BITS) \ + mcdc_fa_arm(n); \ + (void)sp_ecc_check_key_##BITS(key.pubkey.x, key.pubkey.y, \ + ecc_get_k(&key), NULL); \ + mcdc_fa_disarm(); +#else +#define SP_FAULT_CHECK_KEY_ARM(BITS) /* not compiled in this config */ +#endif + +#if defined(HAVE_ECC_SIGN) && defined(HAVE_ECC_VERIFY) +/* A real signature, made disarmed, so the armed verify below starts from + * valid inputs and the failure lands inside the verify. */ +#define SP_FAULT_SIGN_SETUP(BITS) \ + haveSig = (sp_ecc_sign_##BITS(digest, 32, &rng, ecc_get_k(&key), \ + &rmv, &smv, NULL, NULL) == 0); +#define SP_FAULT_SIGNVFY_ARM(BITS) \ + mcdc_fa_arm(n); \ + (void)sp_ecc_sign_##BITS(digest, 32, &rng, ecc_get_k(&key), \ + &rmv2, &smv2, NULL, NULL); \ + mcdc_fa_disarm(); \ + if (haveSig) { \ + mcdc_fa_arm(n); \ + (void)sp_ecc_verify_##BITS(digest, 32, key.pubkey.x, \ + key.pubkey.y, &one, &rmv, &smv, &res, NULL); \ + mcdc_fa_disarm(); \ + } +#else +#define SP_FAULT_SIGN_SETUP(BITS) /* needs HAVE_ECC_SIGN/VERIFY */ +#define SP_FAULT_SIGNVFY_ARM(BITS) /* needs HAVE_ECC_SIGN/VERIFY */ +#endif + +#ifndef WOLFSSL_SP_NO_256 +SP_FAULT_DEFINE_DIRECT(256, 32, ECC_SECP256R1) +#endif +#ifdef WOLFSSL_SP_384 +SP_FAULT_DEFINE_DIRECT(384, 48, ECC_SECP384R1) +#endif +#ifdef WOLFSSL_SP_521 +SP_FAULT_DEFINE_DIRECT(521, 66, ECC_SECP521R1) +#endif + +static void wb_fault_direct_all(void) +{ +#ifndef WOLFSSL_SP_NO_256 + wb_fault_direct_256(); +#endif +#ifdef WOLFSSL_SP_384 + wb_fault_direct_384(); +#endif +#ifdef WOLFSSL_SP_521 + wb_fault_direct_521(); +#endif +} +#else +static void wb_fault_direct_all(void) +{ +} +#endif /* WOLFSSL_HAVE_SP_ECC && HAVE_ECC && !MCDC_FA_UNAVAILABLE */ + #if defined(WOLFSSL_HAVE_SP_DH) && !defined(NO_DH) && \ !defined(MCDC_FA_UNAVAILABLE) /* DH: key agreement over a compiled-in FFDHE group. */ @@ -307,6 +480,12 @@ int main(void) wb_fault_dh(); wb_fault_rsa(); + /* Direct SP entry points, one arming per call: the multi-condition + * `err` chains this module still owes are all behind entry points the + * wc_* API either never takes or only reaches through a wrapper that + * allocates first. */ + wb_fault_direct_all(); + mcdc_fa_disarm(); mcdc_fa_restore(); #endif From 3c4ac2450bd59171d90503aac77ba35992b6dc0d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 10 Aug 2026 21:28:10 +0200 Subject: [PATCH 20/20] tests: drive the falcon sampler, NTRU solver and key-check guards --- tests/unit-mcdc/test_falcon_whitebox.c | 681 ++++++++++++++++++++++++- 1 file changed, 662 insertions(+), 19 deletions(-) diff --git a/tests/unit-mcdc/test_falcon_whitebox.c b/tests/unit-mcdc/test_falcon_whitebox.c index 839d4551ee5..a574d8a7b3f 100644 --- a/tests/unit-mcdc/test_falcon_whitebox.c +++ b/tests/unit-mcdc/test_falcon_whitebox.c @@ -692,32 +692,651 @@ static void wb_native_roundtrip(WC_RNG* rng) } #endif /* !WOLFSSL_FALCON_VERIFY_ONLY */ +/* ================================================================== * + * Gap-close pass: decisions whose remaining half is only reachable by + * calling a file-static helper DIRECTLY with crafted operands. Every + * driver below is bounded (no unbounded retry loop is ever entered with + * a rejecting sampler unless the restart bound itself is tiny) and + * crash-safe: the operands are mathematically invalid but structurally + * well-formed, and every buffer handed in is correctly sized. + * ================================================================== */ + +#ifndef WOLFSSL_FALCON_VERIFY_ONLY + +/* ------------------------------------------------------------------ * + * falcon_berexp: do { i -= 8; w = prng_u8 - ((z >> i) & 0xFF); } + * while ((w == 0) && (i > 0)); + * The (i > 0) operand is only evaluated when w == 0, i.e. when the fresh + * random byte happens to equal the corresponding byte of z -- probability + * 1/256 per iteration from a live PRNG, and 2^-64 for the final (i == 0) + * iteration that yields the operand's FALSE half. + * + * falcon_prng is a plain SHAKE256 squeeze buffer, so the byte stream berexp + * consumes can be planted: recompute z exactly as berexp does (the same + * public helpers are in scope), then stage the eight big-endian bytes of z in + * p.buf. Every iteration then sees w == 0, so the loop walks i = 56, 48, ... + * (cond1 TRUE) down to i == 0 (cond1 FALSE) -- both halves of the (i > 0) + * operand in a single, fully deterministic call that consumes exactly the + * eight staged bytes and never refills. + * ------------------------------------------------------------------ */ +static void wb_berexp_loop(WC_RNG* rng) +{ + falcon_prng p; + fpr x, r, ccs; + word64 z; + word32 sw; + int s, i; + + XMEMSET(&p, 0, sizeof(p)); + if (falcon_prng_init(&p, rng) != 0) { + WB_NOTE("berexp: prng_init failed; loop-operand vector skipped"); + return; + } + + /* Any x >= 0 (berexp's documented precondition) and any ccs in (0, 1). + * ccs must stay strictly below 1: fpr_expm_p63 scales it by 2^63 and + * truncates to a signed 64-bit integer, so ccs == 1 would sit exactly on + * the overflow edge. */ + x = fpr_of(3); + ccs = fpr_onehalf; + + /* Mirror of berexp's own reduction, verbatim, so z matches bit for bit. */ + s = (int)fpr_trunc(fpr_mul(x, falcon_fpr_inv_log2)); + r = fpr_sub(x, fpr_mul(fpr_of((sword64)s), falcon_fpr_log2)); + sw = (word32)s; + sw ^= (sw ^ 63U) & (word32)(0U - ((63U - sw) >> 31)); + s = (int)sw; + z = ((fpr_expm_p63(r, ccs) << 1) - 1) >> s; + + /* Stage the eight comparison bytes so every iteration sees w == 0. */ + for (i = 0; i < 8; i++) { + p.buf[i] = (byte)((z >> (56 - 8 * i)) & 0xFFU); + } + p.ptr = 0; + p.len = 8; + + (void)falcon_berexp(&p, x, ccs); + + wc_Shake256_Free(&p.shake); + ForceZero(&p, sizeof(p)); + WB_OK("falcon_berexp (w==0)&&(i>0) loop operand pair exercised"); +} + +/* ------------------------------------------------------------------ * + * poly_small_mkgauss: if (s < -127 || s > 127) continue; + * mkgauss() sums 2^(10-logn) table samples, so the standard deviation is + * 1.17*sqrt(q/(2n)): ~2.87 at logn = 10 (rejection never observed) but ~64.9 + * at logn = 1, where |s| > 127 is only ~1.96 sigma and fires for roughly 5% of + * the draws -- with both signs equally likely. Driving the helper directly at + * logn = 1 therefore shows both operands' TRUE half (and the all-FALSE half) + * within a few hundred coefficients, with no unbounded loop: every rejection + * is followed by a fresh in-range draw. + * ------------------------------------------------------------------ */ +static void wb_mkgauss_range(WC_RNG* rng) +{ + falcon_rng rc; + sword8 f[2]; /* logn = 1 -> n = 2 */ + int i; + + XMEMSET(&rc, 0, sizeof(rc)); + if (falcon_rng_init(&rc, rng, NULL) != 0) { + WB_NOTE("mkgauss: falcon_rng_init failed; range vectors skipped"); + return; + } + for (i = 0; i < 512; i++) { + poly_small_mkgauss(&rc, f, 1); + if (rc.err != 0) { + WB_NOTE("mkgauss: PRNG squeeze failed mid-run"); + break; + } + } + falcon_rng_free(&rc); + WB_OK("poly_small_mkgauss s<-127 / s>127 operand pair exercised"); +} + +/* ------------------------------------------------------------------ * + * falcon_native_check_key: if (ft[i] == 0 || barrett(h[i]*ft[i]) != gt[i]) + * cond0's TRUE half needs a private key whose f is NOT invertible mod q, i.e. + * with a zero slot in NTT(f). keygen never emits one (it restarts instead), + * but the check runs on a DECODED key blob, so the whole operand pair can be + * built by hand at Falcon-512. NTT is linear and the polynomials below are + * constants, so every slot value is known exactly: + * f = 0, g = 0, h = 0 -> ft[i] = 0 (cond0 TRUE) + * f = 1, g = 0, h = 0 -> ft[i] = 1, h[i]*ft[i] = 0 = gt[i] + * (cond0/cond1 FALSE) + * f = 1, g = 0, h = 1 -> ft[i] = 1, h[i]*ft[i] = 1 != gt[i] = 0 + * (cond0 FALSE, cond1 + * TRUE) + * Every buffer is a real, correctly sized key array and the routine only ever + * decodes and compares. + * ------------------------------------------------------------------ */ +static int wb_check_key_case(falcon_key* key, sword8* poly, word16* h, + sword8 f0, word16 h0) +{ + const unsigned logn = 9; /* FALCON_LEVEL1 -> n = 512 */ + const size_t n = (size_t)1 << 9; + + XMEMSET(key, 0, sizeof(*key)); + XMEMSET(poly, 0, 3 * n); /* f = g = F = 0 */ + XMEMSET(h, 0, n * sizeof(word16)); + poly[0] = f0; /* constant term of f */ + h[0] = h0; /* constant term of h */ + + key->level = FALCON_LEVEL1; + key->heap = NULL; + if (falcon_privkey_encode(key->k, FALCON_LEVEL1_KEY_SIZE, poly, poly + n, + poly + 2 * n, logn) != FALCON_LEVEL1_KEY_SIZE) { + WB_NOTE("check_key: privkey_encode did not fill the blob"); + return 1; + } + key->p[0] = (byte)(FALCON_PUB_HEAD | logn); + if (falcon_modq_encode(key->p + 1, FALCON_LEVEL1_PUB_KEY_SIZE - 1, h, + logn) == 0) { + WB_NOTE("check_key: modq_encode failed"); + return 1; + } + return falcon_native_check_key(key); +} + +static void wb_check_key_ntt_slots(void) +{ + falcon_key* key; + sword8* poly; + word16* h; + const size_t n = (size_t)1 << 9; + + key = (falcon_key*)XMALLOC(sizeof(*key), NULL, DYNAMIC_TYPE_TMP_BUFFER); + poly = (sword8*)XMALLOC(3 * n, NULL, DYNAMIC_TYPE_TMP_BUFFER); + h = (word16*)XMALLOC(n * sizeof(word16), NULL, DYNAMIC_TYPE_TMP_BUFFER); + if ((key == NULL) || (poly == NULL) || (h == NULL)) { + WB_NOTE("check_key: allocation failed; NTT-slot vectors skipped"); + } + else { + /* cond0 FALSE, cond1 FALSE: consistent (all-zero) relation. */ + if (wb_check_key_case(key, poly, h, 1, 0) != 0) { + WB_NOTE("check_key(f=1,h=0) expected acceptance"); + } + /* cond0 FALSE, cond1 TRUE: invertible f but h*f != g. */ + if (wb_check_key_case(key, poly, h, 1, 1) + != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + WB_NOTE("check_key(f=1,h=1) expected a public-key mismatch"); + } + /* cond0 TRUE: f == 0 is not invertible, so NTT(f) is zero. */ + if (wb_check_key_case(key, poly, h, 0, 0) + != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + WB_NOTE("check_key(f=0) expected a public-key mismatch"); + } + } + XFREE(h, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(poly, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(key, NULL, DYNAMIC_TYPE_TMP_BUFFER); + WB_OK("falcon_native_check_key ft[i]==0 operand pair exercised"); +} + +/* ------------------------------------------------------------------ * + * solve_NTRU_deepest: + * if (zint_mul_small(Fp, len, q) != 0 || zint_mul_small(Gp, len, q) != 0) + * Both operands' TRUE half needs the q-scaling of a Bezout coefficient to + * carry out of the len-word big integer. Real keygen operands are Gaussian + * with a tiny norm, so the resultants stay far below the CRT modulus and the + * carry never appears. With adversarial (f, g) whose coefficients fill the + * signed 8-bit range, the resultant of a degree-8 polynomial with X^8+1 + * exceeds the 2-word CRT modulus at logn_top = 3, wraps, and the Bezout + * coefficients become essentially uniform below the modulus -- so the carry + * fires for the large majority of candidates. + * + * The classification is done first, on a private scratch buffer, by replaying + * exactly the prologue solve_NTRU_deepest runs (make_fg -> zint_rebuild_CRT -> + * zint_bezout) and then testing the two carries on COPIES; only once a + * candidate is known to produce the wanted pattern is the real helper invoked + * with it. The search is bounded and every step is a pure big-integer + * computation on correctly sized buffers. + * ------------------------------------------------------------------ */ +#define WB_DEEPEST_LOGN 3 +#define WB_DEEPEST_N ((size_t)1 << WB_DEEPEST_LOGN) +#define WB_SOLVE_WORDS 8192 + +static void wb_deepest_candidate(int trial, sword8* f, sword8* g) +{ + size_t i; + unsigned parity; + + for (i = 0; i < WB_DEEPEST_N; i++) { + f[i] = (sword8)((trial * 7 + (int)i * 31 + 11) % 255 - 127); + g[i] = (sword8)((trial * 13 + (int)i * 17 + 5) % 255 - 127); + } + /* zint_bezout requires both resultants odd, i.e. an odd coefficient sum. */ + for (i = 0, parity = 0; i < WB_DEEPEST_N; i++) { + parity ^= (unsigned)(f[i] & 1); + } + if (parity == 0) { + f[0] = (sword8)(f[0] ^ 1); + } + for (i = 0, parity = 0; i < WB_DEEPEST_N; i++) { + parity ^= (unsigned)(g[i] & 1); + } + if (parity == 0) { + g[0] = (sword8)(g[0] ^ 1); + } +} + +/* Returns 1/2/3 for "no carry", "first operand carries", "only the second + * carries", or 0 when zint_bezout rejects the candidate. */ +static int wb_deepest_classify(word32* scratch, const sword8* f, + const sword8* g) +{ + const size_t len = MAX_BL_SMALL[WB_DEEPEST_LOGN]; + word32* Fp = scratch; + word32* Gp = Fp + len; + word32* fp = Gp + len; + word32* gp = fp + len; + word32* t1 = gp + len; + word32 cF, cG; + + make_fg(fp, f, g, WB_DEEPEST_LOGN, WB_DEEPEST_LOGN, 0); + zint_rebuild_CRT(fp, len, len, 2, FALCON_PRIMES, 0, t1); + if (!zint_bezout(Gp, Fp, fp, gp, len, t1)) { + return 0; + } + /* Carry test on copies: zint_mul_small scales in place. */ + XMEMCPY(t1, Fp, len * sizeof(word32)); + cF = zint_mul_small(t1, len, 12289); + XMEMCPY(t1, Gp, len * sizeof(word32)); + cG = zint_mul_small(t1, len, 12289); + if (cF != 0) { + return 2; + } + return (cG != 0) ? 3 : 1; +} + +static void wb_solve_deepest_overflow(void) +{ + word32* scratch; + word32* live; + sword8 f[WB_DEEPEST_N]; + sword8 g[WB_DEEPEST_N]; + int trial, want, got; + int found[4]; + + scratch = (word32*)XMALLOC(WB_SOLVE_WORDS * sizeof(word32), NULL, + DYNAMIC_TYPE_TMP_BUFFER); + live = (word32*)XMALLOC(WB_SOLVE_WORDS * sizeof(word32), NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if ((scratch == NULL) || (live == NULL)) { + WB_NOTE("solve_NTRU_deepest: allocation failed; carry vectors skipped"); + } + else { + found[0] = found[1] = found[2] = found[3] = 0; + /* want 2 = first operand carries (cond0 TRUE), + * want 3 = only the second carries (cond0 FALSE, cond1 TRUE). */ + for (want = 2; want <= 3; want++) { + for (trial = 0; trial < 256; trial++) { + wb_deepest_candidate(trial, f, g); + got = wb_deepest_classify(scratch, f, g); + if (got != want) { + continue; + } + XMEMSET(live, 0, WB_SOLVE_WORDS * sizeof(word32)); + if (solve_NTRU_deepest(WB_DEEPEST_LOGN, f, g, live) != 0) { + WB_NOTE("solve_NTRU_deepest: expected the carry rejection"); + } + found[want] = 1; + break; + } + if (!found[want]) { + WB_NOTE("solve_NTRU_deepest: no candidate for a carry pattern"); + } + } + } + XFREE(live, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(scratch, NULL, DYNAMIC_TYPE_TMP_BUFFER); + WB_OK("solve_NTRU_deepest zint_mul_small carry operand pair exercised"); +} + +/* ------------------------------------------------------------------ * + * solve_NTRU: + * if (!poly_big_to_small(F, tmp, lim, logn) + * || !poly_big_to_small(G, tmp + n, lim, logn)) + * keygen always passes lim = 127 and the solved (F, G) fit, so only the + * all-FALSE half shows. lim is a plain parameter of solve_NTRU, so a direct + * call with a deliberately tight bound rejects on the first or the second + * conversion at will: + * lim = 0 -> F is out of range (cond0 TRUE) + * lim = max|F[i]| -> F fits, G does not (cond0 FALSE, cond1 TRUE) + * The (f, g) pair comes from a real keygen at logn = 5, so the whole solver + * chain runs on legitimate operands and only the final range gate differs; the + * second vector is only issued once a key with max|G| > max|F| has been drawn. + * ------------------------------------------------------------------ */ +static void wb_solve_ntru_lim(WC_RNG* rng) +{ + const unsigned logn = 5; + const size_t n = (size_t)1 << 5; + sword8 f[32], g[32], F[32], G[32], Fout[32], Gout[32]; + word16 h[32]; + byte* tmpbuf; + size_t u; + int tries, maxF = 0, maxG = 0, haveKey = 0; + + tmpbuf = (byte*)XMALLOC(FALCON_KEYGEN_TEMP[logn] + sizeof(fpr), NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (tmpbuf == NULL) { + WB_NOTE("solve_NTRU: allocation failed; lim vectors skipped"); + return; + } + for (tries = 0; tries < 8; tries++) { + if (falcon_keygen(rng, f, g, F, G, h, logn) != 0) { + break; + } + maxF = 0; + maxG = 0; + for (u = 0; u < n; u++) { + int aF = (F[u] < 0) ? -(int)F[u] : (int)F[u]; + int aG = (G[u] < 0) ? -(int)G[u] : (int)G[u]; + if (aF > maxF) { + maxF = aF; + } + if (aG > maxG) { + maxG = aG; + } + } + haveKey = 1; + if (maxG > maxF) { + break; + } + } + if (!haveKey) { + WB_NOTE("solve_NTRU: keygen(logn=5) failed; lim vectors skipped"); + } + else { + /* cond0 TRUE: no coefficient of F can fit in [-0, 0]. */ + if (solve_NTRU(logn, Fout, Gout, f, g, 0, (word32*)tmpbuf) != 0) { + WB_NOTE("solve_NTRU(lim=0) expected the range rejection"); + } + if (maxG > maxF) { + /* cond0 FALSE, cond1 TRUE: F fits exactly, G overflows. */ + if (solve_NTRU(logn, Fout, Gout, f, g, maxF, + (word32*)tmpbuf) != 0) { + WB_NOTE("solve_NTRU(lim=max|F|) expected the G rejection"); + } + } + else { + WB_NOTE("solve_NTRU: no key with max|G| > max|F| in 8 draws"); + } + } + ForceZero(tmpbuf, (word32)(FALCON_KEYGEN_TEMP[logn] + sizeof(fpr))); + XFREE(tmpbuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + WB_OK("solve_NTRU poly_big_to_small lim operand pair exercised"); +} + +/* ------------------------------------------------------------------ * + * solve_NTRU_intermediate Babai clamp: + * if (!fpr_lt(fpr_mtwo31m1, xv) || !fpr_lt(xv, fpr_ptwo31m1)) return 0; + * xv is a reduction coefficient rescaled by 2^dc, where dc comes from the + * BITLENGTH[] heuristic for the expected coefficient size at that depth. The + * heuristic is tuned for the production degrees, so at logn = 9/10 an + * out-of-range xv is astronomically rare -- which is why the baseline only + * ever showed this pair by luck. At logn = 3 the same heuristic is far coarser + * and the clamp fires for roughly a fifth of the drawn (f, g) pairs, in both + * directions, so a bounded batch of small keygens turns a lottery into a + * near-certainty: with p ~ 0.2 per key, 256 keys leave a miss probability + * below 1e-24. Each key is a full but tiny (n = 8) keygen; the batch runs in + * a few seconds and cannot loop (falcon_keygen either returns or restarts on + * its own bounded acceptance tests). + * ------------------------------------------------------------------ */ +static void wb_solve_ntru_babai_clamp(WC_RNG* rng) +{ + sword8 f[8], g[8], F[8], G[8]; + word16 h[8]; + int i; + + for (i = 0; i < 256; i++) { + if (falcon_keygen(rng, f, g, F, G, h, 3) != 0) { + WB_NOTE("solve_NTRU_intermediate: keygen(logn=3) failed"); + break; + } + } + WB_OK("solve_NTRU_intermediate Babai clamp operand pair exercised"); +} + +/* A sampler that always returns 0. Used to make a signing attempt fully + * deterministic: the sampled lattice coordinates are zero, so the candidate is + * exactly the (huge) target and the shortness test always rejects. It touches + * neither the PRNG nor the ffLDL tree, so it cannot loop or diverge. */ +static int wb_samp_zero(void* ctx, fpr mu, fpr isigma) +{ + (void)ctx; + (void)mu; + (void)isigma; + return 0; +} + +/* A degenerate-but-valid basis at logn = 1: f*G - g*F = -12 + 31*X is nonzero + * at both FFT slots, so the Gram matrix is positive definite and the ffLDL + * leaf sigmas are finite and positive -- the sampler behaves normally. */ +#define WB_SIGN_LOGN 1 +#define WB_SIGN_N 2 + +static const sword8 wb_basis_f[WB_SIGN_N] = { 3, 1 }; +static const sword8 wb_basis_g[WB_SIGN_N] = { 1, -2 }; +static const sword8 wb_basis_F[WB_SIGN_N] = { 5, 0 }; +static const sword8 wb_basis_G[WB_SIGN_N] = { 0, 7 }; + +#ifndef WOLFSSL_FALCON_SIGN_SMALL_MEM +/* ------------------------------------------------------------------ * + * falcon_do_sign_tree restart loop: + * if (samplerErr != NULL && *samplerErr != 0) return *samplerErr; + * The check is only reached after do_sign_tree_once() REJECTS a candidate, + * which real signing does about once in a very long while -- and never with a + * latched sampler error, since the production caller aborts long before. Both + * are supplied directly instead: + * - a sampler that always returns 0 plus an all-zero expanded key makes the + * lattice point identically 0, so the candidate is the raw target; with a + * large hash-to-point value the squared norm is far above the acceptance + * bound and EVERY attempt rejects, deterministically; + * - the three samplerErr shapes then walk the operand pair: + * non-NULL -> nonzero : (T,T), returns after ONE attempt + * NULL : (F,-) + * non-NULL -> zero : (T,F) + * The last two run the full restart bound, which at logn = 1 is 4096 passes + * over a two-coefficient FFT -- microseconds, not a hang risk. + * ------------------------------------------------------------------ */ +static void wb_do_sign_tree_samplererr(void) +{ + fpr expanded[FALCON_EXPANDED_KEY_FPR(WB_SIGN_LOGN)]; + fpr tmp[FALCON_SIGN_TMP_FPR(WB_SIGN_LOGN) + 8]; + sword16 s2[WB_SIGN_N]; + word16 hm[WB_SIGN_N]; + int err = WC_NO_ERR_TRACE(BAD_FUNC_ARG); + int zero = 0; + size_t u; + + XMEMSET(expanded, 0, sizeof(expanded)); + XMEMSET(tmp, 0, sizeof(tmp)); + XMEMSET(s2, 0, sizeof(s2)); + /* 30000^2 * 2 stays inside sword32/word32 yet is ~4 orders of magnitude + * above l2bound[1], so the shortness test always rejects. */ + for (u = 0; u < WB_SIGN_N; u++) { + hm[u] = 30000; + } + + /* (T,T): latched sampler error -> returns after the first rejection. */ + if (falcon_do_sign_tree(wb_samp_zero, NULL, s2, expanded, hm, WB_SIGN_LOGN, + tmp, &err) != err) { + WB_NOTE("do_sign_tree(samplerErr set) expected the latched error"); + } + /* (F,-): no error pointer -> exhausts the restart bound. */ + (void)falcon_do_sign_tree(wb_samp_zero, NULL, s2, expanded, hm, + WB_SIGN_LOGN, tmp, NULL); + /* (T,F): error pointer present but clear -> exhausts the restart bound. */ + (void)falcon_do_sign_tree(wb_samp_zero, NULL, s2, expanded, hm, + WB_SIGN_LOGN, tmp, &zero); + WB_OK("falcon_do_sign_tree samplerErr operand pair exercised"); +} + +/* ------------------------------------------------------------------ * + * falcon_sign_core: if (ret == 0 && spc->p.err != 0) ret = spc->p.err; + * The round-trip only ever shows (T,F) (a clean sign with a healthy PRNG). + * cond0 FALSE : an out-of-range logn makes falcon_do_sign_tree reject the + * arguments, so ret != 0 and cond1 is not evaluated. + * (T,T) : pre-latch spc->p.err and let a signing attempt succeed. The + * target is the zero hash-to-point over a well-conditioned + * basis, so the sampled lattice point is essentially zero and + * the first attempt is accepted; because samplerErr is non-NULL + * and nonzero, a rejected attempt returns IMMEDIATELY instead + * of restarting, so the loop below can never hang. + * ------------------------------------------------------------------ */ +static void wb_sign_core_err(WC_RNG* rng) +{ + falcon_sampler_ctx spc; + fpr expanded[FALCON_EXPANDED_KEY_FPR(WB_SIGN_LOGN)]; + fpr tmp[FALCON_SIGN_TMP_FPR(WB_SIGN_LOGN) + 8]; + sword16 s2[WB_SIGN_N]; + word16 hm[WB_SIGN_N]; + int i, ok = 0; + + XMEMSET(expanded, 0, sizeof(expanded)); + XMEMSET(tmp, 0, sizeof(tmp)); + XMEMSET(hm, 0, sizeof(hm)); + if (falcon_expand_privkey(expanded, wb_basis_f, wb_basis_g, wb_basis_F, + wb_basis_G, WB_SIGN_LOGN, NULL) != 0) { + WB_NOTE("sign_core: expand_privkey(test basis) failed"); + return; + } + XMEMSET(&spc, 0, sizeof(spc)); + if (falcon_sampler_init(&spc, WB_SIGN_LOGN, rng) != 0) { + WB_NOTE("sign_core: sampler_init failed; p.err vectors skipped"); + return; + } + + /* cond0 FALSE: argument rejection inside falcon_do_sign_tree. */ + spc.p.err = 0; + (void)falcon_sign_core(&spc, expanded, hm, s2, tmp, 0); + + /* (T,T): a first-attempt success with the error already latched. */ + for (i = 0; i < 64; i++) { + s2[0] = -1; + s2[1] = -1; + spc.p.err = WC_NO_ERR_TRACE(BAD_FUNC_ARG); + (void)falcon_sign_core(&spc, expanded, hm, s2, tmp, WB_SIGN_LOGN); + if ((s2[0] != -1) || (s2[1] != -1)) { + ok = 1; /* s2 written -> the attempt was accepted */ + break; + } + } + spc.p.err = 0; + if (!ok) { + WB_NOTE("sign_core: no accepted attempt with p.err latched"); + } + wc_Shake256_Free(&spc.p.shake); + ForceZero(&spc, sizeof(spc)); + WB_OK("falcon_sign_core (ret==0)&&(p.err!=0) operand pair exercised"); +} + +#else /* WOLFSSL_FALCON_SIGN_SMALL_MEM */ + +/* ------------------------------------------------------------------ * + * falcon_do_sign_dyn restart loop: the low-memory twin of the decision above + * (same samplerErr shapes, same reasoning). The basis here is passed raw + * instead of expanded; it is well-conditioned (f*G - g*F != 0 at both slots) + * so the on-the-fly LDL never divides by zero, and the always-zero sampler + * again forces every attempt to reject against a large target. + * ------------------------------------------------------------------ */ +static void wb_do_sign_dyn_samplererr(void) +{ + fpr tmp[FALCON_SIGN_DYN_TMP_FPR(WB_SIGN_LOGN) + 16]; + sword16 s2[WB_SIGN_N]; + word16 hm[WB_SIGN_N]; + int err = WC_NO_ERR_TRACE(BAD_FUNC_ARG); + int zero = 0; + size_t u; + + XMEMSET(tmp, 0, sizeof(tmp)); + XMEMSET(s2, 0, sizeof(s2)); + for (u = 0; u < WB_SIGN_N; u++) { + hm[u] = 30000; + } + + if (falcon_do_sign_dyn(wb_samp_zero, NULL, s2, wb_basis_f, wb_basis_g, + wb_basis_F, wb_basis_G, hm, WB_SIGN_LOGN, tmp, &err) != err) { + WB_NOTE("do_sign_dyn(samplerErr set) expected the latched error"); + } + (void)falcon_do_sign_dyn(wb_samp_zero, NULL, s2, wb_basis_f, wb_basis_g, + wb_basis_F, wb_basis_G, hm, WB_SIGN_LOGN, tmp, NULL); + (void)falcon_do_sign_dyn(wb_samp_zero, NULL, s2, wb_basis_f, wb_basis_g, + wb_basis_F, wb_basis_G, hm, WB_SIGN_LOGN, tmp, &zero); + WB_OK("falcon_do_sign_dyn samplerErr operand pair exercised"); +} + +/* ------------------------------------------------------------------ * + * falcon_sign_dyn_core: low-memory twin of falcon_sign_core's + * (ret == 0 && spc->p.err != 0); identical vectors and identical bound on the + * number of attempts (a rejection returns immediately once p.err is latched). + * ------------------------------------------------------------------ */ +static void wb_sign_dyn_core_err(WC_RNG* rng) +{ + falcon_sampler_ctx spc; + fpr tmp[FALCON_SIGN_DYN_TMP_FPR(WB_SIGN_LOGN) + 16]; + sword16 s2[WB_SIGN_N]; + word16 hm[WB_SIGN_N]; + int i, ok = 0; + + XMEMSET(tmp, 0, sizeof(tmp)); + XMEMSET(hm, 0, sizeof(hm)); + XMEMSET(&spc, 0, sizeof(spc)); + if (falcon_sampler_init(&spc, WB_SIGN_LOGN, rng) != 0) { + WB_NOTE("sign_dyn_core: sampler_init failed; p.err vectors skipped"); + return; + } + + spc.p.err = 0; + (void)falcon_sign_dyn_core(&spc, wb_basis_f, wb_basis_g, wb_basis_F, + wb_basis_G, hm, s2, tmp, 0); + + for (i = 0; i < 64; i++) { + s2[0] = -1; + s2[1] = -1; + spc.p.err = WC_NO_ERR_TRACE(BAD_FUNC_ARG); + (void)falcon_sign_dyn_core(&spc, wb_basis_f, wb_basis_g, wb_basis_F, + wb_basis_G, hm, s2, tmp, WB_SIGN_LOGN); + if ((s2[0] != -1) || (s2[1] != -1)) { + ok = 1; + break; + } + } + spc.p.err = 0; + if (!ok) { + WB_NOTE("sign_dyn_core: no accepted attempt with p.err latched"); + } + wc_Shake256_Free(&spc.p.shake); + ForceZero(&spc, sizeof(spc)); + WB_OK("falcon_sign_dyn_core (ret==0)&&(p.err!=0) operand pair exercised"); +} + +#endif /* WOLFSSL_FALCON_SIGN_SMALL_MEM */ + +#endif /* !WOLFSSL_FALCON_VERIFY_ONLY */ + /* ------------------------------------------------------------------ * * Documented residuals: decision halves reachable only from a genuine * mid-computation error or a degenerate/forbidden operand, which cannot be * driven crash-safely from a white-box harness. Their opposite (normal) half is - * covered above (mostly by the real round-trip). + * covered above (mostly by the real round-trip). Each of these is carried as an + * EXCLUSIONS.md row with the source-level argument for why no satisfying vector + * exists. * ------------------------------------------------------------------ */ static void wb_residuals(void) { - WB_NOTE("residual: berexp (w==0)&&(i>0) i>0 FALSE half needs PRNG-forced " - "equal comparison bytes (deep sampler)"); - WB_NOTE("residual: solve_NTRU_deepest zint_mul_small overflow TRUE half " - "(bigint carry error path)"); - WB_NOTE("residual: solve_NTRU_intermediate !fpr_lt(z,2^63) TRUE half " - "(out-of-range Babai coefficient)"); - WB_NOTE("residual: solve_NTRU_intermediate !fpr_lt(+-2^31,xv) range clamp " - "(out-of-range reduction coefficient, data-dependent)"); - WB_NOTE("residual: solve_NTRU poly_big_to_small failure TRUE half " - "(reduction overflow)"); - WB_NOTE("residual: poly_small_mkgauss s<-127||s>127 TRUE half " - "(random sampler tail)"); - WB_NOTE("residual: keygen f/g coeff >=lim/<=-lim TRUE halves " - "(sampler bound forbids |coeff|>=128)"); - WB_NOTE("residual: check-public ft[i]==0 TRUE half " - "(non-invertible/degenerate key)"); - WB_NOTE("residual: do_sign_tree samplerErr!=0 / spc->p.err!=0 TRUE halves " - "(latched PRNG squeeze failure)"); + WB_NOTE("residual: solve_NTRU_binary_depth1 !fpr_lt(z,+-2^63) halves: the " + "Babai coefficient is bounded by sqrt(|F|^2+|G|^2)/sqrt(|f|^2+" + "|g|^2) with |F|,|G| < 2^61 (2-word CRT limbs), so |z| >= 2^63 " + "needs both depth-1 field norms to nearly vanish at one FFT slot"); + WB_NOTE("residual: keygen f[u]/g[u] vs lim halves: poly_small_mkgauss " + "guarantees |coeff| <= 127 and lim is 128 for logn <= 5 (provably " + "dead), while for logn >= 6 lim sits beyond 5.5 sigma of the " + "sampler and keygen seeds its own falcon_rng internally"); + WB_NOTE("residual: sign_msg/verify_msg (ret==0)&&(!key->...Set) cond0 " + "FALSE half: the preceding argument check returns early, so ret is " + "invariably 0 at that line"); } #endif /* HAVE_FALCON */ @@ -762,6 +1381,30 @@ int main(void) wb_native_roundtrip(&rng); #endif } +#ifndef WOLFSSL_FALCON_VERIFY_ONLY + /* Gap-close drivers. */ + if (haveRng) { + wb_berexp_loop(&rng); + wb_mkgauss_range(&rng); + } + wb_check_key_ntt_slots(); + wb_solve_deepest_overflow(); + if (haveRng) { + wb_solve_ntru_lim(&rng); + wb_solve_ntru_babai_clamp(&rng); + } +#ifndef WOLFSSL_FALCON_SIGN_SMALL_MEM + wb_do_sign_tree_samplererr(); + if (haveRng) { + wb_sign_core_err(&rng); + } +#else + wb_do_sign_dyn_samplererr(); + if (haveRng) { + wb_sign_dyn_core_err(&rng); + } +#endif +#endif /* !WOLFSSL_FALCON_VERIFY_ONLY */ wb_residuals(); if (haveRng) {