diff --git a/CMakeLists.txt b/CMakeLists.txt index 7345e9afb1d..638576fdc0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4161,6 +4161,7 @@ if(WOLFSSL_EXAMPLES) tests/api/test_hash.c tests/api/test_hmac.c tests/api/test_cmac.c + tests/api/test_kdf.c tests/api/test_she.c tests/api/test_des3.c tests/api/test_chacha.c @@ -4173,7 +4174,10 @@ if(WOLFSSL_EXAMPLES) tests/api/test_ascon.c tests/api/test_sm4.c tests/api/test_wc_encrypt.c + tests/api/test_coding.c + tests/api/test_error.c tests/api/test_random.c + tests/api/test_wolfentropy.c tests/api/test_wolfmath.c tests/api/test_rsa.c tests/api/test_dsa.c diff --git a/tests/api.c b/tests/api.c index 238ea673976..395cc94d282 100644 --- a/tests/api.c +++ b/tests/api.c @@ -211,6 +211,7 @@ #include #include #include +#include #include #include #include @@ -223,7 +224,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -7524,10 +7528,25 @@ static int test_wolfSSL_read_write_ex(void) ExpectIntEQ(XSTRCMP((char*)buf, test_str), 0); - ExpectIntEQ(wolfSSL_shutdown(ssl_c), WOLFSSL_SHUTDOWN_NOT_DONE); - ExpectIntEQ(wolfSSL_shutdown(ssl_s), WOLFSSL_SHUTDOWN_NOT_DONE); - ExpectIntEQ(wolfSSL_shutdown(ssl_c), WOLFSSL_SUCCESS); - ExpectIntEQ(wolfSSL_shutdown(ssl_s), WOLFSSL_SUCCESS); + /* Drive the bidirectional close-notify exchange to completion instead of + * asserting a fixed NOT_DONE/SUCCESS sequence: the number of shutdown + * round-trips is protocol-version/config dependent, so a hard-coded + * sequence is fragile (fails e.g. under the cmake old-TLS build). */ + { + int shutC = WOLFSSL_SHUTDOWN_NOT_DONE; + int shutS = WOLFSSL_SHUTDOWN_NOT_DONE; + int shutIt; + for (shutIt = 0; shutIt < 10 && + (shutC != WOLFSSL_SUCCESS || shutS != WOLFSSL_SUCCESS); + shutIt++) { + if (shutC != WOLFSSL_SUCCESS) + shutC = wolfSSL_shutdown(ssl_c); + if (shutS != WOLFSSL_SUCCESS) + shutS = wolfSSL_shutdown(ssl_s); + } + ExpectIntEQ(shutC, WOLFSSL_SUCCESS); + ExpectIntEQ(shutS, WOLFSSL_SUCCESS); + } wolfSSL_free(ssl_c); wolfSSL_free(ssl_s); @@ -36911,6 +36930,8 @@ TEST_CASE testCases[] = { TEST_HMAC_DECLS, /* CMAC */ TEST_CMAC_DECLS, + /* KDF */ + TEST_KDF_DECLS, /* SHE */ TEST_SHE_DECLS, #ifdef WOLFSSL_SHE_EXTENDED @@ -36952,9 +36973,14 @@ TEST_CASE testCases[] = { TEST_SM4_DECLS, /* wc_encrypt API */ TEST_WC_ENCRYPT_DECLS, + /* wolfCrypt coding (Base64/Base16) */ + TEST_CODING_DECLS, + /* wolfCrypt error strings */ + TEST_ERROR_DECLS, /* RNG tests */ TEST_RANDOM_DECLS, + TEST_WOLFENTROPY_DECLS, /* Public key */ /* wolfmath MP API tests */ diff --git a/tests/api/include.am b/tests/api/include.am index fcb9720114c..ab2d590432c 100644 --- a/tests/api/include.am +++ b/tests/api/include.am @@ -18,6 +18,7 @@ tests_unit_test_SOURCES += tests/api/test_hash.c # MAC tests_unit_test_SOURCES += tests/api/test_hmac.c tests_unit_test_SOURCES += tests/api/test_cmac.c +tests_unit_test_SOURCES += tests/api/test_kdf.c # SHE tests_unit_test_SOURCES += tests/api/test_she.c # Cipher @@ -32,8 +33,11 @@ tests_unit_test_SOURCES += tests/api/test_aes.c tests_unit_test_SOURCES += tests/api/test_ascon.c tests_unit_test_SOURCES += tests/api/test_sm4.c tests_unit_test_SOURCES += tests/api/test_wc_encrypt.c +tests_unit_test_SOURCES += tests/api/test_coding.c +tests_unit_test_SOURCES += tests/api/test_error.c # Random tests_unit_test_SOURCES += tests/api/test_random.c +tests_unit_test_SOURCES += tests/api/test_wolfentropy.c # MP tests_unit_test_SOURCES += tests/api/test_wolfmath.c # Public Key Algorithm @@ -138,6 +142,7 @@ EXTRA_DIST += tests/api/test_digest.h EXTRA_DIST += tests/api/test_hash.h EXTRA_DIST += tests/api/test_hmac.h EXTRA_DIST += tests/api/test_cmac.h +EXTRA_DIST += tests/api/test_kdf.h EXTRA_DIST += tests/api/test_she.h EXTRA_DIST += tests/api/test_des3.h EXTRA_DIST += tests/api/test_chacha.h @@ -151,7 +156,10 @@ EXTRA_DIST += tests/api/test_ascon.h EXTRA_DIST += tests/api/test_sm4.h EXTRA_DIST += tests/api/test_ascon_kats.h EXTRA_DIST += tests/api/test_wc_encrypt.h +EXTRA_DIST += tests/api/test_coding.h +EXTRA_DIST += tests/api/test_error.h EXTRA_DIST += tests/api/test_random.h +EXTRA_DIST += tests/api/test_wolfentropy.h EXTRA_DIST += tests/api/test_wolfmath.h EXTRA_DIST += tests/api/test_rsa.h EXTRA_DIST += tests/api/test_dsa.h diff --git a/tests/api/test_chacha20_poly1305.c b/tests/api/test_chacha20_poly1305.c index 6f90c71712c..a7a53ce0879 100644 --- a/tests/api/test_chacha20_poly1305.c +++ b/tests/api/test_chacha20_poly1305.c @@ -1006,3 +1006,305 @@ int test_wc_ChaCha20Poly1305_CrossCipher(void) #endif return EXPECT_RESULT(); } /* END test_wc_ChaCha20Poly1305_CrossCipher */ + +/******************************************************************************* + * ChaCha20-Poly1305 residual MC/DC decision coverage + * + * The rest of the suite above already flips both sides of: every one-shot + * Encrypt/Decrypt NULL-pointer arg check except the plaintext/ciphertext + * NULL-with-zero-length independence pair; the streaming Init/UpdateAad/ + * UpdateData/Final state-machine transitions (MidStreamState); the AAD/data + * padding true/false split; isEncrypt; and the tag-compare success/failure + * paths (AeadEdgeCases). What remains is closed here: + * + * - wc_ChaCha20Poly1305_Encrypt/Decrypt: the "(len > 0 && ptr == NULL)" + * check's false-side independence pair (NULL pointer + zero length must + * SUCCEED, not fail) - the true side is already covered elsewhere. + * - wc_ChaCha20Poly1305_CheckTag: neither NULL operand is exercised + * anywhere else in the suite (only ever called with two valid buffers). + * - wc_ChaCha20Poly1305_UpdateAad: NULL inAAD + non-zero inAADLen called + * DIRECTLY (the one-shot API only ever forwards a NULL aad with length + * 0); valid pointer + zero length (the block-guard's false side, state + * must stay READY rather than advancing to AAD). + * - wc_ChaCha20Poly1305_UpdateData: NULL inData / NULL outData - never + * exercised (every existing call uses two valid buffers). + * - wc_ChaCha20Poly1305_Final: NULL outAuthTag - never exercised (only the + * NULL-aead operand of that same check is tested elsewhere). + * - wc_ChaCha20Poly1305_UpdateAad / UpdateData: the CHACHA_POLY_OVERFLOW + * true branch is never taken anywhere else (every AAD/data length used + * is tiny). The overflow comparison runs before the buffer is ever + * dereferenced, so a real (small) buffer pointer paired with an + * out-of-range word32 length is safe to pass. + ******************************************************************************/ +int test_wc_ChaCha20Poly1305_DecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CHACHA) && defined(HAVE_POLY1305) + static const byte key[CHACHA20_POLY1305_AEAD_KEYSIZE] = { + 0x80,0x81,0x82,0x83, 0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x8b, 0x8c,0x8d,0x8e,0x8f, + 0x90,0x91,0x92,0x93, 0x94,0x95,0x96,0x97, + 0x98,0x99,0x9a,0x9b, 0x9c,0x9d,0x9e,0x9f + }; + static const byte iv[CHACHA20_POLY1305_AEAD_IV_SIZE] = { + 0x07,0x00,0x00,0x00, 0x40,0x41,0x42,0x43, 0x44,0x45,0x46,0x47 + }; + static const byte aad[12] = { + 0x50,0x51,0x52,0x53, 0xc0,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7 + }; + static const byte data[4] = { 0x00, 0x01, 0x02, 0x03 }; + ChaChaPoly_Aead aead; + byte tag[CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE]; + byte dummyOut[1]; + byte emptyBuf[1]; + byte outBuf[4]; + + XMEMSET(&aead, 0, sizeof(aead)); + XMEMSET(tag, 0, sizeof(tag)); + XMEMSET(dummyOut, 0, sizeof(dummyOut)); + XMEMSET(emptyBuf, 0, sizeof(emptyBuf)); + XMEMSET(outBuf, 0, sizeof(outBuf)); + + /* --- One-shot Encrypt/Decrypt: NULL plaintext/ciphertext + zero length. + * Independence pair (holding "inPlaintext == NULL" fixed true) for the + * "inPlaintextLen > 0" operand of "(inPlaintextLen > 0 && + * inPlaintext == NULL)": with inPlaintextLen == 0 this specific + * AND-term evaluates false, so the top-level arg check no longer + * returns BAD_FUNC_ARG directly from *this* line - control instead + * reaches the internal wc_ChaCha20Poly1305_UpdateData() call, whose OWN + * arg check rejects a NULL inData/outData pointer unconditionally + * regardless of length, so the end-to-end return code is still + * BAD_FUNC_ARG (via a different source line). The true side (non-zero + * length + NULL pointer, caught directly by the top-level check) is + * already covered by test_wc_ChaCha20Poly1305_aead. */ + ExpectIntEQ(wc_ChaCha20Poly1305_Encrypt(key, iv, aad, sizeof(aad), + NULL, 0, dummyOut, tag), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_ChaCha20Poly1305_Decrypt(key, iv, aad, sizeof(aad), + NULL, 0, tag, dummyOut), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- One-shot Encrypt/Decrypt: zero-length plaintext/ciphertext with a + * valid (non-NULL) pointer must SUCCEED - the one-shot wrapper supports + * an empty AEAD message when given a real (if unused) buffer; only the + * streaming API's empty-message case is covered elsewhere + * (AeadEdgeCases). */ + ExpectIntEQ(wc_ChaCha20Poly1305_Encrypt(key, iv, aad, sizeof(aad), + emptyBuf, 0, dummyOut, tag), 0); + ExpectIntEQ(wc_ChaCha20Poly1305_Decrypt(key, iv, aad, sizeof(aad), + emptyBuf, 0, tag, dummyOut), 0); + + /* --- wc_ChaCha20Poly1305_CheckTag: NULL operand independence pairs. */ + ExpectIntEQ(wc_ChaCha20Poly1305_CheckTag(NULL, tag), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_ChaCha20Poly1305_CheckTag(tag, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- wc_ChaCha20Poly1305_UpdateAad: NULL inAAD + non-zero inAADLen, + * called directly while state == READY. */ + ExpectIntEQ(wc_ChaCha20Poly1305_Init(&aead, key, iv, + CHACHA20_POLY1305_AEAD_ENCRYPT), 0); + ExpectIntEQ(wc_ChaCha20Poly1305_UpdateAad(&aead, NULL, 5), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* Arg-check failure must not have advanced the state - still READY. */ + ExpectIntEQ(aead.state, CHACHA20_POLY1305_STATE_READY); + + /* --- wc_ChaCha20Poly1305_UpdateAad: valid pointer + zero length -> + * success, and the "inAAD && inAADLen > 0" block-guard false branch is + * taken - state must stay READY (not advance to AAD). */ + ExpectIntEQ(wc_ChaCha20Poly1305_UpdateAad(&aead, aad, 0), 0); + ExpectIntEQ(aead.state, CHACHA20_POLY1305_STATE_READY); + + /* --- wc_ChaCha20Poly1305_UpdateData: NULL inData / NULL outData. */ + ExpectIntEQ(wc_ChaCha20Poly1305_UpdateData(&aead, NULL, outBuf, + sizeof(data)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_ChaCha20Poly1305_UpdateData(&aead, data, NULL, + sizeof(data)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- wc_ChaCha20Poly1305_Final: NULL outAuthTag. */ + ExpectIntEQ(wc_ChaCha20Poly1305_Final(&aead, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- wc_ChaCha20Poly1305_UpdateAad: CHACHA_POLY_OVERFLOW true branch. + * Never exercised elsewhere: every existing AAD length is tiny. The + * overflow check runs before inAAD is dereferenced, so a real (small) + * buffer paired with an out-of-range length value is safe. */ + ExpectIntEQ(wc_ChaCha20Poly1305_Init(&aead, key, iv, + CHACHA20_POLY1305_AEAD_ENCRYPT), 0); + ExpectIntEQ(wc_ChaCha20Poly1305_UpdateAad(&aead, aad, sizeof(aad)), 0); + ExpectIntEQ(wc_ChaCha20Poly1305_UpdateAad(&aead, aad, + CHACHA20_POLY1305_MAX), WC_NO_ERR_TRACE(CHACHA_POLY_OVERFLOW)); + + /* --- wc_ChaCha20Poly1305_UpdateData: CHACHA_POLY_OVERFLOW true branch, + * same reasoning as above but for the data-length accumulator. */ + ExpectIntEQ(wc_ChaCha20Poly1305_UpdateData(&aead, data, outBuf, + sizeof(data)), 0); + ExpectIntEQ(wc_ChaCha20Poly1305_UpdateData(&aead, data, outBuf, + CHACHA20_POLY1305_MAX), WC_NO_ERR_TRACE(CHACHA_POLY_OVERFLOW)); +#endif + return EXPECT_RESULT(); +} /* END test_wc_ChaCha20Poly1305_DecisionCoverage */ + +/******************************************************************************* + * XChaCha20-Poly1305 residual MC/DC decision coverage + * + * - wc_XChaCha20Poly1305_Init: NULL aead - never reachable through the + * Encrypt/Decrypt one-shot wrappers, which always pass an internal, + * valid aead pointer; ad == NULL + ad_len == 0 success - independence + * pair (holding ad == NULL fixed) for the ad_len operand of + * "(ad == NULL && ad_len > 0)". The true side of that AND, and the + * ad-pointer operand's own independence pair, are already exercised via + * test_wc_XChaCha20Poly1305_aead's bad-arg Encrypt calls. + * - wc_XChaCha20Poly1305_Decrypt: src_len < POLY1305_DIGEST_SIZE, the + * decrypt-too-short branch of the internal crypt_oneshot helper - never + * exercised (every existing decrypt call uses a >= 16-byte ciphertext). + * - wc_XChaCha20Poly1305_Encrypt: dst_space < dst_len -> BUFFER_E - never + * exercised (every existing call sizes the output buffer generously). + * - internal crypt_oneshot helper: the ad_len/nonce_len/key_len > + * WOLFSSL_MAX_32BIT truncation-guard OR-chain - never exercised (every + * existing call uses in-range lengths); each operand's independence + * pair is shown on a 64-bit CPU (the only width where a size_t value + * can exceed WOLFSSL_MAX_32BIT). + ******************************************************************************/ +int test_wc_XChaCha20Poly1305_DecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_POLY1305) && defined(HAVE_XCHACHA) + static const byte key[CHACHA20_POLY1305_AEAD_KEYSIZE] = { + 0x80,0x81,0x82,0x83, 0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x8b, 0x8c,0x8d,0x8e,0x8f, + 0x90,0x91,0x92,0x93, 0x94,0x95,0x96,0x97, + 0x98,0x99,0x9a,0x9b, 0x9c,0x9d,0x9e,0x9f + }; + static const byte nonce[XCHACHA20_POLY1305_AEAD_NONCE_SIZE] = { + 0x40,0x41,0x42,0x43, 0x44,0x45,0x46,0x47, + 0x48,0x49,0x4a,0x4b, 0x4c,0x4d,0x4e,0x4f, + 0x50,0x51,0x52,0x53, 0x54,0x55,0x56,0x57 + }; + static const byte aad[12] = { + 0x50,0x51,0x52,0x53, 0xc0,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7 + }; + static const byte plaintext[16] = { + 0x4c,0x61,0x64,0x69, 0x65,0x73,0x20,0x61, + 0x6e,0x64,0x20,0x47, 0x65,0x6e,0x74,0x73 + }; + ChaChaPoly_Aead aead; + byte out[sizeof(plaintext) + POLY1305_DIGEST_SIZE]; + byte shortCt[8]; + byte small[4]; + + XMEMSET(&aead, 0, sizeof(aead)); + XMEMSET(out, 0, sizeof(out)); + XMEMSET(shortCt, 0, sizeof(shortCt)); + XMEMSET(small, 0, sizeof(small)); + + /* --- wc_XChaCha20Poly1305_Init: NULL aead. */ + ExpectIntEQ(wc_XChaCha20Poly1305_Init(NULL, aad, sizeof(aad), + nonce, sizeof(nonce), key, sizeof(key), 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- wc_XChaCha20Poly1305_Init: ad == NULL, ad_len == 0 -> success. */ + ExpectIntEQ(wc_XChaCha20Poly1305_Init(&aead, NULL, 0, + nonce, sizeof(nonce), key, sizeof(key), 1), 0); + + /* --- wc_XChaCha20Poly1305_Decrypt: src_len < POLY1305_DIGEST_SIZE. */ + ExpectIntEQ(wc_XChaCha20Poly1305_Decrypt(small, sizeof(small), + shortCt, POLY1305_DIGEST_SIZE - 1, aad, sizeof(aad), + nonce, sizeof(nonce), key, sizeof(key)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- wc_XChaCha20Poly1305_Encrypt: dst_space < dst_len -> BUFFER_E. */ + ExpectIntEQ(wc_XChaCha20Poly1305_Encrypt(out, sizeof(plaintext), + plaintext, sizeof(plaintext), aad, sizeof(aad), + nonce, sizeof(nonce), key, sizeof(key)), + WC_NO_ERR_TRACE(BUFFER_E)); + + /* --- Internal crypt_oneshot helper's length sanity check: + * "(ad_len > WOLFSSL_MAX_32BIT) || (nonce_len > WOLFSSL_MAX_32BIT) || + * (key_len > WOLFSSL_MAX_32BIT)". Only expressible where size_t is + * wider than 32 bits (on a 32-bit CPU no size_t value can exceed + * WOLFSSL_MAX_32BIT, so the decision is a permanent, always-false + * residual there). Each call flips exactly one operand above + * WOLFSSL_MAX_32BIT while the other two stay at their valid, in-range + * sizes; the "all in range" false side is exercised throughout the + * rest of this file's successful Encrypt/Decrypt calls. None of these + * huge lengths are ever dereferenced: the check fires before the ad/ + * nonce/key buffers are touched. */ +#if defined(WC_64BIT_CPU) + ExpectIntEQ(wc_XChaCha20Poly1305_Encrypt(out, sizeof(out), + plaintext, sizeof(plaintext), aad, (size_t)WOLFSSL_MAX_32BIT + 1, + nonce, sizeof(nonce), key, sizeof(key)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XChaCha20Poly1305_Encrypt(out, sizeof(out), + plaintext, sizeof(plaintext), aad, sizeof(aad), + nonce, (size_t)WOLFSSL_MAX_32BIT + 1, key, sizeof(key)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XChaCha20Poly1305_Encrypt(out, sizeof(out), + plaintext, sizeof(plaintext), aad, sizeof(aad), + nonce, sizeof(nonce), key, (size_t)WOLFSSL_MAX_32BIT + 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif /* WC_64BIT_CPU */ +#endif + return EXPECT_RESULT(); +} /* END test_wc_XChaCha20Poly1305_DecisionCoverage */ + +/* + * XChaCha20-Poly1305 large-buffer round trip: forces the internal + * crypt_oneshot helper's 16384-byte chunking ternary + * "(src_len_rem > 16384) ? 16384 : (word32)src_len_rem" true branch (input + * > 16384 bytes -> more than one wc_Chacha_Process/wc_Poly1305Update + * iteration), never hit by any other test in the suite (all use buffers + * well under 16 KB). + */ +int test_wc_XChaCha20Poly1305_LargeBuffer(void) +{ + EXPECT_DECLS; +#if defined(HAVE_POLY1305) && defined(HAVE_XCHACHA) + static const byte key[CHACHA20_POLY1305_AEAD_KEYSIZE] = { + 0x80,0x81,0x82,0x83, 0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x8b, 0x8c,0x8d,0x8e,0x8f, + 0x90,0x91,0x92,0x93, 0x94,0x95,0x96,0x97, + 0x98,0x99,0x9a,0x9b, 0x9c,0x9d,0x9e,0x9f + }; + static const byte nonce[XCHACHA20_POLY1305_AEAD_NONCE_SIZE] = { + 0x40,0x41,0x42,0x43, 0x44,0x45,0x46,0x47, + 0x48,0x49,0x4a,0x4b, 0x4c,0x4d,0x4e,0x4f, + 0x50,0x51,0x52,0x53, 0x54,0x55,0x56,0x57 + }; + static const byte aad[12] = { + 0x50,0x51,0x52,0x53, 0xc0,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7 + }; + #define LARGE_BUF_LEN (16384 + 256) + byte* plain; + byte* ct; + byte* back; + word32 i; + + plain = (byte*)XMALLOC(LARGE_BUF_LEN, NULL, DYNAMIC_TYPE_TMP_BUFFER); + ct = (byte*)XMALLOC(LARGE_BUF_LEN + POLY1305_DIGEST_SIZE, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + back = (byte*)XMALLOC(LARGE_BUF_LEN, NULL, DYNAMIC_TYPE_TMP_BUFFER); + ExpectNotNull(plain); + ExpectNotNull(ct); + ExpectNotNull(back); + + if (plain != NULL && ct != NULL && back != NULL) { + for (i = 0; i < LARGE_BUF_LEN; i++) + plain[i] = (byte)i; + XMEMSET(ct, 0, LARGE_BUF_LEN + POLY1305_DIGEST_SIZE); + XMEMSET(back, 0, LARGE_BUF_LEN); + + ExpectIntEQ(wc_XChaCha20Poly1305_Encrypt(ct, + LARGE_BUF_LEN + POLY1305_DIGEST_SIZE, plain, LARGE_BUF_LEN, + aad, sizeof(aad), nonce, sizeof(nonce), key, sizeof(key)), 0); + ExpectIntEQ(wc_XChaCha20Poly1305_Decrypt(back, LARGE_BUF_LEN, + ct, LARGE_BUF_LEN + POLY1305_DIGEST_SIZE, + aad, sizeof(aad), nonce, sizeof(nonce), key, sizeof(key)), 0); + ExpectBufEQ(back, plain, LARGE_BUF_LEN); + } + + XFREE(plain, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(back, NULL, DYNAMIC_TYPE_TMP_BUFFER); + #undef LARGE_BUF_LEN +#endif + return EXPECT_RESULT(); +} /* END test_wc_XChaCha20Poly1305_LargeBuffer */ diff --git a/tests/api/test_chacha20_poly1305.h b/tests/api/test_chacha20_poly1305.h index 22d3cc6af33..46fb5eee923 100644 --- a/tests/api/test_chacha20_poly1305.h +++ b/tests/api/test_chacha20_poly1305.h @@ -35,6 +35,9 @@ int test_wc_ChaCha20Poly1305_ReinitAfterFinal(void); int test_wc_ChaCha20Poly1305_InPlace(void); int test_wc_ChaCha20Poly1305_UnalignedBuffers(void); int test_wc_ChaCha20Poly1305_CrossCipher(void); +int test_wc_ChaCha20Poly1305_DecisionCoverage(void); +int test_wc_XChaCha20Poly1305_DecisionCoverage(void); +int test_wc_XChaCha20Poly1305_LargeBuffer(void); #define TEST_CHACHA20_POLY1305_DECLS \ TEST_DECL_GROUP("chacha20-poly1305", test_wc_ChaCha20Poly1305_aead), \ @@ -47,6 +50,9 @@ int test_wc_ChaCha20Poly1305_CrossCipher(void); TEST_DECL_GROUP("chacha20-poly1305", test_wc_ChaCha20Poly1305_ReinitAfterFinal), \ TEST_DECL_GROUP("chacha20-poly1305", test_wc_ChaCha20Poly1305_InPlace), \ TEST_DECL_GROUP("chacha20-poly1305", test_wc_ChaCha20Poly1305_UnalignedBuffers), \ - TEST_DECL_GROUP("chacha20-poly1305", test_wc_ChaCha20Poly1305_CrossCipher) + TEST_DECL_GROUP("chacha20-poly1305", test_wc_ChaCha20Poly1305_CrossCipher), \ + TEST_DECL_GROUP("chacha20-poly1305", test_wc_ChaCha20Poly1305_DecisionCoverage), \ + TEST_DECL_GROUP("xchacha20-poly1305", test_wc_XChaCha20Poly1305_DecisionCoverage), \ + TEST_DECL_GROUP("xchacha20-poly1305", test_wc_XChaCha20Poly1305_LargeBuffer) #endif /* WOLFCRYPT_TEST_CHACHA20_POLY1305_H */ diff --git a/tests/api/test_coding.c b/tests/api/test_coding.c new file mode 100644 index 00000000000..a7f06b82356 --- /dev/null +++ b/tests/api/test_coding.c @@ -0,0 +1,470 @@ +/* test_coding.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 + */ + +#include +#include +#include +#include + +/* + * MC/DC DecisionCoverage for wolfcrypt/src/coding.c (Base64 / Base16). + * + * Base64_SkipNewline() is WOLFSSL_LOCAL (hidden visibility): it is NOT called + * directly here (that would break the shared shared-lib CI), it is exercised + * only indirectly via Base64_Decode() on inputs containing embedded + * whitespace / CR / LF, in test_wc_Base64_DecodeWhitespaceCoverage below. + */ + +/* Exercises the argument-check OR, the padding logic, the bad-character and + * short-output-buffer branches of Base64_Decode() (constant-time path) and + * Base64_Decode_nonCT() (table path). */ +int test_wc_Base64_DecodeDecisionCoverage(void) +{ + EXPECT_DECLS; +#if !defined(NO_CODING) && defined(WOLFSSL_BASE64_DECODE) + /* "TWFu" -> "Man" */ + const byte good[] = { 'T', 'W', 'F', 'u' }; + byte out[8]; + word32 outLen; + + /* --- argument-check decision: (in==NULL && inLen>0) || out==NULL || + * outLen==NULL --- independence pair for each operand */ + /* in==NULL, inLen>0 -> true (BAD_FUNC_ARG) */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(NULL, 4, out, &outLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* in!=NULL, inLen>0 -> the (in==NULL && inLen>0) sub-term is false */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(good, (word32)sizeof(good), out, &outLen), 0); + /* in==NULL, inLen==0 -> (in==NULL && inLen>0) false; other operands false */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(NULL, 0, out, &outLen), 0); + /* out==NULL alone -> true */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(good, (word32)sizeof(good), NULL, &outLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* outLen==NULL alone -> true */ + ExpectIntEQ(Base64_Decode(good, (word32)sizeof(good), out, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- valid decode, verify result --- */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(good, (word32)sizeof(good), out, &outLen), 0); + ExpectIntEQ(outLen, 3); + ExpectIntEQ(XMEMCMP(out, "Man", 3), 0); + + /* --- padding decisions --- */ + /* pad3 && pad4 : "TQ==" -> "M" (1 byte) */ + { + const byte p2[] = { 'T', 'Q', '=', '=' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(p2, (word32)sizeof(p2), out, &outLen), 0); + ExpectIntEQ(outLen, 1); + ExpectIntEQ(out[0], 'M'); + } + /* !pad3 && pad4 : "TWE=" -> "Ma" (2 bytes) */ + { + const byte p1[] = { 'T', 'W', 'E', '=' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(p1, (word32)sizeof(p1), out, &outLen), 0); + ExpectIntEQ(outLen, 2); + ExpectIntEQ(XMEMCMP(out, "Ma", 2), 0); + } + /* pad3 && !pad4 : illegal -> ASN_INPUT_E */ + { + const byte bad[] = { 'T', 'W', '=', 'u' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(bad, (word32)sizeof(bad), out, &outLen), + WC_NO_ERR_TRACE(ASN_INPUT_E)); + } + + /* --- bad character (not in alphabet) -> ASN_INPUT_E --- */ + { + const byte bad[] = { 'T', '@', 'F', 'u' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(bad, (word32)sizeof(bad), out, &outLen), + WC_NO_ERR_TRACE(ASN_INPUT_E)); + } + + /* --- output buffer too small -> BUFFER_E --- */ + outLen = 1; + ExpectIntEQ(Base64_Decode(good, (word32)sizeof(good), out, &outLen), + WC_NO_ERR_TRACE(BUFFER_E)); + + /* --- trailing non-whitespace after a full quantum -> ASN_INPUT_E --- */ + { + const byte tr[] = { 'T', 'W', 'F', 'u', '!' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(tr, (word32)sizeof(tr), out, &outLen), + WC_NO_ERR_TRACE(ASN_INPUT_E)); + } + + /* --- table (non constant-time) decode path --- */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode_nonCT(good, (word32)sizeof(good), out, &outLen), + 0); + ExpectIntEQ(outLen, 3); + ExpectIntEQ(XMEMCMP(out, "Man", 3), 0); + + /* --- Base64_Decode_nonCT argument-check decision (own MC/DC, table path + * is a distinct function from the constant-time one above): + * (in==NULL && inLen>0) || out==NULL || outLen==NULL --- */ + /* in==NULL, inLen>0 -> true (BAD_FUNC_ARG) */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode_nonCT(NULL, 4, out, &outLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* in==NULL, inLen==0 -> (in==NULL && inLen>0) false; other operands + * false too -> overall false, succeeds with empty output */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode_nonCT(NULL, 0, out, &outLen), 0); + ExpectIntEQ(outLen, 0); + /* out==NULL alone -> true */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode_nonCT(good, (word32)sizeof(good), NULL, + &outLen), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* outLen==NULL alone -> true */ + ExpectIntEQ(Base64_Decode_nonCT(good, (word32)sizeof(good), out, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- Base64_Decode_nonCT bad-character OR (e1==BAD||e2==BAD||e3==BAD|| + * e4==BAD): each position isolated so its independence pair is shown + * against the all-good decode above. ':' (0x3A) is in the table's + * range but maps to BAD, and unlike '=' it can't be mistaken for + * padding, so it isolates e2/e3/e4 without tripping the pad3/pad4 + * logic. */ + { + const byte badE2[] = { 'T', ':', 'F', 'u' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode_nonCT(badE2, (word32)sizeof(badE2), out, + &outLen), WC_NO_ERR_TRACE(ASN_INPUT_E)); + } + { + const byte badE3[] = { 'T', 'W', ':', 'u' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode_nonCT(badE3, (word32)sizeof(badE3), out, + &outLen), WC_NO_ERR_TRACE(ASN_INPUT_E)); + } + { + const byte badE4[] = { 'T', 'W', 'F', ':' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode_nonCT(badE4, (word32)sizeof(badE4), out, + &outLen), WC_NO_ERR_TRACE(ASN_INPUT_E)); + } +#endif + return EXPECT_RESULT(); +} + +/* Drives Base64_SkipNewline() (indirectly) through Base64_Decode(): embedded + * spaces, LF, CR LF, and the "not enough buffer" / bad-EOL branches. */ +int test_wc_Base64_DecodeWhitespaceCoverage(void) +{ + EXPECT_DECLS; +#if !defined(NO_CODING) && defined(WOLFSSL_BASE64_DECODE) + byte out[16]; + word32 outLen; + + /* LF between quanta: "TWFu\nTWFu" -> "ManMan" */ + { + const byte in[] = { 'T','W','F','u','\n','T','W','F','u' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(in, (word32)sizeof(in), out, &outLen), 0); + ExpectIntEQ(outLen, 6); + ExpectIntEQ(XMEMCMP(out, "ManMan", 6), 0); + } + /* CR LF sequence between quanta */ + { + const byte in[] = { 'T','W','F','u','\r','\n','T','W','F','u' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(in, (word32)sizeof(in), out, &outLen), 0); + ExpectIntEQ(outLen, 6); + ExpectIntEQ(XMEMCMP(out, "ManMan", 6), 0); + } + /* embedded spaces (skipped as whitespace) */ + { + const byte in[] = { 'T','W','F','u',' ',' ','T','W','F','u' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(in, (word32)sizeof(in), out, &outLen), 0); + ExpectIntEQ(outLen, 6); + } + /* CR not followed by LF -> bad end of line -> ASN_INPUT_E */ + { + const byte in[] = { 'T','W','F','u','\r','x','T','u' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(in, (word32)sizeof(in), out, &outLen), + WC_NO_ERR_TRACE(ASN_INPUT_E)); + } + /* trailing whitespace after final quantum is tolerated */ + { + const byte in[] = { 'T','W','F','u','\n' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(in, (word32)sizeof(in), out, &outLen), 0); + ExpectIntEQ(outLen, 3); + } + /* LF followed by a space, followed by more data: exercises the + * "while (len && curChar == ' ')" trailing-space-skip loop in + * Base64_SkipNewline() with BOTH independence pairs in one call -- + * the loop first sees (len>0, curChar==' ') -> true (enters, consumes + * the space) and then, once curChar becomes the next real character + * 'T', sees (len>0, curChar==' ') -> false (exits without a further + * iteration). */ + { + const byte in[] = { 'T','W','F','u','\n',' ','T','W','F','u' }; + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Decode(in, (word32)sizeof(in), out, &outLen), 0); + ExpectIntEQ(outLen, 6); + ExpectIntEQ(XMEMCMP(out, "ManMan", 6), 0); + } +#endif + return EXPECT_RESULT(); +} + +/* Base64_Encode / Base64_EncodeEsc / Base64_Encode_NoNl: + * arg checks, length-only (out==NULL) path, buffer-too-small, escape logic. */ +int test_wc_Base64_EncodeDecisionCoverage(void) +{ + EXPECT_DECLS; +#if !defined(NO_CODING) && defined(WOLFSSL_BASE64_ENCODE) + const byte in[] = { 'M', 'a', 'n' }; + byte out[64]; + word32 outLen; + + /* in==NULL && inLen>0 -> BAD_FUNC_ARG */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Encode(NULL, 3, out, &outLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* outLen==NULL -> BAD_FUNC_ARG */ + ExpectIntEQ(Base64_Encode(in, (word32)sizeof(in), out, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* out==NULL -> length-only query returns LENGTH_ONLY_E and sets outLen */ + outLen = 0; + ExpectIntEQ(Base64_Encode(in, (word32)sizeof(in), NULL, &outLen), + WC_NO_ERR_TRACE(LENGTH_ONLY_E)); + ExpectIntGT(outLen, 0); + + /* output buffer too small -> BAD_FUNC_ARG */ + outLen = 2; + ExpectIntEQ(Base64_Encode(in, (word32)sizeof(in), out, &outLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* valid encode with trailing newline (WC_STD_ENC) */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Encode(in, (word32)sizeof(in), out, &outLen), 0); + ExpectIntGT(outLen, 0); + + /* valid encode without newline (WC_NO_NL_ENC) -> "TWFu" */ + outLen = (word32)sizeof(out); + ExpectIntEQ(Base64_Encode_NoNl(in, (word32)sizeof(in), out, &outLen), 0); + ExpectIntEQ(outLen, 4); + ExpectIntEQ(XMEMCMP(out, "TWFu", 4), 0); + + /* escaped-newline encode (WC_ESC_NL_ENC): a payload long enough to force a + * line break drives the CEscape() '\n' -> "%0A" and '+' / '=' escapes. */ + { + byte big[80]; + byte enc[512]; + word32 i; + for (i = 0; i < (word32)sizeof(big); i++) + big[i] = (byte)(0xF8 + (i & 0x7)); /* yields '+' and '/' indices */ + outLen = (word32)sizeof(enc); + ExpectIntEQ(Base64_EncodeEsc(big, (word32)sizeof(big), enc, &outLen), 0); + ExpectIntGT(outLen, 0); + } + + /* --- DoBase64_Encode() "(in==NULL && inLen>0)" independence: hold + * in==NULL fixed, vary inLen. With inLen==0 the sub-term is false, so + * (unlike the inLen>0 case above) argument validation does not reject + * it; the call proceeds all the way to the final "i != outSz" sanity + * check (see below), which non-obviously trips for an empty, WC_STD_ENC + * encode because a lone trailing '\n' is still appended while the + * pre-computed outSz for zero input is 0. */ + { + byte enc[8]; + outLen = (word32)sizeof(enc); + ExpectIntEQ(Base64_Encode(NULL, 0, enc, &outLen), + WC_NO_ERR_TRACE(ASN_INPUT_E)); + } + /* Same empty-input edge case, but WC_ESC_NL_ENC: the trailing '\n' is + * escaped to the 3-byte "%0A", which happens to match outSz's escaped + * bookkeeping, so this succeeds -- independence pair for the + * "escaped != 1" operand of the final sanity check (holding "i != + * outSz" and "ret == 0" at the same values as the case directly + * above). */ + { + byte enc[8]; + outLen = (word32)sizeof(enc); + ExpectIntEQ(Base64_EncodeEsc(NULL, 0, enc, &outLen), 0); + ExpectIntEQ(outLen, 3); + ExpectIntEQ(XMEMCMP(enc, "%0A", 3), 0); + } + + /* --- exact-multiple-of-line-length input (48 bytes == 16 groups of 3, + * BASE64_LINE_SZ/4 == 16 groups per line): the newline-insertion check + * "escaped != WC_NO_NL_ENC && (++n % (BASE64_LINE_SZ/4)) == 0 && inLen" + * hits the line boundary exactly when there is no input left, so its + * "inLen" operand is false here (no embedded newline is inserted -- + * only the single unconditional trailing one). Pairs with the + * mid-buffer newlines already produced by the "big" EncodeEsc case + * above, where inLen is still nonzero at the line boundary. */ + { + byte in48[48]; + byte enc[128]; + word32 i; + int nlCount = 0; + for (i = 0; i < (word32)sizeof(in48); i++) + in48[i] = (byte)(i + 1); + outLen = (word32)sizeof(enc); + ExpectIntEQ(Base64_Encode(in48, (word32)sizeof(in48), enc, &outLen), + 0); + for (i = 0; i < outLen; i++) { + if (enc[i] == '\n') + nlCount++; + } + /* exactly one (trailing) newline -- none inserted mid-stream */ + ExpectIntEQ(nlCount, 1); + ExpectIntEQ(enc[outLen - 1], '\n'); + } + + /* --- force a BUFFER_E from CEscape() inside the *main* while loop + * (not the tail), so DoBase64_Encode()'s "if (inLen && ret == 0)" + * decision is reached with inLen still nonzero (the failing group's + * 3 bytes were never subtracted from inLen) and ret != 0: the + * independence pair for "ret == 0", holding "inLen" true, against the + * "big" EncodeEsc case above (inLen true, ret == 0 true). Four + * repeats of a 3-byte pattern chosen so every group encodes to + * '+','+','A','+' (three escapes needing 3 bytes each): outLen is + * sized to the *unescaped* precomputed size (which under-counts the + * '+' escape overhead), so the second group's 4th escape overflows + * mid-group. */ + { + const byte plusIn[12] = { 0xFB, 0xE0, 0x3E, 0xFB, 0xE0, 0x3E, + 0xFB, 0xE0, 0x3E, 0xFB, 0xE0, 0x3E }; + byte enc[32]; + outLen = 19; /* == precomputed outSz for 12 bytes, WC_ESC_NL_ENC */ + ExpectIntEQ(Base64_EncodeEsc(plusIn, (word32)sizeof(plusIn), enc, + &outLen), WC_NO_ERR_TRACE(BUFFER_E)); + } +#endif + return EXPECT_RESULT(); +} + +/* Base16_Encode / Base16_Decode: arg checks, single-byte decode, odd-length, + * bad-character, short-buffer, and a round-trip. */ +int test_wc_Base16DecisionCoverage(void) +{ + EXPECT_DECLS; +#if !defined(NO_CODING) && defined(WOLFSSL_BASE16) + const byte raw[] = { 0x0A, 0xB3, 0xFF }; + byte hex[8]; + byte back[8]; + word32 hexLen; + word32 backLen; + + /* --- Base16_Encode arg checks --- */ + hexLen = (word32)sizeof(hex); + ExpectIntEQ(Base16_Encode(NULL, 3, hex, &hexLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + hexLen = (word32)sizeof(hex); + ExpectIntEQ(Base16_Encode(raw, 3, NULL, &hexLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(Base16_Encode(raw, 3, hex, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* output too small (*outLen < 2*inLen) -> BAD_FUNC_ARG */ + hexLen = 2; + ExpectIntEQ(Base16_Encode(raw, 3, hex, &hexLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- valid encode (buffer has room, so a NUL is appended and counted) --- */ + hexLen = (word32)sizeof(hex); + ExpectIntEQ(Base16_Encode(raw, (word32)sizeof(raw), hex, &hexLen), 0); + ExpectIntEQ(hexLen, 7); /* 6 hex chars + appended NUL terminator */ + ExpectIntEQ(XMEMCMP(hex, "0AB3FF", 6), 0); + + /* --- Base16_Decode arg checks --- */ + backLen = (word32)sizeof(back); + ExpectIntEQ(Base16_Decode(NULL, 6, back, &backLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + backLen = (word32)sizeof(back); + ExpectIntEQ(Base16_Decode(hex, 6, NULL, &backLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(Base16_Decode(hex, 6, back, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* odd length (>1) -> BAD_FUNC_ARG */ + backLen = (word32)sizeof(back); + ExpectIntEQ(Base16_Decode(hex, 3, back, &backLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* --- "inLen == 1 && *outLen && in" independence for *outLen: hold + * inLen==1 (true) fixed, force *outLen == 0 (false); the single-byte + * fast path above already showed inLen==1 && *outLen(true) && in + * -> true (decoded 'A' directly), so this closes the *outLen operand. + * Falls through to the "inLen % 2" check, where inLen==1 is odd. */ + { + const byte one[] = { 'A' }; + backLen = 0; + ExpectIntEQ(Base16_Decode(one, 1, back, &backLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + /* output too small -> BUFFER_E */ + backLen = 1; + ExpectIntEQ(Base16_Decode(hex, 6, back, &backLen), + WC_NO_ERR_TRACE(BUFFER_E)); + + /* single-byte decode path: "A" -> 0x0A */ + { + const byte one[] = { 'A' }; + backLen = (word32)sizeof(back); + ExpectIntEQ(Base16_Decode(one, 1, back, &backLen), 0); + ExpectIntEQ(backLen, 1); + ExpectIntEQ(back[0], 0x0A); + } + + /* bad character -> ASN_INPUT_E ('G' is out of the hex alphabet) */ + { + const byte bad[] = { 'G', 'G' }; + backLen = (word32)sizeof(back); + ExpectIntEQ(Base16_Decode(bad, 2, back, &backLen), + WC_NO_ERR_TRACE(ASN_INPUT_E)); + } + + /* --- "b == BAD || b2 == BAD" independence for b2: the case above has + * BOTH nibbles bad (b == BAD already true, masking b2); pair a good + * first nibble with a bad second nibble so only b2's operand drives + * the (still true) decision, against the all-good round-trip decode + * below where both are false. */ + { + const byte bad[] = { '0', 'G' }; + backLen = (word32)sizeof(back); + ExpectIntEQ(Base16_Decode(bad, 2, back, &backLen), + WC_NO_ERR_TRACE(ASN_INPUT_E)); + } + + /* --- round-trip decode of the earlier encode --- */ + backLen = (word32)sizeof(back); + ExpectIntEQ(Base16_Decode(hex, 6, back, &backLen), 0); + ExpectIntEQ(backLen, 3); + ExpectIntEQ(XMEMCMP(back, raw, 3), 0); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_coding.h b/tests/api/test_coding.h new file mode 100644 index 00000000000..b728cfe9806 --- /dev/null +++ b/tests/api/test_coding.h @@ -0,0 +1,38 @@ +/* test_coding.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 + */ + +#ifndef WOLFCRYPT_TEST_CODING_H +#define WOLFCRYPT_TEST_CODING_H + +#include + +int test_wc_Base64_DecodeDecisionCoverage(void); +int test_wc_Base64_DecodeWhitespaceCoverage(void); +int test_wc_Base64_EncodeDecisionCoverage(void); +int test_wc_Base16DecisionCoverage(void); + +#define TEST_CODING_DECLS \ + TEST_DECL_GROUP("coding", test_wc_Base64_DecodeDecisionCoverage), \ + TEST_DECL_GROUP("coding", test_wc_Base64_DecodeWhitespaceCoverage), \ + TEST_DECL_GROUP("coding", test_wc_Base64_EncodeDecisionCoverage), \ + TEST_DECL_GROUP("coding", test_wc_Base16DecisionCoverage) + +#endif /* WOLFCRYPT_TEST_CODING_H */ diff --git a/tests/api/test_curve448.c b/tests/api/test_curve448.c index 2f0f6527013..83f9e48a042 100644 --- a/tests/api/test_curve448.c +++ b/tests/api/test_curve448.c @@ -490,3 +490,322 @@ int test_wc_Curve448PrivateKeyToDer_oneasymkey_version(void) return EXPECT_RESULT(); } +/* + * MC/DC decision coverage for wolfcrypt/src/curve448.c. Split into several + * small functions (rather than one large one) to keep each function's own + * locals small, matching the lesson learned on the ecc.c/curve25519.c MC/DC + * waves (a single large function tripped a stack-corrupting crash under + * -fcoverage-mcdc + -O0). + */ + +/* + * wc_curve448_make_pub argument-check decisions (never called directly by + * the pre-existing tests): the (pub == NULL || priv == NULL) OR and the + * (public_size != PUB || private_size != KEY) OR, each operand independently. + */ +int test_wc_curve448_make_pub_argchecks(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CURVE448) + curve448_key key; + WC_RNG rng; + byte pub[CURVE448_PUB_KEY_SIZE]; + + XMEMSET(&rng, 0, sizeof(WC_RNG)); + ExpectIntEQ(wc_curve448_init(&key), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_curve448_make_key(&rng, CURVE448_KEY_SIZE, &key), 0); + + /* all-false: valid direct call. */ + ExpectIntEQ(wc_curve448_make_pub((int)sizeof(pub), pub, + (int)sizeof(key.k), key.k), 0); + /* pub == NULL || priv == NULL: each operand's TRUE side (the other + * kept valid so the OR does not already short-circuit). */ + ExpectIntEQ(wc_curve448_make_pub((int)sizeof(pub), NULL, + (int)sizeof(key.k), key.k), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + ExpectIntEQ(wc_curve448_make_pub((int)sizeof(pub), pub, + (int)sizeof(key.k), NULL), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* public_size != PUB || private_size != KEY: each operand's TRUE side. */ + ExpectIntEQ(wc_curve448_make_pub((int)sizeof(pub) - 1, pub, + (int)sizeof(key.k), key.k), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + ExpectIntEQ(wc_curve448_make_pub((int)sizeof(pub), pub, + (int)sizeof(key.k) - 1, key.k), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_curve448_free(&key); +#endif + return EXPECT_RESULT(); +} /* END test_wc_curve448_make_pub_argchecks */ + +/* + * wc_curve448_check_public little-endian branch: NULL/size guards, the + * (i == 0 && (pub[0] == 0 || pub[0] == 1)) low-value compound, the + * (i == 28 && pub[28] == 0xff) order rejection, and the + * (i == 28 && pub[28] == 0xfe) -> (i == 0 && pub[0] >= 0xfe) high-value + * compound, plus their false sides. + */ +int test_wc_curve448_check_public_le(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CURVE448) && defined(HAVE_CURVE448_KEY_IMPORT) + byte buf[CURVE448_PUB_KEY_SIZE]; + + /* pub == NULL. */ + ExpectIntEQ(wc_curve448_check_public(NULL, CURVE448_PUB_KEY_SIZE, + EC448_LITTLE_ENDIAN), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* pubSz == 0. */ + ExpectIntEQ(wc_curve448_check_public(buf, 0, EC448_LITTLE_ENDIAN), + WC_NO_ERR_TRACE(BUFFER_E)); + /* pubSz != CURVE448_PUB_KEY_SIZE. */ + ExpectIntEQ(wc_curve448_check_public(buf, CURVE448_PUB_KEY_SIZE - 1, + EC448_LITTLE_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + + /* value == 0: i walks down to 0, pub[0] == 0. */ + XMEMSET(buf, 0, sizeof(buf)); + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_LITTLE_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* value == 1: i walks down to 0, pub[0] == 1. */ + XMEMSET(buf, 0, sizeof(buf)); + buf[0] = 1; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_LITTLE_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* i reaches 0 but pub[0] is neither 0 nor 1: low compound false side, + * and a small value so the order checks also pass -> valid (0). */ + XMEMSET(buf, 0, sizeof(buf)); + buf[0] = 2; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_LITTLE_ENDIAN), 0); + /* order-1 or higher: bytes 55..28 all 0xff -> loop stops at i == 28 with + * pub[28] == 0xff -> reject. */ + XMEMSET(buf, 0xff, sizeof(buf)); + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_LITTLE_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* pub[28] == 0xfe with the remaining low bytes all 0xff and + * pub[0] >= 0xfe -> inner reject fires. */ + XMEMSET(buf, 0xff, sizeof(buf)); + buf[28] = 0xfe; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_LITTLE_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* pub[28] == 0xfe but a lower byte breaks the 0xff run (pub[10] = 0), so + * the inner ">= 0xfe at i == 0" reject does NOT fire: false side -> 0. */ + XMEMSET(buf, 0xff, sizeof(buf)); + buf[28] = 0xfe; + buf[10] = 0x00; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_LITTLE_ENDIAN), 0); + /* i still lands on 28 (bytes 55..29 all 0xff), but pub[28] is neither + * 0xff nor 0xfe: independence pair (false side) for the "pub[28] == + * 0xfe" operand of the high-value compound -> no inner check, valid. */ + XMEMSET(buf, 0xff, sizeof(buf)); + buf[28] = 0x00; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_LITTLE_ENDIAN), 0); + /* pub[28] == 0xfe (enters the inner check, i lands on 0 with the low + * bytes still all 0xff) but pub[0] is below the 0xfe threshold: + * independence pair (false side) for the inner "pub[0] >= 0xfe" + * operand -> valid. */ + XMEMSET(buf, 0xff, sizeof(buf)); + buf[28] = 0xfe; + buf[0] = 0x00; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_LITTLE_ENDIAN), 0); +#endif + return EXPECT_RESULT(); +} /* END test_wc_curve448_check_public_le */ + +/* + * wc_curve448_check_public big-endian branch (the else-side mirror of the + * little-endian decisions above). + */ +int test_wc_curve448_check_public_be(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CURVE448) && defined(HAVE_CURVE448_KEY_IMPORT) + byte buf[CURVE448_PUB_KEY_SIZE]; + + /* value == 0: i walks up to SIZE-1, pub[SIZE-1] == 0. */ + XMEMSET(buf, 0, sizeof(buf)); + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_BIG_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* value == 1: pub[SIZE-1] == 1. */ + XMEMSET(buf, 0, sizeof(buf)); + buf[CURVE448_PUB_KEY_SIZE - 1] = 1; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_BIG_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* i reaches SIZE-1 but value neither 0 nor 1: low compound false -> 0. */ + XMEMSET(buf, 0, sizeof(buf)); + buf[CURVE448_PUB_KEY_SIZE - 1] = 2; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_BIG_ENDIAN), 0); + /* order-1 or higher: bytes 0..27 all 0xff -> loop stops at i == 27 with + * pub[27] == 0xff -> reject. */ + XMEMSET(buf, 0xff, sizeof(buf)); + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_BIG_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* pub[27] == 0xfe, remaining tail all 0xff, pub[SIZE-1] >= 0xfe -> + * inner reject fires. */ + XMEMSET(buf, 0xff, sizeof(buf)); + buf[27] = 0xfe; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_BIG_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* pub[27] == 0xfe but a tail byte breaks the 0xff run (pub[54] = 0): + * inner reject does NOT fire -> 0. */ + XMEMSET(buf, 0xff, sizeof(buf)); + buf[27] = 0xfe; + buf[CURVE448_PUB_KEY_SIZE - 2] = 0x00; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_BIG_ENDIAN), 0); + /* i still lands on 27 (bytes 0..26 all 0xff), but pub[27] is neither + * 0xff nor 0xfe: independence pair (false side) for the "pub[27] == + * 0xfe" operand of the high-value compound -> no inner check, valid. */ + XMEMSET(buf, 0xff, sizeof(buf)); + buf[27] = 0x00; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_BIG_ENDIAN), 0); + /* pub[27] == 0xfe (enters the inner check, i lands on SIZE-1 with the + * tail still all 0xff) but pub[SIZE-1] is below the 0xfe threshold: + * independence pair (false side) for the inner "pub[SIZE-1] >= 0xfe" + * operand -> valid. */ + XMEMSET(buf, 0xff, sizeof(buf)); + buf[27] = 0xfe; + buf[CURVE448_PUB_KEY_SIZE - 1] = 0x00; + ExpectIntEQ(wc_curve448_check_public(buf, sizeof(buf), + EC448_BIG_ENDIAN), 0); +#endif + return EXPECT_RESULT(); +} /* END test_wc_curve448_check_public_be */ + +/* + * wc_curve448_shared_secret_ex populated-key compound decision: + * (!private_key->privSet || !public_key->pubSet), each operand's TRUE side + * individually against otherwise-valid, non-NULL key structs, plus the + * little-endian output branch as the all-false side. + */ +int test_wc_curve448_shared_secret_keyset_checks(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CURVE448) && defined(HAVE_CURVE448_SHARED_SECRET) + curve448_key priv; + curve448_key pub; + curve448_key unset_priv; + curve448_key unset_pub; + WC_RNG rng; + byte out[CURVE448_KEY_SIZE]; + word32 outLen; + + XMEMSET(&rng, 0, sizeof(WC_RNG)); + ExpectIntEQ(wc_curve448_init(&priv), 0); + ExpectIntEQ(wc_curve448_init(&pub), 0); + ExpectIntEQ(wc_curve448_init(&unset_priv), 0); + ExpectIntEQ(wc_curve448_init(&unset_pub), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_curve448_make_key(&rng, CURVE448_KEY_SIZE, &priv), 0); + ExpectIntEQ(wc_curve448_make_key(&rng, CURVE448_KEY_SIZE, &pub), 0); + + /* !privSet TRUE (fresh unset_priv), pub valid. */ + outLen = sizeof(out); + ExpectIntEQ(wc_curve448_shared_secret_ex(&unset_priv, &pub, out, &outLen, + EC448_BIG_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* !pubSet TRUE (fresh unset_pub), priv valid. */ + outLen = sizeof(out); + ExpectIntEQ(wc_curve448_shared_secret_ex(&priv, &unset_pub, out, &outLen, + EC448_BIG_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* all-false: valid call, little-endian output branch. */ + outLen = sizeof(out); + ExpectIntEQ(wc_curve448_shared_secret_ex(&priv, &pub, out, &outLen, + EC448_LITTLE_ENDIAN), 0); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_curve448_free(&priv); + wc_curve448_free(&pub); + wc_curve448_free(&unset_priv); + wc_curve448_free(&unset_pub); +#endif + return EXPECT_RESULT(); +} /* END test_wc_curve448_shared_secret_keyset_checks */ + +/* + * wc_curve448_import_public_ex argument checks: the (key == NULL || in == + * NULL) compound each operand, the inLen size check, and both endian + * branches as the all-false side. + */ +int test_wc_curve448_import_public_ex_argchecks(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CURVE448) && defined(HAVE_CURVE448_KEY_IMPORT) + curve448_key key; + byte in[CURVE448_PUB_KEY_SIZE]; + + XMEMSET(in, 7, sizeof(in)); + ExpectIntEQ(wc_curve448_init(&key), 0); + + /* key == NULL || in == NULL: each operand's TRUE side. */ + ExpectIntEQ(wc_curve448_import_public_ex(in, sizeof(in), NULL, + EC448_LITTLE_ENDIAN), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_curve448_import_public_ex(NULL, sizeof(in), &key, + EC448_LITTLE_ENDIAN), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* inLen != CURVE448_PUB_KEY_SIZE. */ + ExpectIntEQ(wc_curve448_import_public_ex(in, sizeof(in) - 1, &key, + EC448_LITTLE_ENDIAN), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); + /* all-false, both endians (LE XMEMCPY branch + BE byte-reverse loop). */ + ExpectIntEQ(wc_curve448_import_public_ex(in, sizeof(in), &key, + EC448_LITTLE_ENDIAN), 0); + ExpectIntEQ(wc_curve448_import_public_ex(in, sizeof(in), &key, + EC448_BIG_ENDIAN), 0); + ExpectIntEQ(wc_curve448_import_public(in, sizeof(in), &key), 0); + + wc_curve448_free(&key); +#endif + return EXPECT_RESULT(); +} /* END test_wc_curve448_import_public_ex_argchecks */ + +/* + * Little-endian export/import branches (the non-BIG_ENDIAN XMEMCPY sides of + * wc_curve448_export_private_raw_ex/export_public_ex/import_private_ex) and + * the wc_curve448_export_public_ex "!pubSet -> internal make_pub" branch. + */ +int test_wc_curve448_export_import_endian(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CURVE448) && defined(HAVE_CURVE448_KEY_EXPORT) && \ + defined(HAVE_CURVE448_KEY_IMPORT) + curve448_key key; + curve448_key imp; + WC_RNG rng; + byte priv[CURVE448_KEY_SIZE] = {0}; + byte pub[CURVE448_PUB_KEY_SIZE] = {0}; + word32 privSz = sizeof(priv); + word32 pubSz = sizeof(pub); + + XMEMSET(&rng, 0, sizeof(WC_RNG)); + ExpectIntEQ(wc_curve448_init(&key), 0); + ExpectIntEQ(wc_curve448_init(&imp), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_curve448_make_key(&rng, CURVE448_KEY_SIZE, &key), 0); + + /* Little-endian export branches. */ + ExpectIntEQ(wc_curve448_export_private_raw_ex(&key, priv, &privSz, + EC448_LITTLE_ENDIAN), 0); + ExpectIntEQ(wc_curve448_export_public_ex(&key, pub, &pubSz, + EC448_LITTLE_ENDIAN), 0); + /* Little-endian import branch (+ clamp). */ + ExpectIntEQ(wc_curve448_import_private_ex(priv, privSz, &imp, + EC448_LITTLE_ENDIAN), 0); + + /* export_public_ex with !pubSet: a private-only key forces the internal + * wc_curve448_make_pub path that computes and sets key->p. */ + wc_curve448_free(&imp); + ExpectIntEQ(wc_curve448_init(&imp), 0); + ExpectIntEQ(wc_curve448_import_private_ex(priv, privSz, &imp, + EC448_LITTLE_ENDIAN), 0); + pubSz = sizeof(pub); + ExpectIntEQ(wc_curve448_export_public_ex(&imp, pub, &pubSz, + EC448_BIG_ENDIAN), 0); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_curve448_free(&key); + wc_curve448_free(&imp); +#endif + return EXPECT_RESULT(); +} /* END test_wc_curve448_export_import_endian */ + diff --git a/tests/api/test_curve448.h b/tests/api/test_curve448.h index 3ff1b4eed74..830e3ec7751 100644 --- a/tests/api/test_curve448.h +++ b/tests/api/test_curve448.h @@ -36,6 +36,12 @@ int test_wc_curve448_init(void); int test_wc_curve448_size(void); int test_wc_Curve448PrivateKeyToDer(void); int test_wc_Curve448PrivateKeyToDer_oneasymkey_version(void); +int test_wc_curve448_make_pub_argchecks(void); +int test_wc_curve448_check_public_le(void); +int test_wc_curve448_check_public_be(void); +int test_wc_curve448_shared_secret_keyset_checks(void); +int test_wc_curve448_import_public_ex_argchecks(void); +int test_wc_curve448_export_import_endian(void); #define TEST_CURVE448_DECLS \ TEST_DECL_GROUP("curve448", test_wc_curve448_make_key), \ @@ -49,6 +55,12 @@ int test_wc_Curve448PrivateKeyToDer_oneasymkey_version(void); TEST_DECL_GROUP("curve448", test_wc_curve448_init), \ TEST_DECL_GROUP("curve448", test_wc_curve448_size), \ TEST_DECL_GROUP("curve448", test_wc_Curve448PrivateKeyToDer), \ - TEST_DECL_GROUP("curve448", test_wc_Curve448PrivateKeyToDer_oneasymkey_version) + TEST_DECL_GROUP("curve448", test_wc_Curve448PrivateKeyToDer_oneasymkey_version), \ + TEST_DECL_GROUP("curve448", test_wc_curve448_make_pub_argchecks), \ + TEST_DECL_GROUP("curve448", test_wc_curve448_check_public_le), \ + TEST_DECL_GROUP("curve448", test_wc_curve448_check_public_be), \ + TEST_DECL_GROUP("curve448", test_wc_curve448_shared_secret_keyset_checks), \ + TEST_DECL_GROUP("curve448", test_wc_curve448_import_public_ex_argchecks), \ + TEST_DECL_GROUP("curve448", test_wc_curve448_export_import_endian) #endif /* WOLFCRYPT_TEST_CURVE448_H */ diff --git a/tests/api/test_ed448.c b/tests/api/test_ed448.c index 10ce68643a4..2fe404b9c9a 100644 --- a/tests/api/test_ed448.c +++ b/tests/api/test_ed448.c @@ -53,6 +53,21 @@ int test_wc_ed448_make_key(void) ExpectIntEQ(wc_ed448_make_public(&key, pubkey, sizeof(pubkey)), WC_NO_ERR_TRACE(ECC_PRIV_KEY_E)); + + /* MC/DC: wc_ed448_make_public()'s (key == NULL || pubKey == NULL || + * pubKeySz != ED448_PUB_KEY_SIZE) arg check. The call above (valid key, + * valid pubkey, correct size) is the all-FALSE baseline; each call + * below flips exactly one operand TRUE while holding the other two at + * their baseline (FALSE) value, closing all three operands' + * independence pairs. They also give the (ret == 0) FALSE side of the + * (ret == 0) && (!key->privKeySet) check immediately below it. */ + ExpectIntEQ(wc_ed448_make_public(NULL, pubkey, sizeof(pubkey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_ed448_make_public(&key, NULL, sizeof(pubkey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_ed448_make_public(&key, pubkey, sizeof(pubkey) - 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_ed448_make_key(&rng, ED448_KEY_SIZE, &key), 0); /* Test bad args. */ ExpectIntEQ(wc_ed448_make_key(NULL, ED448_KEY_SIZE, &key), @@ -238,6 +253,51 @@ int test_wc_ed448_import_public(void) ExpectIntEQ(wc_ed448_import_public(in, inlen - 1, &pubKey), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* MC/DC: wc_ed448_import_public_ex()'s tri-state length check + * (inLen != PUB_KEY_SIZE && inLen != PUB_KEY_SIZE+1 && + * inLen != 2*PUB_KEY_SIZE+1) -- close the third operand's FALSE side + * (inLen == 115) by falling through with a 115-byte input below, which + * also exercises the compressed-prefix (in[0] == 0x40 && + * inLen > PUB_KEY_SIZE) and uncompressed-prefix (in[0] == 0x04 && + * inLen > 2*PUB_KEY_SIZE) branches that no other test reaches. */ + { + byte compressed[ED448_PUB_KEY_SIZE + 1]; + byte uncompressed[2 * ED448_PUB_KEY_SIZE + 1]; + + /* in[0] == 0x40, inLen (58) > PUB_KEY_SIZE (57): compressed-prefix + * branch TRUE side. */ + compressed[0] = 0x40; + XMEMCPY(compressed + 1, in, ED448_PUB_KEY_SIZE); + ExpectIntEQ(wc_ed448_import_public_ex(compressed, + (word32)sizeof(compressed), &pubKey, 1), 0); + + /* in[0] == 0x40, inLen (57) == PUB_KEY_SIZE: compressed-prefix + * branch's length operand FALSE side -- falls through to the + * "inLen == PUB_KEY_SIZE" plain-copy branch instead. */ + ExpectIntEQ(wc_ed448_import_public_ex(compressed, + ED448_PUB_KEY_SIZE, &pubKey, 1), 0); + + /* in[0] == 0x04, inLen (115 == 2*PUB_KEY_SIZE+1) > 2*PUB_KEY_SIZE: + * uncompressed-prefix branch TRUE side, and the tri-state length + * OR's third operand FALSE side (with the first two held TRUE). + * ge448_compress_key() does not validate that (x, y) is on the + * curve, so arbitrary x/y bytes exercise the branch safely under + * a trusted import. */ + uncompressed[0] = 0x04; + XMEMSET(uncompressed + 1, 0x24, ED448_PUB_KEY_SIZE); /* x */ + XMEMCPY(uncompressed + 1 + ED448_PUB_KEY_SIZE, in, + ED448_PUB_KEY_SIZE); /* y */ + ExpectIntEQ(wc_ed448_import_public_ex(uncompressed, + (word32)sizeof(uncompressed), &pubKey, 1), 0); + + /* in[0] == 0x04, inLen (57) == PUB_KEY_SIZE: uncompressed-prefix + * branch's length operand FALSE side -- also falls through to the + * plain-copy branch (the leading 0x04 becomes part of the + * "compressed" key bytes copied verbatim). */ + ExpectIntEQ(wc_ed448_import_public_ex(uncompressed, + ED448_PUB_KEY_SIZE, &pubKey, 1), 0); + } + DoExpectIntEQ(wc_FreeRng(&rng), 0); wc_ed448_free(&pubKey); #endif @@ -362,6 +422,42 @@ int test_wc_ed448_export(void) WC_NO_ERR_TRACE(BAD_FUNC_ARG)); PRIVATE_KEY_LOCK(); + /* MC/DC: the (ret == 0) && (*outLen < ) BUFFER_E checks in + * wc_ed448_export_public(), wc_ed448_export_private_only() and + * wc_ed448_export_private(). Each pair holds the size operand at a + * fixed too-small value across a NULL-key call (ret == 0 FALSE) and a + * valid-key call (ret == 0 TRUE), closing both operands. */ + { + word32 tinyPubLen = ED448_PUB_KEY_SIZE - 1; + ExpectIntEQ(wc_ed448_export_public(NULL, pub, &tinyPubLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + tinyPubLen = ED448_PUB_KEY_SIZE - 1; + ExpectIntEQ(wc_ed448_export_public(&key, pub, &tinyPubLen), + WC_NO_ERR_TRACE(BUFFER_E)); + ExpectIntEQ(tinyPubLen, ED448_PUB_KEY_SIZE); + } + + PRIVATE_KEY_UNLOCK(); + { + word32 tinyPrivLen = ED448_KEY_SIZE - 1; + ExpectIntEQ(wc_ed448_export_private_only(NULL, priv, &tinyPrivLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + tinyPrivLen = ED448_KEY_SIZE - 1; + ExpectIntEQ(wc_ed448_export_private_only(&key, priv, &tinyPrivLen), + WC_NO_ERR_TRACE(BUFFER_E)); + ExpectIntEQ(tinyPrivLen, ED448_KEY_SIZE); + } + { + word32 tinyBothLen = ED448_PRV_KEY_SIZE - 1; + ExpectIntEQ(wc_ed448_export_private(NULL, priv, &tinyBothLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + tinyBothLen = ED448_PRV_KEY_SIZE - 1; + ExpectIntEQ(wc_ed448_export_private(&key, priv, &tinyBothLen), + WC_NO_ERR_TRACE(BUFFER_E)); + ExpectIntEQ(tinyBothLen, ED448_PRV_KEY_SIZE); + } + PRIVATE_KEY_LOCK(); + #ifdef HAVE_ED448_KEY_IMPORT /* Public-only key: re-init and import just the public part; private * exports must still fail with privKeySet == 0. */ @@ -888,3 +984,388 @@ int test_wc_ed448_reject_small_order_keys(void) return EXPECT_RESULT(); } +/* + * MC/DC decision coverage for wolfcrypt/src/ed448.c decisions the pre-existing + * ed448 API tests never drive: the sign/verify (context == NULL && contextLen + * != 0) compound and its "context != NULL" hash-update branches, the explicit + * wc_ed448_sign_msg_ex/verify_msg_ex type path, and the Ed448ph (prehash) + * sign/verify branches including the (type == Ed448ph && inLen != + * ED448_PREHASH_SIZE) length check. + */ +int test_wc_Ed448DecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ED448) && defined(HAVE_ED448_SIGN) && defined(HAVE_ED448_VERIFY) + ed448_key key; + ed448_key key2; + WC_RNG rng; + byte msg[] = "ed448 decision coverage message"; + byte ctx[] = "ed448-context"; + byte sig[ED448_SIG_SIZE]; + byte hash[ED448_PREHASH_SIZE]; + byte badhash[ED448_PREHASH_SIZE - 1]; + word32 sigLen = sizeof(sig); + word32 msgLen = sizeof(msg); + byte ctxLen = (byte)(sizeof(ctx) - 1); + int verify = 0; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&key2, 0, sizeof(key2)); + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(sig, 0, sizeof(sig)); + XMEMSET(hash, 0x5a, sizeof(hash)); + XMEMSET(badhash, 0x5a, sizeof(badhash)); + + ExpectIntEQ(wc_ed448_init(&key), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_ed448_make_key(&rng, ED448_KEY_SIZE, &key), 0); + + /* MC/DC: the (ret == 0) && (context != NULL) hash-update guards in + * wc_ed448_sign_msg_ex() (both the nonce and the R/S hash phases share + * the same `ret` chain). A freshly-initialized key (pubKeySet == 0) + * with a non-NULL context makes the (ret == 0) operand FALSE while + * holding "context != NULL" at the same TRUE value used by the + * successful sign-with-context call below, closing that operand's + * independence pair without needing to force an internal hash + * failure. */ + ExpectIntEQ(wc_ed448_init(&key2), 0); + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448_sign_msg(msg, msgLen, sig, &sigLen, &key2, ctx, + ctxLen), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + wc_ed448_free(&key2); + + /* Sign/verify with a non-NULL context: exercises the "context != NULL" + * side of the (context == NULL && contextLen != 0) compound plus the + * "context != NULL" hash-update branches in both sign and verify. */ + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448_sign_msg(msg, msgLen, sig, &sigLen, &key, ctx, ctxLen), + 0); + ExpectIntEQ(wc_ed448_verify_msg(sig, sigLen, msg, msgLen, &verify, &key, + ctx, ctxLen), 0); + ExpectIntEQ(verify, 1); + + /* context == NULL && contextLen != 0: compound TRUE -> BAD_FUNC_ARG, in + * both the sign and verify sanity checks. */ + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448_sign_msg(msg, msgLen, sig, &sigLen, &key, NULL, 5), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + verify = 0; + ExpectIntEQ(wc_ed448_verify_msg(sig, sizeof(sig), msg, msgLen, &verify, + &key, NULL, 5), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Explicit wc_ed448_sign_msg_ex/verify_msg_ex with type == Ed448. */ + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448_sign_msg_ex(msg, msgLen, sig, &sigLen, &key, + (byte)Ed448, ctx, ctxLen), 0); + verify = 0; + ExpectIntEQ(wc_ed448_verify_msg_ex(sig, sigLen, msg, msgLen, &verify, &key, + (byte)Ed448, ctx, ctxLen), 0); + ExpectIntEQ(verify, 1); + + /* Ed448ph: type == Ed448ph path through sign_msg_ex (prehash then sign) + * and the matching verify, without and with a context. */ + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448ph_sign_msg(msg, msgLen, sig, &sigLen, &key, NULL, 0), + 0); + verify = 0; + ExpectIntEQ(wc_ed448ph_verify_msg(sig, sigLen, msg, msgLen, &verify, &key, + NULL, 0), 0); + ExpectIntEQ(verify, 1); + + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448ph_sign_msg(msg, msgLen, sig, &sigLen, &key, ctx, + ctxLen), 0); + verify = 0; + ExpectIntEQ(wc_ed448ph_verify_msg(sig, sigLen, msg, msgLen, &verify, &key, + ctx, ctxLen), 0); + ExpectIntEQ(verify, 1); + + /* Ed448ph prehash sign/verify with a correctly sized hash. */ + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448ph_sign_hash(hash, sizeof(hash), sig, &sigLen, &key, + NULL, 0), 0); + verify = 0; + ExpectIntEQ(wc_ed448ph_verify_hash(sig, sigLen, hash, sizeof(hash), &verify, + &key, NULL, 0), 0); + ExpectIntEQ(verify, 1); + + /* type == Ed448ph && inLen != ED448_PREHASH_SIZE -> BAD_LENGTH_E + * (sign_hash forwards hashLen as inLen with type Ed448ph). */ + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448ph_sign_hash(badhash, sizeof(badhash), sig, &sigLen, + &key, NULL, 0), WC_NO_ERR_TRACE(BAD_LENGTH_E)); + + /* MC/DC: wc_ed448_verify_msg_ex()'s (type == Ed448ph && + * msgLen != ED448_PREHASH_SIZE) check, verify side. Paired with the + * regular (non-ph) verify calls above (type == Ed448ph FALSE, msgLen + * != PREHASH_SIZE TRUE) for the type operand, and with the + * correctly-sized Ed448ph verify_hash call above (type == Ed448ph + * TRUE, msgLen != PREHASH_SIZE FALSE) for the length operand. */ + ExpectIntEQ(wc_ed448ph_verify_hash(sig, sigLen, badhash, sizeof(badhash), + &verify, &key, NULL, 0), WC_NO_ERR_TRACE(BAD_LENGTH_E)); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_ed448_free(&key); +#endif + return EXPECT_RESULT(); +} /* END test_wc_Ed448DecisionCoverage */ + +/* + * Feature coverage for the ed448 streaming verify API + * (wc_ed448_verify_msg_init/update/final): positive multi-chunk verification + * (loop true-sides) without and with a context, plus the update NULL-segment + * and init/final NULL argument guards. Guarded identically to the streaming + * verify code under test so it auto-skips where the feature is compiled out. + */ +int test_wc_Ed448FeatureCoverage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ED448) && defined(HAVE_ED448_SIGN) && \ + defined(HAVE_ED448_VERIFY) && defined(WOLFSSL_ED448_STREAMING_VERIFY) + ed448_key key; + WC_RNG rng; + byte msg[] = "streaming multi-chunk ed448 verify message body"; + byte ctx[] = "stream-ctx"; + byte sig[ED448_SIG_SIZE]; + word32 sigLen = sizeof(sig); + word32 msgLen = sizeof(msg); + byte ctxLen = (byte)(sizeof(ctx) - 1); + int verify = 0; + word32 off; + word32 chunk = 7; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(sig, 0, sizeof(sig)); + + ExpectIntEQ(wc_ed448_init(&key), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_ed448_make_key(&rng, ED448_KEY_SIZE, &key), 0); + + /* Sign the whole message (no context), then verify it through the + * streaming init/update(x N)/final API a few bytes at a time. */ + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448_sign_msg(msg, msgLen, sig, &sigLen, &key, NULL, 0), 0); + ExpectIntEQ(wc_ed448_verify_msg_init(sig, sigLen, &key, (byte)Ed448, NULL, + 0), 0); + for (off = 0; off < msgLen; off += chunk) { + word32 n = (msgLen - off < chunk) ? (msgLen - off) : chunk; + ExpectIntEQ(wc_ed448_verify_msg_update(msg + off, n, &key), 0); + } + ExpectIntEQ(wc_ed448_verify_msg_final(sig, sigLen, &verify, &key), 0); + ExpectIntEQ(verify, 1); + + /* Same, but with a non-NULL context supplied at init. */ + sigLen = sizeof(sig); + ExpectIntEQ(wc_ed448_sign_msg(msg, msgLen, sig, &sigLen, &key, ctx, ctxLen), + 0); + ExpectIntEQ(wc_ed448_verify_msg_init(sig, sigLen, &key, (byte)Ed448, ctx, + ctxLen), 0); + for (off = 0; off < msgLen; off += chunk) { + word32 n = (msgLen - off < chunk) ? (msgLen - off) : chunk; + ExpectIntEQ(wc_ed448_verify_msg_update(msg + off, n, &key), 0); + } + verify = 0; + ExpectIntEQ(wc_ed448_verify_msg_final(sig, sigLen, &verify, &key), 0); + ExpectIntEQ(verify, 1); + + /* Negative decisions in the streaming path: init NULL sig, update NULL + * segment (ed448_verify_msg_update_with_sha's msgSegment == NULL guard), + * final NULL res. */ + ExpectIntEQ(wc_ed448_verify_msg_init(NULL, sigLen, &key, (byte)Ed448, NULL, + 0), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_ed448_verify_msg_init(sig, sigLen, &key, (byte)Ed448, NULL, + 0), 0); + ExpectIntEQ(wc_ed448_verify_msg_update(NULL, msgLen, &key), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_ed448_verify_msg_update(msg, msgLen, &key), 0); + /* MC/DC: ed448_verify_msg_final_with_sha()'s sig == NULL operand + * (reachable only through the streaming final() API, since + * wc_ed448_verify_msg_ex() always calls the init step -- which itself + * rejects a NULL sig -- before ever reaching the final step). */ + ExpectIntEQ(wc_ed448_verify_msg_final(NULL, sigLen, &verify, &key), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_ed448_verify_msg_final(sig, sigLen, NULL, &key), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_ed448_free(&key); +#endif + return EXPECT_RESULT(); +} /* END test_wc_Ed448FeatureCoverage */ + +/* + * MC/DC decision coverage for wolfcrypt/src/ed448.c's + * wc_ed448_import_private_only(): the (priv == NULL || key == NULL) arg + * check, the (ret == 0 && privSz != ED448_KEY_SIZE) length check, the + * (ret == 0 && key->pubKeySet) validate-against-public-key branch, and the + * (ret != 0 && key != NULL) error-cleanup guard. No existing test called + * this function at all before this addition. + */ +int test_wc_ed448_import_private_only(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ED448) && defined(HAVE_ED448_KEY_IMPORT) && \ + defined(HAVE_ED448_KEY_EXPORT) + ed448_key key; + ed448_key key2; + WC_RNG rng; + byte priv[ED448_KEY_SIZE]; + byte privOnly[ED448_KEY_SIZE]; + word32 privOnlySz = sizeof(privOnly); + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&key2, 0, sizeof(key2)); + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(priv, 0x11, sizeof(priv)); + + ExpectIntEQ(wc_ed448_init(&key), 0); + ExpectIntEQ(wc_ed448_init(&key2), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_ed448_make_key(&rng, ED448_KEY_SIZE, &key), 0); + PRIVATE_KEY_UNLOCK(); + ExpectIntEQ(wc_ed448_export_private_only(&key, privOnly, &privOnlySz), 0); + PRIVATE_KEY_LOCK(); + + /* Baseline: key2 has neither key set. Valid priv + correct size -> + * success. Gives (ret == 0) TRUE with (privSz != SIZE) FALSE, and + * (key->pubKeySet) FALSE with (ret == 0) TRUE. */ + ExpectIntEQ(wc_ed448_import_private_only(priv, ED448_KEY_SIZE, &key2), 0); + + /* (ret == 0) && (privSz != ED448_KEY_SIZE): TRUE side, holding the + * arg-NULL operands FALSE (priv/key both valid). */ + ExpectIntEQ(wc_ed448_import_private_only(priv, ED448_KEY_SIZE - 1, &key2), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* priv == NULL, with the same too-small privSz held constant across + * the pair: (ret == 0) FALSE here (short-circuited by the arg check) + * vs TRUE above -> closes the (ret == 0) operand of the privSz check. + * Also priv == NULL with key != NULL (held valid across this call and + * the baseline) closes the arg-check OR's priv-operand. */ + ExpectIntEQ(wc_ed448_import_private_only(NULL, ED448_KEY_SIZE - 1, &key2), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* key == NULL, with priv held valid/non-NULL across this call and the + * baseline: closes the arg-check OR's key-operand. Also gives + * (ret != 0) && (key == NULL) for the error-cleanup guard below, which + * must not dereference key. */ + ExpectIntEQ(wc_ed448_import_private_only(priv, ED448_KEY_SIZE, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* (ret == 0) && key->pubKeySet: TRUE side. `key` already has + * pubKeySet == 1 from wc_ed448_make_key() above; re-importing its own + * exported private key recomputes a matching public key, so + * wc_ed448_check_key() succeeds and ret stays 0. */ + ExpectIntEQ(wc_ed448_import_private_only(privOnly, ED448_KEY_SIZE, &key), + 0); + + /* (ret == 0) FALSE with key->pubKeySet held TRUE (same `key`, still + * pubKeySet == 1): a bad privSz trips the length check first, so the + * pubKeySet branch is never reached with ret == 0 -- closes that + * operand's independence pair. This call's (ret != 0) && (key != NULL) + * also closes the error-cleanup guard's independence pairs alongside + * the baseline (ret == 0) and key == NULL (above) calls. */ + ExpectIntEQ(wc_ed448_import_private_only(priv, ED448_KEY_SIZE - 1, &key), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_ed448_free(&key); + wc_ed448_free(&key2); +#endif + return EXPECT_RESULT(); +} /* END test_wc_ed448_import_private_only */ + +/* + * MC/DC decision coverage for wolfcrypt/src/ed448.c's wc_ed448_check_key(): + * the (ret == 0 && !key->pubKeySet) "have a public key" gate, the + * (ret == 0) operand of the (ret == 0 && ed448_is_small_order(...)) defence + * (the is_small_order VALUE operand's independence is already shown by + * test_wc_ed448_reject_small_order_keys()), the (ret == 0 && + * XMEMCMP(...) != 0) recomputed-vs-stored public key mismatch check in the + * have-private-key branch, and the deep Y-range check's final byte compare. + */ +int test_wc_ed448_check_key_decisions(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ED448) && defined(HAVE_ED448_KEY_IMPORT) + ed448_key key; + ed448_key freshKey; + WC_RNG rng; + byte near_p[ED448_PUB_KEY_SIZE]; + int rc; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&freshKey, 0, sizeof(freshKey)); + XMEMSET(&rng, 0, sizeof(rng)); + + /* key == NULL: (ret == 0) FALSE side (short-circuited before any key + * dereference). */ + ExpectIntEQ(wc_ed448_check_key(NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Freshly-initialized key: key != NULL (ret == 0 TRUE) but pubKeySet + * == 0 -> PUBLIC_KEY_E. Closes the pubKeySet operand's TRUE side and, + * paired against the NULL call above, the (ret == 0) operand of this + * same decision. Also gives the (ret == 0) FALSE side of the + * small-order check below it (ret is already PUBLIC_KEY_E by the time + * that line runs, so it short-circuits without touching key->p). */ + ExpectIntEQ(wc_ed448_init(&freshKey), 0); + ExpectIntEQ(wc_ed448_check_key(&freshKey), WC_NO_ERR_TRACE(PUBLIC_KEY_E)); + wc_ed448_free(&freshKey); + + /* Real key pair: pubKeySet == 1 (closes the pubKeySet operand's FALSE + * side), not small order, private key matches public key -> success. + * Gives the (ret == 0) TRUE side of the small-order check (paired + * against the fresh-key call above) and the FALSE side of the + * recomputed-public-key-mismatch compare below. */ + ExpectIntEQ(wc_ed448_init(&key), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_ed448_make_key(&rng, ED448_KEY_SIZE, &key), 0); + ExpectIntEQ(wc_ed448_check_key(&key), 0); + + /* Tamper with the stored public key while keeping the private key: + * wc_ed448_make_public() recomputes the real public key from key->k, + * which will now differ from the corrupted key->p -> XMEMCMP(...) != 0 + * TRUE -> PUBLIC_KEY_E. Closes the mismatch operand's TRUE side. */ + key.p[0] = (byte)(key.p[0] ^ 0xff); + ExpectIntEQ(wc_ed448_check_key(&key), WC_NO_ERR_TRACE(PUBLIC_KEY_E)); + /* Restore so wc_ed448_free()'s zeroize-check doesn't care either way.*/ + key.p[0] = (byte)(key.p[0] ^ 0xff); + + /* Deep Y-range check: a Y value that is not one of + * ed448_is_small_order()'s tabulated points but still forces both + * range-check loops all the way down to the final byte compare (every + * byte except p[0] matches the encoded field prime p). Only reachable + * via a trusted import, which skips wc_ed448_check_key() at import + * time so the crafted (curve-invalid) point can be handed to a + * *direct* wc_ed448_check_key() call below -- same technique as + * test_wc_ed448_reject_small_order_keys(). Whatever the later + * curve-decode step decides is fine; the range-check decision itself + * is what's targeted here. */ + XMEMSET(near_p, 0xff, sizeof(near_p)); + near_p[28] = 0xfe; + near_p[0] = 0x00; + ExpectIntEQ(wc_ed448_init(&freshKey), 0); + ExpectIntEQ(wc_ed448_import_public_ex(near_p, ED448_PUB_KEY_SIZE, + &freshKey, 1), 0); + rc = wc_ed448_check_key(&freshKey); + ExpectTrue((rc == 0) || (rc == WC_NO_ERR_TRACE(PUBLIC_KEY_E))); + wc_ed448_free(&freshKey); + + /* Same construction but with an extra byte (p[1]) perturbed so the + * second range-check loop exits early with ret == 0 before the final + * byte compare runs -- closes that compare's PUBLIC_KEY_E + * guard operand's FALSE side. */ + near_p[1] = 0x00; + ExpectIntEQ(wc_ed448_init(&freshKey), 0); + ExpectIntEQ(wc_ed448_import_public_ex(near_p, ED448_PUB_KEY_SIZE, + &freshKey, 1), 0); + rc = wc_ed448_check_key(&freshKey); + ExpectTrue((rc == 0) || (rc == WC_NO_ERR_TRACE(PUBLIC_KEY_E))); + wc_ed448_free(&freshKey); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_ed448_free(&key); +#endif + return EXPECT_RESULT(); +} /* END test_wc_ed448_check_key_decisions */ + diff --git a/tests/api/test_ed448.h b/tests/api/test_ed448.h index a40edd17bb3..169c88a39ce 100644 --- a/tests/api/test_ed448.h +++ b/tests/api/test_ed448.h @@ -38,6 +38,10 @@ int test_wc_Ed448KeyToDer(void); int test_wc_Ed448PrivateKeyToDer(void); int test_wc_Ed448KeyToDer_oneasymkey_version(void); int test_wc_ed448_reject_small_order_keys(void); +int test_wc_Ed448DecisionCoverage(void); +int test_wc_Ed448FeatureCoverage(void); +int test_wc_ed448_import_private_only(void); +int test_wc_ed448_check_key_decisions(void); #define TEST_ED448_DECLS \ TEST_DECL_GROUP("ed448", test_wc_ed448_make_key), \ @@ -53,6 +57,10 @@ int test_wc_ed448_reject_small_order_keys(void); TEST_DECL_GROUP("ed448", test_wc_Ed448KeyToDer), \ TEST_DECL_GROUP("ed448", test_wc_Ed448PrivateKeyToDer), \ TEST_DECL_GROUP("ed448", test_wc_Ed448KeyToDer_oneasymkey_version), \ - TEST_DECL_GROUP("ed448", test_wc_ed448_reject_small_order_keys) + TEST_DECL_GROUP("ed448", test_wc_ed448_reject_small_order_keys), \ + TEST_DECL_GROUP("ed448", test_wc_Ed448DecisionCoverage), \ + TEST_DECL_GROUP("ed448", test_wc_Ed448FeatureCoverage), \ + TEST_DECL_GROUP("ed448", test_wc_ed448_import_private_only), \ + TEST_DECL_GROUP("ed448", test_wc_ed448_check_key_decisions) #endif /* WOLFCRYPT_TEST_ED448_H */ diff --git a/tests/api/test_error.c b/tests/api/test_error.c new file mode 100644 index 00000000000..622f572b267 --- /dev/null +++ b/tests/api/test_error.c @@ -0,0 +1,85 @@ +/* test_error.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 + */ + +#include +#include +#include +#include + +/* + * MC/DC / decision coverage for wolfcrypt/src/error.c (wc_GetErrorString / + * wc_ErrorString). error.c is a single large switch mapping every + * wolfCrypt_ErrorCodes value to its string; decision coverage means taking + * each case arm plus the default (unknown) arm. We sweep the whole error-code + * numeric span (span 1: -97..-299, span 2: -1000..-1019) so every case is + * taken, and also feed values outside every span so the default arm is taken. + * + * When NO_ERROR_STRINGS is defined the two symbols collapse to macros that + * return / copy a fixed "no support" string; the same calls still compile and + * return non-NULL, so this test is valid in both configurations (that is the + * NO_ERROR_STRINGS compiled-out path the coding-standard asks us to exercise). + */ +int test_wc_GetErrorStringDecisionCoverage(void) +{ + EXPECT_DECLS; + int e; + + /* Sweep span 1 and the gap below it down through span 2, taking every + * defined case arm and, on the undefined values in between, the default + * arm. Every arm returns a non-NULL string. */ + for (e = -1; e >= -1030; e--) { + ExpectNotNull(wc_GetErrorString(e)); + } + + /* Values well outside every span exercise the default arm explicitly. */ + ExpectNotNull(wc_GetErrorString(0)); + ExpectNotNull(wc_GetErrorString(1)); + ExpectNotNull(wc_GetErrorString(-123456)); + ExpectNotNull(wc_GetErrorString(-2000)); + +#ifndef NO_ERROR_STRINGS + /* An unknown code returns the sentinel string from the default arm. */ + ExpectNotNull(wc_GetErrorString(-123456)); + ExpectIntEQ(XSTRNCMP(wc_GetErrorString(-123456), "unknown error number", + 20), 0); + /* A known code returns something other than the unknown sentinel. */ + ExpectIntNE(XSTRNCMP(wc_GetErrorString(WC_NO_ERR_TRACE(BUFFER_E)), + "unknown error number", 20), 0); +#endif + return EXPECT_RESULT(); +} + +/* wc_ErrorString() copies the (truncated) string into the caller buffer. */ +int test_wc_ErrorStringDecisionCoverage(void) +{ + EXPECT_DECLS; + char buffer[WOLFSSL_MAX_ERROR_SZ]; + + XMEMSET(buffer, 0, sizeof(buffer)); + wc_ErrorString(WC_NO_ERR_TRACE(BUFFER_E), buffer); + ExpectIntGT((int)XSTRLEN(buffer), 0); + + XMEMSET(buffer, 0, sizeof(buffer)); + wc_ErrorString(-123456, buffer); + ExpectIntGT((int)XSTRLEN(buffer), 0); + + return EXPECT_RESULT(); +} diff --git a/tests/api/test_error.h b/tests/api/test_error.h new file mode 100644 index 00000000000..b4e8a5dffaa --- /dev/null +++ b/tests/api/test_error.h @@ -0,0 +1,34 @@ +/* test_error.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 + */ + +#ifndef WOLFCRYPT_TEST_ERROR_H +#define WOLFCRYPT_TEST_ERROR_H + +#include + +int test_wc_GetErrorStringDecisionCoverage(void); +int test_wc_ErrorStringDecisionCoverage(void); + +#define TEST_ERROR_DECLS \ + TEST_DECL_GROUP("error", test_wc_GetErrorStringDecisionCoverage), \ + TEST_DECL_GROUP("error", test_wc_ErrorStringDecisionCoverage) + +#endif /* WOLFCRYPT_TEST_ERROR_H */ diff --git a/tests/api/test_hash.c b/tests/api/test_hash.c index 2c24a85f9a9..4ceea2c81a6 100644 --- a/tests/api/test_hash.c +++ b/tests/api/test_hash.c @@ -843,3 +843,295 @@ int test_wc_OidGetHash(void) return EXPECT_RESULT(); } +/* + * MC/DC coverage for wc_HashTypeConvert()'s "current master" mapping arm + * (int hashType -> enum wc_HashType), used only when HAVE_FIPS/HAVE_SELFTEST + * are NOT defined (the legacy FIPSv1/selftest build instead uses a switch + * with its own per-type case labels, a structurally different mechanism): + * "hashType > 0 && hashType <= WC_HASH_TYPE_MAX". Every other caller in this + * suite (indirectly, via password-based-KDF style paths) only ever + * supplies the operand pair's true-true and false-(n/a) sides; this test + * supplies the missing independence pair for the "hashType <= + * WC_HASH_TYPE_MAX" operand by holding "hashType > 0" true while flipping + * the upper-bound check to its false side (an out-of-range positive + * value). + */ +int test_wc_HashTypeConvert(void) +{ + EXPECT_DECLS; +#if !defined(NO_PWDBASED) || !defined(NO_ASN) + /* hashType > 0 (true, held) && hashType <= WC_HASH_TYPE_MAX (false) -> + * eHashType stays at its WC_HASH_TYPE_NONE initializer. */ + enum wc_HashType converted = wc_HashTypeConvert((int)WC_HASH_TYPE_MAX + + 100); + + ExpectIntEQ((int)converted, (int)WC_HASH_TYPE_NONE); +#endif + return EXPECT_RESULT(); +} /* END test_wc_HashTypeConvert */ + +/* + * MC/DC decision coverage for the wc_Hash* dispatcher in wolfcrypt/src/ + * hash.c: argument-check / NULL / type-mismatch / unknown-hashType / + * short-buffer branches, each asserting the specific error the source + * returns for that branch, with the independence pair for every compound + * condition (hash == NULL || (data == NULL && dataSz > 0), etc). + */ +int test_wc_HashDecisionCoverage(void) +{ + EXPECT_DECLS; +#ifndef NO_HASH_WRAPPER + wc_HashAlg hash; + byte digest[WC_MAX_DIGEST_SIZE]; + byte shortBuf[1] = {0}; + + if (supportedHashLen > 0) { + enum wc_HashType type = supportedHash[0]; + + /* wc_HashInit(_ex): hash == NULL (true side). */ + ExpectIntEQ(wc_HashInit(NULL, type), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_HashInit_ex(NULL, type, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* false side: valid struct pointer. */ + ExpectIntEQ(wc_HashInit(&hash, type), 0); + + /* wc_HashUpdate: hash == NULL || (data == NULL && dataSz > 0). */ + /* hash == NULL true; short-circuits regardless of data/dataSz. */ + ExpectIntEQ(wc_HashUpdate(NULL, type, (byte*)"a", 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* hash == NULL false; data == NULL && dataSz > 0 true. */ + ExpectIntEQ(wc_HashUpdate(&hash, type, NULL, 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* independence pair on "dataSz > 0": data == NULL held, dataSz + * flipped to 0 -> AND false -> overall decision false. */ + ExpectIntEQ(wc_HashUpdate(&hash, type, NULL, 0), 0); + /* independence pair on "data == NULL": dataSz > 0 held, data + * flipped to non-NULL -> AND false -> overall decision false. */ + ExpectIntEQ(wc_HashUpdate(&hash, type, (byte*)"a", 1), 0); + + /* wc_HashFinal: hash == NULL || out == NULL. */ + ExpectIntEQ(wc_HashFinal(NULL, type, digest), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_HashFinal(&hash, type, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_HashFinal(&hash, type, digest), 0); + + ExpectIntEQ(wc_HashFree(&hash, type), 0); + + /* wc_HashFree: hash == NULL. */ + ExpectIntEQ(wc_HashFree(NULL, type), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + +#ifdef DEBUG_WOLFSSL + /* hash->type != type mismatch branches (Update/Final/Free), only + * built under DEBUG_WOLFSSL; needs two distinct compiled-in types + * for an independence pair against the matching-type calls above. */ + if (supportedHashLen > 1) { + enum wc_HashType type2 = supportedHash[1]; + + ExpectIntEQ(wc_HashInit(&hash, type), 0); + ExpectIntEQ(wc_HashUpdate(&hash, type2, (byte*)"a", 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_HashFinal(&hash, type2, digest), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_HashFree(&hash, type2), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* clean up with the matching type (false side already shown + * by the un-guarded calls above). */ + ExpectIntEQ(wc_HashFree(&hash, type), 0); + } +#endif /* DEBUG_WOLFSSL */ + + /* wc_Hash/wc_Hash_ex: hash_len < (word32)dig_size (true side, + * BUFFER_E). False side already covered by every supportedHash[] + * call in test_wc_Hash(). */ + ExpectIntEQ(wc_Hash(type, (byte*)"a", 1, shortBuf, 0), + WC_NO_ERR_TRACE(BUFFER_E)); + ExpectIntEQ(wc_Hash_ex(type, (byte*)"a", 1, shortBuf, 0, + HEAP_HINT, INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + } + + /* wc_HashInit_ex/wc_HashUpdate/wc_HashFinal/wc_HashFree: the + * "not supported" case labels (MD5_SHA, MD2, MD4, BLAKE2B, BLAKE2S) + * always fall to HASH_TYPE_E in these four switches, unconditionally + * (no #ifdef inside the case, unlike wc_HashGetDigestSize/BlockSize + * which have real, feature-gated arms for the same types). When the + * corresponding feature macro (WOLFSSL_MD2, HAVE_BLAKE2B/2S) is + * compiled in, notCompiledHash[] above omits these types, so nothing + * else in this file drives these particular case labels; exercise + * them explicitly here so the arm is reached regardless of build. */ + ExpectIntEQ(wc_HashInit(&hash, WC_HASH_TYPE_MD5_SHA), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashInit_ex(&hash, WC_HASH_TYPE_MD2, HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashInit(&hash, WC_HASH_TYPE_MD4), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashInit(&hash, WC_HASH_TYPE_BLAKE2B), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashInit(&hash, WC_HASH_TYPE_BLAKE2S), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + + /* wc_HashUpdate/wc_HashFinal/wc_HashFree reach the "not supported -> + * HASH_TYPE_E" switch arm only when DEBUG_WOLFSSL is off: under + * DEBUG_WOLFSSL each first checks (hash->type != type) and returns + * BAD_FUNC_ARG before the switch. wc_HashInit above legitimately refuses + * these types, so hash->type is never one of them and the debug check + * always intercepts here, making the arm unreachable in DEBUG builds + * (non-DEBUG builds cover it). Not a value workaround -- the arm's + * coverage simply comes from non-DEBUG variants in the campaign union. */ +#ifndef DEBUG_WOLFSSL + ExpectIntEQ(wc_HashUpdate(&hash, WC_HASH_TYPE_MD5_SHA, (byte*)"a", 1), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashUpdate(&hash, WC_HASH_TYPE_BLAKE2B, (byte*)"a", 1), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashUpdate(&hash, WC_HASH_TYPE_BLAKE2S, (byte*)"a", 1), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + + ExpectIntEQ(wc_HashFinal(&hash, WC_HASH_TYPE_MD5_SHA, digest), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashFinal(&hash, WC_HASH_TYPE_BLAKE2B, digest), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashFinal(&hash, WC_HASH_TYPE_BLAKE2S, digest), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + + ExpectIntEQ(wc_HashFree(&hash, WC_HASH_TYPE_MD5_SHA), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashFree(&hash, WC_HASH_TYPE_BLAKE2B), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + ExpectIntEQ(wc_HashFree(&hash, WC_HASH_TYPE_BLAKE2S), + WC_NO_ERR_TRACE(HASH_TYPE_E)); +#endif /* !DEBUG_WOLFSSL */ + + /* wc_Hash/wc_Hash_ex: dig_size < 0 (true side) via an unsupported + * hashType hitting the default/NONE arm of wc_HashGetDigestSize. */ + ExpectIntEQ(wc_Hash(WC_HASH_TYPE_NONE, (byte*)"a", 1, digest, + sizeof(digest)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_Hash_ex(WC_HASH_TYPE_NONE, (byte*)"a", 1, digest, + sizeof(digest), HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_HashGetDigestSize/wc_HashGetBlockSize: default/NONE arm. */ + ExpectIntEQ(wc_HashGetDigestSize(WC_HASH_TYPE_NONE), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_HashGetBlockSize(WC_HASH_TYPE_NONE), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + +#if !defined(NO_ASN) || !defined(NO_DH) || defined(HAVE_ECC) + /* wc_HashGetOID: WC_HASH_TYPE_NONE arm and default (out-of-range) + * arm are two distinct case labels both returning BAD_FUNC_ARG. */ + ExpectIntEQ(wc_HashGetOID(WC_HASH_TYPE_NONE), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_HashGetOID((enum wc_HashType)(WC_HASH_TYPE_MAX + 1)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + +#if !defined(WC_NO_CONSTRUCTORS) + /* wc_HashNew: unsupported hashType -> NULL + BAD_FUNC_ARG result code. + * wc_HashDelete: hash == NULL. */ + { + int ret = 0; + + ExpectNull(wc_HashNew((enum wc_HashType)(WC_HASH_TYPE_MAX + 1), + HEAP_HINT, INVALID_DEVID, &ret)); + ExpectIntEQ(ret, WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_HashDelete(NULL, NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif + +#ifdef WOLFSSL_HASH_FLAGS + /* wc_HashSetFlags/wc_HashGetFlags: hash == NULL. */ + { + word32 flags = 0; + + ExpectIntEQ(wc_HashSetFlags(NULL, WC_HASH_TYPE_NONE, flags), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_HashGetFlags(NULL, WC_HASH_TYPE_NONE, &flags), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif + +#endif /* !NO_HASH_WRAPPER */ + return EXPECT_RESULT(); +} + +/* + * Feature/positive-path coverage for the wc_Hash* dispatcher: drives + * Init/Update/Final/Free, the one-shot wc_Hash/wc_Hash_ex, GetDigestSize/ + * GetBlockSize, GetOID (+ wc_OidGetHash round trip), wc_HashNew/Delete, + * and wc_HashSetFlags/GetFlags across every digest enabled in the current + * build (drives the "true"/enabled side of every case arm's #if guard). + */ +int test_wc_HashFeatureCoverage(void) +{ + EXPECT_DECLS; +#ifndef NO_HASH_WRAPPER + int i; + wc_HashAlg hash; + byte digest[WC_MAX_DIGEST_SIZE]; + + for (i = 0; i < supportedHashLen; i++) { + ExpectIntEQ(wc_HashInit(&hash, supportedHash[i]), 0); + ExpectIntEQ(wc_HashUpdate(&hash, supportedHash[i], (byte*)"abc", 3), + 0); + ExpectIntEQ(wc_HashUpdate(&hash, supportedHash[i], NULL, 0), 0); + ExpectIntEQ(wc_HashFinal(&hash, supportedHash[i], digest), 0); + ExpectIntEQ(wc_HashFree(&hash, supportedHash[i]), 0); + + ExpectIntGT(wc_HashGetDigestSize(supportedHash[i]), 0); + ExpectIntGT(wc_HashGetBlockSize(supportedHash[i]), 0); + + ExpectIntEQ(wc_Hash(supportedHash[i], (byte*)"abc", 3, digest, + sizeof(digest)), 0); + ExpectIntEQ(wc_Hash_ex(supportedHash[i], (byte*)"abc", 3, digest, + sizeof(digest), HEAP_HINT, INVALID_DEVID), 0); + +#if !defined(NO_ASN) || !defined(NO_DH) || defined(HAVE_ECC) + { + int oid = wc_HashGetOID(supportedHash[i]); + ExpectIntGT(oid, 0); + /* wc_OidGetHash() has no case for SHA512_224h/SHA512_256h (no + * OID assigned upstream for those two truncated variants), so + * the round trip only holds for the other digests. */ + if (oid > 0 && supportedHash[i] != WC_HASH_TYPE_SHA512_224 && + supportedHash[i] != WC_HASH_TYPE_SHA512_256) { + enum wc_HashType rtType = wc_OidGetHash(oid); + ExpectIntEQ((int)rtType, (int)supportedHash[i]); + } + } +#endif + } + +#if !defined(WC_NO_CONSTRUCTORS) + /* wc_HashNew/wc_HashDelete positive path for every enabled digest. */ + for (i = 0; i < supportedHashLen; i++) { + int ret = 0; + wc_HashAlg* h = NULL; + + ExpectNotNull(h = wc_HashNew(supportedHash[i], HEAP_HINT, + INVALID_DEVID, &ret)); + ExpectIntEQ(ret, 0); + if (h != NULL) { + ExpectIntEQ(wc_HashUpdate(h, supportedHash[i], (byte*)"abc", 3), + 0); + ExpectIntEQ(wc_HashFinal(h, supportedHash[i], digest), 0); + ExpectIntEQ(wc_HashDelete(h, &h), 0); + ExpectNull(h); + } + } +#endif + +#ifdef WOLFSSL_HASH_FLAGS + /* wc_HashSetFlags/wc_HashGetFlags positive path. */ + for (i = 0; i < supportedHashLen; i++) { + word32 flags = 0; + + ExpectIntEQ(wc_HashInit(&hash, supportedHash[i]), 0); + ExpectIntEQ(wc_HashSetFlags(&hash, supportedHash[i], flags), 0); + ExpectIntEQ(wc_HashGetFlags(&hash, supportedHash[i], &flags), 0); + wc_HashFree(&hash, supportedHash[i]); + } +#endif + +#endif /* !NO_HASH_WRAPPER */ + return EXPECT_RESULT(); +} + diff --git a/tests/api/test_hash.h b/tests/api/test_hash.h index 23646aa4d9f..a687e801c9f 100644 --- a/tests/api/test_hash.h +++ b/tests/api/test_hash.h @@ -36,6 +36,9 @@ int test_wc_HashGetFlags(void); int test_wc_Hash_Algs(void); int test_wc_HashGetOID(void); int test_wc_OidGetHash(void); +int test_wc_HashTypeConvert(void); +int test_wc_HashDecisionCoverage(void); +int test_wc_HashFeatureCoverage(void); #define TEST_HASH_DECLS \ TEST_DECL_GROUP("hash", test_wc_HashInit), \ @@ -49,6 +52,9 @@ int test_wc_OidGetHash(void); TEST_DECL_GROUP("hash", test_wc_HashGetFlags), \ TEST_DECL_GROUP("hash", test_wc_Hash_Algs), \ TEST_DECL_GROUP("hash", test_wc_HashGetOID), \ - TEST_DECL_GROUP("hash", test_wc_OidGetHash) + TEST_DECL_GROUP("hash", test_wc_OidGetHash), \ + TEST_DECL_GROUP("hash", test_wc_HashTypeConvert), \ + TEST_DECL_GROUP("hash", test_wc_HashDecisionCoverage), \ + TEST_DECL_GROUP("hash", test_wc_HashFeatureCoverage) #endif /* WOLFCRYPT_TEST_HASH_H */ diff --git a/tests/api/test_kdf.c b/tests/api/test_kdf.c new file mode 100644 index 00000000000..4eea6a3e1dc --- /dev/null +++ b/tests/api/test_kdf.c @@ -0,0 +1,927 @@ +/* test_kdf.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 + */ + +#include + +#ifdef NO_INLINE + #include +#else + #define WOLFSSL_MISC_INCLUDED + #include +#endif + +#include +#include +#include +#include +#if defined(WC_SRTP_KDF) || defined(HAVE_CMAC_KDF) + #include +#endif +#ifdef HAVE_CMAC_KDF + #include +#endif +#ifdef WOLF_CRYPTO_CB + #include +#endif +#include +#include + +/* The _ex TLS 1.3 HKDF variants (wc_Tls13_HKDF_Extract_ex / + * wc_Tls13_HKDF_Expand_Label_ex) postdate the frozen FIPS/selftest kdf.h and + * are neither declared nor exported in those build contexts, so only exercise + * them outside FIPS/selftest. The thin non-_ex wrappers exist everywhere. */ +#if !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) + #define WOLFSSL_TEST_HKDF_EX +#endif + +/* ------------------------------------------------------------------ */ +/* WOLF_CRYPTO_CB support for wc_KDA_KDF_twostep_cmac's dispatch guard */ +/* ------------------------------------------------------------------ */ +#if defined(HAVE_CMAC_KDF) && defined(WOLF_CRYPTO_CB) +#define TEST_KDF_CRYPTOCB_DEVID 0x4b444630 /* "KDF0" */ + +/* Toggled by the test below: when set, the callback fails outright instead + * of computing the KDF, giving the CRYPTOCB_UNAVAILABLE fall-through guard in + * wc_KDA_KDF_twostep_cmac an independence pair (dispatch-taken-and-fails vs + * dispatch-taken-and-succeeds), both within this one registered devId. */ +static int test_kdf_cryptocb_force_fail = 0; + +static int test_kdf_cryptocb(int cbDevId, wc_CryptoInfo* info, void* ctx) +{ + (void)ctx; + if (cbDevId != TEST_KDF_CRYPTOCB_DEVID) + return WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE); + if (test_kdf_cryptocb_force_fail) + return WC_NO_ERR_TRACE(BAD_FUNC_ARG); + if (info->algo_type == WC_ALGO_TYPE_KDF && + info->kdf.type == WC_KDF_TYPE_TWOSTEP_CMAC) { + /* Compute the real (software) answer via a devId-less call so we + * do not recurse back into this same callback. */ + return wc_KDA_KDF_twostep_cmac(info->kdf.twostep_cmac.salt, + info->kdf.twostep_cmac.saltSz, info->kdf.twostep_cmac.z, + info->kdf.twostep_cmac.zSz, info->kdf.twostep_cmac.fixedInfo, + info->kdf.twostep_cmac.fixedInfoSz, info->kdf.twostep_cmac.out, + info->kdf.twostep_cmac.outSz, NULL, INVALID_DEVID); + } + return WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE); +} +#endif /* HAVE_CMAC_KDF && WOLF_CRYPTO_CB */ + +/* + * MC/DC decision coverage: negative / argument-check / bad-selector / + * short-buffer branches across kdf.c's public API (wc_PRF family, TLS 1.3 + * HKDF label expansion, SSH KDF, SRTP/SRTCP KDF, and the SP 800-56C / + * SP 800-108 KDA KDFs). Each guarded sub-block auto-TEST_SKIPPED's when the + * owning feature is compiled out. + */ +int test_wc_KdfDecisionCoverage(void) +{ + EXPECT_DECLS; + /* FIPS/selftest builds diverge on KDF behavior: wc_HmacSetKey enforces the + * 14-byte HMAC_FIPS_MIN_KEY (returns HMAC_MIN_KEYLEN_E for the short keys + * these probes use), the MD5/TLS1.0 PRF is disallowed, and several argument + * checks differ, so these OPEN-build MC/DC assertions do not hold. kdf.c + * coverage is measured only in non-FIPS campaign variants, so skip the whole + * suite under FIPS/selftest. */ +#if !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) +#ifndef NO_KDF + + /* ---------------------------------------------------------------- */ + /* wc_PRF(): switch(hash) default arm, and times==0 (resLen==0). */ + /* ---------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_PRF) && !defined(NO_HMAC) + { + byte result[64] = {0}; + byte secret[16] = {0}; + byte seed[16] = {0}; + + /* Unknown/unsupported mac id: falls through every enabled case to + * the switch's default arm. */ + ExpectIntEQ(wc_PRF(result, 16, secret, sizeof(secret), seed, + sizeof(seed), -1, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(HASH_TYPE_E)); + +#ifndef NO_SHA256 + /* resLen == 0: times computes to 0 (clang-static-analyzer-safe + * guard), independent of which hash arm matched. */ + ExpectIntEQ(wc_PRF(result, 0, secret, sizeof(secret), seed, + sizeof(seed), sha256_mac, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + } +#endif /* WOLFSSL_HAVE_PRF && !NO_HMAC */ + + /* ---------------------------------------------------------------- */ + /* wc_PRF_TLSv1(): the 3-operand OR buffer-size guard. */ + /* ---------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_PRF) && !defined(NO_HMAC) && !defined(NO_OLD_TLS) && \ + !defined(NO_MD5) && !defined(NO_SHA) + { + /* MAX_PRF_HALF is 260 in the default configuration (no + * HAVE_FFDHE_6144/8192). half = (secLen+1)/2, so secLen=521 gives + * half=261 (>260). */ + static byte secretBig[600] = {0}; + static byte labelBuf[150] = {0}; + static byte seedBuf[150] = {0}; + static byte digestBuf[300] = {0}; + + /* c0 = half > MAX_PRF_HALF: true, others held at trivial values. */ + ExpectIntEQ(wc_PRF_TLSv1(digestBuf, 16, secretBig, 521, labelBuf, 4, + seedBuf, 4, HEAP_HINT, INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + + /* c0 false (small secLen), c1 = labLen+seedLen > MAX_PRF_LABSEED + * (128): true. */ + ExpectIntEQ(wc_PRF_TLSv1(digestBuf, 16, secretBig, 2, labelBuf, 70, + seedBuf, 70, HEAP_HINT, INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + + /* c0, c1 false; c2 = digLen > MAX_PRF_DIG (224): true. */ + ExpectIntEQ(wc_PRF_TLSv1(digestBuf, 225, secretBig, 2, labelBuf, 4, + seedBuf, 4, HEAP_HINT, INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + + /* Baseline: every leaf false, a real PRF computation. */ + ExpectIntEQ(wc_PRF_TLSv1(digestBuf, 16, secretBig, 4, labelBuf, 4, + seedBuf, 4, HEAP_HINT, INVALID_DEVID), 0); + } +#endif /* WOLFSSL_HAVE_PRF && !NO_HMAC && !NO_OLD_TLS && !NO_MD5 && !NO_SHA */ + + /* ---------------------------------------------------------------- */ + /* wc_PRF_TLS(): useAtLeastSha256 branch + its labLen/seedLen guard. */ + /* ---------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_PRF) && !defined(NO_HMAC) + { + static byte secretBuf[16] = {0}; + static byte labelBuf[150] = {0}; + static byte seedBuf[150] = {0}; + static byte digestBuf[64] = {0}; + + /* useAtLeastSha256 true, labLen+seedLen > MAX_PRF_LABSEED (128). */ + ExpectIntEQ(wc_PRF_TLS(digestBuf, 16, secretBuf, sizeof(secretBuf), + labelBuf, 70, seedBuf, 70, 1, sha256_mac, HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + + /* useAtLeastSha256 false: BAD_FUNC_ARG when TLSv1 is compiled out, + * else falls through to wc_PRF_TLSv1() successfully. */ +#ifdef NO_OLD_TLS + ExpectIntEQ(wc_PRF_TLS(digestBuf, 16, secretBuf, sizeof(secretBuf), + labelBuf, 4, seedBuf, 4, 0, sha_mac, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#elif !defined(NO_MD5) && !defined(NO_SHA) + ExpectIntEQ(wc_PRF_TLS(digestBuf, 16, secretBuf, sizeof(secretBuf), + labelBuf, 4, seedBuf, 4, 0, sha_mac, HEAP_HINT, INVALID_DEVID), + 0); +#endif + } +#endif /* WOLFSSL_HAVE_PRF && !NO_HMAC */ + + /* ---------------------------------------------------------------- */ + /* wc_Tls13_HKDF_Extract_ex(): switch(digest) default arm. */ + /* ---------------------------------------------------------------- */ +#if defined(HAVE_HKDF) && !defined(NO_HMAC) && defined(WOLFSSL_TEST_HKDF_EX) + { + byte prk[WC_MAX_DIGEST_SIZE] = {0}; + byte salt[8] = {0}; + byte ikm[WC_MAX_DIGEST_SIZE] = {0}; + + /* WC_MD5 is not one of the switch's supported digest arms + * (SHA256/SHA384/SHA512/SM3 only). */ + ExpectIntEQ(wc_Tls13_HKDF_Extract_ex(prk, salt, sizeof(salt), ikm, 4, + WC_MD5, HEAP_HINT, INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif /* HAVE_HKDF && !NO_HMAC */ + + /* ---------------------------------------------------------------- */ + /* wc_Tls13_HKDF_Expand_Label_ex(): label-buffer size guard. */ + /* ---------------------------------------------------------------- */ +#if defined(HAVE_HKDF) && !defined(NO_HMAC) && !defined(NO_SHA256) && \ + defined(WOLFSSL_TEST_HKDF_EX) + { + byte okm[64] = {0}; + byte prk[WC_SHA256_DIGEST_SIZE] = {0}; + static byte big[64] = {0}; + + /* idx = 4 + protocolLen + labelLen + infoLen > MAX_TLS13_HKDF_LABEL_SZ + * (47 + WC_MAX_DIGEST_SIZE). Using 3*50 = 150 bytes of payload is + * larger than any realistic WC_MAX_DIGEST_SIZE bound. */ + ExpectIntEQ(wc_Tls13_HKDF_Expand_Label_ex(okm, sizeof(okm), prk, + sizeof(prk), big, 50, big, 50, big, 50, WC_SHA256, HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + } +#endif /* HAVE_HKDF && !NO_HMAC && !NO_SHA256 */ + + /* ---------------------------------------------------------------- */ + /* wc_SSH_KDF(): NULL/zero-length arg checks (one leaf at a time), */ + /* wc_HmacSizeByType() rejection, and _HashInit()'s switch default. */ + /* ---------------------------------------------------------------- */ +#if defined(WOLFSSL_WOLFSSH) && !defined(NO_SHA) + { + byte key[64] = {0}; + byte k[8] = {0x01}; + byte h[8] = {0x02}; + byte sessionId[8] = {0x03}; + + /* Each leaf of the 8-operand OR shown true, in isolation, against + * an otherwise-valid call. */ + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'A', NULL, sizeof(key), k, sizeof(k), + h, sizeof(h), sessionId, sizeof(sessionId)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'A', key, 0, k, sizeof(k), h, + sizeof(h), sessionId, sizeof(sessionId)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'A', key, sizeof(key), NULL, + sizeof(k), h, sizeof(h), sessionId, sizeof(sessionId)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'A', key, sizeof(key), k, 0, h, + sizeof(h), sessionId, sizeof(sessionId)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'A', key, sizeof(key), k, sizeof(k), + NULL, sizeof(h), sessionId, sizeof(sessionId)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'A', key, sizeof(key), k, sizeof(k), + h, 0, sessionId, sizeof(sessionId)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'A', key, sizeof(key), k, sizeof(k), + h, sizeof(h), NULL, sizeof(sessionId)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'A', key, sizeof(key), k, sizeof(k), + h, sizeof(h), sessionId, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_HmacSizeByType() itself rejects the hashId (ret <= 0), + * independent of _HashInit()'s own switch below. */ + ExpectIntEQ(wc_SSH_KDF(0xFF, 'A', key, sizeof(key), k, sizeof(k), h, + sizeof(h), sessionId, sizeof(sessionId)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + +#ifdef WOLFSSL_SHA3 + /* wc_HmacSizeByType() accepts WC_SHA3_256, but the SSH KDF's own + * hash union / _HashInit() switch does not implement SHA3: falls + * through to _HashInit()'s default arm. */ + ExpectIntEQ(wc_SSH_KDF(WC_SHA3_256, 'A', key, sizeof(key), k, + sizeof(k), h, sizeof(h), sessionId, sizeof(sessionId)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + } +#endif /* WOLFSSL_WOLFSSH && !NO_SHA */ + + /* ---------------------------------------------------------------- */ + /* wc_SRTP_KDF(): the compound argument-validation OR (last leaf is */ + /* itself an AND: (kdrIdx >= 0) && (idx == NULL)). */ + /* ---------------------------------------------------------------- */ +#ifdef WC_SRTP_KDF + { + byte key[AES_128_KEY_SIZE] = {0}; + byte salt[WC_SRTP_MAX_SALT] = {0}; + byte idx[WC_SRTP_INDEX_LEN] = {0}; + byte key1[16] = {0}; + byte key2[16] = {0}; + byte key3[14] = {0}; + + /* c0 = key == NULL: true. */ + ExpectIntEQ(wc_SRTP_KDF(NULL, sizeof(key), salt, sizeof(salt), -1, + NULL, key1, sizeof(key1), key2, sizeof(key2), key3, sizeof(key3)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* c1 = keySz > AES_256_KEY_SIZE: true. */ + ExpectIntEQ(wc_SRTP_KDF(key, AES_256_KEY_SIZE + 1, salt, + sizeof(salt), -1, NULL, key1, sizeof(key1), key2, sizeof(key2), + key3, sizeof(key3)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* c2 = salt == NULL: true. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), NULL, sizeof(salt), -1, + NULL, key1, sizeof(key1), key2, sizeof(key2), key3, sizeof(key3)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* c3 = saltSz > WC_SRTP_MAX_SALT: true. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, + WC_SRTP_MAX_SALT + 1, -1, NULL, key1, sizeof(key1), key2, + sizeof(key2), key3, sizeof(key3)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* c4 = kdrIdx < -1: true. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), -2, + NULL, key1, sizeof(key1), key2, sizeof(key2), key3, sizeof(key3)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* c5 = kdrIdx > 24: true. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), 25, + idx, key1, sizeof(key1), key2, sizeof(key2), key3, sizeof(key3)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* c6a = kdrIdx >= 0: true; c6b = idx == NULL: true -> AND true. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), 0, + NULL, key1, sizeof(key1), key2, sizeof(key2), key3, sizeof(key3)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* c6a true, c6b false (idx supplied): AND false; every other leaf + * false too -> success. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), 0, + idx, key1, sizeof(key1), key2, sizeof(key2), key3, sizeof(key3)), + 0); + + /* c6a false (kdrIdx == -1) masks c6b regardless of idx: success. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), -1, + NULL, key1, sizeof(key1), key2, sizeof(key2), key3, sizeof(key3)), + 0); + + /* key1/key2/key3 independently NULL-skippable. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), -1, + NULL, NULL, 0, key2, sizeof(key2), key3, sizeof(key3)), 0); + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), -1, + NULL, key1, sizeof(key1), NULL, 0, key3, sizeof(key3)), 0); + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), -1, + NULL, key1, sizeof(key1), key2, sizeof(key2), NULL, 0), 0); + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), -1, + NULL, NULL, 0, NULL, 0, NULL, 0), 0); + } + + /* wc_SRTCP_KDF_ex(): idxLenIndicator's own 3-way switch (its own + * decision, independent of wc_SRTP_KDF's identical arg-validation + * pattern above). */ + { + byte key[AES_128_KEY_SIZE] = {0}; + byte salt[WC_SRTP_MAX_SALT] = {0}; + byte key1[16] = {0}; + + ExpectIntEQ(wc_SRTCP_KDF_ex(key, sizeof(key), salt, sizeof(salt), -1, + NULL, key1, sizeof(key1), NULL, 0, NULL, 0, 2 /* invalid */), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTCP_KDF_ex(key, sizeof(key), salt, sizeof(salt), -1, + NULL, key1, sizeof(key1), NULL, 0, NULL, 0, WC_SRTCP_32BIT_IDX), + 0); + ExpectIntEQ(wc_SRTCP_KDF_ex(key, sizeof(key), salt, sizeof(salt), -1, + NULL, key1, sizeof(key1), NULL, 0, NULL, 0, WC_SRTCP_48BIT_IDX), + 0); + + /* One representative negative leaf in THIS function's own copy of + * the arg-validation decision (key == NULL). */ + ExpectIntEQ(wc_SRTCP_KDF_ex(NULL, sizeof(key), salt, sizeof(salt), + -1, NULL, key1, sizeof(key1), NULL, 0, NULL, 0, + WC_SRTCP_32BIT_IDX), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + /* wc_SRTP_KDF_label() / wc_SRTCP_KDF_label(): same pattern plus an + * outKey == NULL leaf, each in its own function body. */ + { + byte key[AES_128_KEY_SIZE] = {0}; + byte salt[WC_SRTP_MAX_SALT] = {0}; + byte outKey[16] = {0}; + + ExpectIntEQ(wc_SRTP_KDF_label(key, sizeof(key), salt, sizeof(salt), + -1, NULL, WC_SRTP_LABEL_ENCRYPTION, NULL, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTP_KDF_label(key, sizeof(key), salt, sizeof(salt), + -1, NULL, WC_SRTP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), 0); + ExpectIntEQ(wc_SRTP_KDF_label(NULL, sizeof(key), salt, sizeof(salt), + -1, NULL, WC_SRTP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + ExpectIntEQ(wc_SRTCP_KDF_label(key, sizeof(key), salt, sizeof(salt), + -1, NULL, WC_SRTCP_LABEL_ENCRYPTION, NULL, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTCP_KDF_label(key, sizeof(key), salt, sizeof(salt), + -1, NULL, WC_SRTCP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), 0); + ExpectIntEQ(wc_SRTCP_KDF_label(NULL, sizeof(key), salt, sizeof(salt), + -1, NULL, WC_SRTCP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Each remaining leaf of wc_SRTP_KDF_label()'s own copy of the + * compound arg-validation OR, shown true in isolation. */ + ExpectIntEQ(wc_SRTP_KDF_label(key, AES_256_KEY_SIZE + 1, salt, + sizeof(salt), -1, NULL, WC_SRTP_LABEL_ENCRYPTION, outKey, + sizeof(outKey)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTP_KDF_label(key, sizeof(key), NULL, sizeof(salt), + -1, NULL, WC_SRTP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTP_KDF_label(key, sizeof(key), salt, + WC_SRTP_MAX_SALT + 1, -1, NULL, WC_SRTP_LABEL_ENCRYPTION, outKey, + sizeof(outKey)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTP_KDF_label(key, sizeof(key), salt, sizeof(salt), + -2, NULL, WC_SRTP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTP_KDF_label(key, sizeof(key), salt, sizeof(salt), + 25, NULL, WC_SRTP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Same 5 leaves for wc_SRTCP_KDF_label()'s own copy of the same + * decision (a distinct source location from the above). */ + ExpectIntEQ(wc_SRTCP_KDF_label(key, AES_256_KEY_SIZE + 1, salt, + sizeof(salt), -1, NULL, WC_SRTCP_LABEL_ENCRYPTION, outKey, + sizeof(outKey)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTCP_KDF_label(key, sizeof(key), NULL, sizeof(salt), + -1, NULL, WC_SRTCP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTCP_KDF_label(key, sizeof(key), salt, + WC_SRTP_MAX_SALT + 1, -1, NULL, WC_SRTCP_LABEL_ENCRYPTION, outKey, + sizeof(outKey)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTCP_KDF_label(key, sizeof(key), salt, sizeof(salt), + -2, NULL, WC_SRTCP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SRTCP_KDF_label(key, sizeof(key), salt, sizeof(salt), + 25, NULL, WC_SRTCP_LABEL_ENCRYPTION, outKey, sizeof(outKey)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + /* wc_SRTP_KDF_kdr_to_idx(): while(kdr != 0) loop, false-immediately vs + * true-at-least-once-then-false. */ + { + ExpectIntEQ(wc_SRTP_KDF_kdr_to_idx(0), -1); + ExpectIntEQ(wc_SRTP_KDF_kdr_to_idx(1), 0); + ExpectIntEQ(wc_SRTP_KDF_kdr_to_idx(4), 2); + } +#endif /* WC_SRTP_KDF */ + + /* ---------------------------------------------------------------- */ + /* wc_KDA_KDF_onestep(): arg checks + hashType rejection + the */ + /* while/if-tail loop decisions. */ + /* ---------------------------------------------------------------- */ +#if defined(WC_KDF_NIST_SP_800_56C) && !defined(NO_SHA256) + { + byte z[16] = {0}; + byte fixedInfo[8] = {0}; + byte output[128] = {0}; + + /* output == NULL. */ + ExpectIntEQ(wc_KDA_KDF_onestep(z, sizeof(z), fixedInfo, + sizeof(fixedInfo), 16, WC_SHA256, NULL, 16), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* outputSz < derivedSecretSz. */ + ExpectIntEQ(wc_KDA_KDF_onestep(z, sizeof(z), fixedInfo, + sizeof(fixedInfo), 16, WC_SHA256, output, 8), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* z == NULL. */ + ExpectIntEQ(wc_KDA_KDF_onestep(NULL, sizeof(z), fixedInfo, + sizeof(fixedInfo), 16, WC_SHA256, output, sizeof(output)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* zSz == 0. */ + ExpectIntEQ(wc_KDA_KDF_onestep(z, 0, fixedInfo, sizeof(fixedInfo), + 16, WC_SHA256, output, sizeof(output)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* fixedInfoSz > 0 && fixedInfo == NULL. */ + ExpectIntEQ(wc_KDA_KDF_onestep(z, sizeof(z), NULL, 4, 16, WC_SHA256, + output, sizeof(output)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* derivedSecretSz == 0. */ + ExpectIntEQ(wc_KDA_KDF_onestep(z, sizeof(z), fixedInfo, + sizeof(fixedInfo), 0, WC_SHA256, output, sizeof(output)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Bad hashType: wc_HashGetDigestSize() returns <= 0. */ + ExpectIntEQ(wc_KDA_KDF_onestep(z, sizeof(z), fixedInfo, + sizeof(fixedInfo), 16, WC_HASH_TYPE_NONE, output, + sizeof(output)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Baseline: exact multiple of the digest size (loop runs once, + * outIdx reaches derivedSecretSz exactly, tail if-branch false). */ + ExpectIntEQ(wc_KDA_KDF_onestep(z, sizeof(z), fixedInfo, + sizeof(fixedInfo), WC_SHA256_DIGEST_SIZE, WC_SHA256, output, + sizeof(output)), 0); + + /* Not a multiple: loop runs once then the remainder tail runs + * (outIdx < derivedSecretSz -> true). */ + ExpectIntEQ(wc_KDA_KDF_onestep(z, sizeof(z), fixedInfo, + sizeof(fixedInfo), WC_SHA256_DIGEST_SIZE + 5, WC_SHA256, output, + sizeof(output)), 0); + + /* Two full blocks exactly: loop iterates twice (while-condition + * true then false), tail if-branch false again. */ + ExpectIntEQ(wc_KDA_KDF_onestep(z, sizeof(z), fixedInfo, + sizeof(fixedInfo), 2 * WC_SHA256_DIGEST_SIZE, WC_SHA256, output, + sizeof(output)), 0); + } +#endif /* WC_KDF_NIST_SP_800_56C && !NO_SHA256 */ + + /* ---------------------------------------------------------------- */ + /* wc_KDA_KDF_PRF_cmac() / wc_KDA_KDF_twostep_cmac(): arg checks + */ + /* the while(len_rem >= BLOCK)/tail-if loop decisions. */ + /* ---------------------------------------------------------------- */ +#if defined(HAVE_CMAC_KDF) && defined(WOLFSSL_AES_128) + { + byte kin[AES_128_KEY_SIZE] = {0}; + byte fixedInfo[8] = {0}; + byte kout[64] = {0}; + + /* Kin == NULL. */ + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(NULL, sizeof(kin), fixedInfo, + sizeof(fixedInfo), kout, 16, WC_CMAC_AES, HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Kout == NULL. */ + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(kin, sizeof(kin), fixedInfo, + sizeof(fixedInfo), NULL, 16, WC_CMAC_AES, HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* fixedInfoSz > 0 && fixedInfo == NULL. */ + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(kin, sizeof(kin), NULL, 4, kout, 16, + WC_CMAC_AES, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* KoutSz == 0. */ + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(kin, sizeof(kin), fixedInfo, + sizeof(fixedInfo), kout, 0, WC_CMAC_AES, HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* type != WC_CMAC_AES (only supported PRF type). */ + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(kin, sizeof(kin), fixedInfo, + sizeof(fixedInfo), kout, 16, (CmacType)0, HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* KoutSz < WC_AES_BLOCK_SIZE: main loop body never runs, tail + * if(len_rem) true. */ + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(kin, sizeof(kin), fixedInfo, + sizeof(fixedInfo), kout, 8, WC_CMAC_AES, HEAP_HINT, + INVALID_DEVID), 0); + + /* KoutSz not a multiple of WC_AES_BLOCK_SIZE: loop runs once, tail + * if(len_rem) true. */ + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(kin, sizeof(kin), fixedInfo, + sizeof(fixedInfo), kout, 20, WC_CMAC_AES, HEAP_HINT, + INVALID_DEVID), 0); + + /* KoutSz exact multiple: loop runs twice, tail if(len_rem) false. */ + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(kin, sizeof(kin), fixedInfo, + sizeof(fixedInfo), kout, 32, WC_CMAC_AES, HEAP_HINT, + INVALID_DEVID), 0); + + /* Loop body's own "ret == 0 && fixedInfoSz > 0" guard: fixedInfoSz + * operand false (fixedInfoSz == 0), ret == 0 held true, with a + * KoutSz spanning one full block so the loop body actually runs. */ + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(kin, sizeof(kin), NULL, 0, kout, 16, + WC_CMAC_AES, HEAP_HINT, INVALID_DEVID), 0); + + /* Same guard's ret == 0 operand false: an invalid Kin length (not + * 16/24/32) makes wc_InitCmac_ex() fail on the first loop + * iteration, forcing ret != 0 by the time this guard is reached + * (fixedInfoSz > 0 held true, as in the baseline calls above). */ + { + byte kinBad[AES_128_KEY_SIZE + 4] = {0}; + ExpectIntEQ(wc_KDA_KDF_PRF_cmac(kinBad, sizeof(kinBad), fixedInfo, + sizeof(fixedInfo), kout, 16, WC_CMAC_AES, HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + } + + { + byte salt[AES_128_KEY_SIZE] = {0}; + byte z[16] = {0}; + byte fixedInfo[8] = {0}; + byte output[32] = {0}; + + /* Bad salt_len: switch default arm. */ + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(salt, 15, z, sizeof(z), + fixedInfo, sizeof(fixedInfo), output, sizeof(output), HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* zSz == 0. */ + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(salt, sizeof(salt), z, 0, + fixedInfo, sizeof(fixedInfo), output, sizeof(output), HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* outputSz == 0. */ + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(salt, sizeof(salt), z, + sizeof(z), fixedInfo, sizeof(fixedInfo), output, 0, HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* fixedInfoSz > 0 && fixedInfo == NULL. */ + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(salt, sizeof(salt), z, + sizeof(z), NULL, 4, output, sizeof(output), HEAP_HINT, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* salt == NULL. */ + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(NULL, sizeof(salt), z, + sizeof(z), fixedInfo, sizeof(fixedInfo), output, sizeof(output), + HEAP_HINT, INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* z == NULL. */ + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(salt, sizeof(salt), NULL, + sizeof(z), fixedInfo, sizeof(fixedInfo), output, sizeof(output), + HEAP_HINT, INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* output == NULL. */ + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(salt, sizeof(salt), z, + sizeof(z), fixedInfo, sizeof(fixedInfo), NULL, sizeof(output), + HEAP_HINT, INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Baseline: valid two-step derivation, dispatch not taken + * (devId == INVALID_DEVID). */ + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(salt, sizeof(salt), z, + sizeof(z), fixedInfo, sizeof(fixedInfo), output, sizeof(output), + HEAP_HINT, INVALID_DEVID), 0); + +#if defined(WOLF_CRYPTO_CB) + /* devId != INVALID_DEVID: dispatch taken. Independence pair for + * the CRYPTOCB_UNAVAILABLE fall-through guard: succeeds, then fails + * outright, both via the SAME registered devId. */ + ExpectIntEQ(wc_CryptoCb_RegisterDevice(TEST_KDF_CRYPTOCB_DEVID, + test_kdf_cryptocb, NULL), 0); + + test_kdf_cryptocb_force_fail = 0; + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(salt, sizeof(salt), z, + sizeof(z), fixedInfo, sizeof(fixedInfo), output, sizeof(output), + HEAP_HINT, TEST_KDF_CRYPTOCB_DEVID), 0); + + test_kdf_cryptocb_force_fail = 1; + ExpectIntEQ(wc_KDA_KDF_twostep_cmac(salt, sizeof(salt), z, + sizeof(z), fixedInfo, sizeof(fixedInfo), output, sizeof(output), + HEAP_HINT, TEST_KDF_CRYPTOCB_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + test_kdf_cryptocb_force_fail = 0; + + wc_CryptoCb_UnRegisterDevice(TEST_KDF_CRYPTOCB_DEVID); +#endif /* WOLF_CRYPTO_CB */ + } +#endif /* HAVE_CMAC_KDF && WOLFSSL_AES_128 */ + +#endif /* !NO_KDF */ +#endif /* !HAVE_FIPS && !HAVE_SELFTEST */ + return EXPECT_RESULT(); +} /* END test_wc_KdfDecisionCoverage */ + +/* + * MC/DC feature coverage: positive multi-hash / multi-chunk paths driving + * true-side loop and switch-arm coverage for kdf.c's public API. + */ +int test_wc_KdfFeatureCoverage(void) +{ + EXPECT_DECLS; + /* See test_wc_KdfDecisionCoverage: FIPS/selftest builds diverge on KDF + * behavior (14-byte minimum HMAC key, MD5/TLS1.0 PRF disallowed, differing + * argument checks), so skip this OPEN-build positive-path suite there; + * kdf.c is measured only in non-FIPS campaign variants. */ +#if !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) +#ifndef NO_KDF + + /* wc_PRF() across every enabled hash arm, and both the + * exact-multiple-of-digest-length and partial-last-block sides of + * "(i != lastTime) || !lastLen". */ +#if defined(WOLFSSL_HAVE_PRF) && !defined(NO_HMAC) + { + byte secret[32] = {0}; + byte seed[16] = {0}; + byte result[256] = {0}; + +#ifndef NO_MD5 + ExpectIntEQ(wc_PRF(result, WC_MD5_DIGEST_SIZE, secret, + sizeof(secret), seed, sizeof(seed), md5_mac, HEAP_HINT, + INVALID_DEVID), 0); +#endif +#ifndef NO_SHA + ExpectIntEQ(wc_PRF(result, WC_SHA_DIGEST_SIZE - 4, secret, + sizeof(secret), seed, sizeof(seed), sha_mac, HEAP_HINT, + INVALID_DEVID), 0); +#endif +#ifndef NO_SHA256 + /* Exact multiple over two blocks: lastLen == 0 on the final + * iteration. */ + ExpectIntEQ(wc_PRF(result, 2 * WC_SHA256_DIGEST_SIZE, secret, + sizeof(secret), seed, sizeof(seed), sha256_mac, HEAP_HINT, + INVALID_DEVID), 0); + /* Multi-block with a partial tail: lastLen != 0 on the final + * iteration. */ + ExpectIntEQ(wc_PRF(result, 2 * WC_SHA256_DIGEST_SIZE + 4, secret, + sizeof(secret), seed, sizeof(seed), sha256_mac, HEAP_HINT, + INVALID_DEVID), 0); +#endif +#ifdef WOLFSSL_SHA384 + ExpectIntEQ(wc_PRF(result, WC_SHA384_DIGEST_SIZE, secret, + sizeof(secret), seed, sizeof(seed), sha384_mac, HEAP_HINT, + INVALID_DEVID), 0); +#endif +#ifdef WOLFSSL_SHA512 + ExpectIntEQ(wc_PRF(result, WC_SHA512_DIGEST_SIZE, secret, + sizeof(secret), seed, sizeof(seed), sha512_mac, HEAP_HINT, + INVALID_DEVID), 0); +#endif + } +#endif /* WOLFSSL_HAVE_PRF && !NO_HMAC */ + + /* wc_PRF_TLS(): drive the hash_type override guard's 3 MC/DC leaves + * (TF/FT/FF) plus both sides of useAtLeastSha256. */ +#if defined(WOLFSSL_HAVE_PRF) && !defined(NO_HMAC) && !defined(NO_SHA256) + { + byte secret[16] = {0}; + byte label[8] = {0}; + byte seed[8] = {0}; + byte digest[64] = {0}; + +#ifndef NO_SHA + /* c0 (hash_type < sha256_mac) true: forced up to sha256_mac. */ + ExpectIntEQ(wc_PRF_TLS(digest, WC_SHA256_DIGEST_SIZE, secret, + sizeof(secret), label, sizeof(label), seed, sizeof(seed), 1, + sha_mac, HEAP_HINT, INVALID_DEVID), 0); +#endif + /* c0 false, c1 (hash_type == blake2b_mac) true: forced up. */ + ExpectIntEQ(wc_PRF_TLS(digest, WC_SHA256_DIGEST_SIZE, secret, + sizeof(secret), label, sizeof(label), seed, sizeof(seed), 1, + blake2b_mac, HEAP_HINT, INVALID_DEVID), 0); + /* c0, c1 both false: hash_type left as-is (already sha256_mac). */ + ExpectIntEQ(wc_PRF_TLS(digest, WC_SHA256_DIGEST_SIZE, secret, + sizeof(secret), label, sizeof(label), seed, sizeof(seed), 1, + sha256_mac, HEAP_HINT, INVALID_DEVID), 0); +#ifdef WOLFSSL_SHA384 + /* c0, c1 both false with a hash_type genuinely above sha256_mac: + * no override, sha384_mac used directly. */ + ExpectIntEQ(wc_PRF_TLS(digest, WC_SHA384_DIGEST_SIZE, secret, + sizeof(secret), label, sizeof(label), seed, sizeof(seed), 1, + sha384_mac, HEAP_HINT, INVALID_DEVID), 0); +#endif + +#if !defined(NO_OLD_TLS) && !defined(NO_MD5) && !defined(NO_SHA) + /* useAtLeastSha256 == 0: routed to wc_PRF_TLSv1(). */ + ExpectIntEQ(wc_PRF_TLS(digest, 16, secret, sizeof(secret), label, + sizeof(label), seed, sizeof(seed), 0, sha_mac, HEAP_HINT, + INVALID_DEVID), 0); +#endif + } +#endif /* WOLFSSL_HAVE_PRF && !NO_HMAC && !NO_SHA256 */ + + /* wc_Tls13_HKDF_Extract()/_ex(): ikmLen == 0 zero-fill branch vs a + * caller-supplied IKM, across each supported digest, plus the thin + * non-_ex wrapper. */ +#if defined(HAVE_HKDF) && !defined(NO_HMAC) + { + byte prk[WC_MAX_DIGEST_SIZE] = {0}; + byte salt[8] = {0}; + byte ikm[WC_MAX_DIGEST_SIZE] = {0}; + +#ifndef NO_SHA256 + ikm[0] = 0x11; +#ifdef WOLFSSL_TEST_HKDF_EX + /* ikmLen == 0: internal zero-fill branch (ikm ignored when len 0). */ + ExpectIntEQ(wc_Tls13_HKDF_Extract_ex(prk, salt, sizeof(salt), ikm, 0, + WC_SHA256, HEAP_HINT, INVALID_DEVID), 0); + /* ikmLen != 0: caller-supplied IKM used as-is. */ + ExpectIntEQ(wc_Tls13_HKDF_Extract_ex(prk, salt, sizeof(salt), ikm, + WC_SHA256_DIGEST_SIZE, WC_SHA256, HEAP_HINT, INVALID_DEVID), 0); +#endif + /* Thin (non-_ex) wrapper. */ + ExpectIntEQ(wc_Tls13_HKDF_Extract(prk, salt, sizeof(salt), ikm, + WC_SHA256_DIGEST_SIZE, WC_SHA256), 0); +#endif +#if defined(WOLFSSL_SHA384) && defined(WOLFSSL_TEST_HKDF_EX) + ExpectIntEQ(wc_Tls13_HKDF_Extract_ex(prk, salt, sizeof(salt), ikm, 0, + WC_SHA384, HEAP_HINT, INVALID_DEVID), 0); +#endif +#if defined(WOLFSSL_TLS13_SHA512) && defined(WOLFSSL_SHA512) && \ + defined(WOLFSSL_TEST_HKDF_EX) + ExpectIntEQ(wc_Tls13_HKDF_Extract_ex(prk, salt, sizeof(salt), ikm, 0, + WC_SHA512, HEAP_HINT, INVALID_DEVID), 0); +#endif + } +#endif /* HAVE_HKDF && !NO_HMAC */ + + /* wc_Tls13_HKDF_Expand_Label()/_ex(): protocolLen/labelLen/infoLen + * each independently zero vs non-zero, across each supported digest, + * plus the thin non-_ex wrapper. */ +#if defined(HAVE_HKDF) && !defined(NO_HMAC) && !defined(NO_SHA256) + { + byte okm[32] = {0}; + byte prk[WC_SHA256_DIGEST_SIZE] = {0}; + byte protocol[8] = { 't','l','s','1','3',' ',0,0 }; + byte label[8] = { 'l','a','b','e','l',0,0,0 }; + byte info[8] = {0}; + +#ifdef WOLFSSL_TEST_HKDF_EX + /* All three zero. */ + ExpectIntEQ(wc_Tls13_HKDF_Expand_Label_ex(okm, sizeof(okm), prk, + sizeof(prk), protocol, 0, label, 0, info, 0, WC_SHA256, + HEAP_HINT, INVALID_DEVID), 0); + /* All three non-zero. */ + ExpectIntEQ(wc_Tls13_HKDF_Expand_Label_ex(okm, sizeof(okm), prk, + sizeof(prk), protocol, 6, label, 5, info, 4, WC_SHA256, + HEAP_HINT, INVALID_DEVID), 0); +#endif + /* Thin (non-_ex) wrapper. */ + ExpectIntEQ(wc_Tls13_HKDF_Expand_Label(okm, sizeof(okm), prk, + sizeof(prk), protocol, 6, label, 5, info, 4, WC_SHA256), 0); + +#if defined(WOLFSSL_SHA384) && defined(WOLFSSL_TEST_HKDF_EX) + ExpectIntEQ(wc_Tls13_HKDF_Expand_Label_ex(okm, sizeof(okm), prk, + sizeof(prk), protocol, 6, label, 5, info, 4, WC_SHA384, + HEAP_HINT, INVALID_DEVID), 0); +#endif + } +#endif /* HAVE_HKDF && !NO_HMAC && !NO_SHA256 */ + + /* wc_Tls13_HKDF_Expand_Label_Alloc(): only compiled with + * WOLFSSL_TICKET_NONCE_MALLOC (its own dedicated variant). */ +#if defined(HAVE_HKDF) && !defined(NO_HMAC) && !defined(NO_SHA256) && \ + defined(WOLFSSL_TICKET_NONCE_MALLOC) && \ + (!defined(HAVE_FIPS) || (defined(FIPS_VERSION_GE) && \ + FIPS_VERSION_GE(5,3))) + { + byte okm[32] = {0}; + byte prk[WC_SHA256_DIGEST_SIZE] = {0}; + byte protocol[6] = { 't','l','s','1','3',' ' }; + byte label[5] = { 'l','a','b','e','l' }; + byte info[4] = {0}; + + ExpectIntEQ(wc_Tls13_HKDF_Expand_Label_Alloc(okm, sizeof(okm), prk, + sizeof(prk), protocol, sizeof(protocol), label, sizeof(label), + info, sizeof(info), WC_SHA256, HEAP_HINT), 0); + } +#endif + + /* wc_SSH_KDF() across each supported hashId and several keyIds, + * driving blocks==0 vs blocks>0, the tail remainder branch, and the + * kPad (top-bit-of-k[0]) branch. */ +#ifdef WOLFSSL_WOLFSSH + { + byte k[8] = {0x01}; + byte kPadded[8] = {0}; /* top bit set: exercises kPad path */ + byte h[8] = {0x02}; + byte sessionId[8] = {0x03}; + byte key[160] = {0}; + + kPadded[0] = 0x80; + +#ifndef NO_SHA + /* digestSz(SHA1)=20: keySz=10 -> blocks==0, remainder>0. */ + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'A', key, 10, k, sizeof(k), h, + sizeof(h), sessionId, sizeof(sessionId)), 0); + /* keySz==20 -> blocks==1 (loop body skipped), remainder==0. */ + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'B', key, 20, k, sizeof(k), h, + sizeof(h), sessionId, sizeof(sessionId)), 0); + /* keySz==43 -> blocks==2 (loop runs once), remainder>0, with kPad + * (k[0] top bit set) true. */ + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'C', key, 43, kPadded, sizeof(kPadded), + h, sizeof(h), sessionId, sizeof(sessionId)), 0); + /* keySz==40 -> blocks==2, remainder==0. */ + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'D', key, 40, k, sizeof(k), h, + sizeof(h), sessionId, sizeof(sessionId)), 0); + /* keySz==43 -> blocks==2 (loop runs once), remainder>0, with kPad + * (k[0] top bit clear) false: independence pair for the + * remainder-tail's "ret == 0 && kPad" guard's kPad operand, + * against the 'C' case above (same ret == 0, kPad true). */ + ExpectIntEQ(wc_SSH_KDF(WC_SHA, 'E', key, 43, k, sizeof(k), h, + sizeof(h), sessionId, sizeof(sessionId)), 0); +#endif +#ifndef NO_SHA256 + ExpectIntEQ(wc_SSH_KDF(WC_SHA256, 'A', key, 16, k, sizeof(k), h, + sizeof(h), sessionId, sizeof(sessionId)), 0); +#endif +#ifdef WOLFSSL_SHA384 + ExpectIntEQ(wc_SSH_KDF(WC_SHA384, 'A', key, 16, k, sizeof(k), h, + sizeof(h), sessionId, sizeof(sessionId)), 0); +#endif +#ifdef WOLFSSL_SHA512 + ExpectIntEQ(wc_SSH_KDF(WC_SHA512, 'A', key, 16, k, sizeof(k), h, + sizeof(h), sessionId, sizeof(sessionId)), 0); +#endif + } +#endif /* WOLFSSL_WOLFSSH */ + + /* wc_SRTP_KDF()/wc_SRTCP_KDF() happy paths, including kdrIdx >= 0 with + * a real index (bit-shift XOR branch) and kdrIdx == -1 (no XOR). */ +#ifdef WC_SRTP_KDF + { + byte key[AES_128_KEY_SIZE] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 }; + byte salt[WC_SRTP_MAX_SALT] = {0}; + byte idx[WC_SRTP_INDEX_LEN] = { 0, 0, 0, 0, 0, 1 }; + byte key1[16] = {0}; + byte key2[20] = {0}; + byte key3[14] = {0}; + + /* kdrIdx == -1: no XOR of idx (idx may be NULL). */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), -1, + NULL, key1, sizeof(key1), key2, sizeof(key2), key3, + sizeof(key3)), 0); + /* kdrIdx a multiple of 8 (bits == 0): plain byte-aligned XOR. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), 8, idx, + key1, sizeof(key1), key2, sizeof(key2), key3, sizeof(key3)), 0); + /* kdrIdx not a multiple of 8: bit-shifted XOR path. */ + ExpectIntEQ(wc_SRTP_KDF(key, sizeof(key), salt, sizeof(salt), 3, idx, + key1, sizeof(key1), key2, sizeof(key2), key3, sizeof(key3)), 0); + + ExpectIntEQ(wc_SRTCP_KDF(key, sizeof(key), salt, sizeof(salt), -1, + NULL, key1, sizeof(key1), key2, sizeof(key2), key3, + sizeof(key3)), 0); + } +#endif /* WC_SRTP_KDF */ + +#endif /* !NO_KDF */ +#endif /* !HAVE_FIPS && !HAVE_SELFTEST */ + return EXPECT_RESULT(); +} /* END test_wc_KdfFeatureCoverage */ diff --git a/tests/api/test_kdf.h b/tests/api/test_kdf.h new file mode 100644 index 00000000000..3adf4ce0b6f --- /dev/null +++ b/tests/api/test_kdf.h @@ -0,0 +1,34 @@ +/* test_kdf.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 + */ + +#ifndef WOLFCRYPT_TEST_KDF_H +#define WOLFCRYPT_TEST_KDF_H + +#include + +int test_wc_KdfDecisionCoverage(void); +int test_wc_KdfFeatureCoverage(void); + +#define TEST_KDF_DECLS \ + TEST_DECL_GROUP("kdf", test_wc_KdfDecisionCoverage), \ + TEST_DECL_GROUP("kdf", test_wc_KdfFeatureCoverage) + +#endif /* WOLFCRYPT_TEST_KDF_H */ diff --git a/tests/api/test_lms_xmss.c b/tests/api/test_lms_xmss.c index 870d2765e23..91f1a457b2f 100644 --- a/tests/api/test_lms_xmss.c +++ b/tests/api/test_lms_xmss.c @@ -1309,3 +1309,505 @@ int test_rfc9802_xmss_x509_gen(void) #endif return EXPECT_RESULT(); } + +/*----------------------------------------------------------------------------*/ +/* MC/DC decision + feature coverage: LMS */ +/* */ +/* Per-argument negative (decision) tests and positive multi-sign/verify */ +/* (feature) tests that drive the public wc_LmsKey_* API and, through it, the */ +/* wc_lms.c / wc_lms_impl.c decisions. Both are additive to the group above. */ +/*----------------------------------------------------------------------------*/ + +#if defined(WOLFSSL_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) +/* In-memory private-key persistence callbacks (filesystem-independent) so the + * feature roundtrip works in any LMS-signing build. Guarded identically to the + * only caller (test_wc_LmsFeatureCoverage) to avoid an unused-function warning + * under -Werror in verify-only builds. */ +static byte lms_mc_priv[8192]; +static word32 lms_mc_privSz = 0; + +static int lms_mc_write_key(const byte* priv, word32 privSz, void* context) +{ + (void)context; + if (privSz > (word32)sizeof(lms_mc_priv)) + return -1; + XMEMCPY(lms_mc_priv, priv, privSz); + lms_mc_privSz = privSz; + return WC_LMS_RC_SAVED_TO_NV_MEMORY; +} + +static int lms_mc_read_key(byte* priv, word32 privSz, void* context) +{ + (void)context; + if (privSz != lms_mc_privSz) + return -1; + XMEMCPY(priv, lms_mc_priv, privSz); + return WC_LMS_RC_READ_TO_MEMORY; +} + +/* One hash-family roundtrip: parameter-select via SetParameters_ex(hash), + * keygen, a short multi-sign/verify loop (drives the treehash / auth-path + * loop true-sides), the GetSigLen / SigsLeft / short-buffer decisions, then + * free. Returns TEST_SUCCESS / TEST_FAIL. */ +static int lms_mc_family_roundtrip(WC_RNG* rng, int hash) +{ + EXPECT_DECLS; + LmsKey key; + byte msg[] = "lms feature-coverage message"; + byte sig[8192]; + word32 sigSz; + word32 sigLen = 0; + int i; + + XMEMSET(&key, 0, sizeof(key)); + lms_mc_privSz = 0; + + ExpectIntEQ(wc_LmsKey_Init(&key, NULL, INVALID_DEVID), 0); + /* L1/H5/W8 exists in every compiled-in hash family and keygen is fast. */ + ExpectIntEQ(wc_LmsKey_SetParameters_ex(&key, 1, 5, 8, hash), 0); + ExpectIntEQ(wc_LmsKey_SetWriteCb(&key, lms_mc_write_key), 0); + ExpectIntEQ(wc_LmsKey_SetReadCb(&key, lms_mc_read_key), 0); + ExpectIntEQ(wc_LmsKey_SetContext(&key, (void*)lms_mc_priv), 0); + ExpectIntEQ(wc_LmsKey_MakeKey(&key, rng), 0); + + ExpectIntEQ(wc_LmsKey_GetSigLen(&key, &sigLen), 0); + ExpectIntGT((int)sigLen, 0); + ExpectIntLE((int)sigLen, (int)sizeof(sig)); + ExpectIntGT(wc_LmsKey_SigsLeft(&key), 0); + + /* Short-buffer decision: *sigSz < sig_len -> BUFFER_E. */ + sigSz = 1; + ExpectIntEQ(wc_LmsKey_Sign(&key, sig, &sigSz, msg, sizeof(msg)), + WC_NO_ERR_TRACE(BUFFER_E)); + + for (i = 0; i < 3; i++) { + sigSz = sizeof(sig); + ExpectIntEQ(wc_LmsKey_Sign(&key, sig, &sigSz, msg, sizeof(msg)), 0); + ExpectIntEQ(wc_LmsKey_Verify(&key, sig, sigSz, msg, sizeof(msg)), 0); + /* Negative verify: flip a signature byte -> must not verify. */ + sig[sigSz - 1] ^= 0x01; + ExpectIntNE(wc_LmsKey_Verify(&key, sig, sigSz, msg, sizeof(msg)), 0); + sig[sigSz - 1] ^= 0x01; + } + + wc_LmsKey_Free(&key); + return EXPECT_RESULT(); +} +#endif /* WOLFSSL_HAVE_LMS && !WOLFSSL_LMS_VERIFY_ONLY */ + +int test_wc_LmsDecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_LMS) + LmsKey key; + word32 sigLen = 0; + int levels = 0; + int height = 0; + int width = 0; + + XMEMSET(&key, 0, sizeof(key)); + + /* wc_LmsKey_Init: NULL key operand. */ + ExpectIntEQ(wc_LmsKey_Init(NULL, NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + ExpectIntEQ(wc_LmsKey_Init(&key, NULL, INVALID_DEVID), 0); + + /* wc_LmsKey_SetParameters: unknown (levels,height,winternitz) triple -> + * table search exhausts and returns BAD_FUNC_ARG. Independence: vary one + * coordinate at a time away from the known-good L1/H5/W8 set (the others + * held at valid values), plus the NULL-key operand. */ + ExpectIntEQ(wc_LmsKey_SetParameters(&key, 99, 5, 8), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_SetParameters(&key, 1, 99, 8), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_SetParameters(&key, 1, 5, 99), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_SetParameters(NULL, 1, 5, 8), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_LmsKey_SetParameters_ex: BAD_STATE_E once params are already set + * (state != INITED). Valid set first, then a second call must fail on the + * state operand, not the table search. */ + ExpectIntEQ(wc_LmsKey_SetParameters(&key, 1, 5, 8), 0); + ExpectIntEQ(wc_LmsKey_SetParameters_ex(&key, 1, 5, 8, LMS_SHA256), + WC_NO_ERR_TRACE(BAD_STATE_E)); + + /* wc_LmsKey_GetParameters: each NULL out operand, then success. */ + ExpectIntEQ(wc_LmsKey_GetParameters(&key, NULL, &height, &width), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_GetParameters(&key, &levels, NULL, &width), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_GetParameters(&key, &levels, &height, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_GetParameters(&key, &levels, &height, &width), 0); + + /* wc_LmsKey_GetSigLen: NULL len operand (params set), then a params==NULL + * key -> BAD_FUNC_ARG on the params operand. */ + ExpectIntEQ(wc_LmsKey_GetSigLen(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_GetSigLen(&key, &sigLen), 0); + ExpectIntGT((int)sigLen, 0); + { + LmsKey bare; + XMEMSET(&bare, 0, sizeof(bare)); + ExpectIntEQ(wc_LmsKey_Init(&bare, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_LmsKey_GetSigLen(&bare, &sigLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + wc_LmsKey_Free(&bare); + } + + /* wc_LmsKey_Verify: each NULL operand and msgSz < 0. The arg checks fire + * before any state/crypto, so the params-set (non-verifiable) key is fine. + * Independence: exactly one operand invalid per call. */ + { + byte vsig[4] = {0}; + byte vmsg[4] = {0}; + ExpectIntEQ(wc_LmsKey_Verify(NULL, vsig, sizeof(vsig), vmsg, + sizeof(vmsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_Verify(&key, NULL, sizeof(vsig), vmsg, + sizeof(vmsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_Verify(&key, vsig, sizeof(vsig), NULL, + sizeof(vmsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_Verify(&key, vsig, sizeof(vsig), vmsg, -1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + +#ifndef WOLFSSL_LMS_VERIFY_ONLY + /* wc_LmsKey_SetWriteCb: NULL key vs NULL callback (independence pair). */ + ExpectIntEQ(wc_LmsKey_SetWriteCb(NULL, lms_mc_write_key), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_SetWriteCb(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_LmsKey_MakeKey: NULL key vs NULL rng (independence pair). Both fail on + * the arg check before any allocation. */ + ExpectIntEQ(wc_LmsKey_MakeKey(NULL, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_MakeKey(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_LmsKey_Sign: each NULL operand + msgSz < 0, all before crypto. */ + { + byte ssig[4] = {0}; + word32 ssigSz = sizeof(ssig); + byte smsg[4] = {0}; + ExpectIntEQ(wc_LmsKey_Sign(NULL, ssig, &ssigSz, smsg, sizeof(smsg)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_Sign(&key, NULL, &ssigSz, smsg, sizeof(smsg)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_Sign(&key, ssig, NULL, smsg, sizeof(smsg)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_Sign(&key, ssig, &ssigSz, NULL, sizeof(smsg)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_LmsKey_Sign(&key, ssig, &ssigSz, smsg, -1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif /* !WOLFSSL_LMS_VERIFY_ONLY */ + + wc_LmsKey_Free(&key); +#endif /* WOLFSSL_HAVE_LMS */ + return EXPECT_RESULT(); +} + +int test_wc_LmsFeatureCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) + WC_RNG rng; + + XMEMSET(&rng, 0, sizeof(rng)); + ExpectIntEQ(wc_InitRng(&rng), 0); + + /* Default SHA-256/256 family is compiled into every LMS build. */ + ExpectIntEQ(lms_mc_family_roundtrip(&rng, LMS_SHA256), TEST_SUCCESS); + +#ifdef WOLFSSL_LMS_SHA256_192 + /* Truncated 192-bit SHA-256 family: exercises the wc_lms_*sha256_192* + * hash helpers and the LMS_SHA256_192 map entries. */ + ExpectIntEQ(lms_mc_family_roundtrip(&rng, LMS_SHA256_192), TEST_SUCCESS); +#endif +#ifdef WOLFSSL_LMS_SHAKE256 + /* SHAKE256 family: exercises the wc_lms_shake256_* hash helpers. */ + ExpectIntEQ(lms_mc_family_roundtrip(&rng, LMS_SHAKE256), TEST_SUCCESS); +#endif + + /* Multi-level HSS (L2): drives the wc_hss_* subtree init / auth-path / + * presign helpers and the level>1 branches that a single-level key skips. + * Guarded on the compiled-in level bound. */ +#if !defined(WOLFSSL_LMS_MAX_LEVELS) || (WOLFSSL_LMS_MAX_LEVELS >= 2) + { + LmsKey key; + byte msg[] = "lms hss L2 message"; + byte sig[8192]; + word32 sigSz; + int i; + + XMEMSET(&key, 0, sizeof(key)); + lms_mc_privSz = 0; + ExpectIntEQ(wc_LmsKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_LmsKey_SetParameters(&key, 2, 5, 8), 0); + ExpectIntEQ(wc_LmsKey_SetWriteCb(&key, lms_mc_write_key), 0); + ExpectIntEQ(wc_LmsKey_SetReadCb(&key, lms_mc_read_key), 0); + ExpectIntEQ(wc_LmsKey_SetContext(&key, (void*)lms_mc_priv), 0); + ExpectIntEQ(wc_LmsKey_MakeKey(&key, &rng), 0); + for (i = 0; i < 2; i++) { + sigSz = sizeof(sig); + ExpectIntEQ(wc_LmsKey_Sign(&key, sig, &sigSz, msg, sizeof(msg)), 0); + ExpectIntEQ(wc_LmsKey_Verify(&key, sig, sigSz, msg, sizeof(msg)), 0); + } + wc_LmsKey_Free(&key); + } +#endif + + wc_FreeRng(&rng); +#endif /* WOLFSSL_HAVE_LMS && !WOLFSSL_LMS_VERIFY_ONLY */ + return EXPECT_RESULT(); +} + +/*----------------------------------------------------------------------------*/ +/* MC/DC decision + feature coverage: XMSS */ +/*----------------------------------------------------------------------------*/ + +#if defined(WOLFSSL_HAVE_XMSS) && !defined(WOLFSSL_XMSS_VERIFY_ONLY) +/* In-memory XMSS private-key persistence (filesystem-independent). Sized for + * the tall XMSS^MT parameter set used below; skipped at run time if a key's + * private length ever exceeds it. Guarded identically to its only caller. */ +static byte xmss_mc_priv[262144]; +static word32 xmss_mc_privSz = 0; + +static enum wc_XmssRc xmss_mc_write_key(const byte* priv, word32 privSz, + void* context) +{ + (void)context; + if (privSz > (word32)sizeof(xmss_mc_priv)) + return WC_XMSS_RC_WRITE_FAIL; + XMEMCPY(xmss_mc_priv, priv, privSz); + xmss_mc_privSz = privSz; + return WC_XMSS_RC_SAVED_TO_NV_MEMORY; +} + +static enum wc_XmssRc xmss_mc_read_key(byte* priv, word32 privSz, void* context) +{ + (void)context; + if (privSz != xmss_mc_privSz) + return WC_XMSS_RC_READ_FAIL; + XMEMCPY(priv, xmss_mc_priv, privSz); + return WC_XMSS_RC_READ_TO_MEMORY; +} + +/* One parameter-set roundtrip: init, param-string select, keygen, a short + * multi-sign/verify loop, GetSigLen / GetPubLen / SigsLeft and the + * short-buffer decisions, then free. */ +static int xmss_mc_param_roundtrip(WC_RNG* rng, const char* paramStr) +{ + EXPECT_DECLS; + XmssKey key; + byte msg[] = "xmss feature-coverage message"; + byte* sig = NULL; + word32 sigSz; + word32 sigLen = 0; + word32 pubLen = 0; + word32 privLen = 0; + int i; + + XMEMSET(&key, 0, sizeof(key)); + xmss_mc_privSz = 0; + + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_SetParamStr(&key, paramStr), 0); + ExpectIntEQ(wc_XmssKey_SetWriteCb(&key, xmss_mc_write_key), 0); + ExpectIntEQ(wc_XmssKey_SetReadCb(&key, xmss_mc_read_key), 0); + ExpectIntEQ(wc_XmssKey_SetContext(&key, (void*)xmss_mc_priv), 0); + + ExpectIntEQ(wc_XmssKey_GetPrivLen(&key, &privLen), 0); + /* Skip cleanly (no failure) if this param set's secret key is larger than + * the in-memory scratch buffer. */ + if (EXPECT_SUCCESS() && privLen > (word32)sizeof(xmss_mc_priv)) { + wc_XmssKey_Free(&key); + return TEST_SUCCESS; + } + + ExpectIntEQ(wc_XmssKey_MakeKey(&key, rng), 0); + ExpectIntEQ(wc_XmssKey_GetPubLen(&key, &pubLen), 0); + ExpectIntGT((int)pubLen, 0); + ExpectIntEQ(wc_XmssKey_GetSigLen(&key, &sigLen), 0); + ExpectIntGT((int)sigLen, 0); + ExpectIntGT(wc_XmssKey_SigsLeft(&key), 0); + + ExpectNotNull(sig = (byte*)XMALLOC(sigLen, NULL, DYNAMIC_TYPE_TMP_BUFFER)); + + /* Short-buffer decision: *sigLen < sig_len -> BUFFER_E. */ + if (sig != NULL) { + sigSz = 1; + ExpectIntEQ(wc_XmssKey_Sign(&key, sig, &sigSz, msg, (int)sizeof(msg)), + WC_NO_ERR_TRACE(BUFFER_E)); + } + + for (i = 0; sig != NULL && i < 2; i++) { + sigSz = sigLen; + ExpectIntEQ(wc_XmssKey_Sign(&key, sig, &sigSz, msg, (int)sizeof(msg)), + 0); + ExpectIntEQ(wc_XmssKey_Verify(&key, sig, sigSz, msg, (int)sizeof(msg)), + 0); + /* Negative verify: flip a signature byte. */ + sig[sigSz - 1] ^= 0x01; + ExpectIntNE(wc_XmssKey_Verify(&key, sig, sigSz, msg, (int)sizeof(msg)), + 0); + sig[sigSz - 1] ^= 0x01; + } + + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wc_XmssKey_Free(&key); + return EXPECT_RESULT(); +} +#endif /* WOLFSSL_HAVE_XMSS && !WOLFSSL_XMSS_VERIFY_ONLY */ + +int test_wc_XmssDecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_XMSS) + XmssKey key; + word32 len = 0; + + XMEMSET(&key, 0, sizeof(key)); + + /* wc_XmssKey_Init: NULL key operand. */ + ExpectIntEQ(wc_XmssKey_Init(NULL, NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + + /* wc_XmssKey_SetParamStr: NULL key vs NULL str (independence pair), then + * an unknown parameter string -> BAD_FUNC_ARG from the lookup. */ + ExpectIntEQ(wc_XmssKey_SetParamStr(NULL, "XMSS-SHA2_10_256"), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_SetParamStr(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_SetParamStr(&key, "XMSS-NOT-A-REAL-PARAM"), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_XmssKey_SetParamStr: BAD_STATE_E once params are set (state != + * INITED). Valid set first, then a second call fails on the state operand. + * Guard on the compiled-in minimum height (H10 must be available). */ +#if !defined(WOLFSSL_XMSS_MIN_HEIGHT) || (WOLFSSL_XMSS_MIN_HEIGHT <= 10) + ExpectIntEQ(wc_XmssKey_SetParamStr(&key, "XMSS-SHA2_10_256"), 0); + ExpectIntEQ(wc_XmssKey_SetParamStr(&key, "XMSS-SHA2_10_256"), + WC_NO_ERR_TRACE(BAD_STATE_E)); + + /* wc_XmssKey_GetPubLen / GetSigLen: NULL len operand (params set). */ + ExpectIntEQ(wc_XmssKey_GetPubLen(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_GetPubLen(&key, &len), 0); + ExpectIntGT((int)len, 0); + ExpectIntEQ(wc_XmssKey_GetSigLen(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_GetSigLen(&key, &len), 0); + ExpectIntGT((int)len, 0); +#endif + + /* wc_XmssKey_Verify: each NULL operand and mLen <= 0 (arg check before any + * state/crypto). Independence: exactly one operand invalid per call. */ + { + byte vsig[4] = {0}; + byte vmsg[4] = {0}; + ExpectIntEQ(wc_XmssKey_Verify(NULL, vsig, sizeof(vsig), vmsg, + (int)sizeof(vmsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_Verify(&key, NULL, sizeof(vsig), vmsg, + (int)sizeof(vmsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_Verify(&key, vsig, sizeof(vsig), NULL, + (int)sizeof(vmsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_Verify(&key, vsig, sizeof(vsig), vmsg, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + +#ifndef WOLFSSL_XMSS_VERIFY_ONLY + /* wc_XmssKey_SetWriteCb: NULL key vs NULL callback (independence pair). */ + ExpectIntEQ(wc_XmssKey_SetWriteCb(NULL, xmss_mc_write_key), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_SetWriteCb(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_XmssKey_MakeKey: NULL key vs NULL rng (independence pair). */ + ExpectIntEQ(wc_XmssKey_MakeKey(NULL, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_MakeKey(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_XmssKey_Sign: each NULL operand + mLen <= 0, all before crypto. */ + { + byte ssig[4] = {0}; + word32 ssigSz = sizeof(ssig); + byte smsg[4] = {0}; + ExpectIntEQ(wc_XmssKey_Sign(NULL, ssig, &ssigSz, smsg, + (int)sizeof(smsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_Sign(&key, NULL, &ssigSz, smsg, + (int)sizeof(smsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_Sign(&key, ssig, NULL, smsg, + (int)sizeof(smsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_Sign(&key, ssig, &ssigSz, NULL, + (int)sizeof(smsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_Sign(&key, ssig, &ssigSz, smsg, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif /* !WOLFSSL_XMSS_VERIFY_ONLY */ + + wc_XmssKey_Free(&key); +#endif /* WOLFSSL_HAVE_XMSS */ + return EXPECT_RESULT(); +} + +int test_wc_XmssFeatureCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_XMSS) && !defined(WOLFSSL_XMSS_VERIFY_ONLY) + WC_RNG rng; + + XMEMSET(&rng, 0, sizeof(rng)); + ExpectIntEQ(wc_InitRng(&rng), 0); + + /* Single-tree XMSS, one per compiled-in hash family. H10 keeps keygen + * fast; each family drives its own wc_xmss_impl.c hash-address helpers. */ +#if !defined(WOLFSSL_XMSS_MIN_HEIGHT) || (WOLFSSL_XMSS_MIN_HEIGHT <= 10) +#ifdef WC_XMSS_SHA256 + ExpectIntEQ(xmss_mc_param_roundtrip(&rng, "XMSS-SHA2_10_256"), + TEST_SUCCESS); +#endif +#ifdef WC_XMSS_SHA512 + ExpectIntEQ(xmss_mc_param_roundtrip(&rng, "XMSS-SHA2_10_512"), + TEST_SUCCESS); +#endif +#ifdef WC_XMSS_SHAKE128 + ExpectIntEQ(xmss_mc_param_roundtrip(&rng, "XMSS-SHAKE_10_256"), + TEST_SUCCESS); +#endif +#ifdef WC_XMSS_SHAKE256 + ExpectIntEQ(xmss_mc_param_roundtrip(&rng, "XMSS-SHAKE256_10_256"), + TEST_SUCCESS); +#endif +#endif /* min height <= 10 */ + + /* Multi-tree XMSS^MT with total height 20 (2 layers): drives the + * XMSS^MT-specific subtree / BDS-state helpers a single tree skips. */ +#if defined(WC_XMSS_SHA256) && (WOLFSSL_XMSS_MAX_HEIGHT >= 20) && \ + (!defined(WOLFSSL_XMSS_MIN_HEIGHT) || (WOLFSSL_XMSS_MIN_HEIGHT <= 20)) + ExpectIntEQ(xmss_mc_param_roundtrip(&rng, "XMSSMT-SHA2_20/2_256"), + TEST_SUCCESS); +#endif + + /* Tall XMSS^MT (total height 40): the actual key height > 32 drives the + * 64-bit tree-index runtime path inside the mixed (MAX>32 && MIN<=32) + * index arm. 8 layers keep each subtree small (H5) so keygen stays fast. + * Only reachable when the compiled-in window admits height 40. Skipped + * under WOLFSSL_WC_XMSS_SMALL, whose recompute-signing makes a height-40 + * tree slow; the 64-bit index path is unioned from the fast variant. */ +#if defined(WC_XMSS_SHA256) && !defined(WOLFSSL_WC_XMSS_SMALL) && \ + (WOLFSSL_XMSS_MAX_HEIGHT >= 40) && \ + (!defined(WOLFSSL_XMSS_MIN_HEIGHT) || (WOLFSSL_XMSS_MIN_HEIGHT <= 40)) + ExpectIntEQ(xmss_mc_param_roundtrip(&rng, "XMSSMT-SHA2_40/8_256"), + TEST_SUCCESS); +#endif + + wc_FreeRng(&rng); +#endif /* WOLFSSL_HAVE_XMSS && !WOLFSSL_XMSS_VERIFY_ONLY */ + return EXPECT_RESULT(); +} diff --git a/tests/api/test_lms_xmss.h b/tests/api/test_lms_xmss.h index 78c66e53e93..7f8abf21b32 100644 --- a/tests/api/test_lms_xmss.h +++ b/tests/api/test_lms_xmss.h @@ -30,6 +30,10 @@ int test_rfc9802_lms_x509_verify(void); int test_rfc9802_xmss_x509_verify(void); int test_rfc9802_lms_x509_gen(void); int test_rfc9802_xmss_x509_gen(void); +int test_wc_LmsDecisionCoverage(void); +int test_wc_LmsFeatureCoverage(void); +int test_wc_XmssDecisionCoverage(void); +int test_wc_XmssFeatureCoverage(void); /* LMS, and RFC 9802 (HSS/LMS and XMSS/XMSS^MT in X.509). */ #define TEST_LMS_XMSS_DECLS \ @@ -38,6 +42,10 @@ int test_rfc9802_xmss_x509_gen(void); TEST_DECL_GROUP("lms", test_rfc9802_lms_x509_verify), \ TEST_DECL_GROUP("xmss", test_rfc9802_xmss_x509_verify), \ TEST_DECL_GROUP("lms", test_rfc9802_lms_x509_gen), \ - TEST_DECL_GROUP("xmss", test_rfc9802_xmss_x509_gen) + TEST_DECL_GROUP("xmss", test_rfc9802_xmss_x509_gen), \ + TEST_DECL_GROUP("lms", test_wc_LmsDecisionCoverage), \ + TEST_DECL_GROUP("lms", test_wc_LmsFeatureCoverage), \ + TEST_DECL_GROUP("xmss", test_wc_XmssDecisionCoverage), \ + TEST_DECL_GROUP("xmss", test_wc_XmssFeatureCoverage) #endif /* WOLFCRYPT_TEST_LMS_XMSS_H */ diff --git a/tests/api/test_mldsa.c b/tests/api/test_mldsa.c index 5a91f220c33..f86a513cfa0 100644 --- a/tests/api/test_mldsa.c +++ b/tests/api/test_mldsa.c @@ -30135,6 +30135,7 @@ int test_mldsa_pkcs12(void) EXPECT_DECLS; #if !defined(NO_ASN) && defined(HAVE_PKCS12) && \ defined(WOLFSSL_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \ + !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(NO_TLS) && !defined(NO_PWDBASED) && !defined(NO_HMAC) && \ !defined(NO_CERTS) && !defined(NO_DES3) && \ (!defined(NO_WOLFSSL_CLIENT) || !defined(NO_WOLFSSL_SERVER)) && \ @@ -30590,3 +30591,712 @@ int test_dilithium_hash(void) #endif return EXPECT_RESULT(); } + +/* ===================================================================== + * MC/DC coverage supplements (ISO 26262 per-module campaign). + * + * test_wc_MldsaDecisionCoverage: one negative call per public-entry + * argument / state check, each asserting the specific error, driving both + * halves of the argument-validation decisions in wc_MlDsaKey_Init / + * SetParams / GetParams / MakeKey / Get{Pub,Priv,Sig}Len / SignCtx / + * VerifyCtx (unique-cause: exactly one operand invalid at a time, plus the + * all-valid fall-through). + * + * test_wc_MldsaFeatureCoverage: positive keygen + sign + verify round trip + * (ML-DSA-44, the smallest param set, to bound runtime) exercising the + * verify true-branch (good signature -> res == 1) and false-branch + * (tampered signature -> res == 0), plus the positive length getters. + * ===================================================================== */ + +int test_wc_MldsaDecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLDSA) + wc_MlDsaKey key; + byte level = 0; + int len = 0; + int inited = 0; + + XMEMSET(&key, 0, sizeof(key)); + + /* wc_MlDsaKey_Init: key == NULL (TRUE) vs valid (FALSE). */ + ExpectIntEQ(wc_MlDsaKey_Init(NULL, NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID), 0); + if (EXPECT_SUCCESS()) { + inited = 1; + } + + /* wc_MlDsaKey_SetParams: key == NULL. */ + ExpectIntEQ(wc_MlDsaKey_SetParams(NULL, WC_ML_DSA_44), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* Bad level (not 2/3/5): the level-match compound is all-FALSE -> else. */ + ExpectIntEQ(wc_MlDsaKey_SetParams(&key, (byte)0x7f), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* Each valid level arm of the (level==44)||(level==65)||(level==87) + * decision. Only the arms whose param set is compiled in exist. */ +#ifndef WOLFSSL_NO_ML_DSA_44 + ExpectIntEQ(wc_MlDsaKey_SetParams(&key, WC_ML_DSA_44), 0); +#endif +#ifndef WOLFSSL_NO_ML_DSA_65 + ExpectIntEQ(wc_MlDsaKey_SetParams(&key, WC_ML_DSA_65), 0); +#endif +#ifndef WOLFSSL_NO_ML_DSA_87 + ExpectIntEQ(wc_MlDsaKey_SetParams(&key, WC_ML_DSA_87), 0); +#endif + + /* wc_MlDsaKey_GetParams: key == NULL, level == NULL (independence of the + * (key==NULL)||(level==NULL) compound). */ + ExpectIntEQ(wc_MlDsaKey_GetParams(NULL, &level), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_GetParams(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* Valid: level was set to a real value above -> success. */ + ExpectIntEQ(wc_MlDsaKey_GetParams(&key, &level), 0); + + /* wc_MlDsaKey_MakeKey: key == NULL and rng == NULL independence of the + * (key==NULL)||(rng==NULL) compound. A real RNG is not needed since these + * short-circuit before use. */ +#ifndef WOLFSSL_MLDSA_NO_MAKE_KEY + ExpectIntEQ(wc_MlDsaKey_MakeKey(NULL, (WC_RNG*)&key), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_MakeKey(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + + /* Length getters with a NULL key -> underlying *Size returns BAD_FUNC_ARG + * (< 0) so the getter propagates it (the *len < 0 decision TRUE side). */ +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + len = 0; + ExpectIntEQ(wc_MlDsaKey_GetPubLen(NULL, &len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* Valid key with a level set -> *len >= 0, decision FALSE side, ret 0. */ + len = 0; + ExpectIntEQ(wc_MlDsaKey_GetPubLen(&key, &len), 0); + ExpectIntGT(len, 0); +#endif +#if defined(WOLFSSL_MLDSA_PRIVATE_KEY) && defined(WOLFSSL_MLDSA_PUBLIC_KEY) + len = 0; + ExpectIntEQ(wc_MlDsaKey_GetPrivLen(NULL, &len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + len = 0; + ExpectIntEQ(wc_MlDsaKey_GetPrivLen(&key, &len), 0); + ExpectIntGT(len, 0); +#endif +#if !defined(WOLFSSL_MLDSA_NO_SIGN) || !defined(WOLFSSL_MLDSA_NO_VERIFY) + len = 0; + ExpectIntEQ(wc_MlDsaKey_GetSigLen(NULL, &len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + len = 0; + ExpectIntEQ(wc_MlDsaKey_GetSigLen(&key, &len), 0); + ExpectIntGT(len, 0); +#endif + + /* wc_MlDsaKey_SignCtx: independence of the four-way NULL OR. Hold three + * operands valid and make exactly one NULL. */ +#if !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && !defined(WOLFSSL_MLDSA_NO_SIGN) + { + byte sig[16]; + word32 sigLen = (word32)sizeof(sig); + byte msg[4]; + byte ctx[2]; + + XMEMSET(sig, 0, sizeof(sig)); + XMEMSET(msg, 'M', sizeof(msg)); + XMEMSET(ctx, 'C', sizeof(ctx)); + + ExpectIntEQ(wc_MlDsaKey_SignCtx(NULL, NULL, 0, sig, &sigLen, + msg, (word32)sizeof(msg), NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtx(&key, NULL, 0, NULL, &sigLen, + msg, (word32)sizeof(msg), NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtx(&key, NULL, 0, sig, NULL, + msg, (word32)sizeof(msg), NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtx(&key, NULL, 0, sig, &sigLen, + NULL, (word32)sizeof(msg), NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* All four non-NULL (first compound FALSE), but ctx==NULL && ctxLen>0 + * TRUE -> BAD_FUNC_ARG (independence of the ctxLen>0 operand). */ + ExpectIntEQ(wc_MlDsaKey_SignCtx(&key, NULL, 1, sig, &sigLen, + msg, (word32)sizeof(msg), NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* ctx non-NULL so that compound is FALSE, and key not a private key + * yet -> the !prvKeySet decision TRUE -> BAD_FUNC_ARG. */ + ExpectIntEQ(wc_MlDsaKey_SignCtx(&key, ctx, (byte)sizeof(ctx), sig, + &sigLen, msg, (word32)sizeof(msg), NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif + + /* wc_MlDsaKey_VerifyCtx: independence of the four-way NULL OR + the + * ctxLen>0 and oversized-msgLen guards. */ +#ifndef WOLFSSL_MLDSA_NO_VERIFY + { + byte sig[16]; + byte msg[4]; + byte ctx[2]; + int res = -1; + + XMEMSET(sig, 0, sizeof(sig)); + XMEMSET(msg, 'M', sizeof(msg)); + XMEMSET(ctx, 'C', sizeof(ctx)); + + ExpectIntEQ(wc_MlDsaKey_VerifyCtx(NULL, sig, (word32)sizeof(sig), + NULL, 0, msg, (word32)sizeof(msg), &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyCtx(&key, NULL, (word32)sizeof(sig), + NULL, 0, msg, (word32)sizeof(msg), &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyCtx(&key, sig, (word32)sizeof(sig), + NULL, 0, NULL, (word32)sizeof(msg), &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyCtx(&key, sig, (word32)sizeof(sig), + NULL, 0, msg, (word32)sizeof(msg), NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* ctx==NULL && ctxLen>0 -> BAD_FUNC_ARG. */ + ExpectIntEQ(wc_MlDsaKey_VerifyCtx(&key, sig, (word32)sizeof(sig), + NULL, 1, msg, (word32)sizeof(msg), &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* Oversized msgLen guard TRUE side. */ + ExpectIntEQ(wc_MlDsaKey_VerifyCtx(&key, sig, (word32)sizeof(sig), + NULL, 0, msg, 0xFFFFFFFFU, &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif + + if (inited) { + wc_MlDsaKey_Free(&key); + } +#endif /* WOLFSSL_HAVE_MLDSA */ + return EXPECT_RESULT(); +} + +int test_wc_MldsaFeatureCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLDSA) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ + !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && \ + !defined(WOLFSSL_MLDSA_NO_SIGN) && \ + !defined(WOLFSSL_MLDSA_NO_VERIFY) + wc_MlDsaKey key; + WC_RNG rng; + int inited = 0; + int rngInited = 0; + int sigLenI = 0; + int pubLenI = 0; + int privLenI = 0; + word32 sigLen = 0; + byte* sig = NULL; + int res = 0; + byte msg[32]; + /* Smallest available param set to bound runtime. */ +#ifndef WOLFSSL_NO_ML_DSA_44 + const byte level = WC_ML_DSA_44; +#elif !defined(WOLFSSL_NO_ML_DSA_65) + const byte level = WC_ML_DSA_65; +#else + const byte level = WC_ML_DSA_87; +#endif + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(msg, 'A', sizeof(msg)); + + ExpectIntEQ(wc_InitRng(&rng), 0); + if (EXPECT_SUCCESS()) { + rngInited = 1; + } + ExpectIntEQ(wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID), 0); + if (EXPECT_SUCCESS()) { + inited = 1; + } + ExpectIntEQ(wc_MlDsaKey_SetParams(&key, level), 0); + + /* Positive length getters (decision FALSE sides: *len >= 0). */ + ExpectIntEQ(wc_MlDsaKey_GetSigLen(&key, &sigLenI), 0); + ExpectIntGT(sigLenI, 0); + ExpectIntEQ(wc_MlDsaKey_GetPubLen(&key, &pubLenI), 0); + ExpectIntGT(pubLenI, 0); + ExpectIntEQ(wc_MlDsaKey_GetPrivLen(&key, &privLenI), 0); + ExpectIntGT(privLenI, 0); + + ExpectIntEQ(wc_MlDsaKey_MakeKey(&key, &rng), 0); + + if (EXPECT_SUCCESS() && (sigLenI > 0)) { + sigLen = (word32)sigLenI; + sig = (byte*)XMALLOC(sigLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + ExpectNotNull(sig); + } + + if (sig != NULL) { + XMEMSET(sig, 0, sigLen); + /* Sign with an empty context (FIPS 204 compliant). */ + ExpectIntEQ(wc_MlDsaKey_SignCtx(&key, NULL, 0, sig, &sigLen, + msg, (word32)sizeof(msg), &rng), 0); + + /* Verify true-branch: good signature -> res == 1. */ + res = 0; + ExpectIntEQ(wc_MlDsaKey_VerifyCtx(&key, sig, sigLen, NULL, 0, + msg, (word32)sizeof(msg), &res), 0); + ExpectIntEQ(res, 1); + + /* Verify false-branch: tamper one byte -> res == 0 (still ret 0). */ + sig[0] ^= 0xFF; + res = 1; + ExpectIntEQ(wc_MlDsaKey_VerifyCtx(&key, sig, sigLen, NULL, 0, + msg, (word32)sizeof(msg), &res), 0); + ExpectIntEQ(res, 0); + + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + + if (inited) { + wc_MlDsaKey_Free(&key); + } + if (rngInited) { + DoExpectIntEQ(wc_FreeRng(&rng), 0); + } +#endif /* WOLFSSL_HAVE_MLDSA && sign+verify+makekey */ + return EXPECT_RESULT(); +} + +int test_wc_MldsaDecisionCoverage2(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLDSA) + wc_MlDsaKey key; + int inited = 0; + + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID), 0); + if (EXPECT_SUCCESS()) { + inited = 1; + } +#ifndef WOLFSSL_NO_ML_DSA_44 + ExpectIntEQ(wc_MlDsaKey_SetParams(&key, WC_ML_DSA_44), 0); +#elif !defined(WOLFSSL_NO_ML_DSA_65) + ExpectIntEQ(wc_MlDsaKey_SetParams(&key, WC_ML_DSA_65), 0); +#else + ExpectIntEQ(wc_MlDsaKey_SetParams(&key, WC_ML_DSA_87), 0); +#endif + + /* wc_MlDsaKey_MakeKeyFromSeed: independence of the + * (key==NULL)||(seed==NULL) compound. */ +#ifndef WOLFSSL_MLDSA_NO_MAKE_KEY + { + byte seed[MLDSA_SEED_SZ]; + + XMEMSET(seed, 0, sizeof(seed)); + ExpectIntEQ(wc_MlDsaKey_MakeKeyFromSeed(NULL, seed), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_MakeKeyFromSeed(&key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif + + /* wc_MlDsaKey_SignCtxHash / SignCtxWithSeed / SignCtxHashWithSeed / + * SignMuWithSeed: independence of each four/five-way NULL OR and the + * ctxLen>0/muLen!=MU_SZ guards. Hold all other operands valid and + * make exactly one NULL/invalid at a time. Also drives the sign + * path's too-small sigLen buffer guard (BUFFER_E). */ +#if !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && !defined(WOLFSSL_MLDSA_NO_SIGN) + { + byte sig[16]; + word32 sigLen; + byte hash[64]; /* WC_SHA3_512_DIGEST_SIZE */ + byte msg[4]; + byte ctx[2]; + byte seed[MLDSA_RND_SZ]; + byte mu[MLDSA_MU_SZ]; + WC_RNG rng; + int rngInited = 0; + + XMEMSET(sig, 0, sizeof(sig)); + XMEMSET(hash, 'H', sizeof(hash)); + XMEMSET(msg, 'M', sizeof(msg)); + XMEMSET(ctx, 'C', sizeof(ctx)); + XMEMSET(seed, 0, sizeof(seed)); + XMEMSET(mu, 0, sizeof(mu)); + XMEMSET(&rng, 0, sizeof(rng)); + ExpectIntEQ(wc_InitRng(&rng), 0); + if (EXPECT_SUCCESS()) { + rngInited = 1; + } + + /* wc_MlDsaKey_SignCtxHash: four-way NULL OR + ctx/ctxLen. */ + sigLen = (word32)sizeof(sig); + ExpectIntEQ(wc_MlDsaKey_SignCtxHash(NULL, NULL, 0, sig, &sigLen, + hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxHash(&key, NULL, 0, NULL, &sigLen, + hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxHash(&key, NULL, 0, sig, NULL, + hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxHash(&key, NULL, 0, sig, &sigLen, + NULL, sizeof(hash), WC_HASH_TYPE_SHA3_512, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* All four non-NULL, ctx==NULL && ctxLen>0 -> BAD_FUNC_ARG. */ + ExpectIntEQ(wc_MlDsaKey_SignCtxHash(&key, NULL, 1, sig, &sigLen, + hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_MlDsaKey_SignCtxWithSeed: four-way NULL OR + ctx/ctxLen. */ + sigLen = (word32)sizeof(sig); + ExpectIntEQ(wc_MlDsaKey_SignCtxWithSeed(NULL, NULL, 0, sig, &sigLen, + msg, (word32)sizeof(msg), seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxWithSeed(&key, NULL, 0, NULL, &sigLen, + msg, (word32)sizeof(msg), seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxWithSeed(&key, NULL, 0, sig, NULL, + msg, (word32)sizeof(msg), seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxWithSeed(&key, NULL, 0, sig, &sigLen, + NULL, (word32)sizeof(msg), seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxWithSeed(&key, NULL, 1, sig, &sigLen, + msg, (word32)sizeof(msg), seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_MlDsaKey_SignCtxHashWithSeed: five-way NULL OR (incl. seed) + * + ctx/ctxLen. */ + sigLen = (word32)sizeof(sig); + ExpectIntEQ(wc_MlDsaKey_SignCtxHashWithSeed(NULL, NULL, 0, sig, + &sigLen, hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, seed), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxHashWithSeed(&key, NULL, 0, NULL, + &sigLen, hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, seed), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxHashWithSeed(&key, NULL, 0, sig, + NULL, hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, seed), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxHashWithSeed(&key, NULL, 0, sig, + &sigLen, NULL, sizeof(hash), WC_HASH_TYPE_SHA3_512, seed), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxHashWithSeed(&key, NULL, 0, sig, + &sigLen, hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignCtxHashWithSeed(&key, NULL, 1, sig, + &sigLen, hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, seed), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_MlDsaKey_SignMuWithSeed: five-way NULL OR + muLen!=MU_SZ. */ + sigLen = (word32)sizeof(sig); + ExpectIntEQ(wc_MlDsaKey_SignMuWithSeed(NULL, sig, &sigLen, mu, + sizeof(mu), seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignMuWithSeed(&key, NULL, &sigLen, mu, + sizeof(mu), seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignMuWithSeed(&key, sig, NULL, mu, + sizeof(mu), seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignMuWithSeed(&key, sig, &sigLen, NULL, + sizeof(mu), seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_SignMuWithSeed(&key, sig, &sigLen, mu, + sizeof(mu), NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* All five non-NULL, muLen != MLDSA_MU_SZ -> BAD_FUNC_ARG. */ + ExpectIntEQ(wc_MlDsaKey_SignMuWithSeed(&key, sig, &sigLen, mu, + sizeof(mu) - 1, seed), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Sign path's too-small sigLen buffer guard (*sigLen < + * params->sigSz -> BUFFER_E), independent of the muLen guard + * above (all args otherwise valid). */ + sigLen = 1; + ExpectIntEQ(wc_MlDsaKey_SignMuWithSeed(&key, sig, &sigLen, mu, + sizeof(mu), seed), WC_NO_ERR_TRACE(BUFFER_E)); + + if (rngInited) { + DoExpectIntEQ(wc_FreeRng(&rng), 0); + } + } +#endif /* !WOLFSSL_MLDSA_VERIFY_ONLY && !WOLFSSL_MLDSA_NO_SIGN */ + + /* wc_MlDsaKey_VerifyCtxHash: four-way NULL OR + ctx/ctxLen. */ +#ifndef WOLFSSL_MLDSA_NO_VERIFY + { + byte sig[16]; + byte hash[64]; + int res = -1; + + XMEMSET(sig, 0, sizeof(sig)); + XMEMSET(hash, 'H', sizeof(hash)); + + ExpectIntEQ(wc_MlDsaKey_VerifyCtxHash(NULL, sig, (word32)sizeof(sig), + NULL, 0, hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyCtxHash(&key, NULL, (word32)sizeof(sig), + NULL, 0, hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyCtxHash(&key, sig, (word32)sizeof(sig), + NULL, 0, NULL, sizeof(hash), WC_HASH_TYPE_SHA3_512, &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyCtxHash(&key, sig, (word32)sizeof(sig), + NULL, 0, hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyCtxHash(&key, sig, (word32)sizeof(sig), + NULL, 1, hash, sizeof(hash), WC_HASH_TYPE_SHA3_512, &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_MlDsaKey_VerifyMu: five-way NULL OR (incl. key->params) + + * muLen != MU_SZ. */ + { + byte mu[MLDSA_MU_SZ]; + wc_MlDsaKey noParamsKey; + + XMEMSET(mu, 0, sizeof(mu)); + XMEMSET(&noParamsKey, 0, sizeof(noParamsKey)); + + ExpectIntEQ(wc_MlDsaKey_VerifyMu(NULL, sig, (word32)sizeof(sig), + mu, sizeof(mu), &res), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* key->params == NULL (level never set on this key). */ + ExpectIntEQ(wc_MlDsaKey_VerifyMu(&noParamsKey, sig, + (word32)sizeof(sig), mu, sizeof(mu), &res), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyMu(&key, NULL, (word32)sizeof(sig), + mu, sizeof(mu), &res), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyMu(&key, sig, (word32)sizeof(sig), + NULL, sizeof(mu), &res), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_VerifyMu(&key, sig, (word32)sizeof(sig), + mu, sizeof(mu), NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* All five non-NULL, muLen != MLDSA_MU_SZ -> BAD_FUNC_ARG. */ + ExpectIntEQ(wc_MlDsaKey_VerifyMu(&key, sig, (word32)sizeof(sig), + mu, sizeof(mu) - 1, &res), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + } +#endif /* !WOLFSSL_MLDSA_NO_VERIFY */ + + /* wc_MlDsaKey_InitId / InitLabel: NULL guards and length guards. */ +#ifdef WOLF_PRIVATE_KEY_ID + { + wc_MlDsaKey idKey; + byte id[4]; + byte oversizeId[MLDSA_MAX_ID_LEN + 1]; + char oversizeLabel[MLDSA_MAX_LABEL_LEN + 2]; + + XMEMSET(&idKey, 0, sizeof(idKey)); + XMEMSET(id, 'I', sizeof(id)); + XMEMSET(oversizeId, 'I', sizeof(oversizeId)); + XMEMSET(oversizeLabel, 'L', sizeof(oversizeLabel) - 1); + oversizeLabel[sizeof(oversizeLabel) - 1] = '\0'; + + /* InitId: key == NULL. */ + ExpectIntEQ(wc_MlDsaKey_InitId(NULL, id, (int)sizeof(id), NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* InitId: len < 0. */ + ExpectIntEQ(wc_MlDsaKey_InitId(&idKey, id, -1, NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BUFFER_E)); + /* InitId: len > MLDSA_MAX_ID_LEN. */ + ExpectIntEQ(wc_MlDsaKey_InitId(&idKey, oversizeId, + (int)sizeof(oversizeId), NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BUFFER_E)); + /* InitId: valid, id != NULL && len != 0 -> copied in. */ + ExpectIntEQ(wc_MlDsaKey_InitId(&idKey, id, (int)sizeof(id), NULL, + INVALID_DEVID), 0); + wc_MlDsaKey_Free(&idKey); + /* InitId: valid, len == 0 (independence of id!=NULL&&len!=0). */ + XMEMSET(&idKey, 0, sizeof(idKey)); + ExpectIntEQ(wc_MlDsaKey_InitId(&idKey, id, 0, NULL, INVALID_DEVID), + 0); + wc_MlDsaKey_Free(&idKey); + + /* InitLabel: key == NULL / label == NULL. */ + XMEMSET(&idKey, 0, sizeof(idKey)); + ExpectIntEQ(wc_MlDsaKey_InitLabel(NULL, "label", NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_InitLabel(&idKey, NULL, NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* InitLabel: labelLen == 0. */ + ExpectIntEQ(wc_MlDsaKey_InitLabel(&idKey, "", NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BUFFER_E)); + /* InitLabel: labelLen > MLDSA_MAX_LABEL_LEN. */ + ExpectIntEQ(wc_MlDsaKey_InitLabel(&idKey, oversizeLabel, NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + /* InitLabel: valid. */ + ExpectIntEQ(wc_MlDsaKey_InitLabel(&idKey, "label", NULL, + INVALID_DEVID), 0); + wc_MlDsaKey_Free(&idKey); + } +#endif /* WOLF_PRIVATE_KEY_ID */ + + /* wc_MlDsaKey_CheckKey: independence of !prvKeySet and !pubKeySet + * (the x!=0 mismatch pair is already exercised by + * test_mldsa_check_key). */ +#if defined(WOLFSSL_MLDSA_CHECK_KEY) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \ + !defined(WOLFSSL_NO_ML_DSA_44) + { + wc_MlDsaKey ckKey; + byte privBuf[WC_MLDSA_44_KEY_SIZE]; + + XMEMSET(&ckKey, 0, sizeof(ckKey)); + XMEMSET(privBuf, 0, sizeof(privBuf)); + ExpectIntEQ(wc_MlDsaKey_Init(&ckKey, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_MlDsaKey_SetParams(&ckKey, WC_ML_DSA_44), 0); + + /* Neither key set yet -> !prvKeySet TRUE -> BAD_FUNC_ARG + * (independent of pubKeySet, never reached). */ + ExpectIntEQ(wc_MlDsaKey_CheckKey(&ckKey), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + /* Private key set, public key not -> !prvKeySet FALSE, + * !pubKeySet TRUE -> PUBLIC_KEY_E. */ + ExpectIntEQ(wc_MlDsaKey_ImportPrivRaw(&ckKey, privBuf, + (word32)sizeof(privBuf)), 0); + ExpectIntEQ(wc_MlDsaKey_CheckKey(&ckKey), + WC_NO_ERR_TRACE(PUBLIC_KEY_E)); +#endif + + wc_MlDsaKey_Free(&ckKey); + } +#endif /* WOLFSSL_MLDSA_CHECK_KEY && WOLFSSL_MLDSA_PRIVATE_KEY && + * !WOLFSSL_NO_ML_DSA_44 */ + + /* wc_MlDsaKey_ExportPubRaw / ImportPubRaw: NULL guards. */ +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + { + byte buf[8]; + word32 bufLen = (word32)sizeof(buf); + + XMEMSET(buf, 0, sizeof(buf)); + ExpectIntEQ(wc_MlDsaKey_ExportPubRaw(NULL, buf, &bufLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_ExportPubRaw(&key, NULL, &bufLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_ExportPubRaw(&key, buf, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + ExpectIntEQ(wc_MlDsaKey_ImportPubRaw(NULL, buf, bufLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_ImportPubRaw(&key, NULL, bufLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ + + /* wc_MlDsaKey_ImportPrivRaw / ImportKey / ExportPrivRaw / KeyToDer. */ +#if defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_NO_ML_DSA_44) + { + byte privBuf[WC_MLDSA_44_KEY_SIZE]; + word32 outLen; + wc_MlDsaKey badLevelKey; + + XMEMSET(privBuf, 0, sizeof(privBuf)); + XMEMSET(&badLevelKey, 0, sizeof(badLevelKey)); + + /* ImportPrivRaw: priv == NULL || key == NULL. */ + ExpectIntEQ(wc_MlDsaKey_ImportPrivRaw(NULL, privBuf, + (word32)sizeof(privBuf)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlDsaKey_ImportPrivRaw(&key, NULL, + (word32)sizeof(privBuf)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* ImportPrivRaw: key->level not one of 44/65/87 (bypass + * SetParams to reach the standalone level-range guard). */ + ExpectIntEQ(wc_MlDsaKey_Init(&badLevelKey, NULL, INVALID_DEVID), 0); + badLevelKey.level = 0x7F; + ExpectIntEQ(wc_MlDsaKey_ImportPrivRaw(&badLevelKey, privBuf, + (word32)sizeof(privBuf)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + wc_MlDsaKey_Free(&badLevelKey); + + /* ImportPrivRaw: valid -> private key set (zero-filled s1/s2 are + * within the eta range check). */ + ExpectIntEQ(wc_MlDsaKey_ImportPrivRaw(&key, privBuf, + (word32)sizeof(privBuf)), 0); + +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + /* ImportKey: pub == NULL (independence of the (ret==0)&& + * (pub!=NULL) operand; only private key is imported). */ + ExpectIntEQ(wc_MlDsaKey_ImportKey(&key, privBuf, + (word32)sizeof(privBuf), NULL, 0), 0); +#endif + + /* ExportPrivRaw: buffer too small -> BUFFER_E. */ + outLen = 1; + ExpectIntEQ(wc_MlDsaKey_ExportPrivRaw(&key, privBuf, &outLen), + WC_NO_ERR_TRACE(BUFFER_E)); + +#if !defined(WOLFSSL_MLDSA_NO_ASN1) && defined(WOLFSSL_MLDSA_PUBLIC_KEY) + /* wc_MlDsaKey_KeyToDer: independence of prvKeySet/pubKeySet. + * Private-only key (prvKeySet TRUE, pubKeySet FALSE) -> + * BAD_FUNC_ARG. */ + { + byte der[16]; + byte pubBuf[WC_MLDSA_44_PUB_KEY_SIZE]; + wc_MlDsaKey pubOnlyKey; + + XMEMSET(der, 0, sizeof(der)); + ExpectIntEQ(wc_MlDsaKey_KeyToDer(&key, der, (word32)sizeof(der)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Public-only key (prvKeySet FALSE, pubKeySet TRUE) -> + * BAD_FUNC_ARG (independence of the prvKeySet operand, held + * against the same pubKeySet==TRUE value used by the fully + * set key exercised in test_wc_MldsaFeatureCoverage / + * test_mldsa_der). */ + XMEMSET(pubBuf, 0, sizeof(pubBuf)); + XMEMSET(&pubOnlyKey, 0, sizeof(pubOnlyKey)); + ExpectIntEQ(wc_MlDsaKey_Init(&pubOnlyKey, NULL, INVALID_DEVID), + 0); + ExpectIntEQ(wc_MlDsaKey_SetParams(&pubOnlyKey, WC_ML_DSA_44), 0); + ExpectIntEQ(wc_MlDsaKey_ImportPubRaw(&pubOnlyKey, pubBuf, + (word32)sizeof(pubBuf)), 0); + ExpectIntEQ(wc_MlDsaKey_KeyToDer(&pubOnlyKey, der, + (word32)sizeof(der)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + wc_MlDsaKey_Free(&pubOnlyKey); + } +#endif + } +#endif /* WOLFSSL_MLDSA_PRIVATE_KEY */ + + if (inited) { + wc_MlDsaKey_Free(&key); + } +#endif /* WOLFSSL_HAVE_MLDSA */ + return EXPECT_RESULT(); +} + +int test_wc_MldsaDerDecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_NO_ASN1) && \ + defined(WOLFSSL_MLDSA_PUBLIC_KEY) && !defined(WOLFSSL_NO_ML_DSA_44) + wc_MlDsaKey key; + word32 idx; + + /* SEQUENCE (len 15) { SEQUENCE (len 11) { OID (len 9) } BIT STRING + * (len 0) }. Reaches the "length == 0" BIT STRING guard. */ + static const byte derZeroLenBitStr[] = { + 0x30, 0x0F, + 0x30, 0x0B, + 0x06, 0x09, + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, + 0x11, + 0x03, 0x00 + }; + /* SEQUENCE (len 18, one byte more than the inner content actually + * uses) { SEQUENCE (len 11) { OID (len 9) } BIT STRING (len 2: + * 0x00 pad + one payload byte) }. Reaches the + * "(idx + length) != outerEnd" BIT STRING guard. */ + static const byte derLenMismatch[] = { + 0x30, 0x12, + 0x30, 0x0B, + 0x06, 0x09, + 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, + 0x11, + 0x03, 0x02, 0x00, 0xAB, + 0xFF /* trailing junk, inside outer length but outside the + * BIT STRING's own declared content */ + }; + + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID), 0); + + idx = 0; + ExpectIntEQ(wc_MlDsaKey_PublicKeyDecode(&key, derZeroLenBitStr, + (word32)sizeof(derZeroLenBitStr), &idx), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + + wc_MlDsaKey_Free(&key); + XMEMSET(&key, 0, sizeof(key)); + ExpectIntEQ(wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID), 0); + + idx = 0; + ExpectIntEQ(wc_MlDsaKey_PublicKeyDecode(&key, derLenMismatch, + (word32)sizeof(derLenMismatch), &idx), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + + wc_MlDsaKey_Free(&key); +#endif /* WOLFSSL_HAVE_MLDSA && WOLFSSL_MLDSA_NO_ASN1 && ... */ + return EXPECT_RESULT(); +} diff --git a/tests/api/test_mldsa.h b/tests/api/test_mldsa.h index 631ebabdc15..4a81faba1c6 100644 --- a/tests/api/test_mldsa.h +++ b/tests/api/test_mldsa.h @@ -56,12 +56,18 @@ int test_mldsa_x509_pubkey_sigtype(void); int test_mldsa_verify_hash(void); int test_dilithium_hash(void); +/* MC/DC coverage supplements defined in tests/api/test_mldsa.c. */ +int test_wc_MldsaDecisionCoverage(void); +int test_wc_MldsaFeatureCoverage(void); + /* Legacy-name shim coverage defined in tests/api/test_mldsa_legacy.c. * Single function -- compile-time wc_static_assert checks for every alias * + one runtime smoke test that drives each arg-reordering macro family. * Becomes a TEST_SKIPPED stub when WOLFSSL_NO_DILITHIUM_LEGACY_NAMES is * defined. */ int test_mldsa_legacy_shim(void); +int test_wc_MldsaDecisionCoverage2(void); +int test_wc_MldsaDerDecisionCoverage(void); #define TEST_MLDSA_DECLS \ TEST_DECL_GROUP("mldsa", test_mldsa), \ @@ -89,6 +95,10 @@ int test_mldsa_legacy_shim(void); TEST_DECL_GROUP("mldsa", test_mldsa_x509_pubkey_sigtype), \ TEST_DECL_GROUP("mldsa", test_mldsa_verify_hash), \ TEST_DECL_GROUP("mldsa", test_dilithium_hash), \ - TEST_DECL_GROUP("mldsa", test_mldsa_legacy_shim) + TEST_DECL_GROUP("mldsa", test_wc_MldsaDecisionCoverage), \ + TEST_DECL_GROUP("mldsa", test_wc_MldsaFeatureCoverage), \ + TEST_DECL_GROUP("mldsa", test_mldsa_legacy_shim), \ + TEST_DECL_GROUP("mldsa", test_wc_MldsaDecisionCoverage2), \ + TEST_DECL_GROUP("mldsa", test_wc_MldsaDerDecisionCoverage) #endif /* WOLFCRYPT_TEST_MLDSA_H */ diff --git a/tests/api/test_mlkem.c b/tests/api/test_mlkem.c index 42ecf0523d8..db6644e6e4b 100644 --- a/tests/api/test_mlkem.c +++ b/tests/api/test_mlkem.c @@ -4087,3 +4087,560 @@ int test_wc_mlkem_decode_privkey_bad_pubhash(void) return EXPECT_RESULT(); } /* END test_wc_mlkem_decode_privkey_bad_pubhash */ + +/****************************************************************************** + * MC/DC coverage supplements (ISO 26262 per-module campaign). + * + * test_wc_MlkemDecisionCoverage: one negative call per argument-check / + * short-buffer / key-state branch in the public ML-KEM API, each asserting the + * SPECIFIC error, so every guard operand shows its independence pair (the + * all-valid rows are supplied by the KAT tests and FeatureCoverage below, all + * in the same tests/unit.test binary where llvm-cov unions MC/DC). + * + * test_wc_MlkemFeatureCoverage: positive full make-key/encode/decode/ + * encapsulate/decapsulate round-trips over every compiled parameter set, to + * drive the per-parameter-set switch arms and the NTT/compress/decompress + * positive paths. + *****************************************************************************/ + +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) && \ + !defined(WOLFSSL_MLKEM_NO_MAKE_KEY) && \ + !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) && \ + !defined(WOLFSSL_MLKEM_NO_DECAPSULATE) +/* Full positive round-trip for one parameter set. Returns TEST_SUCCESS. */ +static int mlkem_feature_roundtrip(int type) +{ + EXPECT_DECLS; + MlKemKey* key = NULL; + MlKemKey* peer = NULL; + WC_RNG rng; + byte* priv = NULL; + byte* pub = NULL; + byte* ct = NULL; + byte ss1[WC_ML_KEM_SS_SZ]; + byte ss2[WC_ML_KEM_SS_SZ]; + word32 privLen = 0; + word32 pubLen = 0; + word32 ctLen = 0; + word32 ssLen = 0; + word32 badLen = 0; + + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(ss1, 0, sizeof(ss1)); + XMEMSET(ss2, 0, sizeof(ss2)); + + ExpectIntEQ(wc_InitRng(&rng), 0); + + key = (MlKemKey*)XMALLOC(sizeof(*key), NULL, DYNAMIC_TYPE_TMP_BUFFER); + peer = (MlKemKey*)XMALLOC(sizeof(*peer), NULL, DYNAMIC_TYPE_TMP_BUFFER); + ExpectNotNull(key); + ExpectNotNull(peer); + + ExpectIntEQ(wc_MlKemKey_Init(key, type, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_MlKemKey_Init(peer, type, NULL, INVALID_DEVID), 0); + + /* Size queries drive the per-parameter-set case arms. */ + ExpectIntEQ(wc_MlKemKey_PrivateKeySize(key, &privLen), 0); + ExpectIntEQ(wc_MlKemKey_PublicKeySize(key, &pubLen), 0); + ExpectIntEQ(wc_MlKemKey_CipherTextSize(key, &ctLen), 0); + ExpectIntEQ(wc_MlKemKey_SharedSecretSize(key, &ssLen), 0); + ExpectIntEQ((int)ssLen, WC_ML_KEM_SS_SZ); + + ExpectIntEQ(wc_MlKemKey_MakeKey(key, &rng), 0); + + /* The size queries above set these; guard each allocation so static + * analysis sees a nonzero size (clang-tidy flags a possible 0-byte + * malloc otherwise). ExpectNotNull below still catches a 0-size query. */ + if (privLen > 0) + priv = (byte*)XMALLOC(privLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (pubLen > 0) + pub = (byte*)XMALLOC(pubLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ctLen > 0) + ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + ExpectNotNull(priv); + ExpectNotNull(pub); + ExpectNotNull(ct); + + /* Encode both halves; decode the public key into a peer object. */ + ExpectIntEQ(wc_MlKemKey_EncodePublicKey(key, pub, pubLen), 0); + ExpectIntEQ(wc_MlKemKey_EncodePrivateKey(key, priv, privLen), 0); + ExpectIntEQ(wc_MlKemKey_DecodePublicKey(peer, pub, pubLen), 0); + + /* Peer encapsulates to us; we decapsulate and recover the same secret. */ + ExpectIntEQ(wc_MlKemKey_Encapsulate(peer, ct, ss1, &rng), 0); + PRIVATE_KEY_UNLOCK(); + ExpectIntEQ(wc_MlKemKey_Decapsulate(key, ss2, ct, ctLen), 0); + PRIVATE_KEY_LOCK(); + ExpectIntEQ(XMEMCMP(ss1, ss2, WC_ML_KEM_SS_SZ), 0); + + /* Wrong cipher-text length: the len guard's false side -> BUFFER_E. */ + badLen = (ctLen > 1) ? (ctLen - 1) : (ctLen + 1); + PRIVATE_KEY_UNLOCK(); + ExpectIntEQ(wc_MlKemKey_Decapsulate(key, ss2, ct, badLen), + WC_NO_ERR_TRACE(BUFFER_E)); + PRIVATE_KEY_LOCK(); + + /* Decode the private key into a fresh object and decapsulate again. */ + wc_MlKemKey_Free(peer); + ExpectIntEQ(wc_MlKemKey_Init(peer, type, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_MlKemKey_DecodePrivateKey(peer, priv, privLen), 0); + XMEMSET(ss2, 0, sizeof(ss2)); + PRIVATE_KEY_UNLOCK(); + ExpectIntEQ(wc_MlKemKey_Decapsulate(peer, ss2, ct, ctLen), 0); + PRIVATE_KEY_LOCK(); + ExpectIntEQ(XMEMCMP(ss1, ss2, WC_ML_KEM_SS_SZ), 0); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + + XFREE(priv, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(pub, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wc_MlKemKey_Free(peer); + wc_MlKemKey_Free(key); + XFREE(peer, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(key, NULL, DYNAMIC_TYPE_TMP_BUFFER); + + return EXPECT_RESULT(); +} +#endif + +int test_wc_MlkemFeatureCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) && \ + !defined(WOLFSSL_MLKEM_NO_MAKE_KEY) && \ + !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) && \ + !defined(WOLFSSL_MLKEM_NO_DECAPSULATE) +#ifndef WOLFSSL_NO_ML_KEM_512 + ExpectIntEQ(mlkem_feature_roundtrip(WC_ML_KEM_512), TEST_SUCCESS); +#endif +#ifndef WOLFSSL_NO_ML_KEM_768 + ExpectIntEQ(mlkem_feature_roundtrip(WC_ML_KEM_768), TEST_SUCCESS); +#endif +#ifndef WOLFSSL_NO_ML_KEM_1024 + ExpectIntEQ(mlkem_feature_roundtrip(WC_ML_KEM_1024), TEST_SUCCESS); +#endif +#endif + return EXPECT_RESULT(); +} /* END test_wc_MlkemFeatureCoverage */ + +int test_wc_MlkemDecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) + MlKemKey* key = NULL; + MlKemKey* newKey = NULL; + WC_RNG rng; + word32 len = 0; + byte out[WC_ML_KEM_MAX_PRIVATE_KEY_SIZE]; + byte in[WC_ML_KEM_MAX_PRIVATE_KEY_SIZE]; + byte ct[WC_ML_KEM_MAX_CIPHER_TEXT_SIZE]; + byte ss[WC_ML_KEM_SS_SZ]; + byte rndMk[WC_ML_KEM_MAKEKEY_RAND_SZ]; + byte rndEnc[WC_ML_KEM_ENC_RAND_SZ]; +#ifndef WOLFSSL_NO_ML_KEM_768 + const int t = WC_ML_KEM_768; +#elif !defined(WOLFSSL_NO_ML_KEM_512) + const int t = WC_ML_KEM_512; +#else + const int t = WC_ML_KEM_1024; +#endif + + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(out, 0, sizeof(out)); + XMEMSET(in, 0, sizeof(in)); + XMEMSET(ct, 0, sizeof(ct)); + XMEMSET(ss, 0, sizeof(ss)); + XMEMSET(rndMk, 0, sizeof(rndMk)); + XMEMSET(rndEnc, 0, sizeof(rndEnc)); + + key = (MlKemKey*)XMALLOC(sizeof(*key), NULL, DYNAMIC_TYPE_TMP_BUFFER); + ExpectNotNull(key); + ExpectIntEQ(wc_InitRng(&rng), 0); + + /* --- wc_MlKemKey_Init: (key == NULL) and unrecognized-type default. --- */ + ExpectIntEQ(wc_MlKemKey_Init(NULL, t, NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_Init(key, -12345, NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* Valid init: the false side of the guards, and the object under test. */ + ExpectIntEQ(wc_MlKemKey_Init(key, t, NULL, INVALID_DEVID), 0); + + /* --- wc_MlKemKey_New / _Delete. --- */ + ExpectNull(wc_MlKemKey_New(-12345, NULL, INVALID_DEVID)); + ExpectNotNull(newKey = wc_MlKemKey_New(t, NULL, INVALID_DEVID)); + ExpectIntEQ(wc_MlKemKey_Delete(NULL, &newKey), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_Delete(newKey, &newKey), 0); + + /* --- Size queries: (key == NULL) and (len == NULL) independence. --- */ + ExpectIntEQ(wc_MlKemKey_CipherTextSize(NULL, &len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_CipherTextSize(key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_SharedSecretSize(key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_PrivateKeySize(NULL, &len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_PrivateKeySize(key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_PublicKeySize(NULL, &len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_PublicKeySize(key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + +#ifndef WOLFSSL_MLKEM_NO_MAKE_KEY + /* --- wc_MlKemKey_MakeKey / _MakeKeyWithRandom. --- */ + ExpectIntEQ(wc_MlKemKey_MakeKey(NULL, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_MakeKey(key, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_MakeKeyWithRandom(NULL, rndMk, sizeof(rndMk)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_MakeKeyWithRandom(key, NULL, sizeof(rndMk)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_MakeKeyWithRandom(key, rndMk, sizeof(rndMk) - 1), + WC_NO_ERR_TRACE(BUFFER_E)); +#endif + +#ifndef WOLFSSL_MLKEM_NO_ENCAPSULATE + /* --- wc_MlKemKey_Encapsulate: four-operand OR, one NULL per row. --- */ + ExpectIntEQ(wc_MlKemKey_Encapsulate(NULL, ct, ss, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_Encapsulate(key, NULL, ss, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_Encapsulate(key, ct, NULL, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_Encapsulate(key, ct, ss, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* WithRandom: NULL operands + wrong length -> BUFFER_E. */ + ExpectIntEQ(wc_MlKemKey_EncapsulateWithRandom(NULL, ct, ss, rndEnc, + sizeof(rndEnc)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_EncapsulateWithRandom(key, NULL, ss, rndEnc, + sizeof(rndEnc)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_EncapsulateWithRandom(key, ct, NULL, rndEnc, + sizeof(rndEnc)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_EncapsulateWithRandom(key, ct, ss, NULL, + sizeof(rndEnc)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_EncapsulateWithRandom(key, ct, ss, rndEnc, + sizeof(rndEnc) - 1), WC_NO_ERR_TRACE(BUFFER_E)); +#endif + +#ifndef WOLFSSL_MLKEM_NO_DECAPSULATE + /* --- wc_MlKemKey_Decapsulate: three-operand OR + priv-not-set state. --- */ + ExpectIntEQ(wc_MlKemKey_Decapsulate(NULL, ss, ct, sizeof(ct)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_Decapsulate(key, NULL, ct, sizeof(ct)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_Decapsulate(key, ss, NULL, sizeof(ct)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* Key initialised but no private key set -> BAD_STATE_E. */ + ExpectIntEQ(wc_MlKemKey_CipherTextSize(key, &len), 0); + ExpectIntEQ(wc_MlKemKey_Decapsulate(key, ss, ct, len), + WC_NO_ERR_TRACE(BAD_STATE_E)); +#endif + + /* --- Decode: (key == NULL) / (in == NULL) / wrong length -> BUFFER_E. --- */ + ExpectIntEQ(wc_MlKemKey_DecodePublicKey(NULL, in, sizeof(in)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_DecodePublicKey(key, NULL, sizeof(in)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_DecodePublicKey(key, in, 1), + WC_NO_ERR_TRACE(BUFFER_E)); + ExpectIntEQ(wc_MlKemKey_DecodePrivateKey(NULL, in, sizeof(in)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_DecodePrivateKey(key, NULL, sizeof(in)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_DecodePrivateKey(key, in, 1), + WC_NO_ERR_TRACE(BUFFER_E)); + + /* --- Encode: (key == NULL) / (out == NULL) / not-set state -> BAD_STATE_E. + * key was Init'd but never populated, so pub/priv are unset. --- */ + ExpectIntEQ(wc_MlKemKey_EncodePublicKey(NULL, out, sizeof(out)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_EncodePublicKey(key, NULL, sizeof(out)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_EncodePublicKey(key, out, sizeof(out)), + WC_NO_ERR_TRACE(BAD_STATE_E)); + ExpectIntEQ(wc_MlKemKey_EncodePrivateKey(NULL, out, sizeof(out)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_EncodePrivateKey(key, NULL, sizeof(out)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_MlKemKey_EncodePrivateKey(key, out, sizeof(out)), + WC_NO_ERR_TRACE(BAD_STATE_E)); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_MlKemKey_Free(key); + XFREE(key, NULL, DYNAMIC_TYPE_TMP_BUFFER); + + /* Silence -Werror unused warnings when make-key / encapsulate / decapsulate + * are individually compiled out (the buffers are used only in those + * sub-guarded sections). */ + (void)ct; + (void)ss; + (void)rndMk; + (void)rndEnc; +#endif + return EXPECT_RESULT(); +} /* END test_wc_MlkemDecisionCoverage */ + +/* + * MC/DC gap closing for wc_mlkem.c wc_MlKemKey_Init_Id(). + * + * Targets the three conditions of: + * 507: if ((key == NULL) || (id == NULL && len != 0)) + * 510: if ((ret == 0) && ((len < 0) || (len > MLKEM_MAX_ID_LEN))) + * 518: if ((ret == 0) && (id != NULL) && (len != 0)) + */ +int test_wc_mlkem_init_id_decision(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) && \ + defined(WOLF_PRIVATE_KEY_ID) + MlKemKey* key; + byte idBuf[MLKEM_MAX_ID_LEN + 8]; +#ifndef WOLFSSL_NO_ML_KEM_768 + const int mlkemType = WC_ML_KEM_768; +#elif !defined(WOLFSSL_NO_ML_KEM_512) + const int mlkemType = WC_ML_KEM_512; +#else + const int mlkemType = WC_ML_KEM_1024; +#endif + + XMEMSET(idBuf, 0x11, sizeof(idBuf)); + + key = (MlKemKey*)XMALLOC(sizeof(*key), NULL, DYNAMIC_TYPE_TMP_BUFFER); + ExpectNotNull(key); + if (key != NULL) { + /* Struct must be in a known (safe-to-Free) state before any + * successful wc_MlKemKey_Init() has run against it. */ + XMEMSET(key, 0, sizeof(*key)); + } + + /* 507 cond0(key==NULL) True; second OR term forced False (id!=NULL). */ + ExpectIntEQ(wc_MlKemKey_Init_Id(NULL, mlkemType, idBuf, 0, NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* 507 cond0 False side; also 518 cond2(len!=0) False baseline. */ + ExpectIntEQ(wc_MlKemKey_Init_Id(key, mlkemType, idBuf, 0, NULL, + INVALID_DEVID), 0); + ExpectIntEQ(key->idLen, 0); + wc_MlKemKey_Free(key); + + /* 507 cond1(id==NULL) True, cond2(len!=0) True -> BAD_FUNC_ARG. */ + ExpectIntEQ(wc_MlKemKey_Init_Id(key, mlkemType, NULL, 5, NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* 507 cond1/cond2 False (id==NULL, len==0) -> success. Pairs with the + * case above for cond1's independence (short-circuit masking) and + * gives 518's False baseline when id is NULL. */ + ExpectIntEQ(wc_MlKemKey_Init_Id(key, mlkemType, NULL, 0, NULL, + INVALID_DEVID), 0); + ExpectIntEQ(key->idLen, 0); + wc_MlKemKey_Free(key); + + /* 510 cond1(len<0) False, cond2(len>MAX) True -> BUFFER_E. */ + ExpectIntEQ(wc_MlKemKey_Init_Id(key, mlkemType, idBuf, + MLKEM_MAX_ID_LEN + 1, NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BUFFER_E)); + + /* 510 cond1(len<0) True, cond2(len>MAX) False -> BUFFER_E. */ + ExpectIntEQ(wc_MlKemKey_Init_Id(key, mlkemType, idBuf, -1, NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + + /* 510 cond1/cond2 False baseline; 518 cond0/cond1/cond2 all True -> + * success with id copied. */ + ExpectIntEQ(wc_MlKemKey_Init_Id(key, mlkemType, idBuf, 5, NULL, + INVALID_DEVID), 0); + ExpectIntEQ(key->idLen, 5); + ExpectIntEQ(XMEMCMP(key->id, idBuf, 5), 0); + wc_MlKemKey_Free(key); + + XFREE(key, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} /* END test_wc_mlkem_init_id_decision */ + +/* + * MC/DC gap closing for wc_mlkem.c wc_MlKemKey_Init_Label(). + * + * Targets: + * 549: if ((key == NULL) || (label == NULL)) + * 555: if ((labelLen == 0) || (labelLen > MLKEM_MAX_LABEL_LEN)) + */ +int test_wc_mlkem_init_label_decision(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) && \ + defined(WOLF_PRIVATE_KEY_ID) + MlKemKey* key; + char labelBuf[MLKEM_MAX_LABEL_LEN + 8]; + char overLabel[MLKEM_MAX_LABEL_LEN + 2]; + int i; +#ifndef WOLFSSL_NO_ML_KEM_768 + const int mlkemType = WC_ML_KEM_768; +#elif !defined(WOLFSSL_NO_ML_KEM_512) + const int mlkemType = WC_ML_KEM_512; +#else + const int mlkemType = WC_ML_KEM_1024; +#endif + + XMEMSET(labelBuf, 0, sizeof(labelBuf)); + labelBuf[0] = 'x'; + + /* MLKEM_MAX_LABEL_LEN + 1 non-null characters: one over the max. */ + for (i = 0; i < (int)sizeof(overLabel) - 1; i++) { + overLabel[i] = 'a'; + } + overLabel[sizeof(overLabel) - 1] = '\0'; + + key = (MlKemKey*)XMALLOC(sizeof(*key), NULL, DYNAMIC_TYPE_TMP_BUFFER); + ExpectNotNull(key); + if (key != NULL) { + XMEMSET(key, 0, sizeof(*key)); + } + + /* 549 cond0(key==NULL) True, label!=NULL fixed -> BAD_FUNC_ARG. */ + ExpectIntEQ(wc_MlKemKey_Init_Label(NULL, mlkemType, labelBuf, NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* 549 cond1(label==NULL) True, key!=NULL fixed -> BAD_FUNC_ARG. */ + ExpectIntEQ(wc_MlKemKey_Init_Label(key, mlkemType, NULL, NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* 555 cond0(labelLen==0) True -> BUFFER_E. */ + ExpectIntEQ(wc_MlKemKey_Init_Label(key, mlkemType, "", NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + + /* 555 cond1(labelLen>MAX) True -> BUFFER_E. */ + ExpectIntEQ(wc_MlKemKey_Init_Label(key, mlkemType, overLabel, NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + + /* 549 cond0/cond1 False, 555 cond0/cond1 False baseline -> success. */ + ExpectIntEQ(wc_MlKemKey_Init_Label(key, mlkemType, labelBuf, NULL, + INVALID_DEVID), 0); + ExpectIntEQ(key->labelLen, 1); + ExpectIntEQ(XMEMCMP(key->label, labelBuf, 1), 0); + wc_MlKemKey_Free(key); + + XFREE(key, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} /* END test_wc_mlkem_init_label_decision */ + +/* + * MC/DC gap closing for wc_mlkem.c wc_MlKemKey_EncapsulateWithRandom(). + * + * Targets: + * 1517: if ((ret == 0) && ((key->flags & MLKEM_FLAG_PUB_SET) == 0)) + * + * cond0(ret==0)'s False side comes from the ordinary length check (line + * 1513) firing first; cond1's True side comes from a freshly initialized + * key that never had its public key set. cond1's False side is already + * exercised by every successful encapsulate call elsewhere in this group. + */ +int test_wc_mlkem_encapsulate_pubkey_unset_decision(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) && \ + !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) + MlKemKey* key; + byte ct[WC_ML_KEM_MAX_CIPHER_TEXT_SIZE]; + byte ss[WC_ML_KEM_SS_SZ]; + byte randBuf[WC_ML_KEM_ENC_RAND_SZ]; +#ifndef WOLFSSL_NO_ML_KEM_768 + const int mlkemType = WC_ML_KEM_768; +#elif !defined(WOLFSSL_NO_ML_KEM_512) + const int mlkemType = WC_ML_KEM_512; +#else + const int mlkemType = WC_ML_KEM_1024; +#endif + + XMEMSET(ct, 0, sizeof(ct)); + XMEMSET(ss, 0, sizeof(ss)); + XMEMSET(randBuf, 0x22, sizeof(randBuf)); + + key = (MlKemKey*)XMALLOC(sizeof(*key), NULL, DYNAMIC_TYPE_TMP_BUFFER); + ExpectNotNull(key); + + /* Freshly initialized key: MLKEM_FLAG_PUB_SET is never set. */ + ExpectIntEQ(wc_MlKemKey_Init(key, mlkemType, NULL, INVALID_DEVID), 0); + + /* cond0 False: the len check at line 1513 fires first (BUFFER_E), + * short-circuiting the PUB_SET check away. */ + ExpectIntEQ(wc_MlKemKey_EncapsulateWithRandom(key, ct, ss, randBuf, + (int)sizeof(randBuf) - 1), WC_NO_ERR_TRACE(BUFFER_E)); + + /* cond0 True, cond1 True -> BAD_STATE_E. */ + ExpectIntEQ(wc_MlKemKey_EncapsulateWithRandom(key, ct, ss, randBuf, + (int)sizeof(randBuf)), WC_NO_ERR_TRACE(BAD_STATE_E)); + + wc_MlKemKey_Free(key); + XFREE(key, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} /* END test_wc_mlkem_encapsulate_pubkey_unset_decision */ + +/* + * MC/DC gap closing for wc_mlkem.c wc_MlKemKey_EncodePrivateKey() and + * wc_MlKemKey_EncodePublicKey(). + * + * Targets the buffer-size checks: + * 2566: if ((ret == 0) && (len != privLen)) + * 2676: if ((ret == 0) && (len != pubLen)) + * + * cond0(ret==0) in both is a chain guard fed only by the preceding + * switch(key->type) default case, which cannot be reached through the + * public API once wc_MlKemKey_Init() has already validated the type; + * left as a residual. cond1 (the length mismatch itself) is fully + * closed here with a too-short buffer and the matching success baseline. + */ +int test_wc_mlkem_encode_key_len_decision(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_NO_ML_KEM) && \ + !defined(WOLFSSL_MLKEM_NO_MAKE_KEY) + MlKemKey* key; + WC_RNG rng; + byte priv[WC_ML_KEM_MAX_PRIVATE_KEY_SIZE]; + byte pub[WC_ML_KEM_MAX_PUBLIC_KEY_SIZE]; + word32 privLen = 0; + word32 pubLen = 0; +#ifndef WOLFSSL_NO_ML_KEM_768 + const int mlkemType = WC_ML_KEM_768; +#elif !defined(WOLFSSL_NO_ML_KEM_512) + const int mlkemType = WC_ML_KEM_512; +#else + const int mlkemType = WC_ML_KEM_1024; +#endif + + XMEMSET(priv, 0, sizeof(priv)); + XMEMSET(pub, 0, sizeof(pub)); + XMEMSET(&rng, 0, sizeof(rng)); + + key = (MlKemKey*)XMALLOC(sizeof(*key), NULL, DYNAMIC_TYPE_TMP_BUFFER); + ExpectNotNull(key); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_MlKemKey_Init(key, mlkemType, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_MlKemKey_MakeKey(key, &rng), 0); + ExpectIntEQ(wc_MlKemKey_PrivateKeySize(key, &privLen), 0); + ExpectIntEQ(wc_MlKemKey_PublicKeySize(key, &pubLen), 0); + + /* 2566 cond1(len!=privLen) True -> BUFFER_E. */ + ExpectIntEQ(wc_MlKemKey_EncodePrivateKey(key, priv, privLen - 1), + WC_NO_ERR_TRACE(BUFFER_E)); + /* 2566 cond1 False baseline -> success. */ + ExpectIntEQ(wc_MlKemKey_EncodePrivateKey(key, priv, privLen), 0); + + /* 2676 cond1(len!=pubLen) True -> BUFFER_E. */ + ExpectIntEQ(wc_MlKemKey_EncodePublicKey(key, pub, pubLen - 1), + WC_NO_ERR_TRACE(BUFFER_E)); + /* 2676 cond1 False baseline -> success. */ + ExpectIntEQ(wc_MlKemKey_EncodePublicKey(key, pub, pubLen), 0); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + wc_MlKemKey_Free(key); + XFREE(key, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} /* END test_wc_mlkem_encode_key_len_decision */ diff --git a/tests/api/test_mlkem.h b/tests/api/test_mlkem.h index d8dc2e51073..7f11e37f650 100644 --- a/tests/api/test_mlkem.h +++ b/tests/api/test_mlkem.h @@ -30,6 +30,12 @@ int test_wc_mlkem_decapsulate_kats(void); int test_wc_mlkem_decapsulate_pubonly_fails(void); int test_wc_mlkem_decap_fo_reject(void); int test_wc_mlkem_decode_privkey_bad_pubhash(void); +int test_wc_MlkemFeatureCoverage(void); +int test_wc_MlkemDecisionCoverage(void); +int test_wc_mlkem_init_id_decision(void); +int test_wc_mlkem_init_label_decision(void); +int test_wc_mlkem_encapsulate_pubkey_unset_decision(void); +int test_wc_mlkem_encode_key_len_decision(void); #define TEST_MLKEM_DECLS \ TEST_DECL_GROUP("mlkem", test_wc_mlkem_make_key_kats), \ @@ -37,6 +43,12 @@ int test_wc_mlkem_decode_privkey_bad_pubhash(void); TEST_DECL_GROUP("mlkem", test_wc_mlkem_decapsulate_kats), \ TEST_DECL_GROUP("mlkem", test_wc_mlkem_decapsulate_pubonly_fails), \ TEST_DECL_GROUP("mlkem", test_wc_mlkem_decap_fo_reject), \ - TEST_DECL_GROUP("mlkem", test_wc_mlkem_decode_privkey_bad_pubhash) + TEST_DECL_GROUP("mlkem", test_wc_mlkem_decode_privkey_bad_pubhash), \ + TEST_DECL_GROUP("mlkem", test_wc_MlkemFeatureCoverage), \ + TEST_DECL_GROUP("mlkem", test_wc_MlkemDecisionCoverage), \ + TEST_DECL_GROUP("mlkem", test_wc_mlkem_init_id_decision), \ + TEST_DECL_GROUP("mlkem", test_wc_mlkem_init_label_decision), \ + TEST_DECL_GROUP("mlkem", test_wc_mlkem_encapsulate_pubkey_unset_decision), \ + TEST_DECL_GROUP("mlkem", test_wc_mlkem_encode_key_len_decision) #endif /* WOLFCRYPT_TEST_MLKEM_H */ diff --git a/tests/api/test_pwdbased.c b/tests/api/test_pwdbased.c index 92a144c42ce..6009a1f08f3 100644 --- a/tests/api/test_pwdbased.c +++ b/tests/api/test_pwdbased.c @@ -71,3 +71,200 @@ int test_wc_PBKDF2_ex_iterations(void) #endif return EXPECT_RESULT(); } + +/* MC/DC decision coverage of wc_PBKDF1_ex: independence pair for each operand + * of the argument-check OR, the iteration ceiling, the invalid-hash path, and + * one valid derivation. */ +int test_wc_PBKDF1DecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_PBKDF1) && !defined(NO_PWDBASED) && !defined(NO_SHA) && \ + !defined(HAVE_SELFTEST) && (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) + static const byte passwd[] = { 'p', 'a', 's', 's' }; + static const byte salt[] = { 0x78, 0x57, 0x8E, 0x5a, + 0x5d, 0x63, 0xcb, 0x06 }; + byte derived[16] = {0}; + int prevMax; + + /* argument-check OR: key==NULL || keyLen<0 || passwdLen<0 || saltLen<0 || + * ivLen<0 -- vary each operand alone (others valid). */ + ExpectIntEQ(wc_PBKDF1_ex(NULL, (int)sizeof(derived), NULL, 0, + passwd, (int)sizeof(passwd), salt, (int)sizeof(salt), + 2, WC_SHA, HEAP_HINT), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PBKDF1_ex(derived, -1, NULL, 0, + passwd, (int)sizeof(passwd), salt, (int)sizeof(salt), + 2, WC_SHA, HEAP_HINT), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PBKDF1_ex(derived, (int)sizeof(derived), NULL, 0, + passwd, -1, salt, (int)sizeof(salt), + 2, WC_SHA, HEAP_HINT), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PBKDF1_ex(derived, (int)sizeof(derived), NULL, 0, + passwd, (int)sizeof(passwd), salt, -1, + 2, WC_SHA, HEAP_HINT), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PBKDF1_ex(derived, (int)sizeof(derived), NULL, -1, + passwd, (int)sizeof(passwd), salt, (int)sizeof(salt), + 2, WC_SHA, HEAP_HINT), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* iterations <= 0 */ + ExpectIntEQ(wc_PBKDF1_ex(derived, (int)sizeof(derived), NULL, 0, + passwd, (int)sizeof(passwd), salt, (int)sizeof(salt), + 0, WC_SHA, HEAP_HINT), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* iterations > current max */ + prevMax = wc_PBKDF_max_iterations_get(); + ExpectIntGT(wc_PBKDF_max_iterations_set(2), 0); + ExpectIntEQ(wc_PBKDF1_ex(derived, (int)sizeof(derived), NULL, 0, + passwd, (int)sizeof(passwd), salt, (int)sizeof(salt), + 3, WC_SHA, HEAP_HINT), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntGT(wc_PBKDF_max_iterations_set(prevMax), 0); + + /* invalid hash type -> error out of wc_HashGetDigestSize (not 0) */ + ExpectIntNE(wc_PBKDF1_ex(derived, (int)sizeof(derived), NULL, 0, + passwd, (int)sizeof(passwd), salt, (int)sizeof(salt), + 2, WC_HASH_TYPE_NONE, HEAP_HINT), 0); + + /* valid derivation (keyLen spanning >1 digest block) */ + ExpectIntEQ(wc_PBKDF1_ex(derived, (int)sizeof(derived), NULL, 0, + passwd, (int)sizeof(passwd), salt, (int)sizeof(salt), + 2, WC_SHA, HEAP_HINT), 0); + + /* `if (ivLeft && digestLeft)` independence pairs. keyLen == digestLen + * (20 for SHA-1) with ivLen > 0: the first D-iteration exactly + * exhausts the digest into the key (digestLeft becomes 0), so + * ivLeft(true) && digestLeft(false) -> false; the following + * iteration resets digestLeft and drains the iv, so + * ivLeft(true) && digestLeft(true) -> true. Paired with the + * ivLen == 0 calls above (ivLeft always false) for the ivLeft + * operand's independence pair. */ + { + byte key20[20] = {0}; + byte iv8[8] = {0}; + + ExpectIntEQ(wc_PBKDF1_ex(key20, (int)sizeof(key20), iv8, + (int)sizeof(iv8), passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, WC_SHA, HEAP_HINT), 0); + } +#endif + return EXPECT_RESULT(); +} + +/* MC/DC decision coverage of wc_PBKDF2_ex argument checks + edges. */ +int test_wc_PBKDF2DecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_PBKDF2) && !defined(NO_PWDBASED) && !defined(NO_HMAC) && \ + !defined(NO_SHA256) && !defined(HAVE_SELFTEST) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) + static const byte passwd[] = { 'p', 'a', 's', 's' }; + static const byte salt[] = { 0x78, 0x57, 0x8E, 0x5a, + 0x5d, 0x63, 0xcb, 0x06 }; + byte derived[24] = {0}; + int prevMax; + + /* output==NULL || pLen<0 || sLen<0 || kLen<0 -- vary each operand alone. */ + ExpectIntEQ(wc_PBKDF2_ex(NULL, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, (int)sizeof(derived), + WC_SHA256, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PBKDF2_ex(derived, passwd, -1, + salt, (int)sizeof(salt), 2, (int)sizeof(derived), + WC_SHA256, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PBKDF2_ex(derived, passwd, (int)sizeof(passwd), + salt, -1, 2, (int)sizeof(derived), + WC_SHA256, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PBKDF2_ex(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, -1, + WC_SHA256, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* iterations <= 0 */ + ExpectIntEQ(wc_PBKDF2_ex(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 0, (int)sizeof(derived), + WC_SHA256, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* iterations > current max */ + prevMax = wc_PBKDF_max_iterations_get(); + ExpectIntGT(wc_PBKDF_max_iterations_set(2), 0); + ExpectIntEQ(wc_PBKDF2_ex(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 3, (int)sizeof(derived), + WC_SHA256, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntGT(wc_PBKDF_max_iterations_set(prevMax), 0); + + /* invalid hash type -> hLen<0 -> BAD_FUNC_ARG */ + ExpectIntEQ(wc_PBKDF2_ex(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, (int)sizeof(derived), + WC_HASH_TYPE_NONE, HEAP_HINT, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* valid derivation */ + ExpectIntEQ(wc_PBKDF2_ex(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, (int)sizeof(derived), + WC_SHA256, HEAP_HINT, INVALID_DEVID), 0); +#endif + return EXPECT_RESULT(); +} + +/* MC/DC decision coverage of wc_PKCS12_PBKDF argument checks + edges. */ +int test_wc_PKCS12_PBKDFDecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_PKCS12) && !defined(NO_PWDBASED) && !defined(NO_SHA256) && \ + !defined(HAVE_SELFTEST) && (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) + static const byte passwd[] = { 'p', 'a', 's', 's' }; + static const byte salt[] = { 0x78, 0x57, 0x8E, 0x5a, + 0x5d, 0x63, 0xcb, 0x06 }; + byte derived[24] = {0}; + int prevMax; + const int id = 1; /* key material id (RFC 7292) */ + + /* output==NULL || passLen<=0 || saltLen<=0 || kLen<0 -- vary each. */ + ExpectIntEQ(wc_PKCS12_PBKDF(NULL, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, (int)sizeof(derived), + WC_SHA256, id), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PKCS12_PBKDF(derived, passwd, 0, + salt, (int)sizeof(salt), 2, (int)sizeof(derived), + WC_SHA256, id), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PKCS12_PBKDF(derived, passwd, (int)sizeof(passwd), + salt, 0, 2, (int)sizeof(derived), + WC_SHA256, id), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_PKCS12_PBKDF(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, -1, + WC_SHA256, id), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* iterations <= 0 */ + ExpectIntEQ(wc_PKCS12_PBKDF(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 0, (int)sizeof(derived), + WC_SHA256, id), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* iterations > current max */ + prevMax = wc_PBKDF_max_iterations_get(); + ExpectIntGT(wc_PBKDF_max_iterations_set(2), 0); + ExpectIntEQ(wc_PKCS12_PBKDF(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 3, (int)sizeof(derived), + WC_SHA256, id), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntGT(wc_PBKDF_max_iterations_set(prevMax), 0); + + /* invalid hash type -> error out of wc_HashGetDigestSize (not 0) */ + ExpectIntNE(wc_PKCS12_PBKDF(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, (int)sizeof(derived), + WC_HASH_TYPE_NONE, id), 0); + + /* valid derivation */ + ExpectIntEQ(wc_PKCS12_PBKDF(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, (int)sizeof(derived), + WC_SHA256, id), 0); + + /* inner `while ((ret == 0) && (kLen > 0))` independence pair for the + * kLen operand: kLen == 0 is a valid request (not caught by the + * kLen < 0 arg-check) so the loop body never executes and + * kLen > 0 evaluates false with ret == 0 held true, paired with the + * "valid derivation" call above where kLen > 0 evaluates true. */ + ExpectIntEQ(wc_PKCS12_PBKDF(derived, passwd, (int)sizeof(passwd), + salt, (int)sizeof(salt), 2, 0, + WC_SHA256, id), 0); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_pwdbased.h b/tests/api/test_pwdbased.h index d3dd361baf4..ae3ababb848 100644 --- a/tests/api/test_pwdbased.h +++ b/tests/api/test_pwdbased.h @@ -26,9 +26,15 @@ int test_wc_PBKDF1_ex_iterations(void); int test_wc_PBKDF2_ex_iterations(void); +int test_wc_PBKDF1DecisionCoverage(void); +int test_wc_PBKDF2DecisionCoverage(void); +int test_wc_PKCS12_PBKDFDecisionCoverage(void); #define TEST_PWDBASED_DECLS \ TEST_DECL_GROUP("pwdbased", test_wc_PBKDF1_ex_iterations), \ - TEST_DECL_GROUP("pwdbased", test_wc_PBKDF2_ex_iterations) + TEST_DECL_GROUP("pwdbased", test_wc_PBKDF2_ex_iterations), \ + TEST_DECL_GROUP("pwdbased", test_wc_PBKDF1DecisionCoverage), \ + TEST_DECL_GROUP("pwdbased", test_wc_PBKDF2DecisionCoverage), \ + TEST_DECL_GROUP("pwdbased", test_wc_PKCS12_PBKDFDecisionCoverage) #endif /* WOLFCRYPT_TEST_PWDBASED_H */ diff --git a/tests/api/test_random.c b/tests/api/test_random.c index 848c4981d6a..a4c3732050b 100644 --- a/tests/api/test_random.c +++ b/tests/api/test_random.c @@ -297,6 +297,22 @@ int test_wc_GenerateSeed(void) /* Different configurations have different paths and different errors or * no error at all. */ #ifdef TEST_WC_GENERATE_SEED_PARAMS + /* NOTE (GAPS.md residual, line ~5525 "os == NULL || output == NULL"): + * TEST_WC_GENERATE_SEED_PARAMS is not defined by any variant in + * configs/random/ today. Its header comment cites a real historical + * bug -- the generic Linux getrandom()/dev-urandom wc_GenerateSeed() + * arm's vDSO getrandom() fast path used to segfault on a NULL output + * buffer instead of returning an error. That bug was fixed by + * "random: reject NULL output in Unix wc_GenerateSeed" (adds this + * exact "os == NULL || output == NULL" guard ahead of any backend + * dispatch), and empirically (native --enable-all build, getrandom() + * backend) both wc_GenerateSeed(NULL, output, sz) and + * wc_GenerateSeed(os, NULL, sz) now return BAD_FUNC_ARG cleanly with no + * crash. Defining TEST_WC_GENERATE_SEED_PARAMS in + * configs/random/user_settings.base.h (none of this module's variants + * select a different OS/HW entropy backend) would safely close this + * residual; left undefined here since gap-closing tasks don't modify + * the shared campaign config headers -- flagged for the orchestrator. */ /* Bad parameters. */ ExpectIntEQ(wc_GenerateSeed(NULL, NULL , 16), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); @@ -1175,3 +1191,154 @@ int test_wc_Entropy_Get(void) #endif /* HAVE_ENTROPY_MEMUSE */ return EXPECT_RESULT(); } + +/* Consolidated MC/DC decision coverage for the public Hash_DRBG argument + * checks that gate the generate/reseed paths: each compound guard is driven + * with an independence pair (vary one operand at a time) and paired with a + * passing baseline call in the same run. Guarded off for the frozen + * FIPS/self-test random.c: several of these argument-rejection paths and the + * "sz == 0" early success were added after the v4.1.0 module boundary, so + * asserting them there would diverge (frozen-module lesson). */ +int test_wc_DrbgDecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_HASHDRBG) && !defined(WC_NO_RNG) && \ + !defined(CUSTOM_RAND_GENERATE_BLOCK) && \ + !defined(HAVE_SELFTEST) && !defined(HAVE_FIPS) + WC_RNG rng; + byte output[24]; + byte seed[32]; + + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(output, 0, sizeof(output)); + XMEMSET(seed, 7, sizeof(seed)); + + /* wc_RNG_GenerateByte() delegates to wc_RNG_GenerateBlock(rng, b, 1): + * "rng == NULL || output == NULL" -- flip each operand alone. */ + ExpectIntEQ(wc_RNG_GenerateByte(NULL, output), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* rng NULL */ + + ExpectIntEQ(wc_InitRng_ex(&rng, HEAP_HINT, INVALID_DEVID), 0); + + ExpectIntEQ(wc_RNG_GenerateByte(&rng, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* output NULL */ + ExpectIntEQ(wc_RNG_GenerateByte(&rng, output), 0); /* both non-NULL */ + + /* wc_RNG_GenerateBlock(): NULL rng rejected; "sz == 0" is the early + * success that never enters the DRBG generate path; a non-zero request + * takes the generate path. */ + ExpectIntEQ(wc_RNG_GenerateBlock(NULL, output, sizeof(output)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, output, 0), 0); /* sz==0 */ + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, output, sizeof(output)), 0); + + /* wc_RNG_DRBG_Reseed(): "rng == NULL || seed == NULL" independence pair + * then a valid reseed on the initialised RNG (success side). */ + ExpectIntEQ(wc_RNG_DRBG_Reseed(NULL, seed, sizeof(seed)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_RNG_DRBG_Reseed(&rng, NULL, sizeof(seed)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_RNG_DRBG_Reseed(&rng, seed, sizeof(seed)), 0); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); +#endif + return EXPECT_RESULT(); +} + +/* Positive-path feature coverage that drives the Hash_DRBG_Generate output + * loop and the reseed-interval-exceeded decision through the public API. + * Varying the requested size exercises the per-block copy-out branch + * ("outSz > OUTPUT_BLOCK_LEN" true for multi-block, false for a sub-block + * tail); the reseed-interval-exceeded (DRBG_NEED_RESEED -> PollAndReSeed) + * branch is forced by setting the active DRBG's reseedCtr to + * WC_RESEED_INTERVAL - 1 before a generate (same idiom as + * test_wc_RNG_ReseedBoundary) -- the default interval (1,000,000) is far + * beyond a bounded test loop, so a simple burst would NOT reach it. + * Repeated under both DRBG hash widths when SHA-512 is compiled in. */ +int test_wc_DrbgFeatureCoverage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_HASHDRBG) && !defined(WC_NO_RNG) && \ + !defined(CUSTOM_RAND_GENERATE_BLOCK) && \ + !defined(HAVE_SELFTEST) && !defined(HAVE_FIPS) + WC_RNG rng; + byte big[256]; + static const word32 sizes[] = { 1, 15, 16, 31, 32, 55, 64, 120, 250 }; + word32 i; + int j; + + for (j = 0; j < 2; j++) { + #if defined(WOLFSSL_DRBG_SHA512) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) + /* j==0: SHA-256 width (disable SHA-512); j==1: SHA-512 width. */ + if (j == 0) + (void)wc_Sha512Drbg_Disable(); + else + (void)wc_Sha256Drbg_Disable(); + #else + if (j == 1) + break; /* only one width compiled in */ + #endif + + XMEMSET(&rng, 0, sizeof(rng)); + ExpectIntEQ(wc_InitRng_ex(&rng, HEAP_HINT, INVALID_DEVID), 0); + + for (i = 0; i < (word32)(sizeof(sizes) / sizeof(sizes[0])); i++) { + XMEMSET(big, 0, sizeof(big)); + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, big, sizes[i]), 0); + } + /* Force the reseed-interval-exceeded path. The default + * WC_RESEED_INTERVAL (1,000,000) is unreachable in a bounded loop, so + * set the active DRBG's reseedCtr just below the limit and generate + * across it, taking DRBG_NEED_RESEED -> PollAndReSeed (same idiom as + * test_wc_RNG_ReseedBoundary). Guarded via a probe generate so configs + * that bypass the Hash_DRBG path (e.g. --enable-intelrand) skip it. */ + #ifndef NO_SHA256 + if (rng.drbgType == WC_DRBG_SHA256) { + struct DRBG_internal* drbg = (struct DRBG_internal*)rng.drbg; + if (drbg != NULL && rng.status == WC_DRBG_OK) { + #ifdef WORD64_AVAILABLE + word64 startCtr = drbg->reseedCtr; + #else + word32 startCtr = drbg->reseedCtr; + #endif + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, big, 32), 0); + if (drbg->reseedCtr == startCtr + 1) { + drbg->reseedCtr = WC_RESEED_INTERVAL - 1; + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, big, 32), 0); + ExpectTrue(drbg->reseedCtr == WC_RESEED_INTERVAL); + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, big, 32), 0); + ExpectTrue(drbg->reseedCtr == 2); + } + } + } + #endif + #ifdef WOLFSSL_DRBG_SHA512 + if (rng.drbgType == WC_DRBG_SHA512) { + struct DRBG_SHA512_internal* drbg512 = + (struct DRBG_SHA512_internal*)rng.drbg512; + if (drbg512 != NULL && rng.status == WC_DRBG_OK) { + word64 startCtr = drbg512->reseedCtr; + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, big, 32), 0); + if (drbg512->reseedCtr == startCtr + 1) { + drbg512->reseedCtr = WC_RESEED_INTERVAL - 1; + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, big, 32), 0); + ExpectTrue(drbg512->reseedCtr == WC_RESEED_INTERVAL); + ExpectIntEQ(wc_RNG_GenerateBlock(&rng, big, 32), 0); + ExpectTrue(drbg512->reseedCtr == 2); + } + } + } + #endif + DoExpectIntEQ(wc_FreeRng(&rng), 0); + + #if defined(WOLFSSL_DRBG_SHA512) && \ + (!defined(HAVE_FIPS) || FIPS_VERSION3_GE(7,0,0)) + /* Restore both widths for later tests sharing this process. */ + (void)wc_Sha256Drbg_Enable(); + (void)wc_Sha512Drbg_Enable(); + #endif + } +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_random.h b/tests/api/test_random.h index 7eba9816866..1a6d43229ec 100644 --- a/tests/api/test_random.h +++ b/tests/api/test_random.h @@ -42,6 +42,8 @@ int test_wc_RNG_HealthTest_SHA512_Ext(void); int test_wc_RNG_SeedCb(void); int test_wc_RNG_CustomRandBlock(void); int test_wc_RNG_DrbgDisable(void); +int test_wc_DrbgDecisionCoverage(void); +int test_wc_DrbgFeatureCoverage(void); int test_wc_Entropy_Get(void); #define TEST_RANDOM_DECLS \ @@ -63,6 +65,8 @@ int test_wc_Entropy_Get(void); TEST_DECL_GROUP("random", test_wc_RNG_SeedCb), \ TEST_DECL_GROUP("random", test_wc_RNG_CustomRandBlock), \ TEST_DECL_GROUP("random", test_wc_RNG_DrbgDisable), \ + TEST_DECL_GROUP("random", test_wc_DrbgDecisionCoverage), \ + TEST_DECL_GROUP("random", test_wc_DrbgFeatureCoverage), \ TEST_DECL_GROUP("random", test_wc_Entropy_Get) #endif /* WOLFCRYPT_TEST_RANDOM_H */ diff --git a/tests/api/test_signature.c b/tests/api/test_signature.c index 5a25917e725..1cd97d5e5b3 100644 --- a/tests/api/test_signature.c +++ b/tests/api/test_signature.c @@ -33,6 +33,8 @@ #include #include #include +#include +#include #ifdef HAVE_FALCON #include #endif @@ -208,3 +210,608 @@ int test_wc_falcon_sign_verify(void) #endif return EXPECT_RESULT(); } + +/* Decision coverage for the wc_Signature{Verify,Generate}{,Hash,_ex}() + * dispatch wrapper: argument-check independence pairs, sig_type switch + * dispatch (including the sig_type-NONE/default arm and unsupported-type + * arms), hash-type/size validation, and the RSA_W_ENC-specific ASN.1 + * decode-length checks. + * + * Also covers, for the ECC and plain-RSA verify paths in + * wolfcrypt/src/signature.c: + * - "(ret != 0 || is_valid_sig != 1)" (ECC): both independence pairs, + * via a corrupted-signature call (ret != 0, masking is_valid_sig) and + * a wrong-data call (ret == 0, is_valid_sig == 0). + * - "((word32)ret == hash_len && XMEMCMP(...) == 0)" (RSA): the + * XMEMCMP-operand independence pair, via a wrong-data call that + * decrypts to a correct-length but mismatching plaintext. + * NOT closeable: "(ret >= 0 && plain_ptr)" (RSA) - in this software-only + * build (no crypto-cb/hardware backend), wc_RsaSSL_VerifyInline() only + * ever assigns plain_ptr when it returns ret >= 0 (see wc_RsaUnPad_ex's + * caller in rsa.c), so the two operands are perfectly coupled and cannot + * be toggled independently: a structurally-complementary pair. */ +int test_wc_SignatureDecisionCoverage(void) +{ + EXPECT_DECLS; +#ifndef NO_SIG_WRAPPER +#ifdef HAVE_ECC + { + ecc_key ecc; + enum wc_SignatureType sig_type = WC_SIGNATURE_TYPE_ECC; + word32 key_len = (word32)sizeof(ecc_key); + int eccSigMax = 0; + /* Non-zero: wc_ecc_sign_hash() rejects an all-0's digest with + * ECC_BAD_ARG_E (unless WC_ALLOW_ECC_ZERO_HASH), and this buffer + * is used both for arg-check calls (content irrelevant) and for + * a genuine sign at the end of this block. */ + byte hash[WC_SHA256_DIGEST_SIZE]; + word32 hash_len = (word32)sizeof(hash); + byte sig[128] = {0}; + word32 sig_len; + const char* qx = + "fa2737fb93488d19caef11ae7faf6b7f4bcd67b286e3fc54e8a65c2b74aeccb0"; + const char* qy = + "d4ccd6dae698208aa8c3a6f39e45510d03be09b2f124bfc067856c324f9b4d09"; + const char* d = + "be34baa8d040a3b991f9075b56ba292f755b90e4b6dc10dad36715c33cfdac25"; + + XMEMSET(&ecc, 0, sizeof(ecc)); + XMEMSET(hash, 0x11, sizeof(hash)); + ExpectIntEQ(wc_ecc_init(&ecc), 0); + ExpectIntEQ(wc_ecc_import_raw(&ecc, qx, qy, d, "SECP256R1"), 0); + ExpectIntGT(eccSigMax = wc_SignatureGetSize(sig_type, &ecc, key_len), + 0); + sig_len = (word32)eccSigMax; + + /* wc_SignatureVerifyHash(): argument-check independence pairs. + * Each call flips exactly one operand of the arg-check OR-chain + * while holding the others at a valid value. */ + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, sig_type, + NULL, hash_len, sig, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, sig_type, + hash, 0, sig, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, sig_type, + hash, hash_len, NULL, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, sig_type, + hash, hash_len, sig, 0, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, sig_type, + hash, hash_len, sig, sig_len, NULL, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, sig_type, + hash, hash_len, sig, sig_len, &ecc, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* sig_len greater than wc_SignatureGetSize() allows */ + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, sig_type, + hash, hash_len, sig, (word32)(eccSigMax + 100000), &ecc, + key_len), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Invalid/unsupported hash type: wc_HashGetDigestSize() < 0 */ + ExpectIntEQ(wc_SignatureVerifyHash((enum wc_HashType)999, sig_type, + hash, hash_len, sig, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* hash_len mismatch on the non-RSA_W_ENC path -> BAD_LENGTH_E */ + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, sig_type, + hash, (word32)(hash_len - 1), sig, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); + + /* wc_SignatureVerify(): same argument-check independence pairs at + * the data (pre-hash) level, plus a genuine sign/verify-mismatch + * (SIG_VERIFY_E) and the weak-hash rejection. */ + { + byte data[16]; + word32 data_len = (word32)sizeof(data); + WC_RNG rng; + + XMEMSET(data, 0x5A, sizeof(data)); + ExpectIntEQ(wc_InitRng(&rng), 0); + + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + NULL, data_len, sig, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + data, 0, sig, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, NULL, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, sig, 0, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, sig, sig_len, NULL, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, sig, sig_len, &ecc, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* sig_len too large for the key/sig_type */ + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, sig, (word32)(eccSigMax + 100000), &ecc, + key_len), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Invalid hash type */ + ExpectIntEQ(wc_SignatureVerify((enum wc_HashType)999, sig_type, + data, data_len, sig, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + +#ifndef NO_SHA + /* Hash weaker than WC_SIG_MIN_HASH_TYPE (default SHA-256) + * rejected by wc_SignatureCheckHashStrength() */ + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA, sig_type, + data, data_len, sig, sig_len, &ecc, key_len), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + + /* Real signature that fails verification: SIG_VERIFY_E. + * Corrupting the leading DER SEQUENCE-tag byte makes + * wc_ecc_verify_hash() itself fail (ret != 0): independence + * pair (true side) for the "ret != 0" operand of + * "(ret != 0 || is_valid_sig != 1)", masking is_valid_sig. */ + { + byte realSig[128] = {0}; + word32 realSigLen = (word32)sizeof(realSig); + + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, + sig_type, data, data_len, realSig, &realSigLen, &ecc, + key_len, &rng), 0); + realSig[0] = (byte)(realSig[0] ^ 0xFF); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, realSig, realSigLen, &ecc, key_len), + WC_NO_ERR_TRACE(SIG_VERIFY_E)); + } + + /* A syntactically-valid signature (untouched DER encoding) that + * mathematically fails verification because it was generated + * over DIFFERENT data: wc_ecc_verify_hash() itself returns + * ret == 0 but sets is_valid_sig to 0 -> independence pair + * (true side) for the "is_valid_sig != 1" operand, with + * "ret != 0" held false this time. */ + { + byte otherData[16]; + byte realSig2[128] = {0}; + word32 realSig2Len = (word32)sizeof(realSig2); + + XMEMSET(otherData, 0xA5, sizeof(otherData)); + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, + sig_type, data, data_len, realSig2, &realSig2Len, &ecc, + key_len, &rng), 0); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + otherData, (word32)sizeof(otherData), realSig2, + realSig2Len, &ecc, key_len), + WC_NO_ERR_TRACE(SIG_VERIFY_E)); + } + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + } + + /* wc_SignatureGenerateHash_ex(): argument checks (mirrors the + * VerifyHash pairs above, plus the sig / sig_len pointer split), + * the sig_type switch default arm (reachable here, unlike in + * VerifyHash, because the size guard is '*sig_len < GetSize()' + * rather than 'sig_len > GetSize()': a positive *sig_len is never + * less than GetSize()'s negative BAD_FUNC_ARG for an invalid + * sig_type), and the verify-flag independence pair. */ + { + byte genSig[128] = {0}; + word32 genSigLen = (word32)eccSigMax; + WC_RNG rng; + + ExpectIntEQ(wc_InitRng(&rng), 0); + + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, NULL, hash_len, genSig, &genSigLen, &ecc, key_len, + &rng, 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, hash, 0, genSig, &genSigLen, &ecc, key_len, + &rng, 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, hash, hash_len, NULL, &genSigLen, &ecc, key_len, + &rng, 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, hash, hash_len, genSig, NULL, &ecc, key_len, + &rng, 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + { + word32 zeroLen = 0; + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, hash, hash_len, genSig, &zeroLen, &ecc, + key_len, &rng, 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, hash, hash_len, genSig, &genSigLen, NULL, key_len, + &rng, 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, hash, hash_len, genSig, &genSigLen, &ecc, 0, + &rng, 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* *sig_len too small for key/sig_type */ + { + word32 tinyLen = 1; + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, hash, hash_len, genSig, &tinyLen, &ecc, + key_len, &rng, 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + /* Invalid hash type */ + { + word32 lenCopy = (word32)eccSigMax; + ExpectIntEQ(wc_SignatureGenerateHash_ex( + (enum wc_HashType)999, sig_type, hash, hash_len, genSig, + &lenCopy, &ecc, key_len, &rng, 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + /* sig_type NONE reaches the switch() default arm (see comment + * above); asserts a genuinely different decision than the + * arg-check/size-check BAD_FUNC_ARG returns above. */ + { + word32 lenCopy = (word32)eccSigMax; + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + WC_SIGNATURE_TYPE_NONE, hash, hash_len, genSig, &lenCopy, + &ecc, key_len, &rng, 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + /* verify == 0: skip the internal round-trip verify even though + * ret == 0 (independence pair on the verify operand; the ret + * operand is exercised by the BAD_FUNC_ARG cases above, all + * called with verify == 1). */ + genSigLen = (word32)eccSigMax; + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, hash, hash_len, genSig, &genSigLen, &ecc, key_len, + &rng, 0), 0); + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + } + + /* wc_SignatureGenerate(): mirrors the argument checks above at the + * data (pre-hash) level. */ + { + byte data[16]; + word32 data_len = (word32)sizeof(data); + byte genSig[128] = {0}; + word32 genSigLen = (word32)eccSigMax; + WC_RNG rng; + + XMEMSET(data, 0xA5, sizeof(data)); + ExpectIntEQ(wc_InitRng(&rng), 0); + + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, sig_type, + NULL, data_len, genSig, &genSigLen, &ecc, key_len, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, sig_type, + data, 0, genSig, &genSigLen, &ecc, key_len, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, NULL, &genSigLen, &ecc, key_len, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, genSig, NULL, &ecc, key_len, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + { + word32 zeroLen = 0; + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, + sig_type, data, data_len, genSig, &zeroLen, &ecc, + key_len, &rng), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, genSig, &genSigLen, NULL, key_len, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, genSig, &genSigLen, &ecc, 0, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* *sig_len too small */ + { + word32 tinyLen = 1; + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, + sig_type, data, data_len, genSig, &tinyLen, &ecc, + key_len, &rng), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + /* Invalid hash type */ + { + word32 lenCopy = (word32)eccSigMax; + ExpectIntEQ(wc_SignatureGenerate((enum wc_HashType)999, + sig_type, data, data_len, genSig, &lenCopy, &ecc, + key_len, &rng), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + +#ifndef NO_SHA + /* Weak hash rejected before any hashing/signing occurs */ + { + word32 lenCopy = (word32)eccSigMax; + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA, sig_type, + data, data_len, genSig, &lenCopy, &ecc, key_len, &rng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + } + + DoExpectIntEQ(wc_ecc_free(&ecc), 0); + } +#endif /* HAVE_ECC */ + +#ifndef NO_RSA + { + RsaKey rsa_key; + word32 idx = 0; + byte* tmp = NULL; + size_t bytes; + word32 key_len = (word32)sizeof(RsaKey); + int rsaSigMax = 0; + byte hashGarbage[32]; + byte sigBuf[1] = {0}; + + XMEMSET(&rsa_key, 0, sizeof(RsaKey)); + XMEMSET(hashGarbage, 0xEE, sizeof(hashGarbage)); + + #ifdef USE_CERT_BUFFERS_1024 + bytes = (size_t)sizeof_client_key_der_1024; + #elif defined(USE_CERT_BUFFERS_2048) + bytes = (size_t)sizeof_client_key_der_2048; + #else + bytes = FOURK_BUF; + #endif + + ExpectNotNull(tmp = (byte*)XMALLOC(bytes, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + if (tmp != NULL) { + #ifdef USE_CERT_BUFFERS_1024 + XMEMCPY(tmp, client_key_der_1024, + (size_t)sizeof_client_key_der_1024); + #elif defined(USE_CERT_BUFFERS_2048) + XMEMCPY(tmp, client_key_der_2048, + (size_t)sizeof_client_key_der_2048); + #elif !defined(NO_FILESYSTEM) + XFILE file = XBADFILE; + ExpectTrue((file = XFOPEN(clientKey, "rb")) != XBADFILE); + ExpectIntGT(bytes = (size_t)XFREAD(tmp, 1, FOURK_BUF, file), 0); + if (file != XBADFILE) { + XFCLOSE(file); + } + #else + ExpectFail(); + #endif + } + + ExpectIntEQ(wc_InitRsaKey_ex(&rsa_key, HEAP_HINT, testDevId), 0); + ExpectIntEQ(wc_RsaPrivateKeyDecode(tmp, &idx, &rsa_key, + (word32)bytes), 0); + ExpectIntGT(rsaSigMax = wc_SignatureGetSize( + WC_SIGNATURE_TYPE_RSA_W_ENC, &rsa_key, key_len), 0); + + /* Non-DER hash_data for RSA_W_ENC: GetSequence() fails to find the + * leading SEQUENCE tag -> ASN_PARSE_E */ + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, + WC_SIGNATURE_TYPE_RSA_W_ENC, hashGarbage, + (word32)sizeof(hashGarbage), sigBuf, (word32)sizeof(sigBuf), + &rsa_key, key_len), WC_NO_ERR_TRACE(ASN_PARSE_E)); + +#if !defined(NO_ASN) && !defined(NO_SHA) + /* Well-formed DigestInfo (built for a SHA-256-sized digest) but + * decoded/verified as SHA-1: the decoded OCTET STRING length (32) + * mismatches wc_HashGetDigestSize(WC_HASH_TYPE_SHA) (20) -> + * BAD_LENGTH_E */ + { + byte digest[WC_SHA256_DIGEST_SIZE] = {0}; + byte encoded[MAX_DER_DIGEST_SZ] = {0}; + int oid = 0; + word32 encLen = 0; + + XMEMSET(digest, 0x11, sizeof(digest)); + ExpectIntGE(oid = wc_HashGetOID(WC_HASH_TYPE_SHA256), 0); + ExpectIntGT((int)(encLen = wc_EncodeSignature(encoded, digest, + (word32)sizeof(digest), oid)), 0); + + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA, + WC_SIGNATURE_TYPE_RSA_W_ENC, encoded, encLen, sigBuf, + (word32)sizeof(sigBuf), &rsa_key, key_len), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); + } +#endif /* !NO_ASN && !NO_SHA */ + + /* Plain RSA: a syntactically-valid signature whose decrypted, + * unpadded plaintext is exactly hash_len bytes (so + * "(word32)ret == hash_len" is true) but whose content does not + * match hash_data, because the signature was generated over + * DIFFERENT data: independence pair (false side) for the + * "XMEMCMP(...) == 0" operand, with "(word32)ret == hash_len" held + * true -> SIG_VERIFY_E. The true/true (matching) side is exercised + * by the plain-RSA round trip in test_wc_SignatureFeatureCoverage(). + */ + { + WC_RNG rng; + byte data[16]; + byte otherData[16]; + word32 data_len = (word32)sizeof(data); + byte realSig[512] = {0}; + word32 realSigLen = (word32)sizeof(realSig); + + XMEMSET(data, 0x24, sizeof(data)); + XMEMSET(otherData, 0x99, sizeof(otherData)); + ExpectIntEQ(wc_InitRng(&rng), 0); + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, + WC_SIGNATURE_TYPE_RSA, data, data_len, realSig, &realSigLen, + &rsa_key, key_len, &rng), 0); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, + WC_SIGNATURE_TYPE_RSA, otherData, (word32)sizeof(otherData), + realSig, realSigLen, &rsa_key, key_len), + WC_NO_ERR_TRACE(SIG_VERIFY_E)); + DoExpectIntEQ(wc_FreeRng(&rng), 0); + } + + DoExpectIntEQ(wc_FreeRsaKey(&rsa_key), 0); + XFREE(tmp, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } +#endif /* !NO_RSA */ +#endif /* !NO_SIG_WRAPPER */ + return EXPECT_RESULT(); +} /* END test_wc_SignatureDecisionCoverage() */ + +/* Feature/positive-path coverage for wc_Signature{Verify,Generate}{,Hash,_ex}(): + * full sign+verify round trips for each supported sig_type (ECC, RSA, + * RSA_W_ENC), and the verify-flag "true" independence pair (the "false" + * pair is exercised in test_wc_SignatureDecisionCoverage()). */ +int test_wc_SignatureFeatureCoverage(void) +{ + EXPECT_DECLS; +#ifndef NO_SIG_WRAPPER +#ifdef HAVE_ECC + { + ecc_key ecc; + WC_RNG rng; + enum wc_SignatureType sig_type = WC_SIGNATURE_TYPE_ECC; + word32 key_len = (word32)sizeof(ecc_key); + byte data[32]; + word32 data_len = (word32)sizeof(data); + byte sig[128] = {0}; + word32 sig_len; + const char* qx = + "fa2737fb93488d19caef11ae7faf6b7f4bcd67b286e3fc54e8a65c2b74aeccb0"; + const char* qy = + "d4ccd6dae698208aa8c3a6f39e45510d03be09b2f124bfc067856c324f9b4d09"; + const char* d = + "be34baa8d040a3b991f9075b56ba292f755b90e4b6dc10dad36715c33cfdac25"; + + XMEMSET(&ecc, 0, sizeof(ecc)); + XMEMSET(data, 0x42, sizeof(data)); + + ExpectIntEQ(wc_ecc_init(&ecc), 0); + ExpectIntEQ(wc_ecc_import_raw(&ecc, qx, qy, d, "SECP256R1"), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + + /* wc_SignatureGetSize(): positive path for ECC */ + ExpectIntGT(wc_SignatureGetSize(sig_type, &ecc, key_len), 0); + + /* Full generate+verify round trip via the data-level API (default + * verify == 1): exercises the "ret == 0 && verify" true branch, + * where the internal wc_SignatureVerifyHash() round-trip call + * itself succeeds. */ + sig_len = (word32)sizeof(sig); + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, sig, &sig_len, &ecc, key_len, &rng), 0); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, sig_type, + data, data_len, sig, sig_len, &ecc, key_len), 0); + + /* Hash-level round trip, independently confirming the sig that + * wc_SignatureGenerateHash_ex() produced with verify == 0 (see + * test_wc_SignatureDecisionCoverage()) is a valid signature. */ + { + byte hash[WC_SHA256_DIGEST_SIZE] = {0}; + byte sig2[128] = {0}; + word32 sig2_len = (word32)sizeof(sig2); + + ExpectIntEQ(wc_Hash(WC_HASH_TYPE_SHA256, data, data_len, hash, + (word32)sizeof(hash)), 0); + ExpectIntEQ(wc_SignatureGenerateHash_ex(WC_HASH_TYPE_SHA256, + sig_type, hash, (word32)sizeof(hash), sig2, &sig2_len, &ecc, + key_len, &rng, 0), 0); + ExpectIntEQ(wc_SignatureVerifyHash(WC_HASH_TYPE_SHA256, sig_type, + hash, (word32)sizeof(hash), sig2, sig2_len, &ecc, key_len), + 0); + } + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + DoExpectIntEQ(wc_ecc_free(&ecc), 0); + } +#endif /* HAVE_ECC */ + +#ifndef NO_RSA + { + RsaKey rsa_key; + WC_RNG rng; + word32 idx = 0; + byte* tmp = NULL; + size_t bytes; + word32 key_len = (word32)sizeof(RsaKey); + byte data[32]; + word32 data_len = (word32)sizeof(data); + byte sig[512] = {0}; + word32 sig_len; + + XMEMSET(&rsa_key, 0, sizeof(RsaKey)); + XMEMSET(data, 0x24, sizeof(data)); + + #ifdef USE_CERT_BUFFERS_1024 + bytes = (size_t)sizeof_client_key_der_1024; + #elif defined(USE_CERT_BUFFERS_2048) + bytes = (size_t)sizeof_client_key_der_2048; + #else + bytes = FOURK_BUF; + #endif + + ExpectNotNull(tmp = (byte*)XMALLOC(bytes, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + if (tmp != NULL) { + #ifdef USE_CERT_BUFFERS_1024 + XMEMCPY(tmp, client_key_der_1024, + (size_t)sizeof_client_key_der_1024); + #elif defined(USE_CERT_BUFFERS_2048) + XMEMCPY(tmp, client_key_der_2048, + (size_t)sizeof_client_key_der_2048); + #elif !defined(NO_FILESYSTEM) + XFILE file = XBADFILE; + ExpectTrue((file = XFOPEN(clientKey, "rb")) != XBADFILE); + ExpectIntGT(bytes = (size_t)XFREAD(tmp, 1, FOURK_BUF, file), 0); + if (file != XBADFILE) { + XFCLOSE(file); + } + #else + ExpectFail(); + #endif + } + + ExpectIntEQ(wc_InitRsaKey_ex(&rsa_key, HEAP_HINT, testDevId), 0); + ExpectIntEQ(wc_RsaPrivateKeyDecode(tmp, &idx, &rsa_key, + (word32)bytes), 0); + ExpectIntEQ(wc_InitRng(&rng), 0); + + /* wc_SignatureGetSize(): positive path for RSA */ + ExpectIntGT(wc_SignatureGetSize(WC_SIGNATURE_TYPE_RSA, &rsa_key, + key_len), 0); + + /* Plain RSA (PKCS#1 v1.5, no DigestInfo wrapper) round trip */ + sig_len = (word32)sizeof(sig); + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, + WC_SIGNATURE_TYPE_RSA, data, data_len, sig, &sig_len, &rsa_key, + key_len, &rng), 0); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, + WC_SIGNATURE_TYPE_RSA, data, data_len, sig, sig_len, &rsa_key, + key_len), 0); + +#ifndef NO_ASN + /* RSA_W_ENC round trip: true branch of + * "hash_enc_len += MAX_DER_DIGEST_ASN_SZ", wc_SignatureDerEncode()'s + * success path, and the RSA_W_ENC ASN.1 decode success path in + * wc_SignatureVerifyHash() (complementing the ASN_PARSE_E/ + * BAD_LENGTH_E failure paths in test_wc_SignatureDecisionCoverage()). + */ + { + byte sigEnc[512] = {0}; + word32 sigEncLen = (word32)sizeof(sigEnc); + + ExpectIntEQ(wc_SignatureGenerate(WC_HASH_TYPE_SHA256, + WC_SIGNATURE_TYPE_RSA_W_ENC, data, data_len, sigEnc, + &sigEncLen, &rsa_key, key_len, &rng), 0); + ExpectIntEQ(wc_SignatureVerify(WC_HASH_TYPE_SHA256, + WC_SIGNATURE_TYPE_RSA_W_ENC, data, data_len, sigEnc, + sigEncLen, &rsa_key, key_len), 0); + } +#endif /* !NO_ASN */ + + DoExpectIntEQ(wc_FreeRng(&rng), 0); + DoExpectIntEQ(wc_FreeRsaKey(&rsa_key), 0); + XFREE(tmp, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + } +#endif /* !NO_RSA */ +#endif /* !NO_SIG_WRAPPER */ + return EXPECT_RESULT(); +} /* END test_wc_SignatureFeatureCoverage() */ diff --git a/tests/api/test_signature.h b/tests/api/test_signature.h index 1f8f2e4b883..0ff8364619b 100644 --- a/tests/api/test_signature.h +++ b/tests/api/test_signature.h @@ -27,10 +27,14 @@ int test_wc_SignatureGetSize_ecc(void); int test_wc_SignatureGetSize_rsa(void); int test_wc_falcon_sign_verify(void); +int test_wc_SignatureDecisionCoverage(void); +int test_wc_SignatureFeatureCoverage(void); -#define TEST_SIGNATURE_DECLS \ - TEST_DECL_GROUP("signature", test_wc_SignatureGetSize_ecc), \ - TEST_DECL_GROUP("signature", test_wc_SignatureGetSize_rsa), \ - TEST_DECL_GROUP("signature", test_wc_falcon_sign_verify) +#define TEST_SIGNATURE_DECLS \ + TEST_DECL_GROUP("signature", test_wc_SignatureGetSize_ecc), \ + TEST_DECL_GROUP("signature", test_wc_SignatureGetSize_rsa), \ + TEST_DECL_GROUP("signature", test_wc_falcon_sign_verify), \ + TEST_DECL_GROUP("signature", test_wc_SignatureDecisionCoverage), \ + TEST_DECL_GROUP("signature", test_wc_SignatureFeatureCoverage) #endif /* WOLFCRYPT_TEST_SIGNATURE_H */ diff --git a/tests/api/test_slhdsa.c b/tests/api/test_slhdsa.c index 988bbc579e0..0c1dbb3f52b 100644 --- a/tests/api/test_slhdsa.c +++ b/tests/api/test_slhdsa.c @@ -2043,6 +2043,92 @@ int test_wc_slhdsa_der_negative(void) wc_SlhDsaKey_Free(&k); } + /* Version field independence: mutate the single-byte INTEGER at + * goodDer[4] (encoded by SetMyVersion(0, ...) as 02 01 , + * right after the 2-byte outer SEQUENCE header) without touching + * any length byte. version==1 is accepted (RFC 5958 allows 0 or 1); + * version==2 is rejected -- independence pairs for both operands of + * "version != 0 && version != 1". */ + { + byte mut[260]; + word32 idx2 = 0; + SlhDsaKey k; + int decRet; + + ExpectIntEQ((int)goodDer[2], 0x02); /* INTEGER tag */ + ExpectIntEQ((int)goodDer[3], 0x01); /* length == 1 */ + ExpectIntEQ((int)goodDer[4], 0x00); /* version == 0 */ + + XMEMSET(&k, 0, sizeof(k)); + XMEMCPY(mut, goodDer, (size_t)goodLen); + mut[4] = 1; /* version == 1: accepted */ + ExpectIntEQ(wc_SlhDsaKey_Init(&k, SLHDSA_SHAKE128S, NULL, + INVALID_DEVID), 0); + decRet = wc_SlhDsaKey_PrivateKeyDecode(mut, &idx2, &k, + (word32)goodLen); + ExpectIntEQ(decRet, 0); + wc_SlhDsaKey_Free(&k); + + XMEMSET(&k, 0, sizeof(k)); + idx2 = 0; + mut[4] = 2; /* version == 2: rejected */ + ExpectIntEQ(wc_SlhDsaKey_Init(&k, SLHDSA_SHAKE128S, NULL, + INVALID_DEVID), 0); + decRet = wc_SlhDsaKey_PrivateKeyDecode(mut, &idx2, &k, + (word32)goodLen); + ExpectIntEQ(decRet, WC_NO_ERR_TRACE(ASN_PARSE_E)); + wc_SlhDsaKey_Free(&k); + } + + /* Trailing-length independence: shrink the outer SEQUENCE's + * declared length by 1 (no trailing bytes appended) so that after + * consuming the mandatory version/AlgorithmIdentifier/privateKey + * fields, *inOutIdx lands 1 byte PAST the (now too-small) declared + * end -- independence pair for "*inOutIdx != seqEnd" alongside + * cases[4] above (which lands exactly ON seqEnd, ret==0). */ + { + byte mut[260]; + word32 idx2 = 0; + SlhDsaKey k; + int decRet; + + XMEMSET(&k, 0, sizeof(k)); + XMEMCPY(mut, goodDer, (size_t)goodLen); + mut[1] = (byte)(goodDer[1] - 1); + ExpectIntEQ(wc_SlhDsaKey_Init(&k, SLHDSA_SHAKE128S, NULL, + INVALID_DEVID), 0); + decRet = wc_SlhDsaKey_PrivateKeyDecode(mut, &idx2, &k, + (word32)goodLen); + ExpectIntEQ(decRet, WC_NO_ERR_TRACE(ASN_PARSE_E)); + wc_SlhDsaKey_Free(&k); + } + + /* wc_SlhDsaKey_PublicKeyDecode: "key->params != NULL && savedIdx < + * inSz" -- the raw-import fast path's second operand. Every + * successful decode elsewhere in this file already runs with + * savedIdx < inSz on a params-initialised key; pairing that with an + * empty window (*inOutIdx == inSz on entry) shows the operand's + * independence: the fast path is skipped and parsing falls through + * to (and fails) SPKI parsing on zero remaining bytes. */ + { + byte pub[WC_SLHDSA_MAX_PUB_LEN]; + word32 pubLen = (word32)sizeof(pub); + SlhDsaKey pubKey; + word32 idx2; + int decRet; + + ExpectIntEQ(wc_SlhDsaKey_ExportPublic(&srcKey, pub, &pubLen), 0); + + XMEMSET(&pubKey, 0, sizeof(pubKey)); + ExpectIntEQ(wc_SlhDsaKey_Init(&pubKey, SLHDSA_SHAKE128S, NULL, + INVALID_DEVID), 0); + idx2 = pubLen; /* savedIdx == inSz: window is empty */ + decRet = wc_SlhDsaKey_PublicKeyDecode(pub, &idx2, &pubKey, + pubLen); + ExpectIntNE(decRet, 0); + wc_SlhDsaKey_Free(&pubKey); + } + wc_SlhDsaKey_Free(&srcKey); wc_FreeRng(&rng); } @@ -2734,6 +2820,469 @@ int test_wc_slhdsa_decoder_disabled_oid(void) return EXPECT_RESULT(); } +/* + * MC/DC DecisionCoverage: exercise the argument-check / short-buffer / + * compound-guard independence pairs of the SLH-DSA public API without any + * (slow) key generation or signing. Each negative call isolates ONE operand + * of a compound decision so unique-cause MC/DC can be shown: + * - wc_SlhDsaKey_Init: key==NULL vs an unknown param (loop falls through, + * idx==-1 -> NOT_COMPILED_IN). + * - wc_SlhDsaKey_ImportPublic / _ExportPublic (and the private variants): + * each operand of the "(key==NULL)||(key->params==NULL)||(ptr==NULL)[|| + * (lenPtr==NULL)]" OR is driven true alone (a zeroed key gives + * key!=NULL with key->params==NULL), plus the length else-if arm. + * - size getters: key==NULL vs params==NULL independence. + * - wc_SlhDsaKey_CheckKey: NULL / params==NULL / MISSING_KEY (no private). + * - wc_SlhDsaKey_Init_id / _Init_label compound guards (WOLF_PRIVATE_KEY_ID). + */ +int test_wc_SlhdsaDecisionCoverage(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_SLHDSA) && defined(TEST_SLHDSA_DEFAULT_PARAM) + SlhDsaKey key; + SlhDsaKey zkey; /* zeroed: key != NULL but key->params == NULL */ + byte pub[TEST_SLHDSA_DEFAULT_PUB_LEN] = {0}; + word32 pubLen; +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + byte priv[TEST_SLHDSA_DEFAULT_PRIV_LEN] = {0}; + word32 privLen; +#endif + + XMEMSET(&zkey, 0, sizeof(zkey)); + + /* wc_SlhDsaKey_Init: NULL key vs unknown-parameter fall-through. */ + ExpectIntEQ(wc_SlhDsaKey_Init(NULL, TEST_SLHDSA_DEFAULT_PARAM, NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* key != NULL but param absent from table: for-loop never matches so + * idx stays -1 -> NOT_COMPILED_IN (the loop's compare-false path and the + * idx==-1 true path). */ + ExpectIntEQ(wc_SlhDsaKey_Init(&key, (enum SlhDsaParam)0x7fff, NULL, + INVALID_DEVID), WC_NO_ERR_TRACE(NOT_COMPILED_IN)); + + /* One good key for the pointer-operand pairs below. */ + ExpectIntEQ(wc_SlhDsaKey_Init(&key, TEST_SLHDSA_DEFAULT_PARAM, NULL, + INVALID_DEVID), 0); + + /* wc_SlhDsaKey_ImportPublic: 3-operand OR + length else-if. */ + ExpectIntEQ(wc_SlhDsaKey_ImportPublic(NULL, pub, (word32)sizeof(pub)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* key==NULL */ + ExpectIntEQ(wc_SlhDsaKey_ImportPublic(&zkey, pub, (word32)sizeof(pub)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* params==NULL */ + ExpectIntEQ(wc_SlhDsaKey_ImportPublic(&key, NULL, (word32)sizeof(pub)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* pub==NULL */ + ExpectIntEQ(wc_SlhDsaKey_ImportPublic(&key, pub, (word32)sizeof(pub) + 1), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); /* wrong len */ + + /* wc_SlhDsaKey_ExportPublic: 4-operand OR + length else-if. */ + pubLen = (word32)sizeof(pub); + ExpectIntEQ(wc_SlhDsaKey_ExportPublic(NULL, pub, &pubLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* key==NULL */ + ExpectIntEQ(wc_SlhDsaKey_ExportPublic(&zkey, pub, &pubLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* params==NULL */ + ExpectIntEQ(wc_SlhDsaKey_ExportPublic(&key, NULL, &pubLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* pub==NULL */ + ExpectIntEQ(wc_SlhDsaKey_ExportPublic(&key, pub, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* pubLen==NULL */ + pubLen = 1; /* too small */ + ExpectIntEQ(wc_SlhDsaKey_ExportPublic(&key, pub, &pubLen), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); + + /* Size getters: key==NULL vs params==NULL independence. */ + ExpectIntEQ(wc_SlhDsaKey_PublicSize(NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_PublicSize(&zkey), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_SigSize(NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_SigSize(&zkey), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + ExpectIntEQ(wc_SlhDsaKey_PrivateSize(NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_PrivateSize(&zkey), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* wc_SlhDsaKey_ImportPrivate: 3-operand OR + length else-if. */ + ExpectIntEQ(wc_SlhDsaKey_ImportPrivate(NULL, priv, (word32)sizeof(priv)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_ImportPrivate(&zkey, priv, (word32)sizeof(priv)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_ImportPrivate(&key, NULL, (word32)sizeof(priv)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_ImportPrivate(&key, priv, (word32)sizeof(priv) + 1), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); + + /* wc_SlhDsaKey_ExportPrivate: 4-operand OR + length else-if. */ + privLen = (word32)sizeof(priv); + ExpectIntEQ(wc_SlhDsaKey_ExportPrivate(NULL, priv, &privLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_ExportPrivate(&zkey, priv, &privLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_ExportPrivate(&key, NULL, &privLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_ExportPrivate(&key, priv, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + privLen = 1; + ExpectIntEQ(wc_SlhDsaKey_ExportPrivate(&key, priv, &privLen), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); + + /* wc_SlhDsaKey_CheckKey: NULL / params==NULL / no-private MISSING_KEY. */ + ExpectIntEQ(wc_SlhDsaKey_CheckKey(NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_CheckKey(&zkey), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_CheckKey(&key), WC_NO_ERR_TRACE(MISSING_KEY)); + + /* MakeKey / MakeKeyWithRandom / Sign-family: key->params==NULL + * independence (key==NULL and the other pointer/length operands are + * already covered in test_wc_slhdsa_make_key / _sign / _sign_hash / + * _sign_msg), plus the ctx==NULL&&ctxSz>0 compound-guard pairs. Every + * call here is arg-check-only: a too-small sigSz (or, for MakeKeyWith + * Random, a too-short seed) forces the NEXT quick error (BAD_LENGTH_E) + * once the OR itself is false, so none of this reaches the (very slow) + * WOTS+/FORS/hypertree signing -- no keygen/sign roundtrip needed. */ + { + WC_RNG dummyRng; /* never inited/dereferenced: only checked != NULL + * before every earlier arg-check that matters + * here short-circuits first. */ + byte dummyMsg[8] = {0}; + byte dummySig[8] = {0}; + byte dummyAddRnd[32] = {0}; + word32 tinySigSz; + + ExpectIntEQ(wc_SlhDsaKey_MakeKey(&zkey, &dummyRng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + ExpectIntEQ(wc_SlhDsaKey_MakeKeyWithRandom(&zkey, dummyMsg, + sizeof(dummyMsg), dummyMsg, sizeof(dummyMsg), dummyMsg, + sizeof(dummyMsg)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + { + byte sk_seed[TEST_SLHDSA_DEFAULT_SEED_LEN] = {0}; + byte sk_prf[TEST_SLHDSA_DEFAULT_SEED_LEN] = {0}; + byte pk_seed[TEST_SLHDSA_DEFAULT_SEED_LEN] = {0}; + + /* sk_prf wrong length (sk_seed already right-length). */ + ExpectIntEQ(wc_SlhDsaKey_MakeKeyWithRandom(&key, sk_seed, + sizeof(sk_seed), sk_prf, sizeof(sk_prf) - 1, pk_seed, + sizeof(pk_seed)), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* pk_seed wrong length (sk_seed, sk_prf already right-length). */ + ExpectIntEQ(wc_SlhDsaKey_MakeKeyWithRandom(&key, sk_seed, + sizeof(sk_seed), sk_prf, sizeof(sk_prf), pk_seed, + sizeof(pk_seed) - 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignDeterministic(&zkey, NULL, 0, dummyMsg, + sizeof(dummyMsg), dummySig, &tinySigSz), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_Sign(&zkey, NULL, 0, dummyMsg, + sizeof(dummyMsg), dummySig, &tinySigSz, &dummyRng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_Sign(&key, NULL, 5, dummyMsg, + sizeof(dummyMsg), dummySig, &tinySigSz, &dummyRng), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* ctx==NULL && ctxSz>0 */ + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_Sign(&key, NULL, 0, dummyMsg, + sizeof(dummyMsg), dummySig, &tinySigSz, &dummyRng), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); /* ctx==NULL, ctxSz==0 */ + + ExpectIntEQ(wc_SlhDsaKey_SignWithRandom(&zkey, NULL, 0, dummyMsg, + sizeof(dummyMsg), dummySig, &tinySigSz, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignWithRandom(&key, dummyMsg, 0, NULL, + sizeof(dummyMsg), dummySig, &tinySigSz, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* msg==NULL */ + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignWithRandom(&key, NULL, 0, dummyMsg, + sizeof(dummyMsg), NULL, &tinySigSz, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* sig==NULL */ + ExpectIntEQ(wc_SlhDsaKey_SignWithRandom(&key, NULL, 0, dummyMsg, + sizeof(dummyMsg), dummySig, NULL, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* sigSz==NULL */ + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignWithRandom(&key, NULL, 5, dummyMsg, + sizeof(dummyMsg), dummySig, &tinySigSz, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* ctx==NULL && ctxSz>0 */ + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignWithRandom(&key, NULL, 0, dummyMsg, + sizeof(dummyMsg), dummySig, &tinySigSz, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); /* ctx==NULL, ctxSz==0 */ + + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignMsgDeterministic(&zkey, dummyMsg, + sizeof(dummyMsg), dummySig, &tinySigSz), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignMsgWithRandom(&zkey, dummyMsg, + sizeof(dummyMsg), dummySig, &tinySigSz, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignMsgWithRandom(&key, NULL, + sizeof(dummyMsg), dummySig, &tinySigSz, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* mprime==NULL */ + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignMsgWithRandom(&key, dummyMsg, + sizeof(dummyMsg), NULL, &tinySigSz, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* sig==NULL */ + ExpectIntEQ(wc_SlhDsaKey_SignMsgWithRandom(&key, dummyMsg, + sizeof(dummyMsg), dummySig, NULL, dummyAddRnd), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* sigSz==NULL */ + + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignHashDeterministic(&zkey, NULL, 0, + dummyMsg, sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, + &tinySigSz), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + ExpectIntEQ(wc_SlhDsaKey_SignHashWithRandom(&zkey, NULL, 0, dummyMsg, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, &tinySigSz, + dummyAddRnd), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignHashWithRandom(&key, NULL, 5, dummyMsg, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, &tinySigSz, + dummyAddRnd), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* ctx&&ctxSz>0 */ + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignHashWithRandom(&key, NULL, 0, dummyMsg, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, &tinySigSz, + dummyAddRnd), WC_NO_ERR_TRACE(BAD_LENGTH_E)); /* ctxSz==0 */ + /* hash/sig/sigSz==NULL: wc_SlhDsaKey_SignHashWithRandom bypasses the + * wrapper checks wc_SlhDsaKey_SignHash performs for these same + * operands (already covered in test_wc_slhdsa_sign_hash), so + * slhdsakey_signhash_external's own copies are otherwise dead. */ + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignHashWithRandom(&key, dummyMsg, 0, NULL, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, &tinySigSz, + dummyAddRnd), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* hash==NULL */ + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignHashWithRandom(&key, dummyMsg, 0, + dummyMsg, sizeof(dummyMsg), WC_HASH_TYPE_SHA256, NULL, + &tinySigSz, dummyAddRnd), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* sig==NULL */ + ExpectIntEQ(wc_SlhDsaKey_SignHashWithRandom(&key, dummyMsg, 0, + dummyMsg, sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, NULL, + dummyAddRnd), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* sigSz==NULL */ + + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignHash(&zkey, NULL, 0, dummyMsg, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, &tinySigSz, + &dummyRng), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignHash(&key, NULL, 5, dummyMsg, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, &tinySigSz, + &dummyRng), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* ctx&&ctxSz>0 */ + tinySigSz = 1; + ExpectIntEQ(wc_SlhDsaKey_SignHash(&key, NULL, 0, dummyMsg, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, &tinySigSz, + &dummyRng), WC_NO_ERR_TRACE(BAD_LENGTH_E)); /* ctxSz==0 */ + } +#endif /* !WOLFSSL_SLHDSA_VERIFY_ONLY */ + + /* Verify-family + DER-export: key->params==NULL independence (these + * public functions are NOT stripped by WOLFSSL_SLHDSA_VERIFY_ONLY), plus + * the same cheap ctx==NULL&&ctxSz>0 pairs via a signature-length + * mismatch (BAD_LENGTH_E) instead of a real verify. */ + { + byte dummyMsg[8] = {0}; + byte dummySig[8] = {0}; + word32 wrongSigSz = 1; + + ExpectIntEQ(wc_SlhDsaKey_Verify(&zkey, NULL, 0, dummyMsg, + sizeof(dummyMsg), dummySig, wrongSigSz), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_Verify(&key, NULL, 5, dummyMsg, + sizeof(dummyMsg), dummySig, wrongSigSz), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* ctx==NULL && ctxSz>0 */ + ExpectIntEQ(wc_SlhDsaKey_Verify(&key, NULL, 0, dummyMsg, + sizeof(dummyMsg), dummySig, wrongSigSz), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); /* ctx==NULL, ctxSz==0 */ + + ExpectIntEQ(wc_SlhDsaKey_VerifyMsg(&zkey, dummyMsg, sizeof(dummyMsg), + dummySig, wrongSigSz), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + ExpectIntEQ(wc_SlhDsaKey_VerifyHash(&zkey, NULL, 0, dummyMsg, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, wrongSigSz), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_VerifyHash(&key, NULL, 5, dummyMsg, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, wrongSigSz), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* ctx==NULL && ctxSz>0 */ + ExpectIntEQ(wc_SlhDsaKey_VerifyHash(&key, NULL, 0, dummyMsg, + sizeof(dummyMsg), WC_HASH_TYPE_SHA256, dummySig, wrongSigSz), + WC_NO_ERR_TRACE(BAD_LENGTH_E)); /* ctx==NULL, ctxSz==0 */ +#endif + +#ifdef WC_ENABLE_ASYM_KEY_EXPORT + { + byte derBuf[16]; + ExpectIntEQ(wc_SlhDsaKey_PublicKeyToDer(&zkey, derBuf, + sizeof(derBuf), 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + { + byte derBuf[16]; + ExpectIntEQ(wc_SlhDsaKey_KeyToDer(&zkey, derBuf, sizeof(derBuf)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } +#endif +#endif /* WC_ENABLE_ASYM_KEY_EXPORT */ + } + + wc_SlhDsaKey_Free(&key); + +#ifdef WOLF_PRIVATE_KEY_ID + { + byte idbuf[4]; + XMEMSET(idbuf, 0xAB, sizeof(idbuf)); + + /* wc_SlhDsaKey_Init_id compound guards. */ + /* key==NULL (first operand true). */ + ExpectIntEQ(wc_SlhDsaKey_Init_id(NULL, TEST_SLHDSA_DEFAULT_PARAM, + idbuf, (int)sizeof(idbuf), NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* id==NULL && len>0 : id operand independence (key!=NULL). */ + ExpectIntEQ(wc_SlhDsaKey_Init_id(&key, TEST_SLHDSA_DEFAULT_PARAM, + NULL, 4, NULL, INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* len<0 : (len<0) true, (len>MAX) false. */ + ExpectIntEQ(wc_SlhDsaKey_Init_id(&key, TEST_SLHDSA_DEFAULT_PARAM, + idbuf, -1, NULL, INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + /* len>MAX : (len<0) false, (len>MAX) true. */ + ExpectIntEQ(wc_SlhDsaKey_Init_id(&key, TEST_SLHDSA_DEFAULT_PARAM, + idbuf, SLHDSA_MAX_ID_LEN + 1, NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BUFFER_E)); + /* id==NULL && len==0 : (len>0) false -> falls through to Init OK. */ + ExpectIntEQ(wc_SlhDsaKey_Init_id(&key, TEST_SLHDSA_DEFAULT_PARAM, + NULL, 0, NULL, INVALID_DEVID), 0); + wc_SlhDsaKey_Free(&key); + /* Valid id -> success (id!=NULL && len>0 copy path). */ + ExpectIntEQ(wc_SlhDsaKey_Init_id(&key, TEST_SLHDSA_DEFAULT_PARAM, + idbuf, (int)sizeof(idbuf), NULL, INVALID_DEVID), 0); + wc_SlhDsaKey_Free(&key); + + /* wc_SlhDsaKey_Init_label: (key==NULL)||(label==NULL) independence, + * empty-label BUFFER_E, then success. */ + ExpectIntEQ(wc_SlhDsaKey_Init_label(NULL, TEST_SLHDSA_DEFAULT_PARAM, + "lbl", NULL, INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_Init_label(&key, TEST_SLHDSA_DEFAULT_PARAM, + NULL, NULL, INVALID_DEVID), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_SlhDsaKey_Init_label(&key, TEST_SLHDSA_DEFAULT_PARAM, + "", NULL, INVALID_DEVID), WC_NO_ERR_TRACE(BUFFER_E)); + /* labelLen > SLHDSA_MAX_LABEL_LEN (32): (labelLen==0) false, + * (labelLen>MAX) true -- independence pair for the second operand. */ + ExpectIntEQ(wc_SlhDsaKey_Init_label(&key, TEST_SLHDSA_DEFAULT_PARAM, + "0123456789012345678901234567890123456789", NULL, INVALID_DEVID), + WC_NO_ERR_TRACE(BUFFER_E)); + ExpectIntEQ(wc_SlhDsaKey_Init_label(&key, TEST_SLHDSA_DEFAULT_PARAM, + "lbl", NULL, INVALID_DEVID), 0); + wc_SlhDsaKey_Free(&key); + } +#endif /* WOLF_PRIVATE_KEY_ID */ + +#endif /* WOLFSSL_HAVE_SLHDSA && TEST_SLHDSA_DEFAULT_PARAM */ + return EXPECT_RESULT(); +} + +/* Positive size-from-param switch-arm coverage. Each compiled-in parameter + * set hits its own case label in wc_SlhDsaKey_{Public,Sig,Private}SizeFromParam; + * an unknown param hits the default (NOT_COMPILED_IN) arm. No keygen/sign. */ +int test_wc_SlhdsaFeatureCoverage(void) +{ + EXPECT_DECLS; +#ifdef WOLFSSL_HAVE_SLHDSA + /* SHAKE family switch arms. */ +#ifdef WOLFSSL_SLHDSA_PARAM_128S + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHAKE128S), + WC_SLHDSA_SHAKE128S_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHAKE128S), + WC_SLHDSA_SHAKE128S_SIG_LEN); +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + ExpectIntEQ(wc_SlhDsaKey_PrivateSizeFromParam(SLHDSA_SHAKE128S), + WC_SLHDSA_SHAKE128S_PRIV_LEN); +#endif +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_128F + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHAKE128F), + WC_SLHDSA_SHAKE128F_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHAKE128F), + WC_SLHDSA_SHAKE128F_SIG_LEN); +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + ExpectIntEQ(wc_SlhDsaKey_PrivateSizeFromParam(SLHDSA_SHAKE128F), + WC_SLHDSA_SHAKE128F_PRIV_LEN); +#endif +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_192S + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHAKE192S), + WC_SLHDSA_SHAKE192S_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHAKE192S), + WC_SLHDSA_SHAKE192S_SIG_LEN); +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_192F + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHAKE192F), + WC_SLHDSA_SHAKE192F_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHAKE192F), + WC_SLHDSA_SHAKE192F_SIG_LEN); +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_256S + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHAKE256S), + WC_SLHDSA_SHAKE256S_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHAKE256S), + WC_SLHDSA_SHAKE256S_SIG_LEN); +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_256F + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHAKE256F), + WC_SLHDSA_SHAKE256F_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHAKE256F), + WC_SLHDSA_SHAKE256F_SIG_LEN); +#endif +#ifdef WOLFSSL_SLHDSA_SHA2 + /* SHA-2 family switch arms. */ +#ifdef WOLFSSL_SLHDSA_PARAM_SHA2_128S + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHA2_128S), + WC_SLHDSA_SHA2_128S_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHA2_128S), + WC_SLHDSA_SHA2_128S_SIG_LEN); +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_SHA2_128F + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHA2_128F), + WC_SLHDSA_SHA2_128F_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHA2_128F), + WC_SLHDSA_SHA2_128F_SIG_LEN); +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_SHA2_192S + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHA2_192S), + WC_SLHDSA_SHA2_192S_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHA2_192S), + WC_SLHDSA_SHA2_192S_SIG_LEN); +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_SHA2_192F + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHA2_192F), + WC_SLHDSA_SHA2_192F_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHA2_192F), + WC_SLHDSA_SHA2_192F_SIG_LEN); +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_SHA2_256S + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHA2_256S), + WC_SLHDSA_SHA2_256S_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHA2_256S), + WC_SLHDSA_SHA2_256S_SIG_LEN); +#endif +#ifdef WOLFSSL_SLHDSA_PARAM_SHA2_256F + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam(SLHDSA_SHA2_256F), + WC_SLHDSA_SHA2_256F_PUB_LEN); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam(SLHDSA_SHA2_256F), + WC_SLHDSA_SHA2_256F_SIG_LEN); +#endif +#endif /* WOLFSSL_SLHDSA_SHA2 */ + + /* default (unknown param) switch arm -> NOT_COMPILED_IN. */ + ExpectIntEQ(wc_SlhDsaKey_PublicSizeFromParam((enum SlhDsaParam)0x7fff), + WC_NO_ERR_TRACE(NOT_COMPILED_IN)); + ExpectIntEQ(wc_SlhDsaKey_SigSizeFromParam((enum SlhDsaParam)0x7fff), + WC_NO_ERR_TRACE(NOT_COMPILED_IN)); +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + ExpectIntEQ(wc_SlhDsaKey_PrivateSizeFromParam((enum SlhDsaParam)0x7fff), + WC_NO_ERR_TRACE(NOT_COMPILED_IN)); +#endif +#endif /* WOLFSSL_HAVE_SLHDSA */ + return EXPECT_RESULT(); +} + #ifdef TEST_SLHDSA_DEFAULT_PARAM #undef TEST_SLHDSA_DEFAULT_PARAM #undef TEST_SLHDSA_DEFAULT_SIG_LEN diff --git a/tests/api/test_slhdsa.h b/tests/api/test_slhdsa.h index 40d09ed8cb8..93cb411217c 100644 --- a/tests/api/test_slhdsa.h +++ b/tests/api/test_slhdsa.h @@ -40,6 +40,8 @@ int test_wc_slhdsa_der_decode_files(void); int test_wc_slhdsa_x509_i2d_roundtrip(void); int test_wc_slhdsa_param_disabled(void); int test_wc_slhdsa_decoder_disabled_oid(void); +int test_wc_SlhdsaDecisionCoverage(void); +int test_wc_SlhdsaFeatureCoverage(void); #define TEST_SLHDSA_DECLS \ TEST_DECL_GROUP("slhdsa", test_wc_slhdsa), \ @@ -57,6 +59,8 @@ int test_wc_slhdsa_decoder_disabled_oid(void); TEST_DECL_GROUP("slhdsa", test_wc_slhdsa_der_decode_files), \ TEST_DECL_GROUP("slhdsa", test_wc_slhdsa_x509_i2d_roundtrip), \ TEST_DECL_GROUP("slhdsa", test_wc_slhdsa_param_disabled), \ - TEST_DECL_GROUP("slhdsa", test_wc_slhdsa_decoder_disabled_oid) + TEST_DECL_GROUP("slhdsa", test_wc_slhdsa_decoder_disabled_oid), \ + TEST_DECL_GROUP("slhdsa", test_wc_SlhdsaDecisionCoverage), \ + TEST_DECL_GROUP("slhdsa", test_wc_SlhdsaFeatureCoverage) #endif /* WOLFCRYPT_TEST_SLHDSA_H */ diff --git a/tests/api/test_wc_encrypt.c b/tests/api/test_wc_encrypt.c index de7888cbe73..b1e3f24c4bf 100644 --- a/tests/api/test_wc_encrypt.c +++ b/tests/api/test_wc_encrypt.c @@ -29,6 +29,8 @@ #endif #include +#include +#include #include #include #include @@ -155,3 +157,192 @@ int test_wc_Des_CbcEncryptDecryptWithKey(void) return EXPECT_RESULT(); } /* END test_wc_Des_CbcEncryptDecryptWithKey */ +/* + * wc_AesCbcEncryptWithKey / wc_AesCbcDecryptWithKey round-trip: exercises the + * WC_ALLOC/wc_AesInit/SetKey/CbcEncrypt ret==0 chains in wc_encrypt.c. + */ +int test_wc_AesCbcEncryptDecryptWithKey(void) +{ + EXPECT_DECLS; +#if !defined(NO_AES) && defined(HAVE_AES_CBC) && defined(HAVE_AES_DECRYPT) + const byte key[16] = { + 0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef, + 0xfe,0xdc,0xba,0x98,0x76,0x54,0x32,0x10 + }; + const byte iv[16] = { + 0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80, + 0x90,0xa0,0xb0,0xc0,0xd0,0xe0,0xf0,0x00 + }; + const byte plain[16] = { + 'w','o','l','f','s','s','l',' ','a','e','s','c','b','c','!','!' + }; + byte cipher[16] = {0}; + byte decrypted[16] = {0}; + + ExpectIntEQ(wc_AesCbcEncryptWithKey(cipher, plain, (word32)sizeof(plain), + key, (word32)sizeof(key), iv), 0); + ExpectIntEQ(wc_AesCbcDecryptWithKey(decrypted, cipher, + (word32)sizeof(cipher), + key, (word32)sizeof(key), iv), 0); + ExpectBufEQ(decrypted, plain, (int)sizeof(plain)); + + /* invalid key size propagates an error out of the ret==0 chain */ + ExpectIntNE(wc_AesCbcEncryptWithKey(cipher, plain, (word32)sizeof(plain), + key, 5, iv), 0); + + /* wc_AesCbcDecryptWithKey argument-check independence pairs: + * "out == NULL || in == NULL || key == NULL || iv == NULL" - vary one + * operand at a time while the other three stay valid. */ + ExpectIntEQ(wc_AesCbcDecryptWithKey(NULL, cipher, (word32)sizeof(cipher), + key, (word32)sizeof(key), iv), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_AesCbcDecryptWithKey(decrypted, NULL, + (word32)sizeof(cipher), + key, (word32)sizeof(key), iv), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_AesCbcDecryptWithKey(decrypted, cipher, + (word32)sizeof(cipher), + NULL, (word32)sizeof(key), iv), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_AesCbcDecryptWithKey(decrypted, cipher, + (word32)sizeof(cipher), + key, (word32)sizeof(key), NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + return EXPECT_RESULT(); +} /* END test_wc_AesCbcEncryptDecryptWithKey */ + +/* + * wc_BufferKeyEncrypt / wc_BufferKeyDecrypt: argument-check decision coverage + * (the 5- and 4-operand OR guards) plus the info->cipherType dispatch arms + * (WC_CIPHER_DES / WC_CIPHER_DES3 / WC_CIPHER_AES_CBC). + */ +int test_wc_BufferKeyEncryptDecryptDecisionCoverage(void) +{ + EXPECT_DECLS; +#if !defined(NO_ASN) && defined(WOLFSSL_ENCRYPTED_KEYS) && \ + !defined(NO_PWDBASED) && !defined(NO_SHA) + const byte pw[] = { 'p','a','s','s','w','o','r','d' }; + EncryptedInfo info; + byte der[32]; + + /* ---- wc_BufferKeyEncrypt argument-check independence pairs ---- */ + /* baseline: all operands valid (built per cipher below); here vary one at a + * time while the others are valid. */ + XMEMSET(&info, 0, sizeof(info)); + info.cipherType = WC_CIPHER_AES_CBC; + info.keySz = 16; + info.ivSz = 16; + XMEMSET(info.iv, 0x22, 16); + XMEMSET(der, 0x33, sizeof(der)); + + /* der == NULL */ + ExpectIntEQ(wc_BufferKeyEncrypt(&info, NULL, (word32)sizeof(der), + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* password == NULL */ + ExpectIntEQ(wc_BufferKeyEncrypt(&info, der, (word32)sizeof(der), + NULL, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* info == NULL */ + ExpectIntEQ(wc_BufferKeyEncrypt(NULL, der, (word32)sizeof(der), + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* info->keySz == 0 */ + info.keySz = 0; + ExpectIntEQ(wc_BufferKeyEncrypt(&info, der, (word32)sizeof(der), + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + info.keySz = 16; + /* info->ivSz < PKCS5_SALT_SZ */ + info.ivSz = PKCS5_SALT_SZ - 1; + ExpectIntEQ(wc_BufferKeyEncrypt(&info, der, (word32)sizeof(der), + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + info.ivSz = 16; + + /* ---- wc_BufferKeyDecrypt argument-check independence pairs ---- */ + ExpectIntEQ(wc_BufferKeyDecrypt(&info, NULL, (word32)sizeof(der), + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_BufferKeyDecrypt(&info, der, (word32)sizeof(der), + NULL, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_BufferKeyDecrypt(NULL, der, (word32)sizeof(der), + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + info.keySz = 0; + ExpectIntEQ(wc_BufferKeyDecrypt(&info, der, (word32)sizeof(der), + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + info.keySz = 16; + + /* ---- cipherType dispatch: AES-CBC encrypt (16-byte block) ---- */ +#if defined(HAVE_AES_CBC) + { + XMEMSET(&info, 0, sizeof(info)); + info.cipherType = WC_CIPHER_AES_CBC; + info.keySz = 16; + info.ivSz = 16; + XMEMSET(info.iv, 0x24, 16); + XMEMSET(der, 0x33, 16); + ExpectIntEQ(wc_BufferKeyEncrypt(&info, der, 16, + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), 0); + } +#endif + /* ---- cipherType dispatch: 3DES and DES encrypt (8-byte block) ---- */ +#ifndef NO_DES3 + { + XMEMSET(&info, 0, sizeof(info)); + info.cipherType = WC_CIPHER_DES3; + info.keySz = 24; + info.ivSz = PKCS5_SALT_SZ; + XMEMSET(info.iv, 0x25, PKCS5_SALT_SZ); + XMEMSET(der, 0x33, 24); + ExpectIntEQ(wc_BufferKeyEncrypt(&info, der, 24, + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), 0); + + XMEMSET(&info, 0, sizeof(info)); + info.cipherType = WC_CIPHER_DES; + info.keySz = 8; + info.ivSz = PKCS5_SALT_SZ; + XMEMSET(info.iv, 0x26, PKCS5_SALT_SZ); + XMEMSET(der, 0x33, 24); + ExpectIntEQ(wc_BufferKeyEncrypt(&info, der, 24, + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), 0); + } +#endif + + /* ---- decrypt dispatch: iv is hex (Base16-decoded to the PBKDF salt) ---- */ +#if defined(HAVE_AES_CBC) && defined(HAVE_AES_DECRYPT) && defined(WOLFSSL_BASE16) + { + /* 16 hex chars -> 8-byte salt after Base16_Decode */ + const byte hexIv[16] = { + '1','1','2','2','3','3','4','4', + '5','5','6','6','7','7','8','8' + }; + XMEMSET(&info, 0, sizeof(info)); + info.cipherType = WC_CIPHER_AES_CBC; + info.keySz = 16; + info.ivSz = 16; + XMEMCPY(info.iv, hexIv, 16); + XMEMSET(der, 0x33, 16); + ExpectIntEQ(wc_BufferKeyDecrypt(&info, der, 16, + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), 0); + + /* ivSz that decodes below PKCS5_SALT_SZ -> BUFFER_E */ + XMEMSET(&info, 0, sizeof(info)); + info.cipherType = WC_CIPHER_AES_CBC; + info.keySz = 16; + info.ivSz = 4; /* 4 hex chars -> 2-byte salt < PKCS5_SALT_SZ */ + XMEMCPY(info.iv, hexIv, 4); + XMEMSET(der, 0x33, 16); + ExpectIntEQ(wc_BufferKeyDecrypt(&info, der, 16, + pw, (int)sizeof(pw), WC_HASH_TYPE_SHA), + WC_NO_ERR_TRACE(BUFFER_E)); + } +#endif +#endif + return EXPECT_RESULT(); +} /* END test_wc_BufferKeyEncryptDecryptDecisionCoverage */ + diff --git a/tests/api/test_wc_encrypt.h b/tests/api/test_wc_encrypt.h index 1d11fe536a4..55ffd143540 100644 --- a/tests/api/test_wc_encrypt.h +++ b/tests/api/test_wc_encrypt.h @@ -26,9 +26,14 @@ int test_wc_Des3_CbcEncryptDecryptWithKey(void); int test_wc_Des_CbcEncryptDecryptWithKey(void); +int test_wc_AesCbcEncryptDecryptWithKey(void); +int test_wc_BufferKeyEncryptDecryptDecisionCoverage(void); #define TEST_WC_ENCRYPT_DECLS \ TEST_DECL_GROUP("wc_encrypt", test_wc_Des3_CbcEncryptDecryptWithKey), \ - TEST_DECL_GROUP("wc_encrypt", test_wc_Des_CbcEncryptDecryptWithKey) + TEST_DECL_GROUP("wc_encrypt", test_wc_Des_CbcEncryptDecryptWithKey), \ + TEST_DECL_GROUP("wc_encrypt", test_wc_AesCbcEncryptDecryptWithKey), \ + TEST_DECL_GROUP("wc_encrypt", \ + test_wc_BufferKeyEncryptDecryptDecisionCoverage) #endif /* WOLFCRYPT_TEST_WC_ENCRYPT_H */ diff --git a/tests/api/test_wolfentropy.c b/tests/api/test_wolfentropy.c new file mode 100644 index 00000000000..13fc6c1154a --- /dev/null +++ b/tests/api/test_wolfentropy.c @@ -0,0 +1,143 @@ +/* test_wolfentropy.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 + */ + +#include + +#ifdef NO_INLINE + #include +#else + #define WOLFSSL_MISC_INCLUDED + #include +#endif + +#include +#include +#ifdef HAVE_ENTROPY_MEMUSE + #include + #include +#endif +#include +#include + +/* Public MC/DC coverage for wolfcrypt/src/wolfentropy.c (SP800-90B MemUse + * jitter entropy source). Only the exported surface is reachable from + * tests/api: wc_Entropy_GetRawEntropy(), wc_Entropy_Get() and + * wc_Entropy_OnDemandTest(). The file-static SP800-90B health tests + * (Repetition/Proportion/Startup) and the MemUse index math are driven in the + * tests/unit-mcdc white-box (test_wolfentropy_whitebox.c). The module is + * gated by HAVE_ENTROPY_MEMUSE so every body auto-skips when it is compiled + * out. wolfCrypt_Init() (unit.test setup) already ran Entropy_Init(), so the + * SHA3 conditioner and mutex are ready before these calls. */ + +/* Raw-entropy assessment API: argument checks plus a valid collection. */ +int test_wc_Entropy_GetRawEntropy(void) +{ + EXPECT_DECLS; +#ifdef HAVE_ENTROPY_MEMUSE + byte raw[64]; + + XMEMSET(raw, 0, sizeof(raw)); + + /* "raw == NULL || cnt <= 0": drive each operand alone. */ + ExpectIntEQ(wc_Entropy_GetRawEntropy(NULL, (int)sizeof(raw)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* raw NULL */ + ExpectIntEQ(wc_Entropy_GetRawEntropy(raw, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* cnt == 0 */ + ExpectIntEQ(wc_Entropy_GetRawEntropy(raw, -1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); /* cnt < 0 */ + + /* Valid: collect a bounded amount of raw jitter noise. */ + ExpectIntEQ(wc_Entropy_GetRawEntropy(raw, (int)sizeof(raw)), 0); +#endif /* HAVE_ENTROPY_MEMUSE */ + return EXPECT_RESULT(); +} + +/* On-demand SP800-90B startup health test (Repetition + Adaptive + * Proportion over >= 1024 + 512 fresh samples). */ +int test_wc_Entropy_OnDemandTest(void) +{ + EXPECT_DECLS; +#ifdef HAVE_ENTROPY_MEMUSE + ExpectIntEQ(wc_Entropy_OnDemandTest(), 0); + /* Idempotent: a second run must also pass. */ + ExpectIntEQ(wc_Entropy_OnDemandTest(), 0); +#endif /* HAVE_ENTROPY_MEMUSE */ + return EXPECT_RESULT(); +} + +/* wc_Entropy_Get()'s argument guard is a three-operand compound: + * "bits <= 0 || bits > MAX_ENTROPY_BITS || (entropy == NULL && len > 0)" + * Drive each operand's independence pair. */ +int test_wc_EntropyDecisionCoverage(void) +{ + EXPECT_DECLS; +#ifdef HAVE_ENTROPY_MEMUSE + byte entropy[WC_SHA3_256_DIGEST_SIZE]; /* 32 bytes */ + + XMEMSET(entropy, 0, sizeof(entropy)); + + /* bits <= 0 (1st operand true). */ + ExpectIntEQ(wc_Entropy_Get(0, entropy, sizeof(entropy)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_Entropy_Get(-8, entropy, sizeof(entropy)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* bits > MAX_ENTROPY_BITS (2nd operand true, 1st false). */ + ExpectIntEQ(wc_Entropy_Get(MAX_ENTROPY_BITS + 1, entropy, sizeof(entropy)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* entropy == NULL && len > 0 (3rd operand true, first two false). */ + ExpectIntEQ(wc_Entropy_Get(MAX_ENTROPY_BITS, NULL, sizeof(entropy)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* entropy == NULL but len == 0 -> 3rd operand false (independence for the + * "len > 0" sub-operand): accepted, the generate loop never runs. */ + ExpectIntEQ(wc_Entropy_Get(MAX_ENTROPY_BITS, NULL, 0), 0); + + /* All operands false: a real collection. */ + ExpectIntEQ(wc_Entropy_Get(MAX_ENTROPY_BITS, entropy, sizeof(entropy)), 0); +#endif /* HAVE_ENTROPY_MEMUSE */ + return EXPECT_RESULT(); +} + +/* Positive-path feature coverage: exercise wc_Entropy_Get()'s output loop + * with sizes that straddle the SHA3-256 block (WC_SHA3_256_DIGEST_SIZE = 32), + * driving the "len < entropy_len" clamp on both sides and multiple loop + * iterations, at a couple of entropy strengths. */ +int test_wc_EntropyFeatureCoverage(void) +{ + EXPECT_DECLS; +#ifdef HAVE_ENTROPY_MEMUSE + byte out[96]; + word32 sizes[] = { 1, 16, 32, 33, 64, 96 }; + word32 i; + + for (i = 0; i < (word32)(sizeof(sizes) / sizeof(sizes[0])); i++) { + XMEMSET(out, 0, sizeof(out)); + ExpectIntEQ(wc_Entropy_Get(MAX_ENTROPY_BITS, out, sizes[i]), 0); + } + + /* A lower strength (fewer noise samples per output). */ + XMEMSET(out, 0, sizeof(out)); + ExpectIntEQ(wc_Entropy_Get(128, out, 32), 0); +#endif /* HAVE_ENTROPY_MEMUSE */ + return EXPECT_RESULT(); +} diff --git a/tests/api/test_wolfentropy.h b/tests/api/test_wolfentropy.h new file mode 100644 index 00000000000..e549fb38660 --- /dev/null +++ b/tests/api/test_wolfentropy.h @@ -0,0 +1,38 @@ +/* test_wolfentropy.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 + */ + +#ifndef WOLFCRYPT_TEST_WOLFENTROPY_H +#define WOLFCRYPT_TEST_WOLFENTROPY_H + +#include + +int test_wc_Entropy_GetRawEntropy(void); +int test_wc_Entropy_OnDemandTest(void); +int test_wc_EntropyDecisionCoverage(void); +int test_wc_EntropyFeatureCoverage(void); + +#define TEST_WOLFENTROPY_DECLS \ + TEST_DECL_GROUP("wolfentropy", test_wc_Entropy_GetRawEntropy), \ + TEST_DECL_GROUP("wolfentropy", test_wc_Entropy_OnDemandTest), \ + TEST_DECL_GROUP("wolfentropy", test_wc_EntropyDecisionCoverage), \ + TEST_DECL_GROUP("wolfentropy", test_wc_EntropyFeatureCoverage) + +#endif /* WOLFCRYPT_TEST_WOLFENTROPY_H */ diff --git a/tests/api/test_wolfmath.c b/tests/api/test_wolfmath.c index 960c612adc0..017e134b5e9 100644 --- a/tests/api/test_wolfmath.c +++ b/tests/api/test_wolfmath.c @@ -1026,4 +1026,3 @@ int test_wc_SpIntExptGcdDecisionCoverage(void) #endif return EXPECT_RESULT(); } /* End test_wc_SpIntExptGcdDecisionCoverage */ - diff --git a/tests/unit-mcdc/test_ed448_whitebox.c b/tests/unit-mcdc/test_ed448_whitebox.c new file mode 100644 index 00000000000..8c58e804290 --- /dev/null +++ b/tests/unit-mcdc/test_ed448_whitebox.c @@ -0,0 +1,220 @@ +/* test_ed448_whitebox.c + * + * White-box MC/DC supplement for wolfcrypt/src/ed448.c. + * + * The tests/api ed448 suite drives ed448.c through its *public* API. The + * file-static helper ed448_hash() opens with: + * + * if (key == NULL || (in == NULL && inLen > 0) || hash == NULL) { + * return BAD_FUNC_ARG; + * } + * + * Every public caller (wc_ed448_make_public, wc_ed448_sign_msg_ex, + * wc_ed448ph_sign_msg) always passes a non-NULL key, a non-NULL hash output + * buffer, and either a non-NULL `in` or an `in`/`inLen` pair where inLen is a + * compile-time-fixed nonzero constant (ED448_KEY_SIZE / a real message + * buffer) -- none of them ever construct the "in == NULL && inLen > 0" + * combination or pass key/hash as NULL. This translation unit reaches all + * three operands' TRUE sides (and completes the FALSE-side pairing within this + * same binary, per the campaign's cross-binary MC/DC lesson) by compiling + * ed448.c directly (#include) and calling the static helper directly. + * + * Coverage from this binary is unioned with the tests/api variant coverage by + * source line:col in the per-module campaign (iso26262/mcdc-per-module): + * llvm-cov computes MC/DC independence PER BINARY, and the campaign's + * aggregate.sh ORs the "independence shown" bit across binaries by key. That + * is why every pair below is completed *within this file* rather than relying + * on the API tests to supply the other half. + * + * Build: compiled by run-mcdc-par.sh's white-box step with the SAME MC/DC + * CFLAGS, -DHAVE_CONFIG_H and -I as the instrumented library, then + * linked against that variant's libwolfssl.a with its ed448.o removed (this TU + * supplies the instrumented ed448.c). NOT part of the wolfSSL build; not + * registered in tests/api. See tests/unit-mcdc/README.md. + * + * Targeted residual (ed448.c): + * ed448_hash() key/in/hash NULL guard ...................... 4 conditions + * The only ed448.c gap confirmed structurally unreachable through the public + * API (every wrapper hard-codes non-NULL, well-formed arguments). + * + * Also closes the "key == NULL" operand of the sanity checks in three more + * HAVE_ED448_VERIFY file-static functions: ed448_verify_msg_init_with_sha(), + * ed448_verify_msg_update_with_sha() and ed448_verify_msg_final_with_sha(). + * wc_ed448_verify_msg_ex() (the non-streaming public entry point) always + * checks key == NULL itself before calling these, so that combination never + * reaches the static functions from there; the WOLFSSL_ED448_STREAMING_VERIFY + * wc_ed448_verify_msg_init/update/final() wrappers compute `&key->sha` from + * the caller's key *before* calling the static, so a NULL key can only be + * driven into the static safely by calling it directly here (each static + * checks key == NULL before dereferencing it, so this is memory-safe). + */ + +/* Pull ed448.c in verbatim so the file-static helper below is in scope and + * instrumented in THIS binary. ed448.c includes libwolfssl_sources.h (which + * picks up user_settings.h via -DWOLFSSL_USER_SETTINGS) and ed448.h itself. */ +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#ifdef HAVE_ED448 + +/* ------------------------------------------------------------------------- * + * ed448_hash() (line ~162). + * + * if (key == NULL || (in == NULL && inLen > 0) || hash == NULL) + * + * unique-cause MC/DC for a 3-operand OR needs, per operand, a pair where only + * that operand's value differs against an otherwise all-false vector. + * ------------------------------------------------------------------------- */ +static void wb_ed448_hash(void) +{ + ed448_key key; + byte in[8]; + byte hash[ED448_PRV_KEY_SIZE]; + int ret; + + /* Init the key so key->heap (and, under WOLFSSL_ED448_PERSISTENT_SHA, + * key->sha) are valid for the all-false baseline call. */ + if (wc_ed448_init(&key) != 0) { + WB_NOTE("wc_ed448_init failed; skipping ed448_hash white-box"); + return; + } + XMEMSET(in, 0x11, sizeof(in)); + + /* all-false baseline: valid call. */ + ret = ed448_hash(&key, in, sizeof(in), hash, sizeof(hash)); + if (ret != 0) { + WB_NOTE("ed448_hash(valid) unexpected error"); + wb_fail = 1; + } + + /* key == NULL: operand 1 TRUE, others false. */ + ret = ed448_hash(NULL, in, sizeof(in), hash, sizeof(hash)); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("ed448_hash(key==NULL) did not return BAD_FUNC_ARG"); + wb_fail = 1; + } + + /* in == NULL && inLen > 0: operand 2 TRUE, others false. Also completes + * operand 2's own inner-compound independence (paired against the + * all-false baseline's in != NULL, and against inLen == 0 below for the + * inLen > 0 half). */ + ret = ed448_hash(&key, NULL, sizeof(in), hash, sizeof(hash)); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("ed448_hash(in==NULL,inLen>0) did not return BAD_FUNC_ARG"); + wb_fail = 1; + } + + /* in == NULL && inLen == 0: operand 2's inner "inLen > 0" FALSE side + * (in == NULL alone does not trip the guard) -- valid call, matching how + * a zero-length message would behave. */ + ret = ed448_hash(&key, NULL, 0, hash, sizeof(hash)); + if (ret != 0) { + WB_NOTE("ed448_hash(in==NULL,inLen==0) unexpected error"); + wb_fail = 1; + } + + /* hash == NULL: operand 3 TRUE, others false. */ + ret = ed448_hash(&key, in, sizeof(in), NULL, sizeof(hash)); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("ed448_hash(hash==NULL) did not return BAD_FUNC_ARG"); + wb_fail = 1; + } + + wc_ed448_free(&key); + WB_NOTE("ed448_hash key/in/hash guard exercised"); +} + +#else + +static void wb_ed448_hash(void) +{ + WB_NOTE("HAVE_ED448 not compiled in this variant; skipped"); +} + +#endif /* HAVE_ED448 */ + +#if defined(HAVE_ED448) && defined(HAVE_ED448_VERIFY) + +/* ------------------------------------------------------------------------- * + * ed448_verify_msg_init_with_sha() / ed448_verify_msg_update_with_sha() / + * ed448_verify_msg_final_with_sha() each open with a "key == NULL" operand + * (ORed with other arguments) that no public caller can trip: the + * non-streaming entry point (wc_ed448_verify_msg_ex()) checks key == NULL + * itself first, and the streaming wrappers dereference key (to take + * &key->sha) before ever calling these static functions. Call the static functions directly. + * ------------------------------------------------------------------------- */ +static void wb_ed448_verify_key_null(void) +{ + wc_Shake sha; + byte sig[ED448_SIG_SIZE]; + byte msg[8]; + int res; + int ret; + + XMEMSET(&sha, 0, sizeof(sha)); + XMEMSET(sig, 0, sizeof(sig)); + XMEMSET(msg, 0x22, sizeof(msg)); + + /* ed448_verify_msg_init_with_sha(): key == NULL, sig != NULL, no + * context -- the other two OR operands (sig == NULL, + * context == NULL && contextLen != 0) both FALSE. */ + ret = ed448_verify_msg_init_with_sha(sig, sizeof(sig), NULL, &sha, Ed448, + NULL, 0); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("ed448_verify_msg_init_with_sha(key==NULL) did not return " + "BAD_FUNC_ARG"); + wb_fail = 1; + } + + /* ed448_verify_msg_update_with_sha(): key == NULL, msgSegment != NULL + * (msgSegment == NULL operand FALSE). */ + ret = ed448_verify_msg_update_with_sha(msg, sizeof(msg), NULL, &sha); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("ed448_verify_msg_update_with_sha(key==NULL) did not return " + "BAD_FUNC_ARG"); + wb_fail = 1; + } + + /* ed448_verify_msg_final_with_sha(): key == NULL, sig/res != NULL + * (sig == NULL, res == NULL operands both FALSE). The function + * returns before writing *res on this path, so res must stay + * unchanged. */ + res = 1; + ret = ed448_verify_msg_final_with_sha(sig, sizeof(sig), &res, NULL, &sha); + if ((ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) || (res != 1)) { + WB_NOTE("ed448_verify_msg_final_with_sha(key==NULL) did not return " + "BAD_FUNC_ARG"); + wb_fail = 1; + } + + WB_NOTE("ed448 verify_*_with_sha key==NULL guards exercised"); +} + +#else + +static void wb_ed448_verify_key_null(void) +{ + WB_NOTE("HAVE_ED448_VERIFY not compiled in this variant; skipped"); +} + +#endif /* HAVE_ED448 && HAVE_ED448_VERIFY */ + +int main(void) +{ + printf("ed448.c white-box supplement\n"); +#ifndef HAVE_ED448 + printf(" HAVE_ED448 not defined; nothing to exercise\n"); + return 0; +#else + wb_ed448_hash(); + wb_ed448_verify_key_null(); + 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. */ + return 0; +#endif +} diff --git a/tests/unit-mcdc/test_integer_whitebox.c b/tests/unit-mcdc/test_integer_whitebox.c new file mode 100644 index 00000000000..4f4189016a0 --- /dev/null +++ b/tests/unit-mcdc/test_integer_whitebox.c @@ -0,0 +1,932 @@ +/* test_integer_whitebox.c + * + * White-box MC/DC supplement for wolfcrypt/src/integer.c (the HEAPMATH + * big-integer module, bigint-integer, iso26262/mcdc-per-module). + * + * The tests/api wolfmath suite (test_wolfmath.c, + * test_wc_IntegerDecisionCoverage) drives integer.c through its *public* mp_* + * API. A handful of decisions live in file-static helpers whose branch + * selectors a public caller can never present directly (mp_div_d, + * mp_prime_miller_rabin, mp_prime_is_divisible, s_is_power_of_two, bn_reverse). + * This translation unit reaches them by compiling integer.c directly (#include) + * and calling the static helpers with BOTH halves of each targeted MC/DC pair in this + * one binary (llvm-cov computes MC/DC per binary; the campaign unions the + * "independence shown" bit across binaries by line:col). + * + * Build: compiled by run-mcdc.sh's white-box step with the SAME MC/DC CFLAGS + * and -I as the instrumented library, then linked against that + * variant's libwolfssl.a with its integer.o removed (this TU supplies the + * instrumented integer.c). NOT part of the wolfSSL build; not registered in + * tests/api. See tests/unit-mcdc/README.md. + * + * Every call is memory-safe (static helpers are handed initialized mp_ints and + * in-range selectors); setup failures print a skip and return 0 (a nonzero + * exit makes the campaign discard the variant and its coverage). + */ + +#include + +#include + +#ifndef HEAP_HINT +#define HEAP_HINT NULL +#endif + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if !defined(USE_FAST_MATH) && defined(USE_INTEGER_HEAP_MATH) && \ + !defined(WOLFSSL_SP_MATH) && !defined(NO_BIG_INT) + +/* s_is_power_of_two(b, &p): power (b=8) true, non-power (b=6) false, boundary + * b==1, and b==0 guard - both halves of the single-bit test in-binary. */ +static void wb_is_power_of_two(void) +{ + int p = 0; + + (void)s_is_power_of_two(8, &p); + (void)s_is_power_of_two(6, &p); + (void)s_is_power_of_two(1, &p); + (void)s_is_power_of_two(0, &p); + WB_NOTE("s_is_power_of_two both branches exercised"); +} + +/* mp_div_d (static): divisor 0 guard; b==1 / zero-dividend quick out with the + * "d != NULL" and "c != NULL" decisions both ways; power-of-two branch; general + * long division; every c/d NULL combination in-binary. */ +static void wb_div_d(void) +{ + mp_int a, c; + mp_digit d = 0; + + if (mp_init(&a) != MP_OKAY) { WB_NOTE("div_d: init a failed, skipped"); + wb_fail = 1; return; } + if (mp_init(&c) != MP_OKAY) { WB_NOTE("div_d: init c failed, skipped"); + wb_fail = 1; mp_clear(&a); return; } + (void)mp_set(&a, 0x9ABCDE); + + (void)mp_div_d(&a, 0, &c, &d); /* b == 0 */ + (void)mp_div_d(&a, 1, &c, &d); /* b == 1, c&d set */ + (void)mp_div_d(&a, 1, NULL, &d); /* b == 1, c NULL */ + (void)mp_div_d(&a, 1, &c, NULL); /* b == 1, d NULL */ + (void)mp_div_d(&a, 16, &c, &d); /* power of two, c&d set */ + (void)mp_div_d(&a, 16, NULL, &d); /* power of two, c NULL */ + (void)mp_div_d(&a, 16, &c, NULL); /* power of two, d NULL */ + (void)mp_div_d(&a, 13, &c, &d); /* general, c&d set */ + (void)mp_div_d(&a, 13, NULL, &d); /* general, c NULL */ + (void)mp_div_d(&a, 13, &c, NULL); /* general, d NULL */ + (void)mp_set(&a, 0); + (void)mp_div_d(&a, 13, &c, &d); /* zero dividend quick out */ + + mp_clear(&a); + mp_clear(&c); + WB_NOTE("mp_div_d branch selectors exercised"); +} + +/* mp_prime_miller_rabin (static): probable-prime (a=17, base 3) and composite + * (a=15, base 2) so the composite / probably-prime decision arms are both taken + * in this binary. */ +static void wb_miller_rabin(void) +{ + mp_int a, b; + int res = 0; + + if (mp_init(&a) != MP_OKAY) { WB_NOTE("mr: init a failed, skipped"); + wb_fail = 1; return; } + if (mp_init(&b) != MP_OKAY) { WB_NOTE("mr: init b failed, skipped"); + wb_fail = 1; mp_clear(&a); return; } + + (void)mp_set(&a, 17); + (void)mp_set(&b, 3); + (void)mp_prime_miller_rabin(&a, &b, &res); /* probable prime */ + + (void)mp_set(&a, 15); + (void)mp_set(&b, 2); + (void)mp_prime_miller_rabin(&a, &b, &res); /* composite */ + + mp_clear(&a); + mp_clear(&b); + WB_NOTE("mp_prime_miller_rabin prime/composite exercised"); +} + +/* mp_prime_is_divisible (static): divisible by a small table prime (a=15, /3 -> + * res==0 true, early YES) and not divisible (a=17 -> loop runs to completion, + * res==0 false throughout). */ +static void wb_prime_is_divisible(void) +{ + mp_int a; + int res = 0; + + if (mp_init(&a) != MP_OKAY) { WB_NOTE("pid: init failed, skipped"); + wb_fail = 1; return; } + + (void)mp_set(&a, 15); + (void)mp_prime_is_divisible(&a, &res); /* divisible: res==0 true */ + + (void)mp_set(&a, 17); + (void)mp_prime_is_divisible(&a, &res); /* not divisible: loop completes */ + + mp_clear(&a); + WB_NOTE("mp_prime_is_divisible divisible/not exercised"); +} + +/* bn_reverse (static): reverse buffers whose length makes the ix1, both even and odd) and one where it does not (len<=1). */ +static void wb_bn_reverse(void) +{ + unsigned char buf[5]; + + buf[0] = 1; buf[1] = 2; buf[2] = 3; buf[3] = 4; buf[4] = 5; + bn_reverse(buf, 5); /* odd length: middle element untouched */ + bn_reverse(buf, 4); /* even length */ + bn_reverse(buf, 1); /* len 1: ixdp, ->alloc, ->used), so they additionally + * require WOLFSSL_PUBLIC_MP (non-opaque mp_int). + * + * Targeted gaps (wolfcrypt/src/integer.c), by class: + * Class 1 mp_copy() / s_mp_add() / s_mp_sub() / s_mp_mul_high_digs() + * dp==NULL sanity checks paired with a nonzero ->used/->alloc - + * an inconsistent state no public mutator can produce. + * Class 2 mp_set_bit() entry guard's dp==NULL && (alloc!=0||used!=0) + * clause - same reasoning as Class 1. + * Class 3 mp_cnt_lsb() least-significant-zero-digit loop running out of + * digits - only reachable with a non-normalized (all-zero-digit, + * ->used > 0) mp_int. + * ------------------------------------------------------------------------- */ +#if defined(USE_INTEGER_HEAP_MATH) && defined(WOLFSSL_PUBLIC_MP) + +/* ------------------------------------------------------------------------- * + * Class 1a: mp_copy() tail zero-fill loop. + * + * for (; n < b->used && b->dp; n++) { *tmpb++ = 0; } + * + * mp_copy() grows (and thereby (re)allocates) 'b' itself whenever + * b->alloc < a->used || b->alloc == 0, so by the time this loop runs + * b->dp is NULL only if b arrived with ->alloc > 0 already (skipping the + * grow) yet ->dp == NULL - an inconsistent state no public mutator can + * produce (->alloc > 0 always pairs with an allocated ->dp). + * ------------------------------------------------------------------------- */ +static void wb_mp_copy_tail_dp_null(void) +{ + mp_int a, b; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&b, 0, sizeof(b)); + mp_init(&a); + mp_init(&b); + + /* 'a' stays used == 0 (empty/zero source): mp_copy()'s FIRST digit- + * copy loop ("for (n = 0; n < a->used; n++) *tmpb++ = *tmpa++;") never + * dereferences tmpb/tmpa when a->used == 0, so it is safe to reach the + * tail loop below with a corrupted (dp==NULL) destination - the tail + * loop is the only one of the two that actually checks "&& b->dp" + * before dereferencing. */ + + /* Corrupted destination: ->alloc > 0 (skips mp_copy's own grow, whose + * condition is "b->alloc < a->used || b->alloc == 0") but ->dp == + * NULL, and ->used > a->used(0) so the tail loop actually runs. */ + b.dp = NULL; + b.alloc = 4; + b.used = 3; + (void)mp_copy(&a, &b); + b.dp = NULL; /* restore before mp_clear-equivalent cleanup below */ + b.alloc = 0; + b.used = 0; + + /* Ordinary copy into a previously-larger, properly allocated 'b': the + * "b->dp" truthy side, completed in this same binary. */ + mp_init(&b); + mp_grow(&b, 8); + b.used = 8; + (void)mp_copy(&a, &b); + + mp_clear(&a); + mp_clear(&b); + WB_NOTE("mp_copy tail zero-fill loop 'b->dp' both sides exercised"); +} + +/* ------------------------------------------------------------------------- * + * Class 1b/1c: s_mp_add() / s_mp_sub() dp==NULL sanity checks. + * + * s_mp_add(): "(min_ab > 0) && (tmpa==NULL || tmpb==NULL || tmpc==NULL)" + * min_ab = MIN(a->used, b->used), so min_ab > 0 forces BOTH a->used > 0 + * and b->used > 0 - meaning a real (non-corrupted) 'a' or 'b' would + * already have an allocated ->dp. tmpc==NULL is additionally unreachable + * on ANY call: s_mp_add() itself grows 'c' immediately beforehand whenever + * c->dp == NULL, so tmpc is never NULL when this check runs (a documented + * residual, not exercised here - see REPORT.md). + * + * s_mp_sub(): "(min_b > 0) && (tmpa==NULL || tmpb==NULL)" - min_b is just + * b->used (asymmetric with s_mp_add), so tmpa==NULL with min_b > 0 is + * actually API-reachable (a freshly-initialized, never-grown 'a' with + * ->used == 0 legitimately has ->dp == NULL) and is exercised in + * test_wolfmath.c instead; only tmpb==NULL (which DOES require + * b->used > 0 with a corrupted, unallocated ->dp) is exercised here. + * ------------------------------------------------------------------------- */ +static void wb_s_mp_add_sub_null_dp(void) +{ + mp_int a, b, c; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&b, 0, sizeof(b)); + XMEMSET(&c, 0, sizeof(c)); + mp_init(&a); + mp_init(&b); + mp_init(&c); + + /* s_mp_add: tmpa == NULL, min_ab > 0 (both a and b "nonzero", a + * corrupted: ->used > 0 but ->dp == NULL). */ + a.used = 2; /* corrupted: dp stays NULL */ + mp_set(&b, 5); /* real, nonzero: min_ab = MIN(2,1) = 1 > 0 */ + if (s_mp_add(&a, &b, &c) != WC_NO_ERR_TRACE(MP_VAL)) { + wb_fail = 1; + WB_NOTE("unexpected: s_mp_add did not reject corrupted a->dp"); + } + a.used = 0; /* restore */ + + /* s_mp_add: tmpb == NULL, min_ab > 0 (mirror image). 'b' must be + * explicitly reset here: the previous subtest's mp_set(&b, 5) already + * gave it a real, allocated ->dp, so only zeroing ->used (as done + * above) would leave ->dp non-NULL - not the corrupted state this + * pair needs. */ + mp_set(&a, 5); + XMEMSET(&b, 0, sizeof(b)); + b.used = 2; /* corrupted: dp stays NULL */ + if (s_mp_add(&a, &b, &c) != WC_NO_ERR_TRACE(MP_VAL)) { + wb_fail = 1; + WB_NOTE("unexpected: s_mp_add did not reject corrupted b->dp"); + } + b.used = 0; + + /* s_mp_add: min_ab > 0 false (one operand genuinely zero-used) and + * both dp pointers valid/NULL-but-harmless: completes the "min_ab>0" + * operand's own pair within this binary too. */ + mp_set(&a, 5); + mp_zero(&b); + (void)s_mp_add(&a, &b, &c); + + /* s_mp_sub: tmpb == NULL, min_b > 0 (b corrupted: ->used > 0 but + * ->dp == NULL; a real/nonzero). */ + mp_set(&a, 5); + XMEMSET(&b, 0, sizeof(b)); + b.used = 2; /* corrupted */ + if (s_mp_sub(&a, &b, &c) != WC_NO_ERR_TRACE(MP_VAL)) { + wb_fail = 1; + WB_NOTE("unexpected: s_mp_sub did not reject corrupted b->dp"); + } + b.used = 0; + + mp_clear(&a); + mp_clear(&b); + mp_clear(&c); + WB_NOTE("s_mp_add/s_mp_sub dp==NULL sanity checks exercised"); +} + +/* ------------------------------------------------------------------------- * + * Class 1d: s_mp_mul_high_digs() "for (ix = 0; ix < pa && a->dp; ix++)". + * + * Reaching this loop at all requires taking the SLOW path (the + * SUM-threshold fast-path guard - see test_wc_IntegerDecisionCoverage's + * comment on the disjoint-threshold residual - must be false), which needs + * a large a->used + b->used; a real mp_int with ->used that large always + * has an allocated ->dp, so "a->dp" false (with pa > 0) again needs a + * corrupted, unallocated 'a'. + * ------------------------------------------------------------------------- */ +static void wb_s_mp_mul_high_digs_null_dp(void) +{ + mp_int a, b, c; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&b, 0, sizeof(b)); + XMEMSET(&c, 0, sizeof(c)); + mp_init(&a); + mp_init(&b); + mp_init(&c); + + /* 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); + (void)s_mp_mul_high_digs(&a, &b, &c, 0); + a.used = 0; /* restore before mp_clear 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); + a.dp[0] = 3; + (void)s_mp_mul_high_digs(&a, &b, &c, 0); + + mp_clear(&a); + mp_clear(&b); + mp_clear(&c); + WB_NOTE("s_mp_mul_high_digs 'a->dp' loop guard both sides exercised"); +} + +/* ------------------------------------------------------------------------- * + * Class 2: mp_set_bit() entry guard. + * + * if (b < 0 || (a->dp == NULL && (a->alloc != 0 || a->used != 0))) + * + * The parenthesized clause needs a->dp == NULL simultaneously with + * ->alloc != 0 or ->used != 0 - the exact combination the "->dp == NULL + * implies ->alloc == 0 && ->used == 0" invariant rules out for any real + * mp_int, so it can only be shown by corrupting the struct directly. + * ------------------------------------------------------------------------- */ +static void wb_mp_set_bit_corrupted(void) +{ + mp_int a; + + XMEMSET(&a, 0, sizeof(a)); + mp_init(&a); + + /* a->dp == NULL (true) && a->alloc != 0 (true), a->used == 0: isolates + * the "a->alloc != 0" operand of the inner OR (b >= 0 held false). */ + a.alloc = 4; + if (mp_set_bit(&a, 3) != WC_NO_ERR_TRACE(MP_VAL)) { + wb_fail = 1; + WB_NOTE("unexpected: mp_set_bit did not reject dp==NULL/alloc!=0"); + } + a.alloc = 0; + + /* a->dp == NULL (true) && a->used != 0 (true), a->alloc == 0: isolates + * the "a->used != 0" operand. */ + a.used = 2; + if (mp_set_bit(&a, 3) != WC_NO_ERR_TRACE(MP_VAL)) { + wb_fail = 1; + WB_NOTE("unexpected: mp_set_bit did not reject dp==NULL/used!=0"); + } + a.used = 0; + + /* a->dp == NULL (false, via mp_grow), a->alloc/used consistent with a + * real value: completes the "a->dp == NULL" operand's own pair, and + * the ordinary success path. */ + mp_grow(&a, 4); + a.used = 1; + a.dp[0] = 5; + (void)mp_set_bit(&a, 3); + + mp_clear(&a); + WB_NOTE("mp_set_bit corrupted dp==NULL entry guard exercised"); +} + +/* ------------------------------------------------------------------------- * + * Class 3: mp_cnt_lsb() least-significant-zero-digit loop. + * + * for (x = 0; x < a->used && a->dp[x] == 0; x++) {} + * + * Same reasoning as tfm.c's fp_cnt_lsb() / sp_int.c's sp_cnt_lsb(): a + * normalized nonzero mp_int always has SOME nonzero digit below ->used, so + * the loop always terminates via "a->dp[x] == 0" going false, never via + * "x < a->used" going false - reach that side with a non-normalized + * all-zero-digit value (->used > 0, every digit 0), a state no public + * mutator can produce (mp_iszero() only checks ->used == 0). + * ------------------------------------------------------------------------- */ +static void wb_mp_cnt_lsb_all_zero_digits(void) +{ + mp_int a; + + XMEMSET(&a, 0, sizeof(a)); + mp_init(&a); + mp_grow(&a, 3); + + /* Non-normalized: ->used > 0 but every digit below ->used is 0. The + * one-past-used slot a->dp[a->used] is deliberately left NONZERO: + * mp_cnt_lsb() reads it as 'q' right after this loop and, if it were + * also zero, the subsequent "while (qq == 0)" bit-scan would spin + * forever (q never becomes nonzero) - a latent hang in mp_cnt_lsb() + * for a fully zero-digit input, not something this MC/DC pair needs + * to trigger. */ + a.used = 2; + a.dp[0] = 0; + a.dp[1] = 0; + a.dp[2] = 1; + (void)mp_cnt_lsb(&a); + + /* Normalized nonzero value (ordinary, API-reachable case), repeated + * here so both sides of "x < a->used" are shown within this binary. */ + a.used = 1; + a.dp[0] = 4; + (void)mp_cnt_lsb(&a); + + mp_clear(&a); + WB_NOTE("mp_cnt_lsb least-significant-zero-digit loop exercised"); +} + +/* ------------------------------------------------------------------------- * + * Relocated from tests/api (test_wolfmath.c). These drive library-internal, + * non-exported s_mp_* symbols (s_mp_add/s_mp_sub/s_mp_mul_digs/s_mp_exptmod/ + * s_mp_mul_high_digs/mp_invmod_slow/mp_exptmod_fast/...) that are declared + * WITHOUT MP_API and so hidden under -fvisibility=hidden in a shared-library + * build: calling them from tests/api broke the normal (shared) wolfSSL CI + * link. Compiled here via #include of integer.c they resolve directly. + * Coverage-only: both directions of each decision are executed; no return + * values are asserted (the exact call arguments are preserved, since those are + * what drive each MC/DC pair). + * ------------------------------------------------------------------------- */ + +/* Helper for wb_IntegerDecisionCoverage: grow 'a' to 'used' real digits (a + * legitimate, if arbitrary, big value - not a corrupted/non-normalized state) + * and fill every digit with 'fill'. */ +#if !defined(WOLFSSL_SP_MATH) && !defined(WOLFSSL_SP_MATH_ALL) +static void wolfmath_big_fill(mp_int* a, int used, mp_digit fill) +{ + int i; + XMEMSET(a, 0, sizeof(*a)); + mp_init(a); + mp_grow(a, used); + for (i = 0; i < used; i++) { + a->dp[i] = fill; + } + a->used = used; + a->sign = MP_ZPOS; +} +#endif + +/* + * Testing mp_init_multi/mp_copy/mp_grow/mp_mod_2d/mp_exptmod/mp_invmod/ + * mp_invmod_slow/mp_cmp_d/s_mp_add/s_mp_sub/mp_exptmod_fast/ + * mp_exptmod_base_2/mp_montgomery_reduce/mp_set_bit/mp_mul_d/mp_sqr/mp_mul/ + * s_mp_mul_digs/s_mp_exptmod/s_mp_mul_high_digs/mp_add_d/mp_sub_d/ + * mp_prime_is_prime(_ex)/mp_rand_prime/mp_read_radix/mp_radix_size/ + * mp_toradix: argument-check, capacity/MP_WARRAY-threshold and + * sign-dispatch decision branches of the HEAPMATH (integer.c) backend + * (bigint-integer module). + */ +static void wb_IntegerDecisionCoverage(void) +{ +#if !defined(WOLFSSL_SP_MATH) && !defined(WOLFSSL_SP_MATH_ALL) + mp_int a; + mp_int b; + mp_int c; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&b, 0, sizeof(b)); + XMEMSET(&c, 0, sizeof(c)); + + /* mp_init_multi: each "if (X && (mp_init(X) != MP_OKAY))" - the + * pointer-non-null operand's pair (NULL vs non-NULL for that slot); + * mp_init() itself cannot fail for a non-NULL pointer (no allocation + * happens in mp_init - see REPORT.md), so the "!= MP_OKAY" operand's + * true side is a documented residual for all six slots. */ + { + mp_int ia, ib, ic, id, ie, iff; + XMEMSET(&ia, 0, sizeof(ia)); XMEMSET(&ib, 0, sizeof(ib)); + XMEMSET(&ic, 0, sizeof(ic)); XMEMSET(&id, 0, sizeof(id)); + XMEMSET(&ie, 0, sizeof(ie)); XMEMSET(&iff, 0, sizeof(iff)); + + (void)mp_init_multi(NULL, NULL, NULL, NULL, NULL, NULL); + (void)mp_init_multi(&ia, &ib, &ic, &id, &ie, &iff); + mp_clear(&ia); mp_clear(&ib); mp_clear(&ic); + mp_clear(&id); mp_clear(&ie); mp_clear(&iff); + } + + /* mp_copy: NULL args. */ + (void)mp_copy(NULL, &a); + (void)mp_copy(&a, NULL); + (void)mp_init(&a); + (void)mp_set(&a, 5); + (void)mp_init(&b); + (void)mp_copy(&a, &b); + + /* mp_grow: "(a->alloc < size) || (size == 0) || (a->alloc == 0)". A + * fresh mp_int (a->alloc == 0) with a negative size makes the FIRST + * two operands false (0 < -5 is false; -5 == 0 is false) so only the + * third ('a->alloc == 0') determines the true outcome and enters the + * realloc branch; with MP_LOW_MEM's MP_PREC==1 the negative size isn't + * padded back to positive, so the (size_t) cast wraps to a huge + * request and the realloc legitimately fails (MP_MEM) - the branch + * itself is still what this pair targets, not the exact return code. + * A pre-grown 'a' with a smaller, nonzero request makes all three + * operands false, isolating the third operand's false side. */ + (void)mp_init(&a); + (void)mp_grow(&a, -5); /* alloc==0 (true), rest false */ + (void)mp_init(&a); + (void)mp_grow(&a, 10); + (void)mp_grow(&a, 4); /* alloc(>=10) != 0, size(4)!=0, + alloc(>=10) >= size(4): all F */ + + /* mp_mod_2d: same two-guard shape as fp_mod_2d (tfm.c); mp_mod_2d + * copies 'a' into 'c' first, so c->sign tracks a->sign. */ + (void)mp_init(&a); + (void)mp_set(&a, 5); + (void)mp_init(&c); + (void)mp_mod_2d(&a, (int)DIGIT_BIT, &c); /* ZPOS(T) && + b>=DIGIT_BIT*1 (T): early return, c == a unchanged (5) */ + (void)mp_cmp_d(&c, 5); + a.sign = MP_NEG; + (void)mp_mod_2d(&a, (int)DIGIT_BIT, &c); /* ZPOS(F) */ + a.sign = MP_ZPOS; + (void)mp_mod_2d(&a, 0, &c); /* ZPOS(T), b>=...(F) */ + (void)mp_cmp_d(&c, 0); + + /* mp_exptmod: "mp_iszero(P) || P->sign == MP_NEG" - the iszero half is + * shown elsewhere; complete the sign operand's pair with a nonzero + * negative modulus. */ + (void)mp_init(&a); + (void)mp_set(&a, 3); + (void)mp_init(&b); + (void)mp_set(&b, 2); + (void)mp_init(&c); + (void)mp_set(&c, 7); + c.sign = MP_NEG; + (void)mp_exptmod(&a, &b, &c, &a); + + /* mp_invmod: "b sign NEG || iszero(b) || iszero(a)" - three operands, + * each isolated in turn against an otherwise-valid a=3,b=7 pair. */ + (void)mp_init(&a); + (void)mp_set(&a, 3); + (void)mp_init(&b); + (void)mp_set(&b, 7); + (void)mp_init(&c); + b.sign = MP_NEG; + (void)mp_invmod(&a, &b, &c); + (void)mp_set(&b, 7); /* reset sign to ZPOS */ + mp_zero(&b); + (void)mp_invmod(&a, &b, &c); + (void)mp_set(&b, 7); + mp_zero(&a); + (void)mp_invmod(&a, &b, &c); + (void)mp_set(&a, 3); + (void)mp_invmod(&a, &b, &c); + + /* mp_invmod_slow (public in the integer.c backend): its own copy of + * the same "b sign NEG || iszero(b)" guard. */ + (void)mp_init(&a); + (void)mp_set(&a, 3); + (void)mp_init(&b); + (void)mp_set(&b, 10); /* even modulus: slow path */ + b.sign = MP_NEG; + (void)mp_invmod_slow(&a, &b, &c); + mp_zero(&b); + (void)mp_invmod_slow(&a, &b, &c); + (void)mp_set(&b, 10); + (void)mp_invmod_slow(&a, &b, &c); + + /* mp_cmp_d: "a->used==0 && b==0" and "(b && a->used==0) || sign==NEG". + * Five calls complete all four independent operands' pairs (the + * a->used==0 operand is shared between the two decisions). */ + (void)mp_init(&a); + (void)mp_cmp_d(&a, 0); /* a->used==0(T) && b==0(T) */ + (void)mp_set(&a, 9); + (void)mp_cmp_d(&a, 0); /* a->used==0(F): isolates op0 */ + (void)mp_init(&a); /* a->used==0 again */ + (void)mp_cmp_d(&a, 5); /* b==0(F): isolates op1; also + (b(T) && a->used==0(T)) => MP_LT: op2/op3 baseline */ + (void)mp_set(&a, 9); + (void)mp_cmp_d(&a, 5); /* b(T) && a->used==0(F): isolates + the "a->used==0" operand of the + second decision, sign ZPOS(F) */ + a.sign = MP_NEG; + (void)mp_cmp_d(&a, 5); /* sign==NEG(T): isolates op3 */ + + /* mp_add / s_mp_add: "min_ab > 0 && (dp==NULL...)" - the dp==NULL + * combination needs a non-normalized/corrupted mp_int (impossible via + * any public mutator) and is exercised above in this file; complete the + * "min_ab > 0" operand's pair here (both operands nonzero vs one + * operand zero-used). */ + (void)mp_init(&a); + (void)mp_set(&a, 3); + (void)mp_init(&b); + (void)mp_set(&b, 4); + (void)mp_init(&c); + (void)s_mp_add(&a, &b, &c); /* min_ab(1) > 0: true */ + mp_zero(&b); + (void)s_mp_add(&a, &b, &c); /* min_ab(0) > 0: false */ + + /* mp_sub / s_mp_sub: "min_b > 0 && (tmpa==NULL || tmpb==NULL)" - unlike + * s_mp_add above, min_b is just b->used (not MIN(a,b)), so "tmpa==NULL" + * is legitimately reachable: a freshly-initialized, never-grown 'a' + * (->used==0) has ->dp==NULL regardless of b. tmpb==NULL still needs a + * corrupted b (exercised above in this file). */ + (void)mp_set(&a, 3); + (void)mp_set(&b, 4); + (void)s_mp_sub(&a, &b, &c); /* min_b(1) > 0: true */ + mp_zero(&b); + (void)s_mp_sub(&a, &b, &c); /* min_b(0) > 0: false */ + (void)mp_init(&a); /* a->used==0: tmpa == a->dp == NULL */ + (void)mp_set(&b, 4); /* min_b(1) > 0 */ + (void)s_mp_sub(&a, &b, &c); /* tmpa==NULL: true */ + + /* mp_exptmod_fast / mp_exptmod_base_2 / mp_montgomery_reduce: all + * three share + * (N->used * 2 + 1 < MP_WARRAY) && (N->used < 1L<<(WORD_BITS-2*DB)) + * (or the SUM-based digs variant for mp_montgomery_reduce) to pick + * between the comba-accelerated and generic montgomery reduce - both + * arms are functionally equivalent (just performance), so a small and + * a large (real, legitimately grown) modulus both succeed; only the + * dispatch is different. The two thresholds are numerically disjoint + * for this shape (N->used <= 255 from the first vs N->used >= 256 + * needed for the second to go false), so the second operand's false + * side can never coexist with the first operand's true side - + * documented residual (see REPORT.md); only the first operand's pair + * is targeted here (small N takes the comba path, big N the generic + * one). */ + (void)mp_init(&a); + (void)mp_set(&a, 3); + (void)mp_init(&b); + (void)mp_set(&b, 2); + wolfmath_big_fill(&c, 20, 3); /* small odd-ish modulus-shaped value */ + c.dp[0] |= 1; /* keep it odd so montgomery_setup succeeds */ + (void)mp_exptmod_fast(&a, &b, &c, &a, 0); /* small N */ + (void)mp_set(&a, 3); + { + mp_int big; + XMEMSET(&big, 0, sizeof(big)); + wolfmath_big_fill(&big, 300, 3); /* N->used(300)*2+1 >= MP_WARRAY: + takes the generic reduce path */ + big.dp[0] |= 1; + (void)mp_exptmod_fast(&a, &b, &big, &a, 0); + mp_clear(&big); + } + (void)mp_set(&a, 3); + (void)mp_exptmod_base_2(&b, &c, &a); + { + mp_digit rho = 0; + (void)mp_montgomery_setup(&c, &rho); + (void)mp_set(&a, 5); + (void)mp_montgomery_reduce(&a, &c, rho); + } + + /* mp_set_bit: "b < 0 || (a->dp==NULL && (a->alloc!=0 || a->used!=0))". + * The parenthesized clause needs a non-normalized/corrupted mp_int (a + * real dp==NULL is only ever paired with alloc==0 && used==0) and is + * exercised above in this file; complete the "b < 0" operand's pair + * here. */ + (void)mp_init(&a); + (void)mp_set_bit(&a, -1); + (void)mp_set_bit(&a, 3); + + /* mp_mul_d: "c->dp==NULL || c->alloc < a->used+1" - both operands are + * legitimately reachable: a fresh (never-grown) destination naturally + * has dp==NULL; an under-grown destination has dp!=NULL but too small + * an alloc; a sufficiently pre-grown destination makes both false. */ + (void)mp_init(&a); + (void)mp_set(&a, 7); + (void)mp_init(&c); /* fresh: c->dp == NULL */ + (void)mp_mul_d(&a, 3, &c); /* dp==NULL(T): grows */ + (void)mp_init_size(&c, 1); /* dp!=NULL, alloc(1) < 2 */ + (void)mp_mul_d(&a, 3, &c); /* dp==NULL(F), alloc<..(T) */ + (void)mp_init_size(&c, 8); /* alloc(8) >= used(1)+1 */ + (void)mp_mul_d(&a, 3, &c); /* both false */ + + /* mp_sqr / s_mp_sqr: "(a->used*2+1 < MP_WARRAY) && a->used < + * (1 << (WORD_BITS - 2*DIGIT_BIT - 1))" - unlike the montgomery/ + * exptmod shape above, the "- 1" here shrinks the second threshold + * below the first, so both thresholds' independence pairs ARE jointly + * satisfiable: a middling a->used (>= second threshold, < first) shows + * the second operand false while the first stays true. */ + wolfmath_big_fill(&a, 10, 3); + (void)mp_init(&c); + (void)mp_sqr(&a, &c); /* both thresholds true */ + wolfmath_big_fill(&a, 200, 3); /* > 2nd threshold(~128), < 1st(255) */ + (void)mp_sqr(&a, &c); /* 1st true, 2nd false */ + wolfmath_big_fill(&a, 300, 3); /* > 1st threshold too */ + (void)mp_sqr(&a, &c); /* both false: falls back */ + + /* mp_mul: "(digs < MP_WARRAY) && MIN(a->used,b->used) <= threshold" - + * digs is the SUM a->used+b->used+1, so making MIN large forces BOTH + * operands large, which forces digs large too - the same disjoint- + * threshold shape as mp_exptmod_fast/mp_montgomery_reduce above. + * Documented residual for the MIN operand's false side (see + * REPORT.md); the digs operand's pair is targeted here (small + * operands vs one huge operand with the other tiny, which keeps MIN + * small while pushing digs past MP_WARRAY). */ + wolfmath_big_fill(&a, 10, 3); + wolfmath_big_fill(&b, 2, 3); + (void)mp_init(&c); + (void)mp_mul(&a, &b, &c); /* digs small: true */ + wolfmath_big_fill(&a, 600, 3); /* digs = 600+2+1 >= MP_WARRAY(512) */ + (void)mp_mul(&a, &b, &c); /* digs large: false */ + + /* s_mp_mul_digs: same shape, but 'digs' is a caller-supplied parameter + * independent of a->used/b->used (as long as digs >= a->used, which is + * required for memory safety regardless of MC/DC), so BOTH operands' + * pairs are directly reachable. */ + wolfmath_big_fill(&a, 1, 3); + wolfmath_big_fill(&b, 1, 3); + (void)mp_init(&c); + (void)s_mp_mul_digs(&a, &b, &c, 5); /* digs(5) 0" - same window-size + * reasoning as tfm.c's _fp_exptmod_nct (see + * wb_TfmExptModDecisionCoverage): a small exponent (<= 21 bits) + * always flushes its window exactly (bitcpy==0 at the tail); a bigger + * exponent leaves a partial window (bitcpy>0). */ + (void)mp_init(&a); + (void)mp_set(&a, 3); + (void)mp_init(&b); + (void)mp_set(&b, 7); /* 3 bits: winsize 1 */ + (void)s_mp_exptmod(&a, &b, &c, &a, 0); + (void)mp_set(&b, 0x3FFFFFFF); /* 30 bits: winsize 3 */ + (void)s_mp_exptmod(&a, &b, &c, &a, 0); + + /* s_mp_mul_high_digs: "(digs < MP_WARRAY) && MIN(a->used,b->used) < + * threshold" - here 'digs' is only used AFTER the threshold check + * (the check itself is SUM-based, from a->used+b->used, not the + * parameter), so it has the same disjoint-threshold shape as mp_mul; + * documented residual for the MIN operand (see REPORT.md), first + * operand's pair targeted here. */ + wolfmath_big_fill(&a, 10, 3); + wolfmath_big_fill(&b, 2, 3); + (void)mp_init(&c); + (void)s_mp_mul_high_digs(&a, &b, &c, 0); /* sum small:T */ + wolfmath_big_fill(&a, 600, 3); + (void)s_mp_mul_high_digs(&a, &b, &c, 0); /* sum big: F */ + + /* mp_add_d / mp_sub_d: "tmpa==NULL || tmpc==NULL". Both functions grow + * 'c' themselves (via "c->alloc < a->used+1") immediately before this + * check, so tmpc is never NULL when it runs - a documented residual, + * same class as s_mp_add's tmpc (see REPORT.md). tmpa==NULL IS + * legitimately reachable via a freshly mp_init'd (never grown) 'a', + * whose ->dp is NULL; the function then correctly reports MP_MEM + * (the guard doubles as an allocation-sanity check). */ + (void)mp_init(&a); /* a->dp == NULL */ + (void)mp_init(&c); + (void)mp_add_d(&a, 5, &c); /* tmpa==NULL: true */ + (void)mp_init(&a); + (void)mp_init(&c); + (void)mp_sub_d(&a, 5, &c); /* tmpa==NULL: true */ + + /* mp_add_d: "a->sign==MP_NEG && (a->used>1 || a->dp[0]>=b)" - three + * operands, each isolated against an otherwise-fixed baseline. */ + (void)mp_init(&a); + (void)mp_set(&a, 3); /* a->used==1, dp[0]==3 */ + a.sign = MP_NEG; + (void)mp_init(&c); + (void)mp_add_d(&a, 1, &c); /* NEG(T), used>1(F), + dp[0](3)>=b(1)(T) */ + a.sign = MP_ZPOS; + (void)mp_add_d(&a, 1, &c); /* NEG(F): isolates sign op */ + (void)mp_set(&a, 1); + a.sign = MP_NEG; + (void)mp_add_d(&a, 3, &c); /* dp[0](1)>=b(3): F, + isolates that operand */ + + /* mp_sub_d: "(a->used==1 && a->dp[0]<=b) || a->used==0" - the + * a->used==0 operand's pair (a->used==1 half is exercised by the + * ordinary single-digit subtraction paths elsewhere in this file). + * Use a previously-grown-then-zeroed 'a' (->dp allocated, ->used==0) + * rather than a freshly mp_init'd one, so the "tmpa==NULL" guard + * above doesn't intercept the call first. */ + (void)mp_init(&a); + (void)mp_set(&a, 9); + mp_zero(&a); /* ->dp stays allocated; ->used reset to 0 */ + (void)mp_init(&c); + (void)mp_sub_d(&a, 1, &c); /* a->used==0: true */ + (void)mp_set(&a, 9); + (void)mp_sub_d(&a, 1, &c); /* a->used==0: false */ + + /* mp_cnt_lsb: "x < a->used && a->dp[x]==0" - the "x < a->used going + * false via running out of digits" side needs a non-normalized + * all-zero-digit value (exercised above in this file); the "a->dp[x]==0" + * operand's own pair IS legitimately reachable: a value whose lowest + * digit is zero (shifted by exactly one whole digit) vs one whose + * lowest digit is already nonzero. */ + (void)mp_init(&a); + (void)mp_set(&a, 5); + (void)mp_cnt_lsb(&a); /* dp[0]==0: false immediately */ + (void)mp_init(&c); + (void)mp_set(&a, 1); + (void)mp_mul_2d(&a, (int)DIGIT_BIT, &c); /* dp[0]==0, + dp[1]!=0: a legitimate, + normalized value */ + (void)mp_cnt_lsb(&c); /* dp[0]==0(T) then dp[1]!=0 stops it */ + + /* mp_prime_is_prime: "t<=0 || t>PRIME_SIZE" (t<=0 half shown + * elsewhere); complete t>PRIME_SIZE's pair. */ + { + int result = 0; + + (void)mp_init(&a); + (void)mp_set(&a, 17); + (void)mp_prime_is_prime(&a, PRIME_SIZE + 1, &result); + (void)mp_prime_is_prime(&a, 8, &result); + (void)result; + } + +#if !defined(WC_NO_RNG) + { + WC_RNG rng; + int result = 0; + + XMEMSET(&rng, 0, sizeof(rng)); + (void)wc_InitRng(&rng); + + /* mp_prime_is_prime_ex: "t<=0 || t>PRIME_SIZE" - both operands' + * pairs, plus the composite-survives-trial-division case that + * reaches Miller-Rabin. */ + (void)mp_prime_is_prime_ex(&a, 0, &result, &rng); + (void)mp_prime_is_prime_ex(&a, PRIME_SIZE + 1, &result, &rng); + (void)mp_prime_is_prime_ex(&a, 8, &result, &rng); + (void)result; + (void)mp_set(&a, 1032247u); /* 1013 * 1019: both + factors bigger than every ltm_prime_tab entry */ + (void)mp_prime_is_prime_ex(&a, 8, &result, &rng); + (void)result; + + /* mp_rand_prime: "a==NULL||rng==NULL" and "len<2||len>512". + * Declared/defined only under WOLFSSL_KEY_GEN && (!NO_DH || !NO_DSA); + * gate the calls to match the library so other variants don't get an + * implicit declaration or an undefined reference. */ +#if defined(WOLFSSL_KEY_GEN) && (!defined(NO_DH) || !defined(NO_DSA)) + (void)mp_rand_prime(NULL, 32, &rng, HEAP_HINT); + (void)mp_rand_prime(&a, 32, NULL, HEAP_HINT); + (void)mp_rand_prime(&a, 1, &rng, HEAP_HINT); + (void)mp_rand_prime(&a, 513, &rng, HEAP_HINT); + (void)mp_rand_prime(&a, 32, &rng, HEAP_HINT); +#endif + + (void)wc_FreeRng(&rng); + } +#endif /* !WC_NO_RNG */ + + /* mp_read_radix: "radix < MP_RADIX_BIN || radix > MP_RADIX_MAX". */ + (void)mp_read_radix(&a, "10", 1); + (void)mp_read_radix(&a, "10", 65); + (void)mp_read_radix(&a, "10", MP_RADIX_DEC); + + /* mp_radix_size / mp_toradix: "radix < BIN || radix > MAX" (each has + * its own copy); "(digs & 1) && (radix == 16)" hex zero-padding + * (each has its own copy too). A value needing an odd number of hex + * digits (e.g. 0x1, one digit) vs an even count (e.g. 0x12, two + * digits) completes that operand's pair; radix != 16 completes the + * other operand while digs is odd. */ + { + int size = 0; + char buf[16]; + + XMEMSET(buf, 0, sizeof(buf)); + (void)mp_radix_size(&a, 65, &size); + (void)mp_set(&a, 0x1); /* 1 hex digit: odd */ + (void)mp_radix_size(&a, MP_RADIX_HEX, &size); + (void)mp_set(&a, 0x12); /* 2 hex digits: even */ + (void)mp_radix_size(&a, MP_RADIX_HEX, &size); + (void)mp_set(&a, 0x1); + (void)mp_radix_size(&a, MP_RADIX_DEC, &size); /* not hex */ + + (void)mp_toradix(&a, buf, 65); + (void)mp_set(&a, 0x1); + XMEMSET(buf, 0, sizeof(buf)); + (void)mp_toradix(&a, buf, MP_RADIX_HEX); + (void)mp_set(&a, 0x12); + XMEMSET(buf, 0, sizeof(buf)); + (void)mp_toradix(&a, buf, MP_RADIX_HEX); + (void)mp_set(&a, 0x1); + XMEMSET(buf, 0, sizeof(buf)); + (void)mp_toradix(&a, buf, MP_RADIX_DEC); + } + + mp_clear(&a); + mp_clear(&b); + mp_clear(&c); +#endif /* !WOLFSSL_SP_MATH && !WOLFSSL_SP_MATH_ALL */ + WB_NOTE("IntegerDecisionCoverage decision branches exercised"); +} + +#endif /* USE_INTEGER_HEAP_MATH && WOLFSSL_PUBLIC_MP */ + +int main(void) +{ + 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) + printf(" heapmath (integer.c) not the selected backend;" + " nothing to exercise\n"); + return 0; +#else + wb_is_power_of_two(); + wb_div_d(); + wb_miller_rabin(); + wb_prime_is_divisible(); + wb_bn_reverse(); +#ifdef WOLFSSL_PUBLIC_MP + wb_mp_copy_tail_dp_null(); + wb_s_mp_add_sub_null_dp(); + wb_s_mp_mul_high_digs_null_dp(); + wb_mp_set_bit_corrupted(); + wb_mp_cnt_lsb_all_zero_digits(); + wb_IntegerDecisionCoverage(); +#endif + 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. */ + return 0; +#endif +} diff --git a/tests/unit-mcdc/test_random_whitebox.c b/tests/unit-mcdc/test_random_whitebox.c index fd29ab4853f..cdd08d08f44 100644 --- a/tests/unit-mcdc/test_random_whitebox.c +++ b/tests/unit-mcdc/test_random_whitebox.c @@ -48,6 +48,25 @@ * sizeof(reseedCtr)) that always satisfy the guard, so the false side * needs a direct call with mismatched/zero lengths. * + * Two further GAPS.md residual classes remain justified SKIPS, deliberately + * NOT chased by this white-box (per the campaign's no-fault-injection + * convention -- same class as the documented rsa/sp-math residuals): + * + * - Hash_gen()/Hash512_gen()'s "data == NULL || digest == NULL" XMALLOC + * guard (only compiled under WOLFSSL_SMALL_STACK && + * !WOLFSSL_SMALL_STACK_CACHE): reaching either operand's true side needs + * the shared allocator to fail on one of two back-to-back XMALLOC() + * calls; this campaign injects no allocation-failure fault (same + * documented residual class as the rsa/sp-math allocation-failure + * branches -- see db/modules.json's "random" entry). + * - Hash_DRBG_Init()/Hash512_DRBG_Init()'s chained + * "Hash_df(...)==DRBG_SUCCESS && Hash_df(...)==DRBG_SUCCESS" (resp. + * Hash512_df) compound: showing either operand's false side needs + * wc_Sha256Update()/wc_Sha256Final() (resp. SHA-512) to fail mid- + * operation on a live call with valid buffers, which does not happen + * under normal library operation (same transform-failure class as the + * sha module's residuals) and is not forced here. + * * This white-box #includes random.c directly to reach these file-static * helpers and drives both sides of each leaf in the same binary (a single * clang MC/DC bitmap does not merge independence pairs across separately @@ -205,6 +224,288 @@ static void wb_hash512_gen_outsz(void) #endif /* HAVE_HASHDRBG && WOLFSSL_DRBG_SHA512 */ +/* ---- Additional file-static leaves reached only by direct call ---- */ + +#if defined(HAVE_HASHDRBG) && !defined(NO_SHA256) + +/* Hash_df()'s per-block loop: the "len" iteration count derives from outSz, + * and the copy-out "outSz > OUTPUT_BLOCK_LEN" branch is true on every block + * but the last and false on the tail. A DRBG_SEED_LEN (55-byte) request + * spans two SHA-256 blocks, so it drives both sides in one call; a + * single-block request drives only the false (tail) side. The inB/inC + * "!= NULL && Sz > 0" operand guards are exercised with present and absent + * operands. All buffers are sized to outSz, so the copy-out is memory-safe. */ +static void wb_hash_df_multiblock(void) +{ + DRBG_internal drbg; + byte out[DRBG_SEED_LEN]; + byte in[16]; + word32 i; + int ret; + + XMEMSET(&drbg, 0, sizeof(drbg)); + for (i = 0; i < (word32)sizeof(in); i++) + in[i] = (byte)(i + 5); + + /* Multi-block (len == 2): true then false side of "outSz>OUTPUT_BLOCK_LEN", + * with both inB and inC present (their "!= NULL && Sz > 0" true side). */ + ret = Hash_df(&drbg, out, (word32)sizeof(out), drbgInitV, + in, (word32)sizeof(in), in, (word32)sizeof(in), + in, (word32)sizeof(in)); + if (ret != DRBG_SUCCESS) { + WB_NOTE("Hash_df multi-block call failed"); + wb_fail = 1; + } + + /* Single-block (len == 1): tail-only copy, with inB/inC absent (the + * "inB != NULL" / "inC != NULL" false side) -- independence baseline. */ + ret = Hash_df(&drbg, out, WC_SHA256_DIGEST_SIZE, drbgReseed, + in, (word32)sizeof(in), NULL, 0, NULL, 0); + if (ret != DRBG_SUCCESS) { + WB_NOTE("Hash_df single-block call failed"); + wb_fail = 1; + } +} + +/* Hash_DRBG_Generate()'s "drbg->reseedCtr >= WC_RESEED_INTERVAL" decision: + * drive the false side (a real generate) and the true side (early + * DRBG_NEED_RESEED return) in the same binary on one instantiated DRBG. */ +static void wb_hash_drbg_generate_reseed(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 + 1); + for (i = 0; i < (word32)sizeof(nonce); i++) + nonce[i] = (byte)(i + 2); + + 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_DRBG_Generate reseed check"); + return; + } + + /* False side: reseedCtr below the interval -> generate proceeds. */ + drbg.reseedCtr = 1; + ret = Hash_DRBG_Generate(&drbg, out, (word32)sizeof(out), NULL, 0); + if (ret != DRBG_SUCCESS) { + WB_NOTE("Hash_DRBG_Generate (below interval) failed"); + wb_fail = 1; + } + + /* True side: reseedCtr at the interval -> early DRBG_NEED_RESEED. */ + drbg.reseedCtr = WC_RESEED_INTERVAL; + ret = Hash_DRBG_Generate(&drbg, out, (word32)sizeof(out), NULL, 0); + if (ret != DRBG_NEED_RESEED) { + WB_NOTE("Hash_DRBG_Generate did not signal DRBG_NEED_RESEED"); + wb_fail = 1; + } + + (void)Hash_DRBG_Uninstantiate(&drbg); +} + +/* wc_RNG_HealthTest_ex_internal()'s argument guards: + * "seedA == NULL || output == NULL" -> each operand alone + * "reseed != 0 && seedB == NULL" -> each operand alone + * "outputSz != RNG_HEALTH_TEST_CHECK_SIZE" -> wrong size vs correct size + * plus the full pass path. drbg is only touched after all guards pass, so the + * early-return calls are memory-safe with a zeroed drbg. */ +static void wb_rng_healthtest_internal(void) +{ + DRBG_internal drbg; + byte output[RNG_HEALTH_TEST_CHECK_SIZE]; + byte seedB[16]; + int ret; + + XMEMSET(&drbg, 0, sizeof(drbg)); + XMEMSET(output, 0, sizeof(output)); + XMEMSET(seedB, 9, sizeof(seedB)); + + /* seedA == NULL (1st operand true). */ + ret = wc_RNG_HealthTest_ex_internal(&drbg, 0, NULL, 0, + NULL, 0, NULL, 0, output, (word32)sizeof(output), + NULL, INVALID_DEVID); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("health-test seedA==NULL not rejected"); + wb_fail = 1; + } + + /* output == NULL (2nd operand true, 1st false). */ + ret = wc_RNG_HealthTest_ex_internal(&drbg, 0, NULL, 0, + seedA_data, (word32)sizeof(seedA_data), NULL, 0, + NULL, (word32)sizeof(output), NULL, INVALID_DEVID); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("health-test output==NULL not rejected"); + wb_fail = 1; + } + + /* reseed != 0 && seedB == NULL (both operands true). */ + ret = wc_RNG_HealthTest_ex_internal(&drbg, 1, NULL, 0, + seedA_data, (word32)sizeof(seedA_data), NULL, 0, + output, (word32)sizeof(output), NULL, INVALID_DEVID); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("health-test reseed w/ seedB==NULL not rejected"); + wb_fail = 1; + } + + /* Wrong outputSz (outputSz != RNG_HEALTH_TEST_CHECK_SIZE true side). */ + ret = wc_RNG_HealthTest_ex_internal(&drbg, 0, NULL, 0, + seedA_data, (word32)sizeof(seedA_data), NULL, 0, + output, 16, NULL, INVALID_DEVID); + if (ret == 0) { + WB_NOTE("health-test accepted wrong outputSz"); + wb_fail = 1; + } + + /* Full valid pass (all guards false, correct size): KAT self-test. */ + ret = wc_RNG_HealthTest_ex_internal(&drbg, 0, NULL, 0, + seedA_data, (word32)sizeof(seedA_data), NULL, 0, + output, RNG_HEALTH_TEST_CHECK_SIZE, NULL, INVALID_DEVID); + if (ret != 0) { + WB_NOTE("health-test valid pass unexpectedly failed"); + wb_fail = 1; + } + (void)seedB; +} + +#else + +static void wb_hash_df_multiblock(void) +{ WB_NOTE("HAVE_HASHDRBG/!NO_SHA256 off; skipped Hash_df multiblock"); } +static void wb_hash_drbg_generate_reseed(void) +{ WB_NOTE("HAVE_HASHDRBG/!NO_SHA256 off; skipped Hash_DRBG_Generate reseed"); } +static void wb_rng_healthtest_internal(void) +{ WB_NOTE("HAVE_HASHDRBG/!NO_SHA256 off; skipped health-test internal"); } + +#endif /* HAVE_HASHDRBG && !NO_SHA256 */ + +#if defined(HAVE_HASHDRBG) && defined(WOLFSSL_DRBG_SHA512) + +/* SHA-512 counterparts of the leaves above. */ +static void wb_hash512_df_multiblock(void) +{ + DRBG_SHA512_internal drbg; + byte out[128]; + byte in[16]; + word32 i; + int ret; + + XMEMSET(&drbg, 0, sizeof(drbg)); + for (i = 0; i < (word32)sizeof(in); i++) + in[i] = (byte)(i + 6); + + /* outSz(100) > OUTPUT_BLOCK_LEN(64): multi-block true then tail false. */ + ret = Hash512_df(&drbg, out, 100, drbgInitV, + in, (word32)sizeof(in), in, (word32)sizeof(in), + in, (word32)sizeof(in)); + if (ret != DRBG_SUCCESS) { + WB_NOTE("Hash512_df multi-block call failed"); + wb_fail = 1; + } + + /* Single-block tail with inB/inC absent (independence baseline). */ + ret = Hash512_df(&drbg, out, WC_SHA512_DIGEST_SIZE, drbgReseed, + in, (word32)sizeof(in), NULL, 0, NULL, 0); + if (ret != DRBG_SUCCESS) { + WB_NOTE("Hash512_df single-block call failed"); + wb_fail = 1; + } +} + +static void wb_hash512_drbg_generate_reseed(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 + 3); + for (i = 0; i < (word32)sizeof(nonce); i++) + nonce[i] = (byte)(i + 4); + + 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_DRBG_Generate reseed"); + return; + } + + drbg.reseedCtr = 1; /* false side */ + ret = Hash512_DRBG_Generate(&drbg, out, (word32)sizeof(out), NULL, 0); + if (ret != DRBG_SUCCESS) { + WB_NOTE("Hash512_DRBG_Generate (below interval) failed"); + wb_fail = 1; + } + + drbg.reseedCtr = WC_RESEED_INTERVAL; /* true side */ + ret = Hash512_DRBG_Generate(&drbg, out, (word32)sizeof(out), NULL, 0); + if (ret != DRBG_NEED_RESEED) { + WB_NOTE("Hash512_DRBG_Generate did not signal DRBG_NEED_RESEED"); + wb_fail = 1; + } + + (void)Hash512_DRBG_Uninstantiate(&drbg); +} + +static void wb_rng_healthtest512_internal(void) +{ + DRBG_SHA512_internal drbg; + byte output[RNG_HEALTH_TEST_CHECK_SIZE_SHA512]; + int ret; + + XMEMSET(&drbg, 0, sizeof(drbg)); + XMEMSET(output, 0, sizeof(output)); + + /* seedA == NULL. */ + ret = wc_RNG_HealthTest_SHA512_ex_internal(&drbg, 0, NULL, 0, NULL, 0, + NULL, 0, NULL, 0, NULL, 0, NULL, 0, + output, (word32)sizeof(output), NULL, INVALID_DEVID); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("health-test512 seedA==NULL not rejected"); + wb_fail = 1; + } + + /* reseed != 0 && seedB == NULL. */ + ret = wc_RNG_HealthTest_SHA512_ex_internal(&drbg, 1, NULL, 0, NULL, 0, + seedA_data, (word32)sizeof(seedA_data), NULL, 0, NULL, 0, NULL, 0, + output, (word32)sizeof(output), NULL, INVALID_DEVID); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("health-test512 reseed w/ seedB==NULL not rejected"); + wb_fail = 1; + } + + /* Wrong outputSz. */ + ret = wc_RNG_HealthTest_SHA512_ex_internal(&drbg, 0, NULL, 0, NULL, 0, + seedA_data, (word32)sizeof(seedA_data), NULL, 0, NULL, 0, NULL, 0, + output, 16, NULL, INVALID_DEVID); + if (ret == 0) { + WB_NOTE("health-test512 accepted wrong outputSz"); + wb_fail = 1; + } +} + +#else + +static void wb_hash512_df_multiblock(void) +{ WB_NOTE("WOLFSSL_DRBG_SHA512 off; skipped Hash512_df multiblock"); } +static void wb_hash512_drbg_generate_reseed(void) +{ WB_NOTE("WOLFSSL_DRBG_SHA512 off; skipped Hash512_DRBG_Generate reseed"); } +static void wb_rng_healthtest512_internal(void) +{ WB_NOTE("WOLFSSL_DRBG_SHA512 off; skipped health-test512 internal"); } + +#endif /* HAVE_HASHDRBG && WOLFSSL_DRBG_SHA512 */ + int main(void) { printf("random.c white-box supplement\n"); @@ -215,6 +516,12 @@ int main(void) wb_hash_gen_outsz(); wb_array_add(); wb_hash512_gen_outsz(); + wb_hash_df_multiblock(); + wb_hash_drbg_generate_reseed(); + wb_rng_healthtest_internal(); + wb_hash512_df_multiblock(); + wb_hash512_drbg_generate_reseed(); + wb_rng_healthtest512_internal(); 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_slhdsa_whitebox.c b/tests/unit-mcdc/test_slhdsa_whitebox.c new file mode 100644 index 00000000000..6d404544be8 --- /dev/null +++ b/tests/unit-mcdc/test_slhdsa_whitebox.c @@ -0,0 +1,440 @@ +/* test_slhdsa_whitebox.c + * + * White-box MC/DC supplement for wolfcrypt/src/wc_slhdsa.c (SLH-DSA / FIPS 205). + * + * The tests/api slhdsa suite drives wc_slhdsa.c through its *public* API. A few + * decisions live in file-static helpers whose branch/loop independence pairs + * cannot be shown from the public API without either an (extremely slow) full + * SLH-DSA sign/keygen or a structurally impossible argument combination: + * + * - slhdsa_find_params(): the table-scan compare "SlhDsaParams[i].param == + * param" and its found (return &entry) vs not-found (return NULL) exits. + * Every public caller only reaches this with a param already validated by + * wc_SlhDsaOidToParam(), so the NULL-return (not-found) half is dead from + * the API. Driven here directly with a valid param (found) and a bogus + * param (fall through -> NULL). + * - slhdsakey_base_2b(): the nested "for (j as the instrumented library, then + * linked against that variant's libwolfssl.a with its wc_slhdsa.o removed + * (this TU supplies the instrumented wc_slhdsa.c). NOT part of the wolfSSL + * build; not registered in tests/api. See tests/unit-mcdc/README.md. + */ + +/* Pull wc_slhdsa.c in verbatim so its file-static helpers below are in scope + * and instrumented in THIS binary. wc_slhdsa.c includes settings.h (which + * picks up user_settings.h via -DWOLFSSL_USER_SETTINGS) and wc_slhdsa.h. */ +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#ifdef WOLFSSL_HAVE_SLHDSA + +/* ------------------------------------------------------------------------- * + * slhdsa_find_params(): found vs not-found (both halves of the table compare). + * ------------------------------------------------------------------------- */ +static void wb_find_params(void) +{ + const SlhDsaParameters* p; + + if (SLHDSA_PARAM_LEN < 1) { + WB_NOTE("no SLH-DSA parameter sets compiled in; find_params skipped"); + return; + } + + /* Found: use the first compiled-in param id from the table itself so this + * works for any variant (SHAKE-only, SHA-2, restricted param sets). The + * compare "SlhDsaParams[i].param == param" goes true and the function + * returns a non-NULL entry (loop true-exit). */ + p = slhdsa_find_params(SlhDsaParams[0].param); + if (p != &SlhDsaParams[0]) { + WB_NOTE("slhdsa_find_params(valid) did not return the table entry"); + wb_fail = 1; + } + + /* Not found: a bogus param never matches, the loop runs to completion + * (compare false every iteration) and returns NULL. */ + p = slhdsa_find_params((enum SlhDsaParam)0x7fff); + if (p != NULL) { + WB_NOTE("slhdsa_find_params(bogus) did not return NULL"); + wb_fail = 1; + } + + WB_NOTE("slhdsa_find_params found/not-found pair exercised"); +} + +/* ------------------------------------------------------------------------- * + * slhdsakey_base_2b(): nested for/while loop decisions. + * ------------------------------------------------------------------------- */ +static void wb_base_2b(void) +{ + byte x[64]; + word16 out[16]; + int i; + + for (i = 0; i < (int)sizeof(x); i++) { + x[i] = (byte)(0xA5 ^ i); + } + + /* outLen == 0: outer "for (j < outLen)" false on entry (loop body never + * runs). baseb is written to zero times, so `out` is untouched -- valid. */ + slhdsakey_base_2b(x, 8, 0, out); + + /* b == 8, outLen > 0: each output consumes exactly one input byte, so the + * inner "while (bits < b)" runs once (true) then bits==8 !< 8 (false). */ + slhdsakey_base_2b(x, 8, 4, out); + + /* b == 6 < 8: first output makes the inner while true once then false + * with bits left over (6 -> next output enters with bits==2 < 6 true). */ + slhdsakey_base_2b(x, 6, 8, out); + + /* b == 12 > 8: inner while runs twice (bits 0<12 true, 8<12 true) then + * 16 !< 12 false -- the multi-iteration true side of the inner loop. */ + slhdsakey_base_2b(x, 12, 4, out); + + WB_NOTE("slhdsakey_base_2b nested-loop decisions exercised"); +} + +/* ------------------------------------------------------------------------- * + * HA_Encode() / HA_Encode_Compressed(): pure HashAddress encoders. + * ------------------------------------------------------------------------- */ +static void wb_ha_encode(void) +{ + word32 adrs[8]; + byte address[SLHDSA_HA_SZ]; + int i; + + for (i = 0; i < 8; i++) { + adrs[i] = (word32)(0x01020300u + (word32)i); + } + + /* HA_Encode: the WOLFSSL_WC_SLHDSA_SMALL variant takes the for-loop body + * (its "i < 8" decision), the default variant the unrolled writes. Either + * way this is the direct, sign-free drive of that arm. */ + XMEMSET(address, 0, sizeof(address)); + HA_Encode(adrs, address); + if ((address[0] == 0) && (address[SLHDSA_HA_SZ - 1] != 0)) { + /* purely to consume the output so it is not optimized away */ + WB_NOTE("HA_Encode produced an unexpected all-zero prefix"); + } + +#ifdef WOLFSSL_SLHDSA_SHA2 + /* HA_Encode_Compressed is only compiled under WOLFSSL_SLHDSA_SHA2. */ + XMEMSET(address, 0, sizeof(address)); + HA_Encode_Compressed(adrs, address); +#endif + + WB_NOTE("HA_Encode / HA_Encode_Compressed exercised"); +} + +/* ------------------------------------------------------------------------- * + * SHA-2 message-hash static functions: gap-closing supplement (see GAPS.md). + * + * The tests/api DecisionCoverage additions close every arg-check reachable + * from the public API. What's left needs either a real n>16 SHA-2 param + * (every campaign variant restricts to 128-bit only, see config_base's + * notes) or a ctx/ctxSz combination the public API itself rejects before + * ever reaching these static functions (ctx==NULL with ctxSz>0 is BAD_FUNC_ARG at + * the wc_SlhDsaKey_Sign/Verify layer). Both are driven directly here. + * ------------------------------------------------------------------------- */ +#ifdef WOLFSSL_SLHDSA_SHA2 + +/* slhdsakey_precompute_sha2_midstates(): "(ret==0) && (n>16)" -- n>16 + * (SHA-512 midstate) half. A locally-built SlhDsaParameters with n=24 + * exercises it without any variant ever compiling a real n>16 param. */ +static void wb_precompute_sha2_midstates(void) +{ + SlhDsaKey key; + SlhDsaParameters fakeParams; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&fakeParams, 0, sizeof(fakeParams)); + XMEMSET(key.sk, 0x11, sizeof(key.sk)); + + /* n <= 16 (WC_SLHDSA_N_128): false side. */ + fakeParams.n = 16; + key.params = &fakeParams; + if (slhdsakey_precompute_sha2_midstates(&key) != 0) { + WB_NOTE("precompute_sha2_midstates(n=16) failed"); + wb_fail = 1; + } + + /* n > 16 (categories 3, 5): true side. The function only reads + * key->params->n and key->sk, so this crafted entry is a faithful + * exercise of the SHA-512 midstate branch. */ + fakeParams.n = 24; + key.hash.sha2.sha256_mid_inited = 0; + key.hash.sha2.sha512_mid_inited = 0; + if (slhdsakey_precompute_sha2_midstates(&key) != 0) { + WB_NOTE("precompute_sha2_midstates(n=24) failed"); + wb_fail = 1; + } + + /* Free the hash objects directly (not via wc_SlhDsaKey_Free): fakeParams + * isn't a real table entry, so SLHDSA_IS_SHA2(key->params->param) can't + * be trusted to dispatch to the SHA-2 cleanup path. */ + if (key.hash.sha2.sha256_inited) { + wc_Sha256Free(&key.hash.sha2.sha256); + } + if (key.hash.sha2.sha256_2_inited) { + wc_Sha256Free(&key.hash.sha2.sha256_2); + } + if (key.hash.sha2.sha256_mid_inited) { + wc_Sha256Free(&key.hash.sha2.sha256_mid); + } + if (key.hash.sha2.sha512_inited) { + wc_Sha512Free(&key.hash.sha2.sha512); + } + if (key.hash.sha2.sha512_2_inited) { + wc_Sha512Free(&key.hash.sha2.sha512_2); + } + if (key.hash.sha2.sha512_mid_inited) { + wc_Sha512Free(&key.hash.sha2.sha512_mid); + } + + WB_NOTE("slhdsakey_precompute_sha2_midstates n<=16/n>16 pair exercised"); +} + +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY +/* slhdsakey_prf_msg_sha2(): "(ctxSz>0) && (ctx!=NULL)" -- ctx!=NULL half. + * ctxSz>0's own independence is already shown by the public Sign/Verify + * tests (ctx==NULL with ctxSz==0 vs a filled ctx with ctxSz>0); "ctxSz>0 but + * ctx==NULL" is rejected as BAD_FUNC_ARG by every public wrapper before it + * can ever reach this static, so it's driven here directly instead. */ +static void wb_prf_msg_sha2_ctx(void) +{ + SlhDsaKey key; + byte sk_prf[16], opt_rand[16], hdr[2], ctx[5], msg[4]; + byte hashOut[WC_SHA256_DIGEST_SIZE]; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(sk_prf, 0x22, sizeof(sk_prf)); + XMEMSET(opt_rand, 0x33, sizeof(opt_rand)); + XMEMSET(hdr, 0, sizeof(hdr)); + XMEMSET(ctx, 0x44, sizeof(ctx)); + XMEMSET(msg, 0x55, sizeof(msg)); + + if (slhdsakey_prf_msg_sha2(&key, sk_prf, opt_rand, hdr, NULL, + (byte)sizeof(ctx), msg, sizeof(msg), WC_SLHDSA_N_128, + hashOut) != 0) { + WB_NOTE("prf_msg_sha2(ctx==NULL,ctxSz>0) failed"); + wb_fail = 1; + } + if (slhdsakey_prf_msg_sha2(&key, sk_prf, opt_rand, hdr, ctx, + (byte)sizeof(ctx), msg, sizeof(msg), WC_SLHDSA_N_128, + hashOut) != 0) { + WB_NOTE("prf_msg_sha2(ctx!=NULL,ctxSz>0) failed"); + wb_fail = 1; + } + + WB_NOTE("slhdsakey_prf_msg_sha2 ctx!=NULL independence pair exercised"); +} +#endif /* !WOLFSSL_SLHDSA_VERIFY_ONLY */ + +/* slhdsakey_h_msg_sha2(): SHA-256 branch's "ctx!=NULL" pair (same rationale + * as wb_prf_msg_sha2_ctx above) plus the SHA-512 branch (n>16, category 3/5) + * hdr/ctx/ctxSz decisions -- unreachable from the API for the same reason as + * wb_precompute_sha2_midstates (no variant ever builds an n>16 param). */ +static void wb_h_msg_sha2_ctx_pairs(void) +{ + SlhDsaKey key; + SlhDsaParameters fakeParams; + byte r[32], hdr[2], ctx[5], msg[4], md[64]; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&fakeParams, 0, sizeof(fakeParams)); + XMEMSET(r, 0x66, sizeof(r)); + XMEMSET(hdr, 0, sizeof(hdr)); + XMEMSET(ctx, 0x77, sizeof(ctx)); + XMEMSET(msg, 0x88, sizeof(msg)); + XMEMSET(key.sk, 0x99, sizeof(key.sk)); + + /* SHA-256 branch (n == WC_SLHDSA_N_128). */ + fakeParams.n = 16; + key.params = &fakeParams; + if (slhdsakey_h_msg_sha2(&key, r, hdr, NULL, (byte)sizeof(ctx), msg, + sizeof(msg), md, 8) != 0) { + WB_NOTE("h_msg_sha2(n=16,ctx==NULL) failed"); + wb_fail = 1; + } + if (slhdsakey_h_msg_sha2(&key, r, hdr, ctx, (byte)sizeof(ctx), msg, + sizeof(msg), md, 8) != 0) { + WB_NOTE("h_msg_sha2(n=16,ctx!=NULL) failed"); + wb_fail = 1; + } + + /* SHA-512 branch (n > WC_SLHDSA_N_128). Four combinations: + * A: hdr==NULL, ctxSz==0 -> both false + * B: hdr!=NULL, ctxSz==0 -> hdr pair's true side + * C: hdr!=NULL, ctxSz>0, ctx==NULL -> ctxSz>0 T, ctx!=NULL F + * D: hdr!=NULL, ctxSz>0, ctx!=NULL -> both true + * A vs B: hdr!=NULL independence (ctxSz==0 held constant). + * B vs D: ctxSz>0 independence (ctx!=NULL held true/nonNULL). + * C vs D: ctx!=NULL independence (ctxSz>0 held true). */ + fakeParams.n = 24; + key.hash.sha2.sha256_2_inited = 0; + key.hash.sha2.sha512_2_inited = 0; + + if (slhdsakey_h_msg_sha2(&key, r, NULL, ctx, 0, msg, sizeof(msg), md, + 8) != 0) { + WB_NOTE("h_msg_sha2(n=24,A) failed"); + wb_fail = 1; + } + if (slhdsakey_h_msg_sha2(&key, r, hdr, ctx, 0, msg, sizeof(msg), md, + 8) != 0) { + WB_NOTE("h_msg_sha2(n=24,B) failed"); + wb_fail = 1; + } + if (slhdsakey_h_msg_sha2(&key, r, hdr, NULL, (byte)sizeof(ctx), msg, + sizeof(msg), md, 8) != 0) { + WB_NOTE("h_msg_sha2(n=24,C) failed"); + wb_fail = 1; + } + if (slhdsakey_h_msg_sha2(&key, r, hdr, ctx, (byte)sizeof(ctx), msg, + sizeof(msg), md, 8) != 0) { + WB_NOTE("h_msg_sha2(n=24,D) failed"); + wb_fail = 1; + } + + if (key.hash.sha2.sha256_inited) { + wc_Sha256Free(&key.hash.sha2.sha256); + } + if (key.hash.sha2.sha256_2_inited) { + wc_Sha256Free(&key.hash.sha2.sha256_2); + } + if (key.hash.sha2.sha512_inited) { + wc_Sha512Free(&key.hash.sha2.sha512); + } + if (key.hash.sha2.sha512_2_inited) { + wc_Sha512Free(&key.hash.sha2.sha512_2); + } + + WB_NOTE("slhdsakey_h_msg_sha2 hdr/ctx independence pairs exercised"); +} +#endif /* WOLFSSL_SLHDSA_SHA2 */ + +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY +/* slhdsakey_sign_internal_msg(): all 6 operands of + * "(key==NULL)||(key->params==NULL)||(m==NULL)||(sig==NULL)|| + * (sigSz==NULL)||(addRnd==NULL)". This static is only called from other + * static functions/wrappers that already validate these same pointers, so its own + * defensive re-check is otherwise dead from the API. An oversized + * fakeParams.sigLen makes the "all valid" baseline fall to the very next + * check (BAD_LENGTH_E) instead of the (very slow) WOTS+/FORS/hypertree + * signing path. */ +static void wb_sign_internal_msg_argchecks(void) +{ + SlhDsaKey key; + SlhDsaParameters fakeParams; + byte m[8], sig[8], addRnd[32]; + word32 sigSz; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&fakeParams, 0, sizeof(fakeParams)); + fakeParams.n = 16; + fakeParams.sigLen = 100000; + key.params = &fakeParams; + XMEMSET(m, 0xAA, sizeof(m)); + XMEMSET(sig, 0, sizeof(sig)); + XMEMSET(addRnd, 0x55, sizeof(addRnd)); + + sigSz = 1; + if (slhdsakey_sign_internal_msg(&key, m, sizeof(m), sig, &sigSz, addRnd) + != WC_NO_ERR_TRACE(BAD_LENGTH_E)) { + WB_NOTE("sign_internal_msg baseline (all valid) unexpected ret"); + wb_fail = 1; + } + + sigSz = 1; + if (slhdsakey_sign_internal_msg(NULL, m, sizeof(m), sig, &sigSz, addRnd) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("sign_internal_msg key==NULL unexpected ret"); + wb_fail = 1; + } + + key.params = NULL; + sigSz = 1; + if (slhdsakey_sign_internal_msg(&key, m, sizeof(m), sig, &sigSz, addRnd) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("sign_internal_msg key->params==NULL unexpected ret"); + wb_fail = 1; + } + key.params = &fakeParams; + + sigSz = 1; + if (slhdsakey_sign_internal_msg(&key, NULL, sizeof(m), sig, &sigSz, + addRnd) != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("sign_internal_msg m==NULL unexpected ret"); + wb_fail = 1; + } + + sigSz = 1; + if (slhdsakey_sign_internal_msg(&key, m, sizeof(m), NULL, &sigSz, addRnd) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("sign_internal_msg sig==NULL unexpected ret"); + wb_fail = 1; + } + + if (slhdsakey_sign_internal_msg(&key, m, sizeof(m), sig, NULL, addRnd) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("sign_internal_msg sigSz==NULL unexpected ret"); + wb_fail = 1; + } + + sigSz = 1; + if (slhdsakey_sign_internal_msg(&key, m, sizeof(m), sig, &sigSz, NULL) + != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("sign_internal_msg addRnd==NULL unexpected ret"); + wb_fail = 1; + } + + WB_NOTE("slhdsakey_sign_internal_msg arg-check independence pairs " + "exercised"); +} +#endif /* !WOLFSSL_SLHDSA_VERIFY_ONLY */ + +#endif /* WOLFSSL_HAVE_SLHDSA */ + +int main(void) +{ + printf("wc_slhdsa.c white-box supplement\n"); +#ifndef WOLFSSL_HAVE_SLHDSA + printf(" WOLFSSL_HAVE_SLHDSA not defined; nothing to exercise\n"); + return 0; +#else + wb_find_params(); + wb_base_2b(); + wb_ha_encode(); +#ifdef WOLFSSL_SLHDSA_SHA2 + wb_precompute_sha2_midstates(); +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + wb_prf_msg_sha2_ctx(); +#endif + wb_h_msg_sha2_ctx_pairs(); +#endif +#ifndef WOLFSSL_SLHDSA_VERIFY_ONLY + wb_sign_internal_msg_argchecks(); +#endif + 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. */ + return 0; +#endif +} diff --git a/tests/unit-mcdc/test_tfm_whitebox.c b/tests/unit-mcdc/test_tfm_whitebox.c new file mode 100644 index 00000000000..9cb11dd5007 --- /dev/null +++ b/tests/unit-mcdc/test_tfm_whitebox.c @@ -0,0 +1,825 @@ +/* test_tfm_whitebox.c + * + * White-box MC/DC supplement for wolfcrypt/src/tfm.c (the FASTMATH + * big-integer module, bigint-tfm, iso26262/mcdc-per-module). + * + * The tests/api wolfmath suite (test_wolfmath.c, test_wc_Tfm*DecisionCoverage) + * drives tfm.c through its *public* mp_/fp_ API. A number of decisions instead + * live in file-static helpers whose branch-selecting operand combinations a + * public caller can never present (a helper is only ever reached after the + * public entry point has already normalized / range-checked its arguments), so + * their MC/DC independence pairs cannot be demonstrated from the API without + * editing library source. This translation unit reaches them by compiling + * tfm.c directly (#include) and calling the static helpers with BOTH halves of each + * targeted pair in this one binary (llvm-cov computes MC/DC per binary; the + * campaign unions the "independence shown" bit across binaries by line:col). + * + * Build: compiled by run-mcdc.sh's white-box step with the SAME MC/DC CFLAGS + * and -I as the instrumented library, then linked against that + * variant's libwolfssl.a with its tfm.o removed (this TU supplies the + * instrumented tfm.c). NOT part of the wolfSSL build; not registered in + * tests/api. See tests/unit-mcdc/README.md. + * + * Every call is memory-safe (static helpers are handed initialized fp_ints and + * in-range selectors); setup failures print a skip and return 0 (a nonzero + * exit makes the campaign discard the variant and its coverage). + */ + +#include + +#include + +#ifndef HEAP_HINT +#define HEAP_HINT NULL +#endif + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(USE_FAST_MATH) + +/* s_is_power_of_two(b, &p): "b power of two" true (b=8), false-via-not-set + * (b=6, more than one bit), and the b==0 guard. Both halves of the internal + * "is exactly one bit set" decision in one binary. */ +static void wb_is_power_of_two(void) +{ + int p = 0; + + (void)s_is_power_of_two(8, &p); /* power of two -> FP_YES */ + (void)s_is_power_of_two(6, &p); /* not a power of two -> FP_NO */ + (void)s_is_power_of_two(1, &p); /* 1 == 2^0 boundary */ + (void)s_is_power_of_two(0, &p); /* zero: rejected */ + WB_NOTE("s_is_power_of_two both branches exercised"); +} + +/* fp_cond_swap_ct / fp_cond_swap_ct_ex: constant-time conditional swap; the + * mask = 0 - m selects swap (m=1) or no-swap (m=0). Both halves in-binary. */ +static void wb_cond_swap_ct(void) +{ + fp_int a, b; + + fp_init(&a); + fp_init(&b); + fp_set(&a, 0x1234); + fp_set(&b, 0x5678); + + (void)fp_cond_swap_ct(&a, &b, (int)a.used > (int)b.used ? + (int)a.used : (int)b.used, 1); /* m=1: swap */ + (void)fp_cond_swap_ct(&a, &b, (int)a.used > (int)b.used ? + (int)a.used : (int)b.used, 0); /* m=0: no swap */ + WB_NOTE("fp_cond_swap_ct swap/no-swap exercised"); +} + +/* fp_div_d (static): divisor 0 guard; b==1 / zero-dividend quick out; power of + * two branch (both c!=NULL and d!=NULL legs); general long-division loop with + * every c/d NULL combination so the "if (c != NULL)" / "if (d != NULL)" + * decisions are covered both ways. */ +static void wb_div_d(void) +{ + fp_int a, c; + fp_digit d = 0; + + fp_init(&a); + fp_init(&c); + fp_set(&a, 0x9ABCDE); + + (void)fp_div_d(&a, 0, &c, &d); /* b == 0 */ + (void)fp_div_d(&a, 1, &c, &d); /* b == 1 quick out */ + (void)fp_div_d(&a, 16, &c, &d); /* power of two, c&d set */ + (void)fp_div_d(&a, 16, NULL, &d); /* power of two, c NULL */ + (void)fp_div_d(&a, 16, &c, NULL); /* power of two, d NULL */ + (void)fp_div_d(&a, 13, &c, &d); /* general, c&d set */ + (void)fp_div_d(&a, 13, NULL, &d); /* general, c NULL */ + (void)fp_div_d(&a, 13, &c, NULL); /* general, d NULL */ + fp_zero(&a); + (void)fp_div_d(&a, 13, &c, &d); /* zero dividend quick out */ + + (void)fp_mod_d(&a, 13, &d); /* fp_mod_d thin wrapper */ + WB_NOTE("fp_div_d branch selectors exercised"); +} + +/* fp_read_radix_16 (static): the eol_done / whitespace decision. Trailing + * whitespace before any digit is seen (scanned from the end) hits + * "!eol_done && CharIsWhiteSpace" true (continue); a non-hex, non-whitespace + * char returns FP_VAL; embedded whitespace once digits are seen hits eol_done + * true (FP_VAL). Also the '-' sign branch. */ +static void wb_read_radix_16(void) +{ + fp_int a; + + fp_init(&a); + (void)fp_read_radix_16(&a, "1Ab2"); /* plain */ + fp_init(&a); + (void)fp_read_radix_16(&a, "1Ab2 "); /* trailing ws: !eol_done true */ + fp_init(&a); + (void)fp_read_radix_16(&a, "1A b2"); /* embedded ws: eol_done true */ + fp_init(&a); + (void)fp_read_radix_16(&a, "-1Ab2"); /* negative sign */ + fp_init(&a); + (void)fp_read_radix_16(&a, "1G2"); /* invalid hex char -> FP_VAL */ + WB_NOTE("fp_read_radix_16 whitespace/sign branches exercised"); +} + +/* fp_invmod_slow (static, general binary extended Euclid): a real inverse + * (a=3 mod 497) and a small odd modulus so the u/v parity while-loops iterate + * their true and false sides. */ +static void wb_invmod_slow(void) +{ + fp_int a, b, c; + + fp_init(&a); + fp_init(&b); + fp_init(&c); + fp_set(&a, 3); + fp_set(&b, 497); + (void)fp_invmod_slow(&a, &b, &c); + WB_NOTE("fp_invmod_slow exercised"); +} + +/* fp_prime_miller_rabin(_ex) (static): a prime candidate that passes (a=17, + * base 3) and a composite that fails (a=15, base 2) so the composite / probably + * prime decision arms are both taken in-binary. */ +static void wb_miller_rabin(void) +{ + fp_int a, b; + int res = 0; + + fp_init(&a); + fp_init(&b); + + fp_set(&a, 17); + fp_set(&b, 3); + (void)fp_prime_miller_rabin(&a, &b, &res); /* probable prime */ + + fp_set(&a, 15); + fp_set(&b, 2); + (void)fp_prime_miller_rabin(&a, &b, &res); /* composite */ + WB_NOTE("fp_prime_miller_rabin prime/composite exercised"); +} + +/* _fp_exptmod_ct / _fp_exptmod_nct / _fp_exptmod_base_2 (static engines): + * exercise directly with a small odd modulus so their internal + * window/montgomery loops run. Which of the two _fp_exptmod_ct definitions is + * compiled depends on TFM_TIMING_RESISTANT; whichever it is, this reaches it. + */ +static void wb_exptmod_engines(void) +{ + fp_int g, x, p, y; + + fp_init(&g); + fp_init(&x); + fp_init(&p); + fp_init(&y); + fp_set(&g, 4); + fp_set(&x, 13); + fp_set(&p, 497); /* odd modulus */ + +#ifdef TFM_TIMING_RESISTANT + (void)_fp_exptmod_ct(&g, &x, x.used, &p, &y); +#else + (void)_fp_exptmod_nct(&g, &x, &p, &y); +#endif + /* base-2 special engine: X as the exponent, P the modulus. */ + (void)_fp_exptmod_base_2(&x, x.used, &p, &y); + WB_NOTE("_fp_exptmod_* engines exercised"); +} + +#ifdef HAVE_INTEL_MULX +/* fp_montgomery_reduce_mulx (static, only compiled with HAVE_INTEL_MULX): the + * MULX-accelerated Montgomery reduction, non-constant-time and constant-time + * decision arms. */ +static void wb_montgomery_mulx(void) +{ + fp_int a, m; + fp_digit mp = 0; + + fp_init(&a); + fp_init(&m); + fp_set(&m, 0xF1); /* odd modulus */ + if (fp_montgomery_setup(&m, &mp) == FP_OKAY) { + fp_set(&a, 3); + (void)fp_montgomery_reduce_mulx(&a, &m, mp, 0); + fp_set(&a, 3); + (void)fp_montgomery_reduce_mulx(&a, &m, mp, 1); + } + WB_NOTE("fp_montgomery_reduce_mulx exercised"); +} +#endif /* HAVE_INTEL_MULX */ + +#endif /* USE_FAST_MATH */ + +/* ------------------------------------------------------------------------- * + * Additional gap drivers (merged from the former _gap TU). These reach into + * fp_int struct fields directly (->used, ->dp, ->sign), so they additionally + * require WOLFSSL_PUBLIC_MP (non-opaque fp_int). + * + * Targeted gaps (wolfcrypt/src/tfm.c), by class: + * Class 1 fp_mul()/fp_sqr() comba tail-clear loop: destination ->used + * left negative by a corrupted caller (structurally impossible + * via any legitimate call sequence). + * Class 2 fp_invmod_slow() entry guard - fp_invmod() (the only public + * caller) already rejects a negative/zero modulus before calling + * fp_invmod_slow(), so this file-static function's own defensive + * copy of the same guard can only be exercised by calling it + * directly. + * Class 3 fp_cnt_lsb() least-significant-zero-digit loop running out of + * digits: only reachable with a non-normalized (all-zero-digit, + * ->used > 0) fp_int. + * ------------------------------------------------------------------------- */ +#if defined(USE_FAST_MATH) && defined(WOLFSSL_PUBLIC_MP) + +/* ------------------------------------------------------------------------- * + * Class 1a: fp_mul() comba tail-clear loop. + * + * for (y = C->used; y >= 0 && y < oldused; y++) { C->dp[y] = 0; } + * + * y is assigned from C->used at the "clean:" label - on every legitimate + * call path C->used is a real digit count and can never be negative, so + * "y >= 0" is always true there. The only way to show it false is to hand + * fp_mul() a destination whose ->used field was corrupted to a negative + * value beforehand (never producible by any public mutator) and take the + * early "goto clean" path (operand size over FP_SIZE) so fp_mul() never + * overwrites C->used itself before the loop runs. + * ------------------------------------------------------------------------- */ +static void wb_fp_mul_negative_used(void) +{ + fp_int a, b, c; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&b, 0, sizeof(b)); + XMEMSET(&c, 0, sizeof(c)); + fp_init(&a); + fp_init(&b); + fp_init(&c); + + /* Normal small multiply: C->used ends up >= 0 via the ordinary path + * (the "y >= 0" true side, exercised here for this binary's own + * independence pair). */ + fp_set(&a, 3); + fp_set(&b, 5); + (void)fp_mul(&a, &b, &c); + + /* Force the "operand too big" early-exit (goto clean before C is + * touched), with C->used corrupted to a negative value beforehand: + * "y >= 0" now evaluates false on the very first loop check, so the + * clear loop body never runs. */ + fp_set(&a, 1); + a.used = FP_SIZE; /* a->used + b->used >= FP_SIZE forces the overflow + * check (fails before any comba call touches C) */ + fp_set(&b, 1); + c.used = -1; /* corrupted: not reachable via any public mutator */ + (void)fp_mul(&a, &b, &c); + c.used = 0; /* restore before fp_clear-equivalent cleanup below */ + + WB_NOTE("fp_mul comba tail-clear loop 'y >= 0' both sides exercised"); +} + +/* ------------------------------------------------------------------------- * + * Class 1b: fp_sqr() comba tail-clear loop - same reasoning as fp_mul() + * above, on the B (destination) operand of fp_sqr(A, B). + * ------------------------------------------------------------------------- */ +static void wb_fp_sqr_negative_used(void) +{ + fp_int a, b; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&b, 0, sizeof(b)); + fp_init(&a); + fp_init(&b); + + fp_set(&a, 5); + (void)fp_sqr(&a, &b); + + fp_set(&a, 1); + a.used = FP_SIZE; /* a->used * 2 > FP_SIZE forces the overflow check */ + b.used = -1; /* corrupted, unreachable via any public mutator */ + (void)fp_sqr(&a, &b); + b.used = 0; + + WB_NOTE("fp_sqr comba tail-clear loop 'y >= 0' both sides exercised"); +} + +/* ------------------------------------------------------------------------- * + * Class 2: fp_invmod_slow() entry guard. + * + * if (b->sign == FP_NEG || fp_iszero(b) == FP_YES) { return FP_VAL; } + * + * fp_invmod() - the only in-tree caller of this file-static helper - itself + * rejects a negative or zero modulus (the identical check) before ever + * reaching the `fp_iseven(b) == FP_YES` dispatch that calls + * fp_invmod_slow(); a caller can therefore never observe fp_invmod_slow() + * running its OWN copy of that guard with a negative/zero 'b'. Call it + * directly to complete both independence pairs. + * ------------------------------------------------------------------------- */ +static void wb_fp_invmod_slow_entry_guard(void) +{ + fp_int a, b, c; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&b, 0, sizeof(b)); + XMEMSET(&c, 0, sizeof(c)); + fp_init(&a); + fp_init(&b); + fp_init(&c); + + /* b->sign == FP_NEG (true), fp_iszero(b) == FP_YES (false, b is -10): + * isolates "b->sign == FP_NEG" (cond0) with cond1 held false. */ + fp_set(&a, 3); + fp_set(&b, 10); + b.sign = FP_NEG; + if (fp_invmod_slow(&a, &b, &c) != WC_NO_ERR_TRACE(FP_VAL)) { + wb_fail = 1; + WB_NOTE("unexpected: fp_invmod_slow did not reject negative b"); + } + + /* b->sign == FP_ZPOS (false), fp_iszero(b) == FP_YES (true, b == 0): + * isolates "fp_iszero(b) == FP_YES" (cond1) with cond0 held false. */ + fp_zero(&b); + if (fp_invmod_slow(&a, &b, &c) != WC_NO_ERR_TRACE(FP_VAL)) { + wb_fail = 1; + WB_NOTE("unexpected: fp_invmod_slow did not reject zero b"); + } + + /* Both false (b positive, nonzero, even): falls through to the actual + * HAC-based computation - completes both operands' independence pairs + * (cond0 = F held across this call and the negative-b call above via + * the entry guard's own true/false halves; cond1 = F held across this + * call and the zero-b call above). */ + fp_set(&a, 3); + fp_set(&b, 10); + (void)fp_invmod_slow(&a, &b, &c); + + WB_NOTE("fp_invmod_slow entry guard both operands' pairs exercised"); +} + +/* ------------------------------------------------------------------------- * + * Class 3: fp_cnt_lsb() least-significant-zero-digit loop. + * + * for (x = 0; x < a->used && a->dp[x] == 0; x++) {} + * + * fp_cnt_lsb() is public, but a normalized nonzero fp_int always has SOME + * nonzero digit below a->used (that is what "nonzero" means), so the loop + * always terminates via "a->dp[x] == 0" going false, never via running out + * of digits ("x < a->used" going false) - fp_iszero() (checked first, by + * a->used == 0) rejects the only other way to reach x == a->used. Reach the + * "x < a->used" false side with a non-normalized all-zero-digit value + * (->used > 0, every digit 0) - a state no public mutator can produce. + * ------------------------------------------------------------------------- */ +static void wb_fp_cnt_lsb_all_zero_digits(void) +{ + fp_int a; + + XMEMSET(&a, 0, sizeof(a)); + fp_init(&a); + + /* Non-normalized: ->used > 0 but every digit below ->used is 0. + * fp_iszero() only checks ->used == 0, so this slips past it and the + * for-loop runs to x == a->used (loop condition false via + * "x < a->used", not via dp[x] == 0 going false). The one-past-used + * slot a->dp[a->used] is deliberately left NONZERO here: fp_cnt_lsb() + * reads it as 'q' right after this loop and, if it were also zero, + * the subsequent "while (qq == 0)" bit-scan would spin forever (q + * never becomes nonzero) - a latent hang in fp_cnt_lsb() for a fully + * zero-digit input, not something this MC/DC pair needs to trigger. */ + a.used = 2; + a.dp[0] = 0; + a.dp[1] = 0; + a.dp[2] = 1; + (void)fp_cnt_lsb(&a); + + /* Normalized nonzero value (ordinary, API-reachable case), repeated + * here so both sides of "x < a->used" are shown within this binary. */ + a.used = 1; + a.dp[0] = 4; + (void)fp_cnt_lsb(&a); + + WB_NOTE("fp_cnt_lsb least-significant-zero-digit loop exercised"); +} + +/* ------------------------------------------------------------------------- * + * Relocated from tests/api (test_wolfmath.c). These drive library-internal, + * non-exported fp_* symbols (fp_set/fp_mul_2/fp_mul_2d/fp_invmod_mont_ct/ + * fp_exptmod_nct/...) that are declared WITHOUT MP_API and so hidden under + * -fvisibility=hidden in a shared-library build: calling them from tests/api + * broke the normal (shared) wolfSSL CI link. Compiled here via #include of + * tfm.c they resolve directly. Coverage-only: both directions of each decision + * are executed; no return values are asserted (the exact call arguments are + * preserved, since those are what drive each MC/DC pair). + * ------------------------------------------------------------------------- */ + +/* + * Testing fp_mul_2/fp_mul_2d/fp_div_2d/fp_mod_2d/fp_invmod/ + * fp_invmod_mont_ct/fp_to_unsigned_bin_len/mp_read_radix/mp_radix_size/ + * mp_toradix/mp_rand_prime/mp_prime_is_prime(_ex): argument-check, + * capacity/FP_SIZE-guard and sign-dispatch decision branches of the + * FASTMATH (tfm.c) backend (bigint-tfm module). + */ +static void wb_TfmDecisionCoverage(void) +{ +#if !defined(WOLFSSL_SP_MATH) && !defined(WOLFSSL_SP_MATH_ALL) + fp_int a; + fp_int b; + fp_int c; + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(&b, 0, sizeof(b)); + XMEMSET(&c, 0, sizeof(c)); + + /* fp_mul_2: range check is + * (a->used > FP_SIZE-1) || ((a->used == FP_SIZE-1) && (top bit set)) + * Four calls complete all three operands' independence pairs (masking + * MC/DC: the OR's first operand shares 'a->used' with the AND's first + * operand, so they cannot vary independently of each other - each is + * paired against the boundary case instead). */ + fp_zero(&a); + a.used = FP_SIZE; /* a->used > FP_SIZE-1: true, rest masked */ + (void)fp_mul_2(&a, &b); + fp_zero(&a); + a.used = FP_SIZE - 2; /* both OR operands false: ordinary small value */ + a.dp[FP_SIZE - 3] = 1; + (void)fp_mul_2(&a, &b); + /* Note: the guard reads the FIXED sentinel slot a->dp[FP_SIZE-1] (one + * past the last digit ->used == FP_SIZE-1 actually counts), not + * a->dp[a->used-1] - so the "top bit set" side needs that specific + * slot poked directly (fp_zero() leaves it 0, which every legitimate + * caller's value would too, since it is beyond ->used). */ + fp_zero(&a); + a.used = FP_SIZE - 1; /* a->used == FP_SIZE-1 true, top bit clear */ + a.dp[FP_SIZE - 2] = 1; + (void)fp_mul_2(&a, &b); + fp_zero(&a); + a.used = FP_SIZE - 1; /* a->used == FP_SIZE-1 true, top bit SET */ + a.dp[FP_SIZE - 2] = 1; + a.dp[FP_SIZE - 1] = (fp_digit)1 << (DIGIT_BIT - 1); + (void)fp_mul_2(&a, &b); + + /* fp_mul_2d: "carry && x < FP_SIZE" - carry true with room to store it + * (x < FP_SIZE, small 'a') vs carry true with the destination already + * completely full (x == FP_SIZE, top digit high bit set). */ + fp_zero(&a); + fp_set(&a, 1); + a.dp[0] = (fp_digit)1 << (DIGIT_BIT - 1); /* shifting by 1 overflows */ + (void)fp_mul_2d(&a, 1, &c); /* carry true, x(1) < FP_SIZE */ + fp_zero(&a); + a.used = FP_SIZE; + a.dp[FP_SIZE - 1] = (fp_digit)1 << (DIGIT_BIT - 1); + (void)fp_mul_2d(&a, 1, &c); /* x==FP_SIZE */ + + /* fp_div_2d: "a == c && d != NULL" / "a != c && d != NULL". Three + * calls complete both operands' independence pairs. */ + fp_zero(&a); + fp_set(&a, 200); + fp_div_2d(&a, 3, &a, &c); /* a == c (true), d != NULL (true) */ + fp_zero(&a); + fp_set(&a, 200); + fp_div_2d(&a, 3, &b, &c); /* a != c (false/true pair vs above), d!=NULL */ + fp_zero(&a); + fp_set(&a, 200); + fp_div_2d(&a, 3, &b, NULL); /* a != c, d == NULL: isolates d!=NULL */ + + /* fp_mod_2d: first guard "c->sign==FP_ZPOS && b>=DIGIT_BIT*a->used" + * (c is fp_copy(a,c) inside, so c->sign tracks a->sign); second guard + * "c->sign==FP_NEG && bmax>=FP_SIZE" (only reached once the first is + * false). */ + fp_zero(&a); + fp_set(&a, 5); /* a->used == 1 */ + fp_mod_2d(&a, (int)DIGIT_BIT, &c); /* ZPOS(T) && b>=DIGIT_BIT*1(T): + early return, c == a unchanged (5) */ + (void)fp_cmp_d(&c, 5); + fp_zero(&a); + fp_set(&a, 5); + fp_setneg(&a); + fp_mod_2d(&a, (int)DIGIT_BIT, &c); /* ZPOS(F): isolates the sign operand */ + fp_zero(&a); + fp_set(&a, 5); + fp_mod_2d(&a, 0, &c); /* ZPOS(T), b>=... (F): isolates the 'b' operand */ + (void)fp_iszero(&c); + /* second guard: build a huge negative 'a' (a->used == FP_SIZE) so the + * first guard's "b >= DIGIT_BIT*a->used" is false for a 'b' just under + * FP_SIZE*DIGIT_BIT, yet bmax = ceil(b/DIGIT_BIT) can still reach + * FP_SIZE. */ + fp_zero(&a); + a.used = FP_SIZE; + a.dp[FP_SIZE - 1] = 1; + fp_setneg(&a); + fp_mod_2d(&a, (int)(DIGIT_BIT * FP_SIZE) - 1, &c); /* NEG(T), bmax>=FP_SIZE(T) */ + fp_zero(&a); + a.used = FP_SIZE; + a.dp[FP_SIZE - 1] = 1; + fp_mod_2d(&a, (int)(DIGIT_BIT * FP_SIZE) - 1, &c); /* ZPOS: isolates NEG op */ + fp_zero(&a); + a.used = FP_SIZE; + a.dp[FP_SIZE - 1] = 1; + fp_setneg(&a); + fp_mod_2d(&a, 4, &c); /* NEG(T), bmax small (F): isolates bmax operand */ + + /* fp_invmod: "b->sign==FP_NEG || fp_iszero(b)==FP_YES" (fp_iszero(b) + * half already shown elsewhere; complete the sign operand's pair). */ + fp_zero(&a); + fp_set(&a, 3); + fp_zero(&b); + fp_set(&b, 7); + fp_setneg(&b); + (void)fp_invmod(&a, &b, &c); + fp_zero(&b); + fp_set(&b, 7); + (void)fp_invmod(&a, &b, &c); + + /* fp_invmod_mont_ct: "(a->used*2 > FP_SIZE) || (b->used*2 > FP_SIZE)". + * Three calls complete both operands' pairs. */ + fp_zero(&a); + a.used = FP_SIZE; + fp_zero(&b); + fp_set(&b, 3); + (void)fp_invmod_mont_ct(&a, &b, &c, 1); + fp_zero(&a); + fp_set(&a, 2); + fp_zero(&b); + b.used = FP_SIZE; + (void)fp_invmod_mont_ct(&a, &b, &c, 1); + fp_zero(&a); + fp_set(&a, 2); + fp_zero(&b); + fp_set(&b, 3); + (void)fp_invmod_mont_ct(&a, &b, &c, 1); + + /* fp_to_unsigned_bin_len: "i == a->used-1 && (a->dp[i]>>j) != 0" - + * output length exactly reaches the top digit, with (A) enough spare + * high bits to hold it (success) and (B) not enough (truncated, + * FP_VAL). A third call with a fully-sized buffer isolates the + * "i == a->used-1" operand (never reached: overall false). */ +#if DIGIT_BIT == 64 || DIGIT_BIT == 32 || DIGIT_BIT == 16 + { + byte buf[8]; + XMEMSET(buf, 0, sizeof(buf)); + fp_zero(&a); + fp_set(&a, 0xFF); /* fits in 1 byte */ + (void)fp_to_unsigned_bin_len(&a, buf, 4); /* i!=used-1 */ + XMEMSET(buf, 0, sizeof(buf)); + (void)fp_to_unsigned_bin_len(&a, buf, 1); /* i==used-1, + top bits already 0 */ + fp_zero(&a); + fp_set(&a, 0x1FF); /* needs 2 bytes */ + XMEMSET(buf, 0, sizeof(buf)); + (void)fp_to_unsigned_bin_len(&a, buf, 1); /* i==used-1, top bits nonzero */ + } +#endif + + /* mp_read_radix (fp_read_radix): "radix < 2 || radix > 64". */ + (void)mp_read_radix(&a, "10", 1); + (void)mp_read_radix(&a, "10", 65); + (void)mp_read_radix(&a, "10", MP_RADIX_DEC); + + /* mp_radix_size / mp_toradix: "radix < 2 || radix > 64" (each function + * has its own copy of the guard). */ + { + int size = 0; + char buf[8]; + + XMEMSET(buf, 0, sizeof(buf)); + (void)mp_radix_size(&a, 1, &size); + (void)mp_radix_size(&a, 65, &size); + (void)mp_radix_size(&a, MP_RADIX_DEC, &size); + + (void)mp_toradix(&a, buf, 1); + (void)mp_toradix(&a, buf, 65); + XMEMSET(buf, 0, sizeof(buf)); + (void)mp_toradix(&a, buf, MP_RADIX_DEC); + } + +#if !defined(WC_NO_RNG) + /* mp_rand_prime (fp_randprime): "len < 2 || len > 512" (len<2 already + * shown elsewhere; complete the len>512 operand's pair). */ + { + WC_RNG rng; + + XMEMSET(&rng, 0, sizeof(rng)); + (void)wc_InitRng(&rng); + fp_zero(&a); + /* mp_rand_prime (fp_randprime) is declared/defined only under + * WOLFSSL_KEY_GEN; gate the calls to match so keygen-off variants + * neither implicitly declare nor leave it an undefined reference. */ +#ifdef WOLFSSL_KEY_GEN + (void)mp_rand_prime(&a, 513, &rng, HEAP_HINT); + (void)mp_rand_prime(&a, 32, &rng, HEAP_HINT); +#endif + + /* mp_prime_is_prime_ex (own trial-division/Miller-Rabin engine): + * "t <= 0 || t > FP_PRIME_SIZE" (t<=0 already shown elsewhere; + * complete the t>FP_PRIME_SIZE operand's pair). */ + { + int result = 0; + + fp_zero(&a); + fp_set(&a, 17); + (void)mp_prime_is_prime_ex(&a, FP_PRIME_SIZE + 1, &result, &rng); + (void)mp_prime_is_prime_ex(&a, 8, &result, &rng); + (void)result; + } + + (void)wc_FreeRng(&rng); + } +#endif /* !WC_NO_RNG */ + + /* mp_prime_is_prime (fp_isprime_ex): trial-division loop + * "res != MP_OKAY || d == 0" - only the 'd == 0' half is targeted here + * (res != MP_OKAY is a defensive residual, see REPORT.md); and the + * Miller-Rabin loop "err != FP_OKAY || res == FP_NO": a composite that + * survives trial division (product of two primes above the built-in + * table) reaches Miller-Rabin and is correctly rejected there. */ + { + int result = 0; + + fp_zero(&a); + fp_set(&a, 17); /* prime: table hit */ + (void)mp_prime_is_prime(&a, 8, &result); + (void)result; + + fp_zero(&a); + fp_set(&a, 6); /* divisible by 2 and 3 */ + (void)mp_prime_is_prime(&a, 8, &result); + (void)result; + + /* 1013 * 1019 = 1032247: both factors are larger than every prime + * in the FP_PRIME_SIZE (256) built-in table, so trial division + * finds no divisor and the composite reaches Miller-Rabin, which + * correctly reports it composite (res == FP_NO). */ + fp_zero(&a); + fp_set(&a, 1032247u); + (void)mp_prime_is_prime(&a, 8, &result); + (void)result; + } + + mp_clear(&a); + mp_clear(&b); + mp_clear(&c); +#endif /* !WOLFSSL_SP_MATH && !WOLFSSL_SP_MATH_ALL */ + WB_NOTE("TfmDecisionCoverage decision branches exercised"); +} + +/* + * Testing fp_exptmod/fp_exptmod_ex/fp_exptmod_nct: the negative-exponent + * (X->sign == FP_NEG) branch's "invmod succeeded" / "modulus is negative" + * decision chain, common to all three entry points, and the tail + * "mode == 2 && bitcpy > 0" leftover-window decision inside the shared + * non-constant-time engine (bigint-tfm module, tfm.c). + */ +static void wb_TfmExptModDecisionCoverage(void) +{ +#if !defined(POSITIVE_EXP_ONLY) && \ + !defined(WOLFSSL_SP_MATH) && !defined(WOLFSSL_SP_MATH_ALL) + fp_int g; + fp_int x; + fp_int p; + fp_int y; + + XMEMSET(&g, 0, sizeof(g)); + XMEMSET(&x, 0, sizeof(x)); + XMEMSET(&p, 0, sizeof(p)); + XMEMSET(&y, 0, sizeof(y)); + + /* fp_exptmod: "fp_iszero(P) || (P->used > FP_SIZE/2)" - the iszero half + * is exercised elsewhere; complete the size operand's pair with a huge + * (but nonzero) modulus. */ + fp_set(&g, 3); + fp_set(&x, 2); + fp_zero(&p); + p.used = (FP_SIZE / 2) + 1; + p.dp[p.used - 1] = 1; + (void)fp_exptmod(&g, &x, &p, &y); + fp_set(&p, 15); + (void)fp_exptmod(&g, &x, &p, &y); + + /* fp_exptmod / fp_exptmod_ex / fp_exptmod_nct share the same + * negative-exponent chain: + * if ((err == 0) && (P->sign == FP_NEG)) { err = fp_add(Y, P, Y); } + * where 'err' comes from invmod(G, |P|, ...). Three calls per entry + * point complete both operands' independence pairs: + * call A: G=3, X=-3, P=7 (invmod succeeds: err==0 T; P ZPOS: F) + * call B: G=3, X=-3, P=-7 (invmod succeeds: err==0 T; P NEG: T) + * call C: G=7, X=-3, P=-7 (invmod fails (gcd=7): err==0 F; P NEG: T) + * Pair (A,B) isolates the P->sign operand (err==0 held true); + * pair (B,C) isolates the err==0 operand (P->sign held negative). */ + fp_set(&g, 3); + fp_set(&x, 3); + fp_setneg(&x); + fp_set(&p, 7); + (void)fp_exptmod(&g, &x, &p, &y); /* call A */ + fp_set(&g, 3); + fp_set(&x, 3); + fp_setneg(&x); + fp_set(&p, 7); + fp_setneg(&p); + (void)fp_exptmod(&g, &x, &p, &y); /* call B */ + fp_set(&g, 7); + fp_set(&x, 3); + fp_setneg(&x); + fp_set(&p, 7); + fp_setneg(&p); + (void)fp_exptmod(&g, &x, &p, &y); /* call C */ + + /* fp_exptmod_ex: same size guard ("fp_iszero(P)||P->used>FP_SIZE/2") + * and the same negative-exponent chain, reached through its own entry + * point (digits == 0 lets it pick X->used internally). */ + fp_set(&g, 3); + fp_set(&x, 2); + fp_zero(&p); + p.used = (FP_SIZE / 2) + 1; + p.dp[p.used - 1] = 1; + (void)fp_exptmod_ex(&g, &x, 0, &p, &y); + + fp_set(&g, 3); + fp_set(&x, 3); + fp_setneg(&x); + fp_set(&p, 7); + (void)fp_exptmod_ex(&g, &x, 0, &p, &y); /* call A */ + fp_set(&g, 3); + fp_set(&x, 3); + fp_setneg(&x); + fp_set(&p, 7); + fp_setneg(&p); + (void)fp_exptmod_ex(&g, &x, 0, &p, &y); /* call B */ + fp_set(&g, 7); + fp_set(&x, 3); + fp_setneg(&x); + fp_set(&p, 7); + fp_setneg(&p); + (void)fp_exptmod_ex(&g, &x, 0, &p, &y); /* call C */ + + /* fp_exptmod_nct: always uses the non-constant-time engine regardless + * of TFM_TIMING_RESISTANT, so its own copy of the negative-exponent + * chain is independently reachable through this entry point. */ + fp_set(&g, 3); + fp_set(&x, 3); + fp_setneg(&x); + fp_set(&p, 7); + (void)fp_exptmod_nct(&g, &x, &p, &y); /* call A */ + fp_set(&g, 3); + fp_set(&x, 3); + fp_setneg(&x); + fp_set(&p, 7); + fp_setneg(&p); + (void)fp_exptmod_nct(&g, &x, &p, &y); /* call B */ + fp_set(&g, 7); + fp_set(&x, 3); + fp_setneg(&x); + fp_set(&p, 7); + fp_setneg(&p); + (void)fp_exptmod_nct(&g, &x, &p, &y); /* call C */ + + /* _fp_exptmod_nct tail (reached via the public fp_exptmod_nct): + * "mode == 2 && bitcpy > 0" - winsize is chosen from fp_count_bits(X): + * <=21 bits picks winsize 1, which flushes every bit immediately + * (bitcpy is always 0 at the tail: the false side); a bigger X (e.g. + * 30 bits, winsize 3) leaves a partial window at the end (bitcpy > 0: + * the true side). */ + fp_set(&g, 3); + fp_set(&x, 7); /* 3 bits: winsize 1, bitcpy==0 tail */ + fp_set(&p, 15); + (void)fp_exptmod_nct(&g, &x, &p, &y); + fp_set(&g, 3); + fp_set(&x, 0x3FFFFFFF); /* 30 bits: winsize 3 */ + fp_set(&p, 15); + (void)fp_exptmod_nct(&g, &x, &p, &y); + + mp_clear(&g); + mp_clear(&x); + mp_clear(&p); + mp_clear(&y); +#endif /* !POSITIVE_EXP_ONLY && !WOLFSSL_SP_MATH && !WOLFSSL_SP_MATH_ALL */ + WB_NOTE("TfmExptModDecisionCoverage decision branches exercised"); +} + +#endif /* USE_FAST_MATH && WOLFSSL_PUBLIC_MP */ + +int main(void) +{ + printf("tfm.c white-box MC/DC supplement\n"); +#if !defined(USE_FAST_MATH) + printf(" USE_FAST_MATH not defined; nothing to exercise\n"); + return 0; +#else + wb_is_power_of_two(); + wb_cond_swap_ct(); + wb_div_d(); + wb_read_radix_16(); + wb_invmod_slow(); + wb_miller_rabin(); + wb_exptmod_engines(); +#ifdef HAVE_INTEL_MULX + wb_montgomery_mulx(); +#endif +#ifdef WOLFSSL_PUBLIC_MP + wb_fp_mul_negative_used(); + wb_fp_sqr_negative_used(); + wb_fp_invmod_slow_entry_guard(); + wb_fp_cnt_lsb_all_zero_digits(); + wb_TfmDecisionCoverage(); + wb_TfmExptModDecisionCoverage(); +#endif + 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. */ + return 0; +#endif +} diff --git a/tests/unit-mcdc/test_wc_lms_impl_whitebox.c b/tests/unit-mcdc/test_wc_lms_impl_whitebox.c new file mode 100644 index 00000000000..2bb66f67e26 --- /dev/null +++ b/tests/unit-mcdc/test_wc_lms_impl_whitebox.c @@ -0,0 +1,244 @@ +/* test_wc_lms_impl_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 + */ + +/* White-box supplement for wolfcrypt/src/wc_lms_impl.c (the native HSS/LMS + * implementation - 50 file-static WOTS / Merkle / HSS helpers). This TU + * #includes wc_lms_impl.c verbatim so the static helpers are directly reachable, and + * links against libwolfssl.a with wc_lms_impl.o trimmed (wc_lms.o kept). The + * bulk of the static helpers are exercised by driving a full public wc_LmsKey_* + * keygen / multi-sign / verify roundtrip (which flows through this file's + * wc_hss_* / wc_lms_* / wc_lmots_* helpers), one per compiled-in hash family; + * a few targeted direct static calls then flip decision false-sides that the + * public API path never reaches on a valid key. + * + * MC/DC is measured per-binary, so both sides of every targeted independence + * pair are driven here in this one instrumented binary. Crash-safety: every + * key is XMEMSET to zero before use and freed after; direct static calls use + * only stack buffers sized from the file's own constants. + */ + +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFSSL_HAVE_LMS) + +#if !defined(WOLFSSL_LMS_VERIFY_ONLY) + +/* In-memory private-key persistence for the whitebox roundtrip. */ +static byte wb_priv[8192]; +static word32 wb_privSz = 0; + +static int wb_write_key(const byte* priv, word32 privSz, void* context) +{ + (void)context; + if (privSz > (word32)sizeof(wb_priv)) + return -1; + XMEMCPY(wb_priv, priv, privSz); + wb_privSz = privSz; + return WC_LMS_RC_SAVED_TO_NV_MEMORY; +} + +static int wb_read_key(byte* priv, word32 privSz, void* context) +{ + (void)context; + if (privSz != wb_privSz) + return -1; + XMEMCPY(priv, wb_priv, privSz); + return WC_LMS_RC_READ_TO_MEMORY; +} + +/* Full keygen + multi-sign + verify (+ negative verify) for one hash family, + * flowing through the wc_hss_* / wc_lms_* / wc_lmots_* static helpers in this file. */ +static void wb_family_roundtrip(WC_RNG* rng, int hash, const char* label) +{ + LmsKey key; + byte msg[] = "wc_lms_impl whitebox message"; + byte sig[8192]; + word32 sigSz; + int i; + int ret; + + XMEMSET(&key, 0, sizeof(key)); + wb_privSz = 0; + + ret = wc_LmsKey_Init(&key, NULL, INVALID_DEVID); + if (ret == 0) + ret = wc_LmsKey_SetParameters_ex(&key, 1, 5, 8, hash); + if (ret != 0) { + /* Family not usable in this variant: a clean skip, not a failure. */ + WB_NOTE(label); + WB_NOTE(" family unavailable; skipped"); + wc_LmsKey_Free(&key); + return; + } + (void)wc_LmsKey_SetWriteCb(&key, wb_write_key); + (void)wc_LmsKey_SetReadCb(&key, wb_read_key); + (void)wc_LmsKey_SetContext(&key, (void*)wb_priv); + + if (wc_LmsKey_MakeKey(&key, rng) != 0) { + WB_NOTE("MakeKey failed"); + wb_fail = 1; + wc_LmsKey_Free(&key); + return; + } + + for (i = 0; i < 3; i++) { + sigSz = (word32)sizeof(sig); + if (wc_LmsKey_Sign(&key, sig, &sigSz, msg, (word32)sizeof(msg)) != 0) { + WB_NOTE("Sign failed"); + wb_fail = 1; + break; + } + if (wc_LmsKey_Verify(&key, sig, sigSz, msg, (word32)sizeof(msg)) != 0) { + WB_NOTE("Verify(valid) failed"); + wb_fail = 1; + break; + } + /* Negative verify: flip a byte -> drives the mismatch false-sides in + * wc_lms_verify / wc_lmots_compute_kc_from_sig / wc_lms_compute_root. */ + sig[sigSz - 1] ^= 0x01; + if (wc_LmsKey_Verify(&key, sig, sigSz, msg, (word32)sizeof(msg)) == 0) { + WB_NOTE("Verify(tampered) unexpectedly succeeded"); + wb_fail = 1; + break; + } + sig[sigSz - 1] ^= 0x01; + } + + wc_LmsKey_Free(&key); +} + +/* Direct static drives for decision false-sides not reached by a valid-key + * roundtrip. */ +static void wb_direct_statics(void) +{ + /* wc_lms_idx_inc: exercise both the "carry stops" break and the + * "carry propagates" fall-through of the increment loop. */ + { + unsigned char a[3]; + + a[0] = 0x00; a[1] = 0x00; a[2] = 0xFE; + wc_lms_idx_inc(a, sizeof(a)); /* low byte 0xFE->0xFF, break */ + if (!(a[0] == 0x00 && a[1] == 0x00 && a[2] == 0xFF)) { + WB_NOTE("idx_inc no-carry wrong"); + wb_fail = 1; + } + a[0] = 0x00; a[1] = 0x00; a[2] = 0xFF; + wc_lms_idx_inc(a, sizeof(a)); /* carry across two bytes */ + if (!(a[0] == 0x00 && a[1] == 0x01 && a[2] == 0x00)) { + WB_NOTE("idx_inc carry wrong"); + wb_fail = 1; + } + wc_lms_idx_zero(a, sizeof(a)); + if (!(a[0] == 0x00 && a[1] == 0x00 && a[2] == 0x00)) { + WB_NOTE("idx_zero wrong"); + wb_fail = 1; + } + } +} + +static void wb_run(void) +{ + WC_RNG rng; + + XMEMSET(&rng, 0, sizeof(rng)); + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; skipping roundtrips"); + return; + } + + wb_family_roundtrip(&rng, LMS_SHA256, "SHA-256/256 family"); +#ifdef WOLFSSL_LMS_SHA256_192 + wb_family_roundtrip(&rng, LMS_SHA256_192, "SHA-256/192 family"); +#endif +#ifdef WOLFSSL_LMS_SHAKE256 + wb_family_roundtrip(&rng, LMS_SHAKE256, "SHAKE256 family"); +#endif + + /* Multi-level HSS drives the wc_hss_* subtree helpers. */ + { + LmsKey key; + byte msg[] = "wc_lms_impl whitebox L2"; + byte sig[8192]; + word32 sigSz; + int i; + + XMEMSET(&key, 0, sizeof(key)); + wb_privSz = 0; + if (wc_LmsKey_Init(&key, NULL, INVALID_DEVID) == 0 && + wc_LmsKey_SetParameters(&key, 2, 5, 8) == 0) { + (void)wc_LmsKey_SetWriteCb(&key, wb_write_key); + (void)wc_LmsKey_SetReadCb(&key, wb_read_key); + (void)wc_LmsKey_SetContext(&key, (void*)wb_priv); + if (wc_LmsKey_MakeKey(&key, &rng) == 0) { + for (i = 0; i < 2; i++) { + sigSz = (word32)sizeof(sig); + if (wc_LmsKey_Sign(&key, sig, &sigSz, msg, + (word32)sizeof(msg)) != 0 || + wc_LmsKey_Verify(&key, sig, sigSz, msg, + (word32)sizeof(msg)) != 0) { + WB_NOTE("L2 sign/verify failed"); + wb_fail = 1; + break; + } + } + } + } + wc_LmsKey_Free(&key); + } + + wb_direct_statics(); + wc_FreeRng(&rng); +} + +#else /* WOLFSSL_LMS_VERIFY_ONLY */ + +static void wb_run(void) +{ + WB_NOTE("WOLFSSL_LMS_VERIFY_ONLY: no signing static helpers to drive here"); +} + +#endif /* WOLFSSL_LMS_VERIFY_ONLY */ + +#else /* !WOLFSSL_HAVE_LMS */ + +static void wb_run(void) +{ + WB_NOTE("WOLFSSL_HAVE_LMS not defined; nothing to exercise"); +} + +#endif /* WOLFSSL_HAVE_LMS */ + +int main(void) +{ + printf("wc_lms_impl.c white-box supplement\n"); + wb_run(); + printf("done (%s)\n", wb_fail ? "with failures" : "ok"); + /* Setup/skip conditions are surfaced as notes, not failures: the campaign + * discards a variant on nonzero exit. Genuine logic mismatches set + * wb_fail; return 0 regardless so a family-unavailable skip is not a + * variant-killer, but print the state above. */ + return 0; +} diff --git a/tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c b/tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c new file mode 100644 index 00000000000..29f02ab9f5c --- /dev/null +++ b/tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c @@ -0,0 +1,1027 @@ +/* test_wc_lms_impl_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 + */ + +/* White-box supplement for wolfcrypt/src/wc_lms_impl.c. + * + * This build variant does NOT define WOLFSSL_WC_LMS_SMALL, WOLFSSL_LMS_SHAKE256 + * or WOLFSSL_LMS_SHA256_192, so the code paths guarded by those macros are dead + * in this translation unit; the corresponding decisions belong to other + * campaign variants and are skipped here (see notes below and the final + * per-line residual list in the task report). + * + * This white-box #includes wc_lms_impl.c directly so it can call file-static + * helpers (wc_lms_treehash_init/update, wc_lmots_q_expand, ...) and the + * link-local (non-static but not part of the public wc_LmsKey_* API) + * wc_hss_make_key/reload_key/sign/sigsleft/verify entry points, using small, + * hand-built LmsParams instances instead of going through wc_lms.c (which is + * not touched by this file). + */ + +#include + +#include +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#ifdef WOLFSSL_HAVE_LMS + +/* Fixed hash length used throughout: SHA-256/32. Keeps every buffer size + * below fixed and simple regardless of the (levels, height, rootLevels, + * cacheBits) combination chosen per test. */ +#define WB_HLEN WC_SHA256_DIGEST_SIZE +#define WB_WIDTH 8U +#define WB_P 34U /* LMS_P(w=8, wb=3, hLen=32) = LMS_U(32) + LMS_V(2) */ +#define WB_LS 0U /* LMS_LS(w=8, wb=3) = 16 - LMS_V(2)*8 */ + +/* Fill in a small, self-consistent LmsParams instance. levels/height/ + * rootLevels/cacheBits are caller supplied (rootLevels and cacheBits need + * not match the "real" wc_lms_map table -- this white-box never goes + * through wc_lms.c, so the only requirement is internal self-consistency: + * 0 < rootLevels <= height, 0 <= cacheBits <= height). Width is fixed at 8 + * (SHA-256/32, LMOTS_SHA256_N32_W8) for every instance. */ +static void wb_make_params(LmsParams* params, word8 levels, word8 height, + word8 rootLevels, word8 cacheBits) +{ + XMEMSET(params, 0, sizeof(*params)); + params->levels = levels; + params->height = height; + params->width = (word8)WB_WIDTH; + params->ls = (word8)WB_LS; + params->p = (word16)WB_P; + params->lmsType = LMS_SHA256_M32_H5; + params->lmOtsType = LMOTS_SHA256_N32_W8; + params->hash_len = (word16)WB_HLEN; + /* sig_len = leading Nspk count (4) + + * levels * LMS_SIG_LEN(height, p, hash_len) + + * (levels - 1) * LMS_PUBKEY_LEN(hash_len) + * (matches LMS_PARAMS_SIG_LEN() in wc_lms.c, hand-expanded here since + * that macro isn't visible from this file -- see task notes). */ + params->sig_len = 4U + + (word32)levels * LMS_SIG_LEN(height, params->p, params->hash_len) + + (word32)(levels - 1U) * LMS_PUBKEY_LEN(params->hash_len); + params->rootLevels = rootLevels; + params->cacheBits = cacheBits; +} + +/* Set up an LmsState's hash contexts for params. Mirrors + * wc_lmskey_state_init() in wc_lms.c (not callable here -- static in a + * different TU) inlined for this non-SHAKE build. */ +static int wb_state_init(LmsState* state, const LmsParams* params) +{ + int ret; + + XMEMSET(state, 0, sizeof(*state)); + state->params = params; + + 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) +{ + wc_Sha256Free(LMS_STATE_HASH_K(state)); + wc_Sha256Free(LMS_STATE_HASH(state)); +} + +/******************************************************************* + * 814:9:814:53:0-3 + * wc_lmots_q_expand(): if ((w!=8)&&(w!=4)&&(w!=2)&&(w!=1)) + * + * Purely structural: no crypto needed. All 4 independence pairs are + * covered by calling with each valid width (all-false leaf) and with an + * invalid width chosen to make each operand the sole true-to-false + * divergence point (left to right: 8,4,2,1). + ******************************************************************/ +static void wb_q_expand(void) +{ + byte q[WB_HLEN + 2]; + byte a[LMS_MAX_P]; + int ret; + int w; + + XMEMSET(q, 0x42, sizeof(q)); + + /* All-false leaf: every valid width. */ + for (w = 8; w >= 1; w >>= 1) { + ret = wc_lmots_q_expand(q, (word8)WB_HLEN, (word8)w, (word8)0, a); + if (ret != 0) { + WB_NOTE("wc_lmots_q_expand valid width unexpectedly failed"); + wb_fail = 1; + } + } + + /* w=9: first operand (w!=8) true, rest never evaluated in this specific + * short-circuit chain is irrelevant to MC/DC -- what matters is each + * operand is, in turn, the LAST one keeping the overall expression from + * being all-false. Use values that hit each operand's leaf explicitly: + * w=9 -> only "w!=8" distinguishing vs w=8 baseline + * w=5 -> only "w!=4" distinguishing vs w=4 baseline + * w=3 -> only "w!=2" distinguishing vs w=2 baseline + * w=6 -> only "w!=1" distinguishing vs w=1 baseline (w=6 != all of + * 8,4,2,1, so whole expression true; combined with the w=1 + * baseline (all false) above, isolates the last operand). */ + { + static const int bad_w[] = { 9, 5, 3, 6 }; + size_t i; + for (i = 0; i < sizeof(bad_w) / sizeof(bad_w[0]); i++) { + ret = wc_lmots_q_expand(q, (word8)WB_HLEN, (word8)bad_w[i], + (word8)0, a); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("wc_lmots_q_expand invalid width did not fail"); + wb_fail = 1; + } + } + } + + WB_NOTE("814 wc_lmots_q_expand width leaves: closed (0-3)"); +} + +/******************************************************************* + * 1017:17:1017:46:0 wc_lmots_compute_y_from_seed() outer loop + * 1261:21:1261:50:0 wc_lmots_compute_kc_from_sig() outer loop (non-SHAKE + * active arm) + * for (i = 0; (ret == 0) && (i < params->p); i++) + * + * The only gap is the "ret == 0" operand's independence (i < p's pair is + * already covered by ordinary successful use). Both functions call + * wc_lmots_q_expand() before this loop; giving it an invalid width makes + * ret non-zero *before* the loop's first evaluation, where i (0) < p is + * still true -- exactly the independence pair needed, without touching any + * crypto operation. Paired with a normal, valid call (ret stays 0, loop + * runs to completion) as the baseline "true" side. + ******************************************************************/ +static void wb_compute_y_kc_ret(void) +{ + LmsParams good, bad; + LmsState state; + byte seed[WB_HLEN]; + byte msg[8] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + byte c[WB_HLEN]; + byte y[WB_P * WB_HLEN]; + byte sig_y[WB_P * WB_HLEN]; + byte kc[WB_HLEN]; + int ret; + + wb_make_params(&good, 1, 5, 2, 2); + bad = good; + bad.width = 3; /* invalid: not 1/2/4/8 */ + + XMEMSET(seed, 0x11, sizeof(seed)); + XMEMSET(c, 0x22, sizeof(c)); + XMEMSET(sig_y, 0x33, sizeof(sig_y)); + + /* --- wc_lmots_compute_y_from_seed (1017) --- */ + if (wb_state_init(&state, &good) == 0) { + ret = wc_lmots_compute_y_from_seed(&state, seed, msg, sizeof(msg), c, + y); + if (ret != 0) { + WB_NOTE("compute_y_from_seed valid-width baseline failed"); + wb_fail = 1; + } + wb_state_free(&state); + } + else { + WB_NOTE("wb_state_init failed for compute_y_from_seed baseline"); + wb_fail = 1; + } + + if (wb_state_init(&state, &bad) == 0) { + ret = wc_lmots_compute_y_from_seed(&state, seed, msg, sizeof(msg), c, + y); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("compute_y_from_seed invalid-width did not fail early"); + wb_fail = 1; + } + wb_state_free(&state); + } + else { + WB_NOTE("wb_state_init failed for compute_y_from_seed bad-width call"); + wb_fail = 1; + } + WB_NOTE("1017 compute_y_from_seed outer-loop ret leaf: closed"); + + /* --- wc_lmots_compute_kc_from_sig (1261, via wc_lmots_calc_kc) --- */ + if (wb_state_init(&state, &good) == 0) { + byte fake_sig[4 + WB_HLEN]; + XMEMSET(fake_sig, 0, sizeof(fake_sig)); + c32toa(good.lmOtsType & LMS_H_W_MASK, fake_sig); + ret = wc_lmots_calc_kc(&state, fake_sig, msg, sizeof(msg), fake_sig, + kc); + if (ret != 0) { + WB_NOTE("compute_kc_from_sig valid-width baseline failed"); + wb_fail = 1; + } + wb_state_free(&state); + } + else { + WB_NOTE("wb_state_init failed for compute_kc_from_sig baseline"); + wb_fail = 1; + } + + if (wb_state_init(&state, &bad) == 0) { + byte fake_sig[4 + WB_HLEN]; + XMEMSET(fake_sig, 0, sizeof(fake_sig)); + c32toa(bad.lmOtsType & LMS_H_W_MASK, fake_sig); + ret = wc_lmots_calc_kc(&state, fake_sig, msg, sizeof(msg), fake_sig, + kc); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("compute_kc_from_sig invalid-width did not fail early"); + wb_fail = 1; + } + wb_state_free(&state); + } + else { + WB_NOTE("wb_state_init failed for compute_kc_from_sig bad-width call"); + wb_fail = 1; + } + WB_NOTE("1261 compute_kc_from_sig outer-loop ret leaf: closed"); +} + +/******************************************************************* + * 2233:13:2233:66:0-1 wc_lms_treehash_init(): auth_path store + * 2253:17:2254:59:1 wc_lms_treehash_init(): root-cache boundary copy + * 2261:17:2261:77:0-1 wc_lms_treehash_init(): auth_path store (parent) + * + * Called directly with a hand-built LmsPrivState. auth_path!=NULL's false + * side is unreachable from any real caller (wc_hss_init_auth_path always + * supplies a real buffer) but is trivially safe to demonstrate here since + * the code short-circuits before dereferencing a NULL auth_path. rootLevels + * < height (2 < 5) makes both sides of the 2253 boundary check + * (h > height-rootLevels) occur naturally within one full-tree pass, since + * the last leaf's carry chain climbs h from 1 up to height. + * + * The "ret == 0" operand of 2233/2261 is not closed here (would need a mid + * loop failure of the underlying SHA-256 block transform, not selectable + * without corrupting library state) -- residual, documented in the report. + ******************************************************************/ +static void wb_treehash_init_edges(void) +{ + LmsParams params; + LmsState state; + byte id[LMS_I_LEN]; + byte seed[WB_HLEN]; + byte auth_path[5 * WB_HLEN]; + byte stack_buf[(5 + 1) * WB_HLEN]; + byte root_buf[((1U << 2) - 1U) * WB_HLEN]; /* rootLevels=2 */ + byte leaf_cache[(1U << 2) * WB_HLEN]; /* cacheBits=2 */ + LmsPrivState priv; + int ret; + + wb_make_params(¶ms, 1, 5, 2, 2); + XMEMSET(id, 0xAA, sizeof(id)); + XMEMSET(seed, 0xBB, sizeof(seed)); + XMEMSET(auth_path, 0, sizeof(auth_path)); + XMEMSET(stack_buf, 0, sizeof(stack_buf)); + XMEMSET(root_buf, 0, sizeof(root_buf)); + XMEMSET(leaf_cache, 0, sizeof(leaf_cache)); + + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for treehash_init_edges"); + wb_fail = 1; + return; + } + + /* auth_path != NULL: real, non-NULL buffer -- true side of 2233/2261's + * second operand, and exercises 2253's boundary check on both sides + * across the full 32-leaf tree (rootLevels=2 < height=5). */ + XMEMSET(&priv, 0, sizeof(priv)); + priv.auth_path = auth_path; + priv.stack.stack = stack_buf; + priv.root = root_buf; + priv.leaf.cache = leaf_cache; + ret = wc_lms_treehash_init(&state, &priv, id, seed, 5); + if (ret != 0) { + WB_NOTE("treehash_init (auth_path!=NULL) baseline failed"); + wb_fail = 1; + } + + /* auth_path == NULL: unreachable via any real caller, but memory-safe + * (short-circuited) and closes the "auth_path != NULL" leaf's false + * side for 2233 and 2261. */ + XMEMSET(&priv, 0, sizeof(priv)); + priv.auth_path = NULL; + priv.stack.stack = stack_buf; + priv.root = root_buf; + priv.leaf.cache = leaf_cache; + ret = wc_lms_treehash_init(&state, &priv, id, seed, 5); + if (ret != 0) { + WB_NOTE("treehash_init (auth_path==NULL) call failed"); + wb_fail = 1; + } + + wb_state_free(&state); + WB_NOTE("2233/2253/2261 treehash_init auth_path+boundary leaves: " + "auth_path operand + 2253 boundary closed; ret-operand residual"); +} + +/******************************************************************* + * 2370:17:2370:64:0-1 wc_lms_treehash_update(): + * if ((i == leaf->idx + max_cb) && (i < (q + max_cb))) + * + * Fully structural (no ret involved). Three direct calls, each preceded by + * a fresh treehash_init(q=10) to deterministically reset leaf->idx to 10 + * (cacheBits=2 => max_cb=4, so leaf->idx+max_cb=14): + * A: min=max=14, q=20 -> i==14 (true), i<24 (true) [baseline true/true] + * B: min=max=14, q=10 -> i==14 (true), i<14 (false) [operand1 pair vs A] + * C: min=max=16, q=20 -> i==14? no (false), i<24 (true) [operand0 pair vs A] + ******************************************************************/ +static void wb_treehash_update_leafslide(void) +{ + LmsParams params; + LmsState state; + byte id[LMS_I_LEN]; + byte seed[WB_HLEN]; + byte auth_path[5 * WB_HLEN]; + byte stack_buf[(5 + 1) * WB_HLEN]; + byte root_buf[((1U << 2) - 1U) * WB_HLEN]; + byte leaf_cache[(1U << 2) * WB_HLEN]; + LmsPrivState priv; + int ret; + int i; + /* { min_idx, max_idx, q } per call. */ + static const word32 calls[3][3] = { + { 14, 14, 20 }, + { 14, 14, 10 }, + { 16, 16, 20 }, + }; + + wb_make_params(¶ms, 1, 5, 2, 2); + XMEMSET(id, 0xAA, sizeof(id)); + XMEMSET(seed, 0xBB, sizeof(seed)); + XMEMSET(auth_path, 0, sizeof(auth_path)); + XMEMSET(stack_buf, 0, sizeof(stack_buf)); + XMEMSET(root_buf, 0, sizeof(root_buf)); + XMEMSET(leaf_cache, 0, sizeof(leaf_cache)); + + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for treehash_update_leafslide"); + wb_fail = 1; + return; + } + + for (i = 0; i < 3; i++) { + XMEMSET(&priv, 0, sizeof(priv)); + priv.auth_path = auth_path; + priv.stack.stack = stack_buf; + priv.root = root_buf; + priv.leaf.cache = leaf_cache; + + /* Reset leaf cache window deterministically to [10, 14). */ + ret = wc_lms_treehash_init(&state, &priv, id, seed, 10); + if (ret != 0) { + WB_NOTE("treehash_init reset failed in leafslide test"); + wb_fail = 1; + continue; + } + + ret = wc_lms_treehash_update(&state, &priv, id, seed, + calls[i][0], calls[i][1], calls[i][2], 0); + if (ret != 0) { + WB_NOTE("treehash_update failed in leafslide test"); + wb_fail = 1; + } + } + + wb_state_free(&state); + WB_NOTE("2370 treehash_update leaf-cache-slide leaves: closed (0-1)"); +} + +/******************************************************************* + * 2875:9:2875:66:0 wc_lms_verify(): + * if ((ret == 0) && (XMEMCMP(pub_k, tc, params->hash_len) != 0)) + * + * Sign a message for real with a tiny 1-level tree, verify it once + * successfully (XMEMCMP == 0, false side), then corrupt one byte of the + * signature's authentication path (forces a different Tc) and verify again + * (XMEMCMP != 0, true side, SIG_VERIFY_E). + ******************************************************************/ +static void wb_verify_corrupt(void) +{ + LmsParams params; + LmsState state; + WC_RNG rng; + HssPrivKey priv_key; + byte priv_raw[HSS_PRIVATE_KEY_LEN(WB_HLEN)]; + byte pub[HSS_PUBLIC_KEY_LEN(WB_HLEN)]; + byte* priv_data; + word32 priv_data_len; + byte msg[] = "wc_lms_impl whitebox 2875 message"; + byte sig[4U + 5U * LMS_SIG_LEN(5, WB_P, WB_HLEN)]; + int ret; + + XMEMSET(priv_raw, 0, sizeof(priv_raw)); + XMEMSET(pub, 0, sizeof(pub)); + XMEMSET(sig, 0, sizeof(sig)); + + wb_make_params(¶ms, 1, 5, 5, 5); + priv_data_len = LMS_PRIV_DATA_LEN(params.levels, params.height, params.p, + params.rootLevels, params.cacheBits, params.hash_len); + priv_data = (byte*)XMALLOC(priv_data_len, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (priv_data == NULL) { + WB_NOTE("XMALLOC failed for verify_corrupt priv_data"); + wb_fail = 1; + return; + } + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed for verify_corrupt"); + wb_fail = 1; + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return; + } + + XMEMSET(&priv_key, 0, sizeof(priv_key)); + ret = 0; + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for verify_corrupt"); + wb_fail = 1; + } + else { + ret = wc_hss_make_key(&state, &rng, priv_raw, &priv_key, priv_data, + pub); + if (ret != 0) { + WB_NOTE("wc_hss_make_key failed for verify_corrupt"); + wb_fail = 1; + } + else { + ret = wc_hss_sign(&state, priv_raw, &priv_key, priv_data, msg, + (word32)sizeof(msg), sig); + if (ret != 0) { + WB_NOTE("wc_hss_sign failed for verify_corrupt"); + wb_fail = 1; + } + } + wb_state_free(&state); + } + + if (ret == 0) { + /* Baseline: real signature verifies (XMEMCMP == 0, false side). */ + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for verify_corrupt verify baseline"); + wb_fail = 1; + } + else { + ret = wc_hss_verify(&state, pub, msg, (word32)sizeof(msg), sig, + (word32)sizeof(sig)); + if (ret != 0) { + WB_NOTE("wc_hss_verify baseline failed to verify good sig"); + wb_fail = 1; + } + wb_state_free(&state); + } + + /* Corrupt the last byte of the *real* signature data (the sig[] + * buffer is sized generously above the actual params.sig_len, so + * the true end of the authentication path is at params.sig_len - 1, + * not sizeof(sig) - 1). This flips a byte deep in the + * authentication path so Tc changes but all the earlier structural + * checks (type, sizes) still pass. */ + sig[params.sig_len - 1] ^= 0xFF; + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for verify_corrupt corrupt case"); + wb_fail = 1; + } + else { + ret = wc_hss_verify(&state, pub, msg, (word32)sizeof(msg), sig, + (word32)sizeof(sig)); + if (ret != WC_NO_ERR_TRACE(SIG_VERIFY_E)) { + WB_NOTE("wc_hss_verify did not reject corrupted signature"); + wb_fail = 1; + } + wb_state_free(&state); + } + } + + wc_FreeRng(&rng); + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + WB_NOTE("2875 wc_lms_verify XMEMCMP leaf: closed"); +} + +/******************************************************************* + * 3976:9:3976:75:0-1 wc_hss_sign(): rootLevels==0 || rootLevels>height + * 3981:9:3981:59:0-1 wc_hss_sign(): ret==0 && !wc_hss_sigsleft(...) + * 3985:9:3985:42:0-1 wc_hss_sign(): ret==0 && !priv_key->inited + * 4004:9:4004:56:0 wc_hss_sign(): ret==0 && wc_hss_sigsleft(...) + * + * All four checks are the first statements of wc_hss_sign(); calling it + * directly with hand-crafted params/priv_raw/priv_key state reaches every + * combination cheaply and safely (each bad-params call returns before + * touching any per-level state). + ******************************************************************/ +static void wb_hss_sign_checks(void) +{ + LmsParams good; + LmsState state; + HssPrivKey priv_key; + byte priv_raw[HSS_PRIVATE_KEY_LEN(WB_HLEN)]; + byte sig[4U + 5U * LMS_SIG_LEN(5, WB_P, WB_HLEN)]; + byte msg[] = "3976/3981/3985/4004"; + byte* priv_data; + word32 priv_data_len; + WC_RNG rng; + byte pub[HSS_PUBLIC_KEY_LEN(WB_HLEN)]; + int ret; + + XMEMSET(priv_raw, 0, sizeof(priv_raw)); + XMEMSET(sig, 0, sizeof(sig)); + XMEMSET(pub, 0, sizeof(pub)); + + wb_make_params(&good, 1, 5, 5, 5); + priv_data_len = LMS_PRIV_DATA_LEN(good.levels, good.height, good.p, + good.rootLevels, good.cacheBits, good.hash_len); + priv_data = (byte*)XMALLOC(priv_data_len, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (priv_data == NULL) { + WB_NOTE("XMALLOC failed for hss_sign_checks priv_data"); + wb_fail = 1; + return; + } + + /* --- 3976: rootLevels == 0 (true side, operand 0) --- */ + { + LmsParams bad = good; + bad.rootLevels = 0; + XMEMSET(&priv_key, 0, sizeof(priv_key)); + if (wb_state_init(&state, &bad) == 0) { + ret = wc_hss_sign(&state, priv_raw, &priv_key, priv_data, msg, + (word32)sizeof(msg), sig); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("hss_sign rootLevels==0 did not fail"); + wb_fail = 1; + } + wb_state_free(&state); + } + } + /* --- 3976: rootLevels > height (true side, operand 1) --- */ + { + LmsParams bad = good; + bad.rootLevels = (word8)(bad.height + 1U); + XMEMSET(&priv_key, 0, sizeof(priv_key)); + if (wb_state_init(&state, &bad) == 0) { + ret = wc_hss_sign(&state, priv_raw, &priv_key, priv_data, msg, + (word32)sizeof(msg), sig); + if (ret != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) { + WB_NOTE("hss_sign rootLevels>height did not fail"); + wb_fail = 1; + } + wb_state_free(&state); + } + } + WB_NOTE("3976 wc_hss_sign rootLevels leaves: closed (0-1)"); + + /* Build a real key to drive 3981/3985/4004 with valid rootLevels. */ + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed for hss_sign_checks"); + wb_fail = 1; + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return; + } + XMEMSET(&priv_key, 0, sizeof(priv_key)); + ret = -1; + if (wb_state_init(&state, &good) == 0) { + ret = wc_hss_make_key(&state, &rng, priv_raw, &priv_key, priv_data, + pub); + wb_state_free(&state); + } + if (ret != 0) { + WB_NOTE("wc_hss_make_key failed setting up hss_sign_checks"); + wb_fail = 1; + wc_FreeRng(&rng); + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return; + } + + /* --- 3981/4004: !wc_hss_sigsleft() operand --- + * At this point priv_key.inited == 1 (make_key -> reload_key set it), + * so the 3985 "!inited" leaf reads false here; that leaf's true side is + * exercised separately below by resetting inited by hand. Force the + * raw key's q counter to the last valid index (sigsleft() still true) + * for the 4004/3981 "true" baseline, then to the exhausted count + * (sigsleft() false) for 3981's "true" side. */ + { + byte priv_raw_ok[HSS_PRIVATE_KEY_LEN(WB_HLEN)]; + byte priv_raw_exhausted[HSS_PRIVATE_KEY_LEN(WB_HLEN)]; + w64wrapper q; + + XMEMCPY(priv_raw_ok, priv_raw, sizeof(priv_raw_ok)); + q = w64From32(0, ((word32)1U << good.height) - 1U); /* last leaf */ + c64toa(&q, priv_raw_ok); + + XMEMCPY(priv_raw_exhausted, priv_raw, sizeof(priv_raw_exhausted)); + q = w64From32(0, (word32)1U << good.height); /* one past last */ + c64toa(&q, priv_raw_exhausted); + + /* sigsleft() == true, everything valid: ret stays 0 through 3981 + * and 4004 evaluates with sigsleft() still true (natural "true" + * side for 4004, and the baseline "true" side of 3981's operand). */ + if (wb_state_init(&state, &good) == 0) { + ret = wc_hss_sign(&state, priv_raw_ok, &priv_key, priv_data, msg, + (word32)sizeof(msg), sig); + if (ret != 0) { + WB_NOTE("hss_sign with 1 sig left unexpectedly failed"); + wb_fail = 1; + } + wb_state_free(&state); + } + + /* sigsleft() == false: KEY_EXHAUSTED_E, closes 3981's true side. */ + if (wb_state_init(&state, &good) == 0) { + ret = wc_hss_sign(&state, priv_raw_exhausted, &priv_key, + priv_data, msg, (word32)sizeof(msg), sig); + if (ret != WC_NO_ERR_TRACE(KEY_EXHAUSTED_E)) { + WB_NOTE("hss_sign on exhausted key did not fail"); + wb_fail = 1; + } + wb_state_free(&state); + } + } + WB_NOTE("3981/4004 wc_hss_sign sigsleft leaves: closed"); + + /* --- 3985: !priv_key->inited operand --- + * priv_key.inited is fully under our control (our own struct); force + * it to 0 to take the "true" side (re-init auth paths), holding + * rootLevels/sigsleft valid (ret==0) both times. */ + { + byte priv_raw_ok[HSS_PRIVATE_KEY_LEN(WB_HLEN)]; + XMEMCPY(priv_raw_ok, priv_raw, sizeof(priv_raw_ok)); + + priv_key.inited = 0; + if (wb_state_init(&state, &good) == 0) { + ret = wc_hss_sign(&state, priv_raw_ok, &priv_key, priv_data, msg, + (word32)sizeof(msg), sig); + if (ret != 0) { + WB_NOTE("hss_sign with inited==0 unexpectedly failed"); + wb_fail = 1; + } + wb_state_free(&state); + } + } + WB_NOTE("3985 wc_hss_sign inited leaf: closed"); + + wc_FreeRng(&rng); + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); +} + +/******************************************************************* + * 3207:18:3208:40:0-1 wc_hss_next_subtree_inc(): + * else if ((qc == (1< qc==max (true), cp64_hi qc==max (true), cp64_hi qc==max (false), cp64_hi 31 */ + int i; + /* { child q stored in curr, q64 argument } */ + struct { word32 qc; w64wrapper q64; } calls[3]; + int ret; + + wb_make_params(¶ms, 2, 5, 2, 2); + priv_data_len = LMS_PRIV_DATA_LEN(params.levels, params.height, params.p, + params.rootLevels, params.cacheBits, params.hash_len); + priv_data = (byte*)XMALLOC(priv_data_len, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (priv_data == NULL) { + WB_NOTE("XMALLOC failed for next_subtree_inc priv_data"); + wb_fail = 1; + return; + } + + calls[0].qc = maxq; + calls[0].q64 = w64From32(0, maxq + 32U); + calls[1].qc = maxq; + calls[1].q64 = w64From32(0, 0U); + calls[2].qc = 5U; + calls[2].q64 = w64From32(0, maxq + 32U); + + for (i = 0; i < 3; i++) { + LmsState state; + + XMEMSET(&priv_key, 0, sizeof(priv_key)); + wc_hss_priv_data_load(¶ms, &priv_key, priv_data); + + /* Second level (child) block's stored q. */ + c32toa(calls[i].qc, priv_key.priv + LMS_PRIV_LEN(params.hash_len)); + /* First (top) level's stored q is not read by this function, but + * initialize the whole raw private-key area to keep it defined. */ + XMEMSET(priv_key.priv, 0, LMS_PRIV_LEN(params.hash_len) * 2U); + c32toa(calls[i].qc, priv_key.priv + LMS_PRIV_LEN(params.hash_len)); + + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed in next_subtree_inc"); + wb_fail = 1; + continue; + } + ret = wc_hss_next_subtree_inc(&state, &priv_key, calls[i].q64); + if (ret != 0) { + WB_NOTE("wc_hss_next_subtree_inc call failed"); + wb_fail = 1; + } + wb_state_free(&state); + } + + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + WB_NOTE("3207 wc_hss_next_subtree_inc qc/w64LT leaves: closed (0-1)"); +} + +/******************************************************************* + * 4111:9:4111:43:0 wc_hss_verify(): ret==0 && (nspk+1 != levels) + * 4119:21:4119:45:0 wc_hss_verify(): for(...) ret==0 && (i < nspk) + * + * 4111: hold "nspk+1 != levels" false (i.e. nspk+1 == levels) in both runs, + * toggle ret via an earlier check ("levels != state->params->levels") by + * feeding a crafted pub whose L field disagrees with params->levels while + * nspk is chosen to match that (wrong) L, so only ret varies. + * + * 4119: needs a run where the loop's ret becomes non-zero while i < nspk is + * still true at the *next* iteration's check -- only possible when + * nspk >= 2, i.e. levels >= 3. A real 3-level make_key + sign gives a + * genuine nspk=2 signature; corrupting the first embedded LMS signature's + * final byte forces wc_lms_verify() to fail at i=0, leaving i=1 < nspk(=2) + * true at the loop's re-check. + ******************************************************************/ +static void wb_hss_verify_checks(void) +{ + LmsParams params; + LmsState state; + int ret; + + wb_make_params(¶ms, 1, 5, 5, 5); + + /* --- 4111: ret operand, nspk+1!=levels held false --- */ + { + byte pub[HSS_PUBLIC_KEY_LEN(WB_HLEN)]; + byte sig[LMS_L_LEN + LMS_SIG_LEN(5, WB_P, WB_HLEN)]; + byte msg[4] = { 0, 0, 0, 0 }; + word32 wrong_levels = params.levels + 1U; /* != params->levels */ + + XMEMSET(pub, 0, sizeof(pub)); + c32toa(wrong_levels, pub); + XMEMSET(sig, 0, sizeof(sig)); + /* nspk + 1 == wrong_levels, so operand 1 (nspk+1 != levels) is + * false in this run, matching the baseline run below. */ + c32toa(wrong_levels - 1U, sig); + + if (wb_state_init(&state, ¶ms) == 0) { + ret = wc_hss_verify(&state, pub, msg, (word32)sizeof(msg), sig, + (word32)sizeof(sig)); + if (ret != WC_NO_ERR_TRACE(SIG_VERIFY_E)) { + WB_NOTE("hss_verify levels-mismatch case did not fail as " + "expected"); + wb_fail = 1; + } + wb_state_free(&state); + } + } + /* Baseline: a real, matching single-level verify -- see wb_verify_corrupt + * for the successful path (ret==0, operand1 false) reused here to avoid + * duplicating a full make_key/sign cycle. That call already shows ret + * true & operand1 false; combined with the crafted call above (ret + * false & operand1 false), 4111's "ret" independence pair is closed. */ + WB_NOTE("4111 wc_hss_verify levels/nspk leaf: closed"); + + /* --- 4119: 3-level key, corrupt first chained LMS signature --- */ + { + LmsParams p3; + HssPrivKey priv_key; + WC_RNG rng; + byte priv_raw[HSS_PRIVATE_KEY_LEN(WB_HLEN)]; + byte pub[HSS_PUBLIC_KEY_LEN(WB_HLEN)]; + byte* priv_data; + word32 priv_data_len; + byte sig[4U + 3U * LMS_SIG_LEN(5, WB_P, WB_HLEN) + + 2U * LMS_PUBKEY_LEN(WB_HLEN)]; + byte msg[] = "4119 nspk-loop message"; + + XMEMSET(priv_raw, 0, sizeof(priv_raw)); + XMEMSET(pub, 0, sizeof(pub)); + XMEMSET(sig, 0, sizeof(sig)); + + wb_make_params(&p3, 3, 5, 2, 2); + priv_data_len = LMS_PRIV_DATA_LEN(p3.levels, p3.height, p3.p, + p3.rootLevels, p3.cacheBits, p3.hash_len); + priv_data = (byte*)XMALLOC(priv_data_len, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (priv_data == NULL) { + WB_NOTE("XMALLOC failed for 4119 priv_data"); + wb_fail = 1; + } + else if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed for 4119 test"); + wb_fail = 1; + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + else { + XMEMSET(&priv_key, 0, sizeof(priv_key)); + ret = -1; + if (wb_state_init(&state, &p3) == 0) { + ret = wc_hss_make_key(&state, &rng, priv_raw, &priv_key, + priv_data, pub); + if (ret == 0) { + ret = wc_hss_sign(&state, priv_raw, &priv_key, priv_data, + msg, (word32)sizeof(msg), sig); + } + wb_state_free(&state); + } + if (ret != 0) { + WB_NOTE("3-level make_key/sign failed setting up 4119 test"); + wb_fail = 1; + } + else { + /* Corrupt the last byte of the FIRST chained LMS signature + * (right before its trailing auth path ends and the next + * embedded public key begins) so wc_lms_verify() fails at + * loop iteration i=0, leaving the re-check at i=1 with + * i < nspk(=2) still true while ret has just gone false. */ + word32 first_lms_sig_end = LMS_L_LEN + + LMS_SIG_LEN(p3.height, p3.p, p3.hash_len); + sig[first_lms_sig_end - 1] ^= 0xFF; + + if (wb_state_init(&state, &p3) == 0) { + ret = wc_hss_verify(&state, pub, msg, + (word32)sizeof(msg), sig, (word32)sizeof(sig)); + if (ret == 0) { + WB_NOTE("hss_verify accepted a corrupted chained " + "signature"); + wb_fail = 1; + } + wb_state_free(&state); + } + } + wc_FreeRng(&rng); + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + } + WB_NOTE("4119 wc_hss_verify nspk-loop ret leaf: closed"); +} + +/******************************************************************* + * Natural full-cycle drive: levels=2, height=5 HSS key, signing across a + * subtree boundary (33 signatures on a 32-leaf bottom tree forces exactly + * one top-level subtree transition). This exercises, across many (i, q, h) + * combinations for free: + * - 3357/3361 (q==0 new-subtree vs q!=0 branches in + * wc_hss_update_auth_path) + * - 3375/3402 (root-levels loop bound and sign-smoothing update) + * - 3854/3918 (levels-loop and root-copy conditions in + * wc_hss_sign_build_sig, other operand than ret) + * - general sanity that the whole chain (make_key/sign x33/verify x33) + * stays correct with custom rootLevels/cacheBits smaller than height. + * The "ret == 0" operand on all of these remains a residual (mid-operation + * crypto failure, not selectable). + ******************************************************************/ +static void wb_hss_full_cycle(void) +{ + LmsParams params; + LmsState state; + WC_RNG rng; + HssPrivKey priv_key; + byte priv_raw[HSS_PRIVATE_KEY_LEN(WB_HLEN)]; + byte pub[HSS_PUBLIC_KEY_LEN(WB_HLEN)]; + byte* priv_data; + word32 priv_data_len; + byte sig[4U + 2U * LMS_SIG_LEN(5, WB_P, WB_HLEN) + + 1U * LMS_PUBKEY_LEN(WB_HLEN)]; + int ret; + int i; + + XMEMSET(priv_raw, 0, sizeof(priv_raw)); + XMEMSET(pub, 0, sizeof(pub)); + XMEMSET(sig, 0, sizeof(sig)); + + wb_make_params(¶ms, 2, 5, 2, 2); + priv_data_len = LMS_PRIV_DATA_LEN(params.levels, params.height, params.p, + params.rootLevels, params.cacheBits, params.hash_len); + priv_data = (byte*)XMALLOC(priv_data_len, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (priv_data == NULL) { + WB_NOTE("XMALLOC failed for hss_full_cycle priv_data"); + wb_fail = 1; + return; + } + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed for hss_full_cycle"); + wb_fail = 1; + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return; + } + + XMEMSET(&priv_key, 0, sizeof(priv_key)); + ret = -1; + if (wb_state_init(&state, ¶ms) == 0) { + ret = wc_hss_make_key(&state, &rng, priv_raw, &priv_key, priv_data, + pub); + wb_state_free(&state); + } + if (ret != 0) { + WB_NOTE("hss_full_cycle make_key failed"); + wb_fail = 1; + } + else { + for (i = 0; i < 33; i++) { + byte msg[16]; + XMEMSET(msg, (byte)i, sizeof(msg)); + + ret = -1; + if (wb_state_init(&state, ¶ms) == 0) { + ret = wc_hss_sign(&state, priv_raw, &priv_key, priv_data, + msg, (word32)sizeof(msg), sig); + wb_state_free(&state); + } + if (ret != 0) { + WB_NOTE("hss_full_cycle sign failed mid-sequence"); + wb_fail = 1; + break; + } + + if (wb_state_init(&state, ¶ms) == 0) { + ret = wc_hss_verify(&state, pub, msg, (word32)sizeof(msg), + sig, (word32)sizeof(sig)); + if (ret != 0) { + WB_NOTE("hss_full_cycle verify failed mid-sequence"); + wb_fail = 1; + } + wb_state_free(&state); + } + } + } + + wc_FreeRng(&rng); + XFREE(priv_data, NULL, DYNAMIC_TYPE_TMP_BUFFER); + WB_NOTE("hss_full_cycle (levels=2, subtree transition) drive complete"); +} + +#else /* !WOLFSSL_HAVE_LMS */ + +static void wb_q_expand(void) +{ + WB_NOTE("WOLFSSL_HAVE_LMS not compiled in this variant; skipped"); +} +static void wb_compute_y_kc_ret(void) {} +static void wb_treehash_init_edges(void) {} +static void wb_treehash_update_leafslide(void) {} +static void wb_verify_corrupt(void) {} +static void wb_hss_sign_checks(void) {} +static void wb_next_subtree_inc(void) {} +static void wb_hss_verify_checks(void) {} +static void wb_hss_full_cycle(void) {} + +#endif /* WOLFSSL_HAVE_LMS */ + +int main(void) +{ + printf("wc_lms_impl.c white-box supplement\n"); +#ifndef WOLFSSL_HAVE_LMS + printf(" WOLFSSL_HAVE_LMS not defined; nothing to exercise\n"); + return 0; +#else + wb_q_expand(); + wb_compute_y_kc_ret(); + wb_treehash_init_edges(); + wb_treehash_update_leafslide(); + wb_verify_corrupt(); + wb_hss_sign_checks(); + wb_next_subtree_inc(); + wb_hss_verify_checks(); + wb_hss_full_cycle(); + + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Setup failures are surfaced as skips (printed notes + wb_fail), not + * process failures: the campaign discards a variant's whole coverage + * on non-zero exit, so this always returns 0. */ + return 0; +#endif +} diff --git a/tests/unit-mcdc/test_wc_mldsa_whitebox.c b/tests/unit-mcdc/test_wc_mldsa_whitebox.c new file mode 100644 index 00000000000..09b9fd3b3a1 --- /dev/null +++ b/tests/unit-mcdc/test_wc_mldsa_whitebox.c @@ -0,0 +1,731 @@ +/* test_wc_mldsa_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 + */ + +/* White-box MC/DC supplement for wolfcrypt/src/wc_mldsa.c. + * + * wc_mldsa.c carries ~137 file-static helpers implementing the FIPS 204 + * ML-DSA primitives (NTT, encode/decode packing, decompose, make/check/use + * hint, range checks, ASN.1 length parsing, param lookup). Every public + * caller drives them only with well-formed, self-consistent operands, so the + * argument-check and bound-check decisions inside these helpers cannot have + * both halves of each independence pair demonstrated from tests/api. This TU + * #includes wc_mldsa.c so the static functions are in scope and calls each + * targeted helper with BOTH halves of every targeted decision in a single + * binary + * (MC/DC is computed per binary). + * + * Scope choices to keep the binary fast and memory-safe: + * - only branch-bearing helpers are driven (the branchless bit-trick + * helpers -- mldsa_red / mldsa_mont_red / mldsa_decompose_q* -- carry no + * decisions and are exercised structurally by the API tests); + * - ML-DSA-44 constants (smallest param set) are used for the hint/range + * helpers; + * - no keygen/sign/verify round trip is performed here (the API tests own + * those positive paths); every call is on a small stack buffer. + * + * Crash-safety: all inputs are bounded, fixed-size stack arrays sized to + * MLDSA_N coefficients; no helper is handed a short/NULL buffer it would + * dereference past. On any unexpected result we print a note and continue; + * the binary always returns 0 so the campaign keeps the variant. + */ + +#include + +#include + +static int wb_notes = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); wb_notes++; } while (0) +#define WB_OK(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFSSL_HAVE_MLDSA) + +/* ------------------------------------------------------------------ * + * mldsa_get_params: for-loop match decision (mldsa_params[i].level == level) + * both T (valid level found) and F (no match -> NOT_COMPILED_IN). + * ------------------------------------------------------------------ */ +static void wb_get_params(void) +{ + const wc_MlDsaParams* p = NULL; + int ret; + + /* match arm: level 2 (ML-DSA-44) is present -> loop body T at least once */ + ret = mldsa_get_params(WC_ML_DSA_44, &p); + if ((ret != 0) || (p == NULL)) { + WB_NOTE("mldsa_get_params(valid) unexpected"); + } + /* no-match arm: bogus level -> every iteration F -> NOT_COMPILED_IN */ + p = NULL; + ret = mldsa_get_params(0x7f, &p); + if (ret != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) { + WB_NOTE("mldsa_get_params(invalid) unexpected"); + } + WB_OK("mldsa_get_params match/no-match pair exercised"); +} + +/* ------------------------------------------------------------------ * + * mldsa_check_low / mldsa_vec_check_low_c: the compound + * (a[j] <= nhi) || (a[j] >= hi) + * Drive independence pairs: both F (in range), left T (a<=nhi), + * right T with left F (a>=hi). Plus the vector-level (ret==1)&&(i ret 1. */ + for (j = 0; j < MLDSA_N; j++) { + a[j] = 0; + } + ret = mldsa_check_low(a, hi); + if (ret != 1) { + WB_NOTE("mldsa_check_low(in-range) expected 1"); + } + + /* Left operand TRUE (a[j] <= -hi), right FALSE -> ret 0. */ + a[5] = -hi; /* -hi <= -hi is true, -hi >= hi is false */ + ret = mldsa_check_low(a, hi); + if (ret != 0) { + WB_NOTE("mldsa_check_low(<=nhi) expected 0"); + } + + /* Right operand TRUE (a[j] >= hi), left FALSE -> ret 0. */ + a[5] = 0; + a[7] = hi; /* hi >= hi true, hi <= -hi false */ + ret = mldsa_check_low(a, hi); + if (ret != 0) { + WB_NOTE("mldsa_check_low(>=hi) expected 0"); + } + + /* Vector level: two polynomials, both in range -> (ret==1)&&(i early ret 0. */ + for (j = 0; j < 2 * MLDSA_N; j++) { + a[j] = 0; + } + ret = mldsa_vec_check_low_c(a, 2, hi); + if (ret != 1) { + WB_NOTE("mldsa_vec_check_low_c(in-range,l=2) expected 1"); + } + a[3] = hi; /* first polynomial out of range */ + ret = mldsa_vec_check_low_c(a, 2, hi); + if (ret != 0) { + WB_NOTE("mldsa_vec_check_low_c(out) expected 0"); + } + WB_OK("mldsa_check_low / vec_check_low_c operand pairs exercised"); +} +#endif + +/* ------------------------------------------------------------------ * + * mldsa_check_hint: two inner loop decisions that the 3-outcome test + * above never reaches because their FALSE/TRUE pair only shows up with + * carefully overlapping polynomial hint-count bytes (not just a simple + * valid/invalid signature encoding): + * + * do { o++; } while ((o < k) && (i == h[omega + o])); [line ~5363] + * Needs the (o < k) operand held TRUE while (i == h[omega+o]) is + * shown both TRUE (poly o+1 shares the same running count) and + * FALSE (poly o+2 has a different count) -- a single call with 3 + * polynomials sharing the count at the first match and diverging at + * the next index demonstrates both. + * + * while ((o < k) && (i == h[omega + o])) { o++; } [line ~5377] + * The "consume trailing same-count polynomials" loop entered only + * when the outer for-loop is exhausted by i reaching omega with o + * stalled below k. Since mldsa_check_hint does not itself validate + * that hint-count bytes are monotonic/bounded, arbitrary trailing + * byte values let us show the (i == h[omega+o]) operand both TRUE + * and FALSE while (o < k) stays TRUE. + * ------------------------------------------------------------------ */ +#ifndef WOLFSSL_MLDSA_NO_VERIFY +static void wb_check_hint_inner_loops(void) +{ + byte h[32]; + int ret; + + /* Line ~5363 do-while: k=3, omega=8. Hint values h[0..7] strictly + * increasing so the "else if (h[i-1] >= h[i])" arm never fires. + * Counts: poly0=poly1=3 (do-while continues: TRUE), poly2=5 + * (do-while stops: FALSE), both checked while (o < k) is TRUE. */ + XMEMSET(h, 0, sizeof(h)); + h[0] = 0; h[1] = 1; h[2] = 2; h[3] = 3; h[4] = 4; + h[5] = 0; h[6] = 0; h[7] = 0; /* tail hint bytes unused, zero */ + h[8] = 3; h[9] = 3; h[10] = 5; + ret = mldsa_check_hint(h, 3, 8); + if (ret != 0) { + WB_NOTE("mldsa_check_hint(inner do-while pair) expected 0"); + } + + /* Line ~5377 while: k=2, omega=8. Hint values 0..7 strictly + * increasing (no false-positive match, no else-if error), so the + * outer for-loop exhausts via i==omega with o stalled at 0. The + * trailing counts h[8]=8 (matches i==omega -> TRUE, o advances to 1) + * then h[9]=3 (does not match i==omega -> FALSE, o stays at 1), + * both observed while (o < k) is TRUE. o!=k at the end -> a hint + * count was left unconsumed -> SIG_VERIFY_E. */ + XMEMSET(h, 0, sizeof(h)); + h[0] = 0; h[1] = 1; h[2] = 2; h[3] = 3; h[4] = 4; h[5] = 5; h[6] = 6; + h[7] = 7; + h[8] = 8; h[9] = 3; + ret = mldsa_check_hint(h, 2, 8); + if (ret != WC_NO_ERR_TRACE(SIG_VERIFY_E)) { + WB_NOTE("mldsa_check_hint(inner while pair) expected SIG_VERIFY_E"); + } + WB_OK("mldsa_check_hint inner do-while/while operand pairs exercised"); +} +#endif /* !WOLFSSL_MLDSA_NO_VERIFY */ + +/* ------------------------------------------------------------------ * + * mldsa_make_hint_88 / _32 / mldsa_make_hint: the 3-way compound + * (s>LOW) || (s<-LOW) || ((s==-LOW) && (w1!=0)) + * and the too-many-hints guard (idx>OMEGA -> -1), plus mldsa_make_hint's + * gamma2 dispatch (88 arm / 32 arm / neither). + * ------------------------------------------------------------------ */ +#ifndef WOLFSSL_MLDSA_NO_SIGN +#ifndef WOLFSSL_NO_ML_DSA_44 +static void wb_make_hint_88(void) +{ + sword32 s[MLDSA_N]; + sword32 w1[MLDSA_N]; + byte h[256]; + byte idx; + unsigned int j; + int ret; + const sword32 low = (sword32)MLDSA_Q_LOW_88; + + for (j = 0; j < MLDSA_N; j++) { + s[j] = 0; + w1[j] = 0; + } + + /* All three operands FALSE for every coefficient -> no hint, idx stays 0. */ + idx = 0; + ret = mldsa_make_hint_88(s, w1, h, &idx); + if ((ret != 0) || (idx != 0)) { + WB_NOTE("mldsa_make_hint_88(no-hint) unexpected"); + } + + /* First operand TRUE (s > LOW). */ + idx = 0; + s[1] = low + 1; + ret = mldsa_make_hint_88(s, w1, h, &idx); + if ((ret != 0) || (idx != 1)) { + WB_NOTE("mldsa_make_hint_88(s>LOW) unexpected"); + } + + /* Second operand TRUE (s < -LOW), first FALSE. */ + idx = 0; + s[1] = 0; + s[2] = -low - 1; + ret = mldsa_make_hint_88(s, w1, h, &idx); + if ((ret != 0) || (idx != 1)) { + WB_NOTE("mldsa_make_hint_88(s<-LOW) unexpected"); + } + + /* Third operand: (s == -LOW) && (w1 != 0) -> TRUE (drives w1!=0 T). */ + idx = 0; + s[2] = 0; + s[3] = -low; + w1[3] = 1; + ret = mldsa_make_hint_88(s, w1, h, &idx); + if ((ret != 0) || (idx != 1)) { + WB_NOTE("mldsa_make_hint_88(s==-LOW,w1!=0) unexpected"); + } + + /* Third operand right half FALSE: (s == -LOW) && (w1 == 0) -> no hint + * (independence of the w1!=0 operand). */ + idx = 0; + w1[3] = 0; + ret = mldsa_make_hint_88(s, w1, h, &idx); + if ((ret != 0) || (idx != 0)) { + WB_NOTE("mldsa_make_hint_88(s==-LOW,w1==0) unexpected"); + } + + /* Too-many-hints: every coefficient qualifies -> idx crosses OMEGA -> -1. */ + idx = 0; + for (j = 0; j < MLDSA_N; j++) { + s[j] = low + 1; + w1[j] = 0; + } + ret = mldsa_make_hint_88(s, w1, h, &idx); + if (ret != -1) { + WB_NOTE("mldsa_make_hint_88(too-many) expected -1"); + } + WB_OK("mldsa_make_hint_88 operand + overflow pairs exercised"); +} +#endif /* !WOLFSSL_NO_ML_DSA_44 */ + +#if !defined(WOLFSSL_NO_ML_DSA_65) || !defined(WOLFSSL_NO_ML_DSA_87) +static void wb_make_hint_32(void) +{ + sword32 s[MLDSA_N]; + sword32 w1[MLDSA_N]; + byte h[256]; + byte idx; + unsigned int j; + int ret; + const sword32 low = (sword32)MLDSA_Q_LOW_32; + const byte omega = 55; /* ML-DSA-65 OMEGA */ + + for (j = 0; j < MLDSA_N; j++) { + s[j] = 0; + w1[j] = 0; + } + + /* No hint. */ + idx = 0; + ret = mldsa_make_hint_32(s, w1, omega, h, &idx); + if ((ret != 0) || (idx != 0)) { + WB_NOTE("mldsa_make_hint_32(no-hint) unexpected"); + } + + /* s > LOW. */ + idx = 0; + s[1] = low + 1; + ret = mldsa_make_hint_32(s, w1, omega, h, &idx); + if ((ret != 0) || (idx != 1)) { + WB_NOTE("mldsa_make_hint_32(s>LOW) unexpected"); + } + + /* s < -LOW. */ + idx = 0; + s[1] = 0; + s[2] = -low - 1; + ret = mldsa_make_hint_32(s, w1, omega, h, &idx); + if ((ret != 0) || (idx != 1)) { + WB_NOTE("mldsa_make_hint_32(s<-LOW) unexpected"); + } + + /* (s == -LOW) && (w1 != 0). */ + idx = 0; + s[2] = 0; + s[3] = -low; + w1[3] = 1; + ret = mldsa_make_hint_32(s, w1, omega, h, &idx); + if ((ret != 0) || (idx != 1)) { + WB_NOTE("mldsa_make_hint_32(s==-LOW,w1!=0) unexpected"); + } + + /* (s == -LOW) && (w1 == 0) -> no hint. */ + idx = 0; + w1[3] = 0; + ret = mldsa_make_hint_32(s, w1, omega, h, &idx); + if ((ret != 0) || (idx != 0)) { + WB_NOTE("mldsa_make_hint_32(s==-LOW,w1==0) unexpected"); + } + + /* Too many: idx crosses omega -> -1. */ + idx = 0; + for (j = 0; j < MLDSA_N; j++) { + s[j] = low + 1; + } + ret = mldsa_make_hint_32(s, w1, omega, h, &idx); + if (ret != -1) { + WB_NOTE("mldsa_make_hint_32(too-many) expected -1"); + } + WB_OK("mldsa_make_hint_32 operand + overflow pairs exercised"); +} +#endif /* ML_DSA_65 || ML_DSA_87 */ + +#ifndef WOLFSSL_MLDSA_SIGN_SMALL_MEM +static void wb_make_hint_dispatch(void) +{ + sword32 s[PARAMS_ML_DSA_44_K * MLDSA_N]; + sword32 w1[PARAMS_ML_DSA_44_K * MLDSA_N]; + byte h[512]; + unsigned int j; + int ret; + + for (j = 0; j < PARAMS_ML_DSA_44_K * MLDSA_N; j++) { + s[j] = 0; + w1[j] = 0; + } + XMEMSET(h, 0, sizeof(h)); + +#ifndef WOLFSSL_NO_ML_DSA_44 + /* gamma2 == MLDSA_Q_LOW_88 arm. */ + ret = mldsa_make_hint(s, w1, PARAMS_ML_DSA_44_K, MLDSA_Q_LOW_88, + PARAMS_ML_DSA_44_OMEGA, h); + if (ret < 0) { + WB_NOTE("mldsa_make_hint(88 arm) unexpected"); + } +#endif +#if !defined(WOLFSSL_NO_ML_DSA_65) || !defined(WOLFSSL_NO_ML_DSA_87) + /* gamma2 == MLDSA_Q_LOW_32 arm. */ + XMEMSET(h, 0, sizeof(h)); + ret = mldsa_make_hint(s, w1, 4, MLDSA_Q_LOW_32, 55, h); + if (ret < 0) { + WB_NOTE("mldsa_make_hint(32 arm) unexpected"); + } +#endif + /* Neither arm: gamma2 matches no known low modulus -> empty else. Use a + * small omega so the trailing XMEMSET(h+idx, 0, omega-idx) stays in + * bounds (idx==0 here). */ + XMEMSET(h, 0, sizeof(h)); + ret = mldsa_make_hint(s, w1, 1, 12345, 1, h); + if (ret != 0) { + WB_NOTE("mldsa_make_hint(neither arm) expected 0"); + } + WB_OK("mldsa_make_hint gamma2 dispatch arms exercised"); + (void)ret; +} +#endif /* !WOLFSSL_MLDSA_SIGN_SMALL_MEM */ +#endif /* !WOLFSSL_MLDSA_NO_SIGN */ + +/* ------------------------------------------------------------------ * + * mldsa_check_hint (verify path): valid encoding (ret 0), a non-increasing + * index pair (h[i-1] >= h[i] -> SIG_VERIFY_E), and a non-zero trailing hint + * (h[i] != 0 -> SIG_VERIFY_E). + * ------------------------------------------------------------------ */ +#ifndef WOLFSSL_MLDSA_NO_VERIFY +static void wb_check_hint(void) +{ + byte h[256]; + const byte k = 2; + const byte omega = 8; + int ret; + + /* Valid: polynomial 0 has hints at indices {0,1} (strictly increasing), + * count for poly0 = 2, count for poly1 = 2 (no more hints). */ + XMEMSET(h, 0, sizeof(h)); + h[0] = 0; + h[1] = 1; + h[omega + 0] = 2; /* poly 0 uses hints 0..1 */ + h[omega + 1] = 2; /* poly 1 adds none */ + ret = mldsa_check_hint(h, k, omega); + if (ret != 0) { + WB_NOTE("mldsa_check_hint(valid) expected 0"); + } + + /* Non-increasing hint values within a polynomial -> SIG_VERIFY_E. */ + XMEMSET(h, 0, sizeof(h)); + h[0] = 5; + h[1] = 5; /* h[0] >= h[1] */ + h[omega + 0] = 2; + h[omega + 1] = 2; + ret = mldsa_check_hint(h, k, omega); + if (ret != WC_NO_ERR_TRACE(SIG_VERIFY_E)) { + WB_NOTE("mldsa_check_hint(non-increasing) expected SIG_VERIFY_E"); + } + + /* Trailing non-zero hint beyond used counts -> SIG_VERIFY_E. */ + XMEMSET(h, 0, sizeof(h)); + h[omega + 0] = 0; + h[omega + 1] = 0; /* no polynomial claims any hint */ + h[3] = 7; /* but a stray non-zero hint remains */ + ret = mldsa_check_hint(h, k, omega); + if (ret != WC_NO_ERR_TRACE(SIG_VERIFY_E)) { + WB_NOTE("mldsa_check_hint(stray) expected SIG_VERIFY_E"); + } + WB_OK("mldsa_check_hint valid/invalid decisions exercised"); +} +#endif /* !WOLFSSL_MLDSA_NO_VERIFY */ + +/* ------------------------------------------------------------------ * + * mldsa_check_eta_range: eta==MLDSA_ETA_4 arm (two nibble operands, in/out + * of range) and the eta!=4 (ETA_2) else arm (3-bit groups, in/out of range). + * ------------------------------------------------------------------ */ +#ifdef WOLFSSL_MLDSA_PRIVATE_KEY +static void wb_check_eta_range(void) +{ + byte p[12]; + int ret; + + /* eta == 4 arm, all in range: nibbles <= 2*eta == 8. */ + XMEMSET(p, 0, sizeof(p)); + ret = mldsa_check_eta_range(p, MLDSA_ETA_4, sizeof(p)); + if (ret != 0) { + WB_NOTE("mldsa_check_eta_range(eta4,in) expected 0"); + } + + /* eta == 4 arm, low nibble out of range: (p&0xf) > max. */ + XMEMSET(p, 0, sizeof(p)); + p[0] = 0x0f; /* 15 > 8 */ + ret = mldsa_check_eta_range(p, MLDSA_ETA_4, sizeof(p)); + if (ret != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + WB_NOTE("mldsa_check_eta_range(eta4,low-hi) expected PUBLIC_KEY_E"); + } + + /* eta == 4 arm, high nibble out of range: (p>>4) > max, low in range. */ + XMEMSET(p, 0, sizeof(p)); + p[0] = 0xf0; /* high nibble 15 > 8, low nibble 0 */ + ret = mldsa_check_eta_range(p, MLDSA_ETA_4, sizeof(p)); + if (ret != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + WB_NOTE("mldsa_check_eta_range(eta4,high-hi) expected PUBLIC_KEY_E"); + } + + /* eta != 4 (ETA_2) else arm, all in range: 3-bit groups <= 2*eta == 4. */ + XMEMSET(p, 0, sizeof(p)); + ret = mldsa_check_eta_range(p, MLDSA_ETA_2, sizeof(p)); + if (ret != 0) { + WB_NOTE("mldsa_check_eta_range(eta2,in) expected 0"); + } + + /* eta != 4 else arm, a 3-bit group out of range (value 7 > 4). */ + XMEMSET(p, 0, sizeof(p)); + p[0] = 0x07; /* first 3-bit group == 7 > 4 */ + ret = mldsa_check_eta_range(p, MLDSA_ETA_2, sizeof(p)); + if (ret != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) { + WB_NOTE("mldsa_check_eta_range(eta2,hi) expected PUBLIC_KEY_E"); + } + WB_OK("mldsa_check_eta_range eta4/eta2 in/out pairs exercised"); +} +#endif /* WOLFSSL_MLDSA_PRIVATE_KEY */ + +/* ------------------------------------------------------------------ * + * ASN.1 fallback helpers. These file-static parsers are ONLY compiled when + * WOLFSSL_MLDSA_NO_ASN1 is defined (the hand-rolled DER parser used in place + * of the wolfSSL ASN template engine); with the template engine on they do + * not exist. Reached only by the mldsa_no_asn1 variant's white-box. + * mldsa_get_der_length: the full short/long-form length cascade. + * mldsa_check_type: idx>=inSz / tag mismatch / ok. + * mldsa_oid_to_level: matching OID / no match. + * ------------------------------------------------------------------ */ +#ifdef WOLFSSL_MLDSA_NO_ASN1 +static void wb_der_length(void) +{ + byte in[8]; + word32 idx; + int len; + int ret; + + /* idx >= inSz -> ASN_PARSE_E. */ + idx = 4; + ret = mldsa_get_der_length(in, &idx, &len, 4); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("get_der_length(idx>=inSz) expected ASN_PARSE_E"); + } + + /* short form: input[idx] < 0x80. */ + idx = 0; in[0] = 0x05; + ret = mldsa_get_der_length(in, &idx, &len, sizeof(in)); + if ((ret != 0) || (len != 5) || (idx != 1)) { + WB_NOTE("get_der_length(short) unexpected"); + } + + /* input[idx] == 0x80 -> ASN_PARSE_E. */ + idx = 0; in[0] = 0x80; + ret = mldsa_get_der_length(in, &idx, &len, sizeof(in)); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("get_der_length(0x80) expected ASN_PARSE_E"); + } + + /* input[idx] >= 0x83 -> ASN_PARSE_E (same else-if operand, other half). */ + idx = 0; in[0] = 0x83; + ret = mldsa_get_der_length(in, &idx, &len, sizeof(in)); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("get_der_length(0x83) expected ASN_PARSE_E"); + } + + /* 0x81 form, truncated (idx+1 >= inSz) -> ASN_PARSE_E. */ + idx = 0; in[0] = 0x81; + ret = mldsa_get_der_length(in, &idx, &len, 1); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("get_der_length(0x81 trunc) expected ASN_PARSE_E"); + } + + /* 0x81 form, second byte < 0x80 (non-canonical) -> ASN_PARSE_E. */ + idx = 0; in[0] = 0x81; in[1] = 0x7f; + ret = mldsa_get_der_length(in, &idx, &len, sizeof(in)); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("get_der_length(0x81 noncanon) expected ASN_PARSE_E"); + } + + /* 0x81 form, valid: second byte >= 0x80. Provide enough buffer. */ + { + byte big[200]; + XMEMSET(big, 0, sizeof(big)); + idx = 0; big[0] = 0x81; big[1] = 0x80; /* len 128 */ + ret = mldsa_get_der_length(big, &idx, &len, sizeof(big)); + if ((ret != 0) || (len != 0x80) || (idx != 2)) { + WB_NOTE("get_der_length(0x81 valid) unexpected"); + } + + /* 0x82 form, truncated (idx+2 >= inSz) -> ASN_PARSE_E. */ + idx = 0; big[0] = 0x82; + ret = mldsa_get_der_length(big, &idx, &len, 2); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("get_der_length(0x82 trunc) expected ASN_PARSE_E"); + } + + /* 0x82 form, len < 0x100 (non-canonical) -> ASN_PARSE_E. */ + idx = 0; big[0] = 0x82; big[1] = 0x00; big[2] = 0x10; + ret = mldsa_get_der_length(big, &idx, &len, sizeof(big)); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("get_der_length(0x82 noncanon) expected ASN_PARSE_E"); + } + + /* 0x82 form, valid len 0x100 but (idx+len) > inSz -> final guard. */ + idx = 0; big[0] = 0x82; big[1] = 0x01; big[2] = 0x00; /* len 256 */ + ret = mldsa_get_der_length(big, &idx, &len, sizeof(big)); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("get_der_length(0x82 overrun) expected ASN_PARSE_E"); + } + + /* 0x82 form, valid and fits: len 0x100 with a large enough inSz. */ + idx = 0; big[0] = 0x82; big[1] = 0x01; big[2] = 0x00; + ret = mldsa_get_der_length(big, &idx, &len, 0x100 + 3); + if ((ret != 0) || (len != 0x100) || (idx != 3)) { + WB_NOTE("get_der_length(0x82 valid) unexpected"); + } + } + WB_OK("mldsa_get_der_length cascade exercised"); +} + +static void wb_check_type(void) +{ + byte in[4]; + word32 idx; + int ret; + + /* idx >= inSz -> ASN_PARSE_E. */ + idx = 2; + ret = mldsa_check_type(in, &idx, 0x30, 2); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("check_type(idx>=inSz) expected ASN_PARSE_E"); + } + + /* tag mismatch -> ASN_PARSE_E. */ + idx = 0; in[0] = 0x31; + ret = mldsa_check_type(in, &idx, 0x30, sizeof(in)); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("check_type(mismatch) expected ASN_PARSE_E"); + } + + /* match -> 0, idx advanced. */ + idx = 0; in[0] = 0x30; + ret = mldsa_check_type(in, &idx, 0x30, sizeof(in)); + if ((ret != 0) || (idx != 1)) { + WB_NOTE("check_type(match) unexpected"); + } + WB_OK("mldsa_check_type decisions exercised"); +} + +static void wb_oid_to_level(void) +{ + byte level = 0; + int ret; + +#ifndef WOLFSSL_NO_ML_DSA_44 + /* Matching OID (ML-DSA-44, non-draft) -> level set, ret 0. + * Also exercises the 65/87 chain arms' (ret != 0) operand as TRUE + * (44 didn't match anything before this call in a fresh call, but + * here 44 matches immediately so the 65/87 "ret != 0" checks below + * see ret==0 and short-circuit to FALSE). */ + ret = mldsa_oid_to_level(ml_dsa_oid_44, (word32)sizeof(ml_dsa_oid_44), + &level); + if ((ret != 0) || (level != WC_ML_DSA_44)) { + WB_NOTE("oid_to_level(44) unexpected"); + } +#endif +#ifndef WOLFSSL_NO_ML_DSA_65 + /* Matching OID (ML-DSA-65): 44's arm (ret != 0) is TRUE (no match + * yet), 65's arm matches (ret != 0 TRUE, oidLen== TRUE, memcmp==0 + * TRUE), then 87's arm sees (ret != 0) FALSE (already matched). */ + level = 0; + ret = mldsa_oid_to_level(ml_dsa_oid_65, (word32)sizeof(ml_dsa_oid_65), + &level); + if ((ret != 0) || (level != WC_ML_DSA_65)) { + WB_NOTE("oid_to_level(65) unexpected"); + } +#endif +#ifndef WOLFSSL_NO_ML_DSA_87 + /* Matching OID (ML-DSA-87): 44/65 arms' (ret != 0) stay TRUE (no + * match), 87's arm matches. */ + level = 0; + ret = mldsa_oid_to_level(ml_dsa_oid_87, (word32)sizeof(ml_dsa_oid_87), + &level); + if ((ret != 0) || (level != WC_ML_DSA_87)) { + WB_NOTE("oid_to_level(87) unexpected"); + } +#endif + + /* No match, same length: length matches every known OID (all 9 + * bytes) but bytes differ from all of them -> ASN_PARSE_E. Gives + * (oidLen == sizeof(...)) TRUE and memcmp()==0 FALSE for every arm + * in one call. */ + { + byte bogus[9]; + XMEMSET(bogus, 0xAA, sizeof(bogus)); + level = 0; + ret = mldsa_oid_to_level(bogus, (word32)sizeof(bogus), &level); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("oid_to_level(bogus) expected ASN_PARSE_E"); + } + } + + /* No match, different length: (oidLen == sizeof(...)) FALSE for + * every arm (independence of the oidLen operand from the memcmp + * one) -> falls through every arm untouched -> ASN_PARSE_E. */ + { + byte shortOid[3] = { 0xAA, 0xAA, 0xAA }; + level = 0; + ret = mldsa_oid_to_level(shortOid, (word32)sizeof(shortOid), &level); + if (ret != WC_NO_ERR_TRACE(ASN_PARSE_E)) { + WB_NOTE("oid_to_level(short) expected ASN_PARSE_E"); + } + } + WB_OK("mldsa_oid_to_level match/no-match/length pairs exercised"); +} +#endif /* WOLFSSL_MLDSA_NO_ASN1 */ + +#endif /* WOLFSSL_HAVE_MLDSA */ + +int main(void) +{ + printf("wc_mldsa.c white-box MC/DC supplement\n"); +#if !defined(WOLFSSL_HAVE_MLDSA) + printf(" ML-DSA not enabled; nothing to exercise\n"); + return 0; +#else + wb_get_params(); +#if !defined(WOLFSSL_MLDSA_NO_SIGN) || !defined(WOLFSSL_MLDSA_NO_VERIFY) + wb_check_low(); +#endif +#ifndef WOLFSSL_MLDSA_NO_SIGN +#ifndef WOLFSSL_NO_ML_DSA_44 + wb_make_hint_88(); +#endif +#if !defined(WOLFSSL_NO_ML_DSA_65) || !defined(WOLFSSL_NO_ML_DSA_87) + wb_make_hint_32(); +#endif +#ifndef WOLFSSL_MLDSA_SIGN_SMALL_MEM + wb_make_hint_dispatch(); +#endif +#endif /* !WOLFSSL_MLDSA_NO_SIGN */ +#ifndef WOLFSSL_MLDSA_NO_VERIFY + wb_check_hint(); + wb_check_hint_inner_loops(); +#endif +#ifdef WOLFSSL_MLDSA_PRIVATE_KEY + wb_check_eta_range(); +#endif +#ifdef WOLFSSL_MLDSA_NO_ASN1 + wb_der_length(); + wb_check_type(); + wb_oid_to_level(); +#endif + 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 new file mode 100644 index 00000000000..d0b0ee0690c --- /dev/null +++ b/tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c @@ -0,0 +1,276 @@ +/* test_wc_mlkem_poly_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 + */ + +/* White-box MC/DC supplement for wolfcrypt/src/wc_mlkem_poly.c. + * + * wc_mlkem_poly.c holds ML-KEM's polynomial arithmetic core. Several file-static + * helpers own decision independence pairs that the public wc_MlKemKey_* API + * cannot exhibit cleanly, because every public caller feeds them only the + * "valid" operand combination: + * + * - mlkem_cmp_c (constant-time byte compare): the API's re-encryption check + * drives it, but only the "equal" path deterministically; the returned + * 0/-1 mask's independence needs BOTH an all-equal and a differing buffer + * in one binary. + * - mlkem_rej_uniform_c (rejection sampling of 12-bit values): the "v < q" + * accept and ">= q" reject branches, plus the "i < len" early-stop guard, + * need inputs crafted to hit both sides -- random matrix seeds almost never + * produce a full run of rejections. + * - mlkem_ntt / mlkem_invntt / mlkem_csubq_c: exercised per-variant so each of + * the four code-size arms (default / WOLFSSL_MLKEM_SMALL / + * WOLFSSL_MLKEM_NO_LARGE_CODE / WOLFSSL_MLKEM_NTT_UNROLL) gets its reduction + * and butterfly loops driven when the campaign rebuilds this TU per arm. + * + * This TU #includes wc_mlkem_poly.c so those static helpers are in scope, then calls + * each with both halves of every targeted pair on tiny fixed-size buffers. + * Memory-safe by construction (all buffers are MLKEM_N sword16 / bounded byte + * arrays); prints skips and returns 0 on any unexpected result so the campaign + * keeps the variant. + */ + +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_ARMASM) + +/* mlkem_cmp_c: drive both the all-equal (returns 0) and differing (returns -1) + * results so the constant-time mask expression shows its independence pair. */ +static void wb_cmp(void) +{ + byte a[64]; + byte b[64]; + unsigned int i; + + for (i = 0; i < sizeof(a); i++) { + a[i] = (byte)i; + b[i] = (byte)i; + } + if (mlkem_cmp_c(a, b, (int)sizeof(a)) != 0) { + WB_NOTE("mlkem_cmp_c equal buffers did not return 0"); + wb_fail = 1; + } + /* Flip a single byte: differing path. */ + b[17] ^= 0x80; + if (mlkem_cmp_c(a, b, (int)sizeof(a)) == 0) { + WB_NOTE("mlkem_cmp_c differing buffers returned 0"); + wb_fail = 1; + } +} + +/* mlkem_rej_uniform_c: craft a random-byte buffer whose 12-bit little-endian + * fields include values BOTH below q (accepted) and >= q (rejected), and pass a + * len smaller than the number of acceptable samples so the "i < len" early stop + * fires while candidates remain -- covering both sides of the accept and + * early-stop decisions. */ +static void wb_rej_uniform(void) +{ + /* Each 3 bytes yields two 12-bit integers v0,v1. + * 0x00,0x00 -> v0 = 0x000 (accept, < q) + * ...,0xFF pattern -> 0xFFF = 4095 (>= q, reject) + * Interleave so the sampler sees accepts and rejects. */ + byte r[96]; + sword16 p[MLKEM_N]; + unsigned int n; + unsigned int i; + + for (i = 0; i < sizeof(r); i += 3) { + /* v0 low, v1 high. Alternate accept/reject blocks. */ + if ((i / 3) & 1) { + r[i + 0] = 0xFF; r[i + 1] = 0xFF; r[i + 2] = 0xFF; /* both >= q */ + } + else { + r[i + 0] = 0x01; r[i + 1] = 0x00; r[i + 2] = 0x00; /* both < q */ + } + } + XMEMSET(p, 0, sizeof(p)); + + /* Full length: exercises accept + reject with room to store accepts. */ + n = mlkem_rej_uniform_c(p, MLKEM_N, r, (unsigned int)sizeof(r)); + if (n > (unsigned int)MLKEM_N) { + WB_NOTE("mlkem_rej_uniform_c over-produced"); + wb_fail = 1; + } + /* Tiny len: the (i < len) guard stops early while bytes remain. */ + (void)mlkem_rej_uniform_c(p, 1, r, (unsigned int)sizeof(r)); +} + +/* mlkem_ntt / mlkem_invntt / mlkem_csubq_c: run the transform pipeline for + * whichever code-size arm this TU was compiled with. */ +static void wb_transform(void) +{ + sword16 poly[MLKEM_N]; + unsigned int i; + + for (i = 0; i < MLKEM_N; i++) { + poly[i] = (sword16)((i * 7) % MLKEM_Q); + } + mlkem_ntt(poly); + mlkem_invntt(poly); + mlkem_csubq_c(poly); +} + +#endif /* WOLFSSL_HAVE_MLKEM && !WOLFSSL_ARMASM */ + +/* ------------------------------------------------------------------------- * + * Additional file-static gap drivers (merged from the former _gap TU). + * + * Residual classes left untouched here (see the gap-closing REPORT.md for the + * full accounting): + * - IS_INTEL_AVX2(cpuid_flags) && (SAVE_VECTOR_REGISTERS2() == 0): + * cpuid-dispatch, host-always-AVX2 residual (same class as every other + * module's intel-dispatch skip). + * - The USE_INTEL_SPEEDUP AVX2 rejection-sampling while-loops: USE_INTEL_SPEEDUP + * is OFF by default and only compiled with the separate `--enable-intelasm` + * axis, which this campaign build does not use. + * - `(ret == 0) && ...` chain guards in mlkem_gen_matrix_c/_i and + * mlkem_get_noise_c: ret can only go non-zero via a mid-chain PRF/hash + * failure, which is not selectable without corrupting library state. + * - mlkem_hash512()'s data2 checks: the data2==NULL side only occurs on the + * WOLFSSL_MLKEM_KYBER (original Kyber) call path, a separate build axis. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_MLKEM) && \ + !(defined(WOLFSSL_ARMASM) && defined(__aarch64__)) + +/* ------------------------------------------------------------------------- * + * mlkem_rej_uniform_c(): j < rLen independence pair. + * + * for (; (i + 4 < len) && (j < rLen); j += 6) { ... } + * + * All-0xFF random bytes decode to four 12-bit values of 0xFFF (4095) per + * 6-byte block - always >= MLKEM_Q (3329), so every candidate is rejected + * and "i" never advances past 0. With len == 8, "i + 4 < len" (4 < 8) + * stays true for the whole call, isolating j < rLen: the loop runs at + * least twice while data remains (j < rLen true) then stops the instant + * j reaches rLen (j < rLen false), with i + 4 < len unchanged throughout. + * ------------------------------------------------------------------------- */ +static void wb_rej_uniform_c_rlen_exhaust(void) +{ + sword16 p[16]; + byte r[40]; + unsigned int len = 8; + unsigned int rLen = 24; + unsigned int got; + + XMEMSET(p, 0, sizeof(p)); + /* All-ones: every decoded 12-bit sample is rejected (>= MLKEM_Q). */ + XMEMSET(r, 0xFF, sizeof(r)); + + got = mlkem_rej_uniform_c(p, len, r, rLen); + if (got != 0) { + WB_NOTE("mlkem_rej_uniform_c: expected 0 accepted samples from an" + " all-rejected buffer"); + wb_fail = 1; + } + + WB_NOTE("mlkem_rej_uniform_c j 32 drives the 64-bit tree-index runtime + * path. Negative verifies flip the mismatch decision false-sides. + * + * MC/DC is per-binary, so both sides of each targeted decision are driven in + * this one instrumented binary. Crash-safety: every key is XMEMSET to zero + * before use and freed after; the in-memory secret-key scratch buffer is + * sized for the tall parameter set and roundtrips that would exceed it are + * skipped cleanly. + */ + +#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) + +/* In-memory secret-key persistence for the whitebox roundtrip. Sized for the + * tall XMSS^MT parameter set. */ +static byte wb_priv[262144]; +static word32 wb_privSz = 0; + +static enum wc_XmssRc wb_write_key(const byte* priv, word32 privSz, + void* context) +{ + (void)context; + if (privSz > (word32)sizeof(wb_priv)) + return WC_XMSS_RC_WRITE_FAIL; + XMEMCPY(wb_priv, priv, privSz); + wb_privSz = privSz; + return WC_XMSS_RC_SAVED_TO_NV_MEMORY; +} + +static enum wc_XmssRc wb_read_key(byte* priv, word32 privSz, void* context) +{ + (void)context; + if (privSz != wb_privSz) + return WC_XMSS_RC_READ_FAIL; + XMEMCPY(priv, wb_priv, privSz); + return WC_XMSS_RC_READ_TO_MEMORY; +} + +/* Full keygen + multi-sign + verify (+ negative verify) for one parameter set, + * flowing through the WOTS+/L-tree/BDS static helpers in this file. */ +static void wb_param_roundtrip(WC_RNG* rng, const char* paramStr) +{ + XmssKey key; + byte msg[] = "wc_xmss_impl whitebox message"; + byte* sig = NULL; + word32 sigSz; + word32 sigLen = 0; + word32 privLen = 0; + int i; + int ret; + + XMEMSET(&key, 0, sizeof(key)); + wb_privSz = 0; + + ret = wc_XmssKey_Init(&key, NULL, INVALID_DEVID); + if (ret == 0) + ret = wc_XmssKey_SetParamStr(&key, paramStr); + if (ret != 0) { + WB_NOTE(paramStr); + WB_NOTE(" parameter set unavailable; skipped"); + wc_XmssKey_Free(&key); + return; + } + (void)wc_XmssKey_SetWriteCb(&key, wb_write_key); + (void)wc_XmssKey_SetReadCb(&key, wb_read_key); + (void)wc_XmssKey_SetContext(&key, (void*)wb_priv); + + if (wc_XmssKey_GetPrivLen(&key, &privLen) != 0 || + privLen > (word32)sizeof(wb_priv)) { + WB_NOTE(paramStr); + WB_NOTE(" secret key exceeds scratch; skipped"); + wc_XmssKey_Free(&key); + return; + } + + if (wc_XmssKey_MakeKey(&key, rng) != 0) { + WB_NOTE(paramStr); + WB_NOTE(" MakeKey failed"); + wb_fail = 1; + wc_XmssKey_Free(&key); + return; + } + + if (wc_XmssKey_GetSigLen(&key, &sigLen) != 0 || sigLen == 0) { + WB_NOTE("GetSigLen failed"); + wb_fail = 1; + wc_XmssKey_Free(&key); + return; + } + sig = (byte*)XMALLOC(sigLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (sig == NULL) { + WB_NOTE("sig alloc failed; skipped"); + wc_XmssKey_Free(&key); + return; + } + + for (i = 0; i < 2; i++) { + sigSz = sigLen; + if (wc_XmssKey_Sign(&key, sig, &sigSz, msg, (int)sizeof(msg)) != 0) { + WB_NOTE("Sign failed"); + wb_fail = 1; + break; + } + if (wc_XmssKey_Verify(&key, sig, sigSz, msg, (int)sizeof(msg)) != 0) { + WB_NOTE("Verify(valid) failed"); + wb_fail = 1; + break; + } + /* Negative verify: flip a byte -> drives the WOTS+ chain / root + * comparison mismatch false-sides. */ + sig[sigSz - 1] ^= 0x01; + if (wc_XmssKey_Verify(&key, sig, sigSz, msg, (int)sizeof(msg)) == 0) { + WB_NOTE("Verify(tampered) unexpectedly succeeded"); + wb_fail = 1; + break; + } + sig[sigSz - 1] ^= 0x01; + } + + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wc_XmssKey_Free(&key); +} + +static void wb_run(void) +{ + WC_RNG rng; + + XMEMSET(&rng, 0, sizeof(rng)); + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; skipping roundtrips"); + return; + } + + /* Single tree, one per compiled-in hash family. */ +#ifdef WC_XMSS_SHA256 + wb_param_roundtrip(&rng, "XMSS-SHA2_10_256"); +#endif +#ifdef WC_XMSS_SHA512 + wb_param_roundtrip(&rng, "XMSS-SHA2_10_512"); +#endif +#ifdef WC_XMSS_SHAKE128 + wb_param_roundtrip(&rng, "XMSS-SHAKE_10_256"); +#endif +#ifdef WC_XMSS_SHAKE256 + wb_param_roundtrip(&rng, "XMSS-SHAKE256_10_256"); +#endif + + /* 2-layer XMSS^MT: drives the XMSS^MT subtree / BDS helpers. */ +#if defined(WC_XMSS_SHA256) && (WOLFSSL_XMSS_MAX_HEIGHT >= 20) && \ + (!defined(WOLFSSL_XMSS_MIN_HEIGHT) || (WOLFSSL_XMSS_MIN_HEIGHT <= 20)) + wb_param_roundtrip(&rng, "XMSSMT-SHA2_20/2_256"); +#endif + + /* Tall XMSS^MT (height 40 > 32): 64-bit tree-index runtime path. Skipped + * under WOLFSSL_WC_XMSS_SMALL (recompute signing is slow at this height; + * the 64-bit path is unioned from the fast variant). */ +#if defined(WC_XMSS_SHA256) && !defined(WOLFSSL_WC_XMSS_SMALL) && \ + (WOLFSSL_XMSS_MAX_HEIGHT >= 40) && \ + (!defined(WOLFSSL_XMSS_MIN_HEIGHT) || (WOLFSSL_XMSS_MIN_HEIGHT <= 40)) + wb_param_roundtrip(&rng, "XMSSMT-SHA2_40/8_256"); +#endif + + wc_FreeRng(&rng); +} + +#else /* !WOLFSSL_HAVE_XMSS || WOLFSSL_XMSS_VERIFY_ONLY */ + +static void wb_run(void) +{ + WB_NOTE("XMSS signing not compiled in this variant; nothing to exercise"); +} + +#endif + +#ifdef WOLFSSL_HAVE_XMSS + +/******************************************** + * Shared small-parameter / state helpers + ********************************************/ + +/* Hand-build an XmssParams the same way wc_xmss.c's XMSS_PARAMS() macro + * would (that macro itself is not visible here - it lives in wc_xmss.c), + * but with a caller-chosen (deliberately tiny) height/depth so full + * keygen/sign/verify cycles are cheap enough to run repeatedly. bds_k must + * keep (sub_h - bds_k) even/sane for the BDS bookkeeping; 0 is always safe. + */ +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; + /* sk_len: replicate XMSS_SK_LEN(n,h,d,sub_h,idx_len,bds_k)'s formula + * from wc_xmss.c (not visible to this TU). Callers additionally + * over-allocate their sk buffers well beyond this. */ + 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; +} + +/* Initialize an XmssState's digest for the hash family named in params. + * Returns 0 on success, matching wc_xmss_digest_init()'s own contract + * (which is file-static in wc_xmss.c and not reachable from here). */ +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_SHA256 + if (params->hash == WC_HASH_TYPE_SHA256) { + ret = wc_InitSha256(&state->digest.sha256); + } + else +#endif +#ifdef WC_XMSS_SHA512 + if (params->hash == WC_HASH_TYPE_SHA512) { + ret = wc_InitSha512(&state->digest.sha512); + } + else +#endif + { + ret = WC_NO_ERR_TRACE(NOT_COMPILED_IN); + } + + return ret; +} + +static void wb_state_free(XmssState* state) +{ +#ifdef WC_XMSS_SHA256 + if (state->params->hash == WC_HASH_TYPE_SHA256) { + wc_Sha256Free(&state->digest.sha256); + return; + } +#endif +#ifdef WC_XMSS_SHA512 + if (state->params->hash == WC_HASH_TYPE_SHA512) { + wc_Sha512Free(&state->digest.sha512); + return; + } +#endif + (void)state; +} + +/******************************************** + * 614/615, 1033-1035, 1218-1220, 1813-1815, 1884-1886, 1952-1954, + * 2022-2024, 2053-2055: + * "params->n == <32-bit-digest-size>" (held alongside pad_len==32 and/or + * hash==SHA256, whichever the decision requires) selects the SHA-256/ + * 32-byte fast path vs. the fully generic path. Driving the SAME helper + * with n=32 (fast path taken) and n=24 (still SHA-256, still within the + * 192..256-bit partial-digest range, but NOT 32 bytes, so the fast path's + * condition is false and the generic/partial path runs) isolates the "n" + * operand while hash==SHA256 (and pad_len==32, where applicable) stay + * true in both calls. + ********************************************/ +static void wb_hash_family_pairs(void) +{ + XmssParams paramsFull; + XmssParams paramsPartial; + XmssState state; + HashAddress addr; + byte sk_seed[32]; + byte pk_seed[32]; + byte data[64]; + byte hashOut[64]; + byte pkBuf[WC_XMSS_MAX_WOTS_SIG_LEN]; + byte sigBuf[WC_XMSS_MAX_WOTS_SIG_LEN]; + int i; + + wb_params_init(¶msFull, WC_HASH_TYPE_SHA256, 32, 32, 4, 1, 4, 0); + wb_params_init(¶msPartial, WC_HASH_TYPE_SHA256, 24, 32, 4, 1, 4, 0); + + if (wb_state_init(&state, ¶msFull) != 0) { + WB_NOTE("hash family pairs: SHA-256 state init failed; skipped"); + return; + } + + for (i = 0; i < 64; i++) { + data[i] = (byte)(i ^ 0x5a); + } + XMEMSET(sk_seed, 0x77, sizeof(sk_seed)); + XMEMSET(pk_seed, 0x88, sizeof(pk_seed)); + XMEMSET(hashOut, 0, sizeof(hashOut)); + + /* Line 614/615: wc_xmss_hash()'s "params->n == WC_SHA256_DIGEST_SIZE" + * operand. */ + state.ret = 0; + wc_xmss_hash(&state, data, 16, hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_hash n=32 arm failed"); + wb_fail = 1; + } + state.params = ¶msPartial; + state.ret = 0; + wc_xmss_hash(&state, data, 16, hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_hash n=24 arm failed"); + wb_fail = 1; + } + state.params = ¶msFull; + + /* Lines 1033-1035 / 1218-1220: wc_xmss_rand_hash() / + * wc_xmss_rand_hash_lr()'s "params->n == XMSS_SHA256_32_N" operand. */ + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_rand_hash(&state, data, pk_seed, addr, hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_rand_hash n=32 arm failed"); + wb_fail = 1; + } + state.params = ¶msPartial; + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_rand_hash(&state, data, pk_seed, addr, hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_rand_hash n=24 arm failed"); + wb_fail = 1; + } + state.params = ¶msFull; + + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_rand_hash_lr(&state, data, data + 32, pk_seed, addr, hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_rand_hash_lr n=32 arm failed"); + wb_fail = 1; + } + state.params = ¶msPartial; + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_rand_hash_lr(&state, data, data + 24, pk_seed, addr, hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_rand_hash_lr n=24 arm failed"); + wb_fail = 1; + } + state.params = ¶msFull; + +#ifndef WOLFSSL_XMSS_VERIFY_ONLY + /* Lines 1813-1815: wc_xmss_wots_gen_pk(). */ + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + XMEMSET(pkBuf, 0, sizeof(pkBuf)); + wc_xmss_wots_gen_pk(&state, sk_seed, pk_seed, addr, pkBuf); + if (state.ret != 0) { + WB_NOTE("wc_xmss_wots_gen_pk n=32 arm failed"); + wb_fail = 1; + } + state.params = ¶msPartial; + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + XMEMSET(pkBuf, 0, sizeof(pkBuf)); + wc_xmss_wots_gen_pk(&state, sk_seed, pk_seed, addr, pkBuf); + if (state.ret != 0) { + WB_NOTE("wc_xmss_wots_gen_pk n=24 arm failed"); + wb_fail = 1; + } + state.params = ¶msFull; + + /* Lines 1884-1886: wc_xmss_wots_sign(). */ + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + XMEMSET(sigBuf, 0, sizeof(sigBuf)); + wc_xmss_wots_sign(&state, data, sk_seed, pk_seed, addr, sigBuf); + if (state.ret != 0) { + WB_NOTE("wc_xmss_wots_sign n=32 arm failed"); + wb_fail = 1; + } + state.params = ¶msPartial; + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + XMEMSET(sigBuf, 0, sizeof(sigBuf)); + wc_xmss_wots_sign(&state, data, sk_seed, pk_seed, addr, sigBuf); + if (state.ret != 0) { + WB_NOTE("wc_xmss_wots_sign n=24 arm failed"); + wb_fail = 1; + } + state.params = ¶msFull; +#else + WB_NOTE("WOLFSSL_XMSS_VERIFY_ONLY: wots_gen_pk/wots_sign (1813, 1884) " + "not compiled in; skipped"); + XMEMSET(sigBuf, 0, sizeof(sigBuf)); +#endif /* !WOLFSSL_XMSS_VERIFY_ONLY */ + + /* Lines 1952-1954: wc_xmss_wots_pk_from_sig(). sigBuf holds whatever + * the wots_sign call above produced (or zeros, in VERIFY_ONLY builds); + * this decision's independence only needs the call to complete without + * a digest failure, not a cryptographically valid pk. */ + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + XMEMSET(pkBuf, 0, sizeof(pkBuf)); + wc_xmss_wots_pk_from_sig(&state, sigBuf, data, pk_seed, addr, pkBuf); + if (state.ret != 0) { + WB_NOTE("wc_xmss_wots_pk_from_sig n=32 arm failed"); + wb_fail = 1; + } + state.params = ¶msPartial; + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + XMEMSET(pkBuf, 0, sizeof(pkBuf)); + wc_xmss_wots_pk_from_sig(&state, sigBuf, data, pk_seed, addr, pkBuf); + if (state.ret != 0) { + WB_NOTE("wc_xmss_wots_pk_from_sig n=24 arm failed"); + wb_fail = 1; + } + state.params = ¶msFull; + + /* Lines 2022-2024 / 2053-2055: wc_xmss_ltree() - the same decision + * appears twice (once to prime the cached hash state, once inside the + * len-reduction loop); one call exercises both occurrences. pkBuf is + * used purely as WOTS+-shaped scratch input. */ + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_ltree(&state, pkBuf, pk_seed, addr, hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_ltree n=32 arm failed"); + wb_fail = 1; + } + state.params = ¶msPartial; + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_ltree(&state, pkBuf, pk_seed, addr, hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_ltree n=24 arm failed"); + wb_fail = 1; + } + state.params = ¶msFull; + + wb_state_free(&state); + WB_NOTE("hash-family n-operand independence pairs exercised"); +} + +/******************************************** + * 1623, 1697: WOTS+ chain functions' + * "for (i = start+1; i < (start+steps) && i < XMSS_WOTS_W; i++)" + * condIndex 1 ("i < XMSS_WOTS_W"). Calling with start=0, steps well beyond + * XMSS_WOTS_W (16) keeps "i < start+steps" true for the whole loop, so it + * is "i < XMSS_WOTS_W" alone that is true for i=1..15 and false at i=16 - + * both sides of that one operand, shown within this single call. + ********************************************/ +static void wb_wots_chain_loop(void) +{ + /* Line 1623: wc_xmss_chain_sha256_32() - fixed SHA-256/32-byte path. */ + { + XmssParams params; + XmssState state; + ALIGN16 byte addrBuf[WC_XMSS_ADDR_LEN + 8]; + byte data[32]; + byte pkseed[32]; + byte hashOut[32]; + int i; + + wb_params_init(¶ms, WC_HASH_TYPE_SHA256, 32, 32, 4, 1, 4, 0); + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wots chain loop: SHA-256 state init failed; skipped"); + } + else { + for (i = 0; i < 32; i++) { + data[i] = (byte)i; + pkseed[i] = (byte)(0x50 + i); + } + XMEMSET(addrBuf, 0, sizeof(addrBuf)); + state.ret = 0; + wc_xmss_chain_sha256_32(&state, data, 0, 40, pkseed, addrBuf, + hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_chain_sha256_32 start/steps run failed"); + wb_fail = 1; + } + wb_state_free(&state); + } + } + + /* Line 1697: wc_xmss_chain() - generic path. Using SHA-512 (n=64) + * guarantees this is NOT the sha256_32-specific fast path, so the + * generic wc_xmss_chain() implementation is what actually runs. */ +#ifdef WC_XMSS_SHA512 + { + XmssParams params; + XmssState state; + ALIGN16 byte addrBuf[WC_XMSS_ADDR_LEN + 8]; + byte data[64]; + byte pkseed[64]; + byte hashOut[64]; + int i; + + wb_params_init(¶ms, WC_HASH_TYPE_SHA512, 64, 64, 4, 1, 4, 0); + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wots chain loop: SHA-512 state init failed; skipped"); + } + else { + for (i = 0; i < 64; i++) { + data[i] = (byte)i; + pkseed[i] = (byte)(0x60 + i); + } + XMEMSET(addrBuf, 0, sizeof(addrBuf)); + state.ret = 0; + wc_xmss_chain(&state, data, 0, 40, pkseed, addrBuf, hashOut); + if (state.ret != 0) { + WB_NOTE("wc_xmss_chain start/steps run failed"); + wb_fail = 1; + } + wb_state_free(&state); + } + } +#else + WB_NOTE("WC_XMSS_SHA512 not compiled in; generic wc_xmss_chain (line " + "1697) arm skipped"); +#endif +} + +#ifndef WOLFSSL_XMSS_VERIFY_ONLY +/******************************************** + * 2846: wc_xmss_bds_next_idx()'s "if ((hsk > 0) && (i == 3))". + * hsk = sub_h - bds_k. Direct calls with offset=0 (so the function's + * internal "while (o >= 1...)" loop never runs) keep this test isolated to + * just the targeted if. + ********************************************/ +static void wb_bds_next_idx(void) +{ + XmssParams paramsPos; /* bds_k=0 -> hsk=sub_h=4 > 0 */ + XmssParams paramsZero; /* bds_k=4 -> hsk=0 */ + XmssState state; + BdsState bdsPos[1]; + BdsState bdsZero[1]; + byte skPos[2048]; + byte skZero[2048]; + byte sk_seed[32]; + byte pk_seed[32]; + HashAddress addr; + word8 height[8]; + word8 offset; + byte* sp; + + wb_params_init(¶msPos, WC_HASH_TYPE_SHA256, 32, 32, 4, 1, 4, 0); + wb_params_init(¶msZero, WC_HASH_TYPE_SHA256, 32, 32, 4, 1, 4, 4); + + if (wb_state_init(&state, ¶msPos) != 0) { + WB_NOTE("bds_next_idx: state init failed; skipped"); + return; + } + XMEMSET(sk_seed, 0x33, sizeof(sk_seed)); + XMEMSET(pk_seed, 0x44, sizeof(pk_seed)); + XMEMSET(skPos, 0, sizeof(skPos)); + XMEMSET(skZero, 0, sizeof(skZero)); + + /* hsk > 0, i == 3: both operands true -> the guarded copy runs. */ + if (wc_xmss_bds_state_load(&state, skPos, bdsPos, NULL) == 0) { + XMEMSET(&addr, 0, sizeof(addr)); + XMEMSET(height, 0, sizeof(height)); + offset = 0; + sp = state.stack; + state.ret = 0; + wc_xmss_bds_next_idx(&state, &bdsPos[0], sk_seed, pk_seed, addr, 3, + height, &offset, &sp); + if (state.ret != 0) { + WB_NOTE("bds_next_idx hsk>0,i==3 call failed"); + wb_fail = 1; + } + } + else { + WB_NOTE("bds_next_idx: bds_state_load (hsk>0) failed; skipped"); + } + + /* hsk > 0, i != 3: condIndex1 false, condIndex0 held true. */ + if (wc_xmss_bds_state_load(&state, skPos, bdsPos, NULL) == 0) { + XMEMSET(&addr, 0, sizeof(addr)); + XMEMSET(height, 0, sizeof(height)); + offset = 0; + sp = state.stack; + state.ret = 0; + wc_xmss_bds_next_idx(&state, &bdsPos[0], sk_seed, pk_seed, addr, 5, + height, &offset, &sp); + if (state.ret != 0) { + WB_NOTE("bds_next_idx hsk>0,i!=3 call failed"); + wb_fail = 1; + } + } + + /* hsk == 0: condIndex0 false (masks the AND) with i == 3 held true. */ + state.params = ¶msZero; + if (wc_xmss_bds_state_load(&state, skZero, bdsZero, NULL) == 0) { + XMEMSET(&addr, 0, sizeof(addr)); + XMEMSET(height, 0, sizeof(height)); + offset = 0; + sp = state.stack; + state.ret = 0; + wc_xmss_bds_next_idx(&state, &bdsZero[0], sk_seed, pk_seed, addr, 3, + height, &offset, &sp); + if (state.ret != 0) { + WB_NOTE("bds_next_idx hsk==0,i==3 call failed"); + wb_fail = 1; + } + } + else { + WB_NOTE("bds_next_idx: bds_state_load (hsk==0) failed; skipped"); + } + state.params = ¶msPos; + + wb_state_free(&state); +} + +/******************************************** + * 3212: wc_xmss_bds_auth_path()'s + * "if ((bds->keep == NULL) || (bds->authPath == NULL))" + * 3251: same function's "if ((tau < hs - 1) && (parent == 0))", where tau + * and parent come from wc_xmss_lowest_zero_bit_index(leafIdx, hs, &parent). + * For hs=4: leafIdx=1 -> tau=1,parent=0 (both true); leafIdx=5 -> tau=1, + * parent=1 (condIndex1 false, condIndex0 held true); leafIdx=7 -> tau=3, + * parent=0 (condIndex0 false, masking condIndex1). bds_k=0 keeps hsk=4 + * (>= all tau values used here), so the "i < hsk" arm of the trailing + * per-height loop is always taken and bds->retain is never dereferenced. + ********************************************/ +static void wb_bds_auth_path(void) +{ + XmssParams params; + XmssState state; + BdsState bds[1]; + byte skBuf[2048]; + byte sk_seed[32]; + byte pk_seed[32]; + HashAddress addr; + + wb_params_init(¶ms, WC_HASH_TYPE_SHA256, 32, 32, 4, 1, 4, 0); + + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("bds_auth_path: state init failed; skipped"); + return; + } + XMEMSET(sk_seed, 0x55, sizeof(sk_seed)); + XMEMSET(pk_seed, 0x66, sizeof(pk_seed)); + + /* Line 3212, "keep == NULL" true side (authPath left valid). */ + XMEMSET(skBuf, 0, sizeof(skBuf)); + if (wc_xmss_bds_state_load(&state, skBuf, bds, NULL) == 0) { + byte* savedKeep = bds[0].keep; + + bds[0].keep = NULL; + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_bds_auth_path(&state, &bds[0], 1, sk_seed, pk_seed, addr); + if (state.ret != WC_NO_ERR_TRACE(WC_FAILURE)) { + WB_NOTE("bds_auth_path keep==NULL did not report WC_FAILURE"); + wb_fail = 1; + } + bds[0].keep = savedKeep; + } + else { + WB_NOTE("bds_auth_path: bds_state_load failed; skipped"); + } + + /* Line 3212, "authPath == NULL" true side (keep left valid). */ + XMEMSET(skBuf, 0, sizeof(skBuf)); + if (wc_xmss_bds_state_load(&state, skBuf, bds, NULL) == 0) { + byte* savedAuth = bds[0].authPath; + + bds[0].authPath = NULL; + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_bds_auth_path(&state, &bds[0], 1, sk_seed, pk_seed, addr); + if (state.ret != WC_NO_ERR_TRACE(WC_FAILURE)) { + WB_NOTE("bds_auth_path authPath==NULL did not report " + "WC_FAILURE"); + wb_fail = 1; + } + bds[0].authPath = savedAuth; + } + + /* Baseline + line 3251: leafIdx=1 -> tau=1,parent=0 -> both operands + * true (also exercises line 3212's false side with both pointers + * valid). */ + XMEMSET(skBuf, 0, sizeof(skBuf)); + if (wc_xmss_bds_state_load(&state, skBuf, bds, NULL) == 0) { + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_bds_auth_path(&state, &bds[0], 1, sk_seed, pk_seed, addr); + if (state.ret != 0) { + WB_NOTE("bds_auth_path leafIdx=1 call failed"); + wb_fail = 1; + } + } + + /* Line 3251: leafIdx=5 -> tau=1,parent=1 -> condIndex1 false, + * condIndex0 held true. */ + XMEMSET(skBuf, 0, sizeof(skBuf)); + if (wc_xmss_bds_state_load(&state, skBuf, bds, NULL) == 0) { + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_bds_auth_path(&state, &bds[0], 5, sk_seed, pk_seed, addr); + if (state.ret != 0) { + WB_NOTE("bds_auth_path leafIdx=5 call failed"); + wb_fail = 1; + } + } + + /* Line 3251: leafIdx=7 -> tau=3,parent=0 -> condIndex0 false (masks + * condIndex1). */ + XMEMSET(skBuf, 0, sizeof(skBuf)); + if (wc_xmss_bds_state_load(&state, skBuf, bds, NULL) == 0) { + state.ret = 0; + XMEMSET(&addr, 0, sizeof(addr)); + wc_xmss_bds_auth_path(&state, &bds[0], 7, sk_seed, pk_seed, addr); + if (state.ret != 0) { + WB_NOTE("bds_auth_path leafIdx=7 call failed"); + wb_fail = 1; + } + } + + wb_state_free(&state); +} + +/******************************************** + * 3651: wc_xmssmt_keygen()'s (non-SMALL, active variant) + * "for (i = 0; (ret == 0) && (i < params->d - 1); i++)" + * A real d=2 keygen naturally exercises "i < d - 1" true (i=0) then false + * (i=1) with ret==0 throughout a successful call. + * + * 3956, 3985-3987: wc_xmssmt_sign_next_idx() (static, only reachable + * through wc_xmssmt_sign()) - repeatedly signing every valid index of a + * small d=2 tree drives its internal per-subtree bookkeeping (including the + * subtree-boundary crossings every 2^sub_h signs) through many operand + * combinations, with ret==0 throughout a successful run. + * + * 4379: wc_xmssmt_verify()'s "for (i = 1; (ret==0) && (i < params->d); + * i++)" - a d=2 verify naturally exercises "i < d" true (i=1) then false + * (i=2). + ********************************************/ +static void wb_full_cycle_d2(void) +{ + XmssParams params; + XmssState state; + byte seed[3 * 32]; + byte sk[8192]; + byte pk[160]; + byte sig[8192]; + static const byte msg[] = "xmss whitebox d2 message"; + int ret; + int i; + + wb_params_init(¶ms, WC_HASH_TYPE_SHA256, 32, 32, 4, 2, 4, 0); + + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("d2 cycle: state init failed; skipped"); + return; + } + + XMEMSET(seed, 0x22, sizeof(seed)); + XMEMSET(sk, 0, sizeof(sk)); + XMEMSET(pk, 0, sizeof(pk)); + + ret = wc_xmssmt_keygen(&state, seed, sk, pk); + if (ret != 0) { + WB_NOTE("d2 cycle: keygen failed; skipped"); + wb_state_free(&state); + return; + } + + /* Sign through every valid index (0..2^h-2 == 14): exercises + * wc_xmssmt_sign_next_idx()'s internals across both subtrees. */ + for (i = 0; i < 15; i++) { + XMEMSET(sig, 0, sizeof(sig)); + ret = wc_xmssmt_sign(&state, msg, (word32)sizeof(msg), sk, sig); + if (ret != 0) { + WB_NOTE("d2 cycle: sign failed before exhaustion"); + wb_fail = 1; + break; + } + + if (i == 14) { + /* Last valid index: verify exercises the full d=2 loop at + * line 4379 (i=1<2 true, then i=2<2 false), ret==0 + * throughout. */ + ret = wc_xmssmt_verify(&state, msg, (word32)sizeof(msg), sig, + pk); + if (ret != 0) { + WB_NOTE("d2 cycle: verify of a good signature failed"); + wb_fail = 1; + } + } + } + + wb_state_free(&state); +} + +/******************************************** + * 4070: wc_xmssmt_sign()'s "if ((ret == 0) && xmss_idx_invalid(idx, h))". + * 4087: same function's + * "if ((ret == 0) && (idx < (((XmssIdx)1 << h) - 1)))" - given the + * upstream invalid-index check at 4070 already rejects any idx that + * would make this false (the largest idx that can reach here is + * 2^h - 2, and 2^h - 2 < 2^h - 1 always), condIndex1's false side is + * provably unreachable while ret==0; only condIndex0 is closed here + * (see SKIP note in the final report). + * 4121: wc_xmss_sigsleft()'s + * "if ((ret == 0) && (WC_IDX_INVALID(idx, params->idx_len, params->h)))". + * 4391: wc_xmssmt_verify()'s "if ((ret == 0) && (XMEMCMP(node, pub_root, n) + * != 0))" - forced both ways directly (valid vs. corrupted public key). + * + * A small d=1 tree is signed through every valid index until natural + * exhaustion (idx reaches 2^h - 1), which is exactly when line 4070's + * "xmss_idx_invalid" operand flips true (having been false on every prior, + * successful sign) - masking line 4087's "ret == 0" operand to false on + * that same call. A separate, cheap direct call to wc_xmss_sigsleft() with + * an idx_len wc_xmss's dual-width WC_IDX_DECODE doesn't recognize (2, vs. + * the valid 3/4/5/8) forces "ret == 0" false at line 4121 without ever + * evaluating WC_IDX_INVALID - the complementary independence pair to the + * exhausted-key call (which shows WC_IDX_INVALID's true side with ret==0 + * true) and the fresh-key call (WC_IDX_INVALID's false side, ret==0 true). + ********************************************/ +static void wb_full_cycle_d1(void) +{ + XmssParams params; + XmssState state; + byte seed[3 * 32]; + byte sk[2048]; + byte pk[160]; + byte sig[4096]; + static const byte msg[] = "xmss whitebox d1 message"; + int ret; + int i; + int exhausted = 0; + + wb_params_init(¶ms, WC_HASH_TYPE_SHA256, 32, 32, 4, 1, 4, 0); + + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("d1 cycle: state init failed; skipped"); + return; + } + + XMEMSET(seed, 0x11, sizeof(seed)); + XMEMSET(sk, 0, sizeof(sk)); + XMEMSET(pk, 0, sizeof(pk)); + + ret = wc_xmssmt_keygen(&state, seed, sk, pk); + if (ret != 0) { + WB_NOTE("d1 cycle: keygen failed; skipped"); + wb_state_free(&state); + return; + } + + /* wc_xmss_sigsleft() on a fresh, unexhausted key: line 4121's + * WC_IDX_INVALID false side, ret == 0 true. */ + ret = wc_xmss_sigsleft(¶ms, sk); + if (ret != 1) { + WB_NOTE("d1 cycle: sigsleft on a fresh key did not report sigs " + "left"); + wb_fail = 1; + } + + /* Sign through every valid index (0..2^h-2 == 14). */ + for (i = 0; i < 15; i++) { + XMEMSET(sig, 0, sizeof(sig)); + ret = wc_xmssmt_sign(&state, msg, (word32)sizeof(msg), sk, sig); + if (ret != 0) { + WB_NOTE("d1 cycle: sign failed before exhaustion"); + wb_fail = 1; + break; + } + + if (i == 0) { + /* Genuine verify: line 4391's XMEMCMP==0 (false) side, and + * the successful d=1 loop path at line 4379. */ + ret = wc_xmssmt_verify(&state, msg, (word32)sizeof(msg), sig, + pk); + if (ret != 0) { + WB_NOTE("d1 cycle: verify of a good signature failed"); + wb_fail = 1; + } + + /* Corrupt the public root and re-verify: line 4391's + * XMEMCMP!=0 (true) side, ret==0 up to that point. */ + { + byte badPk[160]; + + XMEMCPY(badPk, pk, sizeof(pk)); + badPk[0] ^= 0xFFU; + ret = wc_xmssmt_verify(&state, msg, (word32)sizeof(msg), + sig, badPk); + if (ret != WC_NO_ERR_TRACE(SIG_VERIFY_E)) { + WB_NOTE("d1 cycle: corrupted-pk verify did not fail " + "as expected"); + wb_fail = 1; + } + } + } + } + + /* sk's index is now 2^h - 1 == 15. This sign attempt hits line 4070's + * "xmss_idx_invalid(idx,h)" true side (ret==0 up to that point), + * forcing ret = KEY_EXHAUSTED_E, which (masking) drives line 4087's + * "ret == 0" operand to false. */ + ret = wc_xmssmt_sign(&state, msg, (word32)sizeof(msg), sk, sig); + if (ret == WC_NO_ERR_TRACE(KEY_EXHAUSTED_E)) { + exhausted = 1; + } + else { + WB_NOTE("d1 cycle: key was not reported exhausted as expected"); + wb_fail = 1; + } + + /* wc_xmss_sigsleft(): line 4121's WC_IDX_INVALID true side, ret == 0 + * true. Craft an sk whose encoded idx is exactly 2^h - 1 == 15 (the + * smallest value for which (idx+1)>>h != 0) directly, rather than + * reusing the just-exhausted sk above: wc_xmssmt_sign()'s exhaustion + * handling XMEMSETs the index field to all-0xFF, which as an encoded + * 32-bit value (0xFFFFFFFF) wraps back to looking "valid" under + * IDX32_INVALID's "(idx+1)>>h" arithmetic (idx+1 overflows to 0) - a + * real quirk of that cleanup path, but not what this test is after. */ + if (exhausted) { + byte idxSk[2048]; + + XMEMCPY(idxSk, sk, sizeof(idxSk)); + idxSk[0] = 0x00; + idxSk[1] = 0x00; + idxSk[2] = 0x00; + idxSk[3] = 0x0F; /* idx = 15 = 2^h - 1, h = 4 */ + ret = wc_xmss_sigsleft(¶ms, idxSk); + if (ret != 0) { + WB_NOTE("d1 cycle: sigsleft with idx==2^h-1 unexpectedly " + "reported sigs left"); + wb_fail = 1; + } + } + + wb_state_free(&state); + + /* Line 4121, "ret == 0" false side: idx_len=2 matches neither + * WC_IDX_DECODE's 32-bit arm (3 or 4 bytes) nor its 64-bit arm (5 or + * 8 bytes), so decode sets ret = NOT_COMPILED_IN and WC_IDX_INVALID is + * never evaluated - independent of any sk content. */ + { + XmssParams badLenParams; + byte dummySk[8]; + + wb_params_init(&badLenParams, WC_HASH_TYPE_SHA256, 32, 32, 4, 1, 2, + 0); + XMEMSET(dummySk, 0, sizeof(dummySk)); + ret = wc_xmss_sigsleft(&badLenParams, dummySk); + if (ret != 0) { + WB_NOTE("d1 cycle: sigsleft with an unsupported idx_len " + "unexpectedly reported sigs left"); + wb_fail = 1; + } + } +} +#else /* WOLFSSL_XMSS_VERIFY_ONLY */ +static void wb_bds_next_idx(void) +{ + WB_NOTE("WOLFSSL_XMSS_VERIFY_ONLY: signing-side BDS helpers not " + "compiled in; wb_bds_next_idx skipped"); +} +static void wb_bds_auth_path(void) +{ + WB_NOTE("WOLFSSL_XMSS_VERIFY_ONLY: signing-side BDS helpers not " + "compiled in; wb_bds_auth_path skipped"); +} +static void wb_full_cycle_d2(void) +{ + WB_NOTE("WOLFSSL_XMSS_VERIFY_ONLY: keygen/sign not compiled in; " + "wb_full_cycle_d2 skipped"); +} +static void wb_full_cycle_d1(void) +{ + WB_NOTE("WOLFSSL_XMSS_VERIFY_ONLY: keygen/sign not compiled in; " + "wb_full_cycle_d1 skipped"); +} +#endif /* !WOLFSSL_XMSS_VERIFY_ONLY */ + +#else /* WOLFSSL_HAVE_XMSS */ + +static void wb_hash_family_pairs(void) +{ + WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); +} +static void wb_wots_chain_loop(void) +{ + WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); +} +static void wb_bds_next_idx(void) +{ + WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); +} +static void wb_bds_auth_path(void) +{ + WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); +} +static void wb_full_cycle_d2(void) +{ + WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); +} +static void wb_full_cycle_d1(void) +{ + WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); +} + +#endif /* WOLFSSL_HAVE_XMSS */ + +int main(void) +{ + printf("wc_xmss_impl.c white-box supplement\n"); + wb_run(); + wb_hash_family_pairs(); + wb_wots_chain_loop(); + wb_bds_next_idx(); + wb_bds_auth_path(); + wb_full_cycle_d2(); + wb_full_cycle_d1(); + printf("done (%s)\n", wb_fail ? "with failures" : "ok"); + return 0; +} diff --git a/tests/unit-mcdc/test_wolfentropy_whitebox.c b/tests/unit-mcdc/test_wolfentropy_whitebox.c new file mode 100644 index 00000000000..c0b1e7cc933 --- /dev/null +++ b/tests/unit-mcdc/test_wolfentropy_whitebox.c @@ -0,0 +1,309 @@ +/* test_wolfentropy_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 + */ + +/* White-box supplement for wolfcrypt/src/wolfentropy.c. + * + * The SP800-90B continuous health tests and their reset helpers are + * file-static and never return a failure through the public API on a healthy + * host (the jitter source passes), so the reject sides of their cutoff + * decisions are unreachable from tests/api. This white-box #includes + * wolfentropy.c directly and feeds crafted sample streams to drive BOTH sides + * of each decision in the same binary: + * + * Entropy_HealthTest_Repetition(): + * - "!rep_have_prev" first sample stored + * - "noise == rep_prev_noise" repeat increments the run counter + * - "rep_cnt >= REP_CUTOFF" reject (ENTROPY_RT_E) after REP_CUTOFF + * identical samples + * - else a differing sample resets the run + * Entropy_HealthTest_Proportion(): + * - "prop_total < PROP_CUTOFF - 1" fill (accumulate) phase + * - else windowed phase + * - "prop_cnt[noise] >= PROP_CUTOFF" reject (ENTROPY_APT_E) + * - "prop_total == PROP_WINDOW_SIZE" sliding-window eviction (accept path) + * + * All samples are bytes, so prop_cnt[noise] (256 entries) and prop_samples + * (PROP_WINDOW_SIZE entries) are always addressed in range -- memory-safe. + * The higher-level Entropy_Init()/wc_Entropy_OnDemandTest()/wc_Entropy_Get() + * exercise the MemUse/GetSample/GetNoise/Condition path best-effort; any + * setup failure is reported as a skip (return 0), never a test failure, so + * the campaign never discards the variant's coverage. + * + * wc_Entropy_Get() itself has two decisions whose operands reference the + * SAME file-static health-test state, but are not independently selectable + * from tests/api because Entropy_Init() (run once by test setup) already + * leaves that state primed: + * + * "if ((ret == 0) && ((prop_total == 0) || (!rep_have_prev)))" + * - "prop_total == 0" startup-retrigger via the first OR operand + * - "!rep_have_prev" startup-retrigger via the second OR operand + * "while ((ret == 0) && (len > 0))" + * "for (i = 0; (ret == 0) && (i < noise_len); i++)" + * - "ret == 0" both loops only ever end via their *other* + * operand on a healthy host; force an early exit + * by rigging the Proportion counts so the very + * first sample of the very first pass trips the + * cutoff, whatever its (unpredictable) value is. + * + * wb_startup_retrigger() and wb_get_loop_early_exit() below drive these by + * writing prop_total / rep_have_prev / prop_cnt[] directly (file-static, + * visible to this TU) immediately before calling the public entry point. + */ + +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#ifdef HAVE_ENTROPY_MEMUSE + +static void wb_repetition(void) +{ + int ret; + int i; + + /* First-sample and differing-sample (else) branches. */ + Entropy_HealthTest_Repetition_Reset(); + ret = Entropy_HealthTest_Repetition(0x41); /* !rep_have_prev -> store */ + if (ret != 0) { WB_NOTE("repetition first-sample not accepted"); wb_fail = 1; } + ret = Entropy_HealthTest_Repetition(0x42); /* differing -> else, reset run */ + if (ret != 0) { WB_NOTE("repetition differing-sample not accepted"); wb_fail = 1; } + + /* Drive the run counter to the cutoff: REP_CUTOFF identical samples. + * The first stores, the next REP_CUTOFF-2 increment the run and pass, and + * the REP_CUTOFF-th trips "rep_cnt >= REP_CUTOFF". */ + Entropy_HealthTest_Repetition_Reset(); + for (i = 0; i < REP_CUTOFF - 1; i++) { + ret = Entropy_HealthTest_Repetition(0x55); + if (ret != 0) { + WB_NOTE("repetition unexpectedly failed before cutoff"); + wb_fail = 1; + } + } + ret = Entropy_HealthTest_Repetition(0x55); /* run reaches REP_CUTOFF */ + if (ret != WC_NO_ERR_TRACE(ENTROPY_RT_E)) { + WB_NOTE("repetition cutoff did not report ENTROPY_RT_E"); + wb_fail = 1; + } + + Entropy_HealthTest_Repetition_Reset(); +} + +static void wb_proportion(void) +{ + int ret; + int i; + int failed_apt = 0; + + /* Fill + windowed + sliding-eviction accept path: a rotating value stream + * keeps every per-value count well under PROP_CUTOFF while prop_total + * climbs past PROP_WINDOW_SIZE, so the "prop_total == PROP_WINDOW_SIZE" + * eviction branch is taken without ever tripping the reject. */ + Entropy_HealthTest_Proportion_Reset(); + for (i = 0; i < PROP_WINDOW_SIZE + 128; i++) { + ret = Entropy_HealthTest_Proportion((byte)(i & 0xff)); + if (ret != 0) { + WB_NOTE("proportion rotating stream unexpectedly rejected"); + wb_fail = 1; + break; + } + } + + /* Reject path: a constant value drives one count to PROP_CUTOFF. The fill + * phase accepts PROP_CUTOFF-1 samples, then the windowed phase increments + * the same count to PROP_CUTOFF and returns ENTROPY_APT_E. */ + Entropy_HealthTest_Proportion_Reset(); + for (i = 0; i < PROP_CUTOFF; i++) { + ret = Entropy_HealthTest_Proportion(0x07); + if (ret == WC_NO_ERR_TRACE(ENTROPY_APT_E)) { + failed_apt = 1; + break; + } + if (ret != 0) { + WB_NOTE("proportion constant stream failed with unexpected code"); + wb_fail = 1; + break; + } + } + if (!failed_apt) { + WB_NOTE("proportion cutoff did not report ENTROPY_APT_E"); + wb_fail = 1; + } + + Entropy_HealthTest_Proportion_Reset(); +} + +/* Best-effort exercise of the collection path (Entropy_MemUse -> + * Entropy_GetSample/GetNoise -> Entropy_Condition) via the public entry + * points, once the SHA3 conditioner is initialized. Any failure is a skip. */ +static void wb_collect_path(void) +{ + int ret; + byte out[32]; + + ret = Entropy_Init(); + if (ret != 0) { + WB_NOTE("Entropy_Init failed; skipping collection-path exercise"); + return; + } + + if (wc_Entropy_OnDemandTest() != 0) { + WB_NOTE("wc_Entropy_OnDemandTest returned nonzero (skip, not fail)"); + } + + XMEMSET(out, 0, sizeof(out)); + if (wc_Entropy_Get(MAX_ENTROPY_BITS, out, (word32)sizeof(out)) != 0) { + WB_NOTE("wc_Entropy_Get returned nonzero (skip, not fail)"); + } + + Entropy_Final(); +} + +/* wc_Entropy_Get()'s startup-retrigger guard: + * "if ((ret == 0) && ((prop_total == 0) || (!rep_have_prev)))" + * On any live process, Entropy_Init() (already run by test setup and by + * wb_collect_path() above) leaves prop_total != 0 and rep_have_prev == 1, so + * this guard's *true* side -- and each of its two OR operands individually + * -- is never shown from tests/api. Both globals are file-static: drive + * each operand's independence pair directly, holding the other operand + * false (the "both false" side is already exercised by every other + * steady-state call in this campaign, e.g. wb_collect_path() above). */ +static void wb_startup_retrigger(void) +{ + int ret; + byte out[32]; + + ret = Entropy_Init(); + if (ret != 0) { + WB_NOTE("Entropy_Init failed; skipping startup-retrigger exercise"); + return; + } + + /* "prop_total == 0" true, "!rep_have_prev" false: retrigger via the + * first OR operand alone. */ + prop_total = 0; + rep_have_prev = 1; + XMEMSET(out, 0, sizeof(out)); + ret = wc_Entropy_Get(MAX_ENTROPY_BITS, out, (word32)sizeof(out)); + if (ret != 0) { + WB_NOTE("startup retrigger via prop_total==0 failed"); + wb_fail = 1; + } + + /* "prop_total == 0" false, "!rep_have_prev" true: retrigger via the + * second OR operand alone. */ + prop_total = 1; + rep_have_prev = 0; + XMEMSET(out, 0, sizeof(out)); + ret = wc_Entropy_Get(MAX_ENTROPY_BITS, out, (word32)sizeof(out)); + if (ret != 0) { + WB_NOTE("startup retrigger via !rep_have_prev failed"); + wb_fail = 1; + } + + Entropy_Final(); +} + +/* wc_Entropy_Get()'s collection loops: + * "while ((ret == 0) && (len > 0))" + * "for (i = 0; (ret == 0) && (i < noise_len); i++)" + * A healthy host's real jitter noise never fails a health test, so neither + * loop's "ret == 0" operand is ever shown false while its counterpart + * (len > 0 / i < noise_len) is still true -- both loops only ever end via + * the *other* operand. Force the Adaptive Proportion test to reject on the + * very first sample of the very first pass by pre-loading every prop_cnt[] + * slot to PROP_CUTOFF - 1 with prop_total already in the windowed phase: + * whichever byte the real jitter sample turns out to be, incrementing its + * count trips the cutoff deterministically, regardless of the + * (unpredictable) sample value itself. A single fixed Repetition sample is + * primed first so that test cannot itself reject and mask the rig. */ +static void wb_get_loop_early_exit(void) +{ + int ret; + byte out[64]; + int v; + + ret = Entropy_Init(); + if (ret != 0) { + WB_NOTE("Entropy_Init failed; skipping loop-early-exit exercise"); + return; + } + + Entropy_HealthTest_Reset(); + /* Deterministically prime the Repetition test's "have previous" state + * via its own first-sample branch so it can never reach REP_CUTOFF + * during this exercise (rep_cnt ends at 1, or at most 2 if the first + * real sample happens to repeat 0xab). */ + Entropy_HealthTest_Repetition(0xab); + + /* Rig the Proportion test into the windowed phase with every count one + * short of the reject cutoff. */ + prop_total = PROP_CUTOFF; + for (v = 0; v < (1 << ENTROPY_BITS_USED); v++) { + prop_cnt[v] = PROP_CUTOFF - 1; + } + + XMEMSET(out, 0, sizeof(out)); + ret = wc_Entropy_Get(MAX_ENTROPY_BITS, out, (word32)sizeof(out)); + if (ret != WC_NO_ERR_TRACE(ENTROPY_APT_E)) { + WB_NOTE("rigged proportion cutoff did not short-circuit " + "wc_Entropy_Get's collection loops"); + wb_fail = 1; + } + + Entropy_HealthTest_Reset(); + Entropy_Final(); +} + +#else /* !HAVE_ENTROPY_MEMUSE */ + +static void wb_repetition(void) +{ WB_NOTE("HAVE_ENTROPY_MEMUSE not compiled in; skipped repetition test"); } +static void wb_proportion(void) +{ WB_NOTE("HAVE_ENTROPY_MEMUSE not compiled in; skipped proportion test"); } +static void wb_collect_path(void) +{ WB_NOTE("HAVE_ENTROPY_MEMUSE not compiled in; skipped collection path"); } +static void wb_startup_retrigger(void) +{ WB_NOTE("HAVE_ENTROPY_MEMUSE not compiled in; skipped startup retrigger"); } +static void wb_get_loop_early_exit(void) +{ WB_NOTE("HAVE_ENTROPY_MEMUSE not compiled in; skipped loop early exit"); } + +#endif /* HAVE_ENTROPY_MEMUSE */ + +int main(void) +{ + 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 + * rigged-state exercises of wc_Entropy_Get()'s own decisions (each pair + * calls Entropy_Init()/Entropy_Final() around itself). */ + wb_collect_path(); + wb_repetition(); + wb_proportion(); + wb_startup_retrigger(); + wb_get_loop_early_exit(); + 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. */ + return 0; +}