From f8f603fb7f42f3c433cfdffe5b0e9faaa7f82847 Mon Sep 17 00:00:00 2001 From: JeremiahM37 Date: Thu, 7 May 2026 21:36:53 -0400 Subject: [PATCH 1/5] Add SAKKE OOB regression test --- tests/unit.c | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/tests/unit.c b/tests/unit.c index 058ba6cd70f..9fe03a2ae35 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -34,6 +34,158 @@ #include "wolfcrypt/test/test.h" #endif +#ifdef WOLFCRYPT_HAVE_SAKKE +#include +#endif +#include +#include + +/* The audit_07 OOB regression test uses POSIX fork/mmap/mprotect to put + * a guard page right after the SSV buffer; on non-POSIX targets the test + * SKIPs. */ +#if defined(WOLFCRYPT_HAVE_SAKKE) && \ + (defined(__unix__) || defined(__APPLE__) || defined(__linux__)) +#define SAKKE_OOB_TEST_HAVE_POSIX +#include +#include +#include +#include +#include +#endif + +/* Child exit codes. SETUP_FAILED tells the parent the child never reached + * the buggy code path -> SKIP, not PASS. */ +#define SAKKE_OOB_CHILD_OK 0 +#define SAKKE_OOB_CHILD_SETUP_FAILED 2 +#define SAKKE_OOB_CHILD_MMAP_FAILED 3 + +#ifdef SAKKE_OOB_TEST_HAVE_POSIX +/* The trigger always _exit()s; mark noreturn for GCC/Clang to keep + * -Wmissing-noreturn quiet. The POSIX guard above already constrains us + * to compilers that understand the GNU attribute. */ +#if defined(__GNUC__) || defined(__clang__) +#define SAKKE_OOB_NORETURN __attribute__((noreturn)) +#else +#define SAKKE_OOB_NORETURN +#endif + +/* Regression test for audit_07. Previously sakke_xor_in_v wrote hashSz + * bytes at out+idx without clamping to the bytes remaining in the + * caller's buffer, so with ssvSz=48 and SHA-256 (hashSz=32) the second + * iteration of sakke_hash_to_range overshot the 48-byte buffer by 16 + * bytes. We run the call in a forked child with the buffer placed flush + * against a PROT_NONE guard page: before the fix the OOB write SIGSEGV'd + * (or SIGABRT under ASan); the fix produces a clean exit. */ +static SAKKE_OOB_NORETURN void sakke_oob_trigger(void) +{ + SakkeKey key; + WC_RNG rng; + byte id[] = "user@example.com"; + word16 ssvSz = 48; + word16 authSz = 0; + long pgsz = sysconf(_SC_PAGESIZE); + byte* base; + byte* ssv; + byte* auth = NULL; + int ret; + + if (pgsz <= 0) _exit(SAKKE_OOB_CHILD_MMAP_FAILED); + + base = (byte*)mmap(NULL, (size_t)(2 * pgsz), + PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (base == MAP_FAILED) _exit(SAKKE_OOB_CHILD_MMAP_FAILED); + if (mprotect(base + pgsz, (size_t)pgsz, PROT_NONE) != 0) + _exit(SAKKE_OOB_CHILD_MMAP_FAILED); + ssv = base + pgsz - ssvSz; /* ssv ends exactly at the guard page */ + + if (wc_InitRng(&rng) != 0) _exit(SAKKE_OOB_CHILD_SETUP_FAILED); + if (wc_InitSakkeKey_ex(&key, 128, ECC_SAKKE_1, NULL, INVALID_DEVID) != 0) + _exit(SAKKE_OOB_CHILD_SETUP_FAILED); + if (wc_MakeSakkeKey(&key, &rng) != 0) + _exit(SAKKE_OOB_CHILD_SETUP_FAILED); + if (wc_SetSakkeIdentity(&key, id, sizeof(id) - 1) != 0) + _exit(SAKKE_OOB_CHILD_SETUP_FAILED); + ret = wc_MakeSakkeEncapsulatedSSV(&key, WC_HASH_TYPE_SHA256, ssv, ssvSz, + NULL, &authSz); + if (ret != WC_NO_ERR_TRACE(LENGTH_ONLY_E)) + _exit(SAKKE_OOB_CHILD_SETUP_FAILED); + auth = (byte*)XMALLOC(authSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (auth == NULL) _exit(SAKKE_OOB_CHILD_SETUP_FAILED); + (void)wc_MakeSakkeEncapsulatedSSV(&key, WC_HASH_TYPE_SHA256, ssv, ssvSz, + auth, &authSz); + XFREE(auth, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wc_FreeSakkeKey(&key); + wc_FreeRng(&rng); + _exit(SAKKE_OOB_CHILD_OK); +} + +static int sakke_xor_in_v_oob_test(void) +{ + pid_t pid = fork(); + int status = 0; + if (pid < 0) { + printf("sakke_oob: SKIP (fork failed)\n"); + return 0; + } + if (pid == 0) { + /* Silence ASan/glibc noise from the child; the parent only looks + * at the exit status. freopen() is declared warn_unused_result, + * so we capture the return into a local and (void)-cast it + * rather than discarding the call expression directly. */ + FILE* dn = freopen("/dev/null", "w", stderr); + (void)dn; + sakke_oob_trigger(); + _exit(SAKKE_OOB_CHILD_OK); + } + if (waitpid(pid, &status, 0) < 0) { + printf("sakke_oob: SKIP (waitpid failed)\n"); + return 0; + } + /* The fix is verified iff the child exited cleanly with the OK code. + * Crash signals (SEGV/BUS/ABRT) indicate the bug regressed; the SKIP + * codes mean we never reached the buggy call. Anything else (other + * signals, unexpected exit codes, stopped/continued) is unknown — do + * not treat it as PASS. */ + if (WIFEXITED(status)) { + int code = WEXITSTATUS(status); + if (code == SAKKE_OOB_CHILD_OK) { + printf("sakke_oob: PASS\n"); + return 0; + } + if (code == SAKKE_OOB_CHILD_SETUP_FAILED || + code == SAKKE_OOB_CHILD_MMAP_FAILED) { + printf("sakke_oob: SKIP (child setup failed, code=%d)\n", code); + return 0; + } + printf("sakke_oob: FAIL (unexpected exit code %d)\n", code); + return -1; + } + if (WIFSIGNALED(status)) { + int sig = WTERMSIG(status); + if (sig == SIGSEGV || sig == SIGBUS || sig == SIGABRT) { + printf("sakke_oob: FAIL (signal %d, ssvSz=48 SHA-256)\n", sig); + return -1; + } + printf("sakke_oob: FAIL (unexpected signal %d)\n", sig); + return -1; + } + printf("sakke_oob: FAIL (child neither exited nor signaled)\n"); + return -1; +} +#elif defined(WOLFCRYPT_HAVE_SAKKE) +static int sakke_xor_in_v_oob_test(void) +{ + printf("sakke_oob: SKIP (needs POSIX fork/mmap)\n"); + return 0; +} +#else +static int sakke_xor_in_v_oob_test(void) +{ + printf("sakke_oob: SKIP (WOLFCRYPT_HAVE_SAKKE not defined)\n"); + return 0; +} +#endif + int allTesting = 1; int apiTesting = 1; int myoptind = 0; @@ -260,6 +412,18 @@ int unit_test(int argc, char** argv) goto exit; } + /* The audit_07 SAKKE regression tests live here rather than in + * the ApiTest registry: the OOB test forks/mmaps a guard page, + * which doesn't fit the Assert*-driven ApiTest harness, and + * keeping both checks colocated with the wolfcrypt_test gate + * matches how other targeted regressions are run. */ + if (sakke_xor_in_v_oob_test() != 0) { + ret = -1; + fflush(stdout); + goto exit; + } + fflush(stdout); + XMEMSET(&wc_args, 0, sizeof(wc_args)); wolfcrypt_test(&wc_args); if (wc_args.return_code != 0) { From 1348df28b7e5febee1771c0933db5ad187bde137 Mon Sep 17 00:00:00 2001 From: JeremiahM37 Date: Thu, 7 May 2026 22:19:53 -0400 Subject: [PATCH 2/5] Clamp sakke_xor_in_v write to buffer length --- wolfcrypt/src/sakke.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/wolfcrypt/src/sakke.c b/wolfcrypt/src/sakke.c index f07f4cf4b23..52f8bc20844 100644 --- a/wolfcrypt/src/sakke.c +++ b/wolfcrypt/src/sakke.c @@ -6164,18 +6164,29 @@ static void sakke_xor_in_v(const byte* v, word32 hashSz, byte* out, word32 idx, { int o; word32 i; + word32 len; if (idx == 0) { i = hashSz - (n % hashSz); if (i == hashSz) { i = 0; } + len = hashSz - i; } else { i = 0; + /* Clamp to bytes still remaining in the caller's buffer. Without + * this clamp, the final iteration of sakke_hash_to_range (when + * n > hashSz and (n % hashSz) != 0) writes hashSz bytes at + * out+idx and overshoots the n-byte buffer by hashSz - (n%hashSz) + * bytes. */ + len = (n > idx) ? (n - idx) : 0; + if (len > hashSz) { + len = hashSz; + } } o = (int)i; - xorbuf(out + idx + i - o, v + i, hashSz - i); + xorbuf(out + idx + i - o, v + i, len); } /* From c3b8d9d41234f11a58635e666877b1d121f13989 Mon Sep 17 00:00:00 2001 From: JeremiahM37 Date: Fri, 8 May 2026 12:45:21 -0600 Subject: [PATCH 3/5] Fix sakke_xor_in_v write offset and read base --- wolfcrypt/src/sakke.c | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/wolfcrypt/src/sakke.c b/wolfcrypt/src/sakke.c index 52f8bc20844..cec6a289268 100644 --- a/wolfcrypt/src/sakke.c +++ b/wolfcrypt/src/sakke.c @@ -6162,31 +6162,30 @@ static int sakke_calc_h_v(SakkeKey* key, enum wc_HashType hashType, static void sakke_xor_in_v(const byte* v, word32 hashSz, byte* out, word32 idx, word32 n) { - int o; - word32 i; + word32 skip; + word32 off; word32 len; + /* RFC 6508 5.1: output is the low n octets of v_1||v_2||...||v_l (the + * concatenation of l hash outputs taken modulo 2^(n*8)). When n is not + * a multiple of hashSz, drop the leading 'skip' high bytes of the first + * hash output. */ + skip = n % hashSz; + skip = (skip == 0) ? 0 : (hashSz - skip); + if (idx == 0) { - i = hashSz - (n % hashSz); - if (i == hashSz) { - i = 0; - } - len = hashSz - i; + off = 0; + len = hashSz - skip; + xorbuf(out + off, v + skip, len); } else { - i = 0; - /* Clamp to bytes still remaining in the caller's buffer. Without - * this clamp, the final iteration of sakke_hash_to_range (when - * n > hashSz and (n % hashSz) != 0) writes hashSz bytes at - * out+idx and overshoots the n-byte buffer by hashSz - (n%hashSz) - * bytes. */ - len = (n > idx) ? (n - idx) : 0; + off = idx - skip; + len = n - off; if (len > hashSz) { len = hashSz; } + xorbuf(out + off, v, len); } - o = (int)i; - xorbuf(out + idx + i - o, v + i, len); } /* From f813e1c3afe857ba9a8997884ee96e3407d712ce Mon Sep 17 00:00:00 2001 From: JeremiahM37 Date: Fri, 8 May 2026 12:45:30 -0600 Subject: [PATCH 4/5] Reject ssvSz=0 in SAKKE public APIs --- wolfcrypt/src/sakke.c | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/wolfcrypt/src/sakke.c b/wolfcrypt/src/sakke.c index cec6a289268..6e110c1690a 100644 --- a/wolfcrypt/src/sakke.c +++ b/wolfcrypt/src/sakke.c @@ -6692,11 +6692,12 @@ static int sakke_compute_point_r(SakkeKey* key, const byte* id, word16 idSz, * @param [out] auth Authentication data. * @param [out] authSz Size of authentication data in bytes. * @return 0 on success. - * @return BAD_FUNC_ARG when key, ssv or encSz is NULL, ssvSz is to big or - * encSz is too small. + * @return BAD_FUNC_ARG when key, ssv or authSz is NULL, when encapsulating + * and ssvSz is 0 or larger than the curve modulus byte length, + * or *authSz is too small. * @return BAD_STATE_E when identity not set. * @return LENGTH_ONLY_E when auth is NULL. authSz contains required size of - * auth in bytes. + * auth in bytes. ssvSz is not consulted on the size-query path. * @return MEMORY_E when dynamic memory allocation fails. * @return Other -ve value on internal failure. */ @@ -6728,8 +6729,17 @@ int wc_MakeSakkeEncapsulatedSSV(SakkeKey* key, enum wc_HashType hashType, /* Uncompressed point */ outSz = (word16)(1 + 2 * n); - if ((auth != NULL) && (*authSz < outSz)) { - err = BAD_FUNC_ARG; + /* ssvSz is only meaningful when actually encapsulating; the + * size-query path (auth == NULL) only depends on the curve. RFC + * 6508 6.2.1 step 1 puts SSV in the range 0..2^n-1, so ssvSz must + * be in [1, n] octets. */ + if (auth != NULL) { + if ((ssvSz == 0) || (ssvSz > n)) { + err = BAD_FUNC_ARG; + } + else if (*authSz < outSz) { + err = BAD_FUNC_ARG; + } } } if (err == 0) { @@ -6867,7 +6877,8 @@ int wc_GenerateSakkeSSV(SakkeKey* key, WC_RNG* rng, byte* ssv, word16* ssvSz) * @param [in] auth Authentication data. * @param [in] authSz Size of authentication data in bytes. * @return 0 on success. - * @return BAD_FUNC_ARG when key, ssv or auth is NULL. + * @return BAD_FUNC_ARG when key, ssv or auth is NULL, ssvSz is 0 or + * larger than the curve modulus byte length. * @return BAD_STATE_E when RSK or identity not set. * @return SAKKE_VERIFY_FAIL_E when calculated R doesn't match the encapsulated * data's R. @@ -6886,7 +6897,7 @@ int wc_DeriveSakkeSSV(SakkeKey* key, enum wc_HashType hashType, byte* ssv, byte* test = NULL; byte a[WC_MAX_DIGEST_SIZE] = {0}; - if ((key == NULL) || (ssv == NULL) || (auth == NULL)) { + if ((key == NULL) || (ssv == NULL) || (auth == NULL) || (ssvSz == 0)) { err = BAD_FUNC_ARG; } if ((err == 0) && (!key->rsk.set || (key->idSz == 0))) { @@ -6905,6 +6916,10 @@ int wc_DeriveSakkeSSV(SakkeKey* key, enum wc_HashType hashType, byte* ssv, if (authSz != 2 * n + 1) { err = BAD_FUNC_ARG; } + /* RFC 6508 6.2.1: SSV is in 0..2^n-1, so ssvSz must be <= n. */ + else if (ssvSz > n) { + err = BAD_FUNC_ARG; + } } if (err == 0) { err = sakke_load_base_point(key); From 59b7a7154c5c6b792e3f2f04e55130ce853ba67f Mon Sep 17 00:00:00 2001 From: JeremiahM37 Date: Fri, 8 May 2026 13:33:46 -0600 Subject: [PATCH 5/5] Add SAKKE mask correctness regression test --- tests/unit.c | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/unit.c b/tests/unit.c index 9fe03a2ae35..cedff883bd5 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -186,6 +186,111 @@ static int sakke_xor_in_v_oob_test(void) } #endif +#ifdef WOLFCRYPT_HAVE_SAKKE +/* Verify sakke_hash_to_range produces the full RFC 6508 5.1 mask for + * ssvSz=48 with SHA-256, and that the public APIs reject ssvSz=0. The + * old broken offset math left ssv[16..31] never XORed; encapsulating + * an all-zero ssv exposes the gap (those bytes stay zero under the + * bug, become random mask material once fixed). */ +static int sakke_mask_correctness_test(void) +{ + SakkeKey key; + WC_RNG rng; + byte id[] = "user@example.com"; + word16 ssvSz = 48; + word16 ssvSzZero = 0; + word16 authSz = 0; + byte ssv[48]; + byte* auth = NULL; + int ret; + int i; + int allZero = 1; + int rc = 0; + + if (wc_InitRng(&rng) != 0) { + printf("sakke_correctness: SKIP (rng init)\n"); + return 0; + } + if (wc_InitSakkeKey_ex(&key, 128, ECC_SAKKE_1, NULL, INVALID_DEVID) != 0) { + wc_FreeRng(&rng); + printf("sakke_correctness: SKIP (sakke key init)\n"); + return 0; + } + if (wc_MakeSakkeKey(&key, &rng) != 0) { + wc_FreeSakkeKey(&key); + wc_FreeRng(&rng); + printf("sakke_correctness: SKIP (sakke key gen)\n"); + return 0; + } + if (wc_SetSakkeIdentity(&key, id, sizeof(id) - 1) != 0) { + wc_FreeSakkeKey(&key); + wc_FreeRng(&rng); + printf("sakke_correctness: SKIP (set identity)\n"); + return 0; + } + + ret = wc_MakeSakkeEncapsulatedSSV(&key, WC_HASH_TYPE_SHA256, ssv, ssvSz, + NULL, &authSz); + if (ret != WC_NO_ERR_TRACE(LENGTH_ONLY_E)) { + printf("sakke_correctness: SKIP (auth size query)\n"); + goto cleanup; + } + auth = (byte*)XMALLOC(authSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (auth == NULL) { + printf("sakke_correctness: SKIP (alloc)\n"); + goto cleanup; + } + + XMEMSET(ssv, 0, ssvSz); + ret = wc_MakeSakkeEncapsulatedSSV(&key, WC_HASH_TYPE_SHA256, ssv, ssvSz, + auth, &authSz); + if (ret != 0) { + printf("sakke_correctness: FAIL (encap ret=%d)\n", ret); + rc = -1; + goto cleanup; + } + for (i = 16; i < 32; i++) { + if (ssv[i] != 0) { allZero = 0; break; } + } + if (allZero) { + printf("sakke_correctness: FAIL (ssv[16..31] not XORed)\n"); + rc = -1; + goto cleanup; + } + + ret = wc_MakeSakkeEncapsulatedSSV(&key, WC_HASH_TYPE_SHA256, ssv, ssvSzZero, + auth, &authSz); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + printf("sakke_correctness: FAIL (encap ssvSz=0 ret=%d)\n", ret); + rc = -1; + goto cleanup; + } + ret = wc_DeriveSakkeSSV(&key, WC_HASH_TYPE_SHA256, ssv, ssvSzZero, + auth, authSz); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + printf("sakke_correctness: FAIL (derive ssvSz=0 ret=%d)\n", ret); + rc = -1; + goto cleanup; + } + + printf("sakke_correctness: PASS\n"); + +cleanup: + if (auth != NULL) { + XFREE(auth, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + wc_FreeSakkeKey(&key); + wc_FreeRng(&rng); + return rc; +} +#else +static int sakke_mask_correctness_test(void) +{ + printf("sakke_correctness: SKIP (WOLFCRYPT_HAVE_SAKKE not defined)\n"); + return 0; +} +#endif + int allTesting = 1; int apiTesting = 1; int myoptind = 0; @@ -422,6 +527,11 @@ int unit_test(int argc, char** argv) fflush(stdout); goto exit; } + if (sakke_mask_correctness_test() != 0) { + ret = -1; + fflush(stdout); + goto exit; + } fflush(stdout); XMEMSET(&wc_args, 0, sizeof(wc_args));