diff --git a/CMakeLists.txt b/CMakeLists.txt index 08ae2c8b8a..331b51f5af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4510,6 +4510,9 @@ if(WOLFSSL_EXAMPLES) tests/api/test_evp.c tests/api/test_tls_ext.c tests/api/test_tls.c + tests/api/test_tls_bounds.c + tests/api/test_tls_msgtype.c + tests/api/test_tls_parse.c tests/api/test_session.c tests/api/test_x509.c tests/api/test_asn.c @@ -4550,6 +4553,8 @@ if(WOLFSSL_EXAMPLES) tests/api/test_evp_pkey.c tests/api/test_certman.c tests/api/test_tls13.c + tests/api/test_tls13_bounds.c + tests/api/test_tls13_features.c tests/srp.c tests/suites.c tests/w64wrapper.c diff --git a/tests/api.c b/tests/api.c index 2304f24f86..76778a0ab7 100644 --- a/tests/api.c +++ b/tests/api.c @@ -268,6 +268,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -309,6 +312,8 @@ #include #include #include +#include +#include #if !defined(NO_CERTS) && defined(WOLFSSL_ASN_TEMPLATE) && defined(HAVE_ECC) #include #endif @@ -40276,6 +40281,8 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_set_options), TEST_TLS13_DECLS, + TEST_TLS13_BOUNDS_DECLS, + TEST_TLS13_FEATURES_DECLS, TEST_DECL(test_wolfSSL_tmp_dh), TEST_DECL(test_wolfSSL_tmp_dh_regression), @@ -40678,6 +40685,9 @@ TEST_CASE testCases[] = { TEST_DECL(test_ocsp_responder), TEST_DECL(test_wolfIO_DecodeUrl_crlf_reject), TEST_TLS_DECLS, + TEST_TLS_BOUNDS_DECLS, + TEST_TLS_MSGTYPE_DECLS, + TEST_TLS_PARSE_DECLS, TEST_SESSION_DECLS, TEST_DECL(test_wc_DhSetNamedKey), TEST_DECL(test_DhAgree_rejects_p_minus_1), diff --git a/tests/api/include.am b/tests/api/include.am index 5065e88b39..849414b107 100644 --- a/tests/api/include.am +++ b/tests/api/include.am @@ -81,6 +81,9 @@ tests_unit_test_SOURCES += tests/api/test_ocsp.c tests_unit_test_SOURCES += tests/api/test_evp.c tests_unit_test_SOURCES += tests/api/test_tls_ext.c tests_unit_test_SOURCES += tests/api/test_tls.c +tests_unit_test_SOURCES += tests/api/test_tls_bounds.c +tests_unit_test_SOURCES += tests/api/test_tls_msgtype.c +tests_unit_test_SOURCES += tests/api/test_tls_parse.c tests_unit_test_SOURCES += tests/api/test_session.c # Certs tests_unit_test_SOURCES += tests/api/test_x509.c @@ -138,6 +141,8 @@ tests_unit_test_SOURCES += tests/api/test_evp_pkey.c tests_unit_test_SOURCES += tests/api/test_certman.c # TLS 1.3 specific tests_unit_test_SOURCES += tests/api/test_tls13.c +tests_unit_test_SOURCES += tests/api/test_tls13_bounds.c +tests_unit_test_SOURCES += tests/api/test_tls13_features.c endif EXTRA_DIST += tests/api/api.h @@ -216,6 +221,9 @@ EXTRA_DIST += tests/api/create_x500_unique_id_certs.py EXTRA_DIST += tests/api/test_evp.h EXTRA_DIST += tests/api/test_tls_ext.h EXTRA_DIST += tests/api/test_tls.h +EXTRA_DIST += tests/api/test_tls_bounds.h +EXTRA_DIST += tests/api/test_tls_msgtype.h +EXTRA_DIST += tests/api/test_tls_parse.h EXTRA_DIST += tests/api/test_session.h EXTRA_DIST += tests/api/test_x509.h EXTRA_DIST += tests/api/test_asn.h @@ -256,4 +264,6 @@ EXTRA_DIST += tests/api/test_evp_cipher.h EXTRA_DIST += tests/api/test_evp_pkey.h EXTRA_DIST += tests/api/test_certman.h EXTRA_DIST += tests/api/test_tls13.h +EXTRA_DIST += tests/api/test_tls13_bounds.h +EXTRA_DIST += tests/api/test_tls13_features.h diff --git a/tests/api/test_aes.c b/tests/api/test_aes.c index 5e0696715d..bdf7999a3b 100644 --- a/tests/api/test_aes.c +++ b/tests/api/test_aes.c @@ -8520,7 +8520,7 @@ int test_wc_AesFeatureCoverage(void) * GCM/GMAC block works on all of them, so it only excludes HAVE_SELFTEST; the * CCM block additionally excludes old FIPS (its AAD-only case diverges there, * see the per-block note); the key-wrap block excludes all FIPS + self-test. - * The open MC/DC campaign builds are unaffected. */ + * The open MC/DC builds are unaffected. */ #if !defined(NO_AES) && defined(HAVE_AESGCM) && !defined(HAVE_SELFTEST) /* ---- AES-GCM streaming API: multi-chunk AAD and data ---- */ /* Uses a hardcoded 256-bit key, so requires AES-256. */ diff --git a/tests/api/test_chacha.c b/tests/api/test_chacha.c index 80a8758c38..edc23843be 100644 --- a/tests/api/test_chacha.c +++ b/tests/api/test_chacha.c @@ -73,7 +73,7 @@ int test_wc_Chacha_SetKey(void) /* misaligned key pointer: exercises the (wc_ptr_t)key % 4 realignment * decision in wc_Chacha_SetKey when XSTREAM_ALIGN is forced on (the - * xstream_align campaign variant). settings.h compiles XSTREAM_ALIGN out + * xstream_align variant). settings.h compiles XSTREAM_ALIGN out * by default on x86_64/i386/ia64 (NO_XSTREAM_ALIGN), so this call is a * harmless no-op realignment-free copy on every other build. */ { diff --git a/tests/api/test_dh.c b/tests/api/test_dh.c index 15f5a09c0b..df5a4caec1 100644 --- a/tests/api/test_dh.c +++ b/tests/api/test_dh.c @@ -39,7 +39,7 @@ * DH_MAX_SIZE expands to WC_BITS_FULL_BYTES(SP_INT_BITS), and * WC_BITS_FULL_BYTES(x) is defined as (WC_BITS_TO_BYTES(x) << 3) - i.e. it * returns SP_INT_BITS itself (rounded up to a byte multiple), NOT - * SP_INT_BITS/8 as its name suggests. With this campaign's SP_INT_BITS 4096, + * SP_INT_BITS/8 as its name suggests. With this suite's SP_INT_BITS 4096, * DH_MAX_SIZE is therefore 4096 (bytes!), not the 512 a caller would * reasonably expect. Passing that value as *privSz (a requested private-key * size, not just a buffer capacity) to wc_DhGenerateKeyPair overflows the diff --git a/tests/api/test_ecc.c b/tests/api/test_ecc.c index 0550d9adc2..f462916253 100644 --- a/tests/api/test_ecc.c +++ b/tests/api/test_ecc.c @@ -2712,12 +2712,12 @@ int test_wc_EccPrivateKeyToDer(void) /* * MC/DC wave 1 - decision-targeted negative/edge paths for wolfcrypt/src/ * ecc.c that the existing (already extensive) API tests above do not drive. - * Each block cites the GAPS.md line:col:cond it targets. No library source + * Each block cites the the uncovered-condition report line:col:cond it targets. No library source * is changed; every case is reached through the public wc_ecc_* API. * * Split into several functions (test_wc_EccDecisionCoverage{,2,3,4}) rather * than one large one: a single function covering this many independent - * decisions produced a stack-corrupting crash under this campaign's + * decisions produced a stack-corrupting crash under this suite's * -fcoverage-mcdc + -O0 combination (reproduced with gdb: a plain on-stack * mp_int's used/size fields were already garbage immediately after its own * mp_init(), and clearing it then walked off the end of its dp[] array and @@ -2747,7 +2747,7 @@ int test_wc_EccDecisionCoverage(void) #endif ExpectIntEQ(ret, 0); - /* ---- wc_ecc_set_curve: GAPS.md 1927 ---- + /* ---- wc_ecc_set_curve: the uncovered-condition report 1927 ---- * if (key == NULL || (keysize <= 0 && curve_id < 0)) * key==NULL true side is already exercised elsewhere (BAD_FUNC_ARG on a * NULL key is a common pattern); complete the compound's other operand @@ -2763,7 +2763,7 @@ int test_wc_EccDecisionCoverage(void) ExpectIntEQ(wc_ecc_set_curve(&key, KEY32, -1), 0); #endif - /* ---- wc_ecc_get_curve_id: GAPS.md 4317 ---- + /* ---- wc_ecc_get_curve_id: the uncovered-condition report 4317 ---- * if (wc_ecc_is_valid_idx(curve_idx) && curve_idx >= 0) * curve_idx == -1 makes wc_ecc_is_valid_idx() true (ECC_CUSTOM_IDX is * a valid "user-supplied params" index) but curve_idx>=0 false: the @@ -2774,14 +2774,14 @@ int test_wc_EccDecisionCoverage(void) ExpectIntEQ(wc_ecc_get_curve_id(key.idx), ECC_SECP256R1); #endif - /* ---- wc_ecc_get_curve_params: GAPS.md 4654 ---- + /* ---- wc_ecc_get_curve_params: the uncovered-condition report 4654 ---- * if (curve_idx >= 0 && curve_idx < (int)ECC_SET_COUNT) * both boundary violations (negative, and >= COUNT) plus a valid idx. */ ExpectNull(wc_ecc_get_curve_params(-1)); ExpectNull(wc_ecc_get_curve_params(1000000)); ExpectNotNull(wc_ecc_get_curve_params(key.idx)); - /* ---- wc_ecc_point_is_at_infinity: GAPS.md 5320 ---- + /* ---- wc_ecc_point_is_at_infinity: the uncovered-condition report 5320 ---- * if (mp_iszero(p->x) && mp_iszero(p->y)) * Unique-cause MC/DC for a 2-operand AND needs THREE vectors within * this same binary: (T,T), (F,T), (T,F) (the existing pointFns test's @@ -2805,7 +2805,7 @@ int test_wc_EccDecisionCoverage(void) wc_ecc_del_point(inf); } - /* ---- wc_ecc_gen_k: GAPS.md 5335 ---- + /* ---- wc_ecc_gen_k: the uncovered-condition report 5335 ---- * if (rng==NULL || size<0 || size+8>ECC_MAXSIZE_GEN || k==NULL || * order==NULL) * Exercise each operand's TRUE side individually against an otherwise @@ -2832,7 +2832,7 @@ int test_wc_EccDecisionCoverage(void) } #endif - /* ---- wc_ecc_init_id: GAPS.md 6479, 6483 ---- + /* ---- wc_ecc_init_id: the uncovered-condition report 6479, 6483 ---- * if (ret == 0 && (len < 0 || len > ECC_MAX_ID_LEN)) -> BUFFER_E * if (ret == 0 && id != NULL && len != 0) -> copy branch * Exercise: len<0, len>MAX, id==NULL (len!=0 skipped), len==0 (id!=NULL @@ -2855,7 +2855,7 @@ int test_wc_EccDecisionCoverage(void) XMEMSET(&idKey, 0, sizeof(idKey)); ExpectIntEQ(wc_ecc_init_id(&idKey, NULL, 0, NULL, INVALID_DEVID), 0); wc_ecc_free(&idKey); - /* id != NULL, len == 0: GAPS.md 6483's 3rd operand (len != 0) + /* id != NULL, len == 0: the uncovered-condition report 6483's 3rd operand (len != 0) * independence pair -- id!=NULL fixed TRUE across this call and * the all-true "copy" call below, len toggled 0 vs nonzero. */ XMEMSET(&idKey, 0, sizeof(idKey)); @@ -2868,7 +2868,7 @@ int test_wc_EccDecisionCoverage(void) } #endif - /* ---- wc_ecc_init_label: GAPS.md 6503, 6507 ---- + /* ---- wc_ecc_init_label: the uncovered-condition report 6503, 6507 ---- * if (key == NULL || label == NULL) * if (labelLen == 0 || labelLen > ECC_MAX_LABEL_LEN) */ #ifdef WOLF_PRIVATE_KEY_ID @@ -2898,7 +2898,7 @@ int test_wc_EccDecisionCoverage(void) #endif #if defined(HAVE_ECC_SIGN) && !defined(NO_ASN) - /* ---- wc_ecc_sign_hash / wc_ecc_sign_hash_ex: GAPS.md 6909, 7443 ---- + /* ---- wc_ecc_sign_hash / wc_ecc_sign_hash_ex: the uncovered-condition report 6909, 7443 ---- * if ((inlen > WC_MAX_DIGEST_SIZE) || (inlen < WC_MIN_DIGEST_SIZE_FOR_SIGN)) * The signVerify_hash test above already shows the ">MAX" true side; * complete the other operand with a too-short digest. */ @@ -2919,7 +2919,7 @@ int test_wc_EccDecisionCoverage(void) #endif /* wc_ecc_sign_hash() has its OWN copy of this length check (it does * not delegate to wc_ecc_sign_hash_ex() before running it), so - * GAPS.md 7443 (wc_ecc_sign_hash_ex's identical check) needs a + * the uncovered-condition report 7443 (wc_ecc_sign_hash_ex's identical check) needs a * direct call in the SAME test binary to independently show its own * MC/DC pair -- llvm-cov computes independence per-binary, so * showing the FALSE side via signVerify_hash's normal-length call @@ -2947,7 +2947,7 @@ int test_wc_EccDecisionCoverage(void) #endif /* HAVE_ECC_SIGN && !NO_ASN */ #if defined(HAVE_ECC_VERIFY) && defined(WOLFSSL_PUBLIC_MP) - /* ---- wc_ecc_verify_hash_ex: GAPS.md 9476 ---- + /* ---- wc_ecc_verify_hash_ex: the uncovered-condition report 9476 ---- * Same reasoning as wc_ecc_sign_hash_ex above: wc_ecc_verify_hash() * does not delegate through this check, so it needs its own direct * short-hash call in this binary. */ @@ -2970,7 +2970,7 @@ int test_wc_EccDecisionCoverage(void) } #endif - /* ---- wc_ecc_free: GAPS.md 8209 ---- + /* ---- wc_ecc_free: the uncovered-condition report 8209 ---- * if (key->deallocSet && key->dp != NULL) * Exercise the "deallocSet but dp already NULL" and "dp set but * deallocSet false" independence halves via wc_ecc_set_custom_curve @@ -3025,7 +3025,7 @@ int test_wc_EccDecisionCoverage2(void) #if defined(HAVE_ECC_VERIFY) && !defined(WOLFSSL_SP_MATH) && \ defined(WOLFSSL_PUBLIC_MP) - /* ---- wc_ecc_check_r_s_range (via wc_ecc_verify_hash_ex): GAPS.md + /* ---- wc_ecc_check_r_s_range (via wc_ecc_verify_hash_ex): the uncovered-condition report * 8939, 8942 ---- * if ((err == 0) && (mp_cmp(r, curve->order) != MP_LT)) -> r >= order * if ((err == 0) && (mp_cmp(s, curve->order) != MP_LT)) -> s >= order @@ -3056,7 +3056,7 @@ int test_wc_EccDecisionCoverage2(void) #endif /* ---- wc_ecc_import_point_der_ex / wc_ecc_export_point_der{,_compressed}: - * GAPS.md 9710, 9964, 9970, 9975, 9984, 10030, 10037, 10042 ---- */ + * the uncovered-condition report 9710, 9964, 9970, 9975, 9984, 10030, 10037, 10042 ---- */ #if defined(HAVE_ECC_KEY_EXPORT) && defined(HAVE_ECC_KEY_IMPORT) { ecc_point* point = NULL; @@ -3102,10 +3102,10 @@ int test_wc_EccDecisionCoverage2(void) { /* wc_ecc_export_point_der_compressed is WOLFSSL_LOCAL (hidden in a * shared library), so it is not linkable from the shared-library - * unit test; its own decision coverage is driven by the campaign's + * unit test; its own decision coverage is driven by the * ecc white-box (which includes ecc.c directly). The public * compressed export path wc_ecc_export_x963_ex(..., 1) is exercised - * here (GAPS.md 16058, the static wc_ecc_export_x963_compressed + * here (the uncovered-condition report 16058, the static wc_ecc_export_x963_compressed * helper). */ #ifdef HAVE_ECC_KEY_EXPORT { @@ -3124,7 +3124,7 @@ int test_wc_EccDecisionCoverage2(void) } #endif /* HAVE_ECC_KEY_EXPORT && HAVE_ECC_KEY_IMPORT */ - /* ---- wc_ecc_is_point: GAPS.md 10304, 10329, 10332, 10390, 10396, + /* ---- wc_ecc_is_point: the uncovered-condition report 10304, 10329, 10332, 10390, 10396, * 10403 ---- * Direct call (rather than through wc_ecc_point_is_on_curve) with a * point that is genuinely ON the curve (the generator) and the @@ -3191,7 +3191,7 @@ int test_wc_EccDecisionCoverage3(void) ExpectIntEQ(ret, 0); /* ---- wc_ecc_export_public_raw / wc_ecc_export_private_raw: - * GAPS.md 11477, 11484, 11538, 11548 ---- */ + * the uncovered-condition report 11477, 11484, 11538, 11548 ---- */ #if defined(HAVE_ECC_KEY_EXPORT) { byte qx[MAX_ECC_BYTES], qy[MAX_ECC_BYTES], d[MAX_ECC_BYTES]; @@ -3207,12 +3207,12 @@ int test_wc_EccDecisionCoverage3(void) &qyLen), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); wc_ecc_free(&noDpKey); - /* d != NULL but dLen == NULL: GAPS.md 11484 first operand. */ + /* d != NULL but dLen == NULL: the uncovered-condition report 11484 first operand. */ qxLen = sizeof(qx); qyLen = sizeof(qy); ExpectIntEQ(wc_ecc_export_private_raw(&key, qx, &qxLen, qy, &qyLen, d, NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); - /* d != NULL, dLen != NULL, but key type is public-only: GAPS.md + /* d != NULL, dLen != NULL, but key type is public-only: the uncovered-condition report * 11484 second operand. */ { ecc_key pubOnly; @@ -3229,13 +3229,13 @@ int test_wc_EccDecisionCoverage3(void) ExpectIntEQ(wc_ecc_export_private_raw(&pubOnly, NULL, NULL, NULL, NULL, d, &dLen), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); - /* qx != NULL, qxLen == NULL: GAPS.md 11538 first operand. */ + /* qx != NULL, qxLen == NULL: the uncovered-condition report 11538 first operand. */ ExpectIntEQ(wc_ecc_export_private_raw(&key, qx, NULL, NULL, NULL, NULL, NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); - /* qy != NULL, qyLen == NULL: GAPS.md 11548 first operand. */ + /* qy != NULL, qyLen == NULL: the uncovered-condition report 11548 first operand. */ ExpectIntEQ(wc_ecc_export_private_raw(&key, NULL, NULL, qy, NULL, NULL, NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); - /* qx != NULL against a PRIVATEKEY_ONLY key: GAPS.md 11538 + /* qx != NULL against a PRIVATEKEY_ONLY key: the uncovered-condition report 11538 * second operand (type == ECC_PRIVATEKEY_ONLY). */ pubOnly.type = ECC_PRIVATEKEY_ONLY; qxbLen = sizeof(qxb); @@ -3250,7 +3250,7 @@ int test_wc_EccDecisionCoverage3(void) } #endif /* HAVE_ECC_KEY_EXPORT */ - /* ---- wc_ecc_rs_raw_to_sig: GAPS.md 12015 ---- */ + /* ---- wc_ecc_rs_raw_to_sig: the uncovered-condition report 12015 ---- */ { byte r[KEY32], s[KEY32], sig[ECC_MAX_SIG_SIZE]; word32 sigLen = sizeof(sig); @@ -3269,7 +3269,7 @@ int test_wc_EccDecisionCoverage3(void) NULL), WC_NO_ERR_TRACE(ECC_BAD_ARG_E)); } - /* ---- wc_ecc_import_private_key_ex: GAPS.md 11671 (_ecc_import_ + /* ---- wc_ecc_import_private_key_ex: the uncovered-condition report 11671 (_ecc_import_ * private_key_ex key==NULL||priv==NULL, reached via the public * wrapper's own identical pre-check, same independence pair) ---- */ #if defined(HAVE_ECC_KEY_IMPORT) @@ -3313,7 +3313,7 @@ int test_wc_EccDecisionCoverage4(void) #endif ExpectIntEQ(ret, 0); - /* ---- ecc_mul2add argument guard: GAPS.md 8446 ---- + /* ---- ecc_mul2add argument guard: the uncovered-condition report 8446 ---- * NOT closeable by any current variant, API or white-box: both bodies * of ecc_mul2add() (the argument-checked "normal" one at line ~8417 and * the Shamir/fixed-point-cache one at line ~13909 that supersedes it @@ -3324,13 +3324,13 @@ int test_wc_EccDecisionCoverage4(void) * exercises the unchecked Shamir body under the name ecc_mul2add) or * turns BOTH ECC_SHAMIR and FP_ECC OFF together (no_fp_shamir, per its * config_base's philosophy of flipping the FALSE side of both feature - * guards at once -- see modules.json's ecc notes), which compiles + * guards at once -- see the module registry's ecc notes), which compiles * *neither* body, making ecc_mul2add an undefined symbol there (link * failure, confirmed empirically). Reaching this decision needs a new, * not-yet-scaffolded variant: ECC_SHAMIR on + FP_ECC off. Classified as * a needs-variant residual; see RESIDUALS.md. */ - /* ---- wc_ecc_ctx_set_kdf_salt: GAPS.md 14607 ---- + /* ---- wc_ecc_ctx_set_kdf_salt: the uncovered-condition report 14607 ---- * if (ctx == NULL || (salt == NULL && sz != 0)) * ctx==NULL already the common BAD_FUNC_ARG idiom shown elsewhere; add * the salt==NULL/sz!=0 half here with a live ctx. */ @@ -3347,7 +3347,7 @@ int test_wc_EccDecisionCoverage4(void) } #endif - /* ---- wc_ecc_set_custom_curve: GAPS.md 16181 ---- */ + /* ---- wc_ecc_set_custom_curve: the uncovered-condition report 16181 ---- */ #if defined(WOLFSSL_CUSTOM_CURVES) { ecc_key ccKey2; @@ -3361,7 +3361,7 @@ int test_wc_EccDecisionCoverage4(void) } #endif - /* ---- wc_X963_KDF: GAPS.md 16217, 16221 ---- */ + /* ---- wc_X963_KDF: the uncovered-condition report 16217, 16221 ---- */ #ifdef HAVE_X963_KDF { byte secret[16]; diff --git a/tests/api/test_hash.c b/tests/api/test_hash.c index 3fc90da481..a3a4e7b2e5 100644 --- a/tests/api/test_hash.c +++ b/tests/api/test_hash.c @@ -999,7 +999,7 @@ int test_wc_HashDecisionCoverage(void) * 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. */ + * coverage simply comes from non-DEBUG variants in the harness union. */ #ifndef DEBUG_WOLFSSL ExpectIntEQ(wc_HashUpdate(&hash, WC_HASH_TYPE_MD5_SHA, (byte*)"a", 1), WC_NO_ERR_TRACE(HASH_TYPE_E)); diff --git a/tests/api/test_hmac.c b/tests/api/test_hmac.c index 28e4a16697..fa84e01ac8 100644 --- a/tests/api/test_hmac.c +++ b/tests/api/test_hmac.c @@ -883,7 +883,7 @@ int test_wc_HmacSizeByType(void) /* The FIPS/self-test hmac's wc_HmacSizeByType returns HMAC_KAT_FIPS_E for any * type it doesn't accept (e.g. MD5 is not a FIPS HMAC type), and BAD_FUNC_ARG * differs too, so the size/invalid-type assertions here only hold on the open - * builds the campaign actually measures. Exclude the frozen modules whole. */ + * builds the harness actually measures. Exclude the frozen modules whole. */ #if !defined(NO_HMAC) && !defined(HAVE_SELFTEST) && !defined(HAVE_FIPS) #ifndef NO_MD5 ExpectIntEQ(wc_HmacSizeByType(WC_MD5), WC_MD5_DIGEST_SIZE); @@ -941,7 +941,7 @@ int test_wc_HmacCopy(void) /* wc_HmacCopy() is newer than the frozen FIPS/selftest wolfcrypt modules * (absent from the v4.1.0-stable hmac.h that cavp-selftest-v2 pins, and from * every frozen FIPS bundle), so skip it there to keep those builds warning- - * clean; the campaign measures MC/DC on non-FIPS variants regardless. */ + * clean; the harness measures MC/DC on non-FIPS variants regardless. */ #if !defined(NO_HMAC) && !defined(NO_SHA256) && \ !defined(HAVE_SELFTEST) && !defined(HAVE_FIPS) Hmac src; diff --git a/tests/api/test_kdf.c b/tests/api/test_kdf.c index c0f6dadb72..68a09be92d 100644 --- a/tests/api/test_kdf.c +++ b/tests/api/test_kdf.c @@ -99,7 +99,7 @@ int test_wc_KdfDecisionCoverage(void) * 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 + * coverage is measured only in non-FIPS variants, so skip the whole * suite under FIPS/selftest. */ #if !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) #ifndef NO_KDF @@ -670,7 +670,7 @@ int test_wc_KdfFeatureCoverage(void) /* 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. */ + * kdf.c is measured only in non-FIPS variants. */ #if !defined(HAVE_FIPS) && !defined(HAVE_SELFTEST) #ifndef NO_KDF diff --git a/tests/api/test_mldsa.c b/tests/api/test_mldsa.c index c7abe5f6dc..1b4016564c 100644 --- a/tests/api/test_mldsa.c +++ b/tests/api/test_mldsa.c @@ -30620,7 +30620,7 @@ int test_dilithium_hash(void) } /* ===================================================================== - * MC/DC coverage supplements (ISO 26262 per-module campaign). + * MC/DC coverage supplements (per-module MC/DC suite). * * test_wc_MldsaDecisionCoverage: one negative call per public-entry * argument / state check, each asserting the specific error, driving both diff --git a/tests/api/test_mlkem.c b/tests/api/test_mlkem.c index 759f25f644..a0403fd354 100644 --- a/tests/api/test_mlkem.c +++ b/tests/api/test_mlkem.c @@ -4089,7 +4089,7 @@ int test_wc_mlkem_decode_privkey_bad_pubhash(void) /****************************************************************************** - * MC/DC coverage supplements (ISO 26262 per-module campaign). + * MC/DC coverage supplements (per-module MC/DC suite). * * test_wc_MlkemDecisionCoverage: one negative call per argument-check / * short-buffer / key-state branch in the public ML-KEM API, each asserting the diff --git a/tests/api/test_poly1305.c b/tests/api/test_poly1305.c index a71de52417..3a4e6f0dd2 100644 --- a/tests/api/test_poly1305.c +++ b/tests/api/test_poly1305.c @@ -277,7 +277,7 @@ int test_wc_Poly1305_PadEncodeSizes(void) * of the (paddingLen > 0) decision. (paddingLen < WC_POLY1305_PAD_SZ is * structurally always true whenever paddingLen > 0 is true -- the mod-16 * formula bounds paddingLen to 0..15 -- so that operand's FALSE side is - * an unsatisfiable residual; see campaign RESIDUALS notes.) */ + * an unsatisfiable residual; an argued residual.) */ ExpectIntEQ(wc_Poly1305SetKey(&ctx, key, sizeof(key)), 0); ExpectIntEQ(wc_Poly1305_Pad(&ctx, 16), 0); diff --git a/tests/api/test_port.c b/tests/api/test_port.c index 9c1d13ba19..66e3c76f12 100644 --- a/tests/api/test_port.c +++ b/tests/api/test_port.c @@ -34,7 +34,7 @@ * Residuals left uncovered on purpose: wc_open_cloexec / wc_accept_cloexec's * "fd < 0 && errno == EINVAL" and "errno != ENOSYS && errno != EINVAL" arms are * the fallback for kernels without O_CLOEXEC / SOCK_CLOEXEC, unreachable on any - * host the campaign runs on. + * host the harness runs on. */ #ifndef SINGLE_THREADED diff --git a/tests/api/test_random.c b/tests/api/test_random.c index a4c3732050..b929cd9357 100644 --- a/tests/api/test_random.c +++ b/tests/api/test_random.c @@ -297,7 +297,7 @@ 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"): + /* NOTE (the uncovered-condition report 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() @@ -312,7 +312,7 @@ int test_wc_GenerateSeed(void) * 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. */ + * the shared suite config headers -- flagged for the orchestrator. */ /* Bad parameters. */ ExpectIntEQ(wc_GenerateSeed(NULL, NULL , 16), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); @@ -1088,7 +1088,7 @@ int test_wc_RNG_SeedCb(void) * skipped, since it is guarded by * "defined(HAVE_HASHDRBG) && !defined(CUSTOM_RAND_GENERATE_BLOCK)"). Not * gated on HAVE_HASHDRBG since this path is intentionally independent of - * it -- see configs/random/user_settings.custom_rand.h in the campaign for + * it -- see configs/random/user_settings.custom_rand.h in the harness for * why forcing both together is unsafe. */ int test_wc_RNG_CustomRandBlock(void) { diff --git a/tests/api/test_rsa.c b/tests/api/test_rsa.c index fa130e5bb4..ed930e74ec 100644 --- a/tests/api/test_rsa.c +++ b/tests/api/test_rsa.c @@ -1429,7 +1429,7 @@ int test_wc_RsaDecisionCoverage(void) * differ. The sibling key-gen/decision tests in this file (e.g. * test_wc_CheckProbablePrime, the RsaKeyGeneration group) exclude HAVE_SELFTEST * for the same reason; do so here too. HAVE_FIPS is intentionally left running: - * that (newer) module honours these decisions and the campaign gains coverage + * that (newer) module honours these decisions and the harness gains coverage * from it. */ #if !defined(NO_RSA) && defined(WOLFSSL_KEY_GEN) && \ !defined(WOLFSSL_RSA_PUBLIC_ONLY) && !defined(HAVE_SELFTEST) diff --git a/tests/api/test_sakke.c b/tests/api/test_sakke.c index 78d4a8d823..332c34100f 100644 --- a/tests/api/test_sakke.c +++ b/tests/api/test_sakke.c @@ -119,7 +119,7 @@ * wc_DeriveSakkeSSV()) first calls sakke_calc_a(), which calls * wc_HashInit_ex() -- itself rejecting an invalid/unsupported hashType with * BAD_FUNC_ARG/HASH_TYPE_E before sakke_hash_to_range() is ever reached. - * This is flagged for the DEATHNOTE rather than forced here; the tests below + * This is not forced here; the tests below * instead drive the reachable hashType-invalid path through * wc_HashInit_ex(), which is the only way sakke.c's hashType parameter can * be shown to gate an error from the public API. diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index cc81b5a119..179f08672c 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -525,6 +525,112 @@ int test_tls13_apis(void) WC_NO_ERR_TRACE(BAD_FUNC_ARG)); #endif + /* The accepting vector for the whole guard: + * required == NULL || ssl == NULL || !IsAtLeastTLSv1_3(ssl->version) + * Every case above leaves at least one operand true, so none of the three + * operands had a partner vector with the decision false. A live TLS 1.3 + * object with a non-NULL out-parameter supplies it. */ +#ifndef NO_WOLFSSL_CLIENT + ExpectIntEQ(wolfSSL_key_update_response(clientSsl, &required), 0); +#endif + + /* The remaining TLS 1.3-only entry points guarded by + * ssl == NULL || !IsAtLeastTLSv1_3(ssl->version) + * (or the CTX equivalent). Each needs all three vectors -- NULL, a + * TLS 1.2 object, and a TLS 1.3 object -- for both operands to have an + * independence partner. Return values other than BAD_FUNC_ARG are + * intentional: these calls are made on objects that never handshook, so a + * side/state error is the correct success indication that the argument + * guard was passed. */ +#ifdef WOLFSSL_SEND_HRR_COOKIE + ExpectIntEQ(wolfSSL_disable_hrr_cookie(NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#ifndef NO_WOLFSSL_CLIENT +#ifndef WOLFSSL_NO_TLS12 + ExpectIntEQ(wolfSSL_disable_hrr_cookie(clientTls12Ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + ExpectIntEQ(wolfSSL_disable_hrr_cookie(clientSsl), + WC_NO_ERR_TRACE(SIDE_ERROR)); +#endif +#ifndef NO_WOLFSSL_SERVER + ExpectIntEQ(wolfSSL_disable_hrr_cookie(serverSsl), WOLFSSL_SUCCESS); +#endif +#endif /* WOLFSSL_SEND_HRR_COOKIE */ + +#ifdef HAVE_SUPPORTED_CURVES + ExpectIntEQ(wolfSSL_CTX_only_dhe_psk(NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#ifndef NO_WOLFSSL_CLIENT +#ifndef WOLFSSL_NO_TLS12 + ExpectIntEQ(wolfSSL_CTX_only_dhe_psk(clientTls12Ctx), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + ExpectIntEQ(wolfSSL_CTX_only_dhe_psk(clientCtx), 0); +#endif + + ExpectIntEQ(wolfSSL_only_dhe_psk(NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#ifndef NO_WOLFSSL_CLIENT +#ifndef WOLFSSL_NO_TLS12 + ExpectIntEQ(wolfSSL_only_dhe_psk(clientTls12Ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + ExpectIntEQ(wolfSSL_only_dhe_psk(clientSsl), 0); +#endif +#endif /* HAVE_SUPPORTED_CURVES */ + + /* wolfSSL_require_psk() already has its NULL and TLS 1.3 vectors in + * test_tls13_require_psk_apis(); only the TLS 1.2 middle vector -- the one + * that makes the version operand true on its own -- was missing. */ +#ifndef NO_WOLFSSL_CLIENT +#ifndef WOLFSSL_NO_TLS12 + ExpectIntEQ(wolfSSL_require_psk(clientTls12Ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + ExpectIntEQ(wolfSSL_require_psk(clientSsl), 0); +#endif + +#if !defined(NO_WOLFSSL_SERVER) && defined(HAVE_SESSION_TICKET) + ExpectIntEQ(wolfSSL_send_SessionTicket(NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#ifndef WOLFSSL_NO_TLS12 + ExpectIntEQ(wolfSSL_send_SessionTicket(serverTls12Ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + /* Never handshook, so the argument guard is passed and the handshake-state + * check rejects it. */ + ExpectIntEQ(wolfSSL_send_SessionTicket(serverSsl), + WC_NO_ERR_TRACE(NOT_READY_ERROR)); +#endif + +#ifdef WOLFSSL_EARLY_DATA + ExpectIntEQ(wolfSSL_get_early_data_status(NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#ifndef NO_WOLFSSL_CLIENT +#ifndef WOLFSSL_NO_TLS12 + ExpectIntEQ(wolfSSL_get_early_data_status(clientTls12Ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + ExpectIntEQ(wolfSSL_get_early_data_status(clientSsl), + WOLFSSL_EARLY_DATA_NOT_SENT); +#endif +#endif /* WOLFSSL_EARLY_DATA */ + + /* wolfSSL_get_cipher_name_by_hash(): + * hash == NULL || ssl == NULL || (ssl->suites == NULL && ssl->ctx == NULL) + * Only the first two operands are reachable: ssl->ctx is invariant non-NULL + * for a live WOLFSSL, so the parenthesised sub-expression is dead and both + * of its operands are recorded in the exclusion record. */ +#ifndef NO_PSK +#ifndef NO_WOLFSSL_CLIENT + ExpectNull(wolfSSL_get_cipher_name_by_hash(clientSsl, NULL)); + ExpectNull(wolfSSL_get_cipher_name_by_hash(NULL, "SHA256")); +#if !defined(NO_SHA256) && (defined(HAVE_AESGCM) || defined(HAVE_CHACHA)) + ExpectNotNull(wolfSSL_get_cipher_name_by_hash(clientSsl, "SHA256")); +#endif +#endif +#endif /* !NO_PSK */ + #if !defined(NO_CERTS) && defined(WOLFSSL_POST_HANDSHAKE_AUTH) ExpectIntEQ(wolfSSL_CTX_allow_post_handshake_auth(NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); diff --git a/tests/api/test_tls13_bounds.c b/tests/api/test_tls13_bounds.c new file mode 100644 index 0000000000..054ccb4f8b --- /dev/null +++ b/tests/api/test_tls13_bounds.c @@ -0,0 +1,919 @@ +/* test_tls13_bounds.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* MC/DC vectors for src/tls13.c's protocol-version and length/boundary + * decisions (ISO 26262 Part 7, Track B). + * + * Every test here drives a real handshake through the tests/utils.c memio + * transport and mutates one field of one flight, so no WOLFSSL_LOCAL symbol is + * referenced and the file links in a shared build. Each vector is written to + * complete an independence PAIR with the ordinary handshakes the tls13 group + * already runs: for a decision "A && B" the ordinary handshake supplies the + * (A=1, B=0, D=0) row, so the vector here has to supply either (A=1, B=1, D=1) + * or (A=0, D=0) -- a rejection on its own proves nothing. + */ + +#include + +#ifdef NO_INLINE + #include +#else + #define WOLFSSL_MISC_INCLUDED + #include +#endif + +#include +#include +#include +#include +#include + +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) + +#define TLS13B_CH_BUF_SZ 4096 +#define TLS13B_REC_HDR_SZ 5 +#define TLS13B_HS_HDR_SZ 4 +/* record header (5) + handshake header (4) = offset of legacy_version */ +#define TLS13B_LEGACY_OFF (TLS13B_REC_HDR_SZ + TLS13B_HS_HDR_SZ) + +/* Copy the first complete TLS record out of a memio direction buffer. + * + * dir 0 is the server's inbox (client -> server), dir 1 the client's inbox. + * A single memio "message" can hold several concatenated records (the server + * writes ServerHello + CCS + the first encrypted flight in one go), so the + * record is sliced out using its own 2-byte length field rather than the + * message size. + * + * Returns 0 on success. */ +static int test_tls13b_take_record(struct test_memio_ctx* ctx, int dir, + byte* out, int out_cap, int* out_sz) +{ + const char* msg = NULL; + int msg_sz = 0; + int rec_sz; + + if (test_memio_get_message(ctx, dir, &msg, &msg_sz, 0) != 0) + return -1; + if (msg_sz < TLS13B_REC_HDR_SZ) + return -1; + rec_sz = TLS13B_REC_HDR_SZ + + (((int)(byte)msg[3] << 8) | (int)(byte)msg[4]); + if (rec_sz > msg_sz || rec_sz > out_cap) + return -1; + + XMEMCPY(out, msg, (size_t)rec_sz); + *out_sz = rec_sz; + return 0; +} + +/* Overwrite the legacy_version of the handshake message carried by a + * plaintext handshake record. hs_type guards against patching the wrong + * message. Returns 0 on success. */ +static int test_tls13b_set_legacy_version(byte* rec, int rec_sz, byte hs_type, + byte major, byte minor) +{ + if (rec_sz < TLS13B_LEGACY_OFF + 2) + return -1; + if (rec[0] != handshake) + return -1; + if (rec[TLS13B_REC_HDR_SZ] != hs_type) + return -1; + + rec[TLS13B_LEGACY_OFF] = major; + rec[TLS13B_LEGACY_OFF + 1] = minor; + return 0; +} + +/* Drive a TLS 1.3 client far enough to emit its ClientHello, rewrite the + * ClientHello's legacy_version to major.minor, replay it to a TLS 1.3-only + * server and require the given error. + * + * All three callers land in DoTls13ClientHello()'s legacy-version block + * (tls13.c ~7620-7665). */ +static int test_tls13b_ch_legacy_version(byte major, byte minor, int expErr) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + byte ch[TLS13B_CH_BUF_SZ]; + int ch_sz = 0; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* Client emits the ClientHello and then blocks on the reply. */ + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), + WOLFSSL_ERROR_WANT_READ); + + ExpectIntEQ(test_tls13b_take_record(&test_ctx, 0, ch, (int)sizeof(ch), + &ch_sz), 0); + ExpectIntEQ(test_tls13b_set_legacy_version(ch, ch_sz, client_hello, + major, minor), 0); + + test_memio_clear_buffer(&test_ctx, 0); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(test_memio_inject_message(&test_ctx, 0, (const char*)ch, + ch_sz), 0); + } + + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), + expErr); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} + +/* Run a TLS 1.3 handshake up to the server's first flight, rewrite the + * ServerHello's legacy_version to major.minor, feed the client that single + * record and require the given error. + * + * Lands in DoTls13ServerHello()'s downgrade block (tls13.c ~5432-5470). */ +static int test_tls13b_sh_legacy_version(byte major, byte minor, int expErr) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + byte sh[TLS13B_CH_BUF_SZ]; + int sh_sz = 0; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), + WOLFSSL_ERROR_WANT_READ); + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), + WOLFSSL_ERROR_WANT_READ); + + ExpectIntEQ(test_tls13b_take_record(&test_ctx, 1, sh, (int)sizeof(sh), + &sh_sz), 0); + ExpectIntEQ(test_tls13b_set_legacy_version(sh, sh_sz, server_hello, + major, minor), 0); + + /* Drop the rest of the server flight: only the tampered ServerHello is + * replayed, so the client cannot get past the version check. */ + test_memio_clear_buffer(&test_ctx, 1); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(test_memio_inject_message(&test_ctx, 1, (const char*)sh, + sh_sz), 0); + } + + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), + expErr); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} +/* --- WANT_WRITE resumption harness ----------------------------------------- + * + * SendTls13Certificate() only fragments (and only takes its ssl->fragOffset + * resume path) when a send is interrupted part way through a multi-record + * Certificate. test_memio's own simulate_want_write is all-or-nothing from the + * start of the flight, so instead a counting send callback is layered over + * test_memio_write_cb: write number tls13b_ww_at fails with WANT_WRITE once, + * every other write goes through. Sweeping tls13b_ww_at across the whole + * flight interrupts each record in turn, and the handshake is still required + * to complete, so the resume paths are exercised without a rejection vector. + */ +static int tls13b_ww_at = -1; +static int tls13b_ww_n = 0; + +static int test_tls13b_send_cb(WOLFSSL* ssl, char* buf, int sz, void* ctx) +{ + if (tls13b_ww_n++ == tls13b_ww_at) + return WOLFSSL_CBIO_ERR_WANT_WRITE; + return test_memio_write_cb(ssl, buf, sz, ctx); +} + +/* Build a memio pair whose server presents a certificate CHAIN (the default + * test_memio_setup loads the leaf only, which leaves certChainSz == 0 and the + * whole chain-walk block in SendTls13Certificate unreachable). + * + * The pre-made ctx_s makes test_memio_setup_ex skip its own certificate load + * AND its IO callback installation, so both are done here. */ +static int test_tls13b_setup_chain(struct test_memio_ctx* tc, + WOLFSSL_CTX** ctx_c, WOLFSSL_CTX** ctx_s, WOLFSSL** ssl_c, WOLFSSL** ssl_s, + int wantWriteSide) +{ + *ctx_s = wolfSSL_CTX_new(wolfTLSv1_3_server_method()); + if (*ctx_s == NULL) + return -1; + if (wolfSSL_CTX_use_PrivateKey_file(*ctx_s, svrKeyFile, CERT_FILETYPE) + != WOLFSSL_SUCCESS) + return -1; + if (wolfSSL_CTX_use_certificate_chain_file(*ctx_s, svrCertFile) + != WOLFSSL_SUCCESS) + return -1; + if (wolfSSL_CTX_load_verify_locations(*ctx_s, caCertFile, 0) + != WOLFSSL_SUCCESS) + return -1; + wolfSSL_SetIORecv(*ctx_s, test_memio_read_cb); + wolfSSL_SetIOSend(*ctx_s, wantWriteSide == 0 ? test_tls13b_send_cb + : test_memio_write_cb); + + if (test_memio_setup_ex(tc, ctx_c, ctx_s, ssl_c, ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method, + NULL, 0, NULL, 0, NULL, 0) != 0) + return -1; + + /* Per-SSL, not per-CTX: wolfSSL_new() has already copied ctx_c's + * callbacks into ssl_c by this point, so setting it on the CTX would be + * a no-op for this connection. */ + if (wantWriteSide == 1) + wolfSSL_SSLSetIOSend(*ssl_c, test_tls13b_send_cb); + return 0; +} + +/* One handshake with the server's write number 'at' interrupted by + * WANT_WRITE. Small max_fragment_length forces the Certificate across several + * records so at least one 'at' lands inside it. */ +static int test_tls13b_server_frag_round(int at) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + tls13b_ww_at = at; + tls13b_ww_n = 0; + + ExpectIntEQ(test_tls13b_setup_chain(&test_ctx, &ctx_c, &ctx_s, &ssl_c, + &ssl_s, 0), 0); +#ifdef HAVE_MAX_FRAGMENT + ExpectIntEQ(wolfSSL_UseMaxFragment(ssl_c, WOLFSSL_MFL_2_9), + WOLFSSL_SUCCESS); +#endif + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 64, NULL), 0); + + tls13b_ww_at = -1; + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} + +/* Same, with the client holding a certificate chain and the interrupt on the + * client's writes, so the client-side resume path in wolfSSL_connect_TLSv13() + * runs too. */ +static int test_tls13b_client_frag_round(int at) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + tls13b_ww_at = at; + tls13b_ww_n = 0; + + ExpectIntEQ(test_tls13b_setup_chain(&test_ctx, &ctx_c, &ctx_s, &ssl_c, + &ssl_s, 1), 0); + /* Ask for the client's certificate so the client also fragments one. + * client-cert.pem is its own issuer, so it has to be added to the + * server's store for the verify to succeed. */ + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, 0), + WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_PEER, NULL); + ExpectIntEQ(wolfSSL_use_certificate_chain_file(ssl_c, cliCertFile), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_file(ssl_c, cliKeyFile, CERT_FILETYPE), + WOLFSSL_SUCCESS); +#ifdef HAVE_MAX_FRAGMENT + ExpectIntEQ(wolfSSL_UseMaxFragment(ssl_c, WOLFSSL_MFL_2_9), + WOLFSSL_SUCCESS); +#endif + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 64, NULL), 0); + + tls13b_ww_at = -1; + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} +/* Rewrite a ServerHello record so it carries a present-but-EMPTY extensions + * block. That is the only shape that reaches DoTls13ServerHello()'s + * post-negotiation code with args->totalExtSz == 0: a ServerHello with no + * extensions FIELD at all returns earlier, at the truncated-length branch. + * Returns the new record length. */ +static int test_tls13b_sh_empty_exts(byte* rec, int rec_sz) +{ + int off, sessIdSz, bodySz; + + if (rec_sz < TLS13B_LEGACY_OFF + 2 + RAN_LEN + 1) + return -1; + if (rec[0] != handshake || rec[TLS13B_REC_HDR_SZ] != server_hello) + return -1; + + off = TLS13B_LEGACY_OFF + OPAQUE16_LEN + RAN_LEN; + sessIdSz = rec[off]; + off += 1 + sessIdSz; + off += OPAQUE16_LEN + OPAQUE8_LEN; /* cipher suite + compression */ + if (off + OPAQUE16_LEN > rec_sz) + return -1; + + rec[off] = 0; + rec[off + 1] = 0; + off += OPAQUE16_LEN; + + bodySz = off - (TLS13B_REC_HDR_SZ + TLS13B_HS_HDR_SZ); + rec[6] = (byte)(bodySz >> 16); + rec[7] = (byte)(bodySz >> 8); + rec[8] = (byte)bodySz; + rec[3] = (byte)((off - TLS13B_REC_HDR_SZ) >> 8); + rec[4] = (byte)(off - TLS13B_REC_HDR_SZ); + return off; +} + +/* Find the first extension of the given type in a ClientHello record and + * return the offset of its body, or -1. */ +static int test_tls13b_ch_find_ext(const byte* rec, int rec_sz, word16 type, + int* bodySz) +{ + int off, sessIdSz, suiteSz, compSz, extEnd; + int extTotal; + + if (rec_sz < TLS13B_LEGACY_OFF + 2 + RAN_LEN + 1) + return -1; + if (rec[0] != handshake || rec[TLS13B_REC_HDR_SZ] != client_hello) + return -1; + + off = TLS13B_LEGACY_OFF + OPAQUE16_LEN + RAN_LEN; + sessIdSz = rec[off]; + off += 1 + sessIdSz; + if (off + OPAQUE16_LEN > rec_sz) + return -1; + suiteSz = ((int)rec[off] << 8) | rec[off + 1]; + off += OPAQUE16_LEN + suiteSz; + if (off + 1 > rec_sz) + return -1; + compSz = rec[off]; + off += 1 + compSz; + if (off + OPAQUE16_LEN > rec_sz) + return -1; + extTotal = ((int)rec[off] << 8) | rec[off + 1]; + off += OPAQUE16_LEN; + extEnd = off + extTotal; + if (extEnd > rec_sz) + return -1; + + while (off + 4 <= extEnd) { + word16 t = (word16)(((word16)rec[off] << 8) | rec[off + 1]); + int l = ((int)rec[off + 2] << 8) | rec[off + 3]; + if (off + 4 + l > extEnd) + return -1; + if (t == type) { + *bodySz = l; + return off + 4; + } + off += 4 + l; + } + return -1; +} +/* A mutually authenticated TLS 1.3 handshake with a chosen key type on both + * ends. DoTls13CertificateVerify()'s peer-key / peerSigAlgo dispatch has one + * arm per algorithm and the group only ever ran the RSA one, so each arm's + * operands sat on a single row. */ +/* The ECC, Ed25519 and Ed448 client certificates in certs/ are self-signed, + * so cliCa is the client certificate itself rather than a separate CA. */ +static int test_tls13b_mutual_auth_round(const char* srvCa, + const char* srvCert, const char* srvKey, + const char* cliCa, const char* cliCert, const char* cliKey) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + ExpectNotNull(ctx_c = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_c, srvCa, 0), + WOLFSSL_SUCCESS); + ExpectNotNull(ctx_s = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_use_certificate_chain_file(ctx_s, srvCert), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_s, srvKey, CERT_FILETYPE), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCa, 0), + WOLFSSL_SUCCESS); + if (EXPECT_SUCCESS()) { + wolfSSL_CTX_set_verify(ctx_s, WOLFSSL_VERIFY_PEER | + WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + /* Pre-made CTXs make test_memio_setup_ex skip its own IO callback + * install as well as its cert load. */ + wolfSSL_SetIORecv(ctx_c, test_memio_read_cb); + wolfSSL_SetIOSend(ctx_c, test_memio_write_cb); + wolfSSL_SetIORecv(ctx_s, test_memio_read_cb); + wolfSSL_SetIOSend(ctx_s, test_memio_write_cb); + } + + ExpectIntEQ(test_memio_setup_ex(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method, + NULL, 0, NULL, 0, NULL, 0), 0); + ExpectIntEQ(wolfSSL_use_certificate_chain_file(ssl_c, cliCert), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_file(ssl_c, cliKey, CERT_FILETYPE), + WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 16, NULL), 0); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} +#endif /* guards */ + +/* tls13.c:7645 - "args->pv.major == SSLv3_MAJOR && args->pv.minor >= + * TLSv1_3_MINOR". A legacy_version of 0x0304 makes BOTH operands true, which + * is the decision-true row RFC 8446 4.2.1 lets a server abort on. It is the + * accepting partner for cond 0 (paired with the 0x0403 vector below) and for + * cond 1 (paired with every ordinary ClientHello, whose 0x0303 makes cond 1 + * false). */ +int test_tls13_ch_legacy_version_is_tls13(void) +{ +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) && \ + !defined(WOLFSSL_ALLOW_BAD_TLS_LEGACY_VERSION) + return test_tls13b_ch_legacy_version(SSLv3_MAJOR, TLSv1_3_MINOR, + WC_NO_ERR_TRACE(VERSION_ERROR)); +#else + return TEST_SKIPPED; +#endif +} + +/* tls13.c:7645 cond 0 false row, and tls13.c:7652 cond 0 true row. + * A legacy_version major of 4 is > SSLv3_MAJOR, so :7645 short-circuits false + * on its first operand and :7652's first operand is true on its own. The + * server has downgrade disabled, so the forced downgrade is refused with + * VERSION_ERROR. */ +int test_tls13_ch_legacy_version_major_above_ssl3(void) +{ +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) + return test_tls13b_ch_legacy_version(SSLv3_MAJOR + 1, TLSv1_2_MINOR, + WC_NO_ERR_TRACE(VERSION_ERROR)); +#else + return TEST_SKIPPED; +#endif +} + +/* tls13.c:7660 cond 1 true row - "args->pv.minor < TLSv1_2_MINOR" in the + * else-if that catches a pre-TLS1.2 legacy_version. 0x0301 (TLS 1.0) falls + * through :7645 and :7652 and makes this operand true; the ordinary + * ClientHello's 0x0303 is the false row. */ +int test_tls13_ch_legacy_version_below_tls12(void) +{ +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) + return test_tls13b_ch_legacy_version(SSLv3_MAJOR, TLSv1_MINOR, + WC_NO_ERR_TRACE(VERSION_ERROR)); +#else + return TEST_SKIPPED; +#endif +} + +/* tls13.c:5436 - "args->pv.major == ssl->version.major && args->pv.minor < + * TLSv1_2_MINOR" in DoTls13ServerHello(). A ServerHello legacy_version of + * 0x0302 makes both operands true (the decision-true row that cond 0 and + * cond 1 both need); the client has downgrade disabled so it then fails + * :5466 cond 1 ("args->pv.minor != tls12minor"), whose false row every + * ordinary handshake supplies. */ +int test_tls13_sh_legacy_version_below_tls12(void) +{ +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) + return test_tls13b_sh_legacy_version(SSLv3_MAJOR, TLSv1_1_MINOR, + WC_NO_ERR_TRACE(VERSION_ERROR)); +#else + return TEST_SKIPPED; +#endif +} + +/* tls13.c:5436 cond 0 false row and :5466 cond 0 true row. A ServerHello + * legacy_version major of 4 differs from the client's, so :5436 + * short-circuits false without evaluating its second operand and :5466's + * first operand is true on its own. */ +int test_tls13_sh_legacy_version_major_mismatch(void) +{ +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) + return test_tls13b_sh_legacy_version(SSLv3_MAJOR + 1, TLSv1_2_MINOR, + WC_NO_ERR_TRACE(VERSION_ERROR)); +#else + return TEST_SKIPPED; +#endif +} + +/* Interrupt every write of the server's flight in turn with WANT_WRITE while + * it sends a fragmented Certificate built from a real chain. + * + * Drives SendTls13Certificate()'s resume block: :10019 (certChainSz > 0 && + * fragOffset >= certSz + extSz[0]), :10061 cond 1 (the fragment loop leaving + * with length still > 0 because SendBuffered() answered WANT_WRITE, which is + * the row an uninterrupted send can never produce), :10143, :10156, :10187 and + * :9655, plus wolfSSL_accept_TLSv13()'s :16693 buffered-fragment resume. + * Every round must still complete the handshake, so each vector has its + * accepting partner in the same run. */ +int test_tls13_server_cert_fragment_want_write(void) +{ +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + int at; + + for (at = 0; at < 16 && EXPECT_SUCCESS(); at++) + ExpectIntEQ(test_tls13b_server_frag_round(at), TEST_SUCCESS); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* Client-side counterpart: the client sends its own fragmented Certificate + * and its writes are interrupted in turn, which drives + * wolfSSL_connect_TLSv13()'s :15347 buffered-fragment resume. */ +int test_tls13_client_cert_fragment_want_write(void) +{ +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + int at; + + for (at = 0; at < 12 && EXPECT_SUCCESS(); at++) + ExpectIntEQ(test_tls13b_client_frag_round(at), TEST_SUCCESS); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* tls13.c:5719 cond 0 - "args->totalExtSz > 0" in DoTls13ServerHello(). + * Every ordinary ServerHello supplies the (both operands true, decision true) + * row; this vector supplies the missing (cond 0 false) row by handing a + * downgrade-capable client a ServerHello whose extensions block is present but + * empty. The client then takes the !foundVersion downgrade path and arrives at + * :5719 with totalExtSz == 0. The handshake cannot complete (no key_share), so + * only the failure is asserted. */ +int test_tls13_sh_empty_extensions_block(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + byte sh[TLS13B_CH_BUF_SZ]; + int sh_sz = 0; + int new_sz = -1; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + /* SSLv23 client: downgrade enabled, which the !foundVersion branch needs. */ + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfSSLv23_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_tls13b_take_record(&test_ctx, 1, sh, (int)sizeof(sh), + &sh_sz), 0); + if (EXPECT_SUCCESS()) + new_sz = test_tls13b_sh_empty_exts(sh, sh_sz); + ExpectIntGT(new_sz, 0); + + test_memio_clear_buffer(&test_ctx, 1); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(test_memio_inject_message(&test_ctx, 1, (const char*)sh, + new_sz), 0); + } + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* tls13.c:7425 cond 1 - "!IsAtLeastTLSv1_3(ssl->version)" in + * DoTls13SupportedVersions(). Ordinary handshakes give the (foundVersion set, + * version still 1.3, decision false) row. This vector rewrites the single + * version in the ClientHello's supported_versions extension from 0x0304 to + * 0x0303 - same length, so no other field moves - and hands it to a + * downgrade-capable server, which negotiates TLS 1.2 and makes the operand + * true with foundVersion still set. */ +int test_tls13_ch_supported_versions_tls12_only(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + byte ch[TLS13B_CH_BUF_SZ]; + int ch_sz = 0; + int svOff = -1, svSz = 0, i; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfSSLv23_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(test_tls13b_take_record(&test_ctx, 0, ch, (int)sizeof(ch), + &ch_sz), 0); + if (EXPECT_SUCCESS()) + svOff = test_tls13b_ch_find_ext(ch, ch_sz, TLSX_SUPPORTED_VERSIONS, + &svSz); + ExpectIntGT(svOff, 0); + /* body is: 1-byte list length, then pairs */ + ExpectIntGE(svSz, 3); + if (EXPECT_SUCCESS()) { + for (i = svOff + 1; i + 1 < svOff + svSz; i += 2) { + if (ch[i] == SSLv3_MAJOR && ch[i + 1] == TLSv1_3_MINOR) + ch[i + 1] = TLSv1_2_MINOR; + } + } + + test_memio_clear_buffer(&test_ctx, 0); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(test_memio_inject_message(&test_ctx, 0, (const char*)ch, + ch_sz), 0); + } + /* The server downgrades; the TLS 1.3-only client cannot follow, so the + * handshake must not complete. */ + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* A complete Encrypted ClientHello handshake in the tls13 group. The group's + * existing tests never build an ECHConfig, so every ssl->ctx->echConfigs and + * echX operand in DoTls13ClientHello(), DoTls13HandShakeMsgType() and + * SendTls13ClientHello() sits on its "no ECH" row only. This supplies the + * accepting row for all of them; the ordinary handshakes supply the other. */ +int test_tls13_ech_accepted_handshake(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(HAVE_SNI) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + static const char pubName[] = "ech-public-name.com"; + static const char privName[] = "ech-private-name.com"; + byte configs[512]; + word32 configsLen = (word32)sizeof(configs); + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + + /* The handshake reads ssl->ctx->echConfigs, so generating on the CTX + * after wolfSSL_new() is still in time for this connection. */ + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(ctx_s, pubName, 0, 0, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(ctx_s, configs, &configsLen), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_SetEchConfigs(ssl_c, configs, configsLen), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, privName, + (word16)XSTRLEN(privName)), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_s, WOLFSSL_SNI_HOST_NAME, privName, + (word16)XSTRLEN(privName)), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 16, NULL), 0); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* Same handshake with the ECHConfig's public key corrupted, so the server + * cannot open the outer ClientHello and answers with retry_configs. That is + * the ECH-rejected arm of the same decisions - notably EchCheckAcceptance() + * and the acceptance-confirmation compare in DoTls13HandShakeMsgType(). */ +int test_tls13_ech_rejected_handshake(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(HAVE_SNI) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + static const char pubName[] = "ech-public-name.com"; + static const char privName[] = "ech-private-name.com"; + byte configs[512]; + word32 configsLen = (word32)sizeof(configs); + word16 idx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(ctx_s, pubName, 0, 0, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(ctx_s, configs, &configsLen), + WOLFSSL_SUCCESS); + if (EXPECT_SUCCESS()) { + /* skip list length, version, length, config id, kem id and public + * key length to land on the first byte of the public key */ + idx = OPAQUE16_LEN + OPAQUE16_LEN + OPAQUE16_LEN + OPAQUE8_LEN + + OPAQUE16_LEN + OPAQUE16_LEN; + ExpectIntLT((word32)idx, configsLen); + if (EXPECT_SUCCESS()) + configs[idx] ^= 0xFF; + } + ExpectIntEQ(wolfSSL_SetEchConfigs(ssl_c, configs, configsLen), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, privName, + (word16)XSTRLEN(privName)), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_s, WOLFSSL_SNI_HOST_NAME, pubName, + (word16)XSTRLEN(pubName)), WOLFSSL_SUCCESS); + + /* The server cannot open the outer ClientHello, so it completes the + * handshake against the public name and returns retry_configs; RFC 9849 + * 6.1.7 then makes the client abort with ECH_REQUIRED_E. Both sides run + * their full ECH code paths first, which is the point of the vector. */ + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), + WC_NO_ERR_TRACE(ECH_REQUIRED_E)); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* ECDSA on both ends: drives DoTls13CertificateVerify()'s + * peerEccDsaKey / ecc_dsa_sa_algo arms. */ +int test_tls13_mutual_auth_ecdsa(void) +{ +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECC) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + return test_tls13b_mutual_auth_round(caEccCertFile, eccCertFile, + eccKeyFile, cliEccCertFile, cliEccCertFile, cliEccKeyFile); +#else + return TEST_SKIPPED; +#endif +} + +/* Ed25519 on both ends: drives the peerEd25519Key / ed25519_sa_algo arms. */ +int test_tls13_mutual_auth_ed25519(void) +{ +#if defined(WOLFSSL_TLS13) && defined(HAVE_ED25519) && \ + defined(HAVE_ED25519_SIGN) && defined(HAVE_ED25519_VERIFY) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_ED25519_CLIENT_AUTH) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + return test_tls13b_mutual_auth_round(caEdCertFile, edCertFile, + edKeyFile, cliEdCertFile, cliEdCertFile, cliEdKeyFile); +#else + return TEST_SKIPPED; +#endif +} + +/* Ed448 on both ends: drives the peerEd448Key / ed448_sa_algo arms. */ +int test_tls13_mutual_auth_ed448(void) +{ +#if defined(WOLFSSL_TLS13) && defined(HAVE_ED448) && \ + defined(HAVE_ED448_SIGN) && defined(HAVE_ED448_VERIFY) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_ED448_CLIENT_AUTH) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + return test_tls13b_mutual_auth_round(caEd448CertFile, ed448CertFile, + ed448KeyFile, cliEd448CertFile, cliEd448CertFile, cliEd448KeyFile); +#else + return TEST_SKIPPED; +#endif +} + +/* RSA on both ends, mutually authenticated. The group already runs one-sided + * RSA handshakes; this adds the client-authenticating rows for the RSA arm of + * the same dispatch. */ +int test_tls13_mutual_auth_rsa(void) +{ +#if defined(WOLFSSL_TLS13) && !defined(NO_RSA) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + return test_tls13b_mutual_auth_round(caCertFile, svrCertFile, + svrKeyFile, cliCertFile, cliCertFile, cliKeyFile); +#else + return TEST_SKIPPED; +#endif +} diff --git a/tests/api/test_tls13_bounds.h b/tests/api/test_tls13_bounds.h new file mode 100644 index 0000000000..8f086607fa --- /dev/null +++ b/tests/api/test_tls13_bounds.h @@ -0,0 +1,60 @@ +/* test_tls13_bounds.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_TLS13_BOUNDS_H +#define WOLFCRYPT_TEST_TLS13_BOUNDS_H + +#include + +int test_tls13_ch_legacy_version_is_tls13(void); +int test_tls13_ch_legacy_version_major_above_ssl3(void); +int test_tls13_ch_legacy_version_below_tls12(void); +int test_tls13_sh_legacy_version_below_tls12(void); +int test_tls13_sh_legacy_version_major_mismatch(void); +int test_tls13_server_cert_fragment_want_write(void); +int test_tls13_client_cert_fragment_want_write(void); +int test_tls13_sh_empty_extensions_block(void); +int test_tls13_ch_supported_versions_tls12_only(void); +int test_tls13_ech_accepted_handshake(void); +int test_tls13_ech_rejected_handshake(void); +int test_tls13_mutual_auth_ecdsa(void); +int test_tls13_mutual_auth_ed25519(void); +int test_tls13_mutual_auth_ed448(void); +int test_tls13_mutual_auth_rsa(void); + +#define TEST_TLS13_BOUNDS_DECLS \ + TEST_DECL_GROUP("tls13", test_tls13_ch_legacy_version_is_tls13), \ + TEST_DECL_GROUP("tls13", test_tls13_ch_legacy_version_major_above_ssl3),\ + TEST_DECL_GROUP("tls13", test_tls13_ch_legacy_version_below_tls12), \ + TEST_DECL_GROUP("tls13", test_tls13_sh_legacy_version_below_tls12), \ + TEST_DECL_GROUP("tls13", test_tls13_sh_legacy_version_major_mismatch), \ + TEST_DECL_GROUP("tls13", test_tls13_server_cert_fragment_want_write), \ + TEST_DECL_GROUP("tls13", test_tls13_client_cert_fragment_want_write), \ + TEST_DECL_GROUP("tls13", test_tls13_sh_empty_extensions_block), \ + TEST_DECL_GROUP("tls13", test_tls13_ch_supported_versions_tls12_only), \ + TEST_DECL_GROUP("tls13", test_tls13_ech_accepted_handshake), \ + TEST_DECL_GROUP("tls13", test_tls13_ech_rejected_handshake), \ + TEST_DECL_GROUP("tls13", test_tls13_mutual_auth_ecdsa), \ + TEST_DECL_GROUP("tls13", test_tls13_mutual_auth_ed25519), \ + TEST_DECL_GROUP("tls13", test_tls13_mutual_auth_ed448), \ + TEST_DECL_GROUP("tls13", test_tls13_mutual_auth_rsa) + +#endif /* WOLFCRYPT_TEST_TLS13_BOUNDS_H */ diff --git a/tests/api/test_tls13_features.c b/tests/api/test_tls13_features.c new file mode 100644 index 0000000000..9e10aa844b --- /dev/null +++ b/tests/api/test_tls13_features.c @@ -0,0 +1,1251 @@ +/* test_tls13_features.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* MC/DC vectors for src/tls13.c's FEATURE-FLAG decisions (ISO 26262 Part 7, + * Track B): the rows that test ssl->options.* state, extension presence and + * PSK / ticket / early-data negotiation results. + * + * An ssl->options.X operand that is stuck on one row usually means no test + * ever turns X on -- a fixture gap, not a hard condition. Each test here + * negotiates a feature combination the tls13 group did not previously + * negotiate at all, and is paired with the ordinary handshakes that group + * already runs so that both rows of the independence pair exist in the same + * unit.test binary. + * + * Everything is driven over the tests/utils.c memio transport. The only + * non-WOLFSSL_API symbol used is BuildTls13Message(), which is declared + * WOLFSSL_TEST_VIS (exported for tests) and is already used the same way by + * test_tls13_zero_inner_content_type(), so this file links in a shared build. + */ + +#include + +#ifdef NO_INLINE + #include +#else + #define WOLFSSL_MISC_INCLUDED + #include +#endif + +#include +#include +#include +#include +#include + +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + +/* ------------------------------------------------------------------------- + * Optional client authentication: the server asks for a certificate but does + * not insist on one, and the client has none configured. + * ---------------------------------------------------------------------- */ + +/* wolfSSL_accept_TLSv13() TLS13_ACCEPT_FINISHED_DONE: + * + * if (!resuming && verifyPeer && !verifyPostHandshake && + * !havePeerCert && !failNoCert) + * peerAuthGood = 1; + * + * needs a server that set WOLFSSL_VERIFY_PEER *without* + * WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT and a client that answers the + * CertificateRequest with an empty Certificate. The group's other + * client-auth handshakes all supply a certificate (havePeerCert = 1), so + * this decision had never been true. + * + * The same handshake leaves the server with + * msgsReceived.got_certificate = 1 and got_certificate_verify = 0, which is + * the state test_tls13_feat_post_handshake_unexpected_msg() below needs. */ +int test_tls13_feat_optional_client_cert(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* Request a client certificate but accept the handshake without one. */ + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_PEER, NULL); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* The client had no certificate to send, so the server saw an empty + * Certificate message and no CertificateVerify, and still accepted. */ + ExpectIntEQ(ssl_s->msgsReceived.got_certificate, 1); + ExpectIntEQ(ssl_s->msgsReceived.got_certificate_verify, 0); + ExpectIntEQ(ssl_s->options.havePeerCert, 0); + ExpectIntEQ(ssl_s->options.peerAuthGood, 1); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* ------------------------------------------------------------------------- + * A handshake message of a type that is not legal after the handshake. + * ---------------------------------------------------------------------- */ + +/* DoTls13HandShakeMsgType(): + * + * if (handShakeState == HANDSHAKE_DONE && + * type != session_ticket && type != certificate_request && + * type != certificate && type != key_update && type != finished + * && type != request_connection_id && type != new_connection_id) + * + * The group already produces every "false" row of this chain: a client + * receiving a post-handshake NewSessionTicket, a post-handshake auth + * CertificateRequest / Certificate / Finished, a KeyUpdate, and the DTLS + * connection-id messages. What no test ever produced is the row where the + * whole chain is TRUE -- a post-handshake handshake message of some other + * type -- so none of the eight operands had a pair. + * + * CertificateVerify is the type that reaches the check: on a server, + * SanityCheckTls13MsgReceived() lets it through once the server has sent + * its Finished, has seen a ClientHello and a Certificate, and has not yet + * seen a CertificateVerify. Optional client authentication (the test above) + * leaves exactly that state. + * + * The message is built with the client's own application-data keys via + * BuildTls13Message() and handed to the server with wolfSSL_inject(), so + * this is a real record on the real connection, not a poke at internals. */ +int test_tls13_feat_post_handshake_unexpected_msg(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + WOLFSSL_ALERT_HISTORY h; + /* handshake header for a 4-byte CertificateVerify body, plus a body the + * server never gets as far as parsing */ + byte hsMsg[8] = { certificate_verify, 0x00, 0x00, 0x04, + 0x08, 0x04, 0x00, 0x00 }; + byte record[128]; + char readBuf[16]; + int recordSz = 0; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(&h, 0, sizeof(h)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* Optional client auth: server sees an empty Certificate and no + * CertificateVerify, so a later CertificateVerify is not a duplicate. */ + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_PEER, NULL); + /* Keep the post-handshake flight to just our injected message. */ + ExpectIntEQ(wolfSSL_no_ticket_TLSv13(ssl_s), 0); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(ssl_s->options.handShakeState, HANDSHAKE_DONE); + ExpectIntEQ(ssl_s->msgsReceived.got_certificate, 1); + ExpectIntEQ(ssl_s->msgsReceived.got_certificate_verify, 0); + + if (EXPECT_SUCCESS()) { + recordSz = BuildTls13Message(ssl_c, record, (int)sizeof(record), hsMsg, + (int)sizeof(hsMsg), handshake, 0, 0, 0); + ExpectIntGT(recordSz, 0); + } + if (EXPECT_SUCCESS()) { + ExpectIntEQ(wolfSSL_inject(ssl_s, record, recordSz), + WOLFSSL_SUCCESS); + } + + /* RFC 8446 Section 4.6: an unexpected handshake message after the + * handshake is a fatal unexpected_message alert. */ + ExpectIntEQ(wolfSSL_read(ssl_s, readBuf, (int)sizeof(readBuf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WC_NO_ERR_TRACE(OUT_OF_ORDER_E)); + ExpectIntEQ(wolfSSL_get_alert_history(ssl_s, &h), WOLFSSL_SUCCESS); + ExpectIntEQ(h.last_tx.code, unexpected_message); + ExpectIntEQ(h.last_tx.level, alert_fatal); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* ------------------------------------------------------------------------- + * External PSK negotiated in psk_ke mode -- no (EC)DHE, no key_share. + * ---------------------------------------------------------------------- */ + +#if !defined(NO_PSK) + +static const byte test_tls13_feat_psk[] = { + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f +}; +static const char test_tls13_feat_psk_id[] = "feat_psk_client"; + +static unsigned int test_tls13_feat_psk_client_cb(WOLFSSL* ssl, + const char* hint, char* identity, unsigned int id_max_len, + unsigned char* key, unsigned int key_max_len) +{ + (void)ssl; + (void)hint; + if (id_max_len <= XSTRLEN(test_tls13_feat_psk_id) || + key_max_len < sizeof(test_tls13_feat_psk)) + return 0; + XSTRNCPY(identity, test_tls13_feat_psk_id, id_max_len); + XMEMCPY(key, test_tls13_feat_psk, sizeof(test_tls13_feat_psk)); + return (unsigned int)sizeof(test_tls13_feat_psk); +} + +static unsigned int test_tls13_feat_psk_server_cb(WOLFSSL* ssl, + const char* id, unsigned char* key, unsigned int key_max_len) +{ + (void)ssl; + if (id == NULL || key_max_len < sizeof(test_tls13_feat_psk)) + return 0; + if (XSTRCMP(id, test_tls13_feat_psk_id) != 0) + return 0; + XMEMCPY(key, test_tls13_feat_psk, sizeof(test_tls13_feat_psk)); + return (unsigned int)sizeof(test_tls13_feat_psk); +} + +#endif /* !NO_PSK */ + +/* SetupPskKey() and CheckPreSharedKeys() both branch on whether the peer + * offered psk_dhe_ke and on whether a key_share entry is present: + * + * if (((modes & (1 << PSK_DHE_KE)) != 0) && !noPskDheKe && + * kse != NULL && kse->derived) [SetupPskKey] + * if (((modes & (1 << PSK_DHE_KE)) != 0 && !noPskDheKe && ext != NULL) + * || usingCertWithExternPsk) [CheckPreSharedKeys] + * + * Every PSK handshake in the group runs psk_dhe_ke, so the kse == NULL / + * ext == NULL rows were unreachable. wolfSSL_no_dhe_psk() on both ends + * negotiates plain psk_ke: no key_share is offered and preMasterSz is + * zeroed. The group's existing psk_dhe_ke handshakes are the partner row. */ +int test_tls13_feat_psk_ke_no_dhe(void) +{ + EXPECT_DECLS; +#if !defined(NO_PSK) && defined(HAVE_SUPPORTED_CURVES) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + const char appMsg[] = "psk_ke"; + char readBuf[sizeof(appMsg)]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_psk_client_callback(ssl_c, test_tls13_feat_psk_client_cb); + wolfSSL_set_psk_server_callback(ssl_s, test_tls13_feat_psk_server_cb); + + /* psk_ke only: the ClientHello carries no key_share extension. */ + ExpectIntEQ(wolfSSL_no_dhe_psk(ssl_c), 0); + ExpectIntEQ(wolfSSL_no_dhe_psk(ssl_s), 0); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(ssl_c->options.pskNegotiated, 1); + ExpectIntEQ(ssl_s->options.pskNegotiated, 1); + ExpectIntEQ(ssl_c->options.noPskDheKe, 1); + ExpectIntEQ(ssl_s->options.noPskDheKe, 1); + + ExpectIntEQ(wolfSSL_write(ssl_c, appMsg, (int)XSTRLEN(appMsg)), + (int)XSTRLEN(appMsg)); + ExpectIntEQ(wolfSSL_read(ssl_s, readBuf, (int)sizeof(readBuf)), + (int)XSTRLEN(appMsg)); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* The rejecting partner of the vector above, for the + * + * else if (onlyPskDheKe || (failNoPSK && !resumption)) + * + * arms of CheckPreSharedKeys() and SetupPskKey(): a server that insists on + * forward secrecy (wolfSSL_only_dhe_psk) facing a client that offers + * psk_ke only. onlyPskDheKe had never been set on a live handshake -- the + * group only exercised it through the argument-validation API test. */ +int test_tls13_feat_psk_only_dhe_rejects_psk_ke(void) +{ + EXPECT_DECLS; +#if !defined(NO_PSK) && defined(HAVE_SUPPORTED_CURVES) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_psk_client_callback(ssl_c, test_tls13_feat_psk_client_cb); + wolfSSL_set_psk_server_callback(ssl_s, test_tls13_feat_psk_server_cb); + + ExpectIntEQ(wolfSSL_no_dhe_psk(ssl_c), 0); + ExpectIntEQ(wolfSSL_only_dhe_psk(ssl_s), 0); + + ExpectIntNE(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* wolfSSL_accept_TLSv13() TLS13_ACCEPT_FINISHED_DONE: + * + * if (!noTicketTls13 && ctx->ticketEncCb != NULL) + * SendTls13NewSessionTicket(ssl); + * + * HAVE_SESSION_TICKET installs a default ticket encryption callback, so + * ticketEncCb is non-NULL on every handshake the group runs and operand 1 + * had no false row. Clearing the callback keeps tickets enabled + * (noTicketTls13 stays 0, so operand 0 is still true) but leaves the server + * with no way to protect one, and it silently sends none. The group's + * ordinary ticketed handshakes are the partner row. */ +int test_tls13_feat_no_ticket_enc_cb(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SESSION_TICKET) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* Tickets stay enabled; there is just no callback to encrypt one. */ + ExpectIntEQ(wolfSSL_CTX_set_TicketEncCb(ctx_s, NULL), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(ssl_s->options.noTicketTls13, 0); + ExpectNull(ctx_s->ticketEncCb); + ExpectIntEQ(ssl_c->msgsReceived.got_session_ticket, 0); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + +/* ------------------------------------------------------------------------- + * A PSK ClientHello that offers psk_dhe_ke but carries no key_share entry. + * ---------------------------------------------------------------------- */ + +/* CheckPreSharedKeys(): + * + * if (((modes & (1 << PSK_DHE_KE)) != 0 && !noPskDheKe && ext != NULL) + * || usingCertWithExternPsk) + * + * and SetupPskKey(): + * + * if (((modes & (1 << PSK_DHE_KE)) != 0) && !noPskDheKe && + * kse != NULL && kse->derived) + * + * both need a client that advertises psk_dhe_ke with an EMPTY key_share + * list. wolfSSL_NoKeyShares() produces exactly that (it is how the group + * forces a HelloRetryRequest), but until now it was only ever combined with + * cert_with_extern_psk, which takes the other arm of the || and hides the + * ext == NULL row. Here it is combined with a plain external PSK, so the + * left conjunct is false while usingCertWithExternPsk is also false and the + * decision comes out false -- the partner of + * test_tls13_cert_with_extern_psk_requires_key_share(). */ +int test_tls13_feat_psk_ke_empty_key_share(void) +{ + EXPECT_DECLS; +#if !defined(NO_PSK) && defined(HAVE_SUPPORTED_CURVES) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_psk_client_callback(ssl_c, test_tls13_feat_psk_client_cb); + wolfSSL_set_psk_server_callback(ssl_s, test_tls13_feat_psk_server_cb); + + /* psk_dhe_ke is still offered; the key_share list is empty. */ + ExpectIntEQ(wolfSSL_NoKeyShares(ssl_c), WOLFSSL_SUCCESS); + + /* CH1 has no key share, so the server answers with a HelloRetryRequest + * and the PSK is only bound on CH2. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 20, NULL), 0); + ExpectIntEQ(ssl_c->options.pskNegotiated, 1); + ExpectIntEQ(ssl_s->options.pskNegotiated, 1); + ExpectIntEQ(ssl_s->msgsReceived.got_client_hello, 2); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* CheckPreSharedKeys(), on the branch taken when the ClientHello has no + * pre_shared_key extension at all: + * + * if (ssl->options.havePSK && ssl->options.failNoPSK) + * return PSK_MISSING_ERROR; + * + * havePSK is only set by installing an external-PSK callback, and every + * handshake in the group that installs one also sends a PSK, so operand 1 + * was only ever evaluated with failNoPSK set (the + * test_tls13_fail_if_no_psk_* family). This is the same server -- external + * PSK callback installed, so havePSK is true -- but with failNoPSK left off + * and a client that offers no PSK, so the server falls back to certificate + * authentication instead of refusing. */ +int test_tls13_feat_optional_psk_falls_back_to_cert(void) +{ + EXPECT_DECLS; +#if !defined(NO_PSK) && !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* Server accepts an external PSK but does not require one. The client + * has no PSK callback, so its ClientHello carries no pre_shared_key. */ + wolfSSL_set_psk_server_callback(ssl_s, test_tls13_feat_psk_server_cb); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(ssl_s->options.havePSK, 1); + ExpectIntEQ(ssl_s->options.failNoPSK, 0); + ExpectIntEQ(ssl_s->options.pskNegotiated, 0); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* ------------------------------------------------------------------------- + * Post-handshake client authentication, run to completion, with the + * status_request extension held on the CTX. + * ---------------------------------------------------------------------- */ + +/* Guard must match test_tls13_feat_pha_ctx_status_request() below exactly: these + * are its only callers, and a narrower guard leaves them defined-but-unused + * under -Werror=unused-function. */ +#if defined(WOLFSSL_POST_HANDSHAKE_AUTH) && \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST) && defined(HAVE_OCSP) && \ + defined(KEEP_PEER_CERT) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_RSA) +static int test_tls13_feat_ocsp_io_cb(void* ioCtx, const char* url, int urlSz, + unsigned char* req, int reqSz, unsigned char** resp) +{ + (void)ioCtx; + (void)url; + (void)urlSz; + (void)req; + (void)reqSz; + *resp = NULL; + return 0; +} + +static void test_tls13_feat_ocsp_free_cb(void* ioCtx, unsigned char* resp) +{ + (void)ioCtx; + (void)resp; +} +#endif + +/* SetupOcspResp(): + * + * if (extension == NULL && side == WOLFSSL_CLIENT_END && + * handShakeDone && + * TLSX_Find(ssl->ctx->extensions, TLSX_STATUS_REQUEST) != NULL) + * + * is the post-handshake-auth path where the client has to rebuild the + * status_request extension it offered in its ClientHello, because that + * extension no longer lives on ssl->extensions. The group's existing PHA + * stapling test offers status_request with wolfSSL_UseOCSPStapling() on the + * *SSL*, so ssl->ctx->extensions is empty, the last operand is false and + * the decision was never true -- leaving all four operands unpaired. + * + * This test offers it with wolfSSL_CTX_UseOCSPStapling() instead, so the + * extension is on the CTX and the rebuild actually happens, and then runs + * the post-handshake authentication all the way to the server accepting the + * client certificate. */ +int test_tls13_feat_pha_ctx_status_request(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_POST_HANDSHAKE_AUTH) && \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST) && defined(HAVE_OCSP) && \ + defined(KEEP_PEER_CERT) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + WOLFSSL_X509* peer = NULL; + const char msg[] = "ping"; + char buf[8]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + /* --- Client CTX ------------------------------------------------ */ + ExpectNotNull(ctx_c = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_c, caCertFile, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_allow_post_handshake_auth(ctx_c), 0); + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(ctx_c), WOLFSSL_SUCCESS); + /* The whole point of this vector: status_request is offered from the + * CTX, so ssl->ctx->extensions holds it and the post-handshake rebuild + * in SetupOcspResp() finds it. */ + ExpectIntEQ(wolfSSL_CTX_UseOCSPStapling(ctx_c, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + wolfSSL_SetIORecv(ctx_c, test_memio_read_cb); + wolfSSL_SetIOSend(ctx_c, test_memio_write_cb); + + /* --- Server CTX ------------------------------------------------ */ + ExpectNotNull(ctx_s = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx_s, svrCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx_s, svrKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, caCertFile, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, + "./certs/client-ca.pem", 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(ctx_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_SetOCSP_Cb(ctx_s, test_tls13_feat_ocsp_io_cb, + test_tls13_feat_ocsp_free_cb, NULL), WOLFSSL_SUCCESS); + wolfSSL_CTX_set_verify(ctx_s, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_SetIORecv(ctx_s, test_memio_read_cb); + wolfSSL_SetIOSend(ctx_s, test_memio_write_cb); + + /* --- SSL objects ----------------------------------------------- */ + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNull(wolfSSL_get_peer_certificate(ssl_s)); + + /* Trigger post-handshake authentication. */ + if (EXPECT_SUCCESS()) { + wolfSSL_set_verify(ssl_s, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(wolfSSL_request_certificate(ssl_s), WOLFSSL_SUCCESS); + } + + ExpectIntEQ(wolfSSL_write(ssl_s, msg, (int)sizeof(msg) - 1), + (int)sizeof(msg) - 1); + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf) - 1), + (int)sizeof(msg) - 1); + + /* The client's reply carries Certificate (with the rebuilt staple), + * CertificateVerify and Finished ahead of the application data. */ + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg) - 1), + (int)sizeof(msg) - 1); + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf) - 1), + (int)sizeof(msg) - 1); + + ExpectNotNull(peer = wolfSSL_get_peer_certificate(ssl_s)); + wolfSSL_X509_free(peer); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* ------------------------------------------------------------------------- + * A server that answers an external PSK with psk_ke, so the client's own + * key share is never used. + * ---------------------------------------------------------------------- */ + +/* SetupPskKey() runs on the CLIENT while it processes the ServerHello: + * + * if (((modes & (1 << PSK_DHE_KE)) != 0) && !noPskDheKe && + * kse != NULL && kse->derived) + * else if (onlyPskDheKe || (failNoPSK && !psk->resumption)) + * + * Reaching the kse rows needs a client that offered psk_dhe_ke (so the + * first two operands are true) whose key share was nevertheless not used. + * Every PSK handshake in the group had both ends agree on psk_dhe_ke, so + * kse was always a derived entry. Here only the SERVER is restricted to + * psk_ke (wolfSSL_no_dhe_psk on the server), which is a configuration the + * group never built: the client still offers psk_dhe_ke and a key share, + * the server confirms psk_ke, and the client's key share stays underived. + * + * `variant` selects what the client offered: + * 0 - a real key share (kse != NULL, kse->derived == 0) + * 1 - an empty key_share list via wolfSSL_NoKeyShares (kse == NULL) */ +static int test_tls13_feat_psk_ke_server_side(int variant) +{ + EXPECT_DECLS; +#if !defined(NO_PSK) && defined(HAVE_SUPPORTED_CURVES) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_psk_client_callback(ssl_c, test_tls13_feat_psk_client_cb); + wolfSSL_set_psk_server_callback(ssl_s, test_tls13_feat_psk_server_cb); + + /* Only the server refuses (EC)DHE with a PSK. */ + ExpectIntEQ(wolfSSL_no_dhe_psk(ssl_s), 0); + if (variant == 1) + ExpectIntEQ(wolfSSL_NoKeyShares(ssl_c), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(ssl_c->options.pskNegotiated, 1); + ExpectIntEQ(ssl_s->options.pskNegotiated, 1); + ExpectIntEQ(ssl_s->options.noPskDheKe, 1); + /* The client offered psk_dhe_ke; only the server said no. */ + ExpectIntEQ(ssl_c->options.onlyPskDheKe, 0); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#else + (void)variant; +#endif + return EXPECT_RESULT(); +} + +int test_tls13_feat_psk_ke_server_key_share_unused(void) +{ + return test_tls13_feat_psk_ke_server_side(0); +} + +int test_tls13_feat_psk_ke_server_no_key_share(void) +{ + return test_tls13_feat_psk_ke_server_side(1); +} + +/* The same else-if, one operand further along: + * + * else if (onlyPskDheKe || (failNoPSK && !psk->resumption)) + * + * test_tls13_fail_if_no_psk_client_requires_dhe() supplies the row where a + * client with a mandatory EXTERNAL PSK refuses a psk_ke ServerHello + * (failNoPSK true, !psk->resumption true, decision true). Its partner -- + * failNoPSK still true but the PSK being a session-ticket RESUMPTION, which + * RFC 8446 exempts -- never existed, because no test combined + * wolfSSL_require_psk() on the client with a resumed session. */ +int test_tls13_feat_psk_ke_client_require_psk_resumption(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SESSION_TICKET) && !defined(NO_PSK) && \ + defined(HAVE_SUPPORTED_CURVES) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + WOLFSSL_SESSION* sess = NULL; + struct test_memio_ctx test_ctx; + byte readBuf[16]; + + /* First connection: mint a session ticket. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + /* Drain the post-handshake NewSessionTicket. */ + ExpectIntEQ(wolfSSL_read(ssl_c, readBuf, sizeof(readBuf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + /* Second connection: resume with psk_ke while the CLIENT insists on a + * PSK. The resumption exemption means this must succeed. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_require_psk(ssl_c); + ExpectIntEQ(wolfSSL_no_dhe_psk(ssl_c), 0); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 20, NULL), 0); + ExpectIntEQ(ssl_c->options.resuming, 1); + ExpectIntEQ(ssl_s->options.resuming, 1); + ExpectIntEQ(ssl_c->options.failNoPSK, 1); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + +/* ------------------------------------------------------------------------- + * A TLS 1.3 HelloRetryRequest carrying a stateless cookie, accepted. + * ---------------------------------------------------------------------- */ + +/* The tail of DoTls13ClientHello() is one block guarded by + * ssl->options.sendCookie, and inside it three decisions branch on + * ssl->options.cookieGood: + * + * if (cookieGood && acceptState == TLS13_ACCEPT_FIRST_REPLY_DONE) + * if (cookieGood && serverState == SERVER_HELLO_RETRY_REQUEST_COMPLETE) + * if (!cookieGood && serverState != SERVER_HELLO_RETRY_REQUEST_COMPLETE) + * + * wolfSSL_send_hrr_cookie() had only ever been called by the argument-check + * test and by test_tls13_hrr_bad_cookie(), which corrupts the cookie so + * cookieGood never becomes 1. No test in the group had ever completed a + * TLS 1.3 handshake through an accepted cookie, so all six operands sat on + * one row. This drives both ClientHellos: CH1 with no key share (cookieGood + * still 0, the server emits the HelloRetryRequest and the cookie) and CH2 + * echoing that cookie back (cookieGood 1). */ +static int test_tls13_feat_hrr_cookie_round(int emptyKeyShare) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_SEND_HRR_COOKIE) && defined(HAVE_SUPPORTED_CURVES) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* Stateless HelloRetryRequest: the server puts a cookie in the HRR and + * validates the echo in CH2. NULL/0 lets it generate its own secret. */ + ExpectIntEQ(wolfSSL_send_hrr_cookie(ssl_s, NULL, 0), WOLFSSL_SUCCESS); + /* emptyKeyShare == 1: CH1 carries an empty key_share list, so the server + * is already committed to a HelloRetryRequest by the time the cookie + * block runs (serverState == SERVER_HELLO_RETRY_REQUEST_COMPLETE). + * emptyKeyShare == 0: CH1 carries a usable key share, so serverState is + * still NULL_STATE there and it is the cookie alone that forces the + * retry -- that is the only way :8188's decision comes out true. */ + if (emptyKeyShare) + ExpectIntEQ(wolfSSL_NoKeyShares(ssl_c), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 20, NULL), 0); + ExpectIntEQ(ssl_s->options.sendCookie, 1); + ExpectIntEQ(ssl_s->options.cookieGood, 1); + ExpectIntEQ(ssl_s->msgsReceived.got_client_hello, 2); + ExpectIntEQ(ssl_c->msgsReceived.got_hello_retry_request, 1); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#else + (void)emptyKeyShare; +#endif + return EXPECT_RESULT(); +} + +int test_tls13_feat_hrr_cookie_handshake(void) +{ + return test_tls13_feat_hrr_cookie_round(1); +} + +int test_tls13_feat_hrr_cookie_forces_retry(void) +{ + return test_tls13_feat_hrr_cookie_round(0); +} + +/* ------------------------------------------------------------------------- + * Encrypted ClientHello: the feature-flag operands next to the ECH pointers. + * ---------------------------------------------------------------------- */ + +#if defined(HAVE_ECH) && defined(HAVE_SNI) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) + +/* An ECH handshake that also does client authentication and delivers a + * session ticket, so the ECH guards in DoTls13CertificateRequest() and + * DoTls13NewSessionTicket() -- which the group's ECH tests never reach, + * because they neither request a certificate nor read the post-handshake + * ticket -- are evaluated too. + * + * Every one of those guards has the shape + * + * if (ssl->echConfigs != NULL && !ssl->options.disableECH && ) + * + * and `echEnabled == 0` supplies the row the group never had: the ECH + * configuration is present (operand 0 true) but ECH is switched off at + * run time, so operand 1 is false. wolfSSL_SetEchEnable() had never been + * called from the tls13 group at all. `echEnabled == 1` is the partner row + * on the same code path. */ +static int test_tls13_feat_ech_round(int echEnabled) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + static const char pubName[] = "ech-public-name.com"; + static const char privName[] = "ech-private-name.com"; + byte configs[512]; + word32 configsLen = (word32)sizeof(configs); + byte readBuf[16]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + + /* Client authentication, so the server sends a CertificateRequest. */ + ExpectIntEQ(wolfSSL_use_certificate_file(ssl_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_file(ssl_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), + WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_PEER, NULL); + + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(ctx_s, pubName, 0, 0, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(ctx_s, configs, &configsLen), + WOLFSSL_SUCCESS); + /* wolfSSL_SetEchEnable(ssl, 0) frees any ECHConfig already installed, so + * the switch has to be thrown BEFORE the configs are handed over for the + * (echConfigs != NULL && disableECH) state to exist at all. That is the + * state every `echConfigs != NULL && !disableECH && ...` guard in + * src/tls13.c needs in order to evaluate its second operand false. */ + if (!echEnabled) + wolfSSL_SetEchEnable(ssl_c, 0); + ExpectIntEQ(wolfSSL_SetEchConfigs(ssl_c, configs, configsLen), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, privName, + (word16)XSTRLEN(privName)), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_s, WOLFSSL_SNI_HOST_NAME, privName, + (word16)XSTRLEN(privName)), WOLFSSL_SUCCESS); + + ExpectNotNull(ssl_c->echConfigs); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 20, NULL), 0); + ExpectIntEQ(ssl_c->options.disableECH, echEnabled ? 0 : 1); + ExpectIntEQ(wolfSSL_GetEchStatus(ssl_c), echEnabled ? + WOLFSSL_ECH_STATUS_ACCEPTED : WOLFSSL_ECH_STATUS_NOT_OFFERED); + ExpectIntEQ(ssl_c->msgsReceived.got_certificate_request, 1); + ExpectIntEQ(ssl_s->options.havePeerCert, 1); + + /* Drain the post-handshake NewSessionTicket so DoTls13NewSessionTicket() + * runs with the ECH state still attached. A build can enable ECH without + * session tickets (--enable-ech alone leaves "Session Ticket: no"), and + * then the server sends nothing after the handshake: the read still + * reports WANT_READ, but there is no ticket to have received. */ + ExpectIntEQ(wolfSSL_read(ssl_c, readBuf, sizeof(readBuf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); +#ifdef HAVE_SESSION_TICKET + ExpectIntEQ(ssl_c->msgsReceived.got_session_ticket, 1); +#endif + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} +#endif /* HAVE_ECH && HAVE_SNI && certs */ + +int test_tls13_feat_ech_full_handshake(void) +{ +#if defined(HAVE_ECH) && defined(HAVE_SNI) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) + return test_tls13_feat_ech_round(1); +#else + return TEST_SKIPPED; +#endif +} + +int test_tls13_feat_ech_disabled_client(void) +{ +#if defined(HAVE_ECH) && defined(HAVE_SNI) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) + return test_tls13_feat_ech_round(0); +#else + return TEST_SKIPPED; +#endif +} + +/* The server side of the same switch: + * + * DoTls13ClientHello(): if (ssl->ctx->echConfigs != NULL && + * !ssl->options.disableECH) + * SendTls13ServerHello(): the same pair + * + * The server holds an ECHConfig (operand 0 true) but has ECH switched off, + * so it never opens the client's outer ClientHello and answers against the + * public name. RFC 9849 6.1.7 then makes the client abort with + * ECH_REQUIRED_E once it sees the retry configs. The group's existing + * ECH-rejection vector corrupts the config's public key instead, which + * leaves disableECH false on both ends. */ +int test_tls13_feat_ech_disabled_server(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ECH) && defined(HAVE_SNI) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + static const char pubName[] = "ech-public-name.com"; + static const char privName[] = "ech-private-name.com"; + byte configs[512]; + word32 configsLen = (word32)sizeof(configs); + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + + /* Client authentication as well, so the ECH-rejected client still runs + * DoTls13CertificateRequest() -- whose ECH guard sends a blank + * Certificate per RFC 9849 6.1.7 and is only ever true on this path. */ + ExpectIntEQ(wolfSSL_use_certificate_file(ssl_c, cliCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_file(ssl_c, cliKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_s, cliCertFile, NULL), + WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_PEER, NULL); + + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(ctx_s, pubName, 0, 0, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(ctx_s, configs, &configsLen), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_SetEchConfigs(ssl_c, configs, configsLen), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, privName, + (word16)XSTRLEN(privName)), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_s, WOLFSSL_SNI_HOST_NAME, pubName, + (word16)XSTRLEN(pubName)), WOLFSSL_SUCCESS); + + /* Server keeps its ECHConfig but refuses to use it. */ + wolfSSL_SetEchEnable(ssl_s, 0); + + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), + WC_NO_ERR_TRACE(ECH_REQUIRED_E)); + ExpectIntEQ(ssl_s->options.disableECH, 1); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + +/* CheckPreSharedKeys(): + * + * if (((modes & (1 << PSK_DHE_KE)) != 0 && !noPskDheKe && ext != NULL) + * || usingCertWithExternPsk) + * + * Operand 3 is only ever *evaluated* when the left conjunct is false, and + * every cert_with_extern_psk handshake the group runs has it true (the + * client offers psk_dhe_ke, the server does not set noPskDheKe and a + * key_share is present), so the || short-circuits before reaching it. + * A server that refuses (EC)DHE with a PSK makes the second conjunct false + * and leaves RFC 9973's own arm to carry the decision. */ +int test_tls13_feat_cert_with_extern_psk_psk_ke_server(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_CERT_WITH_EXTERN_PSK) && !defined(NO_PSK) && \ + defined(HAVE_SUPPORTED_CURVES) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_psk_client_callback(ssl_c, test_tls13_feat_psk_client_cb); + wolfSSL_set_psk_server_callback(ssl_s, test_tls13_feat_psk_server_cb); + ExpectIntEQ(wolfSSL_set_cert_with_extern_psk(ssl_c, 1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_cert_with_extern_psk(ssl_s, 1), WOLFSSL_SUCCESS); + /* The server refuses psk_dhe_ke, so the left conjunct of the decision + * is false; RFC 9973's own arm then carries it and the handshake still + * completes with an (EC)DHE key share, as RFC 9973 Section 3 requires. */ + ExpectIntEQ(wolfSSL_no_dhe_psk(ssl_s), 0); + + /* DoTls13ClientHello() clears options.noPskDheKe again once RFC 9973's + * arm has taken the decision, so only the negotiated result is asserted + * here. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 20, NULL), 0); + ExpectIntEQ(ssl_s->options.certWithExternPsk, 1); + ExpectIntEQ(ssl_c->options.certWithExternPsk, 1); + ExpectIntEQ(ssl_c->options.pskNegotiated, 1); + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + +/* DoTls13ServerHello(), inside the branch taken when the server chose a PSK: + * + * if (ssl->echConfigs != NULL && !ssl->options.disableECH && + * !ssl->options.echAccepted) + * return INVALID_PARAMETER; ("ECH rejected but server negotiated PSK") + * + * needs the two features together -- an ECH client whose ECH was rejected AND + * a server that answers with a pre_shared_key. The group ran ECH handshakes + * and PSK handshakes but never one of each, so this decision had never been + * true and none of its three operands paired. Here the server holds an + * ECHConfig but has ECH switched off, so it answers the outer ClientHello and + * negotiates the external PSK; the client must refuse rather than resume + * against an unauthenticated outer handshake. */ +static int test_tls13_feat_ech_psk_round(int mode) +{ + EXPECT_DECLS; +#if defined(HAVE_ECH) && defined(HAVE_SNI) && !defined(NO_PSK) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_RSA) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + static const char pubName[] = "ech-public-name.com"; + static const char privName[] = "ech-private-name.com"; + byte configs[512]; + word32 configsLen = (word32)sizeof(configs); + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_psk_client_callback(ssl_c, test_tls13_feat_psk_client_cb); + wolfSSL_set_psk_server_callback(ssl_s, test_tls13_feat_psk_server_cb); + + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(ctx_s, pubName, 0, 0, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(ctx_s, configs, &configsLen), + WOLFSSL_SUCCESS); + /* mode 1 disables ECH on the client before the configs are installed, so + * echConfigs stays non-NULL while disableECH is set; mode 2 leaves the + * client's ECH on and switches the server's off. (An ECH handshake that + * is ACCEPTED and also negotiates a PSK is not a case wolfSSL supports -- + * it fails with INVALID_PARAMETER at this very guard -- so the operand + * that would need it is left open rather than asserted here.) */ + if (mode == 1) + wolfSSL_SetEchEnable(ssl_c, 0); + ExpectIntEQ(wolfSSL_SetEchConfigs(ssl_c, configs, configsLen), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_c, WOLFSSL_SNI_HOST_NAME, privName, + (word16)XSTRLEN(privName)), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(ssl_s, WOLFSSL_SNI_HOST_NAME, + mode == 2 ? pubName : privName, + (word16)XSTRLEN(mode == 2 ? pubName : privName)), WOLFSSL_SUCCESS); + + /* mode 2: server has the ECHConfig but refuses to use it, so ECH is + * rejected and the decision above comes out true. */ + if (mode == 2) + wolfSSL_SetEchEnable(ssl_s, 0); + + if (mode == 2) { + ExpectIntNE(test_memio_do_handshake(ssl_c, ssl_s, 20, NULL), 0); + ExpectIntEQ(ssl_c->options.echAccepted, 0); + } + else { + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 20, NULL), 0); + ExpectIntEQ(ssl_c->options.pskNegotiated, 1); + ExpectIntEQ(ssl_c->options.echAccepted, mode == 0 ? 1 : 0); + ExpectIntEQ(ssl_c->options.disableECH, mode == 1 ? 1 : 0); + } + + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_s); +#else + (void)mode; +#endif + return EXPECT_RESULT(); +} + +int test_tls13_feat_ech_psk_disabled_client(void) +{ + return test_tls13_feat_ech_psk_round(1); +} + +int test_tls13_feat_ech_rejected_with_psk(void) +{ + return test_tls13_feat_ech_psk_round(2); +} + +#else /* !WOLFSSL_TLS13 || !HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES */ + +int test_tls13_feat_optional_client_cert(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_post_handshake_unexpected_msg(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_psk_ke_no_dhe(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_psk_only_dhe_rejects_psk_ke(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_no_ticket_enc_cb(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_psk_ke_empty_key_share(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_optional_psk_falls_back_to_cert(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_pha_ctx_status_request(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_psk_ke_server_key_share_unused(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_psk_ke_server_no_key_share(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_psk_ke_client_require_psk_resumption(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_hrr_cookie_handshake(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_hrr_cookie_forces_retry(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_cert_with_extern_psk_psk_ke_server(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_ech_full_handshake(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_ech_disabled_client(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_ech_disabled_server(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_ech_rejected_with_psk(void) +{ + return TEST_SKIPPED; +} +int test_tls13_feat_ech_psk_disabled_client(void) +{ + return TEST_SKIPPED; +} + +#endif diff --git a/tests/api/test_tls13_features.h b/tests/api/test_tls13_features.h new file mode 100644 index 0000000000..c29db7704a --- /dev/null +++ b/tests/api/test_tls13_features.h @@ -0,0 +1,68 @@ +/* test_tls13_features.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_TLS13_FEATURES_H +#define WOLFCRYPT_TEST_TLS13_FEATURES_H + +#include + +int test_tls13_feat_optional_client_cert(void); +int test_tls13_feat_post_handshake_unexpected_msg(void); +int test_tls13_feat_psk_ke_no_dhe(void); +int test_tls13_feat_psk_only_dhe_rejects_psk_ke(void); +int test_tls13_feat_no_ticket_enc_cb(void); +int test_tls13_feat_psk_ke_empty_key_share(void); +int test_tls13_feat_optional_psk_falls_back_to_cert(void); +int test_tls13_feat_pha_ctx_status_request(void); +int test_tls13_feat_psk_ke_server_key_share_unused(void); +int test_tls13_feat_psk_ke_server_no_key_share(void); +int test_tls13_feat_psk_ke_client_require_psk_resumption(void); +int test_tls13_feat_hrr_cookie_handshake(void); +int test_tls13_feat_hrr_cookie_forces_retry(void); +int test_tls13_feat_cert_with_extern_psk_psk_ke_server(void); +int test_tls13_feat_ech_full_handshake(void); +int test_tls13_feat_ech_disabled_client(void); +int test_tls13_feat_ech_disabled_server(void); +int test_tls13_feat_ech_rejected_with_psk(void); +int test_tls13_feat_ech_psk_disabled_client(void); + +#define TEST_TLS13_FEATURES_DECLS \ + TEST_DECL_GROUP("tls13", test_tls13_feat_optional_client_cert), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_post_handshake_unexpected_msg), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_psk_ke_no_dhe), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_psk_only_dhe_rejects_psk_ke), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_no_ticket_enc_cb), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_psk_ke_empty_key_share), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_optional_psk_falls_back_to_cert), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_pha_ctx_status_request), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_psk_ke_server_key_share_unused), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_psk_ke_server_no_key_share), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_psk_ke_client_require_psk_resumption), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_hrr_cookie_handshake), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_hrr_cookie_forces_retry), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_cert_with_extern_psk_psk_ke_server), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_ech_full_handshake), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_ech_disabled_client), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_ech_disabled_server), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_ech_psk_disabled_client), \ + TEST_DECL_GROUP("tls13", test_tls13_feat_ech_rejected_with_psk) + +#endif /* WOLFCRYPT_TEST_TLS13_FEATURES_H */ diff --git a/tests/api/test_tls_bounds.c b/tests/api/test_tls_bounds.c new file mode 100644 index 0000000000..d1cb4ef0d4 --- /dev/null +++ b/tests/api/test_tls_bounds.c @@ -0,0 +1,2786 @@ +/* test_tls_bounds.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 + */ + +#include +#include +#include +#include + +#include +#include + +/* Named guards for the regions below: each covers a file-scope helper or + * fixture plus the test(s) that use it, so the condition is written once + * and the region and the in-body guard cannot drift apart. */ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) && !defined(NO_CERTS) + #define TEST_TLS_BOUNDS_CSR2_REQUESTS +#endif +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_WOLFSSL_CLIENT) && defined(HAVE_OCSP) + #define TEST_TLS_BOUNDS_CSR_STATUS_CB +#endif +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && \ + !defined(NO_WOLFSSL_SERVER) && defined(WOLFSSL_TLS_OCSP_MULTI) && \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST) && defined(HAVE_OCSP) && \ + !defined(NO_RSA) && !defined(NO_SHA256) + #define TEST_TLS_BOUNDS_OCSP_CHAIN +#endif +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && \ + !defined(NO_WOLFSSL_CLIENT) && defined(HAVE_SUPPORTED_CURVES) && \ + (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && !defined(NO_PSK) && \ + defined(HAVE_AESGCM) && !defined(NO_AES) && !defined(NO_SHA256) + #define TEST_TLS_BOUNDS_POPULATE_EXT +#endif +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_WOLFSSL_SERVER) + #define TEST_TLS_BOUNDS_CSR_PARSE +#endif +#if defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_TLS12) && \ + !defined(NO_WOLFSSL_SERVER) && !defined(NO_WOLFSSL_CLIENT) + #define TEST_TLS_BOUNDS_SESSION_TICKET_FF +#endif + +/* Each helper below is called only from test bodies whose feature guards differ + * from one another, so no single condition describes "some caller is compiled + * in" -- a shared build, for instance, compiles out every WOLFSSL_TEST_STATIC_BUILD + * body at once. Mark them instead of trying to track the union by hand. */ +#if defined(__GNUC__) || defined(__clang__) + #define TEST_TLS_BOUNDS_UNUSED __attribute__((unused)) +#else + #define TEST_TLS_BOUNDS_UNUSED +#endif + +/* c32to24() (wolfcrypt/src/misc.c) is only externally linkable when NO_INLINE + * is defined; this build inlines it into each translation unit that already + * needs it, so it is not visible here. Same 3-byte big-endian length write, + * spelled out locally. */ +TEST_TLS_BOUNDS_UNUSED +static void test_tls_bounds_c32to24(word32 in, byte* out) +{ + out[0] = (byte)(in >> 16); + out[1] = (byte)(in >> 8); + out[2] = (byte)in; +} + +#if !defined(NO_WOLFSSL_SERVER) && !defined(NO_CERTS) && !defined(NO_RSA) && \ + !defined(NO_FILESYSTEM) +/* SetSSL_CTX() (InitSSL()'s caller) fails wolfSSL_new() with NO_PRIVATE_KEY + * for a server-side ssl with no certificate/key and no PSK/anon/cert-setup-cb + * fallback, so every server-side ssl created only to unit-test a WOLFSSL_LOCAL + * function directly (never running a real handshake) still needs a loaded + * cert/key to get past wolfSSL_new() at all. */ +TEST_TLS_BOUNDS_UNUSED +static int test_tls_bounds_load_server_cert(WOLFSSL_CTX* ctx) +{ + EXPECT_DECLS; + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(ctx, "./certs/server-cert.pem", + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx, "./certs/server-key.pem", + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + return EXPECT_RESULT(); +} +#endif + +/* ---------------------------------------------------------------------- */ +/* TLSX_UseSNI - the extensions list argument is always &ssl->extensions or + * &ctx->extensions (wolfSSL_UseSNI / wolfSSL_CTX_UseSNI), or &ech->extensions + * (the ECH echo path in TLSX_SNI_Parse) - always the address of a struct + * member, never NULL. The "extensions == NULL" half of the guard has no + * reachable caller and is excluded (argued in the report, not retested here). + * The "data == NULL" half, the host-name-length guard, and the duplicate-type + * removal in the linked list are all reachable through wolfSSL_UseSNI(). */ +int test_TLSX_UseSNI_bounds(void) +{ +#if defined(HAVE_SNI) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + char longName[WOLFSSL_HOST_NAME_MAX + 1]; + + XMEMSET(longName, 'a', sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* data == NULL -> BAD_FUNC_ARG (extensions is always non-NULL here). */ + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, NULL, 8), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* type == HOST_NAME && size >= MAX -> BAD_LENGTH_E. */ + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, longName, + (word16)XSTRLEN(longName)), WC_NO_ERR_TRACE(BAD_LENGTH_E)); + + /* type != HOST_NAME: the length guard is skipped regardless of size (the + * BAD_LENGTH_E path is not taken), independence for the first operand of + * the (type == HOST_NAME) && (size >= MAX) guard. WOLFSSL_SNI_HOST_NAME + * is the only type TLSX_SNI_New() accepts, so this then fails later, + * inside TLSX_SNI_New()'s own type switch, with MEMORY_E - a different + * and later guard than the one under test here. */ + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME + 1, longName, + (word16)XSTRLEN(longName)), WC_NO_ERR_TRACE(MEMORY_E)); + + /* type == HOST_NAME && size < MAX -> accepted; also the first insert, + * so the duplicate-type list walk has nothing to match yet. */ + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, "a.example.com", + 13), WOLFSSL_SUCCESS); + + /* A second insert: the walk finds sni->next->type == type on the first + * node and removes the duplicate. + * + * "sni->next && sni->next->type == type" (the duplicate-removal check) + * is not driven for independence anywhere in this file: WOLFSSL_SNI_ + * HOST_NAME is the only type TLSX_SNI_New() will ever construct (its + * switch on sni->type frees and rejects anything else), so every node + * that ever exists in the list has type == WOLFSSL_SNI_HOST_NAME, and + * "type" here (the argument of the call reaching this loop) must also + * be WOLFSSL_SNI_HOST_NAME or TLSX_UseSNI() would already have returned + * MEMORY_E from TLSX_SNI_New() before the loop is reached. So whenever + * "sni->next" is true, "sni->next->type == type" is true too - the loop + * always removes the one prior node on its first check and breaks, + * which also means a second loop iteration (where sni->next could be + * NULL) is never reached. Both operands are pinned to true on every + * real execution; excluded (family 4: contradicted by the callee + * postcondition of TLSX_SNI_New()). */ + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, "c.example.com", + 13), WOLFSSL_SUCCESS); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_UseALPN - extensions is always &ssl->extensions (wolfSSL_UseALPN()); + * unreachable-NULL, excluded. data == NULL: wolfSSL_UseALPN() only calls + * TLSX_UseALPN() with tokens produced by XSTRTOK(), which are never NULL + * inside the "while (token[idx] != NULL)" loop, so the only way to reach + * TLSX_UseALPN() with a NULL data pointer at all is to call it directly. */ +int test_TLSX_UseALPN_bounds(void) +{ +#if defined(HAVE_ALPN) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + ExpectIntEQ(wolfSSL_UseALPN(ssl, (char*)"http/1.1", 8, + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + +#ifdef WOLFSSL_TEST_STATIC_BUILD + /* No wrapper can pass data == NULL; extensions == NULL is likewise + * unreachable through any caller (always &ssl->extensions), and is + * exercised here only to document that the guard exists, not to claim + * it as a caller-reachable pair. */ + ExpectIntEQ(TLSX_UseALPN(&ssl->extensions, NULL, 4, + WOLFSSL_ALPN_FAILED_ON_MISMATCH, ssl->heap), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_UseMaxFragment - extensions always &ssl->extensions / &ctx->extensions; + * unreachable-NULL, excluded. mfl < MIN and mfl > MAX are both reachable + * through the public wrapper with an out-of-range code, alongside a valid + * in-range call. */ +int test_TLSX_UseMaxFragment_bounds(void) +{ +#if defined(HAVE_MAX_FRAGMENT) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* mfl < WOLFSSL_MFL_MIN (0 is below WOLFSSL_MFL_2_9 == 1). */ + ExpectIntEQ(wolfSSL_UseMaxFragment(ssl, WOLFSSL_MFL_DISABLED), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* mfl > WOLFSSL_MFL_MAX (WOLFSSL_MFL_2_8 == 6 is the maximum code). */ + ExpectIntEQ(wolfSSL_UseMaxFragment(ssl, (byte)(WOLFSSL_MFL_MAX + 1)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* In range: accepted. */ + ExpectIntEQ(wolfSSL_UseMaxFragment(ssl, WOLFSSL_MFL_2_11), + WOLFSSL_SUCCESS); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_UseCertificateStatusRequest - extensions is always &ssl->extensions / + * &ctx->extensions (wolfSSL_UseOCSPStapling() / _CTX_); unreachable-NULL, + * excluded. status_type != WOLFSSL_CSR_OCSP is reachable directly: the + * wrapper passes the caller's status_type straight through with no + * validation of its own. */ +int test_TLSX_UseCertificateStatusRequest_bounds(void) +{ +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* status_type != WOLFSSL_CSR_OCSP. */ + ExpectIntEQ(wolfSSL_UseOCSPStapling(ssl, WOLFSSL_CSR_OCSP + 1, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Valid request. */ + ExpectIntEQ(wolfSSL_UseOCSPStapling(ssl, WOLFSSL_CSR_OCSP, + WOLFSSL_CSR_OCSP_USE_NONCE), WOLFSSL_SUCCESS); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_UseCertificateStatusRequestV2 - same pattern as V1 above. */ +int test_TLSX_UseCertificateStatusRequestV2_bounds(void) +{ +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* Neither WOLFSSL_CSR2_OCSP nor WOLFSSL_CSR2_OCSP_MULTI: both operands + * of "status_type != OCSP && status_type != OCSP_MULTI" true. */ + ExpectIntEQ(wolfSSL_UseOCSPStaplingV2(ssl, + WOLFSSL_CSR2_OCSP_MULTI + 1, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* status_type == OCSP_MULTI: first operand true (MULTI != OCSP), second + * operand false (MULTI == MULTI) - independence for the second operand, + * paired against the invalid-type call above. */ + ExpectIntEQ(wolfSSL_UseOCSPStaplingV2(ssl, WOLFSSL_CSR2_OCSP_MULTI, + WOLFSSL_CSR2_OCSP_USE_NONCE), WOLFSSL_SUCCESS); + + /* status_type == OCSP: first operand false, short-circuits past the + * second - independence for the first operand. */ + ExpectIntEQ(wolfSSL_UseOCSPStaplingV2(ssl, WOLFSSL_CSR2_OCSP, + WOLFSSL_CSR2_OCSP_USE_NONCE), WOLFSSL_SUCCESS); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_SupportExtensions() - "return ssl && (IsTLS(ssl) || + * ssl->version.major == DTLS_MAJOR);" reached through the WOLFSSL_TEST_VIS + * TLSX_WriteRequest(), which calls it as its leading guard. + * + * With WOLFSSL_DTLS defined (as it is in this build), IsTLS() itself already + * returns true for ssl->version.major == DTLS_MAJOR (it has its own + * "#ifdef WOLFSSL_DTLS if (ssl->version.major == DTLS_MAJOR) return 1;" + * check). So the third operand here can only be evaluated (IsTLS() false) + * when major is neither a valid TLS major/minor pair nor DTLS_MAJOR - and in + * that same case the third operand's own check of major == DTLS_MAJOR is + * false too. The third operand's true side is therefore contradicted by + * IsTLS()'s postcondition in this build and is excluded (family 4); only its + * (always-false-when-reached) value is exercised below, alongside the first + * and second operands. */ +int test_TLSX_SupportExtensions_bounds(void) +{ +#if !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte out[512]; + word32 offset = 0; + + /* ssl == NULL: the leading operand alone decides the result. */ + ExpectIntEQ(TLSX_WriteRequest(NULL, out, client_hello, &offset), 0); + + /* A live ssl object: IsTLS(ssl) true (real major/minor), so ssl && + * IsTLS(ssl) is already true and the third operand is never reached - + * independence for the first and second operands. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), 0); + + /* Corrupt ssl->version.major to a value IsTLS() does not recognize + * (neither SSLv3_MAJOR nor DTLS_MAJOR): IsTLS(ssl) is now false, and the + * third operand - checking that very same field for DTLS_MAJOR - is + * false too. This is the only way to make IsTLS(ssl) false at all: every + * ssl created through a real method sets a recognized major/minor. */ + if (ssl != NULL) { + ssl->version.major = 0; + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), 0); + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = TLSv1_2_MINOR; + } + + /* TLSX_WriteRequest()'s own leading guard is + * "!TLSX_SupportExtensions(ssl) || output == NULL": a supported ssl with + * output == NULL exercises the second operand independently of the + * ssl == NULL call above. */ + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, NULL, client_hello, &offset), 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_EarlyData_Use() - "extension->val = (WOLFSSL_IS_QUIC(ssl) && + * is_response && maxSz > 0) ? WOLFSSL_MAX_32BIT : maxSz;" - this build has no + * WOLFSSL_QUIC, so WOLFSSL_IS_QUIC() expands to the literal 0: the first + * operand is a compile-time constant false in every translation unit of this + * binary. Because of the left-to-right && short circuit, is_response and + * maxSz > 0 can never be evaluated either - all three operands of this + * decision are unreachable in this configuration. Excluded (family 3: fixed + * by the branch/config that reaches it). + * No test body: nothing to drive. */ + +/* ---------------------------------------------------------------------- */ +/* TLSX_CSR2_InitRequests(), TLSX_CSR2_ForceRequest(), TLSX_CSR_GetRequest_ex() + * are WOLFSSL_LOCAL with no public wrapper; each is unit-tested directly by + * building the minimal extension/context state each one dereferences. */ +int test_TLSX_CSR2_InitRequests_bounds(void) +{ +#if defined(TEST_TLS_BOUNDS_CSR2_REQUESTS) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + DecodedCert cert; + TLSX* ext; + CertificateStatusRequestItemV2* csr2; + + XMEMSET(&cert, 0, sizeof(cert)); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + ExpectIntEQ(TLSX_UseCertificateStatusRequestV2(&ssl->extensions, + WOLFSSL_CSR2_OCSP, 0, ssl->heap, ssl->devId), WOLFSSL_SUCCESS); + ext = TLSX_Find(ssl->extensions, TLSX_STATUS_REQUEST_V2); + ExpectNotNull(ext); + + if (ext != NULL) { + csr2 = (CertificateStatusRequestItemV2*)ext->data; + + /* isPeer == 0: "!isPeer" true, short-circuits past requests != 0. */ + ExpectIntEQ(TLSX_CSR2_InitRequests(ssl->extensions, &cert, 0, + ssl->heap), 0); + + /* isPeer == 1, requests == 0: both operands false, falls through to + * build the request from the (empty) DecodedCert. */ + ExpectIntEQ(TLSX_CSR2_InitRequests(ssl->extensions, &cert, 1, + ssl->heap), 0); + ExpectIntEQ(csr2->requests, 1); + + /* isPeer == 1, requests != 0: "!isPeer" false, "requests != 0" + * true - independence for the second operand. */ + ExpectIntEQ(TLSX_CSR2_InitRequests(ssl->extensions, &cert, 1, + ssl->heap), 0); + ExpectIntEQ(csr2->requests, 1); + } + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +int test_TLSX_CSR2_ForceRequest_bounds(void) +{ +#if defined(TEST_TLS_BOUNDS_CSR2_REQUESTS) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + TLSX* ext; + CertificateStatusRequestItemV2* csr2; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + ExpectIntEQ(TLSX_UseCertificateStatusRequestV2(&ssl->extensions, + WOLFSSL_CSR2_OCSP, 0, ssl->heap, ssl->devId), WOLFSSL_SUCCESS); + ext = TLSX_Find(ssl->extensions, TLSX_STATUS_REQUEST_V2); + ExpectNotNull(ext); + + if (ext != NULL) { + csr2 = (CertificateStatusRequestItemV2*)ext->data; + + /* ocspEnabled false: whole AND is false regardless of requests. */ + SSL_CM(ssl)->ocspEnabled = 0; + csr2->requests = 1; + ExpectIntEQ(TLSX_CSR2_ForceRequest(ssl), WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)); + + /* ocspEnabled true, requests == 0: first operand true, second + * false - independence for the second operand's false side. */ + SSL_CM(ssl)->ocspEnabled = 1; + csr2->requests = 0; + ExpectIntEQ(TLSX_CSR2_ForceRequest(ssl), WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)); + + /* ocspEnabled true, requests >= 1: both operands true, so the + * lookup runs. No responder is configured on this CertManager, so + * CheckOcspRequest() reports BAD_FUNC_ARG on its own ocsp == NULL + * guard rather than reaching the network. */ + csr2->requests = 1; + ExpectIntEQ(TLSX_CSR2_ForceRequest(ssl), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +int test_TLSX_CSR_GetRequest_ex_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + /* A TLS 1.2-only method keeps IsAtLeastTLSv1_3(csr->ssl->version) false, + * so the idx == 0 branch below is deterministic without negotiating a + * version first. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* csr == NULL: no TLSX_STATUS_REQUEST extension present at all. */ + ExpectNull(TLSX_CSR_GetRequest_ex(ssl->extensions, 0)); + + /* csr != NULL, csr->ssl == NULL - reachable only by calling the + * WOLFSSL_LOCAL constructor directly with a NULL ssl; the public + * wrapper (wolfSSL_UseOCSPStapling -> TLSX_UseCertificateStatusRequest) + * always forwards a live ssl pointer, so this half needs the direct + * call too. */ + ExpectIntEQ(TLSX_UseCertificateStatusRequest(&ssl->extensions, + WOLFSSL_CSR_OCSP, 0, NULL, ssl->heap, ssl->devId), + WOLFSSL_SUCCESS); + ExpectNull(TLSX_CSR_GetRequest_ex(ssl->extensions, 0)); + + /* csr != NULL, csr->ssl != NULL. */ + TLSX_Remove(&ssl->extensions, TLSX_STATUS_REQUEST, ssl->heap); + ExpectIntEQ(TLSX_UseCertificateStatusRequest(&ssl->extensions, + WOLFSSL_CSR_OCSP, 0, ssl, ssl->heap, ssl->devId), + WOLFSSL_SUCCESS); + /* Pre-TLS1.3 ssl: idx == 0 returns the sole ocsp[0] slot. */ + ExpectNotNull(TLSX_CSR_GetRequest_ex(ssl->extensions, 0)); + ExpectNull(TLSX_CSR_GetRequest_ex(ssl->extensions, 1)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + + +/* ---------------------------------------------------------------------- */ +/* wolfSSL_make_eap_keys() - "ssl == NULL || ssl->arrays == NULL". ssl->arrays + * is allocated lazily by the handshake and is still NULL on a freshly + * created object, so both operands are reachable without completing a + * handshake at all. */ +int test_wolfSSL_make_eap_keys_bounds(void) +{ +#if defined(WOLFSSL_HAVE_PRF) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(WOLFSSL_NO_TLS12) && defined(WOLFSSL_TEST_STATIC_BUILD) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte key[32]; + + /* ssl == NULL. */ + ExpectIntEQ(wolfSSL_make_eap_keys(NULL, key, sizeof(key), "label"), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* ssl != NULL, ssl->arrays != NULL: wolfSSL_new() -> ReinitSSL() already + * allocated it, so this is the default state. */ + ExpectNotNull(ssl->arrays); + ssl->specs.mac_algorithm = sha256_mac; + ExpectIntEQ(wolfSSL_make_eap_keys(ssl, key, sizeof(key), "label"), 0); + + /* ssl != NULL, ssl->arrays == NULL: FreeArrays() is what clears it in + * real use, once the handshake has finished with the randoms/master + * secret and no longer needs them - called directly here (WOLFSSL_LOCAL) + * rather than running a full handshake just to reach the same state. */ + FreeArrays(ssl, 0); + ExpectNull(ssl->arrays); + ExpectIntEQ(wolfSSL_make_eap_keys(ssl, key, sizeof(key), "label"), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* wolfSSL_SetTlsHmacInner() - "ssl == NULL || inner == NULL", then + * "content == dtls12_cid || (ssl->options.dtls && DtlsGetCidTxSize(ssl) > + * 0)". Both are public (WOLFSSL_API) and reachable directly. */ +int test_wolfSSL_SetTlsHmacInner_bounds(void) +{ +#if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_AEAD_ONLY) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte inner[WOLFSSL_TLS_HMAC_INNER_SZ]; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* ssl == NULL. */ + ExpectIntEQ(wolfSSL_SetTlsHmacInner(NULL, inner, 10, application_data, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* ssl != NULL, inner == NULL. */ + ExpectIntEQ(wolfSSL_SetTlsHmacInner(ssl, NULL, 10, application_data, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* content == dtls12_cid: true on its own, regardless of ssl->options. + * dtls, on a plain TLS object. */ + ExpectIntEQ(wolfSSL_SetTlsHmacInner(ssl, inner, 10, dtls12_cid, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* content != dtls12_cid, ssl->options.dtls == 0: both operands of the + * inner OR are false without evaluating DtlsGetCidTxSize() at all. */ + ExpectIntEQ(wolfSSL_SetTlsHmacInner(ssl, inner, 10, application_data, 0), + 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + +/* DtlsGetCidTxSize() is WOLFSSL_LOCAL, so this block only builds when the + * test links against the static library. */ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_DTLS) && \ + defined(WOLFSSL_DTLS_CID) && defined(WOLFSSL_DTLS13) + /* content != dtls12_cid, ssl->options.dtls == 1, no CID negotiated: + * independence for the second operand while the third is false. */ + ctx = NULL; + ssl = NULL; + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfDTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_SetTlsHmacInner(ssl, inner, 10, application_data, 0), + 0); + + /* content != dtls12_cid, ssl->options.dtls == 1, a tx CID configured: + * independence for the third operand. */ + ExpectIntEQ(wolfSSL_dtls_cid_use(ssl), WOLFSSL_SUCCESS); + { + byte cid[4] = { 1, 2, 3, 4 }; + ExpectIntEQ(wolfSSL_dtls_cid_set(ssl, cid, sizeof(cid)), + WOLFSSL_SUCCESS); + } + if (DtlsGetCidTxSize(ssl) > 0) { + ExpectIntEQ(wolfSSL_SetTlsHmacInner(ssl, inner, 10, application_data, + 0), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* BuildTlsHandshakeHash() - the leading 4-operand NULL/size guard has no + * public wrapper, so it is unit-tested directly (WOLFSSL_TEST_STATIC_BUILD). + * The mac_algorithm <= sha256_mac || mac_algorithm == blake2b_mac branch + * (choosing the SHA-256 handshake hash) only needs its second operand: + * mac_algorithm <= sha256_mac already has an independence pair elsewhere in + * the suite; blake2b_mac (8) is above sha256_mac (4) in enum wc_MACAlgorithm + * ordering regardless of whether HAVE_BLAKE2B is built, so setting + * specs.mac_algorithm to it exercises the SHA-256 path through the second + * operand without requiring BLAKE2b support. */ +int test_BuildTlsHandshakeHash_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && !defined(WOLFSSL_NO_TLS12) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_SHA256) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte hash[WC_MAX_DIGEST_SIZE]; + word32 hashLen; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(InitHandshakeHashes(ssl), 0); + + /* ssl == NULL. */ + hashLen = sizeof(hash); + ExpectIntEQ(BuildTlsHandshakeHash(NULL, hash, &hashLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* hash == NULL. */ + hashLen = sizeof(hash); + ExpectIntEQ(BuildTlsHandshakeHash(ssl, NULL, &hashLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* hashLen == NULL. */ + ExpectIntEQ(BuildTlsHandshakeHash(ssl, hash, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* *hashLen < HSHASH_SZ. */ + hashLen = 1; + ExpectIntEQ(BuildTlsHandshakeHash(ssl, hash, &hashLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* All valid, mac_algorithm <= sha256_mac (sha256_mac itself): the + * already-covered independence pair for the first operand. */ + ssl->specs.mac_algorithm = sha256_mac; + hashLen = sizeof(hash); + ExpectIntEQ(BuildTlsHandshakeHash(ssl, hash, &hashLen), 0); + ExpectIntEQ(hashLen, WC_SHA256_DIGEST_SIZE); + + /* mac_algorithm == blake2b_mac: first operand false (8 > sha256_mac's + * 4), second operand true - still routes to the SHA-256 hash object + * (populated regardless of the negotiated MAC, for exactly this kind of + * lookup), not to an unbuilt BLAKE2b one. */ + ssl->specs.mac_algorithm = blake2b_mac; + hashLen = sizeof(hash); + ExpectIntEQ(BuildTlsHandshakeHash(ssl, hash, &hashLen), 0); + ExpectIntEQ(hashLen, WC_SHA256_DIGEST_SIZE); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLS_hmac() / Hmac_UpdateFinal_CT() - driven directly through ssl->hmac(), + * which InitSSL() points at TLS_hmac() by default whenever TLS 1.2 (or + * older) CBC-MAC support is built, before any handshake runs (see the + * existing test_tls_hmac_size_overflow() in test_hmac.c for the same + * pattern). No live connection is needed: the size-overflow guard and the + * verify/padSz dispatch are pure argument checks over ssl->specs and the + * caller-supplied lengths. */ +int test_TLS_hmac_bounds(void) +{ +#if !defined(NO_HMAC) && !defined(WOLFSSL_AEAD_ONLY) && !defined(NO_TLS) && defined(NO_OLD_TLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_SHA256) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte digest[WC_MAX_DIGEST_SIZE]; + byte in[256]; + + XMEMSET(in, 0xAA, sizeof(in)); + XMEMSET(digest, 0, sizeof(digest)); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectNotNull(ssl->hmac); + + if (EXPECT_SUCCESS()) { + ssl->specs.mac_algorithm = sha256_mac; + ssl->specs.hash_size = WC_SHA256_DIGEST_SIZE; + + /* "verify && padSz >= 0" (first occurrence, guards the size-overflow + * pre-check): verify == 0 - first operand false, independence + * against the verify == 1 calls below. Neither the pre-check nor + * the second occurrence at line ~1465 are reached; the plain + * wc_HmacUpdate/Final path below runs instead. */ + ExpectIntEQ(ssl->hmac(ssl, digest, in, 50, 10, application_data, 0, + PEER_ORDER), 0); + + /* verify == 1, padSz == -1: second operand false - independence for + * the second operand of the same guard, and for the second + * occurrence at line ~1465 (both are skipped, same as above). */ + ExpectIntEQ(ssl->hmac(ssl, digest, in, 50, -1, application_data, 1, + PEER_ORDER), 0); + + /* verify == 1, padSz == 10: both operands true. Reaches the + * overflow pre-check with ordinary values (no overflow) and then + * the constant-time verify path (Hmac_UpdateFinal_CT(), and within + * it Hmac_OuterHash() - both only reachable from here). */ + ExpectIntEQ(ssl->hmac(ssl, digest, in, 50, 10, application_data, 1, + PEER_ORDER), 0); + + /* Overflow in the first addition (sz + hashSz): sz alone is already + * within 32 of the word32 max. */ + ExpectIntEQ(ssl->hmac(ssl, digest, in, + (word32)(WOLFSSL_MAX_32BIT - 10), 0, application_data, 1, + PEER_ORDER), WC_NO_ERR_TRACE(BUFFER_E)); + + /* First addition safe (sz + hashSz == WOLFSSL_MAX_32BIT - 5), but + * adding padSz overflows - independence for the second addition, + * with the first false. */ + ExpectIntEQ(ssl->hmac(ssl, digest, in, + (word32)(WOLFSSL_MAX_32BIT - 32 - 5), 10, + application_data, 1, PEER_ORDER), + WC_NO_ERR_TRACE(BUFFER_E)); + + /* First two additions land exactly on WOLFSSL_MAX_32BIT (sz + + * hashSz(32) + padSz(0)), so neither overflows, but the final "+ 1" + * does - independence for the third addition, with the first two + * false. */ + ExpectIntEQ(ssl->hmac(ssl, digest, in, + (word32)(WOLFSSL_MAX_32BIT - 32), 0, application_data, 1, + PEER_ORDER), WC_NO_ERR_TRACE(BUFFER_E)); + +#ifdef WOLFSSL_TEST_STATIC_BUILD + /* Hmac_UpdateFinal_CT()'s own "macLen <= 0" guard: force hash_size + * to 0 for this call only. wc_HmacSetKey() accepts a zero-length + * key (RFC 2104 permits an empty key), so ret stays 0 and this + * still reaches the constant-time path with macLen == 0. + * + * The guard's other half, "macLen > sizeof(hmac->innerHash)" + * (innerHash is WC_MAX_DIGEST_SIZE bytes), is excluded: macLen is + * always ssl->specs.hash_size or TRUNCATED_HMAC_SZ, and hash_size + * is also the key length wc_HmacSetKey() reads out of + * ssl->keys.*_write_MAC_secret, a fixed WC_MAX_DIGEST_SIZE-byte + * array - so any hash_size big enough to take this branch would + * already have made wc_HmacSetKey() (immediately above, in the same + * call) read out of bounds. Every real MAC algorithm's digest size + * fits within WC_MAX_DIGEST_SIZE by construction, so this half is + * contradicted by that shared field's own bound (family 4). */ + ssl->specs.hash_size = 0; + ExpectIntEQ(ssl->hmac(ssl, digest, in, 50, 0, application_data, 1, + PEER_ORDER), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ssl->specs.hash_size = WC_SHA256_DIGEST_SIZE; +#endif + } + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_ALPN_GetSize() (reached through the WOLFSSL_TEST_VIS + * TLSX_GetRequestSize()) - "alpnSz == 0 && extension->data != NULL" is the + * 16-bit overflow guard on the accumulated ALPN protocol list size. The only + * producer of a TLSX_APPLICATION_LAYER_PROTOCOL extension is TLSX_UseALPN(), + * which always supplies a non-NULL ALPN entry before pushing - so + * extension->data is never NULL while this extension exists, and the second + * operand is true on every real execution that reaches it (which only + * happens when the first operand is already true, i.e. on overflow). + * Excluded (family 4: contradicted by TLSX_UseALPN()'s own postcondition); + * only the first operand is driven here. WOLFSSL_MAX_ALPN_NUMBER (257) + * entries of the maximum WOLFSSL_MAX_ALPN_PROTO_NAME_LEN (255) push the + * running total past 0xFFFF (257 * 256 + 2 == 65794). */ +int test_TLSX_ALPN_GetSize_overflow(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(HAVE_ALPN) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + char name[WOLFSSL_MAX_ALPN_PROTO_NAME_LEN]; + word32 len; + int i; + + XMEMSET(name, 'a', sizeof(name)); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* alpnSz != 0 (no overflow): first operand false, short-circuits past + * the second - independence for the first operand, paired against the + * overflow case below. */ + len = 0; + ExpectIntEQ(wolfSSL_UseALPN(ssl, (char*)"http/1.1", 8, + WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_GetRequestSize(ssl, client_hello, &len), 0); + + wolfSSL_free(ssl); + ssl = NULL; + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + for (i = 0; EXPECT_SUCCESS() && i < WOLFSSL_MAX_ALPN_NUMBER; i++) { + ExpectIntEQ(TLSX_UseALPN(&ssl->extensions, name, sizeof(name), + WOLFSSL_ALPN_FAILED_ON_MISMATCH, ssl->heap), + WOLFSSL_SUCCESS); + } + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, client_hello, &len), + WC_NO_ERR_TRACE(LENGTH_ERROR)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_Cookie_GetSize() / TLSX_Cookie_Write() - "msgType == client_hello || + * msgType == hello_retry_request", reached through the WOLFSSL_TEST_VIS + * TLSX_GetRequestSize()/TLSX_WriteRequest() by passing the message type + * directly: the dispatch inside TLSX_GetSize()/TLSX_Write() only looks at + * the msgType argument (and, once the extension already exists, + * extension->resp for the "is this message type getting a response-only + * extension" skip), not at which top-level wrapper made the call. */ +int test_TLSX_Cookie_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && defined(WOLFSSL_SEND_HRR_COOKIE) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + TLSX* ext; + byte cookieData[4] = { 1, 2, 3, 4 }; + byte out[64]; + word32 offset; + word32 pLen; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + ExpectIntEQ(TLSX_Cookie_Use(ssl, cookieData, sizeof(cookieData), NULL, 0, + 0, &ssl->extensions), 0); + + /* msgType == client_hello: first operand true. isRequest is true for + * client_hello, so the extension is included regardless of resp. Drives + * both TLSX_Cookie_GetSize() (via TLSX_GetRequestSize()) and + * TLSX_Cookie_Write() (via TLSX_WriteRequest()) - the two functions have + * the same guard at the same relative position, reached the same way. */ + pLen = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, client_hello, &pLen), 0); + ExpectTrue(pLen > OPAQUE16_LEN); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), 0); + ExpectTrue(offset > OPAQUE16_LEN); + + /* msgType == hello_retry_request: second operand true, first false - + * independence for the second operand. hello_retry_request is not + * "isRequest" (only client_hello/certificate_request are), so resp must + * be set for TLSX_GetSize()/TLSX_Write() to not skip the extension. */ + ext = TLSX_Find(ssl->extensions, TLSX_COOKIE); + ExpectNotNull(ext); + if (ext != NULL) + ext->resp = 1; + pLen = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, hello_retry_request, &pLen), 0); + ExpectTrue(pLen > OPAQUE16_LEN); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, hello_retry_request, &offset), 0); + ExpectTrue(offset > OPAQUE16_LEN); + + /* msgType == server_hello: both operands false - independence for the + * second operand's false side, paired against the hello_retry_request + * case above (both with resp == 1, so the extension is not skipped + * before TLSX_Cookie_GetSize()/_Write() run). */ + pLen = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, server_hello, &pLen), + WC_NO_ERR_TRACE(SANITY_MSG_E)); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, server_hello, &offset), + WC_NO_ERR_TRACE(SANITY_MSG_E)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_CSR_GetSize_ex() / TLSX_CSR_Write_ex() - both share the same + * response-side shape: + * if (!isRequest && csr->ssl != NULL && IsAtLeastTLSv1_3(csr->ssl->version)) { + * if (csr->ssl != NULL && SSL_CM(csr->ssl) != NULL && + * SSL_CM(csr->ssl)->ocsp_stapling != NULL && + * SSL_CM(csr->ssl)->ocsp_stapling->statusCb != NULL) { + * + * } + * responses[idx] directly> + * } + * + * In TLSX_CSR_Write_ex() specifically, the leading "!isRequest" operand of + * the outer decision is unreachable as false: the function's own + * "#ifndef NO_WOLFSSL_CLIENT if (isRequest) { ...; return (int)offset; }" + * block (present in this build) always returns before this line whenever + * isRequest is true, so control only ever reaches the response-side "if" + * with isRequest already false. Excluded (family 3: fixed by the branch + * that reaches it). TLSX_CSR_GetSize_ex()'s twin block has no such + * unconditional return ahead of it (the request-side "if (isRequest) {...}" + * just falls through), so its !isRequest/csr->ssl!=NULL operands are + * independently reachable there - which is exactly why the worklist only + * carries TLSX_CSR_GetSize_ex's third operand (the version check) and not + * its first two. + * + * The nested decision's leading "csr->ssl != NULL" (in both functions) is + * likewise fixed true: it re-tests the same csr->ssl pointer the enclosing + * "if" already required to be non-NULL two lines above, with no assignment + * to csr->ssl in between. Excluded (family 3) in both functions. + */ +#ifdef TEST_TLS_BOUNDS_CSR_STATUS_CB +TEST_TLS_BOUNDS_UNUSED +static int test_TLSX_CSR_write_getsize_status_cb(WOLFSSL* ssl, void* arg) +{ + (void)ssl; (void)arg; + return WOLFSSL_OCSP_STATUS_CB_OK; +} +#endif + +int test_TLSX_CSR_write_getsize_bounds(void) +{ +#if defined(TEST_TLS_BOUNDS_CSR_STATUS_CB) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + TLSX* ext = NULL; + CertificateStatusRequest* csr = NULL; + WOLFSSL_CERT_MANAGER* origCM = NULL; + ProtocolVersion origVersion; + byte out[OPAQUE8_LEN + OPAQUE24_LEN + 8]; + word16 sz; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(test_tls_bounds_load_server_cert(ctx), TEST_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(TLSX_UseCertificateStatusRequest(&ssl->extensions, + WOLFSSL_CSR_OCSP, 0, ssl, ssl->heap, ssl->devId), + WOLFSSL_SUCCESS); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, TLSX_STATUS_REQUEST)); + if (ext != NULL) + csr = (CertificateStatusRequest*)ext->data; + ExpectNotNull(csr); + + if (csr != NULL) { + origVersion = ssl->version; + + /* Outer decision, operand 1 (csr->ssl != NULL): false side. Short- + * circuits before the version check (csr->ssl->version) is ever + * dereferenced, so this is safe even though ssl is TLS 1.3. Only + * TLSX_CSR_Write_ex() needs this pair (TLSX_CSR_GetSize_ex()'s + * operand 1 is already covered elsewhere). */ + csr->ssl = NULL; + ExpectIntEQ(TLSX_CSR_Write_ex(csr, out, 0, 0), 0); + csr->ssl = ssl; + + /* Outer decision, operand 2 (IsAtLeastTLSv1_3): false side, with + * operand 1 true (csr->ssl == ssl, unchanged). Paired against the + * TLS 1.3 calls below (operand 2 true). */ + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = TLSv1_2_MINOR; + ExpectIntEQ(TLSX_CSR_GetSize_ex(csr, 0, 0), 0); + ExpectIntEQ(TLSX_CSR_Write_ex(csr, out, 0, 0), 0); + ssl->version = origVersion; + + /* Nested decision, operand 1 (SSL_CM(csr->ssl) != NULL): false + * side. Outer decision is true throughout (TLS 1.3, csr->ssl set), + * so this also covers operand 2 of the outer decision (true side). */ + origCM = ssl->ctx->cm; + ssl->ctx->cm = NULL; + ExpectIntEQ(TLSX_CSR_GetSize_ex(csr, 0, 0), + OPAQUE8_LEN + OPAQUE24_LEN); + ExpectIntEQ(TLSX_CSR_Write_ex(csr, out, 0, 0), + OPAQUE8_LEN + OPAQUE24_LEN); + ssl->ctx->cm = origCM; + + /* Nested decision, operand 2 (ocsp_stapling != NULL): false side, + * operand 1 now true (cm restored, no stapling object allocated + * yet). */ + ExpectIntEQ(TLSX_CSR_GetSize_ex(csr, 0, 0), + OPAQUE8_LEN + OPAQUE24_LEN); + ExpectIntEQ(TLSX_CSR_Write_ex(csr, out, 0, 0), + OPAQUE8_LEN + OPAQUE24_LEN); + + /* Nested decision, operand 3 (statusCb != NULL): false side, + * operands 1 and 2 now true (stapling object allocated, no + * callback registered yet). */ + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(ctx), WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_CSR_GetSize_ex(csr, 0, 0), + OPAQUE8_LEN + OPAQUE24_LEN); + ExpectIntEQ(TLSX_CSR_Write_ex(csr, out, 0, 0), + OPAQUE8_LEN + OPAQUE24_LEN); + + /* Nested decision, all operands true: routes through + * TLSX_CSR_WriteWithStatusCB() / the status-callback size path. + * TLSX_CSR_WriteWithStatusCB()'s own leading NULL guards + * ("ssl == NULL || SSL_CM(ssl) == NULL" and "ocsp == NULL || + * ocsp->statusCb == NULL") are unreachable as true: it has exactly + * one caller (this line), which has just proven all four of those + * facts true. Excluded (family 1). Its response==NULL/respSz==0 + * guard is not established by the caller and is driven below. */ + ExpectIntEQ(wolfSSL_CTX_set_tlsext_status_cb(ctx, + test_TLSX_CSR_write_getsize_status_cb), WOLFSSL_SUCCESS); + + /* response == NULL (respSz == 0 too, both untouched): operand 0 + * of the response/respSz guard true. */ + ssl->ocspCsrResp[0].buffer = NULL; + ssl->ocspCsrResp[0].length = 0; + ExpectIntEQ(TLSX_CSR_Write_ex(csr, out, 0, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* response != NULL, respSz == 0: operand 0 false, operand 1 true - + * independence for operand 1. */ + ssl->ocspCsrResp[0].buffer = out; + ssl->ocspCsrResp[0].length = 0; + ExpectIntEQ(TLSX_CSR_Write_ex(csr, out, 0, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* response != NULL, respSz > 0: both operands false - the write + * succeeds. Also exercises TLSX_CSR_GetSize_ex()'s status-callback + * size path (not itself an open condition, but the same call site). */ + ssl->ocspCsrResp[0].length = 4; + sz = TLSX_CSR_GetSize_ex(csr, 0, 0); + ExpectIntEQ(sz, OPAQUE8_LEN + OPAQUE24_LEN + 4); + ExpectIntEQ(TLSX_CSR_Write_ex(csr, out, 0, 0), + OPAQUE8_LEN + OPAQUE24_LEN + 4); + + ssl->ocspCsrResp[0].buffer = NULL; + ssl->ocspCsrResp[0].length = 0; + } + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_CSR_SetResponseWithStatusCB() - unlike TLSX_CSR_WriteWithStatusCB(), + * this one is WOLFSSL_LOCAL (not static-in-file), so it is unit-tested + * directly rather than only through its one real caller (SetupOcspResp() in + * tls13.c, which - like TLSX_CSR_Write_ex() above - already guarantees + * ssl/SSL_CM(ssl)/ocsp_stapling/statusCb are non-NULL before calling it). + * Called directly, none of those preconditions are established, so all four + * operands are independently reachable here. */ +int test_TLSX_CSR_SetResponseWithStatusCB_bounds(void) +{ +#if defined(TEST_TLS_BOUNDS_CSR_STATUS_CB) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + WOLFSSL_CERT_MANAGER* origCM = NULL; + + /* ssl == NULL: first operand of "ssl == NULL || SSL_CM(ssl) == NULL". */ + ExpectIntEQ(TLSX_CSR_SetResponseWithStatusCB(NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(test_tls_bounds_load_server_cert(ctx), TEST_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* ssl != NULL, SSL_CM(ssl) == NULL: second operand true, first false - + * independence for the second operand. */ + origCM = ssl->ctx->cm; + ssl->ctx->cm = NULL; + ExpectIntEQ(TLSX_CSR_SetResponseWithStatusCB(ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ssl->ctx->cm = origCM; + + /* ssl != NULL, SSL_CM(ssl) != NULL, ocsp == NULL (no stapling object + * allocated yet): both operands of the first guard false, first + * operand of the second guard ("ocsp == NULL") true. */ + ExpectIntEQ(TLSX_CSR_SetResponseWithStatusCB(ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* ocsp != NULL, statusCb == NULL: second operand of the second guard + * true, first false - independence for that operand. */ + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(ctx), WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_CSR_SetResponseWithStatusCB(ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* Both guards false: reaches the callback itself. No status_request + * extension is present on ssl, so the callback's WOLFSSL_OCSP_STATUS_CB_OK + * finds nothing in ssl->ocspCsrResp to ack and returns cleanly. */ + ExpectIntEQ(wolfSSL_CTX_set_tlsext_status_cb(ctx, + test_TLSX_CSR_write_getsize_status_cb), WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_CSR_SetResponseWithStatusCB(ssl), 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* ProcessChainOCSPRequest() - walks ssl->buffers.certChain (each entry a + * 3-byte length followed by a DER certificate) and, for each one, builds + * and checks an OCSP request against SSL_CM(ssl)->ocsp_stapling. Response + * bytes below are the same "resp_cert_unknown" fixture used by + * tests/api/test_ocsp.c (generated by create_ocsp_test_blobs.py for + * certs/ocsp/intermediate1-ca-cert.pem against certs/ocsp/root-ca-cert.pem), + * copied here under a local name so this file does not pull in + * test_ocsp_test_blobs.h's non-static globals into a second translation + * unit. */ +#ifdef TEST_TLS_BOUNDS_OCSP_CHAIN +static const unsigned char csrocsp_resp_unknown[] = { + 0x30, 0x82, 0x07, 0x29, 0x0a, 0x01, 0x00, 0xa0, 0x82, 0x07, 0x22, 0x30, + 0x82, 0x07, 0x1e, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, + 0x01, 0x01, 0x04, 0x82, 0x07, 0x0f, 0x30, 0x82, 0x07, 0x0b, 0x30, 0x82, + 0x01, 0x00, 0xa1, 0x81, 0x9b, 0x30, 0x81, 0x98, 0x31, 0x0b, 0x30, 0x09, + 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x13, 0x30, + 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, 0x0a, 0x57, 0x61, 0x73, 0x68, + 0x69, 0x6e, 0x67, 0x74, 0x6f, 0x6e, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, + 0x55, 0x04, 0x07, 0x0c, 0x07, 0x53, 0x65, 0x61, 0x74, 0x74, 0x6c, 0x65, + 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x07, 0x77, + 0x6f, 0x6c, 0x66, 0x53, 0x53, 0x4c, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, + 0x55, 0x04, 0x0b, 0x0c, 0x0b, 0x45, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, + 0x03, 0x0c, 0x0f, 0x77, 0x6f, 0x6c, 0x66, 0x53, 0x53, 0x4c, 0x20, 0x72, + 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x31, 0x20, 0x30, 0x1e, 0x06, 0x09, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01, 0x16, 0x11, 0x66, + 0x61, 0x63, 0x74, 0x73, 0x40, 0x77, 0x6f, 0x6c, 0x66, 0x73, 0x73, 0x6c, + 0x2e, 0x63, 0x6f, 0x6d, 0x18, 0x0f, 0x32, 0x30, 0x32, 0x36, 0x30, 0x36, + 0x32, 0x34, 0x31, 0x36, 0x33, 0x37, 0x30, 0x35, 0x5a, 0x30, 0x4f, 0x30, + 0x4d, 0x30, 0x38, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, + 0x04, 0x14, 0x7a, 0x34, 0xec, 0xb3, 0x2b, 0x4f, 0x1b, 0xa2, 0x72, 0x22, + 0x92, 0xa8, 0x4c, 0xc0, 0x12, 0xc7, 0x7a, 0x56, 0x9e, 0x20, 0x04, 0x14, + 0x73, 0xb0, 0x1c, 0xa4, 0x2f, 0x82, 0xcb, 0xcf, 0x47, 0xa5, 0x38, 0xd7, + 0xb0, 0x04, 0x82, 0x3a, 0x7e, 0x72, 0x15, 0x21, 0x02, 0x01, 0x01, 0x82, + 0x00, 0x18, 0x0f, 0x32, 0x30, 0x32, 0x36, 0x30, 0x36, 0x32, 0x34, 0x31, + 0x36, 0x33, 0x37, 0x30, 0x35, 0x5a, 0x30, 0x0b, 0x06, 0x09, 0x2a, 0x86, + 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x03, 0x82, 0x01, 0x01, 0x00, + 0x1c, 0x64, 0xcb, 0x8a, 0x8e, 0x9c, 0x63, 0x4e, 0xd5, 0x6d, 0xbc, 0xac, + 0x92, 0x78, 0x02, 0xf2, 0xb5, 0x40, 0x93, 0x38, 0x5d, 0x28, 0x6c, 0x05, + 0x57, 0x81, 0xf3, 0x8b, 0xd7, 0x0e, 0xbb, 0xfc, 0xee, 0x63, 0x9a, 0x4a, + 0x02, 0x04, 0xd9, 0xa2, 0x61, 0x0f, 0x8e, 0x44, 0xb6, 0x48, 0xf4, 0xfe, + 0x23, 0xab, 0xd6, 0x0c, 0x92, 0xe3, 0x8e, 0xcf, 0x36, 0xf1, 0x7b, 0x6c, + 0xf7, 0x99, 0xb0, 0x83, 0xa0, 0xbd, 0x66, 0x98, 0x02, 0xb0, 0x4b, 0x7a, + 0xf7, 0x77, 0x6e, 0x47, 0xa4, 0xd0, 0x27, 0x2e, 0xa0, 0xbd, 0xb0, 0xa7, + 0xfa, 0xb6, 0x8d, 0x84, 0xef, 0x3a, 0x38, 0xa3, 0x6b, 0x26, 0x73, 0xc0, + 0xd6, 0xef, 0x2f, 0xf9, 0x1b, 0xef, 0x01, 0x2c, 0x4e, 0x36, 0x8d, 0x9b, + 0x45, 0x58, 0xb3, 0x97, 0x46, 0x9f, 0xb5, 0xd2, 0x5a, 0x7d, 0xa6, 0x47, + 0x37, 0xd6, 0xa4, 0x3b, 0x61, 0x97, 0x20, 0xd3, 0x32, 0x0e, 0x7f, 0xac, + 0x76, 0x62, 0x19, 0xab, 0x74, 0x71, 0x7b, 0x89, 0x75, 0xfa, 0x3f, 0x89, + 0xe7, 0xf2, 0x55, 0xeb, 0x32, 0xce, 0xe2, 0x55, 0x98, 0x0b, 0x67, 0x9a, + 0x94, 0x48, 0x95, 0x8e, 0xa3, 0x61, 0x8a, 0x4c, 0x2e, 0xe9, 0xbe, 0x65, + 0xe5, 0x7c, 0x9e, 0x5f, 0xcc, 0xeb, 0x74, 0xee, 0xb2, 0x59, 0x5a, 0x03, + 0xa0, 0xbd, 0xcf, 0x06, 0x95, 0x6b, 0x34, 0x47, 0x19, 0x7e, 0xd0, 0xb4, + 0xcc, 0xf6, 0xb3, 0xdc, 0x46, 0x74, 0x0a, 0x9a, 0x28, 0x57, 0xba, 0x46, + 0x6f, 0xfc, 0x24, 0xcd, 0x82, 0x20, 0x1a, 0x1c, 0x74, 0x0c, 0x37, 0x8e, + 0x22, 0x1f, 0x00, 0x9c, 0x66, 0x4f, 0xf5, 0xbb, 0xeb, 0xe1, 0x7f, 0x0e, + 0xb0, 0x39, 0xde, 0xd8, 0xf2, 0x56, 0xe0, 0xc2, 0xa2, 0x95, 0xbe, 0xad, + 0x9c, 0x10, 0x56, 0x28, 0x8e, 0x50, 0x79, 0x2f, 0xb6, 0xbf, 0x31, 0x6e, + 0x71, 0x53, 0x13, 0x63, 0xa0, 0x82, 0x04, 0xf1, 0x30, 0x82, 0x04, 0xed, + 0x30, 0x82, 0x04, 0xe9, 0x30, 0x82, 0x03, 0xd1, 0xa0, 0x03, 0x02, 0x01, + 0x02, 0x02, 0x01, 0x63, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x81, 0x98, 0x31, 0x0b, + 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, + 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, 0x0a, 0x57, 0x61, + 0x73, 0x68, 0x69, 0x6e, 0x67, 0x74, 0x6f, 0x6e, 0x31, 0x10, 0x30, 0x0e, + 0x06, 0x03, 0x55, 0x04, 0x07, 0x0c, 0x07, 0x53, 0x65, 0x61, 0x74, 0x74, + 0x6c, 0x65, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, + 0x07, 0x77, 0x6f, 0x6c, 0x66, 0x53, 0x53, 0x4c, 0x31, 0x14, 0x30, 0x12, + 0x06, 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x0b, 0x45, 0x6e, 0x67, 0x69, 0x6e, + 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, + 0x55, 0x04, 0x03, 0x0c, 0x0f, 0x77, 0x6f, 0x6c, 0x66, 0x53, 0x53, 0x4c, + 0x20, 0x72, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x31, 0x20, 0x30, 0x1e, + 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01, 0x16, + 0x11, 0x66, 0x61, 0x63, 0x74, 0x73, 0x40, 0x77, 0x6f, 0x6c, 0x66, 0x73, + 0x73, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x36, + 0x30, 0x36, 0x31, 0x31, 0x32, 0x31, 0x34, 0x34, 0x33, 0x34, 0x5a, 0x17, + 0x0d, 0x32, 0x39, 0x30, 0x33, 0x30, 0x37, 0x32, 0x31, 0x34, 0x34, 0x33, + 0x34, 0x5a, 0x30, 0x81, 0x98, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, + 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, + 0x55, 0x04, 0x08, 0x0c, 0x0a, 0x57, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, + 0x74, 0x6f, 0x6e, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x07, + 0x0c, 0x07, 0x53, 0x65, 0x61, 0x74, 0x74, 0x6c, 0x65, 0x31, 0x10, 0x30, + 0x0e, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x07, 0x77, 0x6f, 0x6c, 0x66, + 0x53, 0x53, 0x4c, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x0b, + 0x0c, 0x0b, 0x45, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x65, 0x72, 0x69, 0x6e, + 0x67, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x0f, + 0x77, 0x6f, 0x6c, 0x66, 0x53, 0x53, 0x4c, 0x20, 0x72, 0x6f, 0x6f, 0x74, + 0x20, 0x43, 0x41, 0x31, 0x20, 0x30, 0x1e, 0x06, 0x09, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01, 0x16, 0x11, 0x66, 0x61, 0x63, 0x74, + 0x73, 0x40, 0x77, 0x6f, 0x6c, 0x66, 0x73, 0x73, 0x6c, 0x2e, 0x63, 0x6f, + 0x6d, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, + 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xab, 0x2c, + 0xb4, 0x2f, 0x1d, 0x06, 0x09, 0xef, 0x4e, 0x29, 0x86, 0x84, 0x7e, 0xcc, + 0xbf, 0xa6, 0x79, 0x7c, 0xf0, 0xc0, 0xc1, 0x64, 0x25, 0x8c, 0x75, 0xb7, + 0x10, 0x05, 0xca, 0x48, 0x27, 0x0c, 0x0e, 0x32, 0x1c, 0xb0, 0xfe, 0x99, + 0x85, 0x39, 0xb6, 0xb9, 0xa2, 0xf7, 0x27, 0xff, 0x6d, 0x3c, 0x8c, 0x16, + 0x73, 0x29, 0x21, 0x7f, 0x8b, 0xa6, 0x54, 0x71, 0x90, 0xad, 0xcc, 0x05, + 0xb9, 0x9f, 0x15, 0xc7, 0x0a, 0x3f, 0x5f, 0x69, 0xf4, 0x0a, 0x5f, 0x8c, + 0x71, 0xb5, 0x2c, 0xbf, 0x66, 0xe2, 0x03, 0x9a, 0x32, 0xf4, 0xd2, 0xec, + 0x2a, 0x89, 0x4b, 0xf9, 0x35, 0x88, 0x14, 0x33, 0x47, 0x4e, 0x2e, 0x05, + 0x79, 0x01, 0xed, 0x64, 0x36, 0x76, 0xb9, 0xf8, 0x85, 0xcd, 0x01, 0x88, + 0xac, 0xc5, 0xb2, 0xb1, 0x59, 0xb8, 0xcd, 0x5a, 0xf4, 0x09, 0x09, 0x38, + 0x9b, 0xda, 0x5a, 0xcf, 0xce, 0x78, 0x99, 0x1f, 0x49, 0x3d, 0x41, 0xd6, + 0x06, 0x7c, 0x52, 0x99, 0xc8, 0x97, 0xd1, 0xb3, 0x80, 0x3a, 0xa2, 0x4f, + 0x36, 0xc4, 0xc5, 0x96, 0x30, 0x77, 0x31, 0x38, 0xc8, 0x70, 0xcc, 0xe1, + 0x67, 0x06, 0xb3, 0x2b, 0x2f, 0x93, 0xb5, 0x69, 0xcf, 0x83, 0x7e, 0x88, + 0x53, 0x9b, 0x0f, 0x46, 0x21, 0x4c, 0xd6, 0x05, 0x36, 0x44, 0x99, 0x60, + 0x68, 0x47, 0xe5, 0x32, 0x01, 0x12, 0xd4, 0x10, 0x73, 0xae, 0x9a, 0x34, + 0x94, 0xfa, 0x6e, 0xb8, 0x58, 0x4f, 0x7b, 0x5b, 0x8a, 0x92, 0x97, 0xad, + 0xfd, 0x97, 0xb9, 0x75, 0xca, 0xc2, 0xd4, 0x45, 0x7d, 0x17, 0x6b, 0xcd, + 0x2f, 0xf3, 0x63, 0x7a, 0x0e, 0x30, 0xb5, 0x0b, 0xa9, 0xd9, 0xa6, 0x7c, + 0x74, 0x60, 0x9d, 0xcc, 0x09, 0x03, 0x43, 0xf1, 0x0f, 0x90, 0xd3, 0xb7, + 0xfe, 0x6c, 0x9f, 0xd9, 0xcd, 0x78, 0x4b, 0x15, 0xae, 0x8c, 0x5b, 0xf9, + 0x99, 0x81, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x82, 0x01, 0x3a, 0x30, + 0x82, 0x01, 0x36, 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x04, 0x05, + 0x30, 0x03, 0x01, 0x01, 0xff, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, + 0x04, 0x16, 0x04, 0x14, 0x73, 0xb0, 0x1c, 0xa4, 0x2f, 0x82, 0xcb, 0xcf, + 0x47, 0xa5, 0x38, 0xd7, 0xb0, 0x04, 0x82, 0x3a, 0x7e, 0x72, 0x15, 0x21, + 0x30, 0x81, 0xc5, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x81, 0xbd, 0x30, + 0x81, 0xba, 0x80, 0x14, 0x73, 0xb0, 0x1c, 0xa4, 0x2f, 0x82, 0xcb, 0xcf, + 0x47, 0xa5, 0x38, 0xd7, 0xb0, 0x04, 0x82, 0x3a, 0x7e, 0x72, 0x15, 0x21, + 0xa1, 0x81, 0x9e, 0xa4, 0x81, 0x9b, 0x30, 0x81, 0x98, 0x31, 0x0b, 0x30, + 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x13, + 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, 0x0a, 0x57, 0x61, 0x73, + 0x68, 0x69, 0x6e, 0x67, 0x74, 0x6f, 0x6e, 0x31, 0x10, 0x30, 0x0e, 0x06, + 0x03, 0x55, 0x04, 0x07, 0x0c, 0x07, 0x53, 0x65, 0x61, 0x74, 0x74, 0x6c, + 0x65, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x07, + 0x77, 0x6f, 0x6c, 0x66, 0x53, 0x53, 0x4c, 0x31, 0x14, 0x30, 0x12, 0x06, + 0x03, 0x55, 0x04, 0x0b, 0x0c, 0x0b, 0x45, 0x6e, 0x67, 0x69, 0x6e, 0x65, + 0x65, 0x72, 0x69, 0x6e, 0x67, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, + 0x04, 0x03, 0x0c, 0x0f, 0x77, 0x6f, 0x6c, 0x66, 0x53, 0x53, 0x4c, 0x20, + 0x72, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x31, 0x20, 0x30, 0x1e, 0x06, + 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01, 0x16, 0x11, + 0x66, 0x61, 0x63, 0x74, 0x73, 0x40, 0x77, 0x6f, 0x6c, 0x66, 0x73, 0x73, + 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x82, 0x01, 0x63, 0x30, 0x0b, 0x06, 0x03, + 0x55, 0x1d, 0x0f, 0x04, 0x04, 0x03, 0x02, 0x01, 0x06, 0x30, 0x32, 0x06, + 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01, 0x04, 0x26, 0x30, + 0x24, 0x30, 0x22, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, + 0x01, 0x86, 0x16, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x32, + 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x3a, 0x32, 0x32, 0x32, 0x32, + 0x30, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x36, 0xb1, 0x86, + 0xd6, 0x72, 0xf8, 0xe7, 0x6a, 0xae, 0x43, 0xfb, 0xc0, 0xed, 0xf1, 0x36, + 0x64, 0x8a, 0xda, 0x8e, 0x5f, 0xf7, 0xc4, 0xad, 0x64, 0xc8, 0x29, 0x03, + 0x74, 0x58, 0xb0, 0x9e, 0xee, 0x41, 0x89, 0x5b, 0x2a, 0x12, 0xf4, 0x82, + 0xa4, 0x03, 0xa5, 0xf0, 0xdf, 0xa2, 0x84, 0xcb, 0x2b, 0xb3, 0x16, 0x0f, + 0xdc, 0xcf, 0xcc, 0x56, 0x99, 0x61, 0xa9, 0xf9, 0x3d, 0x3a, 0x7e, 0xe4, + 0x12, 0x43, 0xc3, 0xb1, 0x4f, 0x58, 0x26, 0x79, 0xe7, 0xe4, 0x0d, 0xa5, + 0x88, 0x3d, 0x79, 0x33, 0xa1, 0x09, 0x7d, 0x78, 0xaf, 0xbd, 0x59, 0x71, + 0x11, 0x54, 0x4a, 0xcc, 0xd6, 0xd2, 0x6d, 0x1f, 0x88, 0x27, 0xac, 0xd5, + 0xbf, 0x75, 0xfc, 0xc3, 0x05, 0x0b, 0xcd, 0xc8, 0x0e, 0x72, 0x41, 0x1d, + 0xd8, 0x68, 0x62, 0xbe, 0x94, 0xd7, 0x60, 0xbe, 0x05, 0x4a, 0x42, 0x9c, + 0x50, 0xb7, 0x45, 0x71, 0x6d, 0x83, 0x9a, 0xef, 0x08, 0x5c, 0x41, 0xdb, + 0xc8, 0x62, 0x33, 0x3c, 0x69, 0xa2, 0x8a, 0xb4, 0x0f, 0xdb, 0x65, 0xc4, + 0xb7, 0x92, 0x0a, 0x76, 0xf7, 0x55, 0x06, 0x77, 0x8c, 0xff, 0x8c, 0x84, + 0x84, 0xd9, 0xdd, 0x46, 0x11, 0x2a, 0x2d, 0x27, 0x96, 0xa7, 0xf5, 0x47, + 0xc1, 0x43, 0x4b, 0xfe, 0x53, 0xd8, 0xbe, 0x16, 0x94, 0x36, 0x0a, 0xd4, + 0xbe, 0xc3, 0x6c, 0x9b, 0x0c, 0x52, 0x31, 0x4a, 0xeb, 0x62, 0xb4, 0x81, + 0x4b, 0x2d, 0xf7, 0xf1, 0x65, 0xc1, 0xee, 0x36, 0x79, 0x19, 0xf7, 0xab, + 0x16, 0xf8, 0x38, 0xd2, 0xea, 0x87, 0x8d, 0xf8, 0xf9, 0x14, 0x82, 0xdb, + 0x67, 0xb6, 0x94, 0xa8, 0x55, 0x0b, 0x90, 0x6b, 0xaf, 0xb0, 0xe9, 0x42, + 0x64, 0x42, 0x6d, 0x2c, 0xe3, 0xf1, 0xb6, 0xe0, 0xf9, 0x58, 0xed, 0x69, + 0x66, 0x62, 0x98, 0xdc, 0x5a, 0x7b, 0xfa, 0x35, 0x5a, 0x23, 0x84, 0x91, + 0x0e, +}; + +enum { + CSROCSP_MODE_UNKNOWN = 0, + CSROCSP_MODE_LOOKUP_FAIL, + CSROCSP_MODE_WANT_READ +}; + +TEST_TLS_BOUNDS_UNUSED +static int test_ProcessChainOCSPRequest_io_cb(void* ctx, const char* url, + int urlSz, unsigned char* req, int reqSz, unsigned char** respBuf) +{ + int mode = *(int*)ctx; + static const unsigned char garbage[] = { 0xFF, 0x00, 0x11, 0x22 }; + (void)url; (void)urlSz; (void)req; (void)reqSz; + + switch (mode) { + case CSROCSP_MODE_UNKNOWN: + *respBuf = (unsigned char*)csrocsp_resp_unknown; + return (int)sizeof(csrocsp_resp_unknown); + case CSROCSP_MODE_LOOKUP_FAIL: + *respBuf = (unsigned char*)garbage; + return (int)sizeof(garbage); + case CSROCSP_MODE_WANT_READ: + default: + return WOLFSSL_CBIO_ERR_WANT_READ; + } +} + +/* Common setup for the "one real, matching chain entry" vectors: server1 + * (leaf) + intermediate1 (the chain entry ProcessChainOCSPRequest() will + * process) issued off root-ca, matching the identity the csrocsp_resp_* + * fixtures above were generated against. */ +TEST_TLS_BOUNDS_UNUSED +static int test_ProcessChainOCSPRequest_setup(WOLFSSL_CTX** pctx, + WOLFSSL** pssl, CertificateStatusRequest** pcsr, int* mode) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + TLSX* ext = NULL; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_use_certificate_chain_file(ctx, + "./certs/ocsp/server1-chain-noroot.pem"), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx, + "./certs/ocsp/server1-key.pem", WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx, + "./certs/ocsp/root-ca-cert.pem", NULL), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(ctx), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSP(ctx, WOLFSSL_OCSP_URL_OVERRIDE | + WOLFSSL_OCSP_NO_NONCE), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_SetOCSP_OverrideURL(ctx, "http://dummy.test"), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_SetOCSP_Cb(ctx, test_ProcessChainOCSPRequest_io_cb, + NULL, (void*)mode), WOLFSSL_SUCCESS); + + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(TLSX_UseCertificateStatusRequest(&ssl->extensions, + WOLFSSL_CSR_OCSP, 0, ssl, ssl->heap, ssl->devId), + WOLFSSL_SUCCESS); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, TLSX_STATUS_REQUEST)); + if (ext != NULL) + *pcsr = (CertificateStatusRequest*)ext->data; + } + + *pctx = ctx; + *pssl = ssl; + return EXPECT_RESULT(); +} +#endif + +int test_ProcessChainOCSPRequest_bounds(void) +{ +#if defined(TEST_TLS_BOUNDS_OCSP_CHAIN) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + CertificateStatusRequest* csr = NULL; + int mode; + byte garbageThenValid[8 + 3 + 4096]; + byte intermediate1Der[4096]; + word32 interLen = 0; + XFILE f = XBADFILE; + DerBuffer fakeChain; + + /* chain == NULL (both certChain and certificate unset): the leading + * "chain && chain->buffer" guard is false at its first operand, the + * whole walk is skipped, and the function returns success with + * nothing done. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(test_tls_bounds_load_server_cert(ctx), TEST_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* ext = NULL; + ExpectIntEQ(TLSX_UseCertificateStatusRequest(&ssl->extensions, + WOLFSSL_CSR_OCSP, 0, ssl, ssl->heap, ssl->devId), + WOLFSSL_SUCCESS); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, TLSX_STATUS_REQUEST)); + if (ext != NULL) + csr = (CertificateStatusRequest*)ext->data; + /* A certificate had to be loaded for wolfSSL_new() to succeed (see + * test_tls_bounds_load_server_cert()); clear both buffers back to + * NULL so ProcessChainOCSPRequest() sees exactly the "chain == + * NULL" state under test. */ + ssl->buffers.certChain = NULL; + ssl->buffers.certificate = NULL; + ExpectIntEQ(ProcessChainOCSPRequest(ssl), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + ctx = NULL; + + /* chain != NULL, chain->buffer == NULL: second operand false, + * independence from the chain == NULL vector above (first operand). */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(test_tls_bounds_load_server_cert(ctx), TEST_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* ext = NULL; + ExpectIntEQ(TLSX_UseCertificateStatusRequest(&ssl->extensions, + WOLFSSL_CSR_OCSP, 0, ssl, ssl->heap, ssl->devId), + WOLFSSL_SUCCESS); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, TLSX_STATUS_REQUEST)); + if (ext != NULL) + csr = (CertificateStatusRequest*)ext->data; + XMEMSET(&fakeChain, 0, sizeof(fakeChain)); + ssl->buffers.certChain = &fakeChain; + ExpectIntEQ(ProcessChainOCSPRequest(ssl), 0); + ssl->buffers.certChain = NULL; + } + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + ctx = NULL; + + /* while-loop operand 0 (ret == 0) false side: a two-entry chain whose + * first entry is not a certificate at all, so CreateOcspRequest() fails + * with a hard ASN.1 parse error - not one of the soft-fail codes the + * loop body resets to 0 - while its second (valid) entry is still + * unconsumed, forcing the while condition's re-check to see ret != 0 + * with more chain data pending. */ + ExpectNotNull(f = XFOPEN("./certs/ocsp/intermediate1-ca-cert.der", "rb")); + if (f != XBADFILE) { + interLen = (word32)XFREAD(intermediate1Der, 1, sizeof(intermediate1Der), + f); + XFCLOSE(f); + } + ExpectIntGT(interLen, 0); + ExpectIntLT(interLen, sizeof(intermediate1Der)); + { + word32 idx = 0; + static const byte notACert[] = { 0xFF, 0xFF, 0xFF, 0xFF }; + + test_tls_bounds_c32to24(sizeof(notACert), garbageThenValid + idx); + idx += OPAQUE24_LEN; + XMEMCPY(garbageThenValid + idx, notACert, sizeof(notACert)); + idx += sizeof(notACert); + + test_tls_bounds_c32to24(interLen, garbageThenValid + idx); + idx += OPAQUE24_LEN; + XMEMCPY(garbageThenValid + idx, intermediate1Der, interLen); + idx += interLen; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(test_tls_bounds_load_server_cert(ctx), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(ctx), WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* ext = NULL; + ExpectIntEQ(TLSX_UseCertificateStatusRequest(&ssl->extensions, + WOLFSSL_CSR_OCSP, 0, ssl, ssl->heap, ssl->devId), + WOLFSSL_SUCCESS); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, + TLSX_STATUS_REQUEST)); + if (ext != NULL) + csr = (CertificateStatusRequest*)ext->data; + + XMEMSET(&fakeChain, 0, sizeof(fakeChain)); + fakeChain.buffer = garbageThenValid; + fakeChain.length = idx; + ssl->buffers.certChain = &fakeChain; + + /* Not zero (ASN parse error) and not one of ProcessChainOCSPRequest's + * own soft-fail codes, so it propagates as the loop's exit ret. */ + ExpectIntNE(ProcessChainOCSPRequest(ssl), 0); + ExpectIntNE(ProcessChainOCSPRequest(ssl), + WC_NO_ERR_TRACE(OCSP_CERT_UNKNOWN)); + ExpectIntNE(ProcessChainOCSPRequest(ssl), + WC_NO_ERR_TRACE(OCSP_LOOKUP_FAIL)); + ExpectIntNE(ProcessChainOCSPRequest(ssl), + WC_NO_ERR_TRACE(OCSP_NO_URL)); + + ssl->buffers.certChain = NULL; + } + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + ctx = NULL; + } + + /* One real, matching chain entry (intermediate1): the while loop enters + * (operand 0 and operand 1 both true) and, after processing the single + * entry, exits normally with pos == chain->length (operand 1 false, + * operand 0 still true) - independence for operand 1, paired against + * the forced-error vector above (operand 0 false). Run three times with + * a different responder outcome each time (a fresh CertManager each + * time, so no cached status from one run leaks into the next), driving + * the three-way soft-fail classification. */ + mode = CSROCSP_MODE_UNKNOWN; + ExpectIntEQ(test_ProcessChainOCSPRequest_setup(&ctx, &ssl, &csr, &mode), + TEST_SUCCESS); + if (csr != NULL) { + /* ret is OCSP_CERT_UNKNOWN: first operand of the three-way OR. */ + ExpectIntEQ(ProcessChainOCSPRequest(ssl), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + ctx = NULL; + csr = NULL; + + mode = CSROCSP_MODE_LOOKUP_FAIL; + ExpectIntEQ(test_ProcessChainOCSPRequest_setup(&ctx, &ssl, &csr, &mode), + TEST_SUCCESS); + if (csr != NULL) { + /* ret is OCSP_LOOKUP_FAIL (garbage response bytes fail to decode): + * second operand true, first false - independence for the second + * operand. */ + ExpectIntEQ(ProcessChainOCSPRequest(ssl), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + ctx = NULL; + csr = NULL; + + /* ret is OCSP_NO_URL: reached without any responder call at all, by + * leaving the override URL unset and using a chain (server-cert.pem + + * ca-cert.pem) whose certificates carry no OCSP responder AIA URL. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_use_certificate_chain_file(ctx, + "./certs/server-cert.pem"), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(ctx, + "./certs/server-key.pem", WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx, + "./certs/ca-cert.pem", NULL), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(ctx), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSP(ctx, WOLFSSL_OCSP_NO_NONCE), + WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* ext = NULL; + ExpectIntEQ(TLSX_UseCertificateStatusRequest(&ssl->extensions, + WOLFSSL_CSR_OCSP, 0, ssl, ssl->heap, ssl->devId), + WOLFSSL_SUCCESS); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, TLSX_STATUS_REQUEST)); + if (ext != NULL) + csr = (CertificateStatusRequest*)ext->data; + ExpectNotNull(ssl->buffers.certChain); + if (csr != NULL) { + /* ret is OCSP_NO_URL: third operand true, first and second + * false - independence for the third operand. */ + ExpectIntEQ(ProcessChainOCSPRequest(ssl), 0); + } + } + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + ctx = NULL; + csr = NULL; + + /* ret is OCSP_WANT_READ (via WOLFSSL_CBIO_ERR_WANT_READ from the I/O + * callback): none of the three - the baseline all-false vector paired + * against each of the three above. */ + mode = CSROCSP_MODE_WANT_READ; + ExpectIntEQ(test_ProcessChainOCSPRequest_setup(&ctx, &ssl, &csr, &mode), + TEST_SUCCESS); + if (csr != NULL) { + ExpectIntEQ(ProcessChainOCSPRequest(ssl), + WC_NO_ERR_TRACE(OCSP_WANT_READ)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_PopulateExtensions() - called directly (WOLFSSL_LOCAL), the same way + * SendClientHello()/SendTls13ClientHello() call it before any handshake + * bytes are produced, so a bare freshly-created ssl is exactly the state a + * real caller reaches this function with. + * + * "16880:17:16880:60:0" - "if (!isServer && ssl->options.postHandshakeAuth)" + * - is not covered here and is excluded rather than driven: it sits inside + * "if (!isServer && IsAtLeastTLSv1_3(ssl->version)) { ... }" (opened many + * lines above, never re-assigning isServer in between), so reaching this + * line at all already requires isServer == 0. Its own "!isServer" operand + * is therefore fixed true on every execution that reaches it - excluded + * (family 3: fixed by the branch that reaches it). */ +#ifdef TEST_TLS_BOUNDS_POPULATE_EXT +TEST_TLS_BOUNDS_UNUSED +static unsigned int test_TLSX_PopulateExtensions_psk_cb(WOLFSSL* ssl, + const char* hint, char* identity, unsigned int id_max_len, + unsigned char* key, unsigned int key_max_len) +{ + int mode = *(int*)wolfSSL_get_psk_callback_ctx(ssl); + (void)hint; (void)key_max_len; + + XSTRNCPY(identity, "id", id_max_len); + key[0] = 0x01; + + /* mode 0: > MAX_PSK_KEY_LEN and not equal to USE_HW_PSK - both operands true. + * mode 1: > MAX_PSK_KEY_LEN and equal to USE_HW_PSK - operand 0 true, operand + * 1 false. + * mode 2: <= MAX_PSK_KEY_LEN - operand 0 false, short-circuits. */ + switch (mode) { + case 0: + return MAX_PSK_KEY_LEN + 1; + case 1: + return (unsigned int)WC_NO_ERR_TRACE(USE_HW_PSK); + default: + return 4; + } +} + +TEST_TLS_BOUNDS_UNUSED +static unsigned int test_TLSX_PopulateExtensions_psk_tls13_cb(WOLFSSL* ssl, + const char* hint, char* identity, unsigned int id_max_len, + unsigned char* key, unsigned int key_max_len, const char** ciphersuite) +{ + (void)ssl; (void)hint; (void)key_max_len; + XSTRNCPY(identity, "id", id_max_len); + key[0] = 0x01; + *ciphersuite = "TLS13-AES128-GCM-SHA256"; + return 4; +} +#endif + +int test_TLSX_PopulateExtensions_bounds(void) +{ +#if defined(TEST_TLS_BOUNDS_POPULATE_EXT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + int mode; + + /* Operand 1 of "!ssl->options.userCurves && !ssl->ctx->userCurves": + * ssl->options.userCurves inherits from ctx->userCurves at wolfSSL_new() + * (InitSSL()), so a ctx with a user curve list makes both true unless + * the ssl-level copy is forced back to 0 - no public caller can produce + * "ctx->userCurves set, ssl->options.userCurves clear" any other way. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectIntEQ(wolfSSL_CTX_UseSupportedCurve(ctx, WOLFSSL_ECC_SECP256R1), + WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(ssl->options.userCurves, 1); + ssl->options.userCurves = 0; + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + ExpectNull(TLSX_Find(ssl->extensions, TLSX_SUPPORTED_GROUPS)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* The point-format decision's four leaf conditions are + * "(!IsAtLeastTLSv1_3(ssl->version) || ssl->options.downgrade) && + * TLSX_Find(ssl->ctx->extensions, TLSX_EC_POINT_FORMATS) == NULL && + * TLSX_Find(ssl->extensions, TLSX_EC_POINT_FORMATS) == NULL" + * (operand indices 0-3); the worklist's ":2" is the ctx-level Find, not + * the ssl-level one (index 3, already paired elsewhere). TLS 1.2 keeps + * operand 0 true throughout; ssl->extensions is left without the + * extension in both vectors below so operand 3 stays fixed true while + * only the ctx-level Find (operand 2) is flipped. */ +#if defined(HAVE_ECC) || defined(HAVE_CURVE25519) || defined(HAVE_CURVE448) + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* ctx-level Find != NULL: operand 2 false. */ + ExpectIntEQ(TLSX_UsePointFormat(&ssl->ctx->extensions, + WOLFSSL_EC_PF_UNCOMPRESSED, ssl->ctx->heap), + WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + ExpectNull(TLSX_Find(ssl->extensions, TLSX_EC_POINT_FORMATS)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* ctx-level Find == NULL (default, fresh ctx): operand 2 true - + * independence for operand 2, paired against the vector above. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + ExpectNotNull(TLSX_Find(ssl->extensions, TLSX_EC_POINT_FORMATS)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + + /* Operand 1 of "ssl->options.resuming && ssl->session->namedGroup != 0": + * resuming fixed true, namedGroup flipped. A default (non-resuming) call + * is the false/false pair already exercised in ordinary handshake tests + * elsewhere; this isolates the second operand specifically. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ssl->options.resuming = 1; + ssl->session->namedGroup = 0; + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + ssl->session->namedGroup = WOLFSSL_ECC_SECP256R1; + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + ssl->options.resuming = 0; + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Operand 1 of "ssl->options.resuming && ssl->session->ticketLen > 0": + * resuming fixed true, ticketLen flipped. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + byte ticket[4] = { 1, 2, 3, 4 }; + + ssl->options.resuming = 1; + ssl->session->ticketLen = 0; + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + + XMEMCPY(ssl->session->ticket, ticket, sizeof(ticket)); + ssl->session->ticketLen = sizeof(ticket); + /* SetCipherSpecs(ssl) (reached because ticketLen > 0) needs a + * suite it recognizes; a fresh session's cipherSuite0/cipherSuite + * default to 0, which is not one. */ + ssl->session->cipherSuite0 = TLS13_BYTE; + ssl->session->cipherSuite = TLS_AES_128_GCM_SHA256; + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + ssl->options.resuming = 0; + ssl->session->ticketLen = 0; + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Operand 1 of "client_psk_cb != NULL || client_psk_tls13_cb != NULL": + * operand 0 fixed false (no client_psk_cb), operand 1 flipped true by + * registering a tls13 callback that reports "no key available" (a + * 0-length key), which OPENSSL_EXTRA would treat specially but this + * build (no OPENSSL_EXTRA) routes through the same + * "> MAX_PSK_KEY_LEN" guard as any other size, taking its false side. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + wolfSSL_set_psk_client_tls13_callback(ssl, + test_TLSX_PopulateExtensions_psk_tls13_cb); + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Both operands of "psk_keySz > MAX_PSK_KEY_LEN" and "not equal to USE_HW_PSK", + * reached through the plain client_psk_cb path. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + +#ifndef OPENSSL_EXTRA + /* An over-long key is rejected with PSK_KEY_ERROR. Under OPENSSL_EXTRA the + * assignment is compiled out and the handshake carries on with a key size + * larger than the buffer holding it, so this case is not driven there. */ + mode = 0; + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + wolfSSL_set_psk_client_callback(ssl, + test_TLSX_PopulateExtensions_psk_cb); + wolfSSL_set_psk_callback_ctx(ssl, &mode); + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + } + wolfSSL_free(ssl); +#endif + + mode = 1; + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + wolfSSL_set_psk_client_callback(ssl, + test_TLSX_PopulateExtensions_psk_cb); + wolfSSL_set_psk_callback_ctx(ssl, &mode); + /* Operand 0 true, operand 1 false - independence for operand 1. */ + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + } + wolfSSL_free(ssl); + + mode = 2; + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + wolfSSL_set_psk_client_callback(ssl, + test_TLSX_PopulateExtensions_psk_cb); + wolfSSL_set_psk_callback_ctx(ssl, &mode); + /* Operand 0 false - independence for operand 0. */ + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + } + wolfSSL_free(ssl); + + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_PopulateSupportedGroups() is static-in-file, reached only through + * TLSX_PopulateExtensions(). Its ML-KEM blocks all share + * "IsAtLeastTLSv1_3(ssl->version) && TLSX_IsMlKemGroupSupported(ssl->options.side)"; + * the worklist carries only the second operand (the first already has a + * pair elsewhere). TLSX_IsMlKemGroupSupported() is: + * if (side == WOLFSSL_CLIENT_END) return ; + * else if (side == WOLFSSL_SERVER_END) return ; + * else return ; + * and this build has both WOLFSSL_HAVE_MLKEM_CLIENT_SUPPORT and + * WOLFSSL_HAVE_MLKEM_SERVER_SUPPORT defined (neither + * WOLFSSL_MLKEM_NO_MAKE_KEY/_NO_DECAPSULATE/_NO_ENCAPSULATE is set) - so + * every one of its three branches returns 1, for every possible byte value + * of ssl->options.side, not just WOLFSSL_CLIENT_END/_SERVER_END/_NEITHER_END. + * Confirmed by direct call: TLSX_IsMlKemGroupSupported(WOLFSSL_NEITHER_END) + * returns 1 here, not 0. There is no side value - real or corrupted - that + * makes this operand false in this build. Excluded (family 4: contradicted + * by TLSX_IsMlKemGroupSupported()'s own postcondition under this build's + * feature set). */ +int test_TLSX_PopulateSupportedGroups_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && defined(HAVE_SUPPORTED_CURVES) && defined(WOLFSSL_HAVE_MLKEM_CLIENT_SUPPORT) && !defined(WOLFSSL_NO_ML_KEM) && !defined(NO_DH) && defined(HAVE_FFDHE_2048) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + /* Default client: side == WOLFSSL_CLIENT_END, so + * TLSX_IsMlKemGroupSupported() is true for all four ML-KEM blocks + * (its only reachable value in this build - see above). */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + + /* "2048/8 >= minDhKeySz && 2048/8 <= maxDhKeySz": both operands. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + + /* Default range: both operands true. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + wolfSSL_free(ssl); + ssl = NULL; + + /* minDhKeySz raised above 2048 bits: first operand false. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(wolfSSL_SetMinDhKey_Sz(ssl, 4096), WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* maxDhKeySz lowered below 2048 bits, minDhKeySz left at its default + * (below 2048 bits): first operand true, second false - independence + * for the second operand. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(wolfSSL_SetMaxDhKey_Sz(ssl, 1024), WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + } + wolfSSL_free(ssl); + + wolfSSL_CTX_free(ctx); + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_CSR_Parse() (client, TLS 1.3, receiving a CertificateStatus + * response) and TLSX_CSR2_Parse() / TLSX_CSR_Parse() (server, parsing a + * status_request(_v2) request from a ClientHello) - reached through the + * WOLFSSL_TEST_VIS TLSX_Parse(). */ +#ifdef TEST_TLS_BOUNDS_CSR_PARSE +static int test_TLSX_CSR_Parse_fail_after = -1; +static int test_TLSX_CSR_Parse_alloc_seen = 0; + +TEST_TLS_BOUNDS_UNUSED +static void* test_TLSX_CSR_Parse_fail_malloc(size_t size) +{ + if (test_TLSX_CSR_Parse_fail_after >= 0) { + if (test_TLSX_CSR_Parse_alloc_seen == test_TLSX_CSR_Parse_fail_after) { + test_TLSX_CSR_Parse_alloc_seen++; + return NULL; + } + test_TLSX_CSR_Parse_alloc_seen++; + } + return malloc(size); +} + +TEST_TLS_BOUNDS_UNUSED +static void test_TLSX_CSR_Parse_fail_free(void* ptr) +{ + free(ptr); +} + +TEST_TLS_BOUNDS_UNUSED +static void* test_TLSX_CSR_Parse_fail_realloc(void* ptr, size_t size) +{ + return realloc(ptr, size); +} +#endif + +int test_TLSX_CSR_Parse_bounds(void) +{ +#if defined(TEST_TLS_BOUNDS_CSR_PARSE) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[16]; + word16 extLen; + wolfSSL_Malloc_cb prevM = NULL; + wolfSSL_Free_cb prevF = NULL; + wolfSSL_Realloc_cb prevR = NULL; + + /* Server side, isRequest == true: "SSL_CM(ssl) == NULL || + * !SSL_CM(ssl)->ocspStaplingEnabled". A single status_request entry + * (status_type == OCSP, empty responder_id_list and request_extensions) + * with SSL_CM(ssl) forced NULL - first operand true. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_server_method())); + ExpectIntEQ(test_tls_bounds_load_server_cert(ctx), TEST_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + WOLFSSL_CERT_MANAGER* origCM = ssl->ctx->cm; + + /* type(2) + len(2) + status_type(1) + responder_id_list len(2)=0 + + * request_ext len(2)=0 */ + ext[0] = (byte)(TLSXT_STATUS_REQUEST >> 8); + ext[1] = (byte)(TLSXT_STATUS_REQUEST & 0xFF); + ext[2] = 0; ext[3] = 5; /* extension body length */ + ext[4] = WOLFSSL_CSR_OCSP; + ext[5] = 0; ext[6] = 0; + ext[7] = 0; ext[8] = 0; + extLen = 4 + 5; + + ssl->ctx->cm = NULL; + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ssl->ctx->cm = origCM; + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Client side, isRequest == false, TLS 1.3: the internal ticket-style + * malloc for csr->responses[idx].buffer. + * Operand 0 (ret == 0) of "if (ret == 0 && + * csr->responses[ssl->response_idx].buffer == NULL)": forced to true + * by failing that allocation (operand 1 held true), then to false by + * pushing ssl->response_idx out of range so an earlier guard already + * set ret != 0 before this line is reached - independence for operand + * 0, with operand 1's own pair already covered elsewhere. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* type(2) + length(2) + status_type(1) + resp_length(3) + 4 bytes + * of (fake) response data. */ + byte body[HELLO_EXT_TYPE_SZ + OPAQUE16_LEN + OPAQUE8_LEN + + OPAQUE24_LEN + 4]; + word16 bodyLen; + + ExpectIntEQ(TLSX_UseCertificateStatusRequest(&ssl->extensions, + WOLFSSL_CSR_OCSP, 0, ssl, ssl->heap, ssl->devId), + WOLFSSL_SUCCESS); + ssl->options.tls1_3 = 1; + + body[0] = (byte)(TLSXT_STATUS_REQUEST >> 8); + body[1] = (byte)(TLSXT_STATUS_REQUEST & 0xFF); + body[2] = 0; + body[3] = OPAQUE8_LEN + OPAQUE24_LEN + 4; + body[4] = WOLFSSL_CSR_OCSP; + test_tls_bounds_c32to24(4, body + 4 + OPAQUE8_LEN); + XMEMSET(body + 4 + OPAQUE8_LEN + OPAQUE24_LEN, 0xAB, 4); + bodyLen = 4 + OPAQUE8_LEN + OPAQUE24_LEN + 4; + + /* ret == 0, forced allocation failure: operand 0 true, operand 1 + * (buffer == NULL) true - overall true, ret becomes MEMORY_ERROR. */ + ssl->response_idx = 0; + ExpectIntEQ(wolfSSL_GetAllocators(&prevM, &prevF, &prevR), 0); + ExpectIntEQ(wolfSSL_SetAllocators(test_TLSX_CSR_Parse_fail_malloc, + test_TLSX_CSR_Parse_fail_free, + test_TLSX_CSR_Parse_fail_realloc), 0); + test_TLSX_CSR_Parse_alloc_seen = 0; + test_TLSX_CSR_Parse_fail_after = 0; + ExpectIntEQ(TLSX_Parse(ssl, body, bodyLen, certificate, NULL), + WC_NO_ERR_TRACE(MEMORY_ERROR)); + test_TLSX_CSR_Parse_fail_after = -1; + (void)wolfSSL_SetAllocators(prevM, prevF, prevR); + + /* response_idx out of range: an earlier guard sets ret == + * BAD_FUNC_ARG before this decision is reached at all - operand 0 + * false, independence from the vector above. */ + ssl->response_idx = 1 + MAX_CHAIN_DEPTH; + ExpectIntEQ(TLSX_Parse(ssl, body, bodyLen, certificate, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ssl->response_idx = 0; + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +int test_TLSX_CSR2_Parse_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) && !defined(NO_WOLFSSL_SERVER) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[9]; + WOLFSSL_CERT_MANAGER* origCM = NULL; + + /* Server side: "SSL_CM(ssl) == NULL || !SSL_CM(ssl)->ocspStaplingEnabled", + * SSL_CM(ssl) forced NULL - first operand true. One status_request_v2 + * entry (status_type == OCSP, empty responder_id_list/request_extensions). */ + ext[0] = 0; ext[1] = 7; /* overall list size */ + ext[2] = WOLFSSL_CSR2_OCSP; /* status_type */ + ext[3] = 0; ext[4] = 4; /* inner request length */ + ext[5] = 0; ext[6] = 0; /* responder_id_list length */ + ext[7] = 0; ext[8] = 0; /* request_extensions length */ + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_server_method())); + ExpectIntEQ(test_tls_bounds_load_server_cert(ctx), TEST_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + byte full[9 + 4]; + word16 fullLen; + + full[0] = (byte)(TLSXT_STATUS_REQUEST_V2 >> 8); + full[1] = (byte)(TLSXT_STATUS_REQUEST_V2 & 0xFF); + full[2] = 0; full[3] = sizeof(ext); + XMEMCPY(full + 4, ext, sizeof(ext)); + fullLen = 4 + sizeof(ext); + + origCM = ssl->ctx->cm; + ssl->ctx->cm = NULL; + ExpectIntEQ(TLSX_Parse(ssl, full, fullLen, client_hello, suites), 0); + ssl->ctx->cm = origCM; + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_WriteRequest() / TLSX_GetRequestSize() - the "ssl->ctx && + * ssl->ctx->extensions" merge guard. msgType values other than client_hello + * and certificate_request skip the whole leading block (including the + * SSL_CM(ssl) dereference that a NULL ssl->ctx could not survive), landing + * directly on this guard, so ssl->ctx can safely be forced NULL only for + * such a msgType. */ +int test_TLSX_ext_dispatch_ctx_extensions_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + WOLFSSL_CTX* savedCtx = NULL; + byte out[64]; + word32 offset; + word32 len; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* ssl->ctx == NULL: first operand false. server_hello is neither + * client_hello nor certificate_request, so the leading blocks that + * dereference ssl->ctx are skipped entirely before this line. */ + savedCtx = ssl->ctx; + ssl->ctx = NULL; + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, server_hello, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, server_hello, &offset), 0); + ssl->ctx = savedCtx; + + /* ssl->ctx != NULL, ssl->ctx->extensions == NULL (default): second + * operand false, first true. Still server_hello: with HAVE_ECH + * defined (as it is in this build), msgType == client_hello takes + * a wholly different "if (!ssl->options.disableECH && msgType == + * client_hello) { ... } else { }" branch that never + * reaches this guard at all - server_hello is what actually lands + * on it, for every vector here. */ + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, server_hello, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, server_hello, &offset), 0); + + /* ssl->ctx != NULL, ssl->ctx->extensions != NULL: both true. A + * renegotiation-info extension with NULL data writes just its + * length byte. server_hello has isRequest == false, so it also + * needs resp marked or TLSX_GetSize()/TLSX_Write()'s own + * "!isRequest && !extension->resp" guard skips it before ever + * reaching the ssl->ctx merge under test. */ + { + TLSX* ext; + ExpectIntEQ(TLSX_Push(&ssl->ctx->extensions, + TLSX_RENEGOTIATION_INFO, NULL, ssl->ctx->heap), 0); + ExpectNotNull(ext = TLSX_Find(ssl->ctx->extensions, + TLSX_RENEGOTIATION_INFO)); + if (ext != NULL) + ext->resp = 1; + } + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, server_hello, &len), 0); + ExpectTrue(len > 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, server_hello, &offset), 0); + ExpectTrue(offset > 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_WriteRequest()'s and TLSX_GetRequestSize()'s extended-master-secret + * tails - both "msgType == client_hello && ssl->options.haveEMS && + * (!IsAtLeastTLSv1_3(ssl->version) || ssl->options.downgrade)" - only the + * second operand (haveEMS) is open on each; the worklist's ":1" suffix. The + * first (msgType) and third (version/downgrade) already have pairs + * elsewhere. Both tails sit after the HAVE_ECH branch merges back together, + * so client_hello reaches them the same way in either function. */ +int test_TLSX_WriteRequest_ems_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(HAVE_EXTENDED_MASTER) && !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte out[64]; + word32 offset; + word32 len; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* haveEMS == 0 (HAVE_EXTENDED_MASTER defaults it to 1; forced off + * here): second operand false. */ + ssl->options.haveEMS = 0; + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, client_hello, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), 0); + + /* haveEMS == 1: second operand true - independence for the second + * operand, first and third held at the same values as above. */ + ssl->options.haveEMS = 1; + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, client_hello, &len), 0); + ExpectTrue(len >= HELLO_EXT_SZ); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), 0); + ExpectTrue(offset > 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_WriteRequest()'s trailing length-prefix guard - + * "offset > OPAQUE16_LEN || msgType != client_hello". */ +int test_TLSX_WriteRequest_length_prefix_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte out[64]; + word32 offset; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* offset == OPAQUE16_LEN (nothing written), msgType == client_hello: + * both operands false - the length prefix is left unwritten. A + * fixed TLS 1.2 method with EMS explicitly off, and the + * empty renegotiation_info InitSSL() advertises by default for + * every client (SetupClientSecureRenegotiation()) removed, keeps + * this ssl from writing anything else of its own accord. */ + ssl->options.haveEMS = 0; + TLSX_Remove(&ssl->extensions, TLSX_RENEGOTIATION_INFO, ssl->heap); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), + 0); + ExpectIntEQ(offset, OPAQUE16_LEN); + + /* offset == OPAQUE16_LEN, msgType == certificate_request: second + * operand true, first false - independence for the second + * operand. */ + offset = 0; +#if defined(WOLFSSL_TLS13) && !defined(NO_CERTS) + ExpectIntEQ(TLSX_WriteRequest(ssl, out, certificate_request, &offset), + 0); +#endif + + /* offset > OPAQUE16_LEN, msgType == client_hello: first operand + * true - independence for the first operand. A pushed + * renegotiation-info extension guarantees at least one byte gets + * written. */ + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_RENEGOTIATION_INFO, + NULL, ssl->heap), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), 0); + ExpectTrue(offset > OPAQUE16_LEN); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_WriteResponse() - "TLSX_SupportExtensions(ssl) && output" (its own + * independence pair, separate from TLSX_WriteRequest()'s call to the same + * predicate) and the extended-master-secret tail + * "ssl->options.haveEMS && msgType == server_hello && + * !IsAtLeastTLSv1_3(ssl->version)" (operands 1 and 2; operand 0 already + * has a pair elsewhere). TLSX_GetResponseSize() has the identical EMS tail + * one call earlier and is driven by the same three vectors. Both are + * WOLFSSL_LOCAL, reachable directly in this static build. */ +int test_TLSX_WriteResponse_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(HAVE_EXTENDED_MASTER) && !defined(NO_WOLFSSL_SERVER) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte out[64]; + word16 offset; + word16 len; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_server_method())); + ExpectIntEQ(test_tls_bounds_load_server_cert(ctx), TEST_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* TLSX_SupportExtensions(ssl) false (corrupted version.major, the + * only way to make it false - see test_TLSX_SupportExtensions_bounds + * above), output != NULL: first operand false. */ + ssl->version.major = 0; + offset = 0; + ExpectIntEQ(TLSX_WriteResponse(ssl, out, server_hello, &offset), 0); + ExpectIntEQ(offset, 0); + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = TLSv1_2_MINOR; + + /* TLSX_SupportExtensions(ssl) true, output == NULL: second operand + * false, first true - independence for the second operand. */ + offset = 0; + ExpectIntEQ(TLSX_WriteResponse(ssl, NULL, server_hello, &offset), 0); + ExpectIntEQ(offset, 0); + + /* Both true, haveEMS && server_hello && !TLS1.3: all three EMS + * operands true. */ + ssl->options.haveEMS = 1; + len = 0; + ExpectIntEQ(TLSX_GetResponseSize(ssl, server_hello, &len), 0); + ExpectTrue(len >= HELLO_EXT_SZ); + offset = 0; + ExpectIntEQ(TLSX_WriteResponse(ssl, out, server_hello, &offset), 0); + ExpectTrue(offset > 0); + + /* haveEMS true, msgType != server_hello: EMS operand 1 false - + * independence for operand 1 (operand 0 held true). */ + len = 0; + ExpectIntEQ(TLSX_GetResponseSize(ssl, encrypted_extensions, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteResponse(ssl, out, encrypted_extensions, + &offset), 0); + + /* haveEMS true, msgType == server_hello, TLS 1.3: EMS operand 2 + * false - independence for operand 2 (operands 0 and 1 held true). */ + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = TLSv1_3_MINOR; + ssl->options.tls1_3 = 1; + len = 0; + ExpectIntEQ(TLSX_GetResponseSize(ssl, server_hello, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteResponse(ssl, out, server_hello, &offset), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* The msgType splits shared by TLSX_SupportedVersions_GetSize()/_Write(), + * TLSX_EncryptThenMac_GetSize()/_Write(), TLSX_ClientCertificateType_GetSize()/ + * _Write() and TLSX_ServerCertificateType_GetSize()/_Write() - all static- + * in-file, reached only through the generic TLSX_GetSize()/TLSX_Write() + * dispatch inside the WOLFSSL_TEST_VIS TLSX_GetRequestSize()/ + * TLSX_WriteRequest(). Those two only use msgType to decide which top-level + * semaphore bits to set before the dispatch; the dispatch itself passes + * msgType straight through to each extension's own handler regardless of + * which wrapper made the call (see test_TLSX_Cookie_bounds() above for the + * same pattern). One ssl populated with all four extensions and driven + * through client_hello/server_hello/hello_retry_request/encrypted_extensions + * therefore exercises every open condition in this group in one binary. */ +int test_TLSX_ext_msgtype_dispatch_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY) && defined(HAVE_RPK) && \ + defined(HAVE_TLS_EXTENSIONS) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + /* TLSX_PopulateExtensions() on a TLS 1.3 client also builds a real + * KeyShare (possibly a large post-quantum/hybrid group) and signature + * algorithm list, so this needs to be sized well past a single + * extension's record, not just the two pushed-by-hand ones under + * test - undersizing this the way an early pass did with a 16-byte + * buffer for a 29-byte record corrupts the stack. */ + byte out[8192]; + word32 offset; + word32 len; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* SupportedVersions and EncryptThenMac come from the normal + * client-side extension population; ClientCertificateType/ + * ServerCertificateType are pushed directly (their real Use() + * paths need a loaded RPK certificate, which is not needed to + * reach the GetSize/Write msgType split under test). */ + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + ExpectNotNull(TLSX_Find(ssl->extensions, TLSX_SUPPORTED_VERSIONS)); + ExpectNotNull(TLSX_Find(ssl->extensions, TLSX_ENCRYPT_THEN_MAC)); + + ssl->options.rpkState.sending_ClientCertTypeCnt = 1; + ssl->options.rpkState.sending_ClientCertTypes[0] = 0; + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_CLIENT_CERTIFICATE_TYPE, + ssl, ssl->heap), 0); + ssl->options.rpkState.sending_ServerCertTypeCnt = 1; + ssl->options.rpkState.sending_ServerCertTypes[0] = 0; + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_SERVER_CERTIFICATE_TYPE, + ssl, ssl->heap), 0); + + /* TLSX_GetSize()/TLSX_Write() skip any extension with resp == 0 + * outright for every msgType that is not client_hello/ + * certificate_request (their own leading "!isRequest && + * !extension->resp" guard) - server_hello, hello_retry_request and + * encrypted_extensions all fall in that set. Each is marked resp + * only for the call(s) that actually exercise it: ServerHello + * needs all four; hello_retry_request only means anything to + * SupportedVersions/EncryptThenMac (ClientCertificateType/ + * ServerCertificateType's own GetSize() has no case for it at all, + * and unlike TLSX_CSR_GetSize_ex() it does not clamp a negative + * SANITY_MSG_E before folding it into the word32 running total, + * so marking it resp for a msgType it was never meant to answer + * corrupts TLSX_GetSize()'s own accumulator - not the thing under + * test here); encrypted_extensions only means anything to + * ClientCertificateType/ServerCertificateType. */ + { + TLSX* svExt = TLSX_Find(ssl->extensions, TLSX_SUPPORTED_VERSIONS); + TLSX* etmExt = TLSX_Find(ssl->extensions, TLSX_ENCRYPT_THEN_MAC); + TLSX* cctExt = TLSX_Find(ssl->extensions, + TLSX_CLIENT_CERTIFICATE_TYPE); + TLSX* sctExt = TLSX_Find(ssl->extensions, + TLSX_SERVER_CERTIFICATE_TYPE); + ExpectNotNull(svExt); + ExpectNotNull(etmExt); + ExpectNotNull(cctExt); + ExpectNotNull(sctExt); + + /* msgType == client_hello: SupportedVersions/ClientCertificateType/ + * ServerCertificateType all take their "if (msgType == + * client_hello)" branch (not part of any open condition here); + * EncryptThenMac's "msgType != client_hello" operand false. */ + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, client_hello, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), + 0); + + /* msgType == server_hello: SupportedVersions/ + * ClientCertificateType/ServerCertificateType operand 0 true; + * EncryptThenMac operand 0 true, operand 1 false. */ + if (svExt != NULL) svExt->resp = 1; + if (etmExt != NULL) etmExt->resp = 1; + if (cctExt != NULL) cctExt->resp = 1; + if (sctExt != NULL) sctExt->resp = 1; + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, server_hello, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, server_hello, &offset), + 0); + if (cctExt != NULL) cctExt->resp = 0; + if (sctExt != NULL) sctExt->resp = 0; + + /* msgType == hello_retry_request: SupportedVersions operand 0 + * false, operand 1 true; EncryptThenMac both operands true - + * SANITY_MSG_E, which TLSX_GetSize()/TLSX_Write() propagate up. + * ClientCertificateType/ServerCertificateType left resp == 0 + * (skipped) for exactly the reason in the comment above. */ + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, hello_retry_request, &len), + WC_NO_ERR_TRACE(SANITY_MSG_E)); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, hello_retry_request, + &offset), WC_NO_ERR_TRACE(SANITY_MSG_E)); + if (svExt != NULL) svExt->resp = 0; + if (etmExt != NULL) etmExt->resp = 0; + } + + /* msgType == encrypted_extensions: ClientCertificateType/ + * ServerCertificateType operand 0 false, operand 1 true. */ + { + TLSX* cctExt = TLSX_Find(ssl->extensions, + TLSX_CLIENT_CERTIFICATE_TYPE); + TLSX* sctExt = TLSX_Find(ssl->extensions, + TLSX_SERVER_CERTIFICATE_TYPE); + if (cctExt != NULL) cctExt->resp = 1; + if (sctExt != NULL) sctExt->resp = 1; + } + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, encrypted_extensions, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, encrypted_extensions, + &offset), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* The "both false" baseline row for each of the three OR-shaped msgType + * splits above (SupportedVersions/ClientCertificateType/ + * ServerCertificateType) still needs one more vector each: a msgType + * that is neither of the two the extension recognizes. server_hello/ + * hello_retry_request/encrypted_extensions above only ever showed one + * extension's operands both false as a side effect of some OTHER + * extension being absent or skipped from that same call - never this + * extension, reached with resp honored, on a call of its own. Each + * pushed alone (fresh ssl, so no cross-extension interaction) with an + * unused msgType supplies it. ClientCertificateType/ + * ServerCertificateType's TLSX_GetSize() case does not clamp the + * SANITY_MSG_E their GetSize() returns here (unlike TLSX_Write()'s + * case, which returns a word16 that is always 0 in this branch) before + * folding it into the running total, so TLSX_GetRequestSize() itself + * comes back BUFFER_E rather than SANITY_MSG_E - a preexisting + * TLSX_GetSize() quirk, not the thing under test, and not asserted on + * beyond "some error". */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* ext = NULL; + /* TLSX_SetSupportedVersions() is static-in-file; its whole body is + * "return TLSX_Push(extensions, TLSX_SUPPORTED_VERSIONS, data, + * heap);" with data == ssl, called directly here instead. */ + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_SUPPORTED_VERSIONS, ssl, + ssl->heap), 0); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, + TLSX_SUPPORTED_VERSIONS)); + if (ext != NULL) + ext->resp = 1; + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, encrypted_extensions, &len), + WC_NO_ERR_TRACE(SANITY_MSG_E)); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, encrypted_extensions, + &offset), WC_NO_ERR_TRACE(SANITY_MSG_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* ext = NULL; + ssl->options.rpkState.sending_ClientCertTypeCnt = 1; + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_CLIENT_CERTIFICATE_TYPE, + ssl, ssl->heap), 0); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, + TLSX_CLIENT_CERTIFICATE_TYPE)); + if (ext != NULL) + ext->resp = 1; + len = 0; + ExpectIntNE(TLSX_GetRequestSize(ssl, hello_retry_request, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, hello_retry_request, + &offset), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* ext = NULL; + ssl->options.rpkState.sending_ServerCertTypeCnt = 1; + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_SERVER_CERTIFICATE_TYPE, + ssl, ssl->heap), 0); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, + TLSX_SERVER_CERTIFICATE_TYPE)); + if (ext != NULL) + ext->resp = 1; + len = 0; + ExpectIntNE(TLSX_GetRequestSize(ssl, hello_retry_request, &len), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, hello_retry_request, + &offset), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_SecureRenegotiation_Write() - "data && data->enabled && + * data->verifySet". Only the first operand is open; a NULL data pointer is + * the "HAVE_SERVER_RENEGOTIATION_INFO only" empty-extension shape the + * function's own comment describes, produced here directly with + * TLSX_Push() rather than through a real renegotiation handshake. */ +int test_TLSX_SecureRenegotiation_Write_bounds(void) +{ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && (defined(HAVE_SECURE_RENEGOTIATION) || defined(HAVE_SERVER_RENEGOTIATION_INFO)) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + /* Sized from the vector: prefix(2) + type(2) + len(2) + reneg length + * byte(1) + client_verify_data(TLS_FINISHED_SZ==12) == 19 bytes for + * the enabled/verifySet case below. */ + byte out[32]; + word32 offset; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* data == NULL: first operand false. InitSSL() already advertises + * an empty renegotiation_info by default for every client + * (SetupClientSecureRenegotiation(), with real, non-NULL data) - + * remove it first so this vector's own NULL-data push is the only + * TLSX_RENEGOTIATION_INFO node on the list. */ + ssl->options.haveEMS = 0; + TLSX_Remove(&ssl->extensions, TLSX_RENEGOTIATION_INFO, ssl->heap); + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_RENEGOTIATION_INFO, + NULL, ssl->heap), 0); + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), 0); + ExpectIntEQ(offset, OPAQUE16_LEN + HELLO_EXT_TYPE_SZ + OPAQUE16_LEN + + OPAQUE8_LEN); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* data != NULL, enabled and verifySet both true: first operand true - + * independence for the first operand. wolfSSL_UseSecureRenegotiation() + * allocates data; verifySet/enabled are then set directly (only ever + * set true together, by a real Finished exchange in the non-test + * path). */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* ext = NULL; + SecureRenegotiation* data; + + ssl->options.haveEMS = 0; + ExpectIntEQ(wolfSSL_UseSecureRenegotiation(ssl), WOLFSSL_SUCCESS); + ExpectNotNull(ext = TLSX_Find(ssl->extensions, TLSX_RENEGOTIATION_INFO)); + if (ext != NULL) { + data = (SecureRenegotiation*)ext->data; + data->enabled = 1; + data->verifySet = 1; + offset = 0; + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &offset), + 0); + ExpectIntEQ(offset, OPAQUE16_LEN + HELLO_EXT_TYPE_SZ + + OPAQUE16_LEN + OPAQUE8_LEN + TLS_FINISHED_SZ); + } + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} + +/* ---------------------------------------------------------------------- */ +/* TLSX_SessionTicket_Parse() (server, TLS 1.2), the "ret is REJECT or + * VERSION_ERROR" branch after "ret = DoClientTicket(...)". The + * false/false row - ret is neither value - needs a DoClientTicket() return + * that is none of WOLFSSL_TICKET_RET_OK/_CREATE/_REJECT or VERSION_ERROR. + * DoDecryptTicket() has exactly one such path: when the registered + * SessionTicketEncCb reports success but writes an out-of-range outLen, + * DoDecryptTicket() returns BAD_TICKET_KEY_CB_SZ directly - none of the + * four TicketEncRet values and unrelated to VERSION_ERROR - and + * DoClientTicket() passes that straight back up unchanged (it does not + * reach WOLFSSL_TICKET_RET_REJECT; every REJECT return in DoDecryptTicket() + * is a distinct, earlier guard). TLSX_SessionTicket_Parse() then falls to + * its own "else if (ret < 0)" tail, leaving ret untouched, so the negative + * value propagates out of TLSX_Parse() and fails the handshake - a real, + * protocol-level row, not a callee postcondition that collapses it into + * REJECT. So: covered, not excluded. */ +#ifdef TEST_TLS_BOUNDS_SESSION_TICKET_FF +TEST_TLS_BOUNDS_UNUSED +static int test_TLSX_SessionTicket_ff_enc_cb(WOLFSSL* ssl, + byte key_name[WOLFSSL_TICKET_NAME_SZ], byte iv[WOLFSSL_TICKET_IV_SZ], + byte mac[WOLFSSL_TICKET_MAC_SZ], int enc, byte* ticket, int inLen, + int* outLen, void* userCtx) +{ + int i; + (void)ssl; (void)userCtx; + + if (enc) { + XMEMSET(key_name, 0x11, WOLFSSL_TICKET_NAME_SZ); + XMEMSET(iv, 0x22, WOLFSSL_TICKET_IV_SZ); + XMEMSET(mac, 0x33, WOLFSSL_TICKET_MAC_SZ); + /* DoCreateTicket()'s own sanity check rejects an encrypt callback + * that leaves the internal ticket bytes unchanged, so this has to + * actually transform them, not just report success. */ + for (i = 0; i < inLen; i++) + ticket[i] = (byte)(ticket[i] ^ 0xA5); + *outLen = inLen; + } + else { + /* Report success with an out-of-range outLen: DoDecryptTicket()'s + * own "outLen > inLen || outLen < WOLFSSL_INTERNAL_TICKET_LEN" + * guard turns this into BAD_TICKET_KEY_CB_SZ. */ + *outLen = 0; + } + return WOLFSSL_TICKET_RET_OK; +} + +TEST_TLS_BOUNDS_UNUSED +static int test_TLSX_SessionTicket_ff_ctx_ready(WOLFSSL_CTX* ctx) +{ + EXPECT_DECLS; + ExpectIntEQ(wolfSSL_CTX_set_TicketEncCb(ctx, + test_TLSX_SessionTicket_ff_enc_cb), WOLFSSL_SUCCESS); + return EXPECT_RESULT(); +} +#endif + +int test_TLSX_SessionTicket_Parse_falsefalse_bounds(void) +{ +#if defined(TEST_TLS_BOUNDS_SESSION_TICKET_FF) && \ + !defined(WOLFSSL_NO_TLS12) + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + WOLFSSL_SESSION* sess = NULL; + + /* First connection: issue a ticket (enc == 1 path only). */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.c_cb.method = wolfTLSv1_2_client_method; + test_ctx.s_cb.method = wolfTLSv1_2_server_method; + test_ctx.s_cb.ctx_ready = test_TLSX_SessionTicket_ff_ctx_ready; + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_UseSessionTicket(test_ctx.c_ssl), WOLFSSL_SUCCESS); + ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + ExpectNotNull(sess = wolfSSL_get1_session(test_ctx.c_ssl)); + test_ssl_memio_cleanup(&test_ctx); + + /* Second connection: present that ticket back (enc == 0 path), which + * the callback now deliberately corrupts. */ + if (sess != NULL) { + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.c_cb.method = wolfTLSv1_2_client_method; + test_ctx.s_cb.method = wolfTLSv1_2_server_method; + test_ctx.s_cb.ctx_ready = test_TLSX_SessionTicket_ff_ctx_ready; + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(test_ctx.c_ssl, sess), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_FAIL); + test_ssl_memio_cleanup(&test_ctx); + } + wolfSSL_SESSION_free(sess); + + return EXPECT_RESULT(); +#else + return TEST_SKIPPED; +#endif +} diff --git a/tests/api/test_tls_bounds.h b/tests/api/test_tls_bounds.h new file mode 100644 index 0000000000..c3e4354365 --- /dev/null +++ b/tests/api/test_tls_bounds.h @@ -0,0 +1,88 @@ +/* test_tls_bounds.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 TESTS_API_TEST_TLS_BOUNDS_H +#define TESTS_API_TEST_TLS_BOUNDS_H + +int test_TLSX_UseSNI_bounds(void); +int test_TLSX_UseALPN_bounds(void); +int test_TLSX_UseMaxFragment_bounds(void); +int test_TLSX_UseCertificateStatusRequest_bounds(void); +int test_TLSX_UseCertificateStatusRequestV2_bounds(void); +int test_TLSX_SupportExtensions_bounds(void); +int test_TLSX_CSR2_InitRequests_bounds(void); +int test_TLSX_CSR2_ForceRequest_bounds(void); +int test_TLSX_CSR_GetRequest_ex_bounds(void); +int test_wolfSSL_make_eap_keys_bounds(void); +int test_wolfSSL_SetTlsHmacInner_bounds(void); +int test_BuildTlsHandshakeHash_bounds(void); +int test_TLS_hmac_bounds(void); +int test_TLSX_ALPN_GetSize_overflow(void); +int test_TLSX_Cookie_bounds(void); +int test_TLSX_CSR_write_getsize_bounds(void); +int test_TLSX_CSR_SetResponseWithStatusCB_bounds(void); +int test_ProcessChainOCSPRequest_bounds(void); +int test_TLSX_PopulateExtensions_bounds(void); +int test_TLSX_PopulateSupportedGroups_bounds(void); +int test_TLSX_CSR_Parse_bounds(void); +int test_TLSX_CSR2_Parse_bounds(void); +int test_TLSX_ext_dispatch_ctx_extensions_bounds(void); +int test_TLSX_WriteRequest_ems_bounds(void); +int test_TLSX_WriteRequest_length_prefix_bounds(void); +int test_TLSX_WriteResponse_bounds(void); +int test_TLSX_ext_msgtype_dispatch_bounds(void); +int test_TLSX_SecureRenegotiation_Write_bounds(void); +int test_TLSX_SessionTicket_Parse_falsefalse_bounds(void); + +#define TEST_TLS_BOUNDS_DECLS \ + TEST_DECL_GROUP("tls", test_TLSX_UseSNI_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_UseALPN_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_UseMaxFragment_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_UseCertificateStatusRequest_bounds),\ + TEST_DECL_GROUP("tls", \ + test_TLSX_UseCertificateStatusRequestV2_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_SupportExtensions_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_CSR2_InitRequests_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_CSR2_ForceRequest_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_CSR_GetRequest_ex_bounds), \ + TEST_DECL_GROUP("tls", test_wolfSSL_make_eap_keys_bounds), \ + TEST_DECL_GROUP("tls", test_wolfSSL_SetTlsHmacInner_bounds), \ + TEST_DECL_GROUP("tls", test_BuildTlsHandshakeHash_bounds), \ + TEST_DECL_GROUP("tls", test_TLS_hmac_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_ALPN_GetSize_overflow), \ + TEST_DECL_GROUP("tls", test_TLSX_Cookie_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_CSR_write_getsize_bounds), \ + TEST_DECL_GROUP("tls", \ + test_TLSX_CSR_SetResponseWithStatusCB_bounds), \ + TEST_DECL_GROUP("tls", test_ProcessChainOCSPRequest_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_PopulateExtensions_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_PopulateSupportedGroups_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_CSR_Parse_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_CSR2_Parse_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_ext_dispatch_ctx_extensions_bounds),\ + TEST_DECL_GROUP("tls", test_TLSX_WriteRequest_ems_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_WriteRequest_length_prefix_bounds),\ + TEST_DECL_GROUP("tls", test_TLSX_WriteResponse_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_ext_msgtype_dispatch_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_SecureRenegotiation_Write_bounds), \ + TEST_DECL_GROUP("tls", test_TLSX_SessionTicket_Parse_falsefalse_bounds) + +#endif /* TESTS_API_TEST_TLS_BOUNDS_H */ diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index 40041c0533..c5ffbe16b3 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -56,4 +56,25 @@ int test_wolfSSL_custom_ext_resumption_fallback(void); int test_wolfSSL_custom_ext_ticket_fallback(void); int test_wolfSSL_custom_ext_add_null(void); +#define TEST_TLS_EXT_DECLS \ + TEST_DECL_GROUP("tls", test_tls_ems_downgrade), \ + TEST_DECL_GROUP("tls", test_tls_ems_resumption_downgrade), \ + TEST_DECL_GROUP("tls", test_tls_ems_resumption_server_downgrade), \ + TEST_DECL_GROUP("tls", test_scr_verify_data_mismatch), \ + TEST_DECL_GROUP("tls", test_scr_no_renegotiation_option), \ + TEST_DECL_GROUP("tls", test_helloRequest_no_renegotiation_option), \ + TEST_DECL_GROUP("tls", test_helloRequest_advertise_only_refused), \ + TEST_DECL_GROUP("tls", test_tls13_hrr_cipher_suite_mismatch), \ + TEST_DECL_GROUP("tls", test_tls13_ticket_age_out_of_window), \ + TEST_DECL_GROUP("tls", test_certificate_authorities_certificate_request), \ + TEST_DECL_GROUP("tls", test_certificate_authorities_client_hello), \ + TEST_DECL_GROUP("tls", test_TLSX_TCA_Find), \ + TEST_DECL_GROUP("tls", test_TLSX_SNI_GetSize_overflow), \ + TEST_DECL_GROUP("tls", test_TLSX_ECH_msg_type_validation), \ + TEST_DECL_GROUP("tls", test_TLSX_CSR2_tls13_msg_type_validation), \ + TEST_DECL_GROUP("tls", test_TLSX_SRTP_msg_type_validation), \ + TEST_DECL_GROUP("tls", test_TLSX_ALPN_server_response_count), \ + TEST_DECL_GROUP("tls", test_TLSX_SupportedCurve_empty_or_unsupported), \ + TEST_DECL_GROUP("tls", test_TLSX_PointFormat_uncompressed_required) + #endif /* TESTS_API_TEST_TLS_EXT_H */ diff --git a/tests/api/test_tls_msgtype.c b/tests/api/test_tls_msgtype.c new file mode 100644 index 0000000000..fcdb64dafd --- /dev/null +++ b/tests/api/test_tls_msgtype.c @@ -0,0 +1,2502 @@ +/* test_tls_msgtype.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 + +#include +#include + +#if defined(__GNUC__) || defined(__clang__) + #define TEST_TLS_MSGTYPE_UNUSED __attribute__((unused)) +#else + #define TEST_TLS_MSGTYPE_UNUSED +#endif + + +/* This file drives TLSX_Parse() (src/tls.c) directly with hand-built + * extension records to exercise the per-extension "not permitted in this + * message" gates from RFC 8446 Section 4.2, plus the argument validation and + * ClientHello-consistency checks at the top and bottom of the same function. + * TLSX_Parse() is WOLFSSL_TEST_VIS, so it is callable here without a + * WOLFSSL_TEST_STATIC_BUILD guard. */ + +#if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + +/* TLSX_SNI_Free() is file-static in src/tls.c. A test that detaches an SNI + * list from its extension has to release it the same way: the host name, then + * the node. */ +#ifdef HAVE_SNI +TEST_TLS_MSGTYPE_UNUSED +static void test_tls_msgtype_free_sni(void* p, void* heap) +{ + SNI* sni = (SNI*)p; + + while (sni != NULL) { + SNI* next = sni->next; + + if (sni->type == WOLFSSL_SNI_HOST_NAME) + XFREE(sni->data.host_name, heap, DYNAMIC_TYPE_TLSX); + XFREE(sni, heap, DYNAMIC_TYPE_TLSX); + sni = next; + } +} +#endif /* HAVE_SNI */ + + +/* Build one extension record (2-byte type, 2-byte length, N zero data + * bytes) into buf and return its total length. Content is all-zero: gates + * are checked before an extension's data is interpreted, so the exact bytes + * only need to satisfy the minimum-size gate, not be semantically valid. */ +TEST_TLS_MSGTYPE_UNUSED +static word16 build_ext(byte* buf, word16 type, word16 dataSz) +{ + buf[0] = (byte)(type >> 8); + buf[1] = (byte)type; + buf[2] = (byte)(dataSz >> 8); + buf[3] = (byte)dataSz; + if (dataSz > 0) + XMEMSET(buf + 4, 0, dataSz); + return (word16)(4 + dataSz); +} + +/* Build one extension record (2-byte type, 2-byte length, then bodyLen + * bytes copied verbatim from body) into buf and return its total length. + * Unlike build_ext(), the data is caller-supplied, for extensions whose + * gates require structurally meaningful content rather than zero bytes. */ +TEST_TLS_MSGTYPE_UNUSED +static word16 build_ext_with_body(byte* buf, word16 type, const byte* body, + word16 bodyLen) +{ + buf[0] = (byte)(type >> 8); + buf[1] = (byte)type; + buf[2] = (byte)(bodyLen >> 8); + buf[3] = (byte)bodyLen; + if (bodyLen > 0) + XMEMCPY(buf + 4, body, bodyLen); + return (word16)(4 + bodyLen); +} + +#endif /* !NO_WOLFSSL_CLIENT && !NO_TLS && HAVE_TLS_EXTENSIONS */ + +/* ---- TLSX_Parse() argument validation ------------------------------- */ +/* if (!ssl || !input || (isRequest && !suites)) return BAD_FUNC_ARG; */ +int test_tls_msgtype_arg_guard(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte dummy[4] = { 0 }; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + /* !ssl */ + ExpectIntEQ(TLSX_Parse(NULL, dummy, 0, finished, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* !input, ssl valid */ + ExpectIntEQ(TLSX_Parse(ssl, NULL, 4, finished, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* baseline: ssl and input valid, isRequest false (finished), no + * extensions - accepted. */ + ExpectIntEQ(TLSX_Parse(ssl, dummy, 0, finished, NULL), 0); + /* isRequest true (client_hello), suites missing */ + ExpectIntEQ(TLSX_Parse(ssl, dummy, 0, client_hello, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* isRequest true (client_hello), suites present - accepted. */ + ExpectIntEQ(TLSX_Parse(ssl, dummy, 0, client_hello, &suites), 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- duplicate Pre-Shared Key extension in one ClientHello ----------- */ +/* if (msgType == client_hello && pskDone) return PSK_KEY_ERROR; */ +#if defined(WOLFSSL_TLS13) && (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) +/* A minimal, structurally valid ClientHello pre_shared_key body: one + * zero-length identity (6 bytes: 2-byte identities-length of a single + * 6-byte entry, 2-byte identity length of 0, 4-byte age of 0) and one + * binder sized at the SHA-256 digest length (33 bytes: 1-byte binder length + * of 32, 32 zero binder bytes). See TLSX_PreSharedKey_Parse_ClientHello() / + * MIN_PSK_ID_LEN / MIN_PSK_BINDERS_LEN. */ +static const byte psk_ch_body[] = { + 0x00, 0x06, /* identities len */ + 0x00, 0x00, /* identity len = 0 */ + 0x00, 0x00, 0x00, 0x00, /* ticket age = 0 */ + 0x00, 0x21, /* binders len = 33 */ + 0x20, /* binder len = 32 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* binder data */ +}; +#endif + +int test_tls_msgtype_psk_duplicate(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[64]; + word16 len, pskExtLen; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + /* op0 false: not a ClientHello, pskDone is never even examined. */ + len = build_ext(buf, 0xfff0, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), 0); + + /* op0 true, op1 false: single ClientHello extension, pskDone stays 0 + * through the only loop iteration. */ + len = build_ext(buf, 0xfff0, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + + /* op0 true, op1 true: a valid pre_shared_key extension sets pskDone, + * then a second extension record in the same ClientHello re-enters the + * loop with pskDone already set. */ + pskExtLen = build_ext(buf, TLSX_PRE_SHARED_KEY, (word16)sizeof(psk_ch_body)); + XMEMCPY(buf + 4, psk_ch_body, sizeof(psk_ch_body)); + buf[pskExtLen] = 0x00; /* one trailing byte so offset < length again */ + len = (word16)(pskExtLen + 1); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Certificate-message extension must have been offered ------------ */ +/* if (msgType == certificate && IsAtLeastTLSv1_3(ssl->version) && + * TLSX_Find(ssl->extensions, type) == NULL && + * (ssl->ctx == NULL || TLSX_Find(ssl->ctx->extensions, type) == NULL)) */ +int test_tls_msgtype_certificate_ext_offered(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && defined(HAVE_MAX_FRAGMENT) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + WOLFSSL_CTX* savedCtx; + + /* op0 false: not a Certificate message. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Baseline reject (op0..op4 all "true", via the ctx->extensions arm): + * Certificate message, TLS 1.3, type not offered anywhere. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, certificate, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op1 false: Certificate message, but not TLS 1.3. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, certificate, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op2 false: the type was offered at the ssl level. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseMaxFragment(ssl, WOLFSSL_MFL_2_9), WOLFSSL_SUCCESS); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, certificate, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op3 false, op4 false: not offered at the ssl level, but the ctx has + * it - so the type was still offered (CTX-level extensions apply to + * every ssl created from this ctx). */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectIntEQ(wolfSSL_CTX_UseMaxFragment(ctx, WOLFSSL_MFL_2_9), WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, certificate, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op3 true: ctx forcibly NULL (restored before free). */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, 0); + if (ssl != NULL) { + savedCtx = ssl->ctx; + ssl->ctx = NULL; + ExpectIntEQ(TLSX_Parse(ssl, buf, len, certificate, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + ssl->ctx = savedCtx; + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- RFC 8446 4.2 "not permitted in this message" per-extension gates */ + +int test_tls_msgtype_sni_tls13(void) +{ + EXPECT_DECLS; +/* Drives TLSX_SNI_Parse's isRequest path, which is server-side code and + * is compiled out by NO_WOLFSSL_SERVER. */ +#if defined(HAVE_SNI) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_SERVER_NAME, WOLFSSL_SNI_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SERVER_NAME, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, encrypted_extensions, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SERVER_NAME, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_sni_tls12(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SNI) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_SERVER_NAME, WOLFSSL_SNI_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SERVER_NAME, WOLFSSL_SNI_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SERVER_NAME, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_tca(void) +{ + EXPECT_DECLS; +#if !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_TRUSTED_CA_KEYS, WOLFSSL_TCA_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_TRUSTED_CA_KEYS, WOLFSSL_TCA_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_TRUSTED_CA_KEYS, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_mfl_tls13(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, WOLFSSL_MFL_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, encrypted_extensions, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_mfl_tls12(void) +{ + EXPECT_DECLS; +#if !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, WOLFSSL_MFL_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, WOLFSSL_MFL_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_MAX_FRAGMENT_LENGTH, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_supported_groups_tls13(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SUPPORTED_CURVES) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_SUPPORTED_GROUPS, WOLFSSL_EC_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SUPPORTED_GROUPS, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, encrypted_extensions, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SUPPORTED_GROUPS, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_point_formats(void) +{ + EXPECT_DECLS; +#if !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_EC_POINT_FORMATS, WOLFSSL_PF_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_EC_POINT_FORMATS, WOLFSSL_PF_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_EC_POINT_FORMATS, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_csr_tls13(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + /* Offer status_request in our own ClientHello first, so the + * Certificate-message extension is recognized as one we asked for by + * the check earlier in TLSX_Parse() (RFC 8446 4.4.2). */ + ExpectIntEQ(wolfSSL_UseOCSPStapling(ssl, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + + len = build_ext(buf, TLSX_STATUS_REQUEST, WOLFSSL_CSR_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_STATUS_REQUEST, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, certificate_request, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_STATUS_REQUEST, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, certificate, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_STATUS_REQUEST, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_csr_tls12(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_STATUS_REQUEST, WOLFSSL_CSR_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_STATUS_REQUEST, WOLFSSL_CSR_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_STATUS_REQUEST, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_csr2_tls12(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_STATUS_REQUEST_V2, WOLFSSL_CSR2_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_STATUS_REQUEST_V2, WOLFSSL_CSR2_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_STATUS_REQUEST_V2, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_extms(void) +{ + EXPECT_DECLS; +#if defined(HAVE_EXTENDED_MASTER) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, HELLO_EXT_EXTMS, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, HELLO_EXT_EXTMS, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, HELLO_EXT_EXTMS, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_renegotiation_info(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SECURE_RENEGOTIATION) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_RENEGOTIATION_INFO, WOLFSSL_SCR_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_RENEGOTIATION_INFO, WOLFSSL_SCR_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_RENEGOTIATION_INFO, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_session_ticket_tls12(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_SESSION_TICKET, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SESSION_TICKET, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SESSION_TICKET, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_alpn_tls13(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ALPN) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_APPLICATION_LAYER_PROTOCOL, + WOLFSSL_ALPN_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_APPLICATION_LAYER_PROTOCOL, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, encrypted_extensions, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_APPLICATION_LAYER_PROTOCOL, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_alpn_tls12(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ALPN) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_APPLICATION_LAYER_PROTOCOL, + WOLFSSL_ALPN_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_APPLICATION_LAYER_PROTOCOL, + WOLFSSL_ALPN_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_APPLICATION_LAYER_PROTOCOL, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_sigalgs_tls13(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(WOLFSSL_NO_SIGALG) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_SIGNATURE_ALGORITHMS, WOLFSSL_SA_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SIGNATURE_ALGORITHMS, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, certificate_request, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SIGNATURE_ALGORITHMS, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_etm(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_ENCRYPT_THEN_MAC, WOLFSSL_ETM_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_ENCRYPT_THEN_MAC, WOLFSSL_ETM_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_ENCRYPT_THEN_MAC, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_supported_versions(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_SUPPORTED_VERSIONS, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SUPPORTED_VERSIONS, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SUPPORTED_VERSIONS, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, hello_retry_request, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SUPPORTED_VERSIONS, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_cookie(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_COOKIE, WOLFSSL_CKE_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_COOKIE, WOLFSSL_CKE_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, hello_retry_request, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_COOKIE, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_psk(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_PRE_SHARED_KEY, WOLFSSL_PSK_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_PRE_SHARED_KEY, WOLFSSL_PSK_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_PRE_SHARED_KEY, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_cert_with_extern_psk(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_CERT_WITH_EXTERN_PSK) && !defined(NO_PSK) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + /* TLSX_CertWithExternPsk_Parse() only records the ClientHello offer - + * and only answers a ServerHello with anything but EXT_NOT_ALLOWED - + * once the server has opted in. */ + ExpectIntEQ(wolfSSL_set_cert_with_extern_psk(ssl, 1), WOLFSSL_SUCCESS); + + len = build_ext(buf, TLSX_CERT_WITH_EXTERN_PSK, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_CERT_WITH_EXTERN_PSK, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_CERT_WITH_EXTERN_PSK, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_early_data(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_EARLY_DATA, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_EARLY_DATA, 4); + ExpectIntNE(TLSX_Parse(ssl, buf, len, session_ticket, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_EARLY_DATA, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, encrypted_extensions, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_EARLY_DATA, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_sigalgs_cert(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(WOLFSSL_NO_SIGALG) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_SIGNATURE_ALGORITHMS_CERT, WOLFSSL_SA_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SIGNATURE_ALGORITHMS_CERT, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, certificate_request, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SIGNATURE_ALGORITHMS_CERT, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_key_share(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SUPPORTED_CURVES) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_KEY_SHARE, WOLFSSL_KS_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_KEY_SHARE, WOLFSSL_KS_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_KEY_SHARE, WOLFSSL_KS_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, hello_retry_request, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_KEY_SHARE, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_client_cert_type_tls13(void) +{ + EXPECT_DECLS; +#if defined(HAVE_RPK) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_CLIENT_CERTIFICATE_TYPE, WOLFSSL_CCT_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_CLIENT_CERTIFICATE_TYPE, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, encrypted_extensions, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_CLIENT_CERTIFICATE_TYPE, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_client_cert_type_tls12(void) +{ + EXPECT_DECLS; +#if defined(HAVE_RPK) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_CLIENT_CERTIFICATE_TYPE, WOLFSSL_CCT_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_CLIENT_CERTIFICATE_TYPE, WOLFSSL_CCT_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_CLIENT_CERTIFICATE_TYPE, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_server_cert_type_tls13(void) +{ + EXPECT_DECLS; +#if defined(HAVE_RPK) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_SERVER_CERTIFICATE_TYPE, WOLFSSL_SCT_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SERVER_CERTIFICATE_TYPE, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, encrypted_extensions, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SERVER_CERTIFICATE_TYPE, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_server_cert_type_tls12(void) +{ + EXPECT_DECLS; +#if defined(HAVE_RPK) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_SERVER_CERTIFICATE_TYPE, WOLFSSL_SCT_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SERVER_CERTIFICATE_TYPE, WOLFSSL_SCT_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_SERVER_CERTIFICATE_TYPE, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_connection_id(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_DTLS_CID) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_CONNECTION_ID, WOLFSSL_CID_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_CONNECTION_ID, WOLFSSL_CID_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_CONNECTION_ID, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_tls_msgtype_ech(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[8]; + word16 len; + Suites suites; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + len = build_ext(buf, TLSX_ECH, WOLFSSL_ECH_MIN_SIZE_CLIENT); + ExpectIntNE(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_ECH, 0); + ExpectIntNE(TLSX_Parse(ssl, buf, len, encrypted_extensions, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_ECH, WOLFSSL_ECH_MIN_SIZE_SERVER); + ExpectIntNE(TLSX_Parse(ssl, buf, len, hello_retry_request, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + len = build_ext(buf, TLSX_ECH, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +#if defined(HAVE_SNI) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_TLS) +/* Builds an SNI extension body: 2-byte list length, 1-byte name type + * (WOLFSSL_SNI_HOST_NAME), 2-byte name length, name bytes. Returns the + * total body length. */ +TEST_TLS_MSGTYPE_UNUSED +static word16 build_sni_body(byte* buf, const char* host) +{ + word16 hostLen = (word16)XSTRLEN(host); + word16 listLen = (word16)(ENUM_LEN + OPAQUE16_LEN + hostLen); + + buf[0] = (byte)(listLen >> 8); + buf[1] = (byte)listLen; + buf[2] = WOLFSSL_SNI_HOST_NAME; + buf[3] = (byte)(hostLen >> 8); + buf[4] = (byte)hostLen; + XMEMCPY(buf + 5, host, hostLen); + return (word16)(5 + hostLen); +} +#endif /* HAVE_SNI && !NO_WOLFSSL_CLIENT && !NO_TLS */ + +/* ---- TLSX_SNI_Find() -------------------------------------------------- */ +/* while (sni && sni->type != type) sni = sni->next; */ +int test_tls_msgtype_sni_find(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SNI) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + const char* host = "example.com"; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, host, + (word16)XSTRLEN(host)), WOLFSSL_SUCCESS); + + /* sni != NULL, sni->type == type: the loop body is never entered - the + * only list entry is found immediately. */ + ExpectIntEQ(wolfSSL_SNI_Status(ssl, WOLFSSL_SNI_HOST_NAME), + WOLFSSL_SNI_NO_MATCH); + + /* sni != NULL, sni->type != type: one non-matching iteration advances + * to sni->next, which is NULL, ending the loop without a match. There + * is only one SNI name type, so a type the list does not hold is the + * only way to exercise this. */ + ExpectIntEQ(wolfSSL_SNI_Status(ssl, (byte)(WOLFSSL_SNI_HOST_NAME + 1)), + 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_SNI_Parse(): client-side response gate ---------------------- */ +/* !isRequest branch: if (!extension || !extension->data) + * return TLSX_HandleUnsupportedExtension(ssl); */ +int test_tls_msgtype_sni_parse_response_gate(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SNI) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + TLSX* extension = NULL; + void* savedExtData = NULL; + const char* host = "example.com"; + byte buf[8]; + word16 len; + + /* extension found, but its data was cleared: the client must still + * treat a ServerHello SNI response as unsolicited rather than + * dereference a NULL SNI list. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, host, + (word16)XSTRLEN(host)), WOLFSSL_SUCCESS); + ExpectNotNull(extension = TLSX_Find(ssl->extensions, TLSX_SERVER_NAME)); + if (extension != NULL) { + savedExtData = extension->data; + extension->data = NULL; + } + + len = build_ext(buf, TLSX_SERVER_NAME, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + + if (extension != NULL) { + if (extension->data == NULL) + extension->data = savedExtData; + else if (extension->data != savedExtData) + test_tls_msgtype_free_sni(savedExtData, ssl->heap); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_SNI_Parse(): server-side list-length gate -------------------- */ +/* if (length != OPAQUE16_LEN + size || size == 0) return BUFFER_ERROR; + * A record this short is already rejected by TLSX_Parse()'s own minimum- + * size gate (WOLFSSL_SNI_MIN_SIZE_CLIENT) before TLSX_SNI_Parse() is ever + * called, so this exercises that outer gate rather than the size == 0 + * check specifically - both return BUFFER_ERROR either way. */ +int test_tls_msgtype_sni_parse_size_gates(void) +{ + EXPECT_DECLS; +/* Drives TLSX_SNI_Parse's isRequest path, which is server-side code and + * is compiled out by NO_WOLFSSL_SERVER. */ +#if defined(HAVE_SNI) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + const char* host = "srv.example"; + byte buf[16]; + byte zeroSize[OPAQUE16_LEN] = { 0x00, 0x00 }; + word16 len; + Suites suites; + + /* An empty server_name_list is rejected as malformed. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, host, + (word16)XSTRLEN(host)), WOLFSSL_SUCCESS); + + len = build_ext_with_body(buf, TLSX_SERVER_NAME, zeroSize, + (word16)sizeof(zeroSize)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +#if defined(HAVE_SNI) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_TLS) +/* SNI receive callback used to force cacheOnly in TLSX_SNI_Parse() when no + * SNI has been configured on the SSL object. */ +TEST_TLS_MSGTYPE_UNUSED +static int sni_recv_cb(WOLFSSL* ssl, int* ret, void* arg) +{ + (void)ssl; (void)ret; (void)arg; + return 0; +} +#endif + +/* ---- TLSX_SNI_Parse(): forced-keep (cacheOnly) path -------------------- */ +/* if (!cacheOnly && !checkPublic && !(sni = TLSX_SNI_Find(...))) + * return 0; + * matched = cacheOnly || (...); + * if (matched || ...) { ... } */ +int test_tls_msgtype_sni_parse_cacheonly(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SNI) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + TLSX* extension = NULL; + void* savedExtData = NULL; + const char* host = "example.com"; + byte sniBody[24]; + byte buf[32]; + word16 sniLen, len; + Suites suites; + + /* extension found (wolfSSL_UseSNI was called) but its data was + * cleared, and a servername callback is registered: cacheOnly is + * forced on, so the extension is silently kept without a real match + * even though the type wasn't actually configured any more. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + wolfSSL_CTX_set_servername_callback(ctx, sni_recv_cb); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, host, + (word16)XSTRLEN(host)), WOLFSSL_SUCCESS); + ExpectNotNull(extension = TLSX_Find(ssl->extensions, TLSX_SERVER_NAME)); + if (extension != NULL) { + savedExtData = extension->data; + extension->data = NULL; + } + + sniLen = build_sni_body(sniBody, "test.example"); + len = build_ext_with_body(buf, TLSX_SERVER_NAME, sniBody, sniLen); + XMEMSET(&suites, 0, sizeof(suites)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + + if (extension != NULL) { + if (extension->data == NULL) + extension->data = savedExtData; + else if (extension->data != savedExtData) + test_tls_msgtype_free_sni(savedExtData, ssl->heap); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_SNI_Parse(): real match / mismatch, options ------------------ */ +/* if (!cacheOnly && !checkPublic && !(sni = TLSX_SNI_Find(...))) return 0; + * - TLSX_SNI_New() only ever constructs an SNI object of type + * WOLFSSL_SNI_HOST_NAME (any other type hits its "invalid type" + * branch and fails), and the incoming record's type is rejected + * earlier in this function unless it is also WOLFSSL_SNI_HOST_NAME. + * So whenever this line is reached with cacheOnly and checkPublic + * both false (i.e. extension->data was non-NULL to begin with), + * TLSX_SNI_Find() is guaranteed to find that single entry, so this + * "not using this type of SNI" return is not reachable that way. + * if (!cacheOnly && sni != NULL && sni->status != WOLFSSL_SNI_NO_MATCH) + * return 0; + * matched = cacheOnly || (hostName != NULL && XSTRLEN(hostName) == size && + * XSTRNCMP(hostName, ..., size) == 0); + * if (!matched && checkPublic) return 0; + * if (matched || (sni != NULL && (sni->options & ANSWER_ON_MISMATCH))) {...} + * else if ((sni == NULL) || !(sni->options & CONTINUE_ON_MISMATCH)) { + * ... return UNKNOWN_SNI_HOST_NAME_E; } */ +int test_tls_msgtype_sni_parse_match(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SNI) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte sniBody[24]; + byte buf[32]; + word16 sniLen, len; + Suites suites; + const char* configured = "a.example"; + + /* M1: configured host matches the ClientHello's host exactly. sni is + * found (not the mismatched-type case below), its status starts at + * WOLFSSL_SNI_NO_MATCH, and every hostName comparison operand is true, + * so the extension is installed and the response is queued. Reusing + * this ssl for a second, identical parse also exercises the "already + * resolved" skip: the second call's sni->status is no longer + * WOLFSSL_SNI_NO_MATCH, so it returns immediately. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, configured, + (word16)XSTRLEN(configured)), WOLFSSL_SUCCESS); + XMEMSET(&suites, 0, sizeof(suites)); + sniLen = build_sni_body(sniBody, configured); + len = build_ext_with_body(buf, TLSX_SERVER_NAME, sniBody, sniLen); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* M2: configured host does not match, and differs in length, so + * XSTRLEN(hostName) == size is false (masking XSTRNCMP). No mismatch + * options are set, so the handshake is aborted. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, configured, + (word16)XSTRLEN(configured)), WOLFSSL_SUCCESS); + XMEMSET(&suites, 0, sizeof(suites)); + sniLen = build_sni_body(sniBody, "bb.example2"); + len = build_ext_with_body(buf, TLSX_SERVER_NAME, sniBody, sniLen); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(UNKNOWN_SNI_HOST_NAME_E)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* M3: configured host does not match, but is the same length, so + * XSTRLEN(hostName) == size is true and XSTRNCMP(...) == 0 is false. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, configured, + (word16)XSTRLEN(configured)), WOLFSSL_SUCCESS); + XMEMSET(&suites, 0, sizeof(suites)); + sniLen = build_sni_body(sniBody, "b.example"); + len = build_ext_with_body(buf, TLSX_SERVER_NAME, sniBody, sniLen); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(UNKNOWN_SNI_HOST_NAME_E)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* M4: mismatch, but WOLFSSL_SNI_ANSWER_ON_MISMATCH is set on the + * configured name - the handshake proceeds with a fake match instead + * of aborting. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, configured, + (word16)XSTRLEN(configured)), WOLFSSL_SUCCESS); + wolfSSL_SNI_SetOptions(ssl, WOLFSSL_SNI_HOST_NAME, + WOLFSSL_SNI_ANSWER_ON_MISMATCH); + XMEMSET(&suites, 0, sizeof(suites)); + sniLen = build_sni_body(sniBody, "bb.example2"); + len = build_ext_with_body(buf, TLSX_SERVER_NAME, sniBody, sniLen); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* M5: mismatch, but WOLFSSL_SNI_CONTINUE_ON_MISMATCH is set - the + * handshake continues without installing a response or aborting. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, configured, + (word16)XSTRLEN(configured)), WOLFSSL_SUCCESS); + wolfSSL_SNI_SetOptions(ssl, WOLFSSL_SNI_HOST_NAME, + WOLFSSL_SNI_CONTINUE_ON_MISMATCH); + XMEMSET(&suites, 0, sizeof(suites)); + sniLen = build_sni_body(sniBody, "bb.example2"); + len = build_ext_with_body(buf, TLSX_SERVER_NAME, sniBody, sniLen); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_SNI_Parse(): outer SNI vs. ECH config publicName ------------- */ +/* checkPublic is only ever set when an ECH extension is already attached + * to ssl->extensions and no SNI was configured for this SSL/CTX; the outer + * SNI is then matched against every configured ECH config's publicName + * instead of a locally configured host name. + * if (XSTRLEN(workingConfig->publicName) == size && + * XSTRNCMP(workingConfig->publicName, ..., size) == 0) matched = 1; + * if (!matched && checkPublic) return 0; */ +int test_tls_msgtype_sni_parse_ech_public(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SNI) && defined(WOLFSSL_TLS13) && defined(HAVE_ECH) && defined(WOLFSSL_TEST_STATIC_BUILD) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + WOLFSSL_EchConfig echConfig; + WOLFSSL_ECH* ech = NULL; + char publicName[] = "pub.example"; + byte sniBody[24]; + byte buf[32]; + word16 sniLen, len; + Suites suites; + + /* Outer SNI equals the ECH config's publicName: the while loop's + * XSTRLEN/XSTRNCMP operands are both true, matched is set, and the + * response is installed onto ech->extensions instead of + * ssl->extensions. No local SNI is configured, so checkPublic is what + * drove this parse at all. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&echConfig, 0, sizeof(echConfig)); + echConfig.publicName = publicName; + ExpectNotNull(ech = (WOLFSSL_ECH*)XMALLOC(sizeof(WOLFSSL_ECH), ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER)); + if (ech != NULL) { + XMEMSET(ech, 0, sizeof(WOLFSSL_ECH)); + ech->echConfig = &echConfig; + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_ECH, ech, ssl->heap), 0); + } + XMEMSET(&suites, 0, sizeof(suites)); + sniLen = build_sni_body(sniBody, publicName); + len = build_ext_with_body(buf, TLSX_SERVER_NAME, sniBody, sniLen); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Outer SNI is the same length as the publicName but differs in + * content: XSTRLEN(...) == size is true, XSTRNCMP(...) == 0 is false, + * so the loop does not match. checkPublic then makes the mismatch a + * silent no-op instead of an alert - unlike a locally configured SNI + * mismatch, which aborts the handshake. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&echConfig, 0, sizeof(echConfig)); + echConfig.publicName = publicName; + ExpectNotNull(ech = (WOLFSSL_ECH*)XMALLOC(sizeof(WOLFSSL_ECH), ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER)); + if (ech != NULL) { + XMEMSET(ech, 0, sizeof(WOLFSSL_ECH)); + ech->echConfig = &echConfig; + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_ECH, ech, ssl->heap), 0); + } + XMEMSET(&suites, 0, sizeof(suites)); + sniLen = build_sni_body(sniBody, "pub.examplx"); + len = build_ext_with_body(buf, TLSX_SERVER_NAME, sniBody, sniLen); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_PreSharedKey_Parse_ClientHello(): identity list --------------- */ +/* if (len < MIN_PSK_ID_LEN || length - idx < len) return BUFFER_E; + * ... + * if (len < OPAQUE16_LEN + identityLen + OPAQUE32_LEN || + * identityLen > MAX_PSK_ID_LEN) return BUFFER_E; */ +int test_tls_msgtype_psk_ch_id_gates(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[MAX_PSK_ID_LEN + 64]; + byte body[MAX_PSK_ID_LEN + 48]; + word16 len; + Suites suites; + + /* op0 true: identities length (5) is below MIN_PSK_ID_LEN (6). Padded + * to 4 bytes total so the earlier "room for both length fields" check + * (length - idx < 2*OPAQUE16_LEN) passes and this is the check that + * fires. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + { + byte shortLen[2 * OPAQUE16_LEN] = { 0x00, 0x05, 0x00, 0x00 }; + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, shortLen, + (word16)sizeof(shortLen)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op0 false, op1 true: identities length (6) meets MIN_PSK_ID_LEN, but + * fewer than 6 bytes actually follow in the extension. (op0 false, + * op1 false is the psk_duplicate test's valid ClientHello body.) */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + { + byte truncated[4] = { 0x00, 0x06, 0x00, 0x00 }; + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, truncated, + (word16)sizeof(truncated)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Per-identity op0 true: identities length (6) is consistent with the + * outer check, but the single identity inside it claims a 10-byte + * identity plus age though only 4 bytes remain for them. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + { + byte b[8] = { 0x00, 0x06, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00 }; + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, b, + (word16)sizeof(b)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Per-identity op0 false, op1 true: identityLen is one more than + * MAX_PSK_ID_LEN, with enough buffer supplied to hold it in full. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + { + word16 idLen = MAX_PSK_ID_LEN + 1; + word16 identitiesLen = (word16)(OPAQUE16_LEN + idLen + OPAQUE32_LEN); + word16 idx = 0; + + body[idx++] = (byte)(identitiesLen >> 8); + body[idx++] = (byte)identitiesLen; + body[idx++] = (byte)(idLen >> 8); + body[idx++] = (byte)idLen; + XMEMSET(body + idx, 0x41, idLen); + idx = (word16)(idx + idLen); + body[idx++] = 0; body[idx++] = 0; body[idx++] = 0; body[idx++] = 0; + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, body, idx); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +#if defined(WOLFSSL_TLS13) && (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) +/* A two-identity ClientHello pre_shared_key body: two distinct identities + * (so TLSX_PreSharedKey_Use() creates two list entries instead of + * deduplicating on identical content) and two SHA-256-sized binders. */ +static const byte psk_ch_body_two[] = { + 0x00, 0x0D, /* identities len */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* identity #1 (empty) */ + 0x00, 0x01, 0xBB, 0x00, 0x00, 0x00, 0x00, /* identity #2 (1 byte) */ + 0x00, 0x42, /* binders len = 66 */ + 0x20, + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, + 0x20, + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 +}; +#endif + +/* ---- TLSX_PreSharedKey_Parse(): server-selected identity index --------- */ +/* for (; list != NULL && idx > 0; idx--) list = list->next; */ +int test_tls_msgtype_psk_sh_index(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + TLSX* extension = NULL; + PreSharedKey* list = NULL; + byte chBuf[96]; + byte shBuf[8]; + byte idxBody[OPAQUE16_LEN]; + word16 chLen, shLen; + Suites suites; + + /* op0 true, op1 true (continue) then op0 true, op1 false (stop): two + * identities on the list, server selects index 1 (the second). */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + chLen = build_ext_with_body(chBuf, TLSX_PRE_SHARED_KEY, psk_ch_body_two, + (word16)sizeof(psk_ch_body_two)); + ExpectIntEQ(TLSX_Parse(ssl, chBuf, chLen, client_hello, &suites), 0); + ExpectNotNull(extension = TLSX_Find(ssl->extensions, TLSX_PRE_SHARED_KEY)); + if (ssl != NULL && ssl->session != NULL && ssl->ctx != NULL) + ssl->session->version = ssl->ctx->method->version; + idxBody[0] = 0x00; idxBody[1] = 0x01; /* choose index 1 */ + shLen = build_ext_with_body(shBuf, TLSX_PRE_SHARED_KEY, idxBody, + (word16)sizeof(idxBody)); + ExpectIntEQ(TLSX_Parse(ssl, shBuf, shLen, server_hello, NULL), 0); + if (extension != NULL) + list = (PreSharedKey*)extension->data; + ExpectNotNull(list); + if (list != NULL) { + ExpectIntEQ(list->chosen, 0); + ExpectNotNull(list->next); + if (list->next != NULL) + ExpectIntEQ(list->next->chosen, 1); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op0 false: a single identity, server selects index 1 - the loop + * runs out of list (masking op1) before idx reaches 0. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + chLen = build_ext_with_body(chBuf, TLSX_PRE_SHARED_KEY, psk_ch_body, + (word16)sizeof(psk_ch_body)); + ExpectIntEQ(TLSX_Parse(ssl, chBuf, chLen, client_hello, &suites), 0); + idxBody[0] = 0x00; idxBody[1] = 0x01; /* index 1, out of range */ + shLen = build_ext_with_body(shBuf, TLSX_PRE_SHARED_KEY, idxBody, + (word16)sizeof(idxBody)); + ExpectIntEQ(TLSX_Parse(ssl, shBuf, shLen, server_hello, NULL), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_PreSharedKey_Parse(): resumed-session consistency ------------ */ +/* if (ssl->options.cipherSuite0 != ssl->session->cipherSuite0 || + * ssl->options.cipherSuite != ssl->session->cipherSuite || + * ssl->session->version.major != ssl->ctx->method->version.major || + * ssl->session->version.minor != ssl->ctx->method->version.minor) + * return PSK_KEY_ERROR; */ +int test_tls_msgtype_psk_sh_resumption(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte chBuf[64]; + byte shBuf[8]; + static const byte idxBody[OPAQUE16_LEN] = { 0x00, 0x00 }; + word16 chLen, shLen; + Suites suites; + + shLen = build_ext_with_body(shBuf, TLSX_PRE_SHARED_KEY, idxBody, + (word16)sizeof(idxBody)); + + /* Baseline: cipherSuite0/cipherSuite/version all match - success. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + chLen = build_ext_with_body(chBuf, TLSX_PRE_SHARED_KEY, psk_ch_body, + (word16)sizeof(psk_ch_body)); + ExpectIntEQ(TLSX_Parse(ssl, chBuf, chLen, client_hello, &suites), 0); + if (ssl != NULL && ssl->session != NULL && ssl->ctx != NULL) + ssl->session->version = ssl->ctx->method->version; + ExpectIntEQ(TLSX_Parse(ssl, shBuf, shLen, server_hello, NULL), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op0 true: cipherSuite0 mismatch. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + chLen = build_ext_with_body(chBuf, TLSX_PRE_SHARED_KEY, psk_ch_body, + (word16)sizeof(psk_ch_body)); + ExpectIntEQ(TLSX_Parse(ssl, chBuf, chLen, client_hello, &suites), 0); + if (ssl != NULL && ssl->session != NULL && ssl->ctx != NULL) { + ssl->session->version = ssl->ctx->method->version; + ssl->session->cipherSuite0 = 1; + } + ExpectIntEQ(TLSX_Parse(ssl, shBuf, shLen, server_hello, NULL), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op1 true: cipherSuite mismatch. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + chLen = build_ext_with_body(chBuf, TLSX_PRE_SHARED_KEY, psk_ch_body, + (word16)sizeof(psk_ch_body)); + ExpectIntEQ(TLSX_Parse(ssl, chBuf, chLen, client_hello, &suites), 0); + if (ssl != NULL && ssl->session != NULL && ssl->ctx != NULL) { + ssl->session->version = ssl->ctx->method->version; + ssl->session->cipherSuite = 1; + } + ExpectIntEQ(TLSX_Parse(ssl, shBuf, shLen, server_hello, NULL), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op2 true: session version.major mismatch. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + chLen = build_ext_with_body(chBuf, TLSX_PRE_SHARED_KEY, psk_ch_body, + (word16)sizeof(psk_ch_body)); + ExpectIntEQ(TLSX_Parse(ssl, chBuf, chLen, client_hello, &suites), 0); + if (ssl != NULL && ssl->session != NULL && ssl->ctx != NULL) { + ssl->session->version = ssl->ctx->method->version; + ssl->session->version.major = (byte)(ssl->session->version.major + 1); + } + ExpectIntEQ(TLSX_Parse(ssl, shBuf, shLen, server_hello, NULL), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op3 true: session version.minor mismatch. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + chLen = build_ext_with_body(chBuf, TLSX_PRE_SHARED_KEY, psk_ch_body, + (word16)sizeof(psk_ch_body)); + ExpectIntEQ(TLSX_Parse(ssl, chBuf, chLen, client_hello, &suites), 0); + if (ssl != NULL && ssl->session != NULL && ssl->ctx != NULL) { + ssl->session->version = ssl->ctx->method->version; + ssl->session->version.minor = (byte)(ssl->session->version.minor + 1); + } + ExpectIntEQ(TLSX_Parse(ssl, shBuf, shLen, server_hello, NULL), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_PreSharedKey_Parse_ClientHello(): binder list ----------------- */ +/* if (len < MIN_PSK_BINDERS_LEN || length - idx < len) return BUFFER_E; + * while (list != NULL && len > 0) { + * if (list->binderLen < WC_SHA256_DIGEST_SIZE || + * list->binderLen > WC_MAX_DIGEST_SIZE) return BUFFER_E; + * ... + * } + * if (list != NULL || len != 0) return BUFFER_E; */ +int test_tls_msgtype_psk_ch_binder_gates(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[128]; + byte body[128]; + word16 len, idx; + + /* op0 true: binders length (10) is below MIN_PSK_BINDERS_LEN (33). One + * valid identity precedes it so the identity list itself is accepted. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + { + Suites suites; + static const byte shortBinders[] = { + 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 1 identity */ + 0x00, 0x0A, /* binders len = 10 */ + 0,0,0,0,0,0,0,0,0,0 /* 10 filler bytes */ + }; + XMEMSET(&suites, 0, sizeof(suites)); + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, shortBinders, + (word16)sizeof(shortBinders)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op0 false, op1 true: binders length (33) meets MIN_PSK_BINDERS_LEN, + * but far fewer bytes actually remain in the extension. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + { + Suites suites; + static const byte truncatedBinders[] = { + 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 1 identity */ + 0x00, 0x21, /* binders len = 33 */ + 0,0,0,0,0 /* only 5 remain */ + }; + XMEMSET(&suites, 0, sizeof(suites)); + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, truncatedBinders, + (word16)sizeof(truncatedBinders)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Per-binder op0 true: the binder length byte (10) is below + * WC_SHA256_DIGEST_SIZE (32). The declared binders length (34) still + * needs to cover the length byte plus filler so the outer binders- + * length gate passes and this check is the one that fires. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + { + Suites suites; + + idx = 0; + body[idx++] = 0x00; body[idx++] = 0x06; /* identities len */ + body[idx++] = 0x00; body[idx++] = 0x00; /* identityLen = 0 */ + body[idx++] = 0; body[idx++] = 0; body[idx++] = 0; body[idx++] = 0; + body[idx++] = 0x00; body[idx++] = 0x22; /* binders len = 34 */ + body[idx++] = 10; /* binderLen = 10 (< 32) */ + XMEMSET(body + idx, 0, 33); + idx = (word16)(idx + 33); + XMEMSET(&suites, 0, sizeof(suites)); + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, body, idx); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Per-binder op1 true (op0 false): the binder length byte (100) is + * above WC_MAX_DIGEST_SIZE (64). The declared binders length (34) only + * needs to cover the length byte itself plus filler - the check fires + * before any binder bytes are read. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + { + Suites suites; + + idx = 0; + body[idx++] = 0x00; body[idx++] = 0x06; /* identities len */ + body[idx++] = 0x00; body[idx++] = 0x00; /* identityLen = 0 */ + body[idx++] = 0; body[idx++] = 0; body[idx++] = 0; body[idx++] = 0; + body[idx++] = 0x00; body[idx++] = 0x22; /* binders len = 34 */ + body[idx++] = 100; /* binderLen = 100 (> 64) */ + XMEMSET(body + idx, 0, 33); + idx = (word16)(idx + 33); + XMEMSET(&suites, 0, sizeof(suites)); + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, body, idx); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* while(list!=NULL && len>0) op1 (len>0), and the trailing "list != + * NULL" gate: two identities, but only one binder - after consuming + * it, len reaches 0 while list still points at the second identity. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + { + Suites suites; + static const byte twoIdOneBinder[] = { + 0x00, 0x0D, /* identities len = 13 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* identity #1, age = 0 */ + 0x00, 0x01, 0xBB, 0x00, 0x00, 0x00, 0x00, /* identity #2 (1 byte), age = 0 */ + 0x00, 0x21, /* binders len = 33 */ + 0x20, + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 + }; + XMEMSET(&suites, 0, sizeof(suites)); + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, twoIdOneBinder, + (word16)sizeof(twoIdOneBinder)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Trailing "len != 0" gate: one identity but two binders' worth of + * data - list runs out (becomes NULL) while len still has a second + * binder's length left over. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + { + Suites suites; + static const byte oneIdTwoBinders[] = { + 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 1 identity */ + 0x00, 0x42, /* binders len = 66 */ + 0x20, + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, + 0x20, + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 + }; + XMEMSET(&suites, 0, sizeof(suites)); + len = build_ext_with_body(buf, TLSX_PRE_SHARED_KEY, oneIdTwoBinders, + (word16)sizeof(oneIdTwoBinders)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(BUFFER_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_Cookie_Parse() ------------------------------------------------ */ +/* This function is only ever reached through TLSX_Parse()'s own extension + * dispatch (src/tls.c, the TLSX_COOKIE case), which already requires + * IsAtLeastTLSv1_3(ssl->version) and msgType being client_hello or + * hello_retry_request before calling it - both are argued as exclusions in + * the campaign report rather than tested here: + * if (msgType != client_hello && msgType != hello_retry_request) {...} + * - the caller's identical check makes this always false. + * if (ssl->options.dtls && IsAtLeastTLSv1_3(ssl->version)) + * - the second operand is always true here for the same reason; + * only the dtls operand can vary. + * + * if (cookie->len != len || XMEMCMP(cookie->data, input + idx, len) != 0) { + * ... return HRR_COOKIE_ERROR; } */ +int test_tls_msgtype_cookie_parse_gates(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_SEND_HRR_COOKIE) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[24]; + byte body[16]; + word16 len; + Suites suites; + +#if defined(WOLFSSL_DTLS13) && defined(WOLFSSL_DTLS) + /* dtls operand true: no Cookie extension configured yet, and the SSL + * object is DTLS 1.3 - the cookie is accepted and stored rather than + * rejected with HRR_COOKIE_ERROR. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfDTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + body[0] = 0x00; body[1] = 0x04; body[2] = 1; body[3] = 2; body[4] = 3; + body[5] = 4; + len = build_ext_with_body(buf, TLSX_COOKIE, body, 6); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + + /* dtls operand false: same setup, but a plain (non-DTLS) TLS 1.3 + * client - HRR_COOKIE_ERROR because no HelloRetryRequest was sent. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + body[0] = 0x00; body[1] = 0x04; body[2] = 1; body[3] = 2; body[4] = 3; + body[5] = 4; + len = build_ext_with_body(buf, TLSX_COOKIE, body, 6); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(HRR_COOKIE_ERROR)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + +#if defined(WOLFSSL_TEST_STATIC_BUILD) + /* An existing Cookie extension (as if this SSL object had already + * sent a HelloRetryRequest cookie) is compared against a second + * ClientHello's cookie. */ + { + static const byte seedCookie[4] = { 1, 2, 3, 4 }; + + /* op0 true: the echoed cookie's length does not match. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(TLSX_Cookie_Use(ssl, seedCookie, sizeof(seedCookie), + NULL, 0, 1, &ssl->extensions), 0); + XMEMSET(&suites, 0, sizeof(suites)); + body[0] = 0x00; body[1] = 0x02; body[2] = 1; body[3] = 2; + len = build_ext_with_body(buf, TLSX_COOKIE, body, 4); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), + WC_NO_ERR_TRACE(HRR_COOKIE_ERROR)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op0 false, op1 false: length and content both match - the + * cookie is accepted and the request-seen flag is cleared. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(TLSX_Cookie_Use(ssl, seedCookie, sizeof(seedCookie), + NULL, 0, 1, &ssl->extensions), 0); + XMEMSET(&suites, 0, sizeof(suites)); + body[0] = 0x00; body[1] = 0x04; + body[2] = 1; body[3] = 2; body[4] = 3; body[5] = 4; + len = build_ext_with_body(buf, TLSX_COOKIE, body, 6); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + } +#endif /* WOLFSSL_TEST_STATIC_BUILD */ +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_TCA_Parse(): gates -------------------------------------------- */ +/* !isRequest branch: if (!extension || !extension->data) + * return TLSX_HandleUnsupportedExtension(ssl); + * server branch: if (!extension || !extension->data) return 0; + * X509_NAME branch: if ((offset > length) || (idSz > length - offset)) + * return BUFFER_ERROR; + * - offset > length is unreachable here: the immediately preceding + * check (offset + OPAQUE16_LEN > length) guarantees offset <= length + * after the OPAQUE16_LEN advance, so this operand never pairs + * (excluded in the campaign report, not tested here). */ +int test_tls_msgtype_tca_parse_gates(void) +{ + EXPECT_DECLS; +#if defined(HAVE_TRUSTED_CA) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + TLSX* extension = NULL; + void* savedExtData = NULL; + const byte id[] = { 1, 2, 3, 4 }; + byte buf[16]; + word16 len; + + /* 3131 op0 true: no TCA configured at all - the client must treat an + * unsolicited response as unsupported. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + len = build_ext(buf, TLSX_TRUSTED_CA_KEYS, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* 3131 op1 true: TCA configured, but its data was cleared - same + * outcome via the other operand. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_X509_NAME, + id, (word32)sizeof(id)), WOLFSSL_SUCCESS); + ExpectNotNull(extension = TLSX_Find(ssl->extensions, + TLSX_TRUSTED_CA_KEYS)); + if (extension != NULL) { + savedExtData = extension->data; + extension->data = NULL; + } + len = build_ext(buf, TLSX_TRUSTED_CA_KEYS, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + if (extension != NULL) + extension->data = savedExtData; + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* 3131 both false: TCA configured normally, empty response body - + * accepted, response flag set. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_X509_NAME, + id, (word32)sizeof(id)), WOLFSSL_SUCCESS); + len = build_ext(buf, TLSX_TRUSTED_CA_KEYS, 0); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, server_hello, NULL), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* 3145 op1 true: server side, TCA configured but its data was + * cleared - "not enabled at server side" is taken, not a crash. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_X509_NAME, + id, (word32)sizeof(id)), WOLFSSL_SUCCESS); + ExpectNotNull(extension = TLSX_Find(ssl->extensions, + TLSX_TRUSTED_CA_KEYS)); + if (extension != NULL) { + savedExtData = extension->data; + extension->data = NULL; + } + /* A ClientHello TCA extension must be at least WOLFSSL_TCA_MIN_SIZE_CLIENT + * bytes to pass TLSX_Parse()'s own minimum-size gate; the body content + * is irrelevant here since extension->data == NULL returns before the + * body is ever read. */ + len = build_ext(buf, TLSX_TRUSTED_CA_KEYS, WOLFSSL_TCA_MIN_SIZE_CLIENT); + { + Suites suites; + XMEMSET(&suites, 0, sizeof(suites)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + } + if (extension != NULL) + extension->data = savedExtData; + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_TCA_Find() ----------------------------------------------------- */ +/* if (tca->type == type && idSz == tca->idSz && + * XMEMCMP(id, tca->id, idSz) == 0) break; */ +int test_tls_msgtype_tca_find(void) +{ + EXPECT_DECLS; +#if defined(HAVE_TRUSTED_CA) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) && !defined(NO_SHA) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[24]; + byte body[16]; + word16 len, entryLen; + Suites suites; + const byte sha1Id[WC_SHA_DIGEST_SIZE] = { + 0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11, + 0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11 + }; + const byte idA[] = { 'A','A','A','A' }; + const byte idB[] = { 'B','B','B','B' }; + const byte idAX[] = { 'A','A','A','A','X' }; + + /* op0 false: the configured entry is CERT_SHA1, the query is + * X509_NAME - tca->type != type on the only list entry. Each body is + * a 2-byte list length followed by one type + idSz + id entry. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_CERT_SHA1, + sha1Id, sizeof(sha1Id)), WOLFSSL_SUCCESS); + XMEMSET(&suites, 0, sizeof(suites)); + entryLen = (word16)(1 + OPAQUE16_LEN + sizeof(idA)); + body[0] = (byte)(entryLen >> 8); body[1] = (byte)entryLen; + body[2] = WOLFSSL_TRUSTED_CA_X509_NAME; + body[3] = 0x00; body[4] = (byte)sizeof(idA); + XMEMCPY(body + 5, idA, sizeof(idA)); + len = build_ext_with_body(buf, TLSX_TRUSTED_CA_KEYS, body, + (word16)(2 + entryLen)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op0 true, op1 false: type matches, but the query's idSz (5) + * differs from the configured entry's idSz (4). */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_X509_NAME, + idA, (word32)sizeof(idA)), WOLFSSL_SUCCESS); + XMEMSET(&suites, 0, sizeof(suites)); + entryLen = (word16)(1 + OPAQUE16_LEN + sizeof(idAX)); + body[0] = (byte)(entryLen >> 8); body[1] = (byte)entryLen; + body[2] = WOLFSSL_TRUSTED_CA_X509_NAME; + body[3] = 0x00; body[4] = (byte)sizeof(idAX); + XMEMCPY(body + 5, idAX, sizeof(idAX)); + len = build_ext_with_body(buf, TLSX_TRUSTED_CA_KEYS, body, + (word16)(2 + entryLen)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op0 true, op1 true, op2 false: type and length match, content + * does not. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_X509_NAME, + idA, (word32)sizeof(idA)), WOLFSSL_SUCCESS); + XMEMSET(&suites, 0, sizeof(suites)); + entryLen = (word16)(1 + OPAQUE16_LEN + sizeof(idB)); + body[0] = (byte)(entryLen >> 8); body[1] = (byte)entryLen; + body[2] = WOLFSSL_TRUSTED_CA_X509_NAME; + body[3] = 0x00; body[4] = (byte)sizeof(idB); + XMEMCPY(body + 5, idB, sizeof(idB)); + len = build_ext_with_body(buf, TLSX_TRUSTED_CA_KEYS, body, + (word16)(2 + entryLen)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op0 true, op1 true, op2 true: exact match - found on the first + * (only) list entry. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_X509_NAME, + idA, (word32)sizeof(idA)), WOLFSSL_SUCCESS); + XMEMSET(&suites, 0, sizeof(suites)); + entryLen = (word16)(1 + OPAQUE16_LEN + sizeof(idA)); + body[0] = (byte)(entryLen >> 8); body[1] = (byte)entryLen; + body[2] = WOLFSSL_TRUSTED_CA_X509_NAME; + body[3] = 0x00; body[4] = (byte)sizeof(idA); + XMEMCPY(body + 5, idA, sizeof(idA)); + len = build_ext_with_body(buf, TLSX_TRUSTED_CA_KEYS, body, + (word16)(2 + entryLen)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +#if defined(HAVE_TRUSTED_CA) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) +/* A small counting allocator used to force a single, targeted malloc + * failure inside TLSX_TCA_New(). Installed narrowly around the call under + * test and restored immediately after. */ +static int tca_fail_after = -1; +static int tca_alloc_seen = 0; + +TEST_TLS_MSGTYPE_UNUSED +static void* tca_fail_malloc(size_t size) +{ + if (tca_fail_after >= 0) { + if (tca_alloc_seen == tca_fail_after) { + tca_alloc_seen++; + return NULL; + } + tca_alloc_seen++; + } + return malloc(size); +} + +TEST_TLS_MSGTYPE_UNUSED +static void tca_fail_free(void* ptr) +{ + free(ptr); +} + +TEST_TLS_MSGTYPE_UNUSED +static void* tca_fail_realloc(void* ptr, size_t size) +{ + return realloc(ptr, size); +} +#endif /* HAVE_TRUSTED_CA && !NO_WOLFSSL_CLIENT && !NO_TLS */ + +/* ---- TLSX_TCA_New(): id allocation failure ------------------------------ */ +/* KEY_SHA1/CERT_SHA1: if (idSz == WC_SHA_DIGEST_SIZE && + * (tca->id = XMALLOC(idSz, ...))) {...} + * X509_NAME: if (idSz > 0 && + * (tca->id = XMALLOC(idSz, ...))) {...} + * In both cases the length operand's pair is already covered elsewhere; + * only the allocation succeeding vs. failing is exercised here. */ +int test_tls_msgtype_tca_new_alloc(void) +{ + EXPECT_DECLS; +#if defined(HAVE_TRUSTED_CA) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && !defined(NO_SHA) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + wolfSSL_Malloc_cb prevM = NULL; + wolfSSL_Free_cb prevF = NULL; + wolfSSL_Realloc_cb prevR = NULL; + const byte sha1Id[WC_SHA_DIGEST_SIZE] = { + 0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22, + 0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22 + }; + const byte nameId[] = { 5, 6, 7, 8 }; + + /* CERT_SHA1: the TCA struct itself (allocation #0) succeeds, the id + * buffer (allocation #1) fails. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_GetAllocators(&prevM, &prevF, &prevR), 0); + ExpectIntEQ(wolfSSL_SetAllocators(tca_fail_malloc, tca_fail_free, + tca_fail_realloc), 0); + tca_alloc_seen = 0; + tca_fail_after = 1; + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_CERT_SHA1, + sha1Id, sizeof(sha1Id)), WC_NO_ERR_TRACE(MEMORY_E)); + tca_fail_after = -1; + (void)wolfSSL_SetAllocators(prevM, prevF, prevR); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* X509_NAME: same shape - struct allocation succeeds, id buffer + * allocation fails. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_GetAllocators(&prevM, &prevF, &prevR), 0); + ExpectIntEQ(wolfSSL_SetAllocators(tca_fail_malloc, tca_fail_free, + tca_fail_realloc), 0); + tca_alloc_seen = 0; + tca_fail_after = 1; + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_X509_NAME, + nameId, (word32)sizeof(nameId)), WC_NO_ERR_TRACE(MEMORY_E)); + tca_fail_after = -1; + (void)wolfSSL_SetAllocators(prevM, prevF, prevR); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_PreSharedKey_Write(): server's chosen identity ---------------- */ +/* for (i=0; list != NULL && !list->chosen; i++) list = list->next; */ +int test_tls_msgtype_psk_write_chosen(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && (defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) && defined(WOLFSSL_TEST_STATIC_BUILD) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + TLSX* extension = NULL; + PreSharedKey* pskA = NULL; + PreSharedKey* pskB = NULL; + byte identityA[] = { 0xAA }; + byte identityB[] = { 0xBB }; + byte output[32]; + word16 offset; + + /* op0 true, op1 true (continue) then op0 true, op1 false (stop): two + * identities, the second one chosen. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(TLSX_PreSharedKey_Use(&ssl->extensions, identityA, + (word16)sizeof(identityA), 0, no_mac, 0, 0, 1, &pskA, + ssl->heap), 0); + ExpectIntEQ(TLSX_PreSharedKey_Use(&ssl->extensions, identityB, + (word16)sizeof(identityB), 0, no_mac, 0, 0, 1, &pskB, + ssl->heap), 0); + if (pskB != NULL) + pskB->chosen = 1; + ExpectNotNull(extension = TLSX_Find(ssl->extensions, TLSX_PRE_SHARED_KEY)); + if (extension != NULL) + extension->resp = 1; + offset = 0; + ExpectIntEQ(TLSX_WriteResponse(ssl, output, server_hello, &offset), 0); + ExpectIntGT(offset, 0); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* op0 false: a single, unchosen identity - the loop runs off the end + * of the list before finding a chosen entry. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(TLSX_PreSharedKey_Use(&ssl->extensions, identityA, + (word16)sizeof(identityA), 0, no_mac, 0, 0, 1, &pskA, + ssl->heap), 0); + ExpectNotNull(extension = TLSX_Find(ssl->extensions, TLSX_PRE_SHARED_KEY)); + if (extension != NULL) + extension->resp = 1; + offset = 0; + ExpectIntEQ(TLSX_WriteResponse(ssl, output, server_hello, &offset), + WC_NO_ERR_TRACE(BUILD_MSG_ERROR)); + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_tls_msgtype.h b/tests/api/test_tls_msgtype.h new file mode 100644 index 0000000000..69fcc48060 --- /dev/null +++ b/tests/api/test_tls_msgtype.h @@ -0,0 +1,124 @@ +/* test_tls_msgtype.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 TESTS_API_TEST_TLS_MSGTYPE_H +#define TESTS_API_TEST_TLS_MSGTYPE_H + +int test_tls_msgtype_arg_guard(void); +int test_tls_msgtype_psk_duplicate(void); +int test_tls_msgtype_certificate_ext_offered(void); +int test_tls_msgtype_sni_tls13(void); +int test_tls_msgtype_sni_tls12(void); +int test_tls_msgtype_tca(void); +int test_tls_msgtype_mfl_tls13(void); +int test_tls_msgtype_mfl_tls12(void); +int test_tls_msgtype_supported_groups_tls13(void); +int test_tls_msgtype_point_formats(void); +int test_tls_msgtype_csr_tls13(void); +int test_tls_msgtype_csr_tls12(void); +int test_tls_msgtype_csr2_tls12(void); +int test_tls_msgtype_extms(void); +int test_tls_msgtype_renegotiation_info(void); +int test_tls_msgtype_session_ticket_tls12(void); +int test_tls_msgtype_alpn_tls13(void); +int test_tls_msgtype_alpn_tls12(void); +int test_tls_msgtype_sigalgs_tls13(void); +int test_tls_msgtype_etm(void); +int test_tls_msgtype_supported_versions(void); +int test_tls_msgtype_cookie(void); +int test_tls_msgtype_psk(void); +int test_tls_msgtype_cert_with_extern_psk(void); +int test_tls_msgtype_early_data(void); +int test_tls_msgtype_sigalgs_cert(void); +int test_tls_msgtype_key_share(void); +int test_tls_msgtype_client_cert_type_tls13(void); +int test_tls_msgtype_client_cert_type_tls12(void); +int test_tls_msgtype_server_cert_type_tls13(void); +int test_tls_msgtype_server_cert_type_tls12(void); +int test_tls_msgtype_connection_id(void); +int test_tls_msgtype_ech(void); +int test_tls_msgtype_sni_find(void); +int test_tls_msgtype_sni_parse_response_gate(void); +int test_tls_msgtype_sni_parse_size_gates(void); +int test_tls_msgtype_sni_parse_cacheonly(void); +int test_tls_msgtype_sni_parse_match(void); +int test_tls_msgtype_sni_parse_ech_public(void); +int test_tls_msgtype_psk_ch_id_gates(void); +int test_tls_msgtype_psk_ch_binder_gates(void); +int test_tls_msgtype_psk_sh_index(void); +int test_tls_msgtype_psk_sh_resumption(void); +int test_tls_msgtype_cookie_parse_gates(void); +int test_tls_msgtype_tca_parse_gates(void); +int test_tls_msgtype_tca_find(void); +int test_tls_msgtype_tca_new_alloc(void); +int test_tls_msgtype_psk_write_chosen(void); + +#define TEST_TLS_MSGTYPE_DECLS \ + TEST_DECL_GROUP("tls", test_tls_msgtype_arg_guard), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_psk_duplicate), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_certificate_ext_offered), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sni_tls13), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sni_tls12), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_tca), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_mfl_tls13), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_mfl_tls12), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_supported_groups_tls13), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_point_formats), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_csr_tls13), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_csr_tls12), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_csr2_tls12), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_extms), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_renegotiation_info), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_session_ticket_tls12), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_alpn_tls13), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_alpn_tls12), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sigalgs_tls13), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_etm), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_supported_versions), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_cookie), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_psk), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_cert_with_extern_psk), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_early_data), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sigalgs_cert), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_key_share), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_client_cert_type_tls13), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_client_cert_type_tls12), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_server_cert_type_tls13), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_server_cert_type_tls12), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_connection_id), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_ech), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sni_find), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sni_parse_response_gate), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sni_parse_size_gates), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sni_parse_cacheonly), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sni_parse_match), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_sni_parse_ech_public), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_psk_ch_id_gates), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_psk_ch_binder_gates), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_psk_sh_index), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_psk_sh_resumption), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_cookie_parse_gates), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_tca_parse_gates), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_tca_find), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_tca_new_alloc), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_psk_write_chosen) + +#endif /* TESTS_API_TEST_TLS_MSGTYPE_H */ diff --git a/tests/api/test_tls_parse.c b/tests/api/test_tls_parse.c new file mode 100644 index 0000000000..0f2f17d005 --- /dev/null +++ b/tests/api/test_tls_parse.c @@ -0,0 +1,3159 @@ +/* test_tls_parse.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 + +#include +#include +#ifndef NO_DH +#include +#endif + +/* Several helpers below are called only from test bodies whose feature guards + * differ, so a configuration can compile in none of their callers. */ +#if defined(__GNUC__) || defined(__clang__) + #define TEST_TLS_PARSE_UNUSED __attribute__((unused)) +#else + #define TEST_TLS_PARSE_UNUSED +#endif + +/* Helper to build a server-side WOLFSSL_CTX with a certificate/key loaded, + * as required for wolfSSL_new() to succeed on a server context. + */ +TEST_TLS_PARSE_UNUSED +static WOLFSSL_CTX* test_tls_parse_server_ctx(WOLFSSL_METHOD* method) +{ + WOLFSSL_CTX* ctx = NULL; + + if (method == NULL) + return NULL; + + ctx = wolfSSL_CTX_new(method); + if (ctx == NULL) + return NULL; + +#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + (!defined(NO_RSA) || defined(HAVE_ECC)) + #ifndef NO_RSA + const char* certFile = svrCertFile; + const char* keyFile = svrKeyFile; + #else + /* An ECC-only build has no RSA server certificate to load. */ + const char* certFile = eccCertFile; + const char* keyFile = eccKeyFile; + #endif + + if (wolfSSL_CTX_use_certificate_file(ctx, certFile, CERT_FILETYPE) + != WOLFSSL_SUCCESS || + wolfSSL_CTX_use_PrivateKey_file(ctx, keyFile, CERT_FILETYPE) + != WOLFSSL_SUCCESS) { + wolfSSL_CTX_free(ctx); + return NULL; + } +#else + wolfSSL_CTX_free(ctx); + return NULL; +#endif + + return ctx; +} + +/* TLSX_Parse() walks a concatenated list of type(2)/length(2)/body + * extensions, exactly as they appear on the wire -- not a single + * extension's body on its own. This wraps one extension's body with that + * header into 'out', which must be at least bodyLen + 4 bytes, and returns + * the total length. + */ +TEST_TLS_PARSE_UNUSED +static word16 test_tls_parse_build_ext(byte* out, word16 outCap, + word16 type, const byte* body, word16 bodyLen) +{ + /* A body that doesn't fit the caller's buffer is a fixture bug: fail + * loudly and locally rather than silently overrunning the stack. */ + if ((word32)bodyLen + 4 > outCap) { + fprintf(stderr, "test_tls_parse_build_ext: body of %u bytes does " + "not fit a %u byte buffer\n", (unsigned)bodyLen, + (unsigned)outCap); + abort(); + } + out[0] = (byte)(type >> 8); + out[1] = (byte)(type & 0xFF); + out[2] = (byte)(bodyLen >> 8); + out[3] = (byte)(bodyLen & 0xFF); + if (bodyLen > 0 && body != NULL) + XMEMCPY(out + 4, body, bodyLen); + return (word16)(4 + bodyLen); +} + +/* A small counting allocator used to force a single, targeted malloc + * failure. Installed narrowly around the call under test and restored + * immediately after, so it never affects unrelated allocations. + */ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && !defined(NO_TLS) +static int tls_parse_fail_after = -1; +static int tls_parse_alloc_seen = 0; + +TEST_TLS_PARSE_UNUSED +static void* tls_parse_fail_malloc(size_t size) +{ + if (tls_parse_fail_after >= 0) { + if (tls_parse_alloc_seen == tls_parse_fail_after) { + tls_parse_alloc_seen++; + return NULL; + } + tls_parse_alloc_seen++; + } + return malloc(size); +} + +TEST_TLS_PARSE_UNUSED +static void tls_parse_fail_free(void* ptr) +{ + free(ptr); +} + +TEST_TLS_PARSE_UNUSED +static void* tls_parse_fail_realloc(void* ptr, size_t size) +{ + return realloc(ptr, size); +} +#endif /* WOLFSSL_TEST_STATIC_BUILD && !NO_TLS */ + +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_SUPPORTED_CURVES) +/* Pushes a single-entry supported_groups restriction directly, bypassing + * TLSX_UseSupportedCurve()'s TLSX_IsGroupSupported() gate -- needed to name + * a group id this build does not itself recognise, the same as a peer's + * raw wire value would. */ +TEST_TLS_PARSE_UNUSED +static int test_tls_parse_push_curve(TLSX** extensions, WOLFSSL* ssl, + word16 name) +{ + SupportedCurve* curve = (SupportedCurve*)XMALLOC(sizeof(SupportedCurve), + ssl->heap, DYNAMIC_TYPE_TLSX); + if (curve == NULL) + return WC_NO_ERR_TRACE(MEMORY_E); + curve->name = name; + curve->next = NULL; + return TLSX_Push(extensions, TLSX_SUPPORTED_GROUPS, curve, ssl->heap); +} + +/* Builds and pushes a minimal key share entry -- a peer offer that was + * never processed into a real key -- for tests that only need the + * bookkeeping fields (group, ke) a negotiation helper looks at. */ +TEST_TLS_PARSE_UNUSED +static KeyShareEntry* test_tls_parse_push_kse(TLSX** extensions, WOLFSSL* ssl, + word16 group) +{ + KeyShareEntry* kse = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX); + if (kse == NULL) + return NULL; + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = group; + kse->ke = (byte*)XMALLOC(1, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + if (kse->ke == NULL) { + XFREE(kse, ssl->heap, DYNAMIC_TYPE_TLSX); + return NULL; + } + kse->ke[0] = 0xAA; + kse->keLen = 1; + if (TLSX_Push(extensions, TLSX_KEY_SHARE, kse, ssl->heap) != 0) { + XFREE(kse->ke, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + XFREE(kse, ssl->heap, DYNAMIC_TYPE_TLSX); + return NULL; + } + return kse; +} + +/* TLSX_KeyShare_FreeAll() is not visible outside src/tls.c even as + * WOLFSSL_LOCAL; releasing a standalone (not already list-linked) entry + * built directly for a test goes through the generic TLSX_FreeAll() + * instead, via a throwaway one-node extension list. */ +TEST_TLS_PARSE_UNUSED +static void test_tls_parse_free_kse(WOLFSSL* ssl, KeyShareEntry* kse) +{ + TLSX* extensions = NULL; + if (kse == NULL) + return; + if (TLSX_Push(&extensions, TLSX_KEY_SHARE, kse, ssl->heap) != 0) + return; + TLSX_FreeAll(extensions, ssl->heap); +} +#endif /* WOLFSSL_TEST_STATIC_BUILD && WOLFSSL_TLS13 && HAVE_SUPPORTED_CURVES */ + +/* ---- ALPN --------------------------------------------------------------- */ +/* RFC 7301: covers the TLSX_APPLICATION_LAYER_PROTOCOL parse helpers that + * TLSX_Parse() reaches for both the client_hello (isRequest) and + * server_hello (response) directions. + */ +int test_TLSX_ALPN_parse(void) +{ + EXPECT_DECLS; +/* TLSX_ALPN_GetRequest() below is WOLFSSL_LOCAL: needs the static library. */ +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(HAVE_ALPN) && defined(HAVE_TLS_EXTENSIONS) && !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[300]; + word16 extLen; + + /* TLSX_ALPN_New(), reached through TLSX_UseALPN(): an over-length name + * is rejected; a normal one is accepted. (A NULL name is rejected by + * TLSX_UseALPN()'s own guard before TLSX_ALPN_New() is reached at all.) + */ +#ifdef WOLFSSL_TEST_STATIC_BUILD + { + TLSX* extensions = NULL; + byte tooLong[WOLFSSL_MAX_ALPN_PROTO_NAME_LEN + 1]; + XMEMSET(tooLong, 'a', sizeof(tooLong)); + + ExpectIntEQ(TLSX_UseALPN(&extensions, tooLong, sizeof(tooLong), 0, + NULL), WC_NO_ERR_TRACE(MEMORY_E)); + ExpectNull(extensions); + ExpectIntEQ(TLSX_UseALPN(&extensions, "http/1.1", 8, 0, NULL), + WOLFSSL_SUCCESS); + ExpectNotNull(extensions); + TLSX_FreeAll(extensions, NULL); + } +#endif + + /* TLSX_ALPN_GetRequest(): each NULL argument independently. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + void* data = NULL; + word16 dataSz = 0; + + ExpectIntEQ(TLSX_ALPN_GetRequest(NULL, &data, &dataSz), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wolfSSL_UseALPN(ssl, (char*)"http/1.1", 8, + WOLFSSL_ALPN_CONTINUE_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_ALPN_GetRequest(ssl->extensions, NULL, &dataSz), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(TLSX_ALPN_GetRequest(ssl->extensions, &data, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(TLSX_ALPN_GetRequest(ssl->extensions, &data, &dataSz), + WOLFSSL_ALPN_NOT_FOUND); + } + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + ctx = NULL; + + /* TLSX_ALPN_ParseAndSet() response direction and TLSX_ALPN_Find()/ + * ALPN_find_match(), all reached through TLSX_Parse(). The client + * configures a multi-entry ALPN list; the crafted server_hello + * response searches it, exercising exact match, same-length mismatch + * and different-length mismatch. + */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseALPN(ssl, (char*)"aa", 2, WOLFSSL_ALPN_FAILED_ON_MISMATCH), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseALPN(ssl, (char*)"bb", 2, + WOLFSSL_ALPN_CONTINUE_ON_MISMATCH), WOLFSSL_SUCCESS); + { + /* mismatched content, same length as "bb": exercises the + * XSTRNCMP() half of the search without an early length miss. */ + const byte respSameLen[] = { 0x00, 0x03, 0x02, 'z', 'z' }; + /* mismatched, longer than any configured name: every configured + * entry is rejected on length before content is compared. */ + const byte respDiffLen[] = { 0x00, 0x04, 0x03, 'z', 'z', 'z' }; + /* server_hello response, exact match on "bb". Run last: a + * successful match rewrites the ALPN list (marks the negotiated + * entry), so it must not run before the mismatch cases above. */ + const byte respExact[] = { 0x00, 0x03, 0x02, 'b', 'b' }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_APPLICATION_LAYER_PROTOCOL, respSameLen, + (word16)sizeof(respSameLen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), 0); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_APPLICATION_LAYER_PROTOCOL, respDiffLen, + (word16)sizeof(respDiffLen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), 0); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_APPLICATION_LAYER_PROTOCOL, respExact, + (word16)sizeof(respExact)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Response direction, nothing configured on this client at all: + * ALPN_find_match()'s extension == NULL path. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte resp[] = { 0x00, 0x03, 0x02, 'h', 'i' }; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_APPLICATION_LAYER_PROTOCOL, resp, + (word16)sizeof(resp)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Response direction, two protocol names instead of the one RFC 7301 + * Section 3.1 allows in a response. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_UseALPN(ssl, (char*)"xx", 2, + WOLFSSL_ALPN_CONTINUE_ON_MISMATCH), WOLFSSL_SUCCESS); + if (ssl != NULL) { + const byte twoEntries[] = { 0x00, 0x06, 0x02, 'x', 'x', 0x02, 'y', 'y' }; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_APPLICATION_LAYER_PROTOCOL, twoEntries, + (word16)sizeof(twoEntries)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Request direction: server parses and stores a client_hello ALPN + * list. Exercises TLSX_ALPN_ParseAndSet()'s length bookkeeping (shared + * with the response direction) on the request side, including the + * per-entry wlen == 0 and length-overflow checks and the overall list + * size mismatch. + */ + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + /* list size claims 5 bytes, only 3 are present. */ + const byte badListLen[] = { 0x00, 0x05, 0x02, 'h', 'i' }; + /* first entry's own length prefix is 0. */ + const byte zeroWlen[] = { 0x00, 0x01, 0x00 }; + /* first entry's length prefix (5) runs past the list. */ + const byte overWlen[] = { 0x00, 0x03, 0x05, 'h', 'i' }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_APPLICATION_LAYER_PROTOCOL, badListLen, + (word16)sizeof(badListLen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_APPLICATION_LAYER_PROTOCOL, zeroWlen, + (word16)sizeof(zeroWlen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_APPLICATION_LAYER_PROTOCOL, overWlen, + (word16)sizeof(overWlen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte req[] = { 0x00, 0x03, 0x02, 'h', 'i' }; + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_APPLICATION_LAYER_PROTOCOL, req, (word16)sizeof(req)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Trusted CA Keys ------------------------------------------------------ + * RFC 6066 Section 6. TLSX_TCA_New()'s per-type id validation, TLSX_TCA_Find() + * and the bounds checks in TLSX_TCA_Parse(), all server side. + */ +int test_TLSX_TCA_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_TRUSTED_CA) && defined(HAVE_TLS_EXTENSIONS) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_SHA) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[64]; + word16 extLen; + + /* TLSX_TCA_New(): SHA1 id of the wrong size is rejected; the right + * size is accepted. Also the X509 name id case (any idSz > 0). + */ +#ifdef WOLFSSL_TEST_STATIC_BUILD + { + TLSX* extensions = NULL; + byte id[WC_SHA_DIGEST_SIZE + 1]; + XMEMSET(id, 0x11, sizeof(id)); + + ExpectIntEQ(TLSX_UseTrustedCA(&extensions, + WOLFSSL_TRUSTED_CA_KEY_SHA1, id, WC_SHA_DIGEST_SIZE + 1, + NULL), WC_NO_ERR_TRACE(MEMORY_E)); + ExpectNull(extensions); + ExpectIntEQ(TLSX_UseTrustedCA(&extensions, + WOLFSSL_TRUSTED_CA_KEY_SHA1, id, WC_SHA_DIGEST_SIZE, + NULL), WOLFSSL_SUCCESS); + ExpectNotNull(extensions); + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + + ExpectIntEQ(TLSX_UseTrustedCA(&extensions, + WOLFSSL_TRUSTED_CA_X509_NAME, id, 0, NULL), + WC_NO_ERR_TRACE(MEMORY_E)); + ExpectNull(extensions); + ExpectIntEQ(TLSX_UseTrustedCA(&extensions, + WOLFSSL_TRUSTED_CA_X509_NAME, id, 1, NULL), + WOLFSSL_SUCCESS); + ExpectNotNull(extensions); + TLSX_FreeAll(extensions, NULL); + } +#endif + + /* TLSX_TCA_Parse(), server side: not configured (no-op success), a + * length-prefix mismatch, and a well-formed list that TLSX_TCA_Find() + * matches by a pre-agreed entry (no id comparison needed). + */ + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* No trusted_ca_keys configured on the server: skip, success. + * An empty list (list length 0) is the shortest body that still + * satisfies the extension's minimum-size gate. */ + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte empty[] = { 0x00, 0x00 }; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_TRUSTED_CA_KEYS, empty, + (word16)sizeof(empty)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + /* list length (1) does not match actual body (3 bytes follow). */ + const byte badLen[] = { 0x00, 0x01, 0x00, 0x00, 0x00 }; + /* trusted_ca_keys list: one entry, type pre_agreed(0), matches the + * configured entry via TLSX_TCA_Find()'s type-only comparison. */ + const byte good[] = { 0x00, 0x01, WOLFSSL_TRUSTED_CA_PRE_AGREED }; + + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_PRE_AGREED, + NULL, 0), WOLFSSL_SUCCESS); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_TRUSTED_CA_KEYS, badLen, + (word16)sizeof(badLen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_TRUSTED_CA_KEYS, good, + (word16)sizeof(good)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + byte id[WC_SHA_DIGEST_SIZE]; + /* X509 name id: id length prefix claims more than remains. */ + const byte truncated[] = { + 0x00, 0x05, /* list length */ + WOLFSSL_TRUSTED_CA_X509_NAME, 0x00, 0x03, 0xAA, 0xBB + /* idSz=3 claimed, only 2 bytes remain: offset+idSz > length. */ + }; + const byte fits[] = { + 0x00, 0x05, + WOLFSSL_TRUSTED_CA_X509_NAME, 0x00, 0x02, 0xAA, 0xBB + }; + + XMEMSET(id, 0x33, sizeof(id)); + ExpectIntEQ(wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_KEY_SHA1, + id, sizeof(id)), WOLFSSL_SUCCESS); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_TRUSTED_CA_KEYS, + truncated, (word16)sizeof(truncated)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_TRUSTED_CA_KEYS, fits, + (word16)sizeof(fits)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Client side, response direction, trusted_ca_keys never requested: + * unsupported extension. */ +#ifndef NO_WOLFSSL_CLIENT + ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_TRUSTED_CA_KEYS, NULL, 0); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif +#endif + return EXPECT_RESULT(); +} + +/* ---- Client/Server Certificate Type (RFC 7250, raw public keys) --------- */ +int test_TLSX_certtype_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_RPK) && defined(HAVE_TLS_EXTENSIONS) && !defined(NO_TLS) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[16]; + word16 extLen; + + /* IsCertTypeListed(): value present vs. absent in an offered list, + * reached through TLSX_ClientCertificateType_Parse()'s client-side + * (server_hello) branch. The offered-type bookkeeping is set directly, + * mirroring what the write side records after a real ClientHello is + * built; only the received value's parse is under test here. + */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte respX509[] = { WOLFSSL_CERT_TYPE_X509 }; + const byte respRpk[] = { WOLFSSL_CERT_TYPE_RPK }; + + ssl->options.rpkState.sending_ClientCertTypeCnt = 1; + ssl->options.rpkState.sending_ClientCertTypes[0] = + WOLFSSL_CERT_TYPE_X509; + + /* offered X509, server confirms X509: listed. */ + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_CLIENT_CERTIFICATE, + respX509, (word16)sizeof(respX509)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), 0); + + /* offered X509, server claims RPK: not listed, rejected. */ + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_CLIENT_CERTIFICATE, + respRpk, (word16)sizeof(respRpk)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + + /* A count past MAX_CLIENT_CERT_TYPE_CNT is treated as not listed + * without reading past the fixed-size offered-types array. */ + ssl->options.rpkState.sending_ClientCertTypeCnt = 5; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_CLIENT_CERTIFICATE, respX509, (word16)sizeof(respX509)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Cookie (RFC 8446 4.2.2 / DTLS 1.3 HelloRetryRequest) ---------------- */ +int test_TLSX_Cookie_parse(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_SEND_HRR_COOKIE) && !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[16]; + word16 extLen; + + /* client_hello direction with a previously stored cookie: length/value + * mismatch is rejected, a match is accepted. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte hrr[] = { 0x00, 0x03, 0xAA, 0xBB, 0xCC }; + const byte chBad[] = { 0x00, 0x03, 0xAA, 0xBB, 0xFF }; + const byte chGood[] = { 0x00, 0x03, 0xAA, 0xBB, 0xCC }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_COOKIE, hrr, + (word16)sizeof(hrr)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, hello_retry_request, NULL), + 0); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_COOKIE, chBad, + (word16)sizeof(chBad)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(HRR_COOKIE_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_COOKIE, chGood, + (word16)sizeof(chGood)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + +#ifdef WOLFSSL_DTLS13 + /* DTLS 1.3: a cookie in client_hello with none stored yet and no + * matching extension is accepted (a different SSL instance may have + * issued it). */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfDTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte ch[] = { 0x00, 0x01, 0xAA }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_COOKIE, ch, + (word16)sizeof(ch)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif +#endif + return EXPECT_RESULT(); +} + +/* ---- Encrypt-Then-Mac (RFC 7366) ----------------------------------------- */ +int test_TLSX_EncryptThenMac_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY) && !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[8]; + word16 extLen; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* server_hello, empty body: accepted. */ + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_ENCRYPT_THEN_MAC, NULL, + 0); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Maximum Fragment Length (RFC 6066 Section 4) ------------------------ */ +int test_TLSX_MFL_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MAX_FRAGMENT) && !defined(WOLFSSL_OLD_UNSUPPORTED_EXTENSION) && !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[8]; + word16 extLen; + + /* Client did not request MFL: any server_hello response is flagged as + * an unrequested extension before the value is even looked at. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte resp[] = { WOLFSSL_MFL_2_9 }; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_MAX_FRAGMENT_LENGTH, + resp, (word16)sizeof(resp)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Client requested MFL_2_9: a mismatching echo is rejected, the same + * value is accepted. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte mismatch[] = { WOLFSSL_MFL_2_10 }; + const byte match[] = { WOLFSSL_MFL_2_9 }; + + ExpectIntEQ(wolfSSL_UseMaxFragment(ssl, WOLFSSL_MFL_2_9), + WOLFSSL_SUCCESS); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_MAX_FRAGMENT_LENGTH, + mismatch, (word16)sizeof(mismatch)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(UNKNOWN_MAX_FRAG_LEN_E)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_MAX_FRAGMENT_LENGTH, + match, (word16)sizeof(match)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Truncated HMAC (RFC 6066 Section 7, deprecated) --------------------- */ +int test_TLSX_THM_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_TRUNCATED_HMAC) && !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[8]; + word16 extLen; + + /* Truncated HMAC is only ever dispatched to on client_hello (the + * server side of the extension); the response direction is not + * reachable through TLSX_Parse() at all. */ + ExpectNotNull(ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + byte dummy = 0; + + /* Non-empty body is invalid: extension_data MUST be empty. */ + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_TRUNCATED_HMAC, &dummy, + 1); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + /* Empty body, valid: enables the extension for this connection. */ + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_TRUNCATED_HMAC, NULL, + 0); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Session Ticket (RFC 5077 / RFC 8446 4.6.1) -------------------------- */ +int test_TLSX_SessionTicket_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SESSION_TICKET) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_WOLFSSL_CLIENT) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + WOLFSSL* ssl2 = NULL; + byte ext[600]; + word16 extLen; + + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + /* Too short to be a real ticket: DoClientTicket() rejects it + * without attempting decryption. */ + const byte tooShort[] = { 0x01, 0x02, 0x03, 0x04 }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SESSION_TICKET, + tooShort, (word16)sizeof(tooShort)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(ssl->options.rejectTicket, 1); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* A genuine ticket, decryptable by the ctx that issued it, but + * presented to a connection whose negotiated version is older than + * the ticket's: the ticket must be rejected with a version mismatch, + * not just a generic reject. Uses its own ctx pair (test_memio_setup() + * only wires up its I/O callbacks on a ctx it creates itself). */ + { + struct test_memio_ctx memio; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx2 = NULL; + WOLFSSL* ssl_c = NULL; + byte ticket[512]; + word32 ticketSz = (word32)sizeof(ticket); + + XMEMSET(&memio, 0, sizeof(memio)); + ExpectIntEQ(test_memio_setup(&memio, &ctx_c, &ctx2, &ssl_c, &ssl, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + if (ssl_c != NULL) + ExpectIntEQ(wolfSSL_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl, 10, NULL), 0); + ExpectIntEQ(wolfSSL_get_SessionTicket(ssl_c, ticket, &ticketSz), + WOLFSSL_SUCCESS); + ExpectIntGT(ticketSz, 0); + + if (EXPECT_SUCCESS() && ticketSz > 0) { + ExpectNotNull(ssl2 = wolfSSL_new(ctx2)); + if (ssl2 != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl2); + /* Force this connection to look like it negotiated a + * version older than the ticket's original session. */ + ssl2->version.minor = SSLv3_MINOR; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SESSION_TICKET, + ticket, (word16)ticketSz); + /* The internal VERSION_ERROR is caught and downgraded to a + * non-fatal reject, same as any other undecryptable + * ticket -- it does not propagate out of TLSX_Parse(). */ + ExpectIntEQ(TLSX_Parse(ssl2, ext, extLen, client_hello, + suites), 0); + ExpectIntEQ(ssl2->options.rejectTicket, 1); + } + } + wolfSSL_free(ssl2); + wolfSSL_free(ssl); + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx2); + wolfSSL_CTX_free(ctx_c); + ssl = NULL; + } + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Secure Renegotiation (RFC 5746) ------------------------------------- + * ret starts at SECURE_RENEGOTIATION_E and is only touched by + * wolfSSL_UseSecureRenegotiation()'s single allocation; forcing that + * allocation to fail is the only way to observe a third value. + */ +int test_TLSX_SecureRenegotiation_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SECURE_RENEGOTIATION) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && defined(WOLFSSL_TEST_STATIC_BUILD) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[8 + 2 * TLS_FINISHED_SZ]; + word16 extLen; + wolfSSL_Malloc_cb prevM = NULL; + wolfSSL_Free_cb prevF = NULL; + wolfSSL_Realloc_cb prevR = NULL; + + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + + /* ret stays SECURE_RENEGOTIATION_E: secure_renegotiation is already + * set up and enabled (as if a prior renegotiation_info round trip had + * already completed on this connection), so the allocating branch is + * skipped, and so are the "not yet enabled" / verify-data branches + * further down the if/else-if chain -- ret is simply never touched. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte arm[] = { 0x00 }; + /* neither 0 (the "not yet enabled" trigger) nor TLS_FINISHED_SZ. */ + const byte req[] = { 0xFF }; + + ExpectIntEQ(wolfSSL_UseSecureRenegotiation(ssl), WOLFSSL_SUCCESS); + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_RENEGOTIATION_INFO, + arm, (word16)sizeof(arm)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_RENEGOTIATION_INFO, + req, (word16)sizeof(req)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(SECURE_RENEGOTIATION_E)); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* ret == 0: a normal first-time allocation succeeds. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte req[] = { 0x00 }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_RENEGOTIATION_INFO, + req, (word16)sizeof(req)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* Force the one allocation inside TLSX_UseSecureRenegotiation() to + * fail, so ret picks up a third, distinct value. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte req[] = { 0x00 }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_RENEGOTIATION_INFO, + req, (word16)sizeof(req)); + + ExpectIntEQ(wolfSSL_GetAllocators(&prevM, &prevF, &prevR), 0); + ExpectIntEQ(wolfSSL_SetAllocators(tls_parse_fail_malloc, + tls_parse_fail_free, tls_parse_fail_realloc), 0); + tls_parse_alloc_seen = 0; + tls_parse_fail_after = 0; + + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(MEMORY_E)); + + tls_parse_fail_after = -1; + (void)wolfSSL_SetAllocators(prevM, prevF, prevR); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Client side response direction: *input == 2*TLS_FINISHED_SZ, but the + * declared extension length disagrees with it -- length is the + * second, independent half of that check. */ + ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + byte full[1 + 2 * TLS_FINISHED_SZ]; + /* declares the full double-verify-data size, but is one byte + * short of it. */ + byte shortBody[2 * TLS_FINISHED_SZ]; + + XMEMSET(full, 0, sizeof(full)); + full[0] = 2 * TLS_FINISHED_SZ; + XMEMSET(shortBody, 0, sizeof(shortBody)); + shortBody[0] = 2 * TLS_FINISHED_SZ; + + ExpectIntEQ(wolfSSL_UseSecureRenegotiation(ssl), WOLFSSL_SUCCESS); + ssl->secure_renegotiation->enabled = 1; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_RENEGOTIATION_INFO, + shortBody, (word16)sizeof(shortBody)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(SECURE_RENEGOTIATION_E)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_RENEGOTIATION_INFO, + full, (word16)sizeof(full)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Supported Versions (RFC 8446 4.2.1) --------------------------------- */ +int test_TLSX_SupportedVersions_parse(void) +{ + EXPECT_DECLS; + /* TLSX_Parse()'s own per-extension dispatch never reaches + * TLSX_SupportedVersions_Parse(): supported_versions is scanned and + * consumed by TLSX_ParseVersion() in an earlier pass (it must be known + * before any other extension can be interpreted), and the main loop's + * TLSX_SUPPORTED_VERSIONS case only re-validates the message type. + * WOLFSSL_LOCAL: called directly (guarded). */ +#if defined(WOLFSSL_TLS13) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_WOLFSSL_CLIENT) && defined(WOLFSSL_TEST_STATIC_BUILD) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[8 + MAX_SV_EXT_LEN]; + word16 extLen; + int found; + + /* client_hello direction, server side: the three independent ways the + * body can fail the initial length sanity check. */ + ctx = test_tls_parse_server_ctx(wolfTLSv1_3_server_method()); + ExpectNotNull(ctx); + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* length (2) < OPAQUE8_LEN + OPAQUE16_LEN (3): too short. */ + const byte tooShort[] = { 0x02, 0x03 }; + /* even length: fails (length & 1) != 1. list len byte says 2, but + * total length here is even (4). */ + const byte evenLen[] = { 0x02, 0x03, 0x04, 0x00 }; + /* well-formed, single TLS 1.3 entry. */ + const byte good[] = { 0x02, 0x03, TLSv1_3_MINOR }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_VERSIONS, + tooShort, (word16)sizeof(tooShort)); + ExpectIntEQ(TLSX_ParseVersion(ssl, ext, extLen, client_hello, + &found), WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_VERSIONS, + evenLen, (word16)sizeof(evenLen)); + ExpectIntEQ(TLSX_ParseVersion(ssl, ext, extLen, client_hello, + &found), WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_VERSIONS, + good, (word16)sizeof(good)); + ExpectIntEQ(TLSX_ParseVersion(ssl, ext, extLen, client_hello, + &found), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* An over-long list also fails on its own: odd total length (so the + * parity check already passes) but still over MAX_SV_EXT_LEN. */ + ctx = test_tls_parse_server_ctx(wolfTLSv1_3_server_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + byte body[MAX_SV_EXT_LEN + 2]; + word16 i; + + body[0] = (byte)(MAX_SV_EXT_LEN + 1); + for (i = 1; i < sizeof(body); i += 2) { + body[i] = SSLv3_MAJOR; + body[i + 1] = TLSv1_3_MINOR; + } + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_VERSIONS, + body, (word16)sizeof(body)); + ExpectIntEQ(TLSX_ParseVersion(ssl, ext, extLen, client_hello, + &found), WC_NO_ERR_TRACE(BUFFER_ERROR)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* server_hello / hello_retry_request direction, client side. */ + ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte v[] = { SSLv3_MAJOR, TLSv1_3_MINOR }; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_VERSIONS, v, + (word16)sizeof(v)); + ExpectIntEQ(TLSX_ParseVersion(ssl, ext, extLen, server_hello, + &found), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte v[] = { SSLv3_MAJOR, TLSv1_3_MINOR }; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_VERSIONS, v, + (word16)sizeof(v)); + ExpectIntEQ(TLSX_ParseVersion(ssl, ext, extLen, hello_retry_request, + &found), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* Neither client_hello, server_hello nor hello_retry_request. */ + const byte v[] = { SSLv3_MAJOR, TLSv1_3_MINOR }; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_VERSIONS, v, + (word16)sizeof(v)); + ExpectIntEQ(TLSX_ParseVersion(ssl, ext, extLen, finished, &found), + WC_NO_ERR_TRACE(SANITY_MSG_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Downgrade bookkeeping: ssl->options.downgrade set and the connection + * already sitting at TLS 1.2 minor -- vs. either being false. */ + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte v[] = { SSLv3_MAJOR, TLSv1_3_MINOR }; + ssl->options.downgrade = 1; + ssl->version.minor = TLSv1_2_MINOR; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_VERSIONS, v, + (word16)sizeof(v)); + ExpectIntEQ(TLSX_ParseVersion(ssl, ext, extLen, server_hello, + &found), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* downgrade left false and version.minor left at its default + * (TLS 1.3): the "no upgrade allowed" check right after this one + * would otherwise reject a version.minor that was forced down + * without downgrade being set. */ + const byte v[] = { SSLv3_MAJOR, TLSv1_3_MINOR }; + ssl->options.downgrade = 0; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_VERSIONS, v, + (word16)sizeof(v)); + ExpectIntEQ(TLSX_ParseVersion(ssl, ext, extLen, server_hello, + &found), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Signature Algorithms (RFC 8446 4.2.3) ------------------------------- */ +int test_TLSX_SignatureAlgorithms_parse(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(WOLFSSL_NO_SIGALG) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && (!defined(NO_RSA) || defined(HAVE_ECC)) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[16]; + word16 extLen; + + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + /* length (3) < OPAQUE16_LEN*2 (4): too short. */ + const byte tooShort[] = { 0x00, 0x02, 0x04 }; + /* odd overall length: fails (length & 1) != 0. */ + const byte oddLen[] = { 0x00, 0x02, 0x04, 0x03, 0x00 }; + /* well-formed, one algorithm. */ + const byte good[] = { 0x00, 0x02, 0x04, 0x03 }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SIGNATURE_ALGORITHMS, + tooShort, (word16)sizeof(tooShort)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SIGNATURE_ALGORITHMS, + oddLen, (word16)sizeof(oddLen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SIGNATURE_ALGORITHMS, + good, (word16)sizeof(good)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + +#if !defined(NO_RSA) && defined(HAVE_TLS_EXTENSIONS) && defined(WOLFSSL_TLS13) + /* SignatureAlgorithmsCert: same length checks, separate extension. + * Only dispatched to on a TLS 1.3+ connection. */ + ctx = test_tls_parse_server_ctx(wolfTLSv1_3_server_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte tooShort[] = { 0x00, 0x02, 0x04 }; + /* long enough, but an odd total length. */ + const byte oddLen[] = { 0x00, 0x02, 0x04, 0x03, 0x00 }; + const byte good[] = { 0x00, 0x02, 0x04, 0x03 }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_SIGNATURE_ALGORITHMS_CERT, tooShort, + (word16)sizeof(tooShort)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_SIGNATURE_ALGORITHMS_CERT, oddLen, + (word16)sizeof(oddLen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_SIGNATURE_ALGORITHMS_CERT, good, + (word16)sizeof(good)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* TLSX_SignatureAlgorithms_MapPss(): rsa_pss_sa_algo entries whose + * second byte is within [pss_sha256, pss_sha512] vs. just above it, + * both already >= pss_sha256. Reached via the plain SignatureAlgorithms + * extension's TLS 1.3 PSS-with-SHA remap. */ +#ifdef WOLFSSL_TLS13 + ctx = test_tls_parse_server_ctx(wolfTLSv1_3_server_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte inRange[] = { 0x00, 0x02, rsa_pss_sa_algo, pss_sha512 }; + const byte aboveRange[] = { + 0x00, 0x02, rsa_pss_sa_algo, (byte)(pss_sha512 + 1) + }; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SIGNATURE_ALGORITHMS, + inRange, (word16)sizeof(inRange)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SIGNATURE_ALGORITHMS, + aboveRange, (word16)sizeof(aboveRange)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif +#endif +#endif + return EXPECT_RESULT(); +} + +/* ---- Certificate Status Request / v2 (RFC 6066 8, RFC 6961) -------------- */ +int test_TLSX_CSR_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[32]; + word16 extLen; + + /* Server side: not able to staple (no OCSP stapling enabled) skips + * rather than failing. */ + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte req[] = { + WOLFSSL_CSR_OCSP, + 0x00, 0x00, /* responder_id_list, empty */ + 0x00, 0x00 /* request_extensions, empty */ + }; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_STATUS_REQUEST, req, + (word16)sizeof(req)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* Same request, but with OCSP stapling enabled on the ctx: taken. */ + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(ctx), WOLFSSL_SUCCESS); + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte req[] = { + WOLFSSL_CSR_OCSP, + 0x00, 0x00, + 0x00, 0x00 + }; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_STATUS_REQUEST, req, + (word16)sizeof(req)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + +#if defined(WOLFSSL_TLS13) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) + /* Client side, TLS 1.3 certificate direction (RFC 8446 4.4.2): the OCSP + * response TLV's status_type and length bookkeeping. */ + ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte respBadType[] = { + 0x00, /* status_type: not WOLFSSL_CSR_OCSP */ + 0x00, 0x00, 0x00 /* 24-bit response length: 0 */ + }; + + ExpectIntEQ(wolfSSL_UseOCSPStapling(ssl, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + /* This synthetic parse never ran a real handshake, so the + * negotiated-TLS-1.3 flag was never set by version negotiation. */ + ssl->options.tls1_3 = 1; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_STATUS_REQUEST, + respBadType, (word16)sizeof(respBadType)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, certificate, NULL), + WC_NO_ERR_TRACE(BAD_CERTIFICATE_STATUS_ERROR)); + } + wolfSSL_free(ssl); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* Too short even for the status_type byte and the 24-bit response + * length: rejected before the status_type byte is looked at. */ + const byte tooShort[] = { 0x00, 0x00, 0x00 }; + + ExpectIntEQ(wolfSSL_UseOCSPStapling(ssl, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + ssl->options.tls1_3 = 1; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_STATUS_REQUEST, + tooShort, (word16)sizeof(tooShort)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, certificate, NULL), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + } + wolfSSL_free(ssl); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte respGood[] = { + WOLFSSL_CSR_OCSP, + 0x00, 0x00, 0x01, + 0xAA + }; + ExpectIntEQ(wolfSSL_UseOCSPStapling(ssl, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + ssl->options.tls1_3 = 1; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_STATUS_REQUEST, + respGood, (word16)sizeof(respGood)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, certificate, NULL), + 0); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* The response buffer allocation: response_idx is always 0 at this + * point in a synthetic parse (no prior certificate chain was + * processed), so a real handshake is not needed to force a memory + * failure at that one allocation. */ +#ifdef WOLFSSL_TEST_STATIC_BUILD + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const byte respGood[] = { + WOLFSSL_CSR_OCSP, + 0x00, 0x00, 0x01, + 0xAA + }; + wolfSSL_Malloc_cb prevM = NULL; + wolfSSL_Free_cb prevF = NULL; + wolfSSL_Realloc_cb prevR = NULL; + + ExpectIntEQ(wolfSSL_UseOCSPStapling(ssl, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + ssl->options.tls1_3 = 1; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_STATUS_REQUEST, + respGood, (word16)sizeof(respGood)); + + ExpectIntEQ(wolfSSL_GetAllocators(&prevM, &prevF, &prevR), 0); + ExpectIntEQ(wolfSSL_SetAllocators(tls_parse_fail_malloc, + tls_parse_fail_free, tls_parse_fail_realloc), 0); + tls_parse_alloc_seen = 0; + tls_parse_fail_after = 0; + + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, certificate, NULL), + WC_NO_ERR_TRACE(MEMORY_ERROR)); + + tls_parse_fail_after = -1; + (void)wolfSSL_SetAllocators(prevM, prevF, prevR); + } + wolfSSL_free(ssl); + ssl = NULL; +#endif + wolfSSL_CTX_free(ctx); +#endif /* WOLFSSL_TLS13 && HAVE_SSL_MEMIO_TESTS_DEPENDENCIES */ +#endif + +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) && !defined(NO_TLS) && \ + !defined(NO_WOLFSSL_SERVER) + { + WOLFSSL_CTX* ctx2 = test_tls_parse_server_ctx( + wolfTLSv1_2_server_method()); + WOLFSSL* ssl2 = NULL; + byte ext2[16]; + word16 ext2Len; + /* One status_request_v2 entry: type OCSP, empty responder_id_list + * and empty request_extensions. */ + const byte req[] = { + 0x00, 0x07, /* overall list length */ + WOLFSSL_CSR2_OCSP, + 0x00, 0x04, /* this entry's length */ + 0x00, 0x00, /* responder_id_list, empty */ + 0x00, 0x00 /* request_extensions, empty */ + }; + + ExpectNotNull(ctx2); + + /* No OCSP stapling enabled: this entry is skipped (continue). */ + if (ctx2 != NULL) + ExpectNotNull(ssl2 = wolfSSL_new(ctx2)); + if (ssl2 != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl2); + ext2Len = test_tls_parse_build_ext(ext2, sizeof(ext2), TLSXT_STATUS_REQUEST_V2, + req, (word16)sizeof(req)); + ExpectIntEQ(TLSX_Parse(ssl2, ext2, ext2Len, client_hello, + suites), 0); + } + wolfSSL_free(ssl2); + ssl2 = NULL; + + /* OCSP stapling enabled: the entry is used. */ + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(ctx2), WOLFSSL_SUCCESS); + if (ctx2 != NULL) + ExpectNotNull(ssl2 = wolfSSL_new(ctx2)); + if (ssl2 != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl2); + ext2Len = test_tls_parse_build_ext(ext2, sizeof(ext2), TLSXT_STATUS_REQUEST_V2, + req, (word16)sizeof(req)); + ExpectIntEQ(TLSX_Parse(ssl2, ext2, ext2Len, client_hello, + suites), 0); + } + wolfSSL_free(ssl2); + wolfSSL_CTX_free(ctx2); + } +#endif + return EXPECT_RESULT(); +} + +/* ---- EC Point Formats (RFC 8422 5.1.2) ----------------------------------- */ +int test_TLSX_PointFormat_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SUPPORTED_CURVES) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && (!defined(NO_RSA) || defined(HAVE_ECC)) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[16]; + word16 extLen; + + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + /* list length byte (2) disagrees with the actual body length (2 + * total, 1 format byte). */ + const byte badLen[] = { 0x02, WOLFSSL_EC_PF_UNCOMPRESSED }; + /* well formed: one format, uncompressed. */ + const byte good[] = { 0x01, WOLFSSL_EC_PF_UNCOMPRESSED }; + + /* length (0) < ENUM_LEN (1). */ + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_EC_POINT_FORMATS, NULL, + 0); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_EC_POINT_FORMATS, + badLen, (word16)sizeof(badLen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_EC_POINT_FORMATS, + good, (word16)sizeof(good)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + +#if defined(WOLFSSL_TLS13) && defined(HAVE_SUPPORTED_CURVES) && \ + !defined(WOLFSSL_NO_SERVER_GROUPS_EXT) && !defined(NO_TLS) && \ + !defined(NO_WOLFSSL_SERVER) && defined(WOLFSSL_TEST_STATIC_BUILD) + /* TLSX_SupportedCurve_Preferred(): checkSupported gating and the + * TLSX_IsGroupSupported() result, driven by a raw (unfiltered) offered + * groups list -- TLSX_SupportedCurve_Parse() records whatever the peer + * sent, support is only checked later by this function. A TLS 1.2 + * server ssl is used so RFC 8446 9.2's "KeyShare requires + * SupportedGroups and vice-versa" check does not apply; the function + * under test does not itself depend on the negotiated version. */ + { + WOLFSSL_CTX* ctxp = test_tls_parse_server_ctx( + wolfTLSv1_2_server_method()); + WOLFSSL* sslp = NULL; + + ExpectNotNull(ctxp); + if (ctxp != NULL) + ExpectNotNull(sslp = wolfSSL_new(ctxp)); +#if !defined(NO_DH) && !defined(WOLFSSL_NO_TLS12) + if (sslp != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(sslp); + /* An FFDHE codepoint this build does not have enabled (only + * WOLFSSL_FFDHE_2048 is): recorded with its real group id + * per RFC 7919 Section 4, but TLSX_IsGroupSupported() is false + * for it -- unlike a non-FFDHE unrecognised id, which is + * recorded as an empty (data == NULL) restriction instead. */ + const byte groups[] = { 0x00, 0x02, 0x01, 0x01 }; /* FFDHE 3072 */ + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(sslp, ext, extLen, client_hello, suites), + 0); + + ExpectIntEQ(TLSX_SupportedCurve_Preferred(sslp, 0), + WOLFSSL_FFDHE_3072); +#ifdef HAVE_FFDHE_3072 + /* The group is in the supported set here, so the first entry is + * returned rather than skipped. */ + ExpectIntEQ(TLSX_SupportedCurve_Preferred(sslp, 1), + WOLFSSL_FFDHE_3072); +#else + ExpectIntEQ(TLSX_SupportedCurve_Preferred(sslp, 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + } +#endif + wolfSSL_free(sslp); + sslp = NULL; + if (ctxp != NULL) + ExpectNotNull(sslp = wolfSSL_new(ctxp)); + if (sslp != NULL) { +#ifdef HAVE_CURVE25519 + Suites* suites = (Suites*)WOLFSSL_SUITES(sslp); + const byte groups[] = { 0x00, 0x02, 0x00, 0x1D }; /* X25519 */ + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(sslp, ext, extLen, client_hello, suites), + 0); + ExpectIntEQ(TLSX_SupportedCurve_Preferred(sslp, 1), + WOLFSSL_ECC_X25519); +#endif + } + wolfSSL_free(sslp); + wolfSSL_CTX_free(ctxp); + } +#endif + +#if defined(HAVE_SUPPORTED_CURVES) && !defined(NO_TLS) && \ + !defined(NO_WOLFSSL_SERVER) && defined(WOLFSSL_TEST_STATIC_BUILD) + /* TLSX_PointFormat_ValidateResponse(): reached while sizing/writing a + * ServerHello, through the WOLFSSL_LOCAL TLSX_GetResponseSize() rather + * than TLSX_Parse() (there is no wire input on this side). A cipher + * suite whose first byte is ECDHE_PSK_BYTE takes the same "already + * covered by the peer's key exchange, do not also send point formats" + * path as ECC_BYTE/CHACHA_BYTE. */ + { + WOLFSSL_CTX* ctxr = test_tls_parse_server_ctx( + wolfTLSv1_2_server_method()); + WOLFSSL* sslr = NULL; + + ExpectNotNull(ctxr); + if (ctxr != NULL) + ExpectNotNull(sslr = wolfSSL_new(ctxr)); + if (sslr != NULL) { + word16 length = 0; + + ExpectIntEQ(TLSX_UsePointFormat(&sslr->extensions, + WOLFSSL_EC_PF_UNCOMPRESSED, sslr->heap), + WOLFSSL_SUCCESS); + { + TLSX* pf = TLSX_Find(sslr->extensions, TLSX_EC_POINT_FORMATS); + ExpectNotNull(pf); + if (pf != NULL) + pf->resp = 1; + } + sslr->options.cipherSuite0 = ECDHE_PSK_BYTE; + ExpectIntEQ(TLSX_GetResponseSize(sslr, server_hello, &length), + 0); + /* Early return leaves the suppression semaphore untouched, so + * the extension is still included (unlike a cipher suite that + * falls all the way through to the TURN_ON() at the end). */ + ExpectIntGT(length, 0); + } + wolfSSL_free(sslr); + wolfSSL_CTX_free(ctxr); + } +#endif + return EXPECT_RESULT(); +} + +/* ---- Server Name Indication (RFC 6066 Section 3) ------------------------- */ +int test_TLSX_SNI_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SNI) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && (!defined(NO_RSA) || defined(HAVE_ECC)) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[64]; + word16 extLen; + + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + + /* server_name_list length disagreements: total-length mismatch and + * zero-length list, then a well-formed one-entry list. SNI must be + * configured on the server for the match logic below it to run. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const char* host = "example.com"; + /* list length (5) does not match the 6 bytes that actually follow + * it. */ + const byte lenMismatch[] = { + 0x00, 0x05, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA + }; + /* [server_name_list length(2)][name_type(1)][name length(2)][name] */ + byte good[2 + 1 + 2 + 11]; + word16 off = 2; + + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, host, + (word16)XSTRLEN(host)), WOLFSSL_SUCCESS); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SERVER_NAME, + lenMismatch, (word16)sizeof(lenMismatch)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + good[off++] = WOLFSSL_SNI_HOST_NAME; + good[off++] = 0x00; good[off++] = 11; + XMEMCPY(good + off, host, 11); + off += 11; + good[0] = 0x00; good[1] = (byte)(off - 2); /* list length */ + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SERVER_NAME, good, off); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* Host-name length disagreements within an otherwise valid list: the + * declared name length runs past the extension body, and a + * zero-length name. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const char* host = "example.com"; + byte truncated[2 + 1 + 2 + 11]; + byte zeroName[2 + 1 + 2]; + word16 off; + + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, host, + (word16)XSTRLEN(host)), WOLFSSL_SUCCESS); + + off = 2; + truncated[off++] = WOLFSSL_SNI_HOST_NAME; + /* claim a name length of 12 (one more than provided). */ + truncated[off++] = 0x00; truncated[off++] = 12; + XMEMCPY(truncated + off, host, 11); + off += 11; + truncated[0] = 0x00; truncated[1] = (byte)(off - 2); + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SERVER_NAME, truncated, + off); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + off = 2; + zeroName[off++] = WOLFSSL_SNI_HOST_NAME; + zeroName[off++] = 0x00; zeroName[off++] = 0x00; + zeroName[0] = 0x00; zeroName[1] = (byte)(off - 2); + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SERVER_NAME, zeroName, + off); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Client side response direction: SNI configured (extension and its + * data both present) accepts the empty echo; a non-empty echo is + * rejected. */ + ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + const char* host = "example.com"; + byte extra = 0; + + ExpectIntEQ(wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, host, + (word16)XSTRLEN(host)), WOLFSSL_SUCCESS); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SERVER_NAME, NULL, 0); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), 0); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SERVER_NAME, &extra, 1); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Client side, SNI not requested at all: response is an unsupported + * extension. */ + ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method()); + ExpectNotNull(ctx); + ssl = NULL; + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SERVER_NAME, NULL, 0); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- TLSX_ValidateSupportedCurves() ------------------------------------- + * Cipher-suite/curve compatibility check used while building a ServerHello. + * WOLFSSL_LOCAL: called directly (guarded), with a supported_groups list + * built by parsing a raw ClientHello extension body -- that path records + * whatever the peer sent without filtering for local support, which is + * what lets a specific curve be selected precisely for this test. + */ +int test_TLSX_ValidateSupportedCurves(void) +{ + EXPECT_DECLS; +#if (defined(HAVE_ECC) || defined(HAVE_CURVE25519) || defined(HAVE_CURVE448)) && defined(HAVE_SUPPORTED_CURVES) && !defined(NO_TLS) && !defined(NO_WOLFSSL_SERVER) && defined(WOLFSSL_TEST_STATIC_BUILD) && defined(HAVE_CURVE25519) && defined(HAVE_CURVE448) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + word32 oid; + byte ext[16]; + word16 extLen; + + ctx = test_tls_parse_server_ctx(wolfTLSv1_2_server_method()); + ExpectNotNull(ctx); + + /* first != {ECC_BYTE, ECDHE_PSK_BYTE, CHACHA_BYTE}: no restriction, + * independent of any configured groups. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, 0x00, 0x00, &oid), 1); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* first == ECDHE_PSK_BYTE: takes the same restriction-lookup branch as + * ECC_BYTE/CHACHA_BYTE. No supported_groups configured: still no + * restriction, but by way of the extension == NULL check instead of + * skipping the lookup outright. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, ECDHE_PSK_BYTE, 0x00, + &oid), 1); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* first == ECC_BYTE, with a supported_groups list offering X25519 + * then X448: exercises the X25519/X448 default-case defOid reset + * (second entry is not the one that set defOid). */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte groups[] = { + 0x00, 0x04, + 0x00, 0x1D, /* X25519 */ + 0x00, 0x1E /* X448 */ + }; + ssl->eccTempKeySz = 0; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + /* second == some value not matched by any explicit ECC_BYTE case + * (no ECDHE_ECDSA/ECDHE_RSA cipher id): falls to "default". */ + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, ECC_BYTE, 0xFF, &oid), + 1); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* Single-entry X25519 list: defOid gets set to X25519 by the first + * curve seen, and the default-case reset then fires for that very + * entry (oid == defOid). */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte groups[] = { 0x00, 0x02, 0x00, 0x1D }; + ssl->eccTempKeySz = 0; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, ECC_BYTE, 0xFF, &oid), + 1); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* eccTempKeySz larger than any real curve size: the "set default", + * "current" and "next" bookkeeping (all gated on eccTempKeySz <= + * octets, or == for current) never fires for any offered curve, so + * ecdhCurveOID is never resolved away from 0 -- the ephemeral-suite + * rejection at the very end is reached with *ecdhCurveOID still 0 + * (index 0), but a "default" (non-ECDHE_ECDSA/RSA) second byte never + * sets ephmSuite (index 1 false). */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte groups[] = { 0x00, 0x02, 0x00, 0x17 }; /* secp256r1 */ + ssl->eccTempKeySz = 100; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, ECC_BYTE, 0xFF, &oid), + 1); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* Same eccTempKeySz, but second is a real ECDHE_ECDSA suite id: + * ephmSuite is now set (index 1 true) while *ecdhCurveOID is still 0 + * (index 0 true, same as above) -- the suite is rejected outright. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte groups[] = { 0x00, 0x02, 0x00, 0x17 }; + ssl->eccTempKeySz = 100; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, ECC_BYTE, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, &oid), 0); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* eccTempKeySz back to the default (0) with the same real ECDHE_ECDSA + * suite id: *ecdhCurveOID does resolve away from 0 this time (via the + * "next highest strength" fallback), so the rejection at the end is + * never reached (index 0 false, index 1 held true as above). */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte groups[] = { 0x00, 0x02, 0x00, 0x17 }; + ssl->eccTempKeySz = 0; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, ECC_BYTE, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, &oid), 1); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* X25519 alone, eccTempKeySz larger than its octets: the "set default" + * assignment at the top of the loop body does not fire for this entry + * (same eccTempKeySz <= octets gate as the 5963 case above), so defOid + * is still 0 -- not equal to this entry's own (non-zero) oid -- when + * the default-case reset check runs. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte groups[] = { 0x00, 0x02, 0x00, 0x1D }; /* X25519 */ + ssl->eccTempKeySz = 100; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, ECC_BYTE, 0xFF, &oid), + 1); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* X448 alone, same oversized eccTempKeySz shape: the X448 counterpart + * of the X25519 case just above. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte groups[] = { 0x00, 0x02, 0x00, 0x1E }; /* X448 */ + ssl->eccTempKeySz = 100; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, ECC_BYTE, 0xFF, &oid), + 1); + } + wolfSSL_free(ssl); + ssl = NULL; + + /* X448 alone: defOid gets set to X448 by the first (and only) curve + * seen, and the default-case reset then fires for that very entry + * (oid == defOid) -- the X448 counterpart of the X25519-alone case + * above. */ + if (ctx != NULL) + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + const byte groups[] = { 0x00, 0x02, 0x00, 0x1E }; /* X448 */ + ssl->eccTempKeySz = 0; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), TLSXT_SUPPORTED_GROUPS, + groups, (word16)sizeof(groups)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(TLSX_ValidateSupportedCurves(ssl, ECC_BYTE, 0xFF, &oid), + 1); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Supported Groups (RFC 8422 Section 5.1.1 / RFC 8446 4.2.7) --------- */ +/* TLSX_SupportedCurve_Parse() is dispatched to only for client_hello (and, + * on TLS 1.3, encrypted_extensions) through TLSX_Parse(); the server_hello + * direction it itself validates is never reached that way, so it is called + * directly here (WOLFSSL_LOCAL). */ +int test_TLSX_SupportedGroups_parse(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SUPPORTED_CURVES) && defined(WOLFSSL_TLS13) && !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && defined(WOLFSSL_TEST_STATIC_BUILD) && \ + defined(HAVE_TLS_EXTENSIONS) && \ + !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + /* secp256r1: a real, locally supported curve. */ + const byte goodBody[] = { 0x00, 0x02, 0x00, 0x17 }; + + /* server_hello direction, pre-TLS-1.3: rejected before the body is + * looked at (this build does not define WOLFSSL_ALLOW_SERVER_SC_EXT). */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* extensions = NULL; + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, goodBody, + (word16)sizeof(goodBody), 0, &extensions), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + ExpectNull(extensions); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* server_hello direction, TLS 1.3: the version half of the guard no + * longer applies, so parsing proceeds. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* extensions = NULL; + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, goodBody, + (word16)sizeof(goodBody), 0, &extensions), 0); + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + + /* A single FFDHE-range group id this build has no key size table + * for (only WOLFSSL_FFDHE_2048 is compiled in): tolerated as + * BAD_FUNC_ARG from TLSX_UseSupportedCurve(), and on the response + * direction (isRequest == 0) the RFC 7919 restriction bookkeeping + * further down is skipped entirely -- it exists only for a server + * recording what a client offered. */ + { + const byte ffdheUnknown[] = { 0x00, 0x02, 0x01, 0x05 }; + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, ffdheUnknown, + (word16)sizeof(ffdheUnknown), 0, &extensions), 0); + ExpectNull(extensions); + } + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* client_hello direction is never checked against the version; a TLS + * 1.2 connection reaches the same body parsing as above. Also the + * first, "accept whatever the peer wants" shape (no local restriction + * configured): a single well-known, supported curve is recorded. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* extensions = NULL; + + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, goodBody, + (word16)sizeof(goodBody), 1, &extensions), 0); + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + + /* Odd total length fails the OPAQUE16_LEN modulus check (the + * length-too-short half is exercised elsewhere already). */ + { + const byte oddLen[] = { 0x00, 0x01, 0x17 }; + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, oddLen, + (word16)sizeof(oddLen), 1, &extensions), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + ExpectNull(extensions); + } + + /* Same unknown FFDHE id, as a client_hello (isRequest == 1): + * recorded as an explicit (non-empty) restriction so DHE suite + * selection still sees the offer. */ + { + const byte ffdheUnknown[] = { 0x00, 0x02, 0x01, 0x05 }; + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, ffdheUnknown, + (word16)sizeof(ffdheUnknown), 1, &extensions), 0); + ExpectNotNull(extensions); + } + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + + /* A single group id above the FFDHE range entirely: also + * BAD_FUNC_ARG, but not FFDHE, so no restriction is recorded for + * it specifically -- yet since it is the only (unsupported) group + * offered, the list is still empty afterwards and the "record an + * empty restriction" fallback fires. */ + { + const byte aboveFfdhe[] = { 0x00, 0x02, 0x02, 0x58 }; + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, aboveFfdhe, + (word16)sizeof(aboveFfdhe), 1, &extensions), 0); + ExpectNotNull(extensions); + } + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + +#ifdef WOLFSSL_TEST_STATIC_BUILD + /* First entry is a real, supported curve, but its allocation is + * forced to fail: a distinct error (neither WOLFSSL_SUCCESS nor + * BAD_FUNC_ARG) that aborts the scan immediately. */ + { + wolfSSL_Malloc_cb prevM = NULL; + wolfSSL_Free_cb prevF = NULL; + wolfSSL_Realloc_cb prevR = NULL; + + ExpectIntEQ(wolfSSL_GetAllocators(&prevM, &prevF, &prevR), 0); + ExpectIntEQ(wolfSSL_SetAllocators(tls_parse_fail_malloc, + tls_parse_fail_free, tls_parse_fail_realloc), 0); + tls_parse_alloc_seen = 0; + tls_parse_fail_after = 0; + + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, goodBody, + (word16)sizeof(goodBody), 1, &extensions), + WC_NO_ERR_TRACE(MEMORY_E)); + + tls_parse_fail_after = -1; + (void)wolfSSL_SetAllocators(prevM, prevF, prevR); + ExpectNull(extensions); + } +#endif + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Second shape: a local restriction is already configured (as if from + * wolfSSL_CTX_set1_groups_list()), so parsing intersects the peer's + * list against it instead of accepting it outright. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* extensions = NULL; + const byte noMatch261[] = { 0x00, 0x02, 0x01, 0x05 }; + const byte noMatch100[] = { 0x00, 0x02, 0x00, 0x64 }; + const byte noMatch600[] = { 0x00, 0x02, 0x02, 0x58 }; + const byte noMatch256[] = { 0x00, 0x02, 0x01, 0x00 }; + + /* client_hello, offered group in the FFDHE range but not one this + * build knows: no match against the local restriction, but the + * restriction bookkeeping records it anyway. */ + ExpectIntEQ(TLSX_UseSupportedCurve(&extensions, + WOLFSSL_ECC_SECP256R1, ssl->heap, ssl->options.side), + WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, noMatch261, + (word16)sizeof(noMatch261), 1, &extensions), 0); + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + + /* client_hello, offered group below the FFDHE range: not FFDHE, + * no match, nothing recorded for it -- no common curve, and this + * connection is not TLS 1.3, so that is a hard error. */ + ExpectIntEQ(TLSX_UseSupportedCurve(&extensions, + WOLFSSL_ECC_SECP256R1, ssl->heap, ssl->options.side), + WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, noMatch100, + (word16)sizeof(noMatch100), 1, &extensions), + WC_NO_ERR_TRACE(ECC_CURVE_ERROR)); + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + + /* client_hello, offered group above the FFDHE range: same as + * below-range, not FFDHE. */ + ExpectIntEQ(TLSX_UseSupportedCurve(&extensions, + WOLFSSL_ECC_SECP256R1, ssl->heap, ssl->options.side), + WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, noMatch600, + (word16)sizeof(noMatch600), 1, &extensions), + WC_NO_ERR_TRACE(ECC_CURVE_ERROR)); + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + + /* client_hello, offered group is WOLFSSL_FFDHE_2048 (0x0100): in + * range, but this build does support it, so the "unsupported + * FFDHE codepoint" restriction bookkeeping does not apply to it + * either -- it just is not in the local restriction's curve list. */ + ExpectIntEQ(TLSX_UseSupportedCurve(&extensions, + WOLFSSL_ECC_SECP256R1, ssl->heap, ssl->options.side), + WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, noMatch256, + (word16)sizeof(noMatch256), 1, &extensions), + WC_NO_ERR_TRACE(ECC_CURVE_ERROR)); + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + +#ifdef WOLFSSL_TEST_STATIC_BUILD + /* Offered group matches the local restriction, but recording it + * in the intersection list is forced to fail: a non-zero ret + * reaches the "no common curve" check below without commonCurves + * ever becoming non-NULL through the normal path. */ + { + wolfSSL_Malloc_cb prevM = NULL; + wolfSSL_Free_cb prevF = NULL; + wolfSSL_Realloc_cb prevR = NULL; + + ExpectIntEQ(TLSX_UseSupportedCurve(&extensions, + WOLFSSL_ECC_SECP256R1, ssl->heap, + ssl->options.side), WOLFSSL_SUCCESS); + + ExpectIntEQ(wolfSSL_GetAllocators(&prevM, &prevF, &prevR), 0); + ExpectIntEQ(wolfSSL_SetAllocators(tls_parse_fail_malloc, + tls_parse_fail_free, tls_parse_fail_realloc), 0); + tls_parse_alloc_seen = 0; + tls_parse_fail_after = 0; + + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, goodBody, + (word16)sizeof(goodBody), 1, &extensions), + WC_NO_ERR_TRACE(MEMORY_E)); + + tls_parse_fail_after = -1; + (void)wolfSSL_SetAllocators(prevM, prevF, prevR); + TLSX_FreeAll(extensions, NULL); + } +#endif + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Same restriction shape, response direction: needs a TLS 1.3 + * connection to get past the version guard tested first (a TLS 1.2 + * connection would be rejected before the body is even looked at). + * The FFDHE restriction bookkeeping is for a server reading a + * ClientHello only, so it is skipped regardless of the offered + * group. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* extensions = NULL; + const byte noMatch261[] = { 0x00, 0x02, 0x01, 0x05 }; + + ExpectIntEQ(TLSX_UseSupportedCurve(&extensions, + WOLFSSL_ECC_SECP256R1, ssl->heap, ssl->options.side), + WOLFSSL_SUCCESS); + ExpectIntEQ(TLSX_SupportedCurve_Parse(ssl, noMatch261, + (word16)sizeof(noMatch261), 0, &extensions), 0); + TLSX_FreeAll(extensions, NULL); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Key Share negotiation (RFC 8446 4.2.8) ------------------------------ + * TLSX_KeyShare_Choose(), TLSX_KeyShare_Setup() and + * TLSX_KeyShare_Parse_ClientHello() are WOLFSSL_LOCAL: called directly here + * (guarded), with a client list built by hand for the shapes that would be + * awkward to reach through a real handshake. + */ +int test_TLSX_KeyShare_negotiate(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_SUPPORTED_CURVES) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_WOLFSSL_CLIENT) && defined(WOLFSSL_TEST_STATIC_BUILD) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + /* TLSX_KeyShare_Choose(): ssl == NULL / wrong side argument guard. */ + { + KeyShareEntry* kse = NULL; + byte searched = 0; + + ExpectIntEQ(TLSX_KeyShare_Choose(NULL, NULL, 0, 0, &kse, &searched), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* kse = NULL; + byte searched = 0; + /* Right type, wrong side: a client-side ssl. */ + ExpectIntEQ(TLSX_KeyShare_Choose(ssl, ssl->extensions, 0, 0, &kse, + &searched), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + ExpectNotNull(ctx = test_tls_parse_server_ctx(wolfTLSv1_3_server_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* kse = NULL; + byte searched = 0; + /* Both false: right side, no KeyShare extension at all -- an empty + * candidate list, not an error. */ + ExpectIntEQ(TLSX_KeyShare_Choose(ssl, ssl->extensions, 0, 0, &kse, + &searched), 0); + ExpectIntEQ(searched, 1); + ExpectNull(kse); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* TLSX_KeyShare_Choose(): extension->resp == 1 means a server key share + * was already chosen (e.g. after a HelloRetryRequest) -- outside of + * async key generation this is state that should not recur. */ + ExpectNotNull(ctx = test_tls_parse_server_ctx(wolfTLSv1_3_server_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* extension; + KeyShareEntry* kse = NULL; + byte searched = 0; + + ExpectIntEQ(TLSX_Push(&ssl->extensions, TLSX_KEY_SHARE, NULL, + ssl->heap), 0); + extension = TLSX_Find(ssl->extensions, TLSX_KEY_SHARE); + ExpectNotNull(extension); + if (extension != NULL) + extension->resp = 1; + ExpectIntEQ(TLSX_KeyShare_Choose(ssl, ssl->extensions, 0, 0, &kse, + &searched), WC_NO_ERR_TRACE(INCOMPLETE_DATA)); + + /* Same extension, resp == 0: falls through to the normal search + * instead (an empty list here too, since data is NULL). */ + if (extension != NULL) + extension->resp = 0; + searched = 0; + ExpectIntEQ(TLSX_KeyShare_Choose(ssl, ssl->extensions, 0, 0, &kse, + &searched), 0); + ExpectIntEQ(searched, 1); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + +#ifdef WOLFSSL_HAVE_MLKEM + /* TLSX_KeyShare_Choose(): a client-offered group above WOLFSSL_ECC_MAX + * is only kept as a candidate when it is a recognised PQC or PQC + * hybrid group id -- anything else in that numeric space is skipped. + * All three group ids below are recorded as a matching + * supported_groups entry directly (bypassing the "is this build + * capable of it" gate TLSX_UseSupportedCurve() would apply), exactly + * as TLSX_SupportedCurve_Parse() would record whatever id a peer + * offered. */ + { + static const word16 groupIds[] = { + WOLFSSL_ML_KEM_512, /* pure PQC: !IS_PQC is false */ + WOLFSSL_SECP256R1MLKEM768,/* hybrid: !IS_PQC true, !IS_HYBRID false */ + 0xBEEF /* neither: both operands true */ + }; + size_t i; + + for (i = 0; i < sizeof(groupIds) / sizeof(groupIds[0]); i++) { + KeyShareEntry* kse = NULL; + byte searched = 0; + + ExpectNotNull(ctx = test_tls_parse_server_ctx( + wolfTLSv1_3_server_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(test_tls_parse_push_curve(&ssl->extensions, ssl, + groupIds[i]), 0); + ExpectNotNull(test_tls_parse_push_kse(&ssl->extensions, ssl, + groupIds[i])); + ExpectIntEQ(TLSX_KeyShare_Choose(ssl, ssl->extensions, 0, 0, + &kse, &searched), 0); + ExpectIntEQ(searched, 1); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + } + } +#endif /* WOLFSSL_HAVE_MLKEM */ + + /* TLSX_KeyShare_Setup(): the same ssl == NULL / wrong side guard as + * Choose(), on a different public entry point. */ + ExpectIntEQ(TLSX_KeyShare_Setup(NULL, NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(TLSX_KeyShare_Setup(ssl, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + ExpectNotNull(ctx = test_tls_parse_server_ctx(wolfTLSv1_3_server_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + /* Right side, right type: falls through to the "no KeyShare + * extension yet" state check instead of the argument guard. */ + ExpectIntEQ(TLSX_KeyShare_Setup(ssl, NULL), + WC_NO_ERR_TRACE(BAD_STATE_E)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* TLSX_KeyShare_Parse_ClientHello(): the list-length prefix and the + * MAX_EXT_DATA_LEN bound. Both are checked against the 'length' + * argument before any byte past the 2-byte prefix is read, so an + * over-large 'length' is exercised without actually allocating an + * extension body anywhere near that size. */ + ExpectNotNull(ctx = test_tls_parse_server_ctx(wolfTLSv1_3_server_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + TLSX* extensions = NULL; + /* declares a 5-entry list length but length says only 10 bytes of + * body follow (short by OPAQUE16_LEN). */ + const byte lenMismatch[] = { 0x00, 0x05 }; + /* declared length matches, but MAX_EXT_DATA_LEN - HELLO_EXT_SZ is + * exceeded; only the first 2 bytes are ever read. */ + const byte overLarge[] = { 0xFF, 0xFD }; + /* an empty list: both checks pass. */ + const byte empty[] = { 0x00, 0x00 }; + + ExpectIntEQ(TLSX_KeyShare_Parse_ClientHello(ssl, lenMismatch, 10, + &extensions), WC_NO_ERR_TRACE(BUFFER_ERROR)); + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + + ExpectIntEQ(TLSX_KeyShare_Parse_ClientHello(ssl, overLarge, 65535, + &extensions), WC_NO_ERR_TRACE(BUFFER_ERROR)); + TLSX_FreeAll(extensions, NULL); + extensions = NULL; + + ExpectIntEQ(TLSX_KeyShare_Parse_ClientHello(ssl, empty, + (word16)sizeof(empty), &extensions), 0); + TLSX_FreeAll(extensions, NULL); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* TLSX_KeyShare_Parse(), server_hello direction: "not in the list sent + * if there isn't a private key". A group that was offered (both + * supported_groups and key_share list it) is guaranteed a non-NULL + * KeyShareEntry by TLSX_KeyShareEntry_Parse()'s own postcondition (it + * only returns a length equal to the input's when it also produced an + * entry), so keyShareEntry itself is never NULL here; only the + * key/privKey half is under test. */ +#if !defined(NO_DH) && defined(HAVE_FFDHE_2048) + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + byte body[2 + 2 + 256]; + word16 off = 0; + + body[off++] = (byte)(WOLFSSL_FFDHE_2048 >> 8); + body[off++] = (byte)(WOLFSSL_FFDHE_2048 & 0xFF); + body[off++] = 0x01; body[off++] = 0x00; /* keLen == 256 */ + XMEMSET(body + off, 0, 256); /* 0: never a valid DH public value */ + off += 256; + + ExpectIntEQ(test_tls_parse_push_curve(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048), 0); + /* client's own offer: no key generated for it (as if the server + * chose a group the client never actually built a key for -- not + * how a real client behaves, but the field state under test). */ + ExpectNotNull(test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048)); + + ExpectIntEQ(TLSX_KeyShare_Parse(ssl, body, off, server_hello), + WC_NO_ERR_TRACE(BAD_KEY_SHARE_DATA)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Same shape, but the client's entry already has a (fully initialised, + * so freeing it later is safe) DH key object -- key != NULL alone is + * enough for the gate to pass, independent of privKey. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + byte body[2 + 2 + 256]; + word16 off = 0; + KeyShareEntry* kse; + DhKey* dhKey = NULL; + + body[off++] = (byte)(WOLFSSL_FFDHE_2048 >> 8); + body[off++] = (byte)(WOLFSSL_FFDHE_2048 & 0xFF); + body[off++] = 0x01; body[off++] = 0x00; + XMEMSET(body + off, 0, 256); + off += 256; + + ExpectIntEQ(test_tls_parse_push_curve(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048), 0); + kse = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048); + ExpectNotNull(kse); + ExpectNotNull(dhKey = (DhKey*)XMALLOC(sizeof(DhKey), ssl->heap, + DYNAMIC_TYPE_DH)); + if (dhKey != NULL) + ExpectIntEQ(wc_InitDhKey_ex(dhKey, ssl->heap, INVALID_DEVID), 0); + if (kse != NULL) + kse->key = dhKey; + + /* key != NULL, privKey == NULL: gate passes; the derivation itself + * then rejects the all-zero peer public value (never a valid DH + * public key) before privKey's absence would even matter. */ + ExpectIntEQ(TLSX_KeyShare_Parse(ssl, body, off, server_hello), + WC_NO_ERR_TRACE(PEER_KEY_ERROR)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Same shape, but the client's entry already has a private key (as a + * real one would by the time a ServerHello arrives): the gate passes + * and parsing proceeds to deriving the secret, which then fails on + * the all-zero peer value above instead. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + byte body[2 + 2 + 256]; + word16 off = 0; + KeyShareEntry* kse; + + body[off++] = (byte)(WOLFSSL_FFDHE_2048 >> 8); + body[off++] = (byte)(WOLFSSL_FFDHE_2048 & 0xFF); + body[off++] = 0x01; body[off++] = 0x00; + XMEMSET(body + off, 0, 256); + off += 256; + + ExpectIntEQ(test_tls_parse_push_curve(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048), 0); + kse = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048); + ExpectNotNull(kse); + if (kse != NULL) { + kse->privKey = (byte*)XMALLOC(1, ssl->heap, + DYNAMIC_TYPE_PRIVATE_KEY); + ExpectNotNull(kse->privKey); + if (kse->privKey != NULL) { + kse->privKey[0] = 0x01; + kse->privKeyLen = 1; + kse->keyLen = 1; + } + } + + ExpectIntEQ(TLSX_KeyShare_Parse(ssl, body, off, server_hello), + WC_NO_ERR_TRACE(PEER_KEY_ERROR)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif /* !NO_DH && HAVE_FFDHE_2048 */ +#endif + return EXPECT_RESULT(); +} + +/* ---- Key Share key generation (RFC 8446 4.2.8) -------------------------- + * TLSX_KeyShare_GenKey() dispatches by group to the per-algorithm Gen*Key() + * helpers, all WOLFSSL_LOCAL, called directly on a hand-built KeyShareEntry + * (a real handshake would need a full ClientHello round trip to reach the + * same pubKey/privKey states). Entries are heap-allocated and released via + * test_tls_parse_free_kse(), matching what TLSX_KeyShare_FreeAll() (not + * itself visible here) expects to own. + */ +int test_TLSX_KeyShare_gen(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_TEST_STATIC_BUILD) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + +#if !defined(NO_DH) && defined(HAVE_FFDHE_2048) + /* TLSX_KeyShare_GenDhKey(): "no key material yet" is true when either + * buffer is missing. A generation from a completely fresh entry hits + * both; pre-seeding one buffer while leaving the other NULL isolates + * each half. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* kse = NULL; + + /* Fresh: pubKey == NULL && privKey == NULL. */ + ExpectNotNull(kse = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (kse != NULL) { + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = WOLFSSL_FFDHE_2048; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), 0); + ExpectNotNull(kse->pubKey); + ExpectNotNull(kse->privKey); + + /* Same entry, called again: both buffers already present, the + * whole generation block (and the two allocation guards + * inside it) is skipped entirely. */ + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), 0); + + test_tls_parse_free_kse(ssl, kse); + } + + /* pubKey missing, privKey pre-seeded: the outer guard is true from + * pubKey alone; the privKey allocation guard is then false. */ + ExpectNotNull(kse = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (kse != NULL) { + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = WOLFSSL_FFDHE_2048; + kse->privKey = (byte*)XMALLOC(128, ssl->heap, + DYNAMIC_TYPE_PRIVATE_KEY); + ExpectNotNull(kse->privKey); + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), 0); + ExpectNotNull(kse->pubKey); + test_tls_parse_free_kse(ssl, kse); + } + + /* privKey missing, pubKey pre-seeded (sized for the FFDHE 2048 + * prime): the outer guard is true from privKey alone; the pubKey + * allocation guard is then false. */ + ExpectNotNull(kse = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (kse != NULL) { + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = WOLFSSL_FFDHE_2048; + kse->pubKey = (byte*)XMALLOC(256, ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(kse->pubKey); + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), 0); + ExpectNotNull(kse->privKey); + test_tls_parse_free_kse(ssl, kse); + } + +#ifdef WOLFSSL_TEST_STATIC_BUILD + /* "ret == 0" itself: force some allocation inside the shared DH + * key setup (the params/key object itself, ahead of either + * buffer's own allocation) to fail, so ret is non-zero by the + * time the pubKey/privKey allocation guards are reached and + * neither one fires. The exact allocation count spent on key + * object setup before either buffer is not part of this + * function's contract, so a small range of failure points is + * tried; harmless if a given one instead lands after a guard + * already ran (that attempt just contributes nothing new). */ + { + int fa; + for (fa = 0; fa <= 12; fa++) { + wolfSSL_Malloc_cb prevM = NULL; + wolfSSL_Free_cb prevF = NULL; + wolfSSL_Realloc_cb prevR = NULL; + + ExpectNotNull(kse = (KeyShareEntry*)XMALLOC( + sizeof(KeyShareEntry), ssl->heap, + DYNAMIC_TYPE_TLSX)); + if (kse == NULL) + break; + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = WOLFSSL_FFDHE_2048; + + ExpectIntEQ(wolfSSL_GetAllocators(&prevM, &prevF, &prevR), 0); + ExpectIntEQ(wolfSSL_SetAllocators(tls_parse_fail_malloc, + tls_parse_fail_free, tls_parse_fail_realloc), 0); + tls_parse_alloc_seen = 0; + tls_parse_fail_after = fa; + + (void)TLSX_KeyShare_GenKey(ssl, kse); + + tls_parse_fail_after = -1; + (void)wolfSSL_SetAllocators(prevM, prevF, prevR); + + test_tls_parse_free_kse(ssl, kse); + } + } +#endif + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif /* !NO_DH && HAVE_FFDHE_2048 */ + +#ifdef HAVE_CURVE25519 + /* TLSX_KeyShare_GenX25519Key(): "ret == 0 && pubKey == NULL". A fresh + * entry gives both true; a second call on the same (now fully + * populated) entry gives pubKey == NULL false while ret stays 0. + * Clearing ssl->rng first forces the key generation itself to fail + * (WC_RNG* rng == NULL is rejected before anything else), giving + * ret == 0 false without needing a malformed group or corrupt state. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* kse = NULL; + WC_RNG* savedRng = ssl->rng; + + ExpectNotNull(kse = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (kse != NULL) { + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = WOLFSSL_ECC_X25519; + ssl->rng = NULL; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectNull(kse->pubKey); + ssl->rng = savedRng; + test_tls_parse_free_kse(ssl, kse); + } + + ExpectNotNull(kse = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (kse != NULL) { + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = WOLFSSL_ECC_X25519; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), 0); + ExpectNotNull(kse->pubKey); + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), 0); + test_tls_parse_free_kse(ssl, kse); + } + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif /* HAVE_CURVE25519 */ + +#ifdef HAVE_CURVE448 + /* TLSX_KeyShare_GenX448Key(): same shape as X25519 above. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* kse = NULL; + WC_RNG* savedRng = ssl->rng; + + ExpectNotNull(kse = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (kse != NULL) { + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = WOLFSSL_ECC_X448; + ssl->rng = NULL; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectNull(kse->pubKey); + ssl->rng = savedRng; + test_tls_parse_free_kse(ssl, kse); + } + + ExpectNotNull(kse = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (kse != NULL) { + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = WOLFSSL_ECC_X448; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), 0); + ExpectNotNull(kse->pubKey); + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), 0); + test_tls_parse_free_kse(ssl, kse); + } + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif /* HAVE_CURVE448 */ + +#if defined(HAVE_ECC) && defined(HAVE_ECC_KEY_EXPORT) + /* TLSX_KeyShare_GenEccKey(): only the "ret == 0" half of "ret == 0 && + * pubKey == NULL" is open (the pubKey half already has coverage + * elsewhere); force it false the same way as the Curve25519/X448 + * cases above. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* kse = NULL; + WC_RNG* savedRng = ssl->rng; + + ExpectNotNull(kse = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (kse != NULL) { + XMEMSET(kse, 0, sizeof(*kse)); + kse->group = WOLFSSL_ECC_SECP256R1; + ssl->rng = NULL; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, kse), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectNull(kse->pubKey); + ssl->rng = savedRng; + test_tls_parse_free_kse(ssl, kse); + } + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif /* HAVE_ECC && HAVE_ECC_KEY_EXPORT */ +#endif + return EXPECT_RESULT(); +} + +/* ---- Key Share free/size/write (RFC 8446 4.2.8) -------------------------- */ +int test_TLSX_KeyShare_freesizewrite(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_SUPPORTED_CURVES) && !defined(NO_DH) && defined(HAVE_FFDHE_2048) && defined(WOLFSSL_TEST_STATIC_BUILD) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + /* TLSX_KeyShare_FreeAll(): "privKey != NULL && privKeyLen > 0" gates + * zeroing an FFDHE entry's private key before it is freed. All four + * combinations are driven directly on a standalone extension list + * (not tied to ssl->extensions), each freed by the same call under + * test. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + int i; + for (i = 0; i < 4; i++) { + TLSX* extensions = NULL; + KeyShareEntry* kse = NULL; + + ExpectNotNull(kse = test_tls_parse_push_kse(&extensions, ssl, + WOLFSSL_FFDHE_2048)); + if (kse != NULL) { + switch (i) { + case 0: /* privKey == NULL, privKeyLen == 0 */ + break; + case 1: /* privKey != NULL, privKeyLen == 0 */ + kse->privKey = (byte*)XMALLOC(1, ssl->heap, + DYNAMIC_TYPE_PRIVATE_KEY); + ExpectNotNull(kse->privKey); + break; + case 2: /* privKey != NULL, privKeyLen > 0 */ + kse->privKey = (byte*)XMALLOC(4, ssl->heap, + DYNAMIC_TYPE_PRIVATE_KEY); + ExpectNotNull(kse->privKey); + kse->privKeyLen = 4; + break; + case 3: /* privKey == NULL, privKeyLen > 0 */ + kse->privKeyLen = 4; + break; + } + } + TLSX_FreeAll(extensions, ssl->heap); + } + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* TLSX_KeyShare_GetSize() / TLSX_KeyShare_Write(): "!isRequest && + * pubKey == NULL" -- a request-direction (client_hello) list always + * writes every entry regardless of pubKey; a response-direction + * (server_hello) list skips any entry without one. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + word32 reqLen; + word16 respLen; + byte out[64]; + word32 reqOff; + word16 respOff; + KeyShareEntry* kse; + + /* pubKey == NULL: response direction skips it (0 bytes); request + * direction still writes it (pubKeyLen == 0, so just the header). + * resp must be set for the response direction to consider this + * extension at all (TLSX_GetSize()'s own, outer "only marked + * extensions are sent back" rule) -- otherwise it would be + * skipped one level up, before ever reaching the pubKey check + * under test. */ + ExpectNotNull(kse = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048)); + { + TLSX* ext = TLSX_Find(ssl->extensions, TLSX_KEY_SHARE); + ExpectNotNull(ext); + if (ext != NULL) + ext->resp = 1; + } + + respLen = 0; + ExpectIntEQ(TLSX_GetResponseSize(ssl, server_hello, &respLen), 0); + respOff = 0; + XMEMSET(out, 0, sizeof(out)); + ExpectIntEQ(TLSX_WriteResponse(ssl, out, server_hello, &respOff), 0); + + /* The request direction never reaches the pubKey test -- isRequest + * short-circuits it -- but it does copy pubKey unconditionally, so + * give the entry one before writing a ClientHello. */ + if (kse != NULL) { + kse->pubKey = (byte*)XMALLOC(2, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(kse->pubKey); + if (kse->pubKey != NULL) { + XMEMSET(kse->pubKey, 0, 2); + kse->pubKeyLen = 2; + } + } + + reqLen = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, client_hello, &reqLen), 0); + ExpectIntGT(reqLen, 0); + reqOff = 0; + XMEMSET(out, 0, sizeof(out)); + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &reqOff), 0); + ExpectIntGT(reqOff, 0); + } + wolfSSL_free(ssl); + ssl = NULL; + + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + word16 respLen; + byte out[64]; + word16 respOff; + KeyShareEntry* kse = NULL; + + /* pubKey != NULL: response direction includes it too. */ + ExpectNotNull(kse = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048)); + if (kse != NULL) { + kse->pubKey = (byte*)XMALLOC(4, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(kse->pubKey); + kse->pubKeyLen = 4; + } + { + TLSX* ext = TLSX_Find(ssl->extensions, TLSX_KEY_SHARE); + ExpectNotNull(ext); + if (ext != NULL) + ext->resp = 1; + } + + respLen = 0; + ExpectIntEQ(TLSX_GetResponseSize(ssl, server_hello, &respLen), 0); + ExpectIntGT(respLen, 0); + respOff = 0; + XMEMSET(out, 0, sizeof(out)); + ExpectIntEQ(TLSX_WriteResponse(ssl, out, server_hello, &respOff), 0); + ExpectIntGT(respOff, 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* ---- Key Share secret derivation (RFC 8446 4.2.8, TLS 1.2 legacy DH) ---- + * TLSX_KeyShare_ProcessDh(), TLSX_KeyShare_ProcessX25519_ex() and + * TLSX_KeyShare_ProcessEcc_ex() are all fully static; TLSX_KeyShare_ + * DeriveSecret() (WOLFSSL_LOCAL) reaches them by dispatching on whatever + * single entry is in the KeyShare extension, so it is used here as the + * entry point, with a hand-built entry standing in for what a real + * handshake would have produced by this point. + */ +int test_TLSX_KeyShare_process(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_SUPPORTED_CURVES) && defined(WOLFSSL_TEST_STATIC_BUILD) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + +#ifdef HAVE_CURVE25519 + /* TLSX_KeyShare_ProcessX25519_ex(): "ret == 0 && key == NULL" -- our + * own side's key. A peer public value is needed either way; borrow + * one from a throwaway key pair generated for this test alone. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* peer = NULL; + KeyShareEntry* target; + + ExpectNotNull(peer = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (peer != NULL) { + XMEMSET(peer, 0, sizeof(*peer)); + peer->group = WOLFSSL_ECC_X25519; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, peer), 0); + } + + target = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_ECC_X25519); + ExpectNotNull(target); + if (target != NULL && peer != NULL && peer->pubKey != NULL) { + XFREE(target->ke, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + target->ke = (byte*)XMALLOC(peer->pubKeyLen, ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(target->ke); + if (target->ke != NULL) { + XMEMCPY(target->ke, peer->pubKey, peer->pubKeyLen); + target->keLen = (word16)peer->pubKeyLen; + } + } + /* target->key stays NULL: our own side never generated a key. */ + ExpectIntEQ(TLSX_KeyShare_DeriveSecret(ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + test_tls_parse_free_kse(ssl, peer); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* Same shape, but our own side has a real key too: both operands + * false, and the exchange actually completes. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* peer = NULL; + KeyShareEntry* target; + + ExpectNotNull(peer = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (peer != NULL) { + XMEMSET(peer, 0, sizeof(*peer)); + peer->group = WOLFSSL_ECC_X25519; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, peer), 0); + } + + target = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_ECC_X25519); + ExpectNotNull(target); + if (target != NULL) { + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, target), 0); + if (peer != NULL && peer->pubKey != NULL) { + XFREE(target->ke, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + target->ke = (byte*)XMALLOC(peer->pubKeyLen, ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(target->ke); + if (target->ke != NULL) { + XMEMCPY(target->ke, peer->pubKey, peer->pubKeyLen); + target->keLen = (word16)peer->pubKeyLen; + } + } + } + ExpectIntEQ(TLSX_KeyShare_DeriveSecret(ssl), 0); + + test_tls_parse_free_kse(ssl, peer); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* "ret == 0" itself: an invalid peer value is rejected before our own + * key is ever looked at, independent of whether one was generated. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* target; + + target = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_ECC_X25519); + ExpectNotNull(target); + if (target != NULL) { + /* wrong length for a Curve25519 public value: rejected by + * wc_curve25519_check_public() before the peer key is even + * imported. */ + XFREE(target->ke, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + target->ke = (byte*)XMALLOC(4, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(target->ke); + if (target->ke != NULL) { + XMEMSET(target->ke, 0, 4); + target->keLen = 4; + } + } + ExpectIntEQ(TLSX_KeyShare_DeriveSecret(ssl), + WC_NO_ERR_TRACE(ECC_PEERKEY_ERROR)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif /* HAVE_CURVE25519 */ + +#if defined(HAVE_ECC) && defined(HAVE_ECC_KEY_EXPORT) + /* TLSX_KeyShare_ProcessEcc_ex(): same "ret == 0 && key == NULL" shape, + * for a plain named ECC curve instead of X25519. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* peer = NULL; + KeyShareEntry* target; + + ExpectNotNull(peer = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (peer != NULL) { + XMEMSET(peer, 0, sizeof(*peer)); + peer->group = WOLFSSL_ECC_SECP256R1; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, peer), 0); + } + + target = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_ECC_SECP256R1); + ExpectNotNull(target); + if (target != NULL && peer != NULL && peer->pubKey != NULL) { + XFREE(target->ke, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + target->ke = (byte*)XMALLOC(peer->pubKeyLen, ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(target->ke); + if (target->ke != NULL) { + XMEMCPY(target->ke, peer->pubKey, peer->pubKeyLen); + target->keLen = (word16)peer->pubKeyLen; + } + } + ExpectIntEQ(TLSX_KeyShare_DeriveSecret(ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + test_tls_parse_free_kse(ssl, peer); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* peer = NULL; + KeyShareEntry* target; + + ExpectNotNull(peer = (KeyShareEntry*)XMALLOC(sizeof(KeyShareEntry), + ssl->heap, DYNAMIC_TYPE_TLSX)); + if (peer != NULL) { + XMEMSET(peer, 0, sizeof(*peer)); + peer->group = WOLFSSL_ECC_SECP256R1; + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, peer), 0); + } + + target = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_ECC_SECP256R1); + ExpectNotNull(target); + if (target != NULL) { + ExpectIntEQ(TLSX_KeyShare_GenKey(ssl, target), 0); + if (peer != NULL && peer->pubKey != NULL) { + XFREE(target->ke, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + target->ke = (byte*)XMALLOC(peer->pubKeyLen, ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(target->ke); + if (target->ke != NULL) { + XMEMCPY(target->ke, peer->pubKey, peer->pubKeyLen); + target->keLen = (word16)peer->pubKeyLen; + } + } + } + ExpectIntEQ(TLSX_KeyShare_DeriveSecret(ssl), 0); + + test_tls_parse_free_kse(ssl, peer); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + /* "ret == 0" itself: an invalid (wrong-length) peer value is rejected + * while importing it, before our own key is ever looked at. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* target; + + target = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_ECC_SECP256R1); + ExpectNotNull(target); + if (target != NULL) { + XFREE(target->ke, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + target->ke = (byte*)XMALLOC(4, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(target->ke); + if (target->ke != NULL) { + XMEMSET(target->ke, 0, 4); + target->keLen = 4; + } + } + ExpectIntEQ(TLSX_KeyShare_DeriveSecret(ssl), + WC_NO_ERR_TRACE(ECC_PEERKEY_ERROR)); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif /* HAVE_ECC && HAVE_ECC_KEY_EXPORT */ + +#if !defined(NO_DH) && defined(HAVE_FFDHE_2048) + /* TLSX_KeyShare_ProcessDh(): "ret == 0 && dhKeySz > preMasterSz" -- a + * raw Diffie-Hellman agreement can legitimately produce a shared value + * shorter than the prime's byte length (a leading zero byte), which is + * then re-padded; a value with no leading zero byte does not need it. + * Both are reached deterministically by fixing our own private + * exponent at 1, so the derived secret is exactly the peer's public + * value (chosen well below the prime either way, so no modular + * reduction occurs). */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* target; + byte* ke = NULL; + + target = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048); + ExpectNotNull(target); + if (target != NULL) { + XFREE(target->ke, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(ke = (byte*)XMALLOC(256, ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY)); + target->ke = ke; + if (ke != NULL) { + /* peer public value 2, left-padded to 256 bytes: a + * leading zero byte. */ + XMEMSET(ke, 0, 256); + ke[255] = 0x02; + target->keLen = 256; + } + target->privKey = (byte*)XMALLOC(1, ssl->heap, + DYNAMIC_TYPE_PRIVATE_KEY); + ExpectNotNull(target->privKey); + if (target->privKey != NULL) { + target->privKey[0] = 0x01; + target->keyLen = 1; + } + } + + ExpectIntEQ(TLSX_KeyShare_DeriveSecret(ssl), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + KeyShareEntry* target; + byte* ke = NULL; + + target = test_tls_parse_push_kse(&ssl->extensions, ssl, + WOLFSSL_FFDHE_2048); + ExpectNotNull(target); + if (target != NULL) { + XFREE(target->ke, ssl->heap, DYNAMIC_TYPE_PUBLIC_KEY); + ExpectNotNull(ke = (byte*)XMALLOC(256, ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY)); + target->ke = ke; + if (ke != NULL) { + /* peer public value 2^2040, left-padded to 256 bytes: no + * leading zero byte. */ + XMEMSET(ke, 0, 256); + ke[0] = 0x01; + target->keLen = 256; + } + target->privKey = (byte*)XMALLOC(1, ssl->heap, + DYNAMIC_TYPE_PRIVATE_KEY); + ExpectNotNull(target->privKey); + if (target->privKey != NULL) { + target->privKey[0] = 0x01; + target->keyLen = 1; + } + } + + ExpectIntEQ(TLSX_KeyShare_DeriveSecret(ssl), 0); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif /* !NO_DH && HAVE_FFDHE_2048 */ +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_tls_parse.h b/tests/api/test_tls_parse.h new file mode 100644 index 0000000000..1d2d8bcd29 --- /dev/null +++ b/tests/api/test_tls_parse.h @@ -0,0 +1,68 @@ +/* test_tls_parse.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 TESTS_API_TEST_TLS_PARSE_H +#define TESTS_API_TEST_TLS_PARSE_H + +int test_TLSX_ALPN_parse(void); +int test_TLSX_TCA_parse(void); +int test_TLSX_certtype_parse(void); +int test_TLSX_Cookie_parse(void); +int test_TLSX_EncryptThenMac_parse(void); +int test_TLSX_MFL_parse(void); +int test_TLSX_THM_parse(void); +int test_TLSX_SessionTicket_parse(void); +int test_TLSX_SecureRenegotiation_parse(void); +int test_TLSX_SupportedVersions_parse(void); +int test_TLSX_SignatureAlgorithms_parse(void); +int test_TLSX_CSR_parse(void); +int test_TLSX_PointFormat_parse(void); +int test_TLSX_SNI_parse(void); +int test_TLSX_ValidateSupportedCurves(void); +int test_TLSX_SupportedGroups_parse(void); +int test_TLSX_KeyShare_negotiate(void); +int test_TLSX_KeyShare_gen(void); +int test_TLSX_KeyShare_freesizewrite(void); +int test_TLSX_KeyShare_process(void); + +#define TEST_TLS_PARSE_DECLS \ + TEST_DECL_GROUP("tls", test_TLSX_ALPN_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_TCA_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_certtype_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_Cookie_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_EncryptThenMac_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_MFL_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_THM_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_SessionTicket_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_SecureRenegotiation_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_SupportedVersions_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_SignatureAlgorithms_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_CSR_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_PointFormat_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_SNI_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_ValidateSupportedCurves), \ + TEST_DECL_GROUP("tls", test_TLSX_SupportedGroups_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_KeyShare_negotiate), \ + TEST_DECL_GROUP("tls", test_TLSX_KeyShare_gen), \ + TEST_DECL_GROUP("tls", test_TLSX_KeyShare_freesizewrite), \ + TEST_DECL_GROUP("tls", test_TLSX_KeyShare_process) + +#endif /* TESTS_API_TEST_TLS_PARSE_H */ diff --git a/tests/api/test_wolfmath.c b/tests/api/test_wolfmath.c index 49ea7f8f07..4a0b65bef6 100644 --- a/tests/api/test_wolfmath.c +++ b/tests/api/test_wolfmath.c @@ -247,7 +247,7 @@ int test_wc_SpIntSizeDecisionCoverage(void) * (sp_div_2d/sp_mod_2d/sp_mul_2d/sp_tohex/sp_exch/sp_2expt/sp_exptmod_ex) need * WOLFSSL_SP_MATH_ALL && !WOLFSSL_RSA_VERIFY_ONLY, the ct helpers * (sp_addmod_ct/sp_submod_ct/sp_div_2_mod_ct/sp_div_2) need HAVE_ECC, and - * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the campaign + * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the harness * sp-math config satisfies) guarantees every helper is compiled. */ #if defined(WOLFSSL_SP_MATH_ALL) && defined(WOLFSSL_PUBLIC_MP) && \ !defined(WOLFSSL_RSA_VERIFY_ONLY) && !defined(NO_RSA) && \ @@ -330,7 +330,7 @@ int test_wc_SpIntShiftDecisionCoverage(void) * (sp_div_2d/sp_mod_2d/sp_mul_2d/sp_tohex/sp_exch/sp_2expt/sp_exptmod_ex) need * WOLFSSL_SP_MATH_ALL && !WOLFSSL_RSA_VERIFY_ONLY, the ct helpers * (sp_addmod_ct/sp_submod_ct/sp_div_2_mod_ct/sp_div_2) need HAVE_ECC, and - * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the campaign + * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the harness * sp-math config satisfies) guarantees every helper is compiled. */ #if defined(WOLFSSL_SP_MATH_ALL) && defined(WOLFSSL_PUBLIC_MP) && \ !defined(WOLFSSL_RSA_VERIFY_ONLY) && !defined(NO_RSA) && \ @@ -392,7 +392,7 @@ int test_wc_SpIntDigitArithDecisionCoverage(void) * (sp_div_2d/sp_mod_2d/sp_mul_2d/sp_tohex/sp_exch/sp_2expt/sp_exptmod_ex) need * WOLFSSL_SP_MATH_ALL && !WOLFSSL_RSA_VERIFY_ONLY, the ct helpers * (sp_addmod_ct/sp_submod_ct/sp_div_2_mod_ct/sp_div_2) need HAVE_ECC, and - * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the campaign + * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the harness * sp-math config satisfies) guarantees every helper is compiled. */ #if defined(WOLFSSL_SP_MATH_ALL) && defined(WOLFSSL_PUBLIC_MP) && \ !defined(WOLFSSL_RSA_VERIFY_ONLY) && !defined(NO_RSA) && \ @@ -543,7 +543,7 @@ int test_wc_SpIntArithDecisionCoverage(void) * (sp_div_2d/sp_mod_2d/sp_mul_2d/sp_tohex/sp_exch/sp_2expt/sp_exptmod_ex) need * WOLFSSL_SP_MATH_ALL && !WOLFSSL_RSA_VERIFY_ONLY, the ct helpers * (sp_addmod_ct/sp_submod_ct/sp_div_2_mod_ct/sp_div_2) need HAVE_ECC, and - * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the campaign + * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the harness * sp-math config satisfies) guarantees every helper is compiled. */ #if defined(WOLFSSL_SP_MATH_ALL) && defined(WOLFSSL_PUBLIC_MP) && \ !defined(WOLFSSL_RSA_VERIFY_ONLY) && !defined(NO_RSA) && \ @@ -674,7 +674,7 @@ int test_wc_SpIntConvDecisionCoverage(void) * (sp_div_2d/sp_mod_2d/sp_mul_2d/sp_tohex/sp_exch/sp_2expt/sp_exptmod_ex) need * WOLFSSL_SP_MATH_ALL && !WOLFSSL_RSA_VERIFY_ONLY, the ct helpers * (sp_addmod_ct/sp_submod_ct/sp_div_2_mod_ct/sp_div_2) need HAVE_ECC, and - * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the campaign + * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the harness * sp-math config satisfies) guarantees every helper is compiled. */ #if defined(WOLFSSL_SP_MATH_ALL) && defined(WOLFSSL_PUBLIC_MP) && \ !defined(WOLFSSL_RSA_VERIFY_ONLY) && !defined(NO_RSA) && \ @@ -861,7 +861,7 @@ int test_wc_SpIntExptGcdDecisionCoverage(void) * (sp_div_2d/sp_mod_2d/sp_mul_2d/sp_tohex/sp_exch/sp_2expt/sp_exptmod_ex) need * WOLFSSL_SP_MATH_ALL && !WOLFSSL_RSA_VERIFY_ONLY, the ct helpers * (sp_addmod_ct/sp_submod_ct/sp_div_2_mod_ct/sp_div_2) need HAVE_ECC, and - * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the campaign + * sp_gcd needs !NO_RSA && WOLFSSL_KEY_GEN. This condition (which the harness * sp-math config satisfies) guarantees every helper is compiled. */ #if defined(WOLFSSL_SP_MATH_ALL) && defined(WOLFSSL_PUBLIC_MP) && \ !defined(WOLFSSL_RSA_VERIFY_ONLY) && !defined(NO_RSA) && \ diff --git a/tests/include.am b/tests/include.am index 5e5ef55ef4..c19499a3f6 100644 --- a/tests/include.am +++ b/tests/include.am @@ -114,7 +114,7 @@ DISTCLEANFILES+= tests/.libs/unit.test # directly and defines its own main(), so it cannot be a unit.test source (that # would duplicate main() and the included .c's symbols). They are compiled # standalone, one at a time, by the out-of-tree per-module MC/DC coverage -# campaign, which reads them from this directory. They are listed in EXTRA_DIST +# suite, which reads them from this directory. They are listed in EXTRA_DIST # only so the source-completeness check accounts for them and so they ship in # the dist tarball -- the same treatment tests/api/include.am gives its # non-compiled files. Do not move them to tests_unit_test_SOURCES. @@ -164,6 +164,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_integer_whitebox.c \ tests/unit-mcdc/test_kdf_hash_fault_whitebox.c \ tests/unit-mcdc/test_kdf_whitebox.c \ + tests/unit-mcdc/test_lms_bds_whitebox.c \ tests/unit-mcdc/test_lms_fault_whitebox.c \ tests/unit-mcdc/test_lms_hash_fault_whitebox.c \ tests/unit-mcdc/test_logging_globalq_whitebox.c \ @@ -184,6 +185,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_pkcs7_mutate_whitebox.c \ tests/unit-mcdc/test_pkcs7_whitebox.c \ tests/unit-mcdc/test_poly1305_whitebox.c \ + tests/unit-mcdc/test_puf_gf_whitebox.c \ tests/unit-mcdc/test_puf_whitebox.c \ tests/unit-mcdc/test_pwdbased_whitebox.c \ tests/unit-mcdc/test_random_fault_whitebox.c \ @@ -210,6 +212,8 @@ EXTRA_DIST += \ tests/unit-mcdc/test_sp_c32_whitebox.c \ tests/unit-mcdc/test_sp_c64_fault_whitebox.c \ tests/unit-mcdc/test_sp_c64_whitebox.c \ + tests/unit-mcdc/test_sp_cortexm_crafted_whitebox.c \ + tests/unit-mcdc/test_sp_cortexm_fault_whitebox.c \ tests/unit-mcdc/test_sp_cortexm_whitebox.c \ tests/unit-mcdc/test_sp_crafted_common.h \ tests/unit-mcdc/test_sp_fault_common.h \ @@ -217,7 +221,11 @@ EXTRA_DIST += \ tests/unit-mcdc/test_sp_int_whitebox.c \ tests/unit-mcdc/test_sp_x86_64_fault_whitebox.c \ tests/unit-mcdc/test_sp_x86_64_whitebox.c \ + tests/unit-mcdc/test_srp_whitebox.c \ + tests/unit-mcdc/test_tfm_fault_whitebox.c \ tests/unit-mcdc/test_tfm_whitebox.c \ + tests/unit-mcdc/test_tls13_null_whitebox.c \ + tests/unit-mcdc/test_tls13_whitebox.c \ tests/unit-mcdc/test_tsp_fault_whitebox.c \ tests/unit-mcdc/test_tsp_whitebox.c \ tests/unit-mcdc/test_wc_encrypt_whitebox.c \ @@ -228,5 +236,6 @@ EXTRA_DIST += \ tests/unit-mcdc/test_wc_port_whitebox.c \ tests/unit-mcdc/test_wc_xmss_impl_whitebox.c \ tests/unit-mcdc/test_wolfentropy_whitebox.c \ + tests/unit-mcdc/test_wolfmath_whitebox.c \ tests/unit-mcdc/test_xmss_fault_whitebox.c \ tests/unit-mcdc/test_xmss_hash_fault_whitebox.c diff --git a/tests/unit-mcdc/README.md b/tests/unit-mcdc/README.md index e0f2641550..af7c66fb1d 100644 --- a/tests/unit-mcdc/README.md +++ b/tests/unit-mcdc/README.md @@ -5,7 +5,7 @@ This directory holds small, standalone white-box programs that raise **MC/DC** reaching decisions that are **structurally unreachable from the public API**. These are **not** part of the wolfSSL build and are **not** registered in -`tests/api`. They exist for the external ISO 26262 per-module coverage campaign +`tests/api`. They exist for the external ISO 26262 per-module coverage suite in `iso26262/mcdc-per-module/`. Nothing here changes library behaviour. ## Why a separate module @@ -24,16 +24,16 @@ MC/DC independence pair** in the same binary. ## How coverage is combined -llvm-cov computes MC/DC independence **per binary**. The campaign's +llvm-cov computes MC/DC independence **per binary**. The suite's `aggregate.sh` unions the "independence shown" bit **across binaries by source `line:col`**. So each pair must be completed *within the white-box binary itself* - it does not lean on the API tests to supply the other half. The white-box result is unioned in as an extra `"_wb"` ledger row, one per build variant, exactly like any other variant. -## Build contract (driven by `run-mcdc.sh`) +## Build contract -The campaign's `run-mcdc.sh` builds each file via `#include` with the **exact** +The external harness builds each file via `#include` with the **exact** compile flags the instrumented library used for that translation unit (captured from the real `libtool` command - struct layout and backend selection depend on `-DHAVE___UINT128_T`, `user_settings.h`, `-DWOLFSSL_TEST_STATIC_BUILD`, ...), then @@ -68,8 +68,8 @@ justified in `iso26262/mcdc-per-module/reports/aes/RESIDUALS.md`: `main()`, calls each unreachable helper with both halves of every targeted MC/DC pair. Keep every call memory-safe (short-circuits protect NULL derefs); surface setup failures as printed skips and **return 0** (a nonzero exit - makes the campaign discard the variant). -2. Point the campaign at it (a per-module white-box source path in - `db/modules.json`); `run-mcdc.sh`'s white-box step handles build/link/export. -3. Re-run `run-mcdc.sh ` then `aggregate.sh `; confirm the - targeted `line:col` keys leave `GAPS.md`. + makes the harness discard the variant). +2. Register it with the external harness as this module's white-box source; + its white-box step handles build, link and coverage export. +3. Re-measure the module and confirm the targeted `line:col` keys no longer + appear among its uncovered conditions. diff --git a/tests/unit-mcdc/mcdc_der_edit.h b/tests/unit-mcdc/mcdc_der_edit.h index 76f9180a8c..b07327b123 100644 --- a/tests/unit-mcdc/mcdc_der_edit.h +++ b/tests/unit-mcdc/mcdc_der_edit.h @@ -20,7 +20,7 @@ */ /* - * mcdc_der_edit.h -- structural DER edits for the per-module MC/DC campaign. + * mcdc_der_edit.h -- structural DER edits for the per-module MC/DC suite. * * PURPOSE * ------- diff --git a/tests/unit-mcdc/mcdc_fault_alloc.h b/tests/unit-mcdc/mcdc_fault_alloc.h index 44281aba18..5dc1e8461e 100644 --- a/tests/unit-mcdc/mcdc_fault_alloc.h +++ b/tests/unit-mcdc/mcdc_fault_alloc.h @@ -21,11 +21,11 @@ /* * mcdc_fault_alloc.h -- header-only, self-contained heap-fault injector for the - * per-module MC/DC campaign. + * per-module MC/DC suite. * * PURPOSE * ------- - * The dominant justified-residual class across the campaign is the FALSE half + * The dominant justified-residual class across the harness is the FALSE half * of success-chain guards shaped * * if ((err == MP_OKAY) && ) ... (drive on FALSE) diff --git a/tests/unit-mcdc/mcdc_fault_hash.h b/tests/unit-mcdc/mcdc_fault_hash.h index 5f135c3254..5ef7708c56 100644 --- a/tests/unit-mcdc/mcdc_fault_hash.h +++ b/tests/unit-mcdc/mcdc_fault_hash.h @@ -21,13 +21,13 @@ /* * mcdc_fault_hash.h -- header-only, self-contained HASH/BLOCK-CIPHER primitive - * fault injector for the per-module MC/DC campaign. It is the second lever + * fault injector for the per-module MC/DC suite. It is the second lever * beside mcdc_fault_alloc.h, and the ONLY one that works for the hash-based * signature/KEM engines. * * WHY A SECOND LEVER * ------------------ - * The dominant justified-residual class campaign-wide is the FALSE half of a + * The dominant justified-residual class suite-wide is the FALSE half of a * success chain: * * if ((ret == 0) && ) ... @@ -44,7 +44,7 @@ * * HOW IT WORKS -- MACRO INTERPOSITION * ----------------------------------- - * Every white-box TU in this campaign #includes the involved .c directly, and + * Every white-box TU in this suite #includes the involved .c directly, and * the harness links it against libwolfssl.a with only that one object trimmed. * The primitives above therefore still come from the archive and cannot be * replaced at link time -- but they CAN be replaced at preprocessing time, @@ -87,7 +87,7 @@ * PORTABILITY * ----------- * Each wrapper/macro pair is behind the same feature guard as the declaration - * it shadows, so a TU that includes this header builds under every campaign + * it shadows, so a TU that includes this header builds under every suite * variant (in a build where a primitive is compiled out, nothing is * interposed). Unused wrappers are ordinary unused static helpers. * diff --git a/tests/unit-mcdc/mcdc_fault_mp.h b/tests/unit-mcdc/mcdc_fault_mp.h index 0497060137..61cdf6e29e 100644 --- a/tests/unit-mcdc/mcdc_fault_mp.h +++ b/tests/unit-mcdc/mcdc_fault_mp.h @@ -21,7 +21,7 @@ /* * mcdc_fault_mp.h -- header-only big-integer (mp_*) fault injector for the - * per-module MC/DC campaign. Third lever, beside mcdc_fault_alloc.h (heap) and + * per-module MC/DC suite. Third lever, beside mcdc_fault_alloc.h (heap) and * mcdc_fault_hash.h (hash / block-cipher primitives). * * WHY @@ -229,7 +229,7 @@ MCDC_FM_MAYBE_UNUSED static int mcdc_fm_to_unsigned_bin_len(const mp_int* a, /* Some entry points are declared only under a feature condition by the * heapmath (integer.h) and fastmath (tfm.h) backends; sp_int.h declares all of * them. Wrapping one the active backend does not declare makes the including - * TU fail to build, which the campaign scores as a silent skip. */ + * TU fail to build, which the harness scores as a silent skip. */ #if defined(WOLFSSL_SP_MATH) || defined(WOLFSSL_SP_MATH_ALL) #define MCDC_FM_HAVE_READ_RADIX #define MCDC_FM_HAVE_PRIME_IS_PRIME_EX diff --git a/tests/unit-mcdc/mcdc_fault_mpint.h b/tests/unit-mcdc/mcdc_fault_mpint.h index e2487b8ead..322a1593a7 100644 --- a/tests/unit-mcdc/mcdc_fault_mpint.h +++ b/tests/unit-mcdc/mcdc_fault_mpint.h @@ -21,7 +21,7 @@ /* * mcdc_fault_mpint.h -- fault injector for the SCRATCH-mp_int LIFECYCLE macros - * NEW_MP_INT_SIZE() / INIT_MP_INT_SIZE(), for the per-module MC/DC campaign. + * NEW_MP_INT_SIZE() / INIT_MP_INT_SIZE(), for the per-module MC/DC suite. * Fourth lever, beside mcdc_fault_alloc.h (heap), mcdc_fault_hash.h (digest / * block-cipher primitives) and mcdc_fault_mp.h (big-integer computations). * @@ -97,7 +97,7 @@ * * AVAILABILITY (read the note in mcdc_seed_rng.h first: a conditionally * available header must still define its API UNCONDITIONALLY, or a TU that - * calls it fails to COMPILE under some variant -- which the campaign scores as + * calls it fails to COMPILE under some variant -- which the harness scores as * a silent skip rather than an error.) * * The INIT lever works under every backend: INIT_MP_INT_SIZE is an expression diff --git a/tests/unit-mcdc/mcdc_fault_mutex.h b/tests/unit-mcdc/mcdc_fault_mutex.h index 54fce2e910..2bac81bc5e 100644 --- a/tests/unit-mcdc/mcdc_fault_mutex.h +++ b/tests/unit-mcdc/mcdc_fault_mutex.h @@ -20,7 +20,7 @@ */ /* - * mcdc_fault_mutex.h -- mutex-fault injector for the per-module MC/DC campaign. + * mcdc_fault_mutex.h -- mutex-fault injector for the per-module MC/DC suite. * * PURPOSE * ------- diff --git a/tests/unit-mcdc/mcdc_seed_rng.h b/tests/unit-mcdc/mcdc_seed_rng.h index 5befe207db..fc76088a94 100644 --- a/tests/unit-mcdc/mcdc_seed_rng.h +++ b/tests/unit-mcdc/mcdc_seed_rng.h @@ -20,7 +20,7 @@ */ /* - * mcdc_seed_rng.h -- deterministic RNG for the per-module MC/DC campaign. + * mcdc_seed_rng.h -- deterministic RNG for the per-module MC/DC suite. * * WHY THIS EXISTS * --------------- @@ -79,6 +79,15 @@ * implementation half for why that matters. */ +/* The availability test below reads feature macros, so the configuration has + * to be in scope before it runs. A driver that includes this header as its + * FIRST wolfSSL include otherwise evaluates the test against an empty macro + * environment, silently selects the inert stubs, and pins nothing -- which + * builds, links and runs, so the harness scores it as a pass. Pull the + * switchboard in here rather than relying on every caller's include order. + * It is idempotent, so callers that already included it are unaffected. */ +#include + /* SHAKE-256 is gated positively by WOLFSSL_SHAKE256; there is no NO_SHA3. */ #if !defined(WOLFSSL_SHAKE256) || defined(WOLFSSL_NO_SHAKE256) || \ defined(WC_NO_RNG) @@ -99,7 +108,7 @@ static int mcdc_sr_active = 0; * before anything else drags random.h in; when it is not, the include * guard skips the prototype, the hook is never declared, and every call * site inside the .c under test fails with "use of undeclared - * identifier". That is a compile failure, which the campaign scores as a + * identifier". That is a compile failure, which the harness scores as a * SILENT SKIP -- the dh module read 107/173 with 6 of 12 variants * aggregating instead of 158/173, and still reported success. * @@ -211,7 +220,7 @@ int mcdc_sr_block(WC_RNG* rng, byte* out, word32 sz) #else /* MCDC_SR_UNAVAILABLE */ /* Inert stubs. Without these a TU that calls mcdc_sr_arm() fails to COMPILE in - * any variant lacking SHAKE -- and the campaign scores a white-box that fails + * any variant lacking SHAKE -- and the harness scores a white-box that fails * to compile as a SILENT SKIP, losing the whole file's coverage rather than * reporting an error. (Observed on the dh module: 12 variants aggregated * became 6, and dh.c read 107/173 instead of 158/173.) A header that is diff --git a/tests/unit-mcdc/test_aes_whitebox.c b/tests/unit-mcdc/test_aes_whitebox.c index 8c0d2209bd..b402a3148c 100644 --- a/tests/unit-mcdc/test_aes_whitebox.c +++ b/tests/unit-mcdc/test_aes_whitebox.c @@ -11,13 +11,13 @@ * MC/DC independence pair. * * 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 + * source line:col in the per-module suite: + * llvm-cov computes MC/DC independence PER BINARY, and the * 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.sh's white-box step with the SAME MC/DC CFLAGS, + * Build: compiled by the coverage runner'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 aes.o removed (this TU supplies * the instrumented aes.c). NOT part of the wolfSSL build; not registered in @@ -39,7 +39,7 @@ * Class 10 AArch64 CTR leftover-keystream loop (WOLFSSL_ARMASM, * __aarch64__, qemu-aarch64 lane only) .. exclusion demonstration * Classes 4, 5 and 10 only compile in the qemu-aarch64 emulator lane (see - * iso26262/mcdc-per-module campaign, db/lanes.json); on every other build + * iso26262/mcdc-per-module suite, db/lanes.json); on every other build * they reduce to a no-op stub so this file still compiles+runs natively. * The remaining union residuals are structurally uncoverable even here * (complementary-operand decisions where unique-cause MC/DC is unsatisfiable, @@ -47,7 +47,7 @@ * loop above, and AesCfbDecrypt_C's `ret == 0` loop guard: the only build * axis that compiles that block, WOLFSSL_ARMASM, also selects a * wc_AesEncrypt() with no failure path). Those stay justified in - * campaign/db/exclusions.json + EXCLUSIONS.md and reports/aes/RESIDUALS.md. + * the exclusion record + the exclusion record and reports/aes/RESIDUALS.md. */ /* Pull aes.c in verbatim so the file-static and WOLFSSL_LOCAL helpers below are @@ -275,7 +275,7 @@ static void wb_aesnew_common(void) /* ------------------------------------------------------------------------- * * Class 3: AES-NI internal pointer guards (WOLFSSL_AESNI). * - * When aes.c is compiled with AES-NI (the campaign's "aesni" variant), the + * When aes.c is compiled with AES-NI (the "aesni" variant), the * AES-NI code paths add file-static helpers whose NULL/size guards every public * caller pre-rejects, exactly like the classic GHASH guards: * @@ -601,7 +601,7 @@ static void wb_aarch64_gcm_ptr_guards(void) * iv == NULL, ivSz > 0 -> idx3 F (group false), idx5 T, idx6 T -> reject * and idx3 flips with idx5 in both, so neither row isolates idx5. The calls * below still run (they pair idx3/idx4/idx6); idx5 is recorded in - * campaign/db/exclusions.json. wc_AesGcmSetIV's idx5 ("ivFixed != NULL", + * the exclusion record. wc_AesGcmSetIV's idx5 ("ivFixed != NULL", * ~14858) is excluded on the identical argument against its idx3 * ("ivFixed == NULL"). * ------------------------------------------------------------------------- */ @@ -1123,7 +1123,7 @@ static void wb_ccm_aesni_dispatch(void) * so on arrival either left == 0 (left <= sz) or sz == 0 (left > sz), never * both non-zero. The loop below is a duplicate of that drain and its body is * dead. With the decision never true, neither operand has an independence - * pair, so both conditions are recorded in campaign/db/exclusions.json. The + * pair, so both conditions are recorded in the exclusion record. The * calls stay so the argument is demonstrated rather than only asserted. * ------------------------------------------------------------------------- */ #if defined(__aarch64__) && defined(WOLFSSL_ARMASM) && \ @@ -1196,7 +1196,7 @@ int main(void) wb_ccm_aesni_dispatch(); wb_aarch64_ctr_leftover(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_asn_cert_whitebox.c b/tests/unit-mcdc/test_asn_cert_whitebox.c index 6855dce24d..801eae97b1 100644 --- a/tests/unit-mcdc/test_asn_cert_whitebox.c +++ b/tests/unit-mcdc/test_asn_cert_whitebox.c @@ -1262,7 +1262,7 @@ static void wb_date_greater_than(void) * Section 15: wc_ValidateDateWithTime() [:15988,:16015] * * :15988 `sizeof(ltime)==sizeof(word32) && (sword32)ltime<0` -- on every - * build this campaign targets, time_t is 64-bit, so the 1st operand is a + * build this suite targets, time_t is 64-bit, so the 1st operand is a * compile-time false and the 2nd is never reached; its true side is not * reachable without a 32-bit time_t target (RESIDUAL, platform-gated, not * a fault-injection case). @@ -1273,7 +1273,7 @@ static void wb_date_greater_than(void) * the offset `i` lands on once the 6 GetTime() fields are consumed, so by * the time this line runs date[i] is always 'Z' on any successful parse. * Structurally dead code under the current ExtractDate() contract - * (candidate for the campaign DEATHNOTE) -- not attempted here. + * -- not attempted here. * ========================================================================= */ static void wb_validate_date_with_time(void) { @@ -2334,6 +2334,143 @@ static Signer* wb_make_signer(const WbFix* fix) return signer; } +/* Build a certificate request whose serialNumber ATTRIBUTE is a single 0x00 + * byte, and run it through ParseCertRelative() as a CERTREQ_TYPE. Returns + * nothing: the point is the decisions evaluated on the way. */ +#ifdef WOLFSSL_CERT_REQ +static void wb_zero_serial_csr(void) +{ + /* Attribute ::= SEQUENCE { type OBJECT IDENTIFIER, values SET OF ANY } + * type = 2.5.4.5 (id-at-serialNumber), value = UTF8String { 0x00 }. + * UTF8String is one of DecodeCertReqAttrValue()'s accepted choices + * (strAttrChoice, asn.c:23082) and the decoder does not validate the + * code points, so a single NUL byte is carried through verbatim. */ + static const byte serialAttr[] = { + 0x30, 0x0A, + 0x06, 0x03, 0x55, 0x04, 0x05, + 0x31, 0x03, + 0x0C, 0x01, 0x00 + }; + byte* req = NULL; + Cert* cert = NULL; + DecodedCert* dc = NULL; + word32 sz; + word32 co, cl, lo, lw; + word32 infoOff, child, attrsOff = 0; + int n; + int ret; + const word32 cap = WB_FIX_DER_SZ; + + if (!wbKeysOk || !wbRngOk) { + return; + } + + req = (byte*)XMALLOC(cap, NULL, DYNAMIC_TYPE_TMP_BUFFER); + cert = (Cert*)XMALLOC(sizeof(Cert), NULL, DYNAMIC_TYPE_TMP_BUFFER); + dc = (DecodedCert*)XMALLOC(sizeof(DecodedCert), NULL, DYNAMIC_TYPE_DCERT); + if ((req == NULL) || (cert == NULL) || (dc == NULL)) { + XFREE(req, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(cert, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(dc, NULL, DYNAMIC_TYPE_DCERT); + return; + } + + ret = wc_InitCert(cert); + if (ret == 0) { + wb_fill_name(&cert->subject, "MCDC ZS CSR"); + cert->sigType = CTC_SHA256wRSA; + /* Non-empty attributes: the structural insert below needs the [0] + * set to have a content region of its own. */ + XSTRNCPY(cert->challengePw, "mcdc", CTC_NAME_SIZE); + ret = wc_MakeCertReq(cert, req, (int)cap, &wbKeyLeaf, NULL); + if (ret > 0) { + ret = 0; + } + } + if (ret == 0) { + ret = wc_SignCert(cert->bodySz, cert->sigType, req, (int)cap, + &wbKeyRoot, NULL, &wbRng); + if (ret > 0) { + sz = (word32)ret; + ret = 0; + } + else { + ret = -1; + } + } + WB_CHECK(ret == 0, "manufacture the challengePassword CSR"); + + /* CertificationRequest ::= SEQ { CertificationRequestInfo, ... }; + * CertificationRequestInfo ::= SEQ { version, subject, spki, [0] attrs } + * -- walk to the fourth child of the second-level SEQUENCE. */ + if (ret == 0) { + if (mcdc_der_hdr(req, sz, 0, &co, &cl, &lo, &lw) == 0) { + ret = -1; + } + else { + infoOff = co; + if (mcdc_der_hdr(req, sz, infoOff, &co, &cl, &lo, &lw) == 0) { + ret = -1; + } + else { + child = co; + for (n = 0; (ret == 0) && (n < 3); n++) { + if (mcdc_der_hdr(req, sz, child, &co, &cl, &lo, &lw) == 0) { + ret = -1; + } + else { + child = co + cl; + } + } + if ((ret == 0) && + (mcdc_der_hdr(req, sz, child, &co, &cl, &lo, &lw) + != 0) && + (req[child] == + (byte)(ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | + 0)) && (cl > 0)) { + attrsOff = co; + } + else { + ret = -1; + } + } + } + } + WB_CHECK(ret == 0, "located the CSR [0] attributes set"); + + if ((ret == 0) && (mcdc_der_grow(req, &sz, cap, attrsOff, serialAttr, + (word32)sizeof(serialAttr)) != 0)) { + WB_NOTE("splicing the serialNumber attribute was refused; " + ":24630 2nd-operand row skipped"); + ret = -1; + } + + if (ret == 0) { + wc_InitDecodedCert(dc, req, sz, NULL); + ret = ParseCertRelative(dc, CERTREQ_TYPE, NO_VERIFY, NULL, NULL); + /* The spliced attribute invalidates the signature, and a CSR is + * always self-verified, so the call ends in a signature failure -- + * long after :24622/:24630. What is asserted is that the serial + * attribute was picked up at all; without it the guard is not even + * entered. */ + WB_CHECK(dc->serialSz == 1 && dc->serial[0] == 0 && dc->isCSR == 1, + ":24630 2nd operand false (CSR with a zero serialNumber " + "attribute)"); + wc_FreeDecodedCert(dc); + } + + wc_SetCert_Free(cert); + XFREE(req, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(cert, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(dc, NULL, DYNAMIC_TYPE_DCERT); +} +#else +static void wb_zero_serial_csr(void) +{ + WB_NOTE("WOLFSSL_CERT_REQ off; :24630 2nd-operand row skipped"); +} +#endif + /* The fixture set. File-scope so the small_stack variant does not put ~80KB * of certificate DER on the stack. */ static WbFix wbRootA; /* self-signed CA, pathLen 1, keyCertSign */ @@ -2737,6 +2874,32 @@ static void wb_fixture_parse_matrix(void) wb_parse_one(&wbZeroSerialSubCA, CA_TYPE, VERIFY, cm, NULL); wb_parse_one(&wbZeroSerialSubCA, TRUSTED_PEER_TYPE, VERIFY, cm, NULL); + /* ---- :24630 `!isTrustAnchorLoad && !isCsr`, second operand -------- * + * A certificate can never take the isCsr arm and no certificate can + * make either of :24624's trailing operands false: DecodeCertInternal() + * (asn.c:23007) already rejects serial 0 for anything that is not + * `isCA && selfSigned`, and ParseCertRelative() returns immediately on a + * negative DecodeCert(), so `ret == 0` at :24622 implies both. + * + * A certificate REQUEST is the exception, and the only one. It is parsed + * by DecodeCertReq(), which does not carry that check, and it CAN carry a + * serial number: DecodeCertReqAttrValue()'s SERIAL_NUMBER_OID arm + * (asn.c:23145) copies a serialNumber ATTRIBUTE into cert->serial. A CSR + * whose attribute value is a single 0x00 byte therefore reaches :24622 + * with serialSz == 1, serial[0] == 0, isTrustAnchorLoad == 0 (the type is + * CERTREQ_TYPE) and isCsr == 1. + * + * wc_MakeCertReq() has no way to emit that attribute, so it is inserted + * structurally: the request is generated with a challengePassword (which + * makes the [0] attributes set non-empty, so it has a content region to + * insert into) and mcdc_der_grow() then splices an + * SEQUENCE { OID 2.5.4.5, SET { UTF8String 0x00 } } + * in at the START of that set's content, rewriting the enclosing + * CertificationRequest / CertificationRequestInfo / [0] lengths. The + * signature is invalidated by the edit, which does not matter: :24630 is + * evaluated well before the CERTREQ_TYPE ConfirmSignature() call. */ + wb_zero_serial_csr(); + /* ---- basicConstraints / keyUsage consistency [:24464] ------------- */ wb_parse_one(&wbLeafKuCertSign, CERT_TYPE, VERIFY, cm, NULL); wb_parse_one(&wbLeafKuCertSign, CA_TYPE, NO_VERIFY, cm, NULL); @@ -3080,7 +3243,7 @@ int main(void) printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_asn_certgen_whitebox.c b/tests/unit-mcdc/test_asn_certgen_whitebox.c index b3e1804b52..724df78990 100644 --- a/tests/unit-mcdc/test_asn_certgen_whitebox.c +++ b/tests/unit-mcdc/test_asn_certgen_whitebox.c @@ -42,7 +42,7 @@ * driven and paired for MC/DC. * * NOT COMPILED IN THIS MODULE'S CONFIG (verified against - * campaign/configs/asn/user_settings.base.h): + * suite/configs/asn/user_settings.base.h): * - WOLFSSL_ACERT: attribute-certificate parsing (ParseX509Acert, * DecodeAcertGeneralName(s), VerifyX509Acert, ...) is entirely gated * behind "#if defined(WOLFSSL_ACERT) && defined(WOLFSSL_ASN_TEMPLATE)" @@ -592,6 +592,17 @@ static void wb_set_name_ex(void) #endif /* ======================================================================== + * ARGUED UNREACHABLE, do not re-open (suite the exclusion record + + * db/exclusions.json): wc_SetSubjectRaw() :32713 cond 0 and wc_SetIssuerRaw() + * :32750 cond 0 (`decodedCert->subjectRaw` non-NULL). GetCertName() assigns + * cert->subjectRaw = &input[srcIdx] (asn.c:15513) on every path where the + * subject Name SEQUENCE parses, and DecodeCertInternal() only calls it once + * the template walk has succeeded; if either fails, DecodeCert() returns + * negative, wc_SetCert_LoadDer() propagates that, and the enclosing + * `if (ret >= 0)` is never entered. Cond 1 (subjectRawLen <= sizeof(CertName)) + * is NOT excluded -- a subject longer than sizeof(CertName) is constructible + * in principle, just not from any corpus certificate. + * * SECTION I: EncodeExtensions() direct call. * :~28792 if (cert->pathLenSet && ((keyUsage & KEYUSE_KEY_CERT_SIGN) || (!keyUsage))) * :~29126 else if ((output!=NULL) && (sz>maxSz)) @@ -629,6 +640,18 @@ static void wb_encode_extensions(void) WB_CHECK(sz > 0, ":28792 pathLenSet true, (keyUsage&CERT_SIGN)||!keyUsage both false"); + /* :28792 third operand (!keyUsage) true: pathLenSet with NO keyUsage at + * all, which is the case the `|| (!keyUsage)` arm exists for. Row (B) + * above is its false partner (keyUsage non-zero without KEY_CERT_SIGN), + * and both live in this binary. */ + WB_CHECK(wc_InitCert(&cert) == 0, "wc_InitCert (B2)"); + cert.isCA = 1; + cert.pathLenSet = 1; + cert.pathLen = 2; + cert.keyUsage = 0; + sz = EncodeExtensions(&cert, NULL, 0, 0); + WB_CHECK(sz > 0, ":28792 pathLenSet true, keyUsage == 0 (third operand)"); + /* :28792 pathLenSet false -> whole AND short-circuits false. */ WB_CHECK(wc_InitCert(&cert) == 0, "wc_InitCert (C)"); cert.isCA = 1; @@ -1308,6 +1331,47 @@ static void wb_set_subject_issuer_raw(void) * BAD_FUNC_ARG guard); shown here for completeness of the wrapper. */ ret = wc_SetSubjectRaw(&cert, client_cert_der_2048, -1); WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "wc_SetSubjectRaw derSz<0 guard"); + + /* Second operand false: a subject Name whose raw encoding is longer than + * sizeof(CertName). No certificate in the corpus has one -- the whole + * struct is over a kilobyte -- and the generator cannot produce one + * either, because every CertName field is a fixed CTC_NAME_SIZE buffer. + * The guard is reached with such a length by loading a real certificate + * with wc_SetCert_LoadDer() (the same file-static wc_SetSubjectRaw() + * itself calls), writing the length into the cached DecodedCert, and + * then calling the public entry point with the SAME der pointer: the + * `cert->der != der` test at asn.c:32707 is false, so the reload is + * skipped, ret stays 0 and the enclosing `if (ret >= 0)` is entered with + * the oversized length in place. The XMEMCPY the guard protects is + * exactly what does NOT run, so nothing is overrun. */ + { + DecodedCert* dc; + + WB_CHECK(wc_InitCert(&cert) == 0, "wc_InitCert (raw C)"); + ret = wc_SetCert_LoadDer(&cert, client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, INVALID_DEVID); + WB_CHECK(ret >= 0, "wc_SetCert_LoadDer (oversized-subject fixture)"); + if (ret >= 0) { + dc = (DecodedCert*)cert.decodedCert; + dc->subjectRawLen = (int)sizeof(CertName) + 1; + ret = wc_SetSubjectRaw(&cert, client_cert_der_2048, + (int)sizeof_client_cert_der_2048); + WB_CHECK(ret >= 0, + ":32713 2nd operand false (subjectRawLen > sizeof(CertName))"); + } + + WB_CHECK(wc_InitCert(&cert) == 0, "wc_InitCert (raw D)"); + ret = wc_SetCert_LoadDer(&cert, client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, INVALID_DEVID); + if (ret >= 0) { + dc = (DecodedCert*)cert.decodedCert; + dc->subjectRawLen = (int)sizeof(CertName) + 1; + ret = wc_SetIssuerRaw(&cert, client_cert_der_2048, + (int)sizeof_client_cert_der_2048); + WB_CHECK(ret >= 0, + ":32750 2nd operand false (subjectRawLen > sizeof(CertName))"); + } + } } #else static void wb_set_subject_issuer_raw(void) @@ -1899,7 +1963,7 @@ static void wb_rsa_public_key_decode_raw(void) * RESIDUAL: :40397/:40411/:40542's "badDate" arm is only taken when * CheckDate() actually reports the acert as expired/not-yet-valid. The * corpus certs (certs/acert/acert.pem, acert_ietf.pem) are fixed test - * vectors not guaranteed to straddle "today" for the life of this campaign, + * vectors not guaranteed to straddle "today" for the life of this suite, * so only the verify-mode short-circuits (NO_VERIFY / VERIFY_SKIP_DATE) * are driven below, not a genuine bad-date trigger; doing so safely would * need a deliberately-expired ACERT DER fixture, which is left for a @@ -2740,6 +2804,169 @@ static void wb_acert_general_names(void) #endif FreeAltNames(entries, NULL); } + +/* wc_ParseX509Acert()'s AttCertIssuer dispatch + * i_issuer = (dataASN[ACERT_IDX_ACINFO_ISSUER_V2].tag != 0) ? + * ACERT_IDX_ACINFO_ISSUER_V2 : ACERT_IDX_ACINFO_ISSUER_V1; + * ... + * if (i_issuer == ACERT_IDX_ACINFO_ISSUER_V2 && issuer_len > 0) { ... } + * [:40879] + * + * AcertASN declares the two issuer forms as one CHOICE group (asn.c:40594 and + * :40596): `[0] IMPLICIT V2Form` (tag 0xA0) and the bare `GeneralNames` + * SEQUENCE (tag 0x30). Both corpus attribute certificates carry the v2Form, + * so every existing vector arrives with i_issuer == ..._ISSUER_V2 and a + * non-empty content -- the decision is always true and neither operand has an + * independence pair. + * + * Two edits of certs/acert/acert.pem produce the two missing rows, and the + * unmodified certificate supplies the true row in the same binary: + * + * - retag the AttCertIssuer element from 0xA0 to 0x30. Nothing else moves + * (the tag is one byte and the length field is untouched), the CHOICE + * then matches the v1Form alternative, and cond 0 goes false. + * - delete the whole content of the [0] element with mcdc_der_shrink(), so + * the encoding still carries an `A0 00` -- tag present, length zero. The + * template records the tag, i_issuer stays ..._ISSUER_V2, and cond 1 goes + * false with cond 1's partner (the untouched cert) true. + * + * The AttCertIssuer element is located by walking the encoding rather than by + * scanning for 0xA0: Holder itself contains [0]/[1]/[2] members, so a linear + * search would find the wrong item. AttributeCertificate ::= SEQUENCE { + * acinfo, ... }, AttributeCertificateInfo ::= SEQUENCE { version, holder, + * issuer, ... }, so the issuer is the third child of the second child of the + * root. + */ +#if defined(WOLFSSL_ACERT) && defined(WOLFSSL_ASN_TEMPLATE) +/* Offset of the AttCertIssuer element, or 0 when the walk fails. */ +static word32 wb_acert_issuer_off(const byte* der, word32 sz) +{ + word32 co, cl, lo, lw; + word32 acinfo; + word32 child; + int n; + + if (mcdc_der_hdr(der, sz, 0, &co, &cl, &lo, &lw) == 0) { + return 0; /* AttributeCertificate SEQUENCE */ + } + acinfo = co; + if (mcdc_der_hdr(der, sz, acinfo, &co, &cl, &lo, &lw) == 0) { + return 0; /* AttributeCertificateInfo SEQUENCE */ + } + child = co; + for (n = 0; n < 2; n++) { /* skip version, then holder */ + if (mcdc_der_hdr(der, sz, child, &co, &cl, &lo, &lw) == 0) { + return 0; + } + child = co + cl; + } + if (mcdc_der_hdr(der, sz, child, &co, &cl, &lo, &lw) == 0) { + return 0; + } + return child; +} + +static void wb_acert_issuer_form(void) +{ + static const char* path = "./certs/acert/acert.pem"; + byte* pem; + long pemSz = 0; + DerBuffer* der = NULL; + byte edit[2048]; + word32 sz; + word32 issuerOff; + word32 co, cl, lo, lw; + int ret; + int v; + + WB_NOTE("wc_ParseX509Acert(): AttCertIssuer v2Form/v1Form dispatch " + "[:40879]"); + + pem = wb_read_file(path, &pemSz); + if (pem == NULL) { + WB_NOTE("corpus ACERT PEM not found at runtime cwd; skipping"); + return; + } + ret = wc_PemToDer(pem, pemSz, ACERT_TYPE, &der, NULL, NULL, NULL); + XFREE(pem, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if ((ret != 0) || (der == NULL)) { + WB_CHECK(0, "wc_PemToDer ACERT (issuer-form fixture)"); + if (der != NULL) { + FreeDer(&der); + } + return; + } + if (der->length > (word32)sizeof(edit)) { + WB_NOTE("acert larger than the edit buffer; skipped"); + FreeDer(&der); + return; + } + + issuerOff = wb_acert_issuer_off(der->buffer, der->length); + WB_CHECK(issuerOff != 0, "located the AttCertIssuer element"); + if (issuerOff == 0) { + FreeDer(&der); + return; + } + WB_CHECK(der->buffer[issuerOff] == + (byte)(ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | 0), + "corpus AttCertIssuer is the v2Form [0]"); + + /* v == 0: untouched (both operands true). + * v == 1: [0] retagged to SEQUENCE -> v1Form chosen, cond 0 false. + * v == 2: [0] emptied -> tag present, issuer_len == 0, cond 1 false. */ + for (v = 0; v < 3; v++) { + WC_DECLARE_VAR(acert, DecodedAcert, 1, 0); + + sz = der->length; + XMEMCPY(edit, der->buffer, sz); + if (v == 1) { + edit[issuerOff] = ASN_SEQUENCE | ASN_CONSTRUCTED; + } + else if (v == 2) { + if (mcdc_der_hdr(edit, sz, issuerOff, &co, &cl, &lo, &lw) == 0) { + WB_CHECK(0, "AttCertIssuer header re-read"); + break; + } + if (mcdc_der_shrink(edit, &sz, co, cl) != 0) { + WB_NOTE("emptying the AttCertIssuer was refused " + "(length width change); row skipped"); + continue; + } + } + +#ifdef WOLFSSL_SMALL_STACK + acert = (DecodedAcert*)XMALLOC(sizeof(DecodedAcert), NULL, + DYNAMIC_TYPE_DCERT); + if (acert == NULL) { + WB_CHECK(0, "alloc DecodedAcert (issuer-form fixture)"); + break; + } +#else + XMEMSET(acert, 0, sizeof(DecodedAcert)); +#endif + wc_InitDecodedAcert(acert, edit, sz, NULL); + ret = wc_ParseX509Acert(acert, NO_VERIFY); + if (v == 0) { + WB_CHECK(ret == 0, ":40879 both operands true (corpus v2Form)"); + } + else if (v == 1) { + WB_CHECK(ret == 0, + ":40879 1st operand false (v1Form GeneralNames)"); + } + else { + WB_CHECK(ret == 0, + ":40879 2nd operand false (empty v2Form, issuer_len 0)"); + } + wc_FreeDecodedAcert(acert); +#ifdef WOLFSSL_SMALL_STACK + XFREE(acert, NULL, DYNAMIC_TYPE_DCERT); +#endif + } + + FreeDer(&der); +} +#endif /* WOLFSSL_ACERT && WOLFSSL_ASN_TEMPLATE */ #else static void wb_decode_holder_issuer_guards(void) { @@ -2765,6 +2992,10 @@ static void wb_parse_acert_bad_dates(void) { WB_NOTE("wc_ParseX509Acert bad-date gates (no WOLFSSL_ACERT); skipped"); } +static void wb_acert_issuer_form(void) +{ + WB_NOTE("wc_ParseX509Acert issuer form (no WOLFSSL_ACERT); skipped"); +} #endif @@ -3113,6 +3344,7 @@ int main(void) wb_parse_acert_bad_dates(); wb_acert_rsapss_params(); wb_acert_general_names(); + wb_acert_issuer_form(); wb_pem_to_der_guards(); wb_encrypted_info_parse_guards(); @@ -3120,7 +3352,7 @@ int main(void) printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_asn_ext_whitebox.c b/tests/unit-mcdc/test_asn_ext_whitebox.c index 03a451c0fc..0cc1e1aec9 100644 --- a/tests/unit-mcdc/test_asn_ext_whitebox.c +++ b/tests/unit-mcdc/test_asn_ext_whitebox.c @@ -21,7 +21,7 @@ /* * White-box MC/DC supplement for wolfcrypt/src/asn.c, "extensions" wave - * (Part 5 of the ISO 26262 MC/DC campaign): name-constraint matching, + * (Part 5 of the ISO 26262 MC/DC suite): name-constraint matching, * X.509 extension decoding, and the certificate/CSR decode core * (asn.c lines ~18537-23356 at the time this file was written). * @@ -33,7 +33,7 @@ * certificate -- the malformed/edge-case arms never fire from there. * * Coverage is unioned by source line:col with every other variant/whitebox - * in the per-module campaign; independence pairs are completed *within this + * in the per-module suite; independence pairs are completed *within this * binary*. * * Sections (asn.c line numbers as of this writing): @@ -88,10 +88,16 @@ * with certs/rsapss/server-rsapss.der but the mismatched-parameters * (false) arm would need byte-level PSS parameter surgery not attempted * here. - * - ParseCert() :23263-:23267 operands 2/3 (publicKey != NULL, - * pubKeySize > 0): once keyOID == RSAk and ParseCertRelative succeeds, - * GetCertKey always sets publicKey/pubKeySize together; only the - * all-true combination is reachable without editing library source. + * - ParseCert() :23442 operands 2/3 (publicKey != NULL, pubKeySize > 0): + * once keyOID == RSAk and ParseCertRelative succeeds, GetCertKey always + * sets publicKey/pubKeySize together; only the all-true combination is + * reachable without editing library source. Now FILED as an exclusion + * (suite the exclusion record + db/exclusions.json), together with the same + * arrival argument for ParseCertRelative()'s :24656 cond 1 and :24790 + * cond 0. The matching pubKeySize operands at :24656 cond 2 and :24790 + * cond 2 are deliberately NOT excluded: outside the RSA-family branch + * nothing validates the key body, so a zero-length SubjectPublicKeyInfo + * BIT STRING is arguably reachable there. * - DecodeCertInternal() :22815/:22821 2nd operand (issuer/subject != * NULL): both are initialised to NULL and assigned unconditionally * inside the single `if (ret == 0)` block at asn.c:22637 (:22683, @@ -118,7 +124,7 @@ /* Some leading `ret == 0` operands in this file's decoders have no crafted * input that reaches them: the only preceding statement that can set ret is a * DECL_ASNGETDATA allocation or a hash call, neither of which fails on valid - * input. Those are driven with the campaign's heap-fault injector, which is + * input. Those are driven with the heap-fault injector, which is * only effective in the WOLFSSL_SMALL_STACK variant (where the ASN.1 data * arrays and wc_ShaHash()'s context are heap-allocated); the matching TRUE * rows are issued unarmed in the same binary. */ @@ -634,6 +640,17 @@ static void wb_permitted_excluded_lists(void) { WB_NOTE("IGNORE_NAME_CONSTRAINTS * :19366-:19368 URI-without-DNS-host rejection under uriConstraintsApply * :19392 subjectDnsName fallback len>0 && name!=NULL * :19414-:19415 critical + unsupported GeneralName form -> fail closed + * + * RESIDUAL (argued unreachable, recorded in the exclusion record under + * "Condition-level exclusions"): the SECOND operand of the subjectDnsName + * fallback, `subjectDnsName.name != NULL`. subjectDnsName is XMEMSET to zero + * at the top of every nameTypes[] iteration and only three switch arms ever + * write it -- ASN_DNS_TYPE inside `cert->subjectCN != NULL`, ASN_RFC822_TYPE + * inside `cert->subjectEmail != NULL` and ASN_DIR_TYPE inside + * `cert->subjectRaw != NULL` -- each assigning the length and the pointer + * from the same object it has just tested. A non-zero .len therefore implies + * a non-NULL .name: the operand is fixed by the branch that reaches it and + * has no independence pair. * ------------------------------------------------------------------------- */ #ifndef IGNORE_NAME_CONSTRAINTS static void wb_confirm_name_constraints(void) @@ -730,6 +747,29 @@ static void wb_confirm_name_constraints(void) WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 1, "no URI constraints in force (2nd operand false, skipped)"); + /* Same URI constraints in force, but the SAN is a dNSName. The + * nameTypes[] sweep reaches ASN_DNS_TYPE with cert->altNames holding an + * entry whose type matches, so the per-entry body IS entered with + * nameType != ASN_URI_TYPE -- the only shape that makes the URI check's + * FIRST operand false. Every vector above enters that body only for + * ASN_URI_TYPE, which pins it true. */ + XMEMSET(&signer, 0, sizeof(signer)); + XMEMSET(&cert, 0, sizeof(cert)); + signer.permittedNames = wb_mk_base(NULL, ".good.com", 9, ASN_URI_TYPE); + cert.isCA = 1; /* suppresses the subjectCN fallback */ + altName = wb_mk_dns("sub.good.com", 12, ASN_DNS_TYPE); + cert.altNames = altName; + WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 1, + ":19505 1st operand false (dNSName SAN under URI constraints)"); + + /* The `subjectDnsName.name != NULL` operand of the subject fallback is a + * justified residual, not an untried row: subjectDnsName is memset to + * zero at the top of every nameTypes[] iteration and only three switch + * arms ever write it -- ASN_DNS_TYPE under `cert->subjectCN != NULL`, + * ASN_RFC822_TYPE under `cert->subjectEmail != NULL` and ASN_DIR_TYPE + * under `cert->subjectRaw != NULL` -- and each of those assigns the + * length and the pointer from the same object it has just tested. A + * non-zero .len therefore implies a non-NULL .name; see the exclusion record. */ WB_NOTE("ConfirmNameConstraints(): subjectDnsName fallback len/name [:19392]"); /* subjectEmail present -> synthetic RFC822 name len>0 && name!=NULL, * both true, checked against an excluded email base. */ @@ -1979,6 +2019,78 @@ static void wb_decode_cert_internal(void) FreeDecodedCert(&cert); } + /* --- trailing-data guard [:22749] --------------------------------- * + * `(ret == 0) && (!stopAtPubKey) && (!stopAfterPubKey) && + * (!cert->allowTrailing) && (cert->srcIdx != cert->maxIdx)` + * The decision's TRUE outcome needs a buffer that is longer than the + * certificate inside it, which no corpus file and no API caller + * supplies -- every loader hands DecodeCert() a buffer sized to the + * certificate. Calling the static decoder directly with one extra byte + * produces it, and the four other rows (one per operand) are driven + * against that same buffer so each operand alone decides the outcome. */ +#ifndef WOLFSSL_NO_ASN_STRICT + { + DecodedCert cert; + static byte trail[4096]; + int ret, crit; + word32 trailSz; + + if (origSz + 1 <= sizeof(trail)) { + XMEMCPY(trail, orig, origSz); + trail[origSz] = 0x00; + trailSz = (word32)origSz + 1; + + WB_NOTE("DecodeCertInternal(): one byte of trailing data" + " [:22749 all five operands true]"); + wc_InitDecodedCert(&cert, trail, trailSz, NULL); + ret = DecodeCertInternal(&cert, NO_VERIFY, &crit, NULL, 0, 0); + WB_CHECK(ret == WC_NO_ERR_TRACE(ASN_PARSE_E), + ":22749 trailing data rejected"); + FreeDecodedCert(&cert); + + WB_NOTE("DecodeCertInternal(): same buffer, stopAtPubKey" + " [:22749 second operand false]"); + wc_InitDecodedCert(&cert, trail, trailSz, NULL); + ret = DecodeCertInternal(&cert, NO_VERIFY, &crit, NULL, 1, 0); + /* the stopAtPubKey path returns the SubjectPublicKeyInfo offset, + * not 0 (asn.c:22964) */ + WB_CHECK(ret > 0, ":22749 stopAtPubKey ignores the trailer"); + FreeDecodedCert(&cert); + + WB_NOTE("DecodeCertInternal(): same buffer, stopAfterPubKey" + " [:22749 third operand false]"); + wc_InitDecodedCert(&cert, trail, trailSz, NULL); + ret = DecodeCertInternal(&cert, NO_VERIFY, &crit, NULL, 0, 1); + WB_CHECK(ret == 0, ":22749 stopAfterPubKey ignores the trailer"); + FreeDecodedCert(&cert); + + WB_NOTE("DecodeCertInternal(): same buffer with allowTrailing set," + " the TRUSTED CERTIFICATE shape [:22749 fourth operand" + " false]"); + wc_InitDecodedCert(&cert, trail, trailSz, NULL); + cert.allowTrailing = 1; + ret = DecodeCertInternal(&cert, NO_VERIFY, &crit, NULL, 0, 0); + WB_CHECK(ret == 0, ":22749 allowTrailing accepts the trailer"); + FreeDecodedCert(&cert); + + WB_NOTE("DecodeCertInternal(): buffer sized exactly to the" + " certificate [:22749 fifth operand false]"); + wc_InitDecodedCert(&cert, trail, (word32)origSz, NULL); + ret = DecodeCertInternal(&cert, NO_VERIFY, &crit, NULL, 0, 0); + WB_CHECK(ret == 0, ":22749 no trailing data"); + FreeDecodedCert(&cert); + + WB_NOTE("DecodeCertInternal(): truncated certificate, so the" + " guard is reached with ret already non-zero [:22749" + " leading operand false]"); + wc_InitDecodedCert(&cert, trail, (word32)(origSz / 2), NULL); + ret = DecodeCertInternal(&cert, NO_VERIFY, &crit, NULL, 0, 0); + WB_CHECK(ret != 0, ":22749 template walk failed first"); + FreeDecodedCert(&cert); + } + } +#endif /* !WOLFSSL_NO_ASN_STRICT */ + /* --- BEFORE/AFTER date propagation [:22608,:22609,:22620,:22621, * :22812] ------------------------------------------------------- */ { @@ -2456,6 +2568,70 @@ static void wb_decode_cert_req_version(void) WB_NOTE("version==0: version-check line executed with 2nd operand false " "(function may still fail later on the placeholder key material)"); (void)ret; + + /* The hand-built request above is minimal enough that the template walk + * can stop before the version guard, which leaves the guard's `ret == 0 + * && version <= MAX_X509_VERSION` row unproven. A real request settles + * it: certs/csr.signed.der parses cleanly, and the same bytes with the + * version INTEGER rewritten to 9 give the true row in this binary. */ + { + static byte realCsr[4096]; + static byte realCsrBad[4096]; + size_t realSz = 0; + word32 verIdx = 0, i; + + if (wb_load_file("./certs/csr.signed.der", realCsr, sizeof(realCsr), + &realSz) == 0 && realSz > 16) { + /* version INTEGER is the first element of the + * CertificationRequestInfo SEQUENCE, i.e. inside the first + * dozen bytes. */ + for (i = 0; i + 2 < 12; i++) { + if (realCsr[i] == 0x02 && realCsr[i + 1] == 0x01) { + verIdx = i + 2; + break; + } + } + WB_CHECK(verIdx != 0, "sanity: version INTEGER found in csr.signed.der"); + } + if (verIdx != 0) { + WB_NOTE("DecodeCertReq(): real request, version within range" + " [:23365 second operand false with ret == 0]"); + XMEMSET(&cert, 0, sizeof(cert)); + cert.source = realCsr; + cert.maxIdx = (word32)realSz; + ret = DecodeCertReq(&cert, &crit); + WB_CHECK(ret == 0, "csr.signed.der decodes"); + FreeDecodedCert(&cert); + + WB_NOTE("DecodeCertReq(): the same request with version 9" + " [:23365 both operands true]"); + XMEMCPY(realCsrBad, realCsr, realSz); + realCsrBad[verIdx] = 0x09; + XMEMSET(&cert, 0, sizeof(cert)); + cert.source = realCsrBad; + cert.maxIdx = (word32)realSz; + ret = DecodeCertReq(&cert, &crit); + WB_CHECK(ret == WC_NO_ERR_TRACE(ASN_PARSE_E), + "version 9 rejected"); + FreeDecodedCert(&cert); + + /* :23365's leading operand needs a request whose template walk + * fails outright, so the version guard is reached with ret + * already non-zero. Half the request is enough. */ + WB_NOTE("DecodeCertReq(): truncated request, template walk fails" + " [:23365 leading operand false]"); + XMEMSET(&cert, 0, sizeof(cert)); + cert.source = realCsr; + cert.maxIdx = (word32)(realSz / 2); + ret = DecodeCertReq(&cert, &crit); + WB_CHECK(ret != 0, "truncated request rejected"); + FreeDecodedCert(&cert); + } + else { + WB_NOTE("certs/csr.signed.der unavailable; real-request version" + " rows skipped"); + } + } } #else static void wb_decode_cert_req_version(void) { WB_NOTE("WOLFSSL_CERT_REQ off; skipped"); } @@ -2682,6 +2858,50 @@ static void wb_ext_error_propagation(void) #endif /* WOLFSSL_SUBJ_DIR_ATTR */ } +/* ------------------------------------------------------------------------- * + * SetDNSEntry(): the name-copy guard [:15067] + * `if (str != NULL && strLen > 0) XMEMCPY(dnsEntry_name, str, strLen);` + * + * Every production caller derives (str, strLen) from one parsed GeneralName, + * so the two are always consistent: a non-NULL pointer with a positive + * length. The guard exists for the degenerate combinations, and only a direct + * call can produce them. All three rows live in this binary: + * (F,-) str == NULL, strLen == 0 -> a zero-length entry is still created + * (T,T) a real string -> the copy runs + * (T,F) a real pointer, strLen 0 -> the copy is skipped + * ------------------------------------------------------------------------- */ +#ifndef WC_ASN_NO_HEAP +static void wb_set_dns_entry(void) +{ + DNS_entry* list = NULL; + static const char name[] = "example.com"; + int ret; + + WB_NOTE("SetDNSEntry(): str==NULL with strLen==0 [:15067 first operand" + " false]"); + ret = SetDNSEntry(NULL, NULL, NULL, NULL, 0, ASN_DNS_TYPE, &list); + WB_CHECK(ret == 0, ":15067 NULL/0 entry created"); + + WB_NOTE("SetDNSEntry(): real string, positive length [:15067 both" + " operands true]"); + ret = SetDNSEntry(NULL, NULL, NULL, name, (int)(sizeof(name) - 1), + ASN_DNS_TYPE, &list); + WB_CHECK(ret == 0, ":15067 name copied"); + + WB_NOTE("SetDNSEntry(): real string pointer with strLen==0 [:15067" + " second operand false]"); + ret = SetDNSEntry(NULL, NULL, NULL, name, 0, ASN_DNS_TYPE, &list); + WB_CHECK(ret == 0, ":15067 zero-length entry, copy skipped"); + + FreeAltNames(list, NULL); +} +#else +static void wb_set_dns_entry(void) +{ + WB_NOTE("WC_ASN_NO_HEAP build; SetDNSEntry name-copy guard skipped"); +} +#endif + int main(void) { setvbuf(stdout, NULL, _IONBF, 0); @@ -2717,10 +2937,11 @@ int main(void) wb_parse_cert_rsa_pubkey(); wb_get_decoded_cert_accessors(); wb_ext_error_propagation(); + wb_set_dns_entry(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_asn_fault_whitebox.c b/tests/unit-mcdc/test_asn_fault_whitebox.c index acbf6594fd..a9086aecc5 100644 --- a/tests/unit-mcdc/test_asn_fault_whitebox.c +++ b/tests/unit-mcdc/test_asn_fault_whitebox.c @@ -113,7 +113,7 @@ * caller passes a sigCpy of at least sigSz bytes, so both * mp_to_unsigned_bin() calls write inside the buffer and cannot fail. * - * GAPS.md rows in the deep certificate chain-verification internals + * the uncovered-condition report rows in the deep certificate chain-verification internals * (name-constraint enforcement, X.509 extension decoding/verification, * CRL/OCSP responder verification, ASN.1 dump/print) were left untouched by * this file -- they need a fully valid, parsed DecodedCert/Signer/chain @@ -1156,6 +1156,106 @@ static void wb_pem_to_der_entry_points(void) } #endif +/* ------------------------------------------------------------------------- * + * Section 18a: PemToDer()'s DES-EDE3-CBC trailing-pad trim (:26685). + * + * if (info->cipherType == WC_CIPHER_DES3) { + * if (der->length > DES_BLOCK_SIZE && + * (der->length % DES_BLOCK_SIZE) != 0) { + * padVal = der->buffer[der->length-1]; + * ... + * + * der->length here is the *base64-decoded body length* of the encrypted PEM, + * and the block is NOT guarded on wc_BufferKeyDecrypt()'s return value, so + * the three rows are chosen purely by how many bytes the body decodes to. + * No corpus PEM reaches it: every DES-EDE3-CBC key wolfSSL itself writes is + * a whole number of 8-byte blocks, which pins the second operand false, and + * section 18's encrypted fixture uses AES-128-CBC, which does not enter the + * DES3 arm at all. + * + * Three hand-built PEMs, all with the same header/IV and the same password + * callback, give the three rows in this one binary: + * 8 bytes -> der->length > DES_BLOCK_SIZE is false (cond 0 false) + * 20 bytes -> both operands true, the pad byte is read (decision true) + * 24 bytes -> length is a multiple of the block size (cond 1 false) + * The 20-byte body additionally makes wc_Des3_CbcDecryptWithKey() return + * BAD_LENGTH_E without touching the buffer (des3.c rejects a size that is + * not a multiple of DES_BLOCK_SIZE), so the byte the trim reads is the + * committed ciphertext byte 0x13 -- deterministic, and the same in every + * variant. The 8- and 24-byte bodies do decrypt, with a fixed password and a + * fixed salt, so their trim decisions are deterministic too. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_PEM_TO_DER) && defined(WOLFSSL_ENCRYPTED_KEYS) && \ + !defined(NO_DES3) && !defined(NO_WOLFSSL_SKIP_TRAILING_PAD) && \ + !defined(NO_PWDBASED) +static void wb_pem_des3_trailing_pad(void) +{ + /* Bodies are 0x00..0x07 (8 bytes), 0x00..0x13 (20) and 0x00..0x17 (24). */ + static const char pemDes3_8[] = + "-----BEGIN RSA PRIVATE KEY-----\n" + "Proc-Type: 4,ENCRYPTED\n" + "DEK-Info: DES-EDE3-CBC,0123456789ABCDEF\n" + "\n" + "AAECAwQFBgc=\n" + "-----END RSA PRIVATE KEY-----\n"; + static const char pemDes3_20[] = + "-----BEGIN RSA PRIVATE KEY-----\n" + "Proc-Type: 4,ENCRYPTED\n" + "DEK-Info: DES-EDE3-CBC,0123456789ABCDEF\n" + "\n" + "AAECAwQFBgcICQoLDA0ODxAREhM=\n" + "-----END RSA PRIVATE KEY-----\n"; + static const char pemDes3_24[] = + "-----BEGIN RSA PRIVATE KEY-----\n" + "Proc-Type: 4,ENCRYPTED\n" + "DEK-Info: DES-EDE3-CBC,0123456789ABCDEF\n" + "\n" + "AAECAwQFBgcICQoLDA0ODxAREhMUFRYX\n" + "-----END RSA PRIVATE KEY-----\n"; + static const char* shapes[3]; + static const char* names[3] = { + "8-byte body: der->length > DES_BLOCK_SIZE false (1st operand)", + "20-byte body: both operands true (pad byte read)", + "24-byte body: length is a block multiple (2nd operand false)" + }; + size_t i; + + WB_NOTE("PemToDer(): DES-EDE3-CBC trailing-pad trim [:26685]"); + + shapes[0] = pemDes3_8; + shapes[1] = pemDes3_20; + shapes[2] = pemDes3_24; + + for (i = 0; i < 3; i++) { + EncryptedInfo info; + DerBuffer* d = NULL; + int ret; + + XMEMSET(&info, 0, sizeof(info)); + info.passwd_cb = KeyPemToDerPassCb; + info.passwd_userdata = (void*)"password"; + + ret = PemToDer((const unsigned char*)shapes[i], + (long)XSTRLEN(shapes[i]), PRIVATEKEY_TYPE, &d, NULL, &info, + NULL); + /* Every one of these bodies is random data, so the parse that + * follows the decrypt always fails; what matters is that the trim + * block ran with the intended length. The 20-byte body cannot even + * be decrypted (not a block multiple), which is exactly why its + * second operand is true. */ + WB_CHECK(ret != 0 || d != NULL, names[i]); + if (d != NULL) { + FreeDer(&d); + } + } +} +#else +static void wb_pem_des3_trailing_pad(void) +{ + WB_NOTE("DES3/encrypted-keys/pad-trim not compiled in; skipped"); +} +#endif + /* ------------------------------------------------------------------------- * * Section 18b: the *remaining* PEM<->DER entry guards, each rejection vector * paired with its accepting vector in this same binary. @@ -2544,6 +2644,173 @@ static void wb_confirm_signature_dsa_sigsz(void) static void wb_confirm_signature_dsa_sigsz(void) { WB_NOTE("NO_DSA/HAVE_SELFTEST; skipped"); } #endif +/* ------------------------------------------------------------------------- * + * Section 24c: ConfirmSignature()'s RSA PKCS#1 v1.5 encode-match (:18466). + * + * if (encodedSigSz == verifySz && sigCtx->out != NULL && + * XMEMCMP(sigCtx->out, encodedSig, encodedSigSz) == 0) { ret = 0; } + * + * This is the SIG_STATE_CHECK arm of the classic (non-PSS) RSA case: the + * recovered DigestInfo is re-encoded locally and compared. All THREE operands + * were open, which says the decision had never once been true in this module + * -- no test in the asn/x509/certman groups performs a successful RSA + * certificate-signature verification. Loading a CA is not enough: for a + * self-signed CA_TYPE/TRUSTED_PEER_TYPE certificate ParseCertRelative() skips + * the whole ConfirmSignature() block (asn.c:24680), and the module's tests + * never present a leaf together with its issuer. + * + * Calling ConfirmSignature() directly with the corpus pair -- certs/ + * server-cert.der signed by certs/ca-cert.der with sha256WithRSAEncryption -- + * gives the all-true row with no RNG and no clock. Three more rows, all in + * this binary: + * + * cond 2 false: the same signature verified over a tbs whose first byte is + * flipped. The recovered DigestInfo is still 51 bytes and + * sigCtx->out is still set, so only the XMEMCMP differs. + * cond 0 false: HAVE_PK_CALLBACKS lets an RSA verify callback stand in for + * wc_RsaSSL_VerifyInline() (asn.c:18199). One that reports a + * 1-byte result makes encodedSigSz != verifySz. + * cond 1 false: the same callback reporting exactly the DigestInfo size but + * leaving *out untouched -- sigCtx->out is NULL-initialised at + * asn.c:17743 and only wc_RsaSSL_VerifyInline() ever assigns + * it, so this is the only producer of a matching size with a + * null pointer. + * ------------------------------------------------------------------------- */ +#if !defined(NO_RSA) && !defined(NO_CERTS) && !defined(NO_ASN_CRYPT) && \ + defined(HAVE_PK_CALLBACKS) && !defined(NO_SHA256) +static int wb_rsa_cb_ret = 0; + +static int wb_rsa_verify_stub(unsigned char* sig, unsigned int sigSz, + unsigned char** out, const unsigned char* keyDer, unsigned int keySz, + void* ctx) +{ + (void)sig; + (void)sigSz; + (void)keyDer; + (void)keySz; + (void)ctx; + (void)out; /* deliberately leaves *out as ConfirmSignature set it */ + return wb_rsa_cb_ret; +} + +static void wb_confirm_signature_rsa_encode(void) +{ + byte* caPem = NULL; + byte* leafPem = NULL; + long caSz = 0; + long leafSz = 0; + DecodedCert* ca = NULL; + DecodedCert* leaf = NULL; + byte* tbs = NULL; + word32 tbsSz; + byte digest[WC_SHA256_DIGEST_SIZE]; + byte encoded[MAX_ENCODED_CLASSIC_SIG_SZ]; + int encSz; + int ret; + + WB_NOTE("ConfirmSignature(): RSA PKCS#1 v1.5 encode match [:18466]"); + + caPem = wb_read_pem_file("./certs/ca-cert.der", &caSz); + leafPem = wb_read_pem_file("./certs/server-cert.der", &leafSz); + if ((caPem == NULL) || (leafPem == NULL)) { + WB_NOTE("certs/ca-cert.der or certs/server-cert.der not found; " + "encode-match rows skipped"); + XFREE(caPem, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(leafPem, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return; + } + + ca = (DecodedCert*)XMALLOC(sizeof(DecodedCert), NULL, DYNAMIC_TYPE_DCERT); + leaf = (DecodedCert*)XMALLOC(sizeof(DecodedCert), NULL, DYNAMIC_TYPE_DCERT); + if ((ca == NULL) || (leaf == NULL)) { + XFREE(ca, NULL, DYNAMIC_TYPE_DCERT); + XFREE(leaf, NULL, DYNAMIC_TYPE_DCERT); + XFREE(caPem, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(leafPem, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return; + } + + wc_InitDecodedCert(ca, caPem, (word32)caSz, NULL); + ret = ParseCert(ca, CA_TYPE, NO_VERIFY, NULL); + WB_CHECK(ret == 0, "parsed certs/ca-cert.der"); + wc_InitDecodedCert(leaf, leafPem, (word32)leafSz, NULL); + if (ret == 0) { + ret = ParseCert(leaf, CERT_TYPE, NO_VERIFY, NULL); + WB_CHECK(ret == 0, "parsed certs/server-cert.der"); + } + + if ((ret == 0) && (leaf->sigIndex > leaf->certBegin) && + (ca->publicKey != NULL) && (ca->pubKeySize > 0)) { + SignatureCtx sigCtx; + + tbsSz = leaf->sigIndex - leaf->certBegin; + + /* Row 1: the real pair -> all three operands true. */ + InitSignatureCtx(&sigCtx, NULL, INVALID_DEVID); + ret = ConfirmSignature(&sigCtx, leaf->source + leaf->certBegin, tbsSz, + ca->publicKey, ca->pubKeySize, ca->keyOID, leaf->signature, + leaf->sigLength, leaf->signatureOID, NULL, 0, NULL); + WB_CHECK(ret == 0, ":18466 all three operands true (valid signature)"); + FreeSignatureCtx(&sigCtx); + + /* Row 2: same signature, one tbs byte flipped -> only the XMEMCMP + * differs, so cond 0 and cond 1 stay true and cond 2 goes false. */ + tbs = (byte*)XMALLOC(tbsSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (tbs != NULL) { + XMEMCPY(tbs, leaf->source + leaf->certBegin, tbsSz); + tbs[tbsSz - 1] ^= 0xFFU; + InitSignatureCtx(&sigCtx, NULL, INVALID_DEVID); + ret = ConfirmSignature(&sigCtx, tbs, tbsSz, ca->publicKey, + ca->pubKeySize, ca->keyOID, leaf->signature, + leaf->sigLength, leaf->signatureOID, NULL, 0, NULL); + WB_CHECK(ret != 0, ":18466 3rd operand false (tampered tbs)"); + FreeSignatureCtx(&sigCtx); + XFREE(tbs, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + + /* Row 3: a verify callback that reports a 1-byte result -> the sizes + * disagree and cond 0 goes false. */ + wb_rsa_cb_ret = 1; + InitSignatureCtx(&sigCtx, NULL, INVALID_DEVID); + sigCtx.pkCbRsa = wb_rsa_verify_stub; + ret = ConfirmSignature(&sigCtx, leaf->source + leaf->certBegin, tbsSz, + ca->publicKey, ca->pubKeySize, ca->keyOID, leaf->signature, + leaf->sigLength, leaf->signatureOID, NULL, 0, NULL); + WB_CHECK(ret != 0, ":18466 1st operand false (size mismatch)"); + FreeSignatureCtx(&sigCtx); + + /* Row 4: the same callback reporting exactly the DigestInfo size the + * CHECK stage re-encodes, with *out left NULL -> cond 0 true, + * cond 1 false. */ + XMEMSET(digest, 0, sizeof(digest)); + encSz = (int)wc_EncodeSignature(encoded, digest, + WC_SHA256_DIGEST_SIZE, SHA256h); + WB_CHECK(encSz > 0, "sized the SHA-256 DigestInfo"); + wb_rsa_cb_ret = encSz; + InitSignatureCtx(&sigCtx, NULL, INVALID_DEVID); + sigCtx.pkCbRsa = wb_rsa_verify_stub; + ret = ConfirmSignature(&sigCtx, leaf->source + leaf->certBegin, tbsSz, + ca->publicKey, ca->pubKeySize, ca->keyOID, leaf->signature, + leaf->sigLength, leaf->signatureOID, NULL, 0, NULL); + WB_CHECK(ret != 0, ":18466 2nd operand false (out left NULL)"); + FreeSignatureCtx(&sigCtx); + wb_rsa_cb_ret = 0; + } + + wc_FreeDecodedCert(leaf); + wc_FreeDecodedCert(ca); + XFREE(leaf, NULL, DYNAMIC_TYPE_DCERT); + XFREE(ca, NULL, DYNAMIC_TYPE_DCERT); + XFREE(caPem, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(leafPem, NULL, DYNAMIC_TYPE_TMP_BUFFER); +} +#else +static void wb_confirm_signature_rsa_encode(void) +{ + WB_NOTE("RSA/PK-callbacks/SHA-256 not compiled in; :18466 rows skipped"); +} +#endif + /* ------------------------------------------------------------------------- * * Section 25: leading `ret == 0` operand of the encoders' "is the caller's * buffer big enough" guards. @@ -2783,6 +3050,7 @@ int main(void) wb_get_pubkey_der_from_cert_null_args(); wb_encrypted_info_get_null_args(); wb_pem_to_der_entry_points(); + wb_pem_des3_trailing_pad(); wb_pem_der_remaining_guards(); wb_parse_key_usage_str_null_args(); wb_cert_file_setters_null_args(); @@ -2797,12 +3065,13 @@ int main(void) wb_decode_cert_extensions_unknown_cb(); wb_parse_alloc_sweep(); wb_confirm_signature_dsa_sigsz(); + wb_confirm_signature_rsa_encode(); wb_encoder_size_guards(); wb_decode_dsa_asn1_sig_alloc(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_asn_keys_whitebox.c b/tests/unit-mcdc/test_asn_keys_whitebox.c index 40f53b57a4..bdb17f5e32 100644 --- a/tests/unit-mcdc/test_asn_keys_whitebox.c +++ b/tests/unit-mcdc/test_asn_keys_whitebox.c @@ -728,6 +728,13 @@ static void wb_get_key_oid(void) { WB_NOTE("HAVE_PKCS8/12 off; wc_GetKeyOID skip /* ======================================================================== * Section A10: wc_EncryptPKCS8Key_ex() argument/salt/version checks. + * + * ARGUED UNREACHABLE, do not re-open (suite the exclusion record + + * db/exclusions.json): :10805 BOTH operands. GetAlgoV2() (asn.c:10713-10748) + * assigns *oid on every switch arm that returns 0 and leaves the caller's + * initialiser untouched only on the default arm, which returns ALGO_ID_E. + * encOid == NULL at :10805 therefore implies ret != 0, so the AND is never + * true: cond 1 has no true row and cond 0 has no (true, true) row. * :10724 key==NULL||outSz==NULL||password==NULL * :10731 ret==0 && (salt==NULL||saltSz==0) * :10735 ret==0 && version==PKCS5v2 @@ -737,6 +744,7 @@ static void wb_encrypt_pkcs8_key_ex(void) { byte key[16]; byte salt[8]; + static byte encOut[512]; word32 outSz; int ret; @@ -800,7 +808,50 @@ static void wb_encrypt_pkcs8_key_ex(void) PBES2, AES128CBCb, salt, sizeof(salt), 1000, 0, NULL, NULL); WB_CHECK(ret != 0, ":11531 both operands true (PBES2 dispatch)"); #endif + + /* :10799 third operand (`saltSz == 0`). Every public caller passes a + * salt pointer together with its real length, or neither; a non-NULL + * pointer with a zero length is the combination the OR's second operand + * exists for. The salt-provided call above is the row it pairs against. */ + WB_NOTE("wc_EncryptPKCS8Key_ex(): salt pointer with saltSz==0 [:10799" + " third operand]"); + outSz = 0; + ret = wc_EncryptPKCS8Key_ex(key, sizeof(key), NULL, &outSz, "pw", 2, + PKCS5, PBES1_SHA1_DES, 0, salt, 0, 1000, 0, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(LENGTH_ONLY_E), + ":10799 salt != NULL with saltSz == 0 still generates a salt"); + + /* :11650 second operand's false row. The size-only calls above all set + * ret to LENGTH_ONLY_E at the preceding `out == NULL` branch, so they + * never reach this check with ret == 0; only a real encode with a large + * enough buffer does. PBES1 needs no RNG (the CBC IV is derived from the + * password), so this runs with rng == NULL. */ + WB_NOTE("EncryptContent(): full encode into a big-enough buffer [:11650" + " second operand false]"); + outSz = 0; + (void)EncryptContent(key, sizeof(key), NULL, &outSz, "pw", 2, PKCS5, + PBES1_SHA1_DES, 0, salt, sizeof(salt), 1000, 0, NULL, NULL); + if (outSz > 1 && outSz <= sizeof(encOut)) { + word32 room = outSz - 1; + + /* Both halves of :11650's second operand have to be in THIS binary: + * the too-small row lives in test_asn_fault_whitebox.c as well, but + * a pair completed across two binaries proves nothing. */ + ret = EncryptContent(key, sizeof(key), encOut, &room, "pw", 2, PKCS5, + PBES1_SHA1_DES, 0, salt, sizeof(salt), 1000, 0, NULL, NULL); + WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), + ":11650 second operand true (one byte short)"); + + room = (word32)sizeof(encOut); + ret = EncryptContent(key, sizeof(key), encOut, &room, "pw", 2, PKCS5, + PBES1_SHA1_DES, 0, salt, sizeof(salt), 1000, 0, NULL, NULL); + WB_CHECK(ret > 0, ":11650 PBES1 encode succeeds with room to spare"); + } + else { + WB_NOTE("PBES1 size query out of range; :11650 row skipped"); + } } + #else static void wb_encrypt_pkcs8_key_ex(void) { WB_NOTE("HAVE_PKCS8/PWDBASED off; wc_EncryptPKCS8Key_ex skipped"); } #endif @@ -969,6 +1020,35 @@ static void wb_encrypt_content_pbes2(void) WB_CHECK(ret == WC_NO_ERR_TRACE(BAD_FUNC_ARG), "out!=NULL, *outSz too small (11378 false, 11383 true)"); } + + /* :11466's second operand also needs the row where the buffer IS big + * enough, i.e. a completed encode. That is the only call in this section + * that gets past the size check, and it is also the only one that needs + * a WC_RNG: the PBES2 path draws the CBC IV before the check. The random + * bytes are written straight into the output and steer no decision, so + * the coverage this row produces is reproducible. */ + { + static byte bigOut[1024]; + word32 bigOutSz; + WC_RNG rng; + + outSz = 0; + (void)EncryptContentPBES2(input, sizeof(input), NULL, &outSz, "pw", 2, + AES128CBCb, salt, sizeof(salt), 1000, 0, NULL, NULL); + if (outSz > 0 && outSz <= sizeof(bigOut) && wc_InitRng(&rng) == 0) { + WB_NOTE("EncryptContentPBES2(): buffer large enough, encode runs" + " [:11466 second operand false]"); + bigOutSz = (word32)sizeof(bigOut); + ret = EncryptContentPBES2(input, sizeof(input), bigOut, &bigOutSz, + "pw", 2, AES128CBCb, salt, sizeof(salt), 1000, 0, &rng, + NULL); + WB_CHECK(ret > 0, ":11466 PBES2 encode completes"); + wc_FreeRng(&rng); + } + else { + WB_NOTE("no RNG or size out of range; :11466 encode row skipped"); + } + } #endif } #else @@ -1961,6 +2041,61 @@ static void wb_build_ecc_key_der(void) ret = wc_BuildEccKeyDer(&key, out, NULL, 1, 1); WB_CHECK(ret > 0, ":33566 2nd operand false (outLen==NULL, size check skipped)"); + /* :33780's leading operand (`ret == 0`) can only go false when the + * private-value export at :33772 itself errors, which no key that got + * this far normally does. A key decoded from a public-key SPKI has a dp + * and a public point but no private scalar, so the export fails while + * every earlier step succeeds. The successful build above is the row it + * pairs against. */ + WB_NOTE("wc_BuildEccKeyDer(): public-only key, private export fails" + " before the public-point export [:33780 leading operand]"); + { + ecc_key pubOnly; + word32 pubIdx = 0; + + if (wc_ecc_init(&pubOnly) == 0) { + if (wc_EccPublicKeyDecode(ecc_key_pub_der_256, &pubIdx, &pubOnly, + (word32)sizeof_ecc_key_pub_der_256) == 0) { + outLen = sizeof(out); + ret = wc_BuildEccKeyDer(&pubOnly, out, &outLen, 1, 1); + WB_CHECK(ret != 0, + ":33780 private-value export fails on a public-only" + " key"); + } + else { + WB_NOTE("ecc_key_pub_der_256 decode failed; :33780 row" + " skipped"); + } + wc_ecc_free(&pubOnly); + } + } + + /* :33537's second operand (`dataASN[ECCKEYASN_IDX_PARAMS].tag != 0`). + * Every ECC private key in certs_test.h carries the [0] parameters, so + * the operand only ever reads true. Building one with curveIn == 0 emits + * exactly the same structure minus the parameters, and decoding it in + * this binary supplies the false row. */ + WB_NOTE("wc_EccPrivateKeyDecode(): key DER built without the [0] curve" + " parameters [:33537 second operand false]"); + outLen = sizeof(out); + ret = wc_BuildEccKeyDer(&key, out, &outLen, 1, 0); + if (ret > 0) { + ecc_key noParams; + word32 npIdx = 0; + word32 derSz = (word32)ret; + + if (wc_ecc_init(&noParams) == 0) { + /* The decode cannot succeed without a curve to attach the key + * to; reaching the guard with the PARAMS tag clear is the + * point. */ + (void)wc_EccPrivateKeyDecode(out, &npIdx, &noParams, derSz); + wc_ecc_free(&noParams); + } + } + else { + WB_NOTE("wc_BuildEccKeyDer(curveIn==0) failed; :33537 row skipped"); + } + wc_ecc_free(&key); } @@ -2655,7 +2790,7 @@ int main(void) printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_asn_revocation_whitebox.c b/tests/unit-mcdc/test_asn_revocation_whitebox.c index 02c53d4d1e..e9e1bb85ce 100644 --- a/tests/unit-mcdc/test_asn_revocation_whitebox.c +++ b/tests/unit-mcdc/test_asn_revocation_whitebox.c @@ -35,21 +35,28 @@ * helpers below, rather than typed-out byte arrays) plus a few real * certificate/key buffers borrowed from existing test fixtures. * - * NOTE on HAVE_OCSP_RESPONDER: the asn campaign's config_base + * NOTE on HAVE_OCSP_RESPONDER: the asn suite's config_base * (configs/asn/user_settings.base.h) never defines HAVE_OCSP_RESPONDER, so * EncodeCertID/EncodeSingleResponse/EncodeResponseData/EncodeBasicOcspResponse/ * OcspResponseEncode are compiled out for every variant of this module and - * GAPS.md carries no lines inside them -- this file does not attempt to + * the uncovered-condition report carries no lines inside them -- this file does not attempt to * cover that side and never needs it to build test input (all decode-side * buffers below are constructed by hand instead of via a round trip). * * Coverage is unioned by source line:col with the tests/api asn/ocsp run in - * the per-module campaign; every pair below is completed *within this file* + * the per-module suite; every pair below is completed *within this file* * (masking MC/DC is computed per binary, then ORed across binaries by key). */ +/* Installed BEFORE asn.c is #included: it interposes the two scratch-mp_int + * lifecycle macros so ParseCRL_Extensions()'s CRL-number branch can be made + * to fail its NEW_MP_INT_SIZE()/INIT_MP_INT_SIZE() pair. See section 12. */ +#include "mcdc_fault_mpint.h" + #include +#include "mcdc_fault_alloc.h" + #include #include @@ -279,7 +286,7 @@ static word32 wb_build_single_response(byte* out, * :35012/35013 if ((!AsnSkipDateCheck) && !XVALIDATE_DATE(nextDate, ..., ASN_AFTER, ...)) * :35021/35022 (WOLFSSL_OCSP_PARSE_STATUS) duplicate of :35006/35007 * AsnSkipDateCheck is a compile-time constant 0 unless - * WC_ASN_RUNTIME_DATE_CHECK_CONTROL is defined (not set for this campaign), + * WC_ASN_RUNTIME_DATE_CHECK_CONTROL is defined (not set for this suite), * so its "true" (skip) value is a structural residual here; only the * XVALIDATE_DATE operand is driven both ways. * ------------------------------------------------------------------------- */ @@ -1192,7 +1199,7 @@ static void wb_compare_ocsp_req_resp(void) * Section 11: ParseCRL_EntryExtensions() [:36841,:36842,:36854-:36856, * :36863,:36864,:36877,:36878,:36881,:36882,:36891,:36892,:36917,:36921, * :36935] - * WC_ASN_UNKNOWN_EXT_CB is active for this campaign (WOLFSSL_ASN_ALL pulls + * WC_ASN_UNKNOWN_EXT_CB is active for this suite (WOLFSSL_ASN_ALL pulls * in WOLFSSL_CUSTOM_OID + HAVE_OID_DECODING + WOLFSSL_ASN_TEMPLATE), so the * callback-dispatch branch is live, not compiled out. * ------------------------------------------------------------------------- */ @@ -1535,25 +1542,40 @@ static void wb_parse_crl_entry_extensions(void) { WB_NOTE("HAVE_CRL off or WOLFC * [:37284,:37285(idx 2,3),:37325,:37326,:37335,:37349,:37358,:37359,:37384, * :37407(idx1)] * - * Two adjacent conditions are ARGUED UNREACHABLE (not attempted below): - * - the CRL-number branch's "if (ret == 0 && (INIT_MP_INT_SIZE(...) != - * MP_OKAY))" guard: outside WOLFSSL_SMALL_STACK, DECL_MP_INT_SIZE_DYN - * stack-allocates `m` unconditionally (MP_INT_SIZE_CHECK_NULL is only - * defined under WOLFSSL_SMALL_STACK), so ret==0 never goes false here - * without a heap-allocation fault injector; INIT_MP_INT_SIZE() on that - * buffer is a plain mp_init() that cannot itself fail. Both operands - * are therefore compile-time constant in every variant this white-box - * runs under. - * - "if (ret == 0 && mp_toradix(m, dcrl->crlNumber, MP_RADIX_HEX) != - * MP_OKAY)": dcrl->crlNumber is CRL_MAX_NUM_HEX_STR_SZ bytes - * (CRL_MAX_NUM_SZ*2+1 = 41), which is exactly the space a maximum-size - * (20-byte, CRL_MAX_NUM_SZ) positive CRL number's hex-radix conversion - * needs -- verified empirically with a 20-byte value (0x7F followed by - * 19 bytes of 0xFF, the largest positive value the preceding size/sign - * checks admit): mp_toradix() still succeeds. ret==0 is also always - * true reaching this line (nothing between the two checks can set it - * nonzero without the residual above already applying), so this - * decision's operands are constant too. + * The CRL-number branch's scratch-mp_int guard + * NEW_MP_INT_SIZE(m, ...); + * #ifdef MP_INT_SIZE_CHECK_NULL + * if (m == NULL) ret = MEMORY_E; + * #endif + * if (ret == 0 && (INIT_MP_INT_SIZE(m, CRL_MAX_NUM_SZ * CHAR_BIT) + * != MP_OKAY)) ret = MP_INIT_E; [:37680] + * was previously recorded here as unreachable in both operands, on the + * grounds that outside WOLFSSL_SMALL_STACK `m` is stack storage that cannot + * be NULL and INIT_MP_INT_SIZE() on it cannot fail. Both halves of that are + * true of the PRODUCT, and neither is a reason the operands have no + * independence pair -- they are exactly what a fault injector exists for. + * mcdc_fault_mpint.h (included above, before asn.c) interposes both macros: + * - mcdc_fmi_arm_new(1) leaves `m` NULL and, because the header also + * defines MP_INT_SIZE_CHECK_NULL, compiles the caller's own NULL guard, + * so ret carries the memory-allocation error on arrival and cond 0 + * goes false; + * - mcdc_fmi_arm_init(1) makes INIT_MP_INT_SIZE() report MP_VAL, which is + * the decision's only true row and therefore the partner BOTH operands + * need; + * - the unarmed vector at the top of this section supplies cond 1's false + * row. All three are in this binary. + * + * The following guard + * if (ret == 0 && mp_toradix(m, (char*)dcrl->crlNumber, MP_RADIX_HEX) + * != MP_OKAY) [:37713] + * IS argued unreachable, and now from the source rather than empirically: + * mp_toradix() is sp_toradix() (sp_int.h:1437), which for MP_RADIX_HEX + * returns anything other than MP_OKAY only when `a` or `str` is NULL + * (sp_int.c:18962 and sp_tohex() at :18782 -- the conversion itself has no + * other failure path). `m` is non-NULL on every arrival (the NULL case sets + * ret at :37676, so cond 0 short-circuits) and dcrl->crlNumber is an array + * member of DecodedCRL, so the decision is never true and neither operand + * pairs. Recorded in the exclusion record. * ------------------------------------------------------------------------- */ static word32 wb_build_crl_number_ext(byte* out, const byte* intContent, word32 intContentSz) @@ -1662,7 +1684,7 @@ static void wb_parse_crl_extensions(void) /* Duplicate CRL_NUMBER_OID extensions -> :37284/:37285 CRL_NUMBER_OID * term both true on the 2nd occurrence (WOLFSSL_NO_ASN_STRICT is not - * defined for this campaign, so strict duplicate rejection applies). */ + * defined for this suite, so strict duplicate rejection applies). */ InitDecodedCRL(&dcrl, NULL); { byte ext1[64], ext2[64]; @@ -1694,7 +1716,7 @@ static void wb_parse_crl_extensions(void) #ifndef WC_ASN_UNKNOWN_EXT_CB /* Only reachable as "handled==0" residual note when the callback - * feature is compiled out; this campaign has it on (see below), kept + * feature is compiled out; this suite has it on (see below), kept * here for portability to a variant that does not. */ InitDecodedCRL(&dcrl, NULL); { @@ -1796,6 +1818,51 @@ static void wb_parse_crl_extensions(void) FreeDecodedCRL(&dcrl); } #endif /* !WC_ASN_UNKNOWN_EXT_CB */ + + /* :37680 -- the scratch-mp_int lifecycle guard. The valid small CRL + * number at the top of this function is the unarmed partner (cond 1 + * false); these two are the faulted rows. Both use the same fixture so + * nothing but the injector differs. */ + { + byte val = 0x05; + + sz = wb_build_crl_number_ext(extList, &val, 1); + + /* INIT_MP_INT_SIZE() reports failure -> the decision's only true + * row, which is the partner both operands need. */ + if (mcdc_fmi_init_available()) { + InitDecodedCRL(&dcrl, NULL); + mcdc_fmi_arm_init(1); + ret = ParseCRL_Extensions(&dcrl, extList, 0, sz); + mcdc_fmi_disarm(); + WB_CHECK(ret != 0 && dcrl.crlNumberSet == 0, + ":37680 both operands true (INIT_MP_INT_SIZE faulted)"); + FreeDecodedCRL(&dcrl); + } + else { + WB_NOTE("INIT_MP_INT_SIZE lever unavailable; :37680 true row " + "skipped"); + } + + /* NEW_MP_INT_SIZE() leaves `m` NULL, the caller's own guard sets + * ret = MEMORY_E, so cond 0 is false. Only available where + * DECL_MP_INT_SIZE_DYN declares an assignable pointer -- under + * WOLFSSL_SMALL_STACK the allocation is real and this row is + * contributed by the other variants. */ + if (mcdc_fmi_new_available()) { + InitDecodedCRL(&dcrl, NULL); + mcdc_fmi_arm_new(1); + ret = ParseCRL_Extensions(&dcrl, extList, 0, sz); + mcdc_fmi_disarm(); + WB_CHECK(ret != 0 && dcrl.crlNumberSet == 0, + ":37680 1st operand false (NEW_MP_INT_SIZE faulted)"); + FreeDecodedCRL(&dcrl); + } + else { + WB_NOTE("NEW_MP_INT_SIZE lever unavailable in this build; " + ":37680 1st-operand row skipped"); + } + } } #else static void wb_parse_crl_extensions(void) { WB_NOTE("HAVE_CRL/ASN_TEMPLATE off; ParseCRL_Extensions skipped"); } @@ -2548,8 +2615,78 @@ static void wb_make_crl_ex(void) { WB_NOTE("HAVE_OCSP off; skipped"); } static void wb_sign_crl(void) { WB_NOTE("HAVE_OCSP off; skipped"); } #endif /* HAVE_OCSP && !WOLFCRYPT_ONLY */ +/* ------------------------------------------------------------------------- * + * GetRevoked(): the cleanup guard [:37416] + * `if ((ret != 0) && (rc != NULL)) { ... free rc ... }` + * + * The second operand's false row needs a failure in which `rc` was never + * allocated, i.e. the RevokedCert XMALLOC at :37337 itself returning NULL. + * No input can produce that, so the allocator is faulted for exactly that + * one allocation. The paired true row is an ordinary parse failure with the + * allocation intact. + * ------------------------------------------------------------------------- */ +#ifndef CRL_STATIC_REVOKED_LIST +static void wb_get_revoked_cleanup(void) +{ + /* Revoked ::= SEQUENCE { userCertificate INTEGER, revocationDate Time } */ + static byte revoked[] = { + 0x30, 0x12, + 0x02, 0x01, 0x01, + 0x17, 0x0D, '2','5','0','1','0','1','0','0','0','0','0','0','Z' + }; + DecodedCRL dcrl; + word32 idx; + int ret; + + WB_NOTE("GetRevoked(): parse failure with the RevokedCert allocated" + " [:37416 both operands true]"); + XMEMSET(&dcrl, 0, sizeof(dcrl)); + idx = 0; + /* maxIdx cuts the entry in half, so GetASN_Items fails after the + * allocation has already succeeded. */ + ret = GetRevoked(NULL, revoked, &idx, &dcrl, (word32)sizeof(revoked) / 2); + WB_CHECK(ret != 0, ":37416 truncated Revoked entry rejected"); + + WB_NOTE("GetRevoked(): the RevokedCert allocation itself fails, so the" + " cleanup guard sees a NULL pointer [:37416 second operand" + " false]"); + mcdc_fa_install(); + mcdc_fa_disarm(); + mcdc_fa_arm_only(1); + XMEMSET(&dcrl, 0, sizeof(dcrl)); + idx = 0; + ret = GetRevoked(NULL, revoked, &idx, &dcrl, (word32)sizeof(revoked)); + mcdc_fa_disarm(); + mcdc_fa_restore(); + WB_CHECK(ret == WC_NO_ERR_TRACE(MEMORY_E), + ":37416 RevokedCert allocation failed"); + + WB_NOTE("GetRevoked(): a well-formed entry, so the guard's leading" + " operand is false [:37416]"); + XMEMSET(&dcrl, 0, sizeof(dcrl)); + idx = 0; + ret = GetRevoked(NULL, revoked, &idx, &dcrl, (word32)sizeof(revoked)); + WB_CHECK(ret == 0, ":37416 well-formed Revoked entry accepted"); + if (ret == 0) { + RevokedCert* rc = dcrl.certs; + while (rc != NULL) { + RevokedCert* next = rc->next; + XFREE(rc, dcrl.heap, DYNAMIC_TYPE_CRL); + rc = next; + } + dcrl.certs = NULL; + } +} +#else +static void wb_get_revoked_cleanup(void) +{ + WB_NOTE("CRL_STATIC_REVOKED_LIST; GetRevoked cleanup guard skipped"); +} +#endif + int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("asn.c revocation (OCSP/CRL) white-box MC/DC supplement\n"); wb_ocsp_decode_certid(); @@ -2569,10 +2706,11 @@ int main(void) wb_encode_crl_serial(); wb_make_crl_ex(); wb_sign_crl(); + wb_get_revoked_cleanup(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_asn_whitebox.c b/tests/unit-mcdc/test_asn_whitebox.c index 7c0bbc9d1a..8f0fedf849 100644 --- a/tests/unit-mcdc/test_asn_whitebox.c +++ b/tests/unit-mcdc/test_asn_whitebox.c @@ -33,7 +33,7 @@ * minimal custom ASNItem templates. * * Coverage is unioned by source line:col with the tests/api asn/x509/... run - * in the per-module campaign; every pair below is completed *within this + * in the per-module suite; every pair below is completed *within this * file* (masking MC/DC is computed per binary). * * Sections (asn.c line numbers as of this writing): @@ -74,7 +74,7 @@ * itself cannot show a false outcome. * - GetOID() :7960 first operand (`ret == 0`): ret is a local initialised * to 0 at asn.c:7873 and assigned only inside `#ifdef ASN_DUMP_OID` - * (:7952), which settings.h never defines and no campaign variant sets. + * (:7952), which settings.h never defines and no suite variant sets. * Constant true. The other three operands are driven below with two * hand-computed wc_oid_sum() collisions. * - wc_Asn1_PrintAll() :39680 both operands: every path through @@ -1215,6 +1215,7 @@ static void wb_asn1_print_all(void) { WB_NOTE("WOLFSSL_ASN_PRINT off; skipped"); int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("asn.c white-box MC/DC supplement\n"); wb_get_asn_tag(); @@ -1236,7 +1237,7 @@ int main(void) printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_chacha20_poly1305_whitebox.c b/tests/unit-mcdc/test_chacha20_poly1305_whitebox.c index a0f1d36517..fc1f3a758a 100644 --- a/tests/unit-mcdc/test_chacha20_poly1305_whitebox.c +++ b/tests/unit-mcdc/test_chacha20_poly1305_whitebox.c @@ -50,7 +50,7 @@ * guards (encrypt and decrypt arms), "len > 0" FALSE half: a zero-length * data update, valid but unused by the tests. * - * NOT closable, recorded in campaign/db/exclusions.json (three conditions, + * NOT closable, recorded in the exclusion record (three conditions, * one argument): * * :943 if (ret == 0 && aead->state == CHACHA20_POLY1305_STATE_AAD) @@ -80,7 +80,7 @@ * * No ciphertext or tag value is checked here: the AEAD KATs own correctness, * this file only drives the decisions. Every failure is reported as a skip and - * main() always returns 0 -- a non-zero exit makes the campaign discard the + * main() always returns 0 -- a non-zero exit makes the harness discard the * whole variant. */ @@ -320,7 +320,7 @@ int main(void) printf("chacha20_poly1305.c white-box MC/DC supplement\n"); wb_run(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_chacha_whitebox.c b/tests/unit-mcdc/test_chacha_whitebox.c index 96f14366b1..18ff0d538b 100644 --- a/tests/unit-mcdc/test_chacha_whitebox.c +++ b/tests/unit-mcdc/test_chacha_whitebox.c @@ -30,10 +30,10 @@ * else { chacha_encrypt_x64(...); return 0; } * * Each of these is a single-condition branch (not a compound MC/DC decision: - * chacha.c's own db/modules.json-measured MC/DC total is unaffected by which + * chacha.c's own the module registry-measured MC/DC total is unaffected by which * of these paths a given build takes), so this white-box does not change the - * campaign's covered/total counts. It is kept anyway, matching the intel- - * dispatch technique used by the aes/sha3 white-boxes and this campaign's + * suite's covered/total counts. It is kept anyway, matching the intel- + * dispatch technique used by the aes/sha3 white-boxes and this suite's * poly1305 sibling, for FEATURE/branch-coverage evidence that the AVX2-false * sides (AVX1-only and the generic x64 fallback) are reachable and correct: * on an AVX2-capable CI host, cpuid_get_flags_ex()'s real detection always @@ -46,7 +46,7 @@ * before calling wc_Chacha_Process() makes it trust our forced value instead * of re-detecting. Crash-safety: we only ever CLEAR capability bits the real * host does not actually have removed either -- this host has both AVX1 and - * AVX2 hardware (see db/modules.json chacha notes), so forcing cpuidFlags to + * AVX2 hardware (see the module registry chacha notes), so forcing cpuidFlags to * "AVX1 only" or "neither" and letting the dispatch call the real * chacha_encrypt_avx1/chacha_encrypt_x64 asm is always safe: we never claim * a capability the CPU lacks, only hide one it has. @@ -307,7 +307,7 @@ int main(void) wb_chacha_predicate(); wb_chacha_avx512_dispatch(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_cmac_whitebox.c b/tests/unit-mcdc/test_cmac_whitebox.c index dda9b09cd9..18112d60d9 100644 --- a/tests/unit-mcdc/test_cmac_whitebox.c +++ b/tests/unit-mcdc/test_cmac_whitebox.c @@ -197,7 +197,7 @@ int main(void) wb_cross_combos(); 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 + * suite treats a nonzero exit as a failed variant and discards its * coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_curve25519_whitebox.c b/tests/unit-mcdc/test_curve25519_whitebox.c index 82d01e5e9a..ec378a765e 100644 --- a/tests/unit-mcdc/test_curve25519_whitebox.c +++ b/tests/unit-mcdc/test_curve25519_whitebox.c @@ -16,8 +16,8 @@ * helpers with both halves of each MC/DC independence pair. * * 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 + * by source line:col in the per-module suite: + * llvm-cov computes MC/DC independence PER BINARY, and the * 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. @@ -25,7 +25,7 @@ * Only meaningful under WC_X25519_NONBLOCK (requires CURVE25519_SMALL, * per curve25519.c's own top-of-file doc comment); a no-op elsewhere. * - * Build: compiled by run-mcdc-par.sh's white-box step with the SAME MC/DC + * Build: compiled by the coverage runner'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 curve25519.o * removed (this TU supplies the instrumented curve25519.c). NOT part of @@ -40,21 +40,65 @@ * These are the only curve25519.c gaps confirmed structurally unreachable * through the public API: wc_curve25519_make_key() pre-validates key/rng * non-NULL identically before ever calling either static, and only enters - * either with ret==0 already true. See the campaign's RESIDUALS.md for + * either with ret==0 already true. See the RESIDUALS.md for * everything else (notably curve25519_smul_blind()'s RNG-retry loop, which * needs a controllable/mockable RNG to force its rare all-0xff/large-first- * byte draw and is left as a structural residual, matching the ecc.c - * campaign's Tonelli-Shanks precedent). + * suite's Tonelli-Shanks precedent). */ /* Pull curve25519.c in verbatim so the file-static helpers below are in * scope and instrumented in THIS binary. curve25519.c includes settings.h * (which picks up user_settings.h via -DWOLFSSL_USER_SETTINGS) and * curve25519.h itself. */ +/* ---- wc_RNG_GenerateBlock() interposer ---------------------------------- + * + * curve25519_smul_blind()'s blinding-value rejection loop (:279-:293) draws a + * fresh rz until it is acceptable. Its guard + * + * if ((i >= 0) || (rz[0] <= 0xec)) break; + * + * only takes "i >= 0" FALSE when EVERY byte of rz is 0xff -- a 2^-256 event + * that no seeded stream can be relied on to produce (and rule 3 of this + * suite forbids evidence that depends on a live draw). Interposing + * wc_RNG_GenerateBlock() for THIS translation unit lets one scripted draw + * return 32 x 0xff, so the loop's first iteration evaluates the guard with + * i == -1, while the second (unscripted) draw ends the loop normally -- both + * halves of the idx0 pair in one call, and cnt never reaches + * WOLFSSL_CURVE25519_BLINDING_RAND_CNT, so no RNG_FAILURE_E bail-out. + * + * random.h is included and the hook declared FIRST so the macro never has to + * rewrite random.h's own prototype (see the same note in mcdc_seed_rng.h: an + * undeclared hook is a compile failure, which the harness scores as a silent + * skip). The hook's body sits after the #undef, so it still reaches the real + * RNG when the script is idle. + */ +#include +#include + +static int wb_c25519_rng_block(WC_RNG* rng, byte* out, word32 sz); + +#define wc_RNG_GenerateBlock(rng, out, sz) wb_c25519_rng_block((rng), (out), (sz)) + #include +#undef wc_RNG_GenerateBlock + #include +/* Number of remaining draws to answer with all-0xff instead of real random. */ +static int wb_ff_draws = 0; + +static int wb_c25519_rng_block(WC_RNG* rng, byte* out, word32 sz) +{ + if (wb_ff_draws > 0) { + wb_ff_draws--; + XMEMSET(out, 0xff, sz); + return 0; + } + return wc_RNG_GenerateBlock(rng, out, sz); +} + static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) @@ -278,8 +322,83 @@ static void wb_generic_arg_guards(void) } #endif /* HAVE_CURVE25519 && WOLFSSL_CURVE25519_BLINDING */ +/* ------------------------------------------------------------------------- * + * curve25519.c:288 if ((i >= 0) || (rz[0] <= 0xec)) + * + * idx0 ("i >= 0") needs a draw whose every byte is 0xff, so the scan at + * :283-:286 falls off the bottom with i == -1. One scripted draw does that; + * the loop then goes round once more with a real draw, which breaks at some + * i >= 0 and gives the TRUE partner in the same call and the same binary. + * + * idx1 ("rz[0] <= 0xec") stays EXCLUDED and is not attempted here: reaching + * it at all requires i < 0, which the loop bound (i >= 0, not i >= 1) makes + * synonymous with rz[0] == 0xff, so the operand is unreachable-as-true. That + * is a product defect; if the loop bound + * is ever corrected the exclusion must be withdrawn and BOTH operands + * re-measured from this same interposer. + * ------------------------------------------------------------------------- */ +#if defined(HAVE_CURVE25519) && defined(WOLFSSL_CURVE25519_BLINDING) && \ + !defined(FREESCALE_LTC_ECC) && !defined(WOLF_CRYPTO_CB_ONLY_CURVE25519) +static void wb_blind_rz_all_ff(void) +{ + byte pub[CURVE25519_KEYSIZE]; + byte priv[CURVE25519_KEYSIZE]; + WC_RNG rng; + int ret; + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; skipping blinding rz vectors"); + return; + } + + XMEMSET(pub, 0, sizeof(pub)); + XMEMSET(priv, 0x5a, sizeof(priv)); + if (curve25519_priv_clamp(priv) != 0) { + WB_NOTE("curve25519_priv_clamp failed; skipping blinding rz vectors"); + wc_FreeRng(&rng); + return; + } + + /* Unscripted: every draw is real, so the guard is only ever evaluated + * with i >= 0 (idx0 TRUE, decision TRUE). */ + ret = wc_curve25519_make_pub_blind(CURVE25519_KEYSIZE, pub, + CURVE25519_KEYSIZE, priv, &rng); + if (ret != 0) { + WB_NOTE("wc_curve25519_make_pub_blind failed unscripted"); + wb_fail = 1; + } + + /* Scripted: exactly one all-0xff draw, so the first iteration evaluates + * the guard with i == -1 and rz[0] == 0xff -- (F,F), decision FALSE -- + * and the retry draws real bytes and breaks normally. */ + XMEMSET(pub, 0, sizeof(pub)); + wb_ff_draws = 1; + ret = wc_curve25519_make_pub_blind(CURVE25519_KEYSIZE, pub, + CURVE25519_KEYSIZE, priv, &rng); + if (wb_ff_draws != 0) { + WB_NOTE("scripted all-0xff draw was never consumed"); + wb_fail = 1; + wb_ff_draws = 0; + } + if (ret != 0) { + WB_NOTE("wc_curve25519_make_pub_blind failed after an all-0xff rz"); + wb_fail = 1; + } + + wc_FreeRng(&rng); + WB_NOTE("curve25519_smul_blind rz rejection-loop guard exercised"); +} +#else +static void wb_blind_rz_all_ff(void) +{ + (void)&wb_c25519_rng_block; + WB_NOTE("curve25519 blinding not compiled in; rz vectors skipped"); +} +#endif + int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("curve25519.c white-box supplement\n"); #ifndef HAVE_CURVE25519 printf(" HAVE_CURVE25519 not defined; nothing to exercise\n"); @@ -288,9 +407,10 @@ int main(void) wb_make_pub_nb(); wb_make_key_nb(); wb_generic_arg_guards(); + wb_blind_rz_all_ff(); 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 + * suite treats a nonzero exit as a failed variant and discards its * coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_dh_fault_whitebox.c b/tests/unit-mcdc/test_dh_fault_whitebox.c index a8a77aa0d0..0cc3382d20 100644 --- a/tests/unit-mcdc/test_dh_fault_whitebox.c +++ b/tests/unit-mcdc/test_dh_fault_whitebox.c @@ -27,7 +27,7 @@ * file-static, full-capacity sp_int) is a cross-TU call into sp_int.c, and * mcdc_fault_alloc.h's XMALLOC hook only reaches allocations made BY dh.c * itself (see its header comment) - it cannot fail an sp_int.c internal - * allocation, and under this campaign's math backend (WOLFSSL_SP_MATH_ALL, + * allocation, and under this suite's math backend (WOLFSSL_SP_MATH_ALL, * SP_INT_BITS=4096, fixed-size sp_int, no heap growth) most of those calls * cannot fail at all except on a NULL argument that can never be NULL here. * This file therefore does not use mcdc_fault_alloc.h; instead it drives the @@ -71,7 +71,7 @@ * "oversized" buffer, whose LENGTH argument (never its dereferenced past-end * content) is what trips the size guard - no OOB read ever happens. * - * STRUCTURALLY UNSATISFIABLE (recorded in campaign/db/exclusions.json). + * STRUCTURALLY UNSATISFIABLE (recorded in the exclusion record). * Three dh.c conditions are not driven here because no input produces the * missing vector: * @@ -119,7 +119,7 @@ #include "mcdc_fault_mp.h" /* EXPERIMENT (2026-08-11 flake hunt): dh.c:3359 `(ret == 0) && (primeCheckCount)` - * was the last non-deterministic condition campaign-wide -- covered in 2 of 3 + * was the last non-deterministic condition suite-wide -- covered in 2 of 3 * sweeps of an unchanged tree. primeCheckCount counts rejected prime * candidates, so it is a direct function of the random draws. */ #include "mcdc_seed_rng.h" @@ -284,7 +284,7 @@ static void test_generate_keypair_null_guards(void) * dispatch above returns first): a p whose bit count is not 2048/3072/4096 * takes this generic path in every variant. 1408's mp_to_unsigned_bin(y,pub) * call is given the exact size it needs (never truncated) and only checks - * for NULL - its failure arm is unreachable (see DEATHNOTE note below), so + * for NULL - its failure arm is unreachable (), so * only 1408's ret==0 operand is exercised here (via the same cascade as * 1405's). */ static void test_generate_public_cascade(void) @@ -315,7 +315,7 @@ static void test_generate_public_cascade(void) /* 1405:1 - a modulus bigger than SP_INT_BITS makes sp_exptmod_ex's own * "m->used*2 >= SP_INT_DIGITS" guard fail deterministically (verified: * MP_EXPTMOD_E), with ret==0 still true entering the check. Self-built - * odd 6144-bit-ish value (no HAVE_FFDHE_6144 table in this campaign's + * odd 6144-bit-ish value (no HAVE_FFDHE_6144 table in this suite's * base config) - trusted=1 skips the (irrelevant) primality check. */ { byte bigp[768]; @@ -433,7 +433,7 @@ static void test_validate_and_pairwise(void) * candidate; a single flipped byte makes it composite with overwhelming * probability, giving isPrime==0 (2706's TRUE row) which sets ret= * DH_CHECK_PUB_E and cascades ret!=0 into 2710 (its FALSE row). mp_init() - * on &key->g itself cannot fail here (see DEATHNOTE note). */ + * on &key->g itself cannot fail here. */ static void test_setkey_primality(void) { WC_RNG rng; diff --git a/tests/unit-mcdc/test_dsa_fault_whitebox.c b/tests/unit-mcdc/test_dsa_fault_whitebox.c index 09f7ea57dc..e0d83fb634 100644 --- a/tests/unit-mcdc/test_dsa_fault_whitebox.c +++ b/tests/unit-mcdc/test_dsa_fault_whitebox.c @@ -80,8 +80,8 @@ * ./test_dsa_fault_whitebox baseline: unarmed valid ops only * ./test_dsa_fault_whitebox sweep baseline + the fault-index sweeps * (Two modes so the injector's contribution can be measured as a delta; the - * campaign's run_whitebox harness runs it with no args -- pass "sweep" there by - * default via argv, see the modules.json entry note.) + * suite's run_whitebox harness runs it with no args -- pass "sweep" there by + * default via argv, see the the module registry entry note.) */ /* Installed BEFORE dsa.c so its mp_* calls resolve to the fault wrappers -- @@ -129,6 +129,46 @@ static long wb_fmi_seen(void) { return wb_fmi_count; } #define mp_init_multi(a, b, c, d, e, f) \ wb_fm_init_multi((a), (b), (c), (d), (e), (f)) +/* ---- narrow, opt-in MEMORY_E fault on mp_prime_is_prime_ex ------------ + * wc_MakeDsaParameters' tmp2 cleanup guard + * + * if ((err not MP_INIT_E) && (err not MEMORY_E)) mp_clear(tmp2); + * + * can only take its `err not MEMORY_E` FALSE half with tmp2 ALLOCATED, and the + * only MEMORY_E the function assigns itself (the tmp/tmp2 XMALLOC guard at + * :426) is a short-circuit `||`: when it fires, tmp2 is NULL by construction + * and the guard is never reached. So that half needs a MEMORY_E arriving from + * DEEPER, after both allocations succeeded. + * + * mp_prime_is_prime_ex() at :466 is such a source in the product: the sp_int + * implementation propagates wc_RNG_GenerateBlock()'s error code VERBATIM out + * of its Miller-Rabin base draw (wolfcrypt/src/sp_int.c, the `err = + * wc_RNG_GenerateBlock(rng, (byte*)b->dp, baseSz); if (err != MP_OKAY) break;` + * in sp_prime_is_prime_ex), and wc_RNG_GenerateBlock returns MEMORY_E when its + * own WOLFSSL_SMALL_STACK scratch allocation fails. Injecting MEMORY_E here is + * therefore the same value the real code path produces, just without having to + * guess which of the several hundred allocations inside a 1024-bit parameter + * generation is the DRBG's (an index-walk that far in is neither cheap nor + * stable across builds). + * + * One-shot, and armed only around a dedicated wc_MakeDsaParameters call, so + * nothing else in the file sees it. dsa.c has exactly one direct call site. */ +static int wb_pip_mem = 0; + +static int wb_fm_prime_is_prime_ex(const mp_int* a, int t, int* result, + WC_RNG* rng) +{ + if (wb_pip_mem) { + wb_pip_mem = 0; + return MEMORY_E; + } + return mp_prime_is_prime_ex(MCDC_FM_MI(a), t, result, rng); +} + +#undef mp_prime_is_prime_ex +#define mp_prime_is_prime_ex(a, t, r, g) \ + wb_fm_prime_is_prime_ex((a), (t), (r), (g)) + #include #include "mcdc_fault_alloc.h" @@ -264,7 +304,7 @@ static void wb_crafted(WC_RNG* rng, DsaKey* key, const byte* digest, /* ---- wc_MakeDsaParameters reached with err != MP_OKAY. 518's `err != * MP_OKAY` operand is only FALSE-able from a successful run (the baseline - * call) and TRUE-able from a failed one; nothing in the normal campaign + * call) and TRUE-able from a failed one; nothing in the normal suite * ever fails it, and the heap sweep cannot (see the MakeDsaParameters note * further down). One armed mp step is enough and stops before the * expensive prime search: index 1 is mp_read_unsigned_bin at :426, index 2 @@ -284,6 +324,75 @@ static void wb_crafted(WC_RNG* rng, DsaKey* key, const byte* digest, } } + /* ---- KEY-GENERATION MP_INIT_E cleanup halves: wc_MakeDsaKey 332 idx1 + * and wc_MakeDsaParameters 520 idx0 / 526 idx0 / 537 idx1. + * + * These were dead until wolfcrypt commit 1e8807b13. Neither key-generation + * entry point mapped a failed mp_init_multi() to MP_INIT_E -- mp_init_multi + * reports the BACKEND's code (MP_MEM from the heap backends) -- so + * wc_MakeDsaParameters' "err not MP_INIT_E" guards could never fire and + * wc_MakeDsaKey had no guard at all and ran mp_clear(tmpQ) on never- + * initialised storage (the SIGSEGV). Both sites + * now use the idiom wc_DsaSign_ex/wc_DsaVerify_ex already used, so failing + * THEIR mp_init_multi is the way into these halves and is crash-safe: the + * guards exist precisely to skip the clears. + * + * Each entry point performs exactly one mp_init_multi and it is the first + * one of the call (nothing ahead of it initialises an mp_int), so index 1 + * selects it in both cases. The accepting halves of the same guards come + * from the unarmed key/parameter generation in main(). */ + { + DsaKey ik; + XMEMSET(&ik, 0, sizeof(ik)); + if (wc_InitDsaKey(&ik) == 0) { + if (wc_DsaImportParamsRaw(&ik, kP, kQ, kG) == 0) { + wb_fmi_arm(1); + ret = wc_MakeDsaKey(rng, &ik); + wb_fmi_disarm(); + if (ret != WC_NO_ERR_TRACE(MP_INIT_E)) + printf(" [wb] MakeDsaKey init fault returned %d, not " + "MP_INIT_E: 332 idx1 NOT driven\n", ret); + } + wc_FreeDsaKey(&ik); + } + } + { + DsaKey pk; + XMEMSET(&pk, 0, sizeof(pk)); + if (wc_InitDsaKey(&pk) == 0) { + wb_fmi_arm(1); + ret = wc_MakeDsaParameters(rng, 1024, &pk); + wb_fmi_disarm(); + if (ret != WC_NO_ERR_TRACE(MP_INIT_E)) + printf(" [wb] MakeDsaParameters init fault returned %d, not " + "MP_INIT_E: 520/526/537 NOT driven\n", ret); + wc_FreeDsaKey(&pk); + } + } + + /* ---- wc_MakeDsaParameters 526 idx1 (`err not MEMORY_E` FALSE with tmp2 + * allocated). The function's OWN MEMORY_E (:426) is a short-circuit `||` + * over the tmp/tmp2 XMALLOCs, so whenever it fires tmp2 is NULL and the + * `if (tmp2 != NULL)` gate above 526 skips the guard entirely -- that path + * closes 520 idx1 (tmp allocated, tmp2 not) and can never close 526 idx1. + * The vector needs a MEMORY_E raised AFTER both allocations succeeded; + * mp_prime_is_prime_ex is such a source in the product (see the + * wb_fm_prime_is_prime_ex note). One-shot, fires on dsa.c's single direct + * call site at :466. */ + { + DsaKey pk; + XMEMSET(&pk, 0, sizeof(pk)); + if (wc_InitDsaKey(&pk) == 0) { + wb_pip_mem = 1; + ret = wc_MakeDsaParameters(rng, 1024, &pk); + wb_pip_mem = 0; + if (ret != WC_NO_ERR_TRACE(MEMORY_E)) + printf(" [wb] MakeDsaParameters MEMORY_E fault returned %d: " + "526 idx1 NOT driven\n", ret); + wc_FreeDsaKey(&pk); + } + } + /* ---- _DsaImportParamsRaw with trusted == 0: the (T,T) vector of * `err == MP_OKAY && !trusted` at :540. wc_DsaImportParamsRaw (used * everywhere else, including by the sweeps) hard-codes trusted = 1, so @@ -350,10 +459,14 @@ static void wb_crafted(WC_RNG* rng, DsaKey* key, const byte* digest, } else { /* Expected for every WOLFSSL_SP_INT_NEGATIVE-less build: - * sp_int.h then #defines mp_isneg(a) to the constant (0), - * so 887's second operand cannot be TRUE there at all. */ + * sp_int.h then #defines sp_isneg(a) to the constant (0), + * so 906's second operand cannot be TRUE there at all -- + * which is exactly why the module carries an + * `sp_negative` variant (-DWOLFSSL_SP_INT_NEGATIVE), the + * one build in which this vector is productive. */ printf(" [wb] no negative mp_int (set=%d sub=%d neg=%d " - "zero=%d): 887 idx1 skipped\n", + "zero=%d): 906 idx1 needs the sp_negative " + "variant\n", e1, e2, (int)mp_isneg(neg), (int)mp_iszero(neg)); } mp_clear(neg); @@ -552,7 +665,7 @@ static void wb_crafted(WC_RNG* rng, DsaKey* key, const byte* digest, int main(int argc, char** argv) { - /* Default action is the fault sweep so the campaign's run_whitebox harness + /* Default action is the fault sweep so the run_whitebox harness * (which runs this binary with NO arguments) gets full coverage. Pass * "baseline" to run only the unarmed valid ops (used to measure the * injector's contribution as a delta), or "probe" to print the @@ -601,7 +714,7 @@ int main(int argc, char** argv) /* Diagnostic: count the allocations each entry point performs, WITHOUT * failing any (arm a huge index so the counter advances but never * trips). Use these counts to choose each sweep's K -- see the header - * and the campaign fan-out recipe. Exits without sweeping. */ + * and the harness fan-out recipe. Exits without sweeping. */ int a = 0; byte s2[256]; XMEMSET(s2, 0, sizeof(s2)); mcdc_fa_arm(1000000); diff --git a/tests/unit-mcdc/test_ecc_fault_whitebox.c b/tests/unit-mcdc/test_ecc_fault_whitebox.c index 69aa5be47a..1444b17a98 100644 --- a/tests/unit-mcdc/test_ecc_fault_whitebox.c +++ b/tests/unit-mcdc/test_ecc_fault_whitebox.c @@ -1374,7 +1374,7 @@ int main(void) mcdc_fa_disarm(); mcdc_fa_restore(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_ecc_whitebox.c b/tests/unit-mcdc/test_ecc_whitebox.c index 8f6e9d6466..85704605e7 100644 --- a/tests/unit-mcdc/test_ecc_whitebox.c +++ b/tests/unit-mcdc/test_ecc_whitebox.c @@ -13,13 +13,13 @@ * halves of each MC/DC independence pair. * * 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 + * source line:col in the per-module suite: + * llvm-cov computes MC/DC independence PER BINARY, and the * 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.sh's white-box step with the SAME MC/DC CFLAGS, + * Build: compiled by the coverage runner'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 ecc.o removed (this TU * supplies the instrumented ecc.c). NOT part of the wolfSSL build; not @@ -47,7 +47,7 @@ * with ecc.o removed, so the opt-in is local to the white-box binary: it does * not change the library under test, and it touches nothing outside ecc.c * (the macro appears in no header, so no shared type or layout moves). The - * campaign unions MC/DC per source line, which is exactly how a white-box is + * suite unions MC/DC per source line, which is exactly how a white-box is * meant to add rows the native variants cannot produce. */ #ifndef WOLFSSL_ECIES_STATIC_GCM_NONCE #define WOLFSSL_ECIES_STATIC_GCM_NONCE @@ -875,7 +875,7 @@ static void wb_projective_wrappers(void) mp_clear(&a); } -/* ECC_SHAMIR is unconditional in the base config (see modules.json "ecc" +/* ECC_SHAMIR is unconditional in the base config (see the module registry "ecc" * notes): under it, ecc_mul2add() is `static normal_ecc_mul2add()` (FP_ECC * on, the default) or the public `ecc_mul2add()` itself (no_fp_shamir, FP_ECC * off) -- select the same symbol the source itself would use. */ @@ -1658,7 +1658,7 @@ static void wb_arg_guards(void) /* The accepting vectors below need the real curve constants: a zero * modulus/order would make wc_ecc_gen_deterministic_k's RFC 6979 retry - * loop spin, and the campaign kills the variant on TEST_TIMEOUT. */ + * loop spin, and the harness kills the variant on TEST_TIMEOUT. */ { mp_int prime; mp_int order; @@ -1932,7 +1932,7 @@ static void wb_gap_pass2(void) /* (F,-): the only way to reach the guard with the first operand * FALSE is a NON-NULL pubOut (which leaves key->type alone) on a key * that is a full ECC_PRIVATEKEY. Every API caller passes NULL, which - * is why this row is missing from the campaign. */ + * is why this row is missing from the harness. */ if (pubPt != NULL) { key.type = ECC_PRIVATEKEY; (void)wc_ecc_make_pub(&key, pubPt); @@ -2008,7 +2008,7 @@ static void wb_gap_pass2(void) * The public import paths reduce/reject out-of-range coordinates before * building a point, so only a hand-built point reaches these with a * coordinate >= p. (The mp_isneg operand of each is a separate matter -- - * see the residual note in the campaign report.) */ + * see the residual note in the harness report.) */ { mp_int prime, af, bf; ecc_point* pt = wc_ecc_new_point(); @@ -2429,7 +2429,7 @@ static void wb_ecies_algos(void) /* (T,T,-) and (T,F,T): longer than one byte AND a compressed-point * prefix, once for each parity tag. Only an ECIES sender that chose - * point compression emits these, and nothing in the campaign does -- + * point compression emits these, and nothing in the harness does -- * which also made the 0x03 operand a COIN FLIP before this vector * existed: it was only covered when a random ephemeral key happened to * have an odd y, so the module's number moved between runs of an @@ -2510,7 +2510,7 @@ static const int wbCurveIds[] = { /* Curve availability is a RUNTIME question here, not a compile-time one: the * per-curve HAVE_ECCnnn macros are only set when a build hand-picks curves, - * and this campaign's configs take the HAVE_ALL_CURVES default instead -- so + * and this suite's configs take the HAVE_ALL_CURVES default instead -- so * guarding on them would silently reduce every sweep below to P-256 and leave * each `ecc_sets[idx].id == ` operand permanently TRUE. Asking the * table is correct for both kinds of build. */ @@ -3618,7 +3618,7 @@ static void wb_ecies_bad_kdf(void) * 5110:1 `if ((err == MP_OKAY) && checkInf)` * checkInf is a local initialized to the constant 1 and never * assigned again outside WOLFSSL_SE050 builds, so it cannot be FALSE - * in any variant this campaign compiles. + * in any variant this suite compiles. * * 5088:1 `x < mp_unsigned_bin_size(result->x)` * x is mp_unsigned_bin_size(curve->prime) and the guard only runs @@ -3689,7 +3689,7 @@ static void wb_ecies_bad_kdf(void) * NOT a residual -- COVERED by Class 33 below. The argument that * used to be filed here (MAX_ECC_BITS caps the scalars at 521 bits, * 66 bytes, well under KB_SIZE - 2 = 126) is false for this - * campaign's configs: they #define WOLFCRYPT_HAVE_SAKKE, which + * suite's configs: they #define WOLFCRYPT_HAVE_SAKKE, which * raises MAX_ECC_BITS to 1024 and puts the 128-byte ECC_SAKKE_1 * curve in ecc_sets[], while accel_fp_mul2add() uses a flat * `#define KB_SIZE 128` (accel_fp_mul() is 256 under the same @@ -3877,7 +3877,7 @@ int main(void) wb_ecies_bad_kdf(); wb_fp_mul2add_kb_size(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_eccsi_fault_whitebox.c b/tests/unit-mcdc/test_eccsi_fault_whitebox.c index 8f40314002..6738d45472 100644 --- a/tests/unit-mcdc/test_eccsi_fault_whitebox.c +++ b/tests/unit-mcdc/test_eccsi_fault_whitebox.c @@ -42,7 +42,7 @@ * the map guard sees err != 0. It #includes eccsi.c directly (like the sibling * test_eccsi_whitebox.c) so the file-static helpers are in scope. * - * Not driven here (justified, documented in test_eccsi_whitebox.c and GAPS.md): + * Not driven here (justified, documented in test_eccsi_whitebox.c and the uncovered-condition report): * - eccsi_load_ecc_params() 196/202/208 `err == 0` FALSE halves: reaching them * requires eccsi_load_order() / the a/b radix reads to fail. Those are * mp_read_radix() calls on fixed, static, in-struct sp_int members of known- @@ -64,7 +64,7 @@ * * Invocation: * ./test_eccsi_fault_whitebox default: full fault sweep (used by the - * campaign run_whitebox, no args) + * suite run_whitebox, no args) * ./test_eccsi_fault_whitebox baseline unarmed valid ops only (delta baseline) * ./test_eccsi_fault_whitebox probe per-target allocation-site counts */ @@ -80,6 +80,75 @@ * (mp_free/mp_forcezero) are NOT interposed, so cleanup keeps working. */ #include "mcdc_fault_mp.h" +/* -------------------------------------------------------------------------- + * Value-forcing mp_addmod() interposer for eccsi_gen_sig()'s rejection loop + * (eccsi.c:1934): + * + * do { ... err = mp_mulmod(r, &key->ssk, &key->params.order, s); + * err = mp_addmod(he, s, &key->params.order, s); } + * while ((err == 0) && (mp_iszero(s) || (mp_cmp(s, he) == MP_EQ))); + * + * RFC 6507 step 4 rejects the candidate when s == 0 or s == HE. Both are + * ~2^-256 events on real entropy, so no amount of API driving reaches them, + * and a seeded RNG cannot force them either: s is the output of a modular + * multiply-and-add over an ephemeral scalar, not a value the generator hands + * out. Without one of the two rejecting draws the decision only ever records + * (T,F,F), which is a single vector -- that is why ALL THREE conditions were + * open, including `err == 0`, whose independence pair needs a vector where the + * loop actually REPEATS. + * + * eccsi.c has exactly two mp_addmod() call sites -- eccsi_make_pair() at 920 + * and this one at 1931 -- and only the second is reached from + * wc_SignEccsiHash(), so a one-shot armed immediately around a sign call needs + * no disambiguation. + * + * The modes are ONE-SHOT, so the loop's SECOND iteration computes a genuine s + * and terminates on real data: no retry loop here depends on a random draw + * going a particular way, and the accepting (T,F,F) row is produced by that + * same iteration in the same binary as the rejecting rows. + * + * Ordering is the load-bearing trick from mcdc_fault_mp.h: the wrapper is + * compiled while mp_addmod still names the (already interposed) real thing, + * and only then is the name redefined. eccsi.c is #included AFTER this block. + * ----------------------------------------------------------------------- */ +#define WB_EA_OFF 0 /* pass through */ +#define WB_EA_ZERO 1 /* succeed, but hand back s == 0 -> 1934 idx1 */ +#define WB_EA_EQ 2 /* succeed, but hand back s == he -> 1934 idx2 */ +#define WB_EA_FAIL 3 /* fail the add -> 1934 idx0 */ + +static int wb_ea_mode = WB_EA_OFF; + +MCDC_FM_MAYBE_UNUSED static int wb_ea_addmod(const mp_int* a, const mp_int* b, + const mp_int* m, mp_int* r) +{ + int mode = wb_ea_mode; + int err; + + if (mode != WB_EA_OFF) { + wb_ea_mode = WB_EA_OFF; /* one-shot */ + } + if (mode == WB_EA_FAIL) { + return MCDC_FM_ERR; + } + + err = mp_addmod(a, b, m, r); + if (err == 0) { + if (mode == WB_EA_ZERO) { + /* s = 0: RFC 6507's first rejection test. */ + mp_zero(r); + } + else if (mode == WB_EA_EQ) { + /* s = HE: the second rejection test. `a` IS he at the 1931 call + * site, so this needs no extra handle on the key. */ + err = mp_copy(a, r); + } + } + return err; +} + +#undef mp_addmod +#define mp_addmod(a, b, c, d) wb_ea_addmod((a), (b), (c), (d)) + #include #include "mcdc_fault_alloc.h" @@ -165,6 +234,81 @@ static int wb_mp_expired(void) printf(" [wb] mp sweep %s: K=%ld\n", (lbl), k_); \ } while (0) +/* ---- eccsi_gen_sig() 1934 rejection loop ------------------------------- + * while ((err == 0) && (mp_iszero(s) || (mp_cmp(s, he) == MP_EQ))); + * + * Three conditions, and before this vector set ALL THREE were open, because + * the loop had only ever been observed taking the single (T,F,F) exit: the two + * RFC 6507 step-4 rejections (s == 0, s == HE) are ~2^-256 draws, and `err==0` + * cannot show independence without a partner vector in which the decision is + * TRUE -- i.e. in which the loop actually repeats. + * + * The one-shot mp_addmod modes at the top of this file supply all of it: + * WB_EA_ZERO -> (T,T,-) TRUE, retries; the retry is the accepting (T,F,F) + * WB_EA_EQ -> (T,F,T) TRUE, likewise + * WB_EA_FAIL -> (F,-,-) FALSE + * + * Self-contained fixture on purpose: the shared fixture in wb_mp_sweeps() is + * driven through armed sweeps before the sign path is reached, and one of them + * currently leaves the key in a state wc_SetEccsiPair rejects (see the note + * there). A fresh key here means these vectors cannot be lost to that. + */ +static void wb_gen_sig_reject(WC_RNG* rng) +{ + EccsiKey k; + ecc_point* pvt = NULL; + mp_int ssk; + byte id[] = "eccsi-gensig@wolfssl.com"; + byte hash[WC_MAX_DIGEST_SIZE]; + byte hashSz = 0; + int ready = 0; + int mode; + + mcdc_fm_disarm(); + XMEMSET(&k, 0, sizeof(k)); + XMEMSET(&ssk, 0, sizeof(ssk)); + XMEMSET(hash, 0, sizeof(hash)); + + if (wc_InitEccsiKey(&k, NULL, INVALID_DEVID) != 0) { + WB_NOTE("gen_sig fixture: wc_InitEccsiKey failed; 1934 skipped"); + return; + } + pvt = wc_ecc_new_point_h(NULL); + if ((pvt != NULL) && (mp_init(&ssk) == 0) && + (wc_MakeEccsiKey(&k, rng) == 0) && + (wc_MakeEccsiPair(&k, rng, WC_HASH_TYPE_SHA256, id, + (word32)sizeof(id), &ssk, pvt) == 0) && + (wc_SetEccsiPair(&k, &ssk, pvt) == 0) && + (wc_HashEccsiId(&k, WC_HASH_TYPE_SHA256, id, (word32)sizeof(id), + pvt, hash, &hashSz) == 0) && + (wc_SetEccsiHash(&k, hash, hashSz) == 0)) { + ready = 1; + } + if (!ready) { + WB_NOTE("gen_sig fixture setup failed; 1934 skipped"); + wb_fail = 1; + } + else { + for (mode = WB_EA_ZERO; mode <= WB_EA_FAIL; mode++) { + byte sg[257]; + word32 z = (word32)sizeof(sg); + int e; + + XMEMSET(sg, 0, sizeof(sg)); + wb_ea_mode = mode; + e = wc_SignEccsiHash(&k, rng, WC_HASH_TYPE_SHA256, hash, + WC_SHA256_DIGEST_SIZE, sg, &z); + wb_ea_mode = WB_EA_OFF; + printf(" [wb] gen_sig reject mode %d -> %d\n", mode, e); + } + } + + mp_free(&ssk); + if (pvt != NULL) + wc_ecc_del_point_h(pvt, NULL); + wc_FreeEccsiKey(&k); +} + static void wb_mp_sweeps(WC_RNG* rng) { EccsiKey k; @@ -172,6 +316,7 @@ static void wb_mp_sweeps(WC_RNG* rng) mp_int ssk; byte id[] = "eccsi-mp-fault@wolfssl.com"; byte hash[WC_MAX_DIGEST_SIZE]; + byte hashSz = 0; byte sig[257]; word32 sigSz; int verified = 0; @@ -180,6 +325,10 @@ static void wb_mp_sweeps(WC_RNG* rng) wb_mp_t0 = time(NULL); mcdc_fm_disarm(); + /* Runs first, on its own fixture: see wb_gen_sig_reject(). */ + wb_gen_sig_reject(rng); + mcdc_fm_disarm(); + XMEMSET(&k, 0, sizeof(k)); XMEMSET(&ssk, 0, sizeof(ssk)); XMEMSET(hash, 0x5a, sizeof(hash)); @@ -219,8 +368,8 @@ static void wb_mp_sweeps(WC_RNG* rng) mcdc_fm_disarm(); if ((wc_SetEccsiPair(&k, &ssk, pvt) == 0) && (wc_HashEccsiId(&k, WC_HASH_TYPE_SHA256, id, (word32)sizeof(id), - pvt, hash, NULL) == 0) && - (wc_SetEccsiHash(&k, hash, WC_SHA256_DIGEST_SIZE) == 0)) { + pvt, hash, &hashSz) == 0) && + (wc_SetEccsiHash(&k, hash, hashSz) == 0)) { WB_MP_SWEEP("SignEccsiHash", 200, { byte s2[257]; @@ -242,8 +391,16 @@ static void wb_mp_sweeps(WC_RNG* rng) } } else { - WB_NOTE("SetEccsiPair/HashEccsiId/SetEccsiHash failed; sign+verify " - "sweeps skipped"); + /* Print WHICH step refused, so this never has to be bisected again: + * the original spelling passed hashSz = NULL to wc_HashEccsiId(), + * which rejects it with BAD_FUNC_ARG, and the SignEccsiHash / + * VerifyEccsiHash sweeps below were silently skipped on every run. */ + int e1 = wc_SetEccsiPair(&k, &ssk, pvt); + int e2 = wc_HashEccsiId(&k, WC_HASH_TYPE_SHA256, id, + (word32)sizeof(id), pvt, hash, &hashSz); + int e3 = wc_SetEccsiHash(&k, hash, hashSz); + printf(" [wb] SetEccsiPair=%d HashEccsiId=%d SetEccsiHash=%d; " + "sign+verify sweeps skipped\n", e1, e2, e3); } /* 196/202/208 eccsi_load_ecc_params(): diff --git a/tests/unit-mcdc/test_ed25519_hash_fault_whitebox.c b/tests/unit-mcdc/test_ed25519_hash_fault_whitebox.c index 4253f09e66..89d616ff41 100644 --- a/tests/unit-mcdc/test_ed25519_hash_fault_whitebox.c +++ b/tests/unit-mcdc/test_ed25519_hash_fault_whitebox.c @@ -34,7 +34,7 @@ * Every open "ret == 0" operand left in ed25519.c takes its `ret` from the * SHA-512 chain (ed25519_hash_init/update/final -> wc_InitSha512_ex / * wc_Sha512Update / wc_Sha512Final) and from nothing else. ed25519.c performs - * no allocation on these paths in this campaign's configs (WOLFSSL_SMALL_STACK + * no allocation on these paths in this suite's configs (WOLFSSL_SMALL_STACK * is unset, so WC_DECLARE_VAR/WC_ALLOC_VAR_EX are a plain stack object and a * no-op), so mcdc_fault_alloc.h has nothing to fault -- only a failing hash * primitive can break the chain. mcdc_fault_hash.h shadows Update/Final; @@ -103,7 +103,7 @@ * fixed sweep length. main() always returns 0 -- a nonzero exit would discard * the variant's whole coverage. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with ed25519.o removed. Not part of the wolfSSL build. */ diff --git a/tests/unit-mcdc/test_ed25519_whitebox.c b/tests/unit-mcdc/test_ed25519_whitebox.c index 6a42c2d573..3b2215cba9 100644 --- a/tests/unit-mcdc/test_ed25519_whitebox.c +++ b/tests/unit-mcdc/test_ed25519_whitebox.c @@ -17,17 +17,17 @@ * 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 ed25519.c directly + * the cross-binary MC/DC lesson) by compiling ed25519.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 + * by source line:col in the per-module suite: + * llvm-cov computes MC/DC independence PER BINARY, and the * 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 + * Build: compiled by the coverage runner'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 ed25519.o * removed (this TU supplies the instrumented ed25519.c). NOT part of the @@ -38,7 +38,7 @@ * Class 1 ed25519_hash() key/in/hash NULL guard ............ 4 conditions * The only ed25519.c gap confirmed structurally unreachable through the * public API (every wrapper hard-codes non-NULL, well-formed arguments). - * See the campaign's RESIDUALS.md for everything else: the "ret==0" FALSE + * See the RESIDUALS.md for everything else: the "ret==0" FALSE * sides following a successful ed25519_hash() call (would need a mockable * malloc/wc_InitSha512Ex failure to force), and the WOLFSSL_CHECK_VER_FAULTS * redundant post-verify ConstantCompare (a deterministic double-call on the @@ -248,7 +248,7 @@ int main(void) wb_ed25519_verify_helper_key_guard(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures are surfaced as skips, not test failures: the - * campaign treats a nonzero exit as a failed variant and discards its + * suite treats a nonzero exit as a failed variant and discards its * coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_ed448_hash_fault_whitebox.c b/tests/unit-mcdc/test_ed448_hash_fault_whitebox.c index 2d8c950a54..968701f0f6 100644 --- a/tests/unit-mcdc/test_ed448_hash_fault_whitebox.c +++ b/tests/unit-mcdc/test_ed448_hash_fault_whitebox.c @@ -34,7 +34,7 @@ * Every open "(ret == 0)" operand left in ed448.c takes its `ret` from the * SHAKE256 chain (ed448_hash_init/update/final -> wc_InitShake256 / * wc_Shake256_Update / wc_Shake256_Final) and from nothing else. ed448.c - * performs no allocation on these paths in this campaign's configs + * performs no allocation on these paths in this suite's configs * (WOLFSSL_SMALL_STACK is unset, so WC_DECLARE_VAR/WC_ALLOC_VAR_EX are a plain * stack object and a no-op), so mcdc_fault_alloc.h has nothing to fault -- * only a failing hash primitive can break the chain. mcdc_fault_hash.h shadows @@ -102,7 +102,7 @@ * wall clock: fixed seeds, fixed message, fixed sweep length. main() always * returns 0 -- a nonzero exit would discard the variant's whole coverage. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with ed448.o removed. Not part of the wolfSSL build. */ diff --git a/tests/unit-mcdc/test_ed448_whitebox.c b/tests/unit-mcdc/test_ed448_whitebox.c index 45d09ef6d9..20a84f42a4 100644 --- a/tests/unit-mcdc/test_ed448_whitebox.c +++ b/tests/unit-mcdc/test_ed448_whitebox.c @@ -16,17 +16,17 @@ * 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 + * same binary, per the 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 + * source line:col in the per-module suite: + * llvm-cov computes MC/DC independence PER BINARY, and the * 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 + * Build: compiled by the coverage runner'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 @@ -276,7 +276,7 @@ int main(void) 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 + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_falcon_whitebox.c b/tests/unit-mcdc/test_falcon_whitebox.c index 318623a879..91b617dc14 100644 --- a/tests/unit-mcdc/test_falcon_whitebox.c +++ b/tests/unit-mcdc/test_falcon_whitebox.c @@ -48,7 +48,7 @@ * a genuine mid-computation error (PRNG squeeze failure, bigint overflow, an * out-of-range coefficient the sampler bounds forbid, a degenerate/non-invertible * key) are documented as WB_NOTE residuals rather than forced unsafely. main() - * always returns 0 so the campaign keeps the variant. + * always returns 0 so the harness keeps the variant. */ #include @@ -1321,15 +1321,30 @@ static void wb_sign_dyn_core_err(WC_RNG* rng) * mid-computation error or a degenerate/forbidden operand, which cannot be * driven crash-safely from a white-box harness. Their opposite (normal) half is * covered above (mostly by the real round-trip). Each of these is carried as an - * EXCLUSIONS.md row with the source-level argument for why no satisfying vector + * the exclusion record row with the source-level argument for why no satisfying vector * exists. * ------------------------------------------------------------------ */ static void wb_residuals(void) { + /* Re-examined in step 7 (2026-08-20); still open, and the reasoning is in + * the exclusion record so it is not redone. Two dead ends were ruled out for + * good: an all-zero (f, g) makes the denominator exactly 0, and 0/0 gives + * NaN, whose comparison makes the FIRST operand true -- a row that is + * already covered; and the wb_solve_ntru_babai_clamp trick above (256 + * keygens at logn = 3) does not transfer, because that clamp is at + * +-(2^31-1) on a RESCALED coefficient while this one is at +-(2^63-1) on + * an unscaled ratio, which at logn = 3 would need ||(f,g)(zeta)||^2 near + * 1e-15 -- excluded by the field-norm floor (the norm is a non-zero + * integer, so one tiny slot forces the others large). The norm floor is + * also why this is NOT an exclusion: at the production degrees it only + * bounds the slot below by about 2^-10000, i.e. it bounds |z| by nothing + * useful. */ WB_NOTE("residual: solve_NTRU_binary_depth1 !fpr_lt(z,+-2^63) halves: the " "Babai coefficient is bounded by sqrt(|F|^2+|G|^2)/sqrt(|f|^2+" "|g|^2) with |F|,|G| < 2^61 (2-word CRT limbs), so |z| >= 2^63 " - "needs both depth-1 field norms to nearly vanish at one FFT slot"); + "needs both depth-1 field norms to nearly vanish at one FFT slot; " + "x^n+1 is irreducible over Q so they never vanish exactly, but no " + "usable lower bound exists either -- see the exclusion record"); WB_NOTE("residual: keygen f[u]/g[u] vs lim halves: lim is 1 << " "(falcon_max_fg_bits[logn] - 1), i.e. 32 at logn 9 and 16 at " "logn 10, while poly_small_mkgauss sums 1 << (10 - logn) draws of " diff --git a/tests/unit-mcdc/test_frodokem_cryptocb_whitebox.c b/tests/unit-mcdc/test_frodokem_cryptocb_whitebox.c index 265da48cf1..cb074bbd88 100644 --- a/tests/unit-mcdc/test_frodokem_cryptocb_whitebox.c +++ b/tests/unit-mcdc/test_frodokem_cryptocb_whitebox.c @@ -36,9 +36,9 @@ * ... * #endif * - * so the operand needs THREE things at once that no existing campaign vector + * so the operand needs THREE things at once that no existing suite vector * supplies: the build must define WOLF_CRYPTO_CB (it is now set in - * campaign/configs/frodokem/user_settings.base.h), the key must carry a real + * suite/configs/frodokem/user_settings.base.h), the key must carry a real * devId, and a registered device must actually service the request rather than * declining with CRYPTOCB_UNAVAILABLE. Miss any one and `cbHandled` is a * constant 0 -- not merely undriven, but with no false side to drive. @@ -404,6 +404,6 @@ int main(void) #endif printf("done (%s)\n", wb_fail ? "with failures" : "ok"); - /* A non-zero exit makes the campaign discard this binary's coverage. */ + /* A non-zero exit makes the harness discard this binary's coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_frodokem_fault_common.h b/tests/unit-mcdc/test_frodokem_fault_common.h index eacca96112..7eb20887ac 100644 --- a/tests/unit-mcdc/test_frodokem_fault_common.h +++ b/tests/unit-mcdc/test_frodokem_fault_common.h @@ -78,7 +78,7 @@ * ./wb.test default: baseline valid ops + the full fault sweep * ./wb.test baseline only the unarmed valid ops (measure sweep as a delta) * ./wb.test probe print per-entry-point allocation counts (sizes K) - * The campaign's run_whitebox harness runs the binary with NO args, so the + * The suite's run_whitebox harness runs the binary with NO args, so the * default action is the full sweep. */ diff --git a/tests/unit-mcdc/test_frodokem_mat_fault_whitebox.c b/tests/unit-mcdc/test_frodokem_mat_fault_whitebox.c index 5dd371b562..4f0985a45a 100644 --- a/tests/unit-mcdc/test_frodokem_mat_fault_whitebox.c +++ b/tests/unit-mcdc/test_frodokem_mat_fault_whitebox.c @@ -34,7 +34,7 @@ * AES-ECB over the aligned scratch takes a non-allocating path). The heap-fault * mock therefore closes NONE of the 13 here -- they need a primitive-return * fault mock instead. This driver still runs the mat file end to end (baseline - * coverage) and is kept so the campaign has a documented, reproducible negative + * coverage) and is kept so the harness has a documented, reproducible negative * result. See test_frodokem_fault_common.h for the full rationale. */ diff --git a/tests/unit-mcdc/test_frodokem_mat_hash_fault_whitebox.c b/tests/unit-mcdc/test_frodokem_mat_hash_fault_whitebox.c index 48eb4bfb9f..a0ac2226d3 100644 --- a/tests/unit-mcdc/test_frodokem_mat_hash_fault_whitebox.c +++ b/tests/unit-mcdc/test_frodokem_mat_hash_fault_whitebox.c @@ -73,7 +73,7 @@ * only set with qMask != 0xffff, and the 976/1344 sets run the identical code * with a bigger n at several times the cost. The larger sets still get their * baseline pass. Everything is bounded by a point budget AND a CPU deadline so - * the binary can never hit the campaign's 600 s TEST_TIMEOUT (a timeout is a + * the binary can never hit the 600 s TEST_TIMEOUT (a timeout is a * SILENT SKIP that would lose the whole file). */ @@ -462,7 +462,7 @@ int main(void) } printf("done (%s)\n", wb_fail ? "with failures" : "ok"); - /* A non-zero exit makes the campaign discard this binary's coverage. */ + /* A non-zero exit makes the harness discard this binary's coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_hpke_fault_whitebox.c b/tests/unit-mcdc/test_hpke_fault_whitebox.c index 7a2921d9e6..bd10d8ede1 100644 --- a/tests/unit-mcdc/test_hpke_fault_whitebox.c +++ b/tests/unit-mcdc/test_hpke_fault_whitebox.c @@ -93,7 +93,7 @@ * ./test_hpke_fault_whitebox default: baseline + fault sweeps * ./test_hpke_fault_whitebox baseline only the unarmed valid ops * ./test_hpke_fault_whitebox probe print per-target allocation counts - * (No-arg default runs the sweep so the campaign's run_whitebox harness, which + * (No-arg default runs the sweep so the run_whitebox harness, which * runs the binary with no arguments, gets full coverage.) */ diff --git a/tests/unit-mcdc/test_integer_fault_whitebox.c b/tests/unit-mcdc/test_integer_fault_whitebox.c index dda5b13f6c..1798f87afc 100644 --- a/tests/unit-mcdc/test_integer_fault_whitebox.c +++ b/tests/unit-mcdc/test_integer_fault_whitebox.c @@ -53,7 +53,7 @@ * exactly one earlier op returns MP_MEM, so exactly one operand of one chain is * driven TRUE (short-circuiting the rest) per call. The unarmed baseline call * supplies the all-FALSE half of every pair in the SAME binary (llvm-cov - * computes MC/DC per binary; the campaign unions the "independence shown" bit + * computes MC/DC per binary; the harness unions the "independence shown" bit * across binaries by line:col). * * Which operands are alloc-closable: mp_abs (grows a fresh temp from NULL), diff --git a/tests/unit-mcdc/test_integer_whitebox.c b/tests/unit-mcdc/test_integer_whitebox.c index a8e155c2be..aac40073ee 100644 --- a/tests/unit-mcdc/test_integer_whitebox.c +++ b/tests/unit-mcdc/test_integer_whitebox.c @@ -10,10 +10,10 @@ * 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 + * one binary (llvm-cov computes MC/DC per binary; the harness 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 + * Build: compiled by the coverage runner'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 @@ -21,7 +21,7 @@ * * 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). + * exit makes the harness discard the variant and its coverage). */ #include @@ -1227,7 +1227,7 @@ int main(void) #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. */ + * suite discard this variant's coverage. */ return 0; #endif } diff --git a/tests/unit-mcdc/test_kdf_hash_fault_whitebox.c b/tests/unit-mcdc/test_kdf_hash_fault_whitebox.c index 97ae8fdbd7..37d2c0d771 100644 --- a/tests/unit-mcdc/test_kdf_hash_fault_whitebox.c +++ b/tests/unit-mcdc/test_kdf_hash_fault_whitebox.c @@ -95,7 +95,7 @@ * targets, with an #else stub, so this TU builds under kdf_default, * small_stack, ticket_nonce_malloc and crypto_cb alike. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with kdf.o removed. Not part of the wolfSSL build. */ diff --git a/tests/unit-mcdc/test_kdf_whitebox.c b/tests/unit-mcdc/test_kdf_whitebox.c index ed78089e6f..c8b3db03fa 100644 --- a/tests/unit-mcdc/test_kdf_whitebox.c +++ b/tests/unit-mcdc/test_kdf_whitebox.c @@ -29,7 +29,7 @@ * * 1. wc_Tls13_HKDF_Extract_ex() (~line 354) * if (prk == NULL || (ikm == NULL && ikmLen > 0)) - * A public entry point, but the campaign's group tests only ever call it + * A public entry point, but the group tests only ever call it * with a valid prk and a present ikm, so none of the three operands gets * a pair. All four call shapes are memory-safe: the guard short-circuits * before either pointer is read, and the accepted "ikm == NULL && @@ -76,7 +76,7 @@ * this file cannot do. They are closed in the sibling translation unit * tests/unit-mcdc/test_kdf_hash_fault_whitebox.c instead. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with kdf.o removed. Not part of the wolfSSL build. */ diff --git a/tests/unit-mcdc/test_lms_bds_whitebox.c b/tests/unit-mcdc/test_lms_bds_whitebox.c new file mode 100644 index 0000000000..d464e2dcd2 --- /dev/null +++ b/tests/unit-mcdc/test_lms_bds_whitebox.c @@ -0,0 +1,786 @@ +/* test_lms_bds_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC BDS-state white-box supplement for wolfcrypt/src/wc_lms_impl.c. + * + * WHAT THIS FILE ADDS OVER THE OTHER THREE LMS WHITE-BOXES + * ------------------------------------------------------- + * test_wc_lms_impl_whitebox.c / _gap.c drive the file-static helpers with + * bad arguments; test_lms_hash_fault_whitebox.c sweeps a *global* hash-call + * index over whole make_key / sign / verify / reload operations. That sweep + * is necessarily strided: one WOTS leaf is p * (2^w - 1) = 34 * 255 = 8670 + * primitive calls, so the handful of Merkle *interior* node hashes are one + * call in ~8700 and a strided sweep essentially never lands on them. The + * decisions that need `ret != 0` at an interior-node step therefore stayed + * open no matter how many sweep points were spent on them. + * + * This file replaces the search with arithmetic. Every target is reached by + * calling the static helper DIRECTLY with a hand-built LmsParams / + * LmsPrivState, and where a fault is needed the fault index is COMPUTED: + * + * L = mcdc_fh_seen() after one wc_lms_leaf_hash() (constant per leaf) + * C = mcdc_fh_seen() after one wc_lms_interior_hash() (constant per node) + * + * In wc_lms_treehash{,_init}() the loop body is exactly "leaf hash, then the + * carry chain of interior hashes", so leaf 0 occupies primitive calls + * [1 .. L], leaf 1 occupies [L+1 .. 2L], and the FIRST interior hash (i=1, + * h=1) occupies [2L+1 .. 2L+C]. Arming there fails that interior hash and + * nothing before it, which is precisely the state the `(ret == 0)` operand + * of the auth-path store needs. A small window around the computed index is + * swept so the vector survives a change in how many primitives one hash + * costs (raw-block vs WC_LMS_FULL_HASH vs SHAKE). + * + * wc_lms_compute_root() is small enough (one leaf hash + `height` node + * hashes) that its whole primitive range is swept DENSELY, which is what + * reaches the per-hash-family copies of the auth-path climb loop. + * + * TARGETS (suite/reports/lms/the uncovered-condition report keys, wc_lms_impl.c) + * 2109:...:0 wc_lms_treehash() `ret == 0` at the auth-path store + * (WOLFSSL_WC_LMS_SMALL arm) -- fault + * 2262:...:0 wc_lms_treehash_init() same, table-based arm -- fault + * 2397:...:2 wc_lms_treehash_update() `h <= params->height` -- direct call + * with a leaf index whose trailing-ones run exceeds the tree + * height; no in-tree caller can pass one, which is exactly why + * the guard exists and why only a white-box can falsify it. + * 2414:...:2 wc_lms_treehash_update() `!useRoot` -- direct call + * with q == 0 AND useRoot != 0 (real callers only ever pair + * useRoot=1 with q != 0), plus the all-true partner row in the + * same binary (q == 0, useRoot = 0). + * 2659:...:0 wc_lms_compute_root() SHAKE256 climb loop -- fault + * 2685:...:0 wc_lms_compute_root() SHA-256/192 climb loop -- fault + * 3359:...:1 wc_hss_update_auth_path() `i >= 0` -- direct call + * with levels == 1 and q == 0 so the loop runs off the bottom + * instead of taking the `break` in the q != 0 arm. + * 4120:...:0 wc_hss_verify() `ret == 0` -- public key + * whose encoded level count disagrees with the parameters. + * + * PAIRING (HARD RULE: MC/DC independence is computed per binary). Every + * rejecting vector above is issued together with its accepting partner in + * THIS program: the fault sweeps always run one DISARMED baseline first, the + * treehash_update rows are issued as a useRoot=0 / useRoot=1 pair over the + * same leaf range, and wc_hss_verify() is called twice (matching and + * mismatching level count). + * + * DETERMINISM: this file uses NO RNG at all -- no wc_InitRng, no + * wc_hss_make_key. Every input is a fixed byte pattern and every fault index + * is derived from measured, input-independent counts, so two runs of an + * unchanged tree produce byte-identical coverage. (The lms module's coverage + * is known to depend on RNG-driven key diversity across variants; nothing + * here pins or perturbs any RNG, so that diversity is untouched.) + * + * WHAT THIS FILE DELIBERATELY DOES NOT COVER + * ------------------------------------------ + * `2254:...:1` and `2414:...:4` are the SAME operand text, + * + * ((i >> (h-1)) != ((i + 1) >> (h - 1))) + * + * in wc_lms_treehash_init() and wc_lms_treehash_update(). It is a tautology + * at every evaluation, so no vector can pair it. Both sites reach it only + * from inside + * + * while ((ret == 0) && ((j & 0x1) == 1)) { ...; j >>= 1; h++; ... } + * + * whose k-th entry requires the pre-shift value i >> (k-1) to be odd. On + * arrival with a given h, bits 0 .. h-1 of i are therefore all set, i.e. + * i == (i >> (h-1)) * 2^(h-1) + (2^(h-1) - 1). Adding one carries out of + * bit h-1, so (i + 1) >> (h - 1) == (i >> (h - 1)) + 1: the two sides differ + * by exactly one on every evaluation and the `!=` is never false. Recorded + * in the exclusion record and the exclusion record. + * + * COST: no keygen and no signing. The most expensive driver computes 8 WOTS + * leaves; the whole program is a few hundred thousand SHA-256 blocks, orders + * of magnitude inside the 600 s TEST_TIMEOUT. + * + * VARIANTS (HARD RULE: must compile under every variant of the module): + * WOLFSSL_LMS_VERIFY_ONLY - keygen/signing static helpers are compiled + * out, so only the compute_root sweep and the + * wc_hss_verify rows are built. + * WOLFSSL_WC_LMS_SMALL - selects wc_lms_treehash(); the table-based + * treehash_init/update/update_auth_path drivers + * are compiled out (and vice versa). + * WOLFSSL_LMS_SHA256_192 / WOLFSSL_LMS_SHAKE256 - add a family row to the + * compute_root sweep table. + * main() always returns 0; setup problems are printed skips. + */ + +#include "mcdc_fault_hash.h" + +/* wc_lms_impl.c is #included AFTER the interposers are installed. */ +#include + +#include +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFSSL_HAVE_LMS) && !defined(WOLFSSL_NO_LMS_SHA256_256) + +#define WB_HAVE_DRIVER 1 + +/* Signing-side static helpers (treehash / auth-path) exist only outside the + * verify-only build; the two treehash flavours are mutually exclusive. */ +#ifdef WOLFSSL_LMS_VERIFY_ONLY + #define WB_TREE_SMALL 0 + #define WB_TREE_TABLE 0 +#elif defined(WOLFSSL_WC_LMS_SMALL) + #define WB_TREE_SMALL 1 + #define WB_TREE_TABLE 0 +#else + #define WB_TREE_SMALL 0 + #define WB_TREE_TABLE 1 +#endif + +/* Largest hash length over all compiled families; sizes every fixed buffer. */ +#define WB_HLEN_MAX WC_SHA256_DIGEST_SIZE /* 32 */ +/* Winternitz w=8, wb=3: LMS_V = 2, ls = 0, p = hash_len + 2. */ +#define WB_WIDTH 8U +#define WB_LS 0U +#define WB_P_OF(hLen) ((word16)((hLen) + 2U)) + +/* Tree shape used by every driver here. Height 2 is the smallest that gives + * the carry chain more than one level (h = 1 and h = 2), which is all the + * targeted decisions need; keygen cost is 2^height WOTS leaves and each leaf + * is p * 255 hash calls, so height is the entire runtime budget. */ +#define WB_HEIGHT 2U +#define WB_ROOTLEVELS 2U +#define WB_CACHEBITS 2U +/* Generous fixed buffers: several drivers deliberately drive indices past + * what an in-tree caller would produce (that is the point of 2397), so the + * auth-path / root / stack buffers are sized well beyond the tree shape. */ +#define WB_NODES 32U + +typedef struct WbFam { + const char* name; + word16 lmsType; + word16 lmOtsType; + word16 hash_len; +} WbFam; + +static const WbFam wb_fams[] = { + { "sha256_256", LMS_SHA256_M32_H5, LMOTS_SHA256_N32_W8, + WC_SHA256_DIGEST_SIZE }, +#ifdef WOLFSSL_LMS_SHA256_192 + { "sha256_192", LMS_SHA256_M24_H5, LMOTS_SHA256_N24_W8, 24 }, +#endif +#ifdef WOLFSSL_LMS_SHAKE256 + { "shake256", LMS_SHAKE_M32_H5, LMOTS_SHAKE_N32_W8, + WC_SHA256_DIGEST_SIZE }, +#endif +}; +#define WB_NFAMS (sizeof(wb_fams) / sizeof(wb_fams[0])) + +/* Build a self-consistent LmsParams by hand. This TU never goes through + * wc_lms.c, so the values need only be internally consistent (see the same + * construction in test_wc_lms_impl_whitebox_gap.c). */ +static void wb_params(LmsParams* p, const WbFam* f, word8 levels, word8 height) +{ + XMEMSET(p, 0, sizeof(*p)); + p->levels = levels; + p->height = height; + p->width = (word8)WB_WIDTH; + p->ls = (word8)WB_LS; + p->p = WB_P_OF(f->hash_len); + p->lmsType = f->lmsType; + p->lmOtsType = f->lmOtsType; + p->hash_len = f->hash_len; + p->sig_len = 4U + + (word32)levels * LMS_SIG_LEN(height, p->p, p->hash_len) + + (word32)(levels - 1U) * LMS_PUBKEY_LEN(p->hash_len); +#ifndef WOLFSSL_WC_LMS_SMALL + p->rootLevels = (word8)WB_ROOTLEVELS; + p->cacheBits = (word8)WB_CACHEBITS; +#endif +} + +/* Mirrors wc_lmskey_state_init()/_free() in wc_lms.c (static in another TU). + * wc_InitSha256 / wc_InitShake256 are NOT interposed by mcdc_fault_hash.h, + * so this setup can never be faulted. */ +static int wb_state_init(LmsState* state, const LmsParams* params) +{ + int ret; + + XMEMSET(state, 0, sizeof(*state)); + state->params = params; + +#ifdef WOLFSSL_LMS_SHAKE256 + if (LMS_IS_SHAKE(params->lmOtsType)) { + ret = wc_InitShake256(LMS_STATE_SHAKE(state), NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_InitShake256(LMS_STATE_SHAKE_K(state), NULL, + INVALID_DEVID); + if (ret != 0) { + wc_Shake256_Free(LMS_STATE_SHAKE(state)); + } + } + return ret; + } +#endif + + ret = wc_InitSha256(LMS_STATE_HASH(state)); + if (ret == 0) { + ret = wc_InitSha256(LMS_STATE_HASH_K(state)); + if (ret != 0) { + wc_Sha256Free(LMS_STATE_HASH(state)); + } + } + return ret; +} + +static void wb_state_free(LmsState* state) +{ +#ifdef WOLFSSL_LMS_SHAKE256 + if (LMS_IS_SHAKE(state->params->lmOtsType)) { + wc_Shake256_Free(LMS_STATE_SHAKE_K(state)); + wc_Shake256_Free(LMS_STATE_SHAKE(state)); + return; + } +#endif + wc_Sha256Free(LMS_STATE_HASH_K(state)); + wc_Sha256Free(LMS_STATE_HASH(state)); +} + +/* Fixed, RNG-free inputs. */ +static const byte wb_id[LMS_I_LEN] = { + 0xA0,0xA1,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7, + 0xA8,0xA9,0xAA,0xAB,0xAC,0xAD,0xAE,0xAF +}; +static byte wb_seed[WB_HLEN_MAX]; + +static void wb_init_seed(void) +{ + unsigned i; + for (i = 0; i < (unsigned)sizeof(wb_seed); i++) { + wb_seed[i] = (byte)(0x5A + i); + } +} + +/******************************************************************* + * 2659:...:0 (SHAKE256 arm) and 2685:...:0 (SHA-256/192 arm), plus the + * SHA-256/256 twin of the same loop: + * + * for (i = 0; (ret == 0) && (i < params->height - 1); i++) + * + * inside wc_lms_compute_root(). The `ret == 0` operand can only go false + * when the node hash of an EARLIER iteration failed, and the loop lives at + * the very end of a verify, behind ~4300 WOTS primitive calls -- out of + * reach of any strided sweep. + * + * wc_lms_compute_root() is a static, so it is called here on its own: one + * leaf hash followed by `height` node hashes, a handful of primitive calls + * in total, which lets the sweep be DENSE over [1 .. K]. Point n = C + 1 + * (C = primitive calls per node hash) fails the first loop iteration and + * nothing before it, giving the (ret != 0) row; the disarmed baseline that + * measured K is the accepting row, in this same binary. + * + * Each compiled hash family gets its own pass because wc_lms_compute_root() + * keeps a separate copy of the climb loop per family arm. + ******************************************************************/ +static void wb_compute_root_sweep(const WbFam* f) +{ + LmsParams params; + LmsState state; + byte kc[WB_HLEN_MAX]; + byte tc[WB_HLEN_MAX]; + byte path[WB_NODES * WB_HLEN_MAX]; + long k, n; + int ret; + + /* height 3 so the "all but last height" loop runs more than once. */ + wb_params(¶ms, f, 1, 3); + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for compute_root sweep"); + wb_fail = 1; + return; + } + XMEMSET(state.buffer, 0x3C, sizeof(state.buffer)); + XMEMCPY(state.buffer, wb_id, LMS_I_LEN); + XMEMSET(kc, 0x11, sizeof(kc)); + XMEMSET(tc, 0, sizeof(tc)); + XMEMSET(path, 0x22, sizeof(path)); + + /* Disarmed baseline: the all-true row for every guard in the function, + * and the sweep length K. */ + mcdc_fh_disarm(); + ret = wc_lms_compute_root(&state, 1, kc, path, tc); + k = mcdc_fh_seen(); + if (ret != 0) { + WB_NOTE("baseline wc_lms_compute_root failed; sweep skipped"); + wb_fail = 1; + wb_state_free(&state); + return; + } + + for (n = 1; n <= k; n++) { + mcdc_fh_arm(n); + (void)wc_lms_compute_root(&state, 1, kc, path, tc); + mcdc_fh_disarm(); + } + + wb_state_free(&state); + printf(" [wb] compute_root sweep (%s): K=%ld, dense\n", f->name, k); +} + +/******************************************************************* + * 4120:...:0 wc_hss_verify(): if ((ret == 0) && (nspk + 1 != levels)) + * + * The `ret == 0` operand is false exactly when the immediately preceding + * check rejected the key: `if (levels != state->params->levels)`. Both rows + * are issued here, in this binary: + * + * A. encoded level count = params->levels + 1 -> ret != 0 at :4120, + * decision false with operand 0 false. + * B. encoded level count = params->levels, nspk deliberately wrong + * -> both operands true, decision true. + * + * Neither call reads past the 4-byte L field of the key or of the + * signature: wc_hss_verify() returns SIG_VERIFY_E before the chain walk. + ******************************************************************/ +static void wb_hss_verify_levels(const WbFam* f) +{ + LmsParams params; + LmsState state; + byte pub[HSS_PUBLIC_KEY_LEN(WB_HLEN_MAX)]; + byte sig[64]; + static const byte msg[] = "4120 hss_verify level-count message"; + int ret; + + wb_params(¶ms, f, 1, WB_HEIGHT); + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for hss_verify level rows"); + wb_fail = 1; + return; + } + XMEMSET(pub, 0, sizeof(pub)); + XMEMSET(sig, 0, sizeof(sig)); + + /* Row A: level count in the public key disagrees with the parameters. */ + c32toa((word32)params.levels + 1U, pub); + c32toa(0, sig); + ret = wc_hss_verify(&state, pub, msg, (word32)sizeof(msg), sig, + (word32)sizeof(sig)); + if (ret == 0) { + WB_NOTE("hss_verify accepted a key with the wrong level count"); + wb_fail = 1; + } + + /* Row B: level count matches, nspk does not. */ + c32toa((word32)params.levels, pub); + c32toa(7, sig); + ret = wc_hss_verify(&state, pub, msg, (word32)sizeof(msg), sig, + (word32)sizeof(sig)); + if (ret == 0) { + WB_NOTE("hss_verify accepted a signature with the wrong nspk"); + wb_fail = 1; + } + + wb_state_free(&state); + WB_NOTE("4120 hss_verify level-count rows issued"); +} + +#if WB_TREE_SMALL || WB_TREE_TABLE +/* Primitive-call cost of one leaf hash (L) and one interior node hash (C). + * Both are input independent: the WOTS chain length is (2^width - 1) and the + * node hash is a single fixed-length hash, so measuring one of each is + * enough to locate any leaf/node boundary in the treehash loops. */ +static int wb_measure_lc(LmsState* state, long* pL, long* pC) +{ + byte temp[WB_HLEN_MAX]; + byte left[WB_HLEN_MAX]; + int ret; + + XMEMSET(temp, 0, sizeof(temp)); + XMEMSET(left, 0x77, sizeof(left)); + + XMEMCPY(state->buffer, wb_id, LMS_I_LEN); + mcdc_fh_disarm(); + ret = wc_lms_leaf_hash(state, wb_seed, 0, 4, temp); + *pL = mcdc_fh_seen(); + if (ret != 0) { + return ret; + } + + XMEMCPY(state->buffer, wb_id, LMS_I_LEN); + mcdc_fh_disarm(); + ret = wc_lms_interior_hash(state, left, 2, temp); + *pC = mcdc_fh_seen(); + mcdc_fh_disarm(); + return ret; +} +#endif /* WB_TREE_SMALL || WB_TREE_TABLE */ + +#if WB_TREE_TABLE +/******************************************************************* + * 2262:...:0 wc_lms_treehash_init(): + * if ((ret == 0) && (auth_path != NULL) && (((q >> h) ^ 0x1) == j)) + * + * inside the carry `while ((ret == 0) && ((j & 0x1) == 1))` loop. Reaching + * this decision with ret != 0 needs wc_lms_interior_hash() -- and NOT the + * leaf hash before it -- to fail, because a failed leaf hash keeps the while + * loop from being entered at all. + * + * The loop body is exactly "one leaf hash then the carry chain", so with L + * primitive calls per leaf and C per node hash, leaf 0 is [1 .. L], leaf 1 is + * [L+1 .. 2L], and the first interior hash is [2L+1 .. 2L+C]. The sweep runs + * a small window around that so it stays correct if a hash costs a different + * number of primitives (raw block vs WC_LMS_FULL_HASH vs SHAKE). + * + * The disarmed baseline call is the accepting partner: with q = 1 and + * height = 2, leaf i = 3 climbs to h = 1 with j = 1 and (q >> 1) ^ 1 == 1, + * so all three operands are true there. + ******************************************************************/ +static void wb_treehash_init_fault(const WbFam* f) +{ + LmsParams params; + LmsState state; + LmsPrivState priv; + byte auth_path[WB_NODES * WB_HLEN_MAX]; + byte stack_buf[WB_NODES * WB_HLEN_MAX]; + byte root_buf[WB_NODES * WB_HLEN_MAX]; + byte leaf_cache[WB_NODES * WB_HLEN_MAX]; + long L = 0, C = 0, n, lo, hi; + int ret; + + wb_params(¶ms, f, 1, WB_HEIGHT); + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for treehash_init fault"); + wb_fail = 1; + return; + } + 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_measure_lc(&state, &L, &C) != 0) { + WB_NOTE("leaf/interior hash cost measurement failed"); + wb_fail = 1; + wb_state_free(&state); + return; + } + + /* Accepting baseline (disarmed). */ + XMEMSET(&priv, 0, sizeof(priv)); + priv.auth_path = auth_path; + priv.stack.stack = stack_buf; + priv.root = root_buf; + priv.leaf.cache = leaf_cache; + mcdc_fh_disarm(); + ret = wc_lms_treehash_init(&state, &priv, wb_id, wb_seed, 1); + mcdc_fh_disarm(); + if (ret != 0) { + WB_NOTE("baseline wc_lms_treehash_init failed; fault window skipped"); + wb_fail = 1; + wb_state_free(&state); + return; + } + + lo = 2 * L + 1; + hi = 2 * L + 2 * C; + for (n = lo; n <= hi; n++) { + XMEMSET(&priv, 0, sizeof(priv)); + priv.auth_path = auth_path; + priv.stack.stack = stack_buf; + priv.root = root_buf; + priv.leaf.cache = leaf_cache; + mcdc_fh_arm(n); + (void)wc_lms_treehash_init(&state, &priv, wb_id, wb_seed, 1); + mcdc_fh_disarm(); + } + + wb_state_free(&state); + printf(" [wb] treehash_init fault window: L=%ld C=%ld n=[%ld..%ld]\n", + L, C, lo, hi); +} + +/******************************************************************* + * 2397:...:2 wc_lms_treehash_update(): + * if (useRoot && (h > params->height - params->rootLevels) && + * (h <= params->height)) + * 2414:...:2 wc_lms_treehash_update(): + * if ((ret == 0) && (q == 0) && (!useRoot) && + * (h > params->height - params->rootLevels) && ...) + * 2414:...:0 and 2424:...:0 wc_lms_treehash_update(): the `ret == 0` + * operand of the same root copy and of the auth-path store below it. + * + * Four direct calls over the same fixed leaf range, all with q == 0: + * + * 1. useRoot = 0, leaves [0 .. 3] -> 2414 all-true row (accepting). + * 2. useRoot = 1, leaves [0 .. 3] -> 2414 with `!useRoot` false while + * ret == 0 and q == 0 hold (rejecting partner for cond 2), and 2397 + * all-true. + * 3. useRoot = 1, leaves [0 .. 7] -> leaf 7 has three trailing one bits, + * so the carry chain climbs to h = 3 on a height-2 tree and + * `h <= params->height` is FALSE with the two preceding operands true. + * No in-tree caller can produce that index (wc_hss_update_auth_path() + * derives max_idx from LMS_AUTH_PATH_IDX(), bounded by 2^height - 1), + * which is why the bound exists and why only a direct call can falsify + * it. Every buffer here is sized WB_NODES nodes, well past what the + * over-long climb indexes. + * + * 4. useRoot = 0, leaves [0 .. 3], armed at primitive call 1 -> the two + * `ret == 0` operands (2414 cond 0, 2424 cond 0) go false, paired with + * call 1's all-true rows. The fault index needs no arithmetic here: + * leaves 0..3 are served from the leaf cache, so wc_lms_treehash_update + * issues NO primitive call until the carry chain of leaf 1 reaches + * wc_lms_interior_hash() -- primitive call 1 IS that node hash. + * These two conditions are the pair the 2026-08-11 flake + * hunt recorded as non-deterministic (they depend on where the global + * strided hash-fault sweep in test_lms_hash_fault_whitebox.c happens to + * land, which moves with the RNG-drawn key). This vector pins them. + * + * leaf.idx starts at 0 with cacheBits = 2, so leaves 0..3 are served from + * the (zeroed) leaf cache -- the tree content is irrelevant to these + * decisions, it keeps calls 1, 2 and 4 nearly free, and it is what makes + * call 4's fault index exact. + ******************************************************************/ +static void wb_treehash_update_roots(const WbFam* f) +{ + LmsParams params; + LmsState state; + LmsPrivState priv; + byte auth_path[WB_NODES * WB_HLEN_MAX]; + byte stack_buf[WB_NODES * WB_HLEN_MAX]; + byte root_buf[WB_NODES * WB_HLEN_MAX]; + byte leaf_cache[WB_NODES * WB_HLEN_MAX]; + int ret; + int i; + static const struct { + word32 max_idx; int useRoot; long arm; const char* what; + } calls[] = { + { 3, 0, 0, "q=0 useRoot=0 (2414/2424 accepting rows)" }, + { 3, 1, 0, "q=0 useRoot=1 (2414 cond-2 rejecting row)" }, + { 7, 1, 0, "leaf 7 on a height-2 tree (2397 cond-2 rejecting row)" }, + { 3, 0, 1, "q=0 useRoot=0, node hash faulted " + "(2414/2424 cond-0 rejecting rows)" }, + }; + + wb_params(¶ms, f, 1, WB_HEIGHT); + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for treehash_update roots"); + wb_fail = 1; + return; + } + XMEMSET(auth_path, 0, sizeof(auth_path)); + XMEMSET(stack_buf, 0, sizeof(stack_buf)); + XMEMSET(root_buf, 0x66, sizeof(root_buf)); + XMEMSET(leaf_cache, 0x44, sizeof(leaf_cache)); + + mcdc_fh_disarm(); + for (i = 0; i < (int)(sizeof(calls) / sizeof(calls[0])); i++) { + XMEMSET(&priv, 0, sizeof(priv)); + priv.auth_path = auth_path; + priv.stack.stack = stack_buf; + priv.stack.offset = 0; + priv.root = root_buf; + priv.leaf.cache = leaf_cache; + priv.leaf.idx = 0; + priv.leaf.offset = 0; + + if (calls[i].arm != 0) { + mcdc_fh_arm(calls[i].arm); + } + ret = wc_lms_treehash_update(&state, &priv, wb_id, wb_seed, 0, + calls[i].max_idx, 0, calls[i].useRoot); + mcdc_fh_disarm(); + if ((calls[i].arm == 0) && (ret != 0)) { + printf(" [wb] treehash_update %s returned %d\n", calls[i].what, + ret); + wb_fail = 1; + } + else if ((calls[i].arm != 0) && (ret == 0)) { + printf(" [wb] treehash_update %s did NOT propagate the faulted " + "node hash\n", calls[i].what); + wb_fail = 1; + } + } + + wb_state_free(&state); + WB_NOTE("2397/2414 treehash_update useRoot + over-long climb rows issued"); +} + +/******************************************************************* + * 3359:...:1 wc_hss_update_auth_path(): + * for (i = levels - 1; (ret == 0) && (i >= 0); i--) + * + * The `i >= 0` operand is only false when the loop runs off the bottom, and + * every level's arm for q != 0 ends in an unconditional `break`. So the loop + * reaches i == -1 only if EVERY level's q is zero -- which for the in-tree + * caller (wc_hss_sign(), after wc_lms_idx_inc()) means a raw index of 0, a + * state that call site can never present. + * + * A direct call with levels = 1 and a zeroed private key gives it: q == 0 + * skips the break arm, i drops to -1 and the loop condition is re-evaluated + * with ret == 0 and i >= 0 false. The i = 0 pass of the same call is the + * accepting row. The per-level LmsPrivState is given real buffers so the + * WOLFSSL_LMS_NO_SIGN_SMOOTHING spelling of the q == 0 arm (which calls + * wc_lms_treehash_init() instead of doing nothing) is equally safe. + ******************************************************************/ +static void wb_update_auth_path_bottom(const WbFam* f) +{ + LmsParams params; + LmsState state; + HssPrivKey pk; + byte priv_buf[LMS_MAX_LEVELS * LMS_PRIV_LEN(WB_HLEN_MAX)]; + byte priv_raw[HSS_PRIVATE_KEY_LEN(WB_HLEN_MAX)]; + byte auth_path[WB_NODES * WB_HLEN_MAX]; + byte stack_buf[WB_NODES * WB_HLEN_MAX]; + byte root_buf[WB_NODES * WB_HLEN_MAX]; + byte leaf_cache[WB_NODES * WB_HLEN_MAX]; + int ret; + + wb_params(¶ms, f, 1, WB_HEIGHT); + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for update_auth_path bottom"); + wb_fail = 1; + return; + } + XMEMSET(&pk, 0, sizeof(pk)); + XMEMSET(priv_buf, 0, sizeof(priv_buf)); + XMEMSET(priv_raw, 0, sizeof(priv_raw)); + 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)); + + pk.priv = priv_buf; + pk.state[0].auth_path = auth_path; + pk.state[0].stack.stack = stack_buf; + pk.state[0].root = root_buf; + pk.state[0].leaf.cache = leaf_cache; + + mcdc_fh_disarm(); + ret = wc_hss_update_auth_path(&state, &pk, priv_raw, 1); + if (ret != 0) { + WB_NOTE("wc_hss_update_auth_path(levels=1, q=0) failed"); + wb_fail = 1; + } + + wb_state_free(&state); + WB_NOTE("3359 update_auth_path run-off-the-bottom row issued"); +} +#endif /* WB_TREE_TABLE */ + +#if WB_TREE_SMALL +/******************************************************************* + * 2109:...:0 wc_lms_treehash() (WOLFSSL_WC_LMS_SMALL arm): + * if ((ret == 0) && (auth_path != NULL) && (((q >> h) ^ 0x1) == j)) + * + * Same shape and same arithmetic as the table-based treehash_init above: + * the first interior hash of the recompute treehash occupies primitive + * calls [2L+1 .. 2L+C]. The disarmed baseline (q = 1, height = 2, leaf + * i = 3 climbing to h = 1) is the accepting partner in this binary. + ******************************************************************/ +static void wb_treehash_small_fault(const WbFam* f) +{ + LmsParams params; + LmsState state; + byte auth_path[WB_NODES * WB_HLEN_MAX]; + byte pub[WB_HLEN_MAX]; + long L = 0, C = 0, n, lo, hi; + int ret; + + wb_params(¶ms, f, 1, WB_HEIGHT); + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for small treehash fault"); + wb_fail = 1; + return; + } + XMEMSET(auth_path, 0, sizeof(auth_path)); + XMEMSET(pub, 0, sizeof(pub)); + + if (wb_measure_lc(&state, &L, &C) != 0) { + WB_NOTE("leaf/interior hash cost measurement failed"); + wb_fail = 1; + wb_state_free(&state); + return; + } + + mcdc_fh_disarm(); + ret = wc_lms_treehash(&state, wb_id, wb_seed, 1, auth_path, pub); + mcdc_fh_disarm(); + if (ret != 0) { + WB_NOTE("baseline wc_lms_treehash failed; fault window skipped"); + wb_fail = 1; + wb_state_free(&state); + return; + } + + lo = 2 * L + 1; + hi = 2 * L + 2 * C; + for (n = lo; n <= hi; n++) { + mcdc_fh_arm(n); + (void)wc_lms_treehash(&state, wb_id, wb_seed, 1, auth_path, pub); + mcdc_fh_disarm(); + } + + wb_state_free(&state); + printf(" [wb] small treehash fault window: L=%ld C=%ld n=[%ld..%ld]\n", + L, C, lo, hi); +} +#endif /* WB_TREE_SMALL */ + +#endif /* WOLFSSL_HAVE_LMS && !WOLFSSL_NO_LMS_SHA256_256 */ + +int main(void) +{ + /* Unbuffered: a TEST_TIMEOUT kill discards anything still in the stdio + * buffer, which reads as an empty log and no clue where it stopped. */ + setvbuf(stdout, NULL, _IONBF, 0); + printf("wc_lms_impl.c BDS-state white-box supplement\n"); + +#ifdef WB_HAVE_DRIVER + { + size_t i; + + wb_init_seed(); + + /* Family-specific: wc_lms_compute_root() keeps one copy of the + * auth-path climb loop per compiled hash family. */ + for (i = 0; i < WB_NFAMS; i++) { + wb_compute_root_sweep(&wb_fams[i]); + } + + /* Family-independent: the remaining targets are in code shared by + * every family, so the default SHA-256/256 family is enough. */ + wb_hss_verify_levels(&wb_fams[0]); +#if WB_TREE_TABLE + wb_treehash_init_fault(&wb_fams[0]); + wb_treehash_update_roots(&wb_fams[0]); + wb_update_auth_path_bottom(&wb_fams[0]); +#elif WB_TREE_SMALL + wb_treehash_small_fault(&wb_fams[0]); +#else + printf(" [wb] WOLFSSL_LMS_VERIFY_ONLY: treehash/auth-path " + "helpers not compiled; those rows are skipped\n"); +#endif + mcdc_fh_disarm(); + } +#else + printf(" [wb] LMS (SHA-256/256) not compiled in; nothing to do\n"); +#endif + + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Setup problems are printed skips, never a non-zero exit: a non-zero + * exit makes the harness discard this binary's whole coverage. */ + return 0; +} diff --git a/tests/unit-mcdc/test_lms_fault_whitebox.c b/tests/unit-mcdc/test_lms_fault_whitebox.c index efd29da869..20ddf961c9 100644 --- a/tests/unit-mcdc/test_lms_fault_whitebox.c +++ b/tests/unit-mcdc/test_lms_fault_whitebox.c @@ -37,7 +37,7 @@ * Keygen cost: every real wc_LmsKey_MakeKey/Reload below uses the smallest * mapped parameter set, levels=1 height=5 width=8 (WC_LMS_PARM_L1_H5_W8, 32 * leaves) -- the same set test_wc_lms_impl_whitebox.c uses for its per-family - * roundtrip. WOLFSSL_LMS_MAX_LEVELS is pinned to 2 by this module's campaign + * roundtrip. WOLFSSL_LMS_MAX_LEVELS is pinned to 2 by this module's suite * config, so no larger key is attempted here. * * VERIFY_ONLY: wc_LmsKey_SetLmsParm/SetParameters(_ex)/GetParameters(_ex), @@ -51,7 +51,7 @@ * whole group is behind one #ifndef with a skip stub. * * No allocation-fault sweep here: every uncovered decision in - * campaign/reports/lms/GAPS.md for wc_lms.c is a NULL/argument guard or a + * suite/reports/lms/the uncovered-condition report for wc_lms.c is a NULL/argument guard or a * state-machine check, not a post-XMALLOC error chain, so mcdc_fault_alloc.h * is not needed by this file. */ @@ -96,6 +96,45 @@ static int wb_read_key(byte* priv, word32 privSz, void* context) * SigsLeft() and bails out with BAD_STATE_E/NOSIGS before ever touching the * rest of the buffer (wc_lms.c:1359, ahead of the wc_hss_reload_key() call), * so the remaining bytes are left zeroed. */ +#ifndef WOLFSSL_LMS_VERIFY_ONLY +/* 1258 (see wb_makekey_checks): the LmsKey whose Q counter the exhausting + * write callback below advances, or NULL to leave it alone. */ +static LmsKey* wb_exhaust_key = NULL; + +/* Write callback that saves the key and then advances the LIVE key's Q + * counter to the total leaf count, so wc_LmsKey_SigsLeft() reads the key as + * exhausted on the very next statement. + * + * wc_LmsKey_MakeKey() runs exactly this callback between wc_hss_make_key() + * and its "This should not happen" SigsLeft() guard, so this is the only + * point at which the state that guard defends against can be presented -- and + * presenting it is what an MC/DC pair for that decision requires. The Q value + * matches the parameter set used below (levels=1, height=5 -> 32 leaves); + * wc_hss_sigsleft() returns w64LT(q, 1 << (levels*height)), which is 0 for + * q == 32. */ +static int wb_write_key_exhaust(const byte* priv, word32 privSz, void* context) +{ + int rc = wb_write_key(priv, privSz, context); + + if ((rc == WC_LMS_RC_SAVED_TO_NV_MEMORY) && (wb_exhaust_key != NULL)) { + w64wrapper q = w64From32(0, (word32)1U << 5); + c64toa(&q, wb_exhaust_key->priv_raw); + } + return rc; +} + +/* Write callback that reports a failed NV write: wc_LmsKey_MakeKey() turns + * that into IO_FAILED_E, which is the only way ret is non-zero when the + * SigsLeft() guard at 1258 is reached. */ +static int wb_write_key_fail(const byte* priv, word32 privSz, void* context) +{ + (void)priv; + (void)privSz; + (void)context; + return -1; +} +#endif /* !WOLFSSL_LMS_VERIFY_ONLY */ + static int wb_read_exhausted(byte* priv, word32 privSz, void* context) { w64wrapper q; @@ -110,7 +149,7 @@ static int wb_read_exhausted(byte* priv, word32 privSz, void* context) /******************************************************************* * wc_LmsKey_InitId (665, 668, 674) / wc_LmsKey_InitLabel (698, 703). - * Only compiled when WOLF_PRIVATE_KEY_ID is set; this campaign's base + * Only compiled when WOLF_PRIVATE_KEY_ID is set; this suite's base * enables HAVE_PK_CALLBACKS, which settings.h auto-derives it from. * * 665: if ((key == NULL) || ((id == NULL) && (len != 0))) @@ -132,7 +171,7 @@ static int wb_read_exhausted(byte* priv, word32 privSz, void* context) * B=F (id==NULL, len!=0, ret==0) is UNREACHABLE: 665 already forces * ret=BAD_FUNC_ARG whenever id==NULL && len!=0, so "ret==0 && id==NULL" * can never coexist with len!=0. No test call issued for this row -- - * DEATHNOTE candidate, see task report. + * ******************************************************************/ #ifdef WOLF_PRIVATE_KEY_ID static void wb_initid(void) @@ -317,7 +356,7 @@ static void wb_setlmsparm_setparams(void) WB_NOTE("819 SetParameters state leaves closed"); /* --- 879 SetParameters_ex: only the ret==0 operand is flagged in - * GAPS.md, but its independence pair still needs the *wrong-state* row + * the uncovered-condition report, but its independence pair still needs the *wrong-state* row * held alongside it: for (ret==0) && (state!=INITED), the ret==0 * operand's own pair requires the OTHER operand pinned TRUE (masking * MC/DC on an AND chain -- pinning it FALSE, i.e. the plain success @@ -1087,14 +1126,25 @@ static void wb_sign_checks(WC_RNG* rng) * wc_LmsKey_MakeKey: 1163 (state!=PARMSET), 1195 (write_private_key==NULL), * 1208 (priv_data==NULL, only the FALSE/reuse row is uncovered). * - * 1261 if ((ret==0) && (wc_LmsKey_SigsLeft(key)==0)) -- PROVEN UNREACHABLE: - * wc_hss_make_key() (wc_lms_impl.c) always starts by zeroing Q via - * wc_lms_idx_zero() before it can fail, and wc_hss_sigsleft() (same file) - * with Q==0 is true for any params -- either the "levels*height>=64" - * shortcut forces ret=1 outright, or w64LT(0, 1<<(levels*height)) is true - * for any levels*height>=0. So SigsLeft()==0 can never hold directly after - * a successful wc_hss_make_key(), for any parameter set. No test call is - * possible; DEATHNOTE candidate (see task report), not closed here. + * 1258 if ((ret==0) && (wc_LmsKey_SigsLeft(key)==0)) -- the "This should not + * happen" guard on a freshly generated key. wc_hss_make_key() zeroes Q + * (wc_lms_idx_zero) before it can fail and wc_hss_sigsleft() is true for + * Q==0 under every parameter set, so the guard cannot fire on the value + * make_key itself leaves behind. An earlier pass concluded from that that + * the decision was unreachable. It is not: wc_LmsKey_MakeKey() calls the + * caller-supplied write_private_key() callback BETWEEN wc_hss_make_key() + * and this guard, and that callback is handed the key's own priv_raw + * buffer. Anything the NV-write step does to the stored index is therefore + * visible to the guard -- which is exactly the failure mode a guard placed + * after the write, rather than before it, exists to catch. + * + * Three rows, all in this binary: + * R1 write cb advances Q to the leaf count -> ret==0, SigsLeft()==0 + * -> decision TRUE (BAD_STATE_E, key->state = WC_LMS_STATE_NOSIGS) + * R2 write cb reports a failed NV write -> the I/O failure code + * -> decision FALSE on operand 0 + * R3 plain successful MakeKey -> ret==0, SigsLeft()!=0 + * -> decision FALSE on operand 1 ******************************************************************/ static void wb_makekey_checks(WC_RNG* rng) { @@ -1152,8 +1202,47 @@ static void wb_makekey_checks(WC_RNG* rng) } wc_LmsKey_Free(&key); - WB_NOTE("1163/1195/1208 MakeKey leaves closed (1261 unreachable, see " - "report)"); + /* R2: the NV write fails, so ret != 0 when 1258 is evaluated. */ + { + LmsKey keyIoFail; + + XMEMSET(&keyIoFail, 0, sizeof(keyIoFail)); + wc_LmsKey_Init(&keyIoFail, NULL, INVALID_DEVID); + wc_LmsKey_SetParameters(&keyIoFail, 1, 5, 8); + wc_LmsKey_SetWriteCb(&keyIoFail, wb_write_key_fail); + ret = wc_LmsKey_MakeKey(&keyIoFail, rng); + if (ret != WC_NO_ERR_TRACE(IO_FAILED_E)) { + WB_NOTE("MakeKey(write cb fails) did not report IO_FAILED_E"); + wb_fail = 1; + } + wc_LmsKey_Free(&keyIoFail); + } + + /* R1: the NV write succeeds but leaves the key's index at the leaf + * count, so SigsLeft() is 0 with ret still 0. */ + { + LmsKey keyExhaust; + + XMEMSET(&keyExhaust, 0, sizeof(keyExhaust)); + wc_LmsKey_Init(&keyExhaust, NULL, INVALID_DEVID); + wc_LmsKey_SetParameters(&keyExhaust, 1, 5, 8); + wc_LmsKey_SetWriteCb(&keyExhaust, wb_write_key_exhaust); + wb_exhaust_key = &keyExhaust; + ret = wc_LmsKey_MakeKey(&keyExhaust, rng); + wb_exhaust_key = NULL; + if (ret != WC_NO_ERR_TRACE(BAD_STATE_E)) { + WB_NOTE("MakeKey(exhausted after write) did not report " + "BAD_STATE_E"); + wb_fail = 1; + } + else if (keyExhaust.state != WC_LMS_STATE_NOSIGS) { + WB_NOTE("MakeKey(exhausted after write) left the wrong state"); + wb_fail = 1; + } + wc_LmsKey_Free(&keyExhaust); + } + + WB_NOTE("1163/1195/1208/1258 MakeKey leaves closed"); } /******************************************************************* @@ -1322,7 +1411,7 @@ int main(void) printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Setup/skip conditions are surfaced as notes, not process failures: - * the campaign discards a variant's whole coverage on non-zero exit. */ + * the harness discards a variant's whole coverage on non-zero exit. */ return 0; } diff --git a/tests/unit-mcdc/test_lms_hash_fault_whitebox.c b/tests/unit-mcdc/test_lms_hash_fault_whitebox.c index 73f8549024..45d25c5121 100644 --- a/tests/unit-mcdc/test_lms_hash_fault_whitebox.c +++ b/tests/unit-mcdc/test_lms_hash_fault_whitebox.c @@ -24,7 +24,7 @@ * * WHAT IS LEFT AFTER THE OTHER TWO LMS WHITE-BOXES * ------------------------------------------------ - * campaign/reports/lms/GAPS.md is dominated by ONE shape inside + * suite/reports/lms/the uncovered-condition report is dominated by ONE shape inside * wc_lms_impl.c's WOTS / Merkle / HSS engine: * * for (i = 0; (ret == 0) && (i < params->p); i++) ... @@ -34,7 +34,7 @@ * * The (ret == 0) operand only ever goes FALSE when an earlier step failed * *inside the same operation*. wc_lms_impl.c performs ZERO allocations - * (grep XMALLOC: none), so mcdc_fault_alloc.h -- the campaign's usual lever -- + * (grep XMALLOC: none), so mcdc_fault_alloc.h -- the usual lever -- * has nothing to fault here: `ret` in this file comes exclusively from * wc_Sha256HashBlock / wc_Sha256Update / wc_Sha256Final (and the SHAKE * equivalents). test_wc_lms_impl_whitebox_gap.c already closed everything that @@ -78,7 +78,7 @@ * a CPU-time deadline (WB_DEADLINE_S). The parameter set is the smallest that * still exercises the HSS multi-level machinery: levels=2, height=2 (16 * signatures total, subtree rollover after 4), Winternitz w=8, SHA-256/32. - * WOLFSSL_LMS_MAX_LEVELS is pinned to 2 by this module's campaign config, so + * WOLFSSL_LMS_MAX_LEVELS is pinned to 2 by this module's suite config, so * levels=2 is the maximum available. * * VARIANT COVERAGE (HARD RULE 3): WOLFSSL_LMS_VERIFY_ONLY compiles keygen and @@ -169,7 +169,7 @@ static const WbFamily wb_families[] = { * chosen so the total never exceeds WB_POINTS. */ #define WB_DENSE 48 #define WB_POINTS 192 -/* Hard CPU-time ceiling for the whole program, well under the campaign's +/* Hard CPU-time ceiling for the whole program, well under the * 600 s TEST_TIMEOUT even with variants running concurrently and even in the * (much slower) WOLFSSL_WC_LMS_SMALL recompute build. Every sweep tests it, so * the program degrades to fewer points rather than being killed -- a killed @@ -370,7 +370,7 @@ static int wb_do_make_key(void) * ~60 pinned keys inside one driver, multiplying a 10-minute module by * 30. Trading 40 real conditions for determinism on 3 is a bad deal, * so the two conditions above are recorded as known-flaky in the - * module residual note instead. See STEP6.md, flake hunt 2026-08-11. */ + * module residual note instead. See the working notes, flake hunt 2026-08-11. */ ret = wc_hss_make_key(&state, &wb_rng, wb_priv_raw, &wb_pk, wb_priv_data, wb_pub); wb_state_free(&state); @@ -660,6 +660,6 @@ int main(void) printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Setup/skip conditions are notes, not process failures: a non-zero exit - * makes the campaign discard this binary's whole coverage. */ + * makes the harness discard this binary's whole coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_logging_whitebox.c b/tests/unit-mcdc/test_logging_whitebox.c index 6c7d4523b9..b546c058ca 100644 --- a/tests/unit-mcdc/test_logging_whitebox.c +++ b/tests/unit-mcdc/test_logging_whitebox.c @@ -23,7 +23,7 @@ * White-box MC/DC supplement for wolfcrypt/src/logging.c -- CORE (non * OpenSSL-compat) decisions only. * - * The campaign's default build for this module measures 0 MC/DC on + * The suite's default build for this module measures 0 MC/DC on * logging.c's core decisions because every one of them lives behind a * debug/error-queue macro that the default variant does not enable * (DEBUG_WOLFSSL / WOLFSSL_DEBUG_CERTS for certificate logging, @@ -34,7 +34,7 @@ * tests/unit-mcdc/README.md's per-binary MC/DC contract). * * IMPORTANT SCOPE: OPENSSL_EXTRA is deliberately NEVER defined here. The - * per-module campaign excludes logging.c's OPENSSL_EXTRA-guarded (OpenSSL + * per-module suite excludes logging.c's OPENSSL_EXTRA-guarded (OpenSSL * compatibility) decisions from its MC/DC boundary; this file only targets * the CORE decisions. Where a core decision needs the same top-level guard * that also (independently) admits OPENSSL_EXTRA builds (e.g. the error diff --git a/tests/unit-mcdc/test_memory_whitebox.c b/tests/unit-mcdc/test_memory_whitebox.c index ea7937e760..82df762315 100644 --- a/tests/unit-mcdc/test_memory_whitebox.c +++ b/tests/unit-mcdc/test_memory_whitebox.c @@ -55,7 +55,7 @@ * Verified against a throwaway library built with: * ./configure --enable-usersettings --enable-static --disable-shared \ * --enable-staticmemory - * (CC=clang, CFLAGS/LDFLAGS carrying the campaign's + * (CC=clang, CFLAGS/LDFLAGS carrying the * -fprofile-instr-generate -fcoverage-mapping -fcoverage-mcdc, plus * -Wno-error=unused-function: wc_MemFailCount_AllocMem/FreeMem (memory.c * :150/:165) are `static` helpers whose only OTHER caller is the diff --git a/tests/unit-mcdc/test_mldsa_fault_whitebox.c b/tests/unit-mcdc/test_mldsa_fault_whitebox.c index a125b94de4..2f60e2746b 100644 --- a/tests/unit-mcdc/test_mldsa_fault_whitebox.c +++ b/tests/unit-mcdc/test_mldsa_fault_whitebox.c @@ -48,7 +48,7 @@ * buffer guard. * * These working-buffer allocations exist in every variant with the base - * campaign config (the WC_MLDSA_CACHE_* caching macros are OFF, so sign/verify + * suite config (the WC_MLDSA_CACHE_* caching macros are OFF, so sign/verify * XMALLOC their scratch on every call); they multiply under the small-memory * arms (WOLFSSL_MLDSA_SIGN_SMALL_MEM / _VERIFY_SMALL_MEM / * _SIGN_SMALL_MEM_PRECALC_A), whose per-column recompute blocks add extra @@ -86,7 +86,7 @@ * ./test_mldsa_fault_whitebox default: baseline + fault sweeps * ./test_mldsa_fault_whitebox baseline unarmed valid ops only (delta base) * ./test_mldsa_fault_whitebox probe print per-entry-point alloc counts - * (The campaign run_whitebox harness runs this binary with NO arguments, so the + * (The white-box harness runs this binary with NO arguments, so the * default action is the full sweep.) * * WHY WOLFSSL_SMALL_STACK IS FORCED BELOW diff --git a/tests/unit-mcdc/test_mldsa_hash_fault_whitebox.c b/tests/unit-mcdc/test_mldsa_hash_fault_whitebox.c index 835c7f49ba..57d53a2878 100644 --- a/tests/unit-mcdc/test_mldsa_hash_fault_whitebox.c +++ b/tests/unit-mcdc/test_mldsa_hash_fault_whitebox.c @@ -67,7 +67,7 @@ * needs key == NULL, which the public wrapper rejects before calling. The * static helper is in scope here and is called directly. * - * DOCUMENTED RESIDUALS (mirrored in campaign/db/exclusions.json): + * DOCUMENTED RESIDUALS (mirrored in the exclusion record): * - `for (; (ret == 0) && valid && (r < params->k); r++)` and * `if ((ret == 0) && valid)`: `valid` is assigned 1 unconditionally and is * only ever assigned 0 inside `#ifdef WOLFSSL_MLDSA_SIGN_CHECK_Y`, @@ -308,7 +308,7 @@ static void wb_sign_no_private(void) /* The partner row: the SAME entry point on a key that DOES have a private * key, so the operand is false with the decision reaching its other - * outcome. Nothing else in the campaign calls the seeded pre-hash signer, + * outcome. Nothing else in the harness calls the seeded pre-hash signer, * so without this the operand above has no vector to pair against. */ #ifndef WOLFSSL_MLDSA_NO_MAKE_KEY XMEMSET(&key, 0, sizeof(key)); @@ -442,7 +442,7 @@ static void wb_verify_ctx_hash_null(void) * two. An empty privateKey with no seed falls to asn.c:33992 and returns * ASN_PARSE_E. privKeyLen != 0 therefore holds on every arrival at both * else-ifs, and all three operands are recorded in - * campaign/db/exclusions.json rather than left open. The rows below stay: they + * the exclusion record rather than left open. The rows below stay: they * are what establishes the rejection, and they cost two decodes. * * The AlgorithmIdentifier carries id-ml-dsa-44 (2.16.840.1.101.3.4.3.17) @@ -667,7 +667,7 @@ int main(void) mcdc_fh_disarm(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); - /* A non-zero exit makes the campaign discard this binary's coverage. */ + /* A non-zero exit makes the harness discard this binary's coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_mlkem_fault_whitebox.c b/tests/unit-mcdc/test_mlkem_fault_whitebox.c index 5d653244c1..ef38b3e662 100644 --- a/tests/unit-mcdc/test_mlkem_fault_whitebox.c +++ b/tests/unit-mcdc/test_mlkem_fault_whitebox.c @@ -50,11 +50,11 @@ * * SCOPE NOTE: wc_mlkem_poly.c's uncovered decisions are NOT addressed here. * Its only heap-allocation sites are all guarded by WOLFSSL_SMALL_STACK (which - * no ML-KEM campaign variant defines), and its remaining `(ret==0) && ...` loop + * no ML-KEM suite variant defines), and its remaining `(ret==0) && ...` loop * guards go non-zero only via a mid-loop PRF/XOF failure, not via an allocation * -- neither is reachable through a pass-through allocation fault. The bulk of * wc_mlkem_poly.c's residuals are AVX2 cpuid-dispatch and rejection-sampling - * data-path decisions (a separate, input-driven effort). See the campaign + * data-path decisions (a separate, input-driven effort). See the harness * report for the full accounting. * * It #includes wc_mlkem.c directly (like the other unit-mcdc white-boxes) to @@ -70,7 +70,7 @@ * ./test_mlkem_fault_whitebox full fault-index sweep (default) * ./test_mlkem_fault_whitebox baseline unarmed valid ops only (delta base) * ./test_mlkem_fault_whitebox probe print per-entry-point alloc counts - * (The campaign run_whitebox harness runs the binary with NO arguments, so the + * (The white-box harness runs the binary with NO arguments, so the * default action is the full sweep.) */ diff --git a/tests/unit-mcdc/test_mlkem_poly_hash_fault_whitebox.c b/tests/unit-mcdc/test_mlkem_poly_hash_fault_whitebox.c index d4807f2d1e..ad044e4d24 100644 --- a/tests/unit-mcdc/test_mlkem_poly_hash_fault_whitebox.c +++ b/tests/unit-mcdc/test_mlkem_poly_hash_fault_whitebox.c @@ -22,7 +22,7 @@ /* * MC/DC hash-fault white-box supplement for wolfcrypt/src/wc_mlkem_poly.c. * - * campaign/reports/mlkem/GAPS.md leaves ten residuals on this file and all ten + * suite/reports/mlkem/the uncovered-condition report leaves ten residuals on this file and all ten * are the FALSE half of a success chain or an optional-argument guard that the * public wc_MlKemKey_* API never produces: * @@ -40,7 +40,7 @@ * All four helpers are file-static (or WOLFSSL_LOCAL) and this TU #includes * wc_mlkem_poly.c, so each is called DIRECTLY with the operand combination the * API cannot produce. No key generation is performed at all, which keeps the - * whole binary well inside the campaign's wall-clock budget. + * whole binary well inside the wall-clock budget. * * 1. mlkem_hash512()'s three conditions need no injector: * - data2 == NULL and data2Len == 0 are simply passed in; @@ -72,7 +72,7 @@ * NOT REACHABLE HERE: under USE_INTEL_SPEEDUP mlkem_prf() writes the Keccak * state directly (sha3_block_bmi2 / sha3_block_avx2 / BlockSha3) and returns a * literal 0, so mlkem_get_noise_c()'s chain cannot be broken in the mlkem_avx2 - * variant. The six portable-C variants supply those rows and the campaign + * variant. The six portable-C variants supply those rows and the harness * unions by line:col. * * VARIANT COVERAGE (HARD RULE 2): every helper is behind the same #if the @@ -313,6 +313,6 @@ int main(void) mcdc_fh_disarm(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); - /* A non-zero exit makes the campaign discard this binary's coverage. */ + /* A non-zero exit makes the harness discard this binary's coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_pkcs12_fault_whitebox.c b/tests/unit-mcdc/test_pkcs12_fault_whitebox.c index 4ac8481e6a..ab2a02a636 100644 --- a/tests/unit-mcdc/test_pkcs12_fault_whitebox.c +++ b/tests/unit-mcdc/test_pkcs12_fault_whitebox.c @@ -22,14 +22,14 @@ /* * MC/DC white-box supplement for wolfcrypt/src/pkcs12.c, closing the last * closable residual left after test_pkcs12_whitebox.c and - * test_pkcs12_parse_whitebox.c (GAPS.md: 58/65): PKCS12_CheckConstructedZero() + * test_pkcs12_parse_whitebox.c (the uncovered-condition report: 58/65): PKCS12_CheckConstructedZero() * * if (ret == 0 && GetObjectId(data, idx, &oid, oidIgnoreType, dataSz)) { * (pkcs12.c:1239) * * condition index 0 (`ret == 0`). * - * Both rows this condition needs already exist in the campaign -- just not + * Both rows this condition needs already exist in the harness -- just not * in the same binary. test_pkcs12_whitebox.c's wb_check_constructed_zero() * drives a valid chain (ret==0 entering 1239, GetObjectId succeeds -> * (T,F)=FALSE) and a chain truncated right after the outer SEQUENCE header @@ -57,7 +57,7 @@ * The function is static, so it is called directly (same idiom as the two * other pkcs12 white-boxes: #include pkcs12.c to reach file-static helpers). * - * The other six GAPS.md residuals are all structurally unreachable and are + * The other six the uncovered-condition report residuals are all structurally unreachable and are * deliberately NOT exercised here -- inventing a vector for a decision that * cannot occur would misrepresent the code as more exercised than it is. * Each was independently re-derived from the current source (not taken on @@ -113,9 +113,9 @@ * `ret < 0` check could see as false while the length-only comparison * that precedes it stays true. Dead code. * - * All six are logged as DEATHNOTE candidates by the caller; not repeated as + * All six are recorded by the caller; not repeated as * test code here. mcdc_fault_alloc.h is included for idiom consistency with - * the rest of the campaign's *_fault_whitebox.c files, but is unused: the + * the rest of the *_fault_whitebox.c files, but is unused: the * one closable residual here is a pure ASN decode-path decision, not an * allocation-failure guard. */ @@ -191,7 +191,7 @@ int main(void) printf("pkcs12.c fault white-box MC/DC supplement\n"); wb_check_zero_cond0(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); - /* Always return 0: a nonzero exit makes the campaign discard the whole + /* Always return 0: a nonzero exit makes the harness discard the whole * variant's coverage, including the parts that did succeed. */ return 0; } diff --git a/tests/unit-mcdc/test_pkcs12_parse_whitebox.c b/tests/unit-mcdc/test_pkcs12_parse_whitebox.c index ac64bc121f..d7d29f3ff8 100644 --- a/tests/unit-mcdc/test_pkcs12_parse_whitebox.c +++ b/tests/unit-mcdc/test_pkcs12_parse_whitebox.c @@ -505,7 +505,7 @@ int main(void) wb_d2i_eoc_skip(); wb_encrypted_zero_check(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Always return 0: a nonzero exit makes the campaign discard the whole + /* Always return 0: a nonzero exit makes the harness discard the whole * variant's coverage, including the parts that did succeed. */ return 0; } diff --git a/tests/unit-mcdc/test_pkcs12_whitebox.c b/tests/unit-mcdc/test_pkcs12_whitebox.c index 52fb3566fa..1b63a55609 100644 --- a/tests/unit-mcdc/test_pkcs12_whitebox.c +++ b/tests/unit-mcdc/test_pkcs12_whitebox.c @@ -8,11 +8,11 @@ * wrapper pre-validates). This translation unit compiles pkcs12.c directly * (#include) to reach its static helpers and calls them with both halves of * each targeted MC/DC independence pair. Heap-allocation failures use the - * shared campaign fault injector (mcdc_fault_alloc.h) to force a specific + * shared suite fault injector (mcdc_fault_alloc.h) to force a specific * XMALLOC call to return NULL deterministically. * * 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). + * source line:col in the per-module suite. * * Targeted residuals (pkcs12.c), by class: * Class 1 GetSignData() digest/salt alloc-failure guards ....... 2 conds @@ -38,7 +38,7 @@ * once GetLength has succeeded. Confirmed empirically (a totalSz small * enough to trip the overflow makes GetLength itself fail first, with a * BUFFER_E/ASN_PARSE_E return, never reaching this line with digest/salt - * already allocated). Logged in DEATHNOTE.md (Part 5 findings) as + * already allocated). Logged in the defect notes as * dead/simplify candidates; only the alloc-failure half is exercised here. * - wc_PKCS12_create_mac() kLen<0 (line ~599): every hash OID that * wc_OidGetHash() maps to a non-NONE wc_HashType is guarded in @@ -130,7 +130,7 @@ static void wb_free_signdata(WC_PKCS12* pkcs12) /* Class 1: GetSignData() digest/salt alloc-failure guards (pkcs12.c:445 * mac->digest==NULL; pkcs12.c:477 mac->salt==NULL -- the reachable half of * each `|| size+curIdx>totalSz` guard; see file header for why the size half - * is dead code, logged in DEATHNOTE.md). Both operands normally false (real + * is dead code). Both operands normally false (real * DER + successful alloc); the alloc-failure half is white-box only, reached * here with the shared fault injector on a static-function-direct call. */ static void wb_getsigndata(void) @@ -166,7 +166,7 @@ static void wb_getsigndata(void) mcdc_fa_restore(); WB_NOTE("GetSignData digest/salt alloc-failure pairs exercised " - "(size-overflow half is dead code, see file header / DEATHNOTE.md)"); + "(size-overflow half is dead code, see file header)"); } /* Class 2: wc_PKCS12_create_mac() NULL guard (pkcs12.c:546-547) and the @@ -651,7 +651,7 @@ int main(void) wb_check_constructed_zero(); wb_shroud_and_keybag(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_pkcs7_arg_whitebox.c b/tests/unit-mcdc/test_pkcs7_arg_whitebox.c index fdece5e43a..2217b60a03 100644 --- a/tests/unit-mcdc/test_pkcs7_arg_whitebox.c +++ b/tests/unit-mcdc/test_pkcs7_arg_whitebox.c @@ -33,6 +33,16 @@ * vector is paired with the all-false vector inside this same file. The * accepting vector only has to make the guard evaluate false; failing * deeper in is fine and expected. + * + * ARGUED UNREACHABLE, do not re-open: + * + * :4183 cond 1 (`pkcs7->sidType != DEGENERATE_SID`). PKCS7_EncodeSigned's + * only assignment of a non-zero flatSignedAttribsSz is at :3836, inside + * the `if (pkcs7->sidType != DEGENERATE_SID)` block that opens at :3730. + * The enclosing `if (flatSignedAttribsSz > 0)` at :4180 therefore already + * implies sidType != DEGENERATE_SID: with a degenerate SID the attribute + * block never runs, flatSignedAttribsSz stays 0, and :4183 is not + * reached at all. The operand is constant-true where it is evaluated. */ #include @@ -1586,7 +1596,7 @@ static void wb_small_matrices(void) /* all-false baseline: the 3 operand-true rows above never pair against * a call that gets past the guard, so the guard's independence pairs * were never actually closed (mirrors the arg-guard trap noted in the - * campaign brief -- an operand-true-only batch without the baseline). */ + * suite brief -- an operand-true-only batch without the baseline). */ { wc_PKCS7 hp; int ret; @@ -1867,6 +1877,100 @@ static void wb_size_guards(void) } } +/* ------------------------------------------------------------------------- * + * wc_PKCS7_EncodeContentStream(): the trailing-pad gate + * :3413 `(cipherType != WC_CIPHER_NONE) && (totalSz == pkcs7->contentSz)` + * + * Both operands need rows that no public encode call produces. The only + * cipherType != WC_CIPHER_NONE callers are inside wc_PKCS7_EncryptContent() + * and are reached only when pkcs7->encodeStream is set, and every such call + * hands the whole content over in one piece, so totalSz always ends up equal + * to pkcs7->contentSz. Calling the (file-static) encoder directly supplies + * all three rows in this binary: + * + * (F,-) cipherType WC_CIPHER_NONE, the signed-bundle shape + * (T,T) AES-CBC with contentSz == inSz -> the pad block runs + * (T,F) AES-CBC with contentSz > inSz -> the read loop runs out of + * input first (`contentDataRead <= 0` breaks the do/while), so the + * pad block is skipped and the partial block is flushed as-is + * ------------------------------------------------------------------------- */ +#if !defined(NO_AES) && defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_128) && \ + defined(ASN_BER_TO_DER) +static void wb_encode_content_stream_pad(void) +{ + static byte in[32]; + static byte out[512]; + byte key[16], iv[16]; + Aes aes; + wc_PKCS7* p; + int ret; + + XMEMSET(in, 0x41, sizeof(in)); + XMEMSET(key, 0x42, sizeof(key)); + XMEMSET(iv, 0x43, sizeof(iv)); + + WB_NOTE("wc_PKCS7_EncodeContentStream(): WC_CIPHER_NONE, pad gate short-" + "circuits on the first operand [:3413 cond 0 false]"); + p = wc_PKCS7_New(NULL, INVALID_DEVID); + if (p != NULL) { + if (wc_PKCS7_InitWithCert(p, NULL, 0) == 0) { + p->encodeStream = 1; + p->contentSz = (word32)sizeof(in); + ret = wc_PKCS7_EncodeContentStream(p, NULL, NULL, in, + (int)sizeof(in), out, WC_CIPHER_NONE); + WB_CHECK(ret == 0, ":3413 WC_CIPHER_NONE stream copy"); + } + wc_PKCS7_Free(p); + } + + WB_NOTE("wc_PKCS7_EncodeContentStream(): AES-CBC with the whole content" + " consumed, so the pad block runs [:3413 both operands true]"); + p = wc_PKCS7_New(NULL, INVALID_DEVID); + if (p != NULL) { + if (wc_PKCS7_InitWithCert(p, NULL, 0) == 0 && wc_AesInit(&aes, NULL, + INVALID_DEVID) == 0) { + if (wc_AesSetKey(&aes, key, (word32)sizeof(key), iv, + AES_ENCRYPTION) == 0) { + p->encodeStream = 1; + p->encryptOID = AES128CBCb; + p->contentSz = (word32)sizeof(in); + ret = wc_PKCS7_EncodeContentStream(p, NULL, &aes, in, + (int)sizeof(in), out, WC_CIPHER_AES_CBC); + WB_CHECK(ret == 0, ":3413 AES-CBC padded flush"); + } + wc_AesFree(&aes); + } + wc_PKCS7_Free(p); + } + + WB_NOTE("wc_PKCS7_EncodeContentStream(): AES-CBC whose declared contentSz" + " is larger than the input, so the read loop stops short and the" + " pad block is skipped [:3413 cond 1 false]"); + p = wc_PKCS7_New(NULL, INVALID_DEVID); + if (p != NULL) { + if (wc_PKCS7_InitWithCert(p, NULL, 0) == 0 && wc_AesInit(&aes, NULL, + INVALID_DEVID) == 0) { + if (wc_AesSetKey(&aes, key, (word32)sizeof(key), iv, + AES_ENCRYPTION) == 0) { + p->encodeStream = 1; + p->encryptOID = AES128CBCb; + p->contentSz = (word32)sizeof(in) * 2; + ret = wc_PKCS7_EncodeContentStream(p, NULL, &aes, in, + (int)sizeof(in), out, WC_CIPHER_AES_CBC); + WB_CHECK(ret == 0, ":3413 AES-CBC short read, no pad block"); + } + wc_AesFree(&aes); + } + wc_PKCS7_Free(p); + } +} +#else +static void wb_encode_content_stream_pad(void) +{ + WB_NOTE("no AES-CBC/BER-to-DER; EncodeContentStream pad gate skipped"); +} +#endif + int main(void) { setvbuf(stdout, NULL, _IONBF, 0); @@ -1885,10 +1989,11 @@ int main(void) wb_small_matrices(); wb_auth_encode_shapes(); wb_size_guards(); + wb_encode_content_stream_pad(); printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_pkcs7_decode_whitebox.c b/tests/unit-mcdc/test_pkcs7_decode_whitebox.c index 172e773507..973637116d 100644 --- a/tests/unit-mcdc/test_pkcs7_decode_whitebox.c +++ b/tests/unit-mcdc/test_pkcs7_decode_whitebox.c @@ -50,8 +50,8 @@ * The "ret == 0 && Get*(...)" idiom in this file produces a large number of * operands that have no MC/DC independence pair for a reason in the source, * not for want of a test. They fall into four provable families. Recording - * them here so the next pass does not re-spend effort on them; the campaign - * EXCLUSIONS.md carries the same arguments. + * them here so the next pass does not re-spend effort on them; the harness + * the exclusion record carries the same arguments. * * (1) LEADING OPERAND OF THE FIRST LINK IN A SWITCH CASE. `ret` is a local * initialised to 0, and the only statement before the link is a @@ -120,6 +120,43 @@ * inSz at :6963/:6965 and forced to defSz by * wc_PKCS7_SetMaxStream when it computes 0; it is * never 0 on arrival at VERIFY_STAGE3. + * + * Added by the 2026-08-20 streaming-state wave (Sections 18-19 below), both + * filed in the exclusion record: + * + * :7516 cond 2 -- family (4). stream->content is freed and NULLed at + * :7501-:7502 before VERIFY_STAGE3 runs, so it is + * non-NULL at :7516 only because the + * wc_PKCS7_HandleOctetStrings() call two statements + * above made it so. That function stores a non-NULL + * pointer there in exactly two places, and both set + * contentSz strictly positive in the same block: + * :6648, entered only under + * `if (pkcs7->content && pkcs7->contentSz > 0)` and + * followed at :6655 by + * `stream->contentSz = pkcs7->contentSz`; and :6821, + * followed at :6849 by + * `stream->contentSz += stream->expected`, in the + * `else` of `if (currContRmnSz == 0)` where every + * assignment of expected consistent with + * currContRmnSz > 0 is + * min(currContRmnSz, MAX_PKCS7_STREAM_BUFFER) >= 1. + * The only arm that grows contentSz without allocating + * (the streamOutCb branch at :6790) leaves content + * NULL, which drives cond 1 false, not cond 2. + * :12036 cond 1 -- family (3). The branch that reaches it, + * `stream->expected == MAX_SEQ_SZ`, is what bounds + * stream->length: wc_PKCS7_AddDataToStream() grows it + * only by min(inSz - rdSz, expected - length), and the + * largest expected on any path into + * WC_PKCS7_DECRYPT_KTRI_2 is + * MAX_OID_SZ + MAX_LENGTH_SZ + ASN_TAG_SZ == 38, at + * WC_PKCS7_INFOSET_STAGE1. :12034 has just assigned + * expected = sz + MAX_ALGO_SZ + ASN_TAG_SZ + + * MAX_LENGTH_SZ + 512, i.e. at least 538, so + * `length < expected` is constant true. Section 18's + * true row shows the ceiling: the most the guard can + * ever see is MAX_SEQ_SZ (6) against expected 542. */ #include @@ -1283,6 +1320,33 @@ static void wb_octet_accum(word32 seedAccum) wc_PKCS7_FreeStream(&pkcs7); } +/* The "another OCTET STRING follows" branch, :6689. Its trailing operand needs + * an 0x04 tag whose length field is itself malformed -- a shape no encoder + * emits, since every OCTET STRING wolfSSL writes carries a well-formed length. + * Seeded directly, like wb_octet_accum() above, with currContRmnSz == 0 so the + * branch is entered on the first pass. */ +static int wb_octet_next_len(byte* in, word32 inSz) +{ + wc_PKCS7 pkcs7; + word32 idx = 0, tmpIdx = 0; + int ret; + + XMEMSET(&pkcs7, 0, sizeof(pkcs7)); + + if (wc_PKCS7_CreateStream(&pkcs7) != 0) { + return BAD_FUNC_ARG; + } + pkcs7.stream->currContSz = 0; + pkcs7.stream->currContRmnSz = 0; + pkcs7.stream->expected = 1; + pkcs7.stream->noContent = 0; + pkcs7.stream->maxLen = inSz; + + ret = wc_PKCS7_HandleOctetStrings(&pkcs7, in, inSz, &tmpIdx, &idx, 1); + wc_PKCS7_FreeStream(&pkcs7); + return ret; +} + static void wb_octet_accum_chains(void) { WB_NOTE("wc_PKCS7_HandleOctetStrings(): existing content buffer with a" @@ -1291,6 +1355,25 @@ static void wb_octet_accum_chains(void) WB_NOTE("wc_PKCS7_HandleOctetStrings(): existing content buffer with a" " non-zero accumulated size [:6817 trailing operand true]"); wb_octet_accum(4); + + { + static byte okLen[] = { 0x04, 0x02, 0xAA, 0xBB }; + static byte badLen[] = { 0x04 }; + int ret; + + WB_NOTE("wc_PKCS7_HandleOctetStrings(): a following OCTET STRING whose" + " length parses [:6689 trailing operand false]"); + ret = wb_octet_next_len(okLen, (word32)sizeof(okLen)); + WB_CHECK(ret != WC_NO_ERR_TRACE(ASN_PARSE_E), + ":6689 well-formed following OCTET STRING length"); + + WB_NOTE("wc_PKCS7_HandleOctetStrings(): a following OCTET STRING tag" + " with no length byte behind it [:6689 trailing operand" + " true]"); + ret = wb_octet_next_len(badLen, (word32)sizeof(badLen)); + WB_CHECK(ret == WC_NO_ERR_TRACE(ASN_PARSE_E), + ":6689 malformed following OCTET STRING length"); + } } #else static void wb_octet_accum_chains(void) @@ -1427,13 +1510,537 @@ static void wb_verify_outer_shapes(void) } /* ------------------------------------------------------------------------- * - * main -- always returns 0 so the campaign harness keeps this variant's + * Section 16: identity- and version-dispatch operands that neither a sweep + * nor any public encoder can produce, because no encoder in the tree ever + * emits the shape the operand tests. + * + * :6398 cond 1/2 wc_PKCS7_ParseSignerInfo()'s noDegenerate guard. The + * inner OR is `inSz == 0 || degenerate == 1`; both operands + * need a call with noDegenerate set, which the public + * decode path only ever makes with the *same* (inSz, + * degenerate) pair for a given bundle. Called directly with + * the three combinations instead. + * :5166 cond 1 wc_PKCS7_RsaVerify()'s `keyOID != RSAk && keyOID != + * :5298 cond 1 RSAPSSk` defence-in-depth guard, and the same guard in + * wc_PKCS7_RsaPssVerify(). The false row needs an + * RSASSA-PSS SubjectPublicKeyInfo (keyOID == RSAPSSk) in + * pkcs7->cert[], which no bundle this module builds carries; + * the true row needs a non-RSA-family cert in the same + * binary. Both are supplied here from certs/. + * :14106 cond 2 wc_PKCS7_ParseToRecipientInfoSet()'s BER marker test + * `ret == 0 && length == 0 && pkiMsg[(*idx)-1] == 0x80`. + * A zero-length *definite* outer SEQUENCE (`30 00`) is the + * only input that reaches the third operand with a false + * value; the `30 80` companion in the same binary supplies + * the true row. + * :14215 cond 4 the ECDSA arm of the envelopedData version dispatch, + * `publicKeyOID == ECDSAk && (version != 0 && ...)`. The + * false row needs an ECC signer key with version 0, the + * true row the same key with a version that is none of + * 0/2/3 -- one field of one hand-built header apart. + * ------------------------------------------------------------------------- */ + +/* ContentInfo/EnvelopedData header, parsed as far as the RecipientInfo SET. + * Padded well past MAX_OID_SZ + MAX_LENGTH_SZ so that the streaming + * wc_PKCS7_AddDataToStream() never has to ask for more input. */ +static byte wbRisHdr[96]; +/* the same prefix with a zero-length definite outer SEQUENCE, and with the + * indefinite-length marker, so :14106's third operand sees both values */ +static byte wbRisEmptyDef[96]; +static byte wbRisEmptyIndef[96]; + +static word32 wb_build_ris_hdr(byte* buf, word32 bufSz, byte version) +{ + word32 i = 0; + + XMEMSET(buf, 0, bufSz); + buf[i++] = 0x30; buf[i++] = 0x16; /* ContentInfo SEQUENCE */ + buf[i++] = 0x06; buf[i++] = 0x09; /* envelopedData OID */ + buf[i++] = 0x2A; buf[i++] = 0x86; buf[i++] = 0x48; buf[i++] = 0x86; + buf[i++] = 0xF7; buf[i++] = 0x0D; buf[i++] = 0x01; buf[i++] = 0x07; + buf[i++] = 0x03; + buf[i++] = 0xA0; buf[i++] = 0x09; /* [0] EXPLICIT */ + buf[i++] = 0x30; buf[i++] = 0x07; /* EnvelopedData SEQUENCE */ + buf[i++] = 0x02; buf[i++] = 0x01; buf[i++] = version; + buf[i++] = 0x31; buf[i++] = 0x02; /* RecipientInfo SET */ + buf[i++] = 0x30; buf[i++] = 0x00; + return bufSz; +} + +static int wb_parse_ris(byte* buf, word32 bufSz, word32 pubKeyOID) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + word32 idx = 0; + int ret; + + if (p == NULL) { + return MEMORY_E; + } + if (wc_PKCS7_InitWithCert(p, NULL, 0) != 0) { + wc_PKCS7_Free(p); + return BAD_FUNC_ARG; + } + p->publicKeyOID = pubKeyOID; + ret = wc_PKCS7_ParseToRecipientInfoSet(p, buf, bufSz, &idx, ENVELOPED_DATA); + wc_PKCS7_Free(p); + return ret; +} + +static void wb_recipient_info_set_shapes(void) +{ + int ret; + + WB_NOTE("wc_PKCS7_ParseToRecipientInfoSet(): zero-length outer SEQUENCE," + " definite vs indefinite, isolates the 0x80 marker operand" + " [:14106 cond 2]"); + XMEMSET(wbRisEmptyDef, 0, sizeof(wbRisEmptyDef)); + wbRisEmptyDef[0] = 0x30; + wbRisEmptyDef[1] = 0x00; + ret = wb_parse_ris(wbRisEmptyDef, (word32)sizeof(wbRisEmptyDef), RSAk); + WB_CHECK(ret != 0, ":14106 definite zero-length SEQUENCE (marker false)"); + + XMEMSET(wbRisEmptyIndef, 0, sizeof(wbRisEmptyIndef)); + wbRisEmptyIndef[0] = 0x30; + wbRisEmptyIndef[1] = 0x80; + ret = wb_parse_ris(wbRisEmptyIndef, (word32)sizeof(wbRisEmptyIndef), RSAk); + WB_CHECK(ret != 0, ":14106 indefinite-length SEQUENCE (marker true)"); + +#ifdef HAVE_ECC + WB_NOTE("wc_PKCS7_ParseToRecipientInfoSet(): ECDSA signer key with" + " envelopedData version 0 and version 1 [:14215 cond 4]"); + (void)wb_build_ris_hdr(wbRisHdr, (word32)sizeof(wbRisHdr), 0x00); + ret = wb_parse_ris(wbRisHdr, (word32)sizeof(wbRisHdr), ECDSAk); + /* the success return is the RecipientInfo SET length, not 0 */ + WB_CHECK(ret > 0, ":14215 ECDSAk with version 0 is accepted"); + + (void)wb_build_ris_hdr(wbRisHdr, (word32)sizeof(wbRisHdr), 0x01); + ret = wb_parse_ris(wbRisHdr, (word32)sizeof(wbRisHdr), ECDSAk); + WB_CHECK(ret == WC_NO_ERR_TRACE(ASN_VERSION_E), + ":14215 ECDSAk with version 1 is rejected"); +#endif + + /* RSA companion rows, so the RSAk arm's operands are decided by the same + * two headers rather than only by real bundles. */ + (void)wb_build_ris_hdr(wbRisHdr, (word32)sizeof(wbRisHdr), 0x00); + ret = wb_parse_ris(wbRisHdr, (word32)sizeof(wbRisHdr), RSAk); + WB_CHECK(ret > 0, ":14215 RSAk with version 0 is accepted"); + (void)wb_build_ris_hdr(wbRisHdr, (word32)sizeof(wbRisHdr), 0x01); + ret = wb_parse_ris(wbRisHdr, (word32)sizeof(wbRisHdr), RSAk); + WB_CHECK(ret == WC_NO_ERR_TRACE(ASN_VERSION_E), + ":14215 RSAk with version 1 is rejected"); +} + +/* wc_PKCS7_ParseSignerInfo() with pkcs7->noDegenerate set, across the three + * (inSz, degenerate) combinations the inner OR needs. */ +static int wb_parse_signer_info_nodeg(byte* in, word32 inSz, int degenerate) +{ + wc_PKCS7 pkcs7; + word32 idx = 0; + byte* signedAttrib = NULL; + int signedAttribSz = 0; + int ret; + + XMEMSET(&pkcs7, 0, sizeof(pkcs7)); + pkcs7.version = 1; + pkcs7.noDegenerate = 1; + ret = wc_PKCS7_ParseSignerInfo(&pkcs7, in, inSz, &idx, degenerate, + &signedAttrib, &signedAttribSz); + wc_PKCS7_SignerInfoFree(&pkcs7); + return ret; +} + +static void wb_parse_signer_info_nodegenerate(void) +{ + int ret; + + WB_NOTE("wc_PKCS7_ParseSignerInfo(): noDegenerate matrix, each operand of" + " `inSz == 0 || degenerate == 1` isolated [:6398]"); + + /* (T,T,-): inSz == 0 decides the OR */ + ret = wb_parse_signer_info_nodeg(wbSiNoSeq, 0, 0); + WB_CHECK(ret == WC_NO_ERR_TRACE(PKCS7_NO_SIGNER_E), + ":6398 inSz == 0 with noDegenerate set"); + + /* (T,F,T): degenerate decides the OR */ + ret = wb_parse_signer_info_nodeg(wbSiNoSeq, (word32)sizeof(wbSiNoSeq), 1); + WB_CHECK(ret == WC_NO_ERR_TRACE(PKCS7_NO_SIGNER_E), + ":6398 degenerate == 1 with noDegenerate set"); + + /* (T,F,F): the guard does not fire and the parse runs on */ + ret = wb_parse_signer_info_nodeg(wbSiNoSeq, (word32)sizeof(wbSiNoSeq), 0); + WB_CHECK(ret != WC_NO_ERR_TRACE(PKCS7_NO_SIGNER_E), + ":6398 neither OR operand true, parse proceeds"); +} + +/* ------------------------------------------------------------------------- * + * The RSA-family SPKI guards. Both verifiers walk pkcs7->cert[] themselves, + * so the vector is just "put this DER in cert[0] and call". + * ------------------------------------------------------------------------- */ +#ifndef NO_RSA +static byte wbSpkiCert[2048]; + +static void wb_rsa_spki_guards(void) +{ + byte sig[256]; + byte hash[32]; + word32 certSz; + wc_PKCS7* p; + + XMEMSET(sig, 0x5A, sizeof(sig)); + XMEMSET(hash, 0x5B, sizeof(hash)); + + /* (a) keyOID is neither RSAk nor RSAPSSk: an ECDSA certificate. */ + certSz = wb_load_file("./certs/client-ecc-cert.der", wbSpkiCert, + (word32)sizeof(wbSpkiCert)); + if (certSz > 0) { + WB_NOTE("wc_PKCS7_RsaVerify(): non-RSA-family SPKI rejected" + " [:5166 both operands true]"); + p = wc_PKCS7_New(NULL, INVALID_DEVID); + if (p != NULL) { + if (wc_PKCS7_InitWithCert(p, NULL, 0) == 0) { + p->cert[0] = wbSpkiCert; + p->certSz[0] = certSz; + p->hashOID = SHA256h; + WB_CHECK(wc_PKCS7_RsaVerify(p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)) != 0, + ":5166 ECDSA cert is skipped"); +#ifdef WC_RSA_PSS + WB_CHECK(wc_PKCS7_RsaPssVerify(p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)) != 0, + ":5298 ECDSA cert is skipped"); +#endif + } + wc_PKCS7_Free(p); + } + } + + /* (b) keyOID == RSAPSSk: the second operand alone decides the guard. */ + certSz = wb_load_file("./certs/rsapss/client-rsapss.der", wbSpkiCert, + (word32)sizeof(wbSpkiCert)); + if (certSz > 0) { + WB_NOTE("wc_PKCS7_RsaVerify(): RSASSA-PSS SPKI accepted by the guard" + " [:5166 second operand false]"); + p = wc_PKCS7_New(NULL, INVALID_DEVID); + if (p != NULL) { + if (wc_PKCS7_InitWithCert(p, NULL, 0) == 0) { + p->cert[0] = wbSpkiCert; + p->certSz[0] = certSz; + p->hashOID = SHA256h; + /* the signature is garbage, so the call still fails -- but it + * fails *after* the guard, which is the point. */ + WB_CHECK(wc_PKCS7_RsaVerify(p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)) != 0, + ":5166 RSAPSS cert passes the guard, signature fails"); +#ifdef WC_RSA_PSS + WB_CHECK(wc_PKCS7_RsaPssVerify(p, sig, (int)sizeof(sig), hash, + (word32)sizeof(hash)) != 0, + ":5298 RSAPSS cert passes the guard, signature fails"); +#endif + } + wc_PKCS7_Free(p); + } + } +} +#else +static void wb_rsa_spki_guards(void) +{ + WB_NOTE("NO_RSA; RSA-family SPKI guards skipped"); +} +#endif /* !NO_RSA */ + +/* ------------------------------------------------------------------------- * + * Section 17: the KTRI key-encryption-algorithm dispatch, :12114 + * `encOID != RSAk && encOID != RSAESOAEPk` + * Every KTRI this tree can *emit* carries rsaEncryption, which short-circuits + * on the first operand. The two rows the second operand needs are one byte + * apart from that: the last arc of the 9-byte algorithm OID inside the + * KeyTransRecipientInfo is rewritten in place -- 0x07 for id-RSAES-OAEP (the + * guard's false row) and 0x0A for id-RSASSA-PSS, which is in neither arm (the + * true row). Both replacements are the same length as rsaEncryption, so no + * enclosing ASN.1 length changes. + * ------------------------------------------------------------------------- */ +#if !defined(NO_RSA) && defined(USE_CERT_BUFFERS_2048) +static const byte wbRsaEncOid[] = { + 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 +}; + +static void wb_ktri_alg_call(byte* buf, word32 len) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + static byte out[WB_SCRATCH_SZ]; + + if (p == NULL) { + return; + } + if (wc_PKCS7_InitWithCert(p, (byte*)client_cert_der_2048, + sizeof_client_cert_der_2048) == 0 && + wc_PKCS7_SetKey(p, (byte*)client_key_der_2048, + sizeof_client_key_der_2048) == 0) { + (void)wc_PKCS7_DecodeEnvelopedData(p, buf, len, out, sizeof(out)); + } + wc_PKCS7_Free(p); +} + +static void wb_ktri_key_alg_dispatch(void) +{ + word32 fullLen, i; + int found = -1; + + fullLen = wb_load_file("./certs/test/ktri-keyid-cms.msg", wbScratch, + sizeof(wbScratch)); + if (fullLen < sizeof(wbRsaEncOid)) { + WB_NOTE("ktri-keyid-cms.msg unavailable; KTRI algorithm dispatch" + " skipped"); + return; + } + /* the LAST rsaEncryption OID in the message is the KTRI's + * keyEncryptionAlgorithm (the earlier one is the certificate's SPKI) */ + for (i = 0; i + (word32)sizeof(wbRsaEncOid) <= fullLen; i++) { + if (XMEMCMP(wbScratch + i, wbRsaEncOid, sizeof(wbRsaEncOid)) == 0) { + found = (int)i; + } + } + if (found < 0) { + WB_NOTE("no rsaEncryption OID found in ktri-keyid-cms.msg; KTRI" + " algorithm dispatch skipped"); + return; + } + + WB_NOTE("wc_PKCS7_DecryptKtri(): rsaEncryption (first operand false)" + " [:12114]"); + wb_ktri_alg_call(wbScratch, fullLen); + + WB_NOTE("wc_PKCS7_DecryptKtri(): id-RSAES-OAEP (second operand false)" + " [:12114 cond 1]"); + wbScratch[(word32)found + 10] = 0x07; + wb_ktri_alg_call(wbScratch, fullLen); + + WB_NOTE("wc_PKCS7_DecryptKtri(): an OID in neither arm (both operands" + " true) [:12114]"); + wbScratch[(word32)found + 10] = 0x0A; + wb_ktri_alg_call(wbScratch, fullLen); + + wbScratch[(word32)found + 10] = 0x01; +} +#else +static void wb_ktri_key_alg_dispatch(void) +{ + WB_NOTE("NO_RSA or no 2048-bit cert buffers; KTRI algorithm dispatch" + " skipped"); +} +#endif + +/* ------------------------------------------------------------------------- * + * Section 18: wc_PKCS7_DecryptKtri()'s WC_PKCS7_DECRYPT_KTRI_2 "peek at the + * next SEQUENCE" block, :12036 cond 0 (`pkcs7->stream->length > 0`). + * + * The block is guarded by `pkcs7->stream->expected == MAX_SEQ_SZ`, and + * `expected` on the only path into this state is the RecipientInfo SET length + * that wc_PKCS7_ParseToRecipientInfoSet() stored at :14259 (EnvelopedData then + * copies it into stream->expected at :14424 as recipientSetSz). MAX_SEQ_SZ is + * 6, so the whole block is dead for every bundle this tree can emit -- a real + * KeyTransRecipientInfo SET is hundreds of bytes -- and the measured runs + * execute :12027-:12036 exactly zero times. Reaching it needs a SET whose + * length field says 6 while the recipient behind it is full size, which + * GetSet_ex(..., NO_USER_CHECK) at :14235 does accept; rather than build that + * whole bundle, the state is seeded directly here, which is the same shape. + * + * Both rows of cond 0 in one binary, on the state the guard actually reads: + * false row -- stream->length == 0, so wc_PKCS7_AddDataToStream() hands back + * the caller's buffer (:357) and the decision is false; the KTRI parse + * then runs on and fails in the IssuerAndSerialNumber walk. + * true row -- the stream is pre-loaded with 4 buffered bytes and fed a + * 2-byte chunk, so AddDataToStream() tops the internal buffer up to + * exactly MAX_SEQ_SZ (:379-:399) and returns it; stream->length is then 6 + * and the recomputed `expected` is sz + 538, so the decision is true and + * the call answers WC_PKCS7_WANT_READ_E. + * + * cond 1 (`stream->length < stream->expected`) has no false row and is filed + * in the exclusion record: `expected` was just assigned `sz + MAX_ALGO_SZ + + * ASN_TAG_SZ + MAX_LENGTH_SZ + 512` (>= 538) two statements above, while + * stream->length can never exceed the largest `expected` ever passed to + * wc_PKCS7_AddDataToStream() on the way here -- MAX_OID_SZ + MAX_LENGTH_SZ + + * ASN_TAG_SZ == 38, at WC_PKCS7_INFOSET_STAGE1 -- because AddDataToStream only + * ever grows length by min(inSz - rdSz, expected - length). + * ------------------------------------------------------------------------- */ +#ifndef NO_PKCS7_STREAM +static int wb_ktri2_seq_peek(const byte* seed, word32 seedSz, + byte* in, word32 inSz) +{ + wc_PKCS7 pkcs7; + byte decryptedKey[MAX_ENCRYPTED_KEY_SZ]; + word32 decryptedKeySz = (word32)sizeof(decryptedKey); + word32 idx = 0; + int recipFound = 0; + int ret; + + XMEMSET(&pkcs7, 0, sizeof(pkcs7)); + XMEMSET(decryptedKey, 0, sizeof(decryptedKey)); + pkcs7.publicKeyOID = RSAk; + + if (wc_PKCS7_CreateStream(&pkcs7) != 0) { + return MEMORY_E; + } + if (seedSz > 0) { + if (wc_PKCS7_GrowStream(&pkcs7, MAX_SEQ_SZ) != 0) { + wc_PKCS7_FreeStream(&pkcs7); + return MEMORY_E; + } + XMEMCPY(pkcs7.stream->buffer, seed, seedSz); + pkcs7.stream->length = seedSz; + } + pkcs7.stream->idx = 0; + pkcs7.stream->expected = MAX_SEQ_SZ; + pkcs7.stream->varTwo = CMS_ISSUER_AND_SERIAL_NUMBER; /* sidType */ + pkcs7.stream->varThree = 0; /* version */ + wc_PKCS7_ChangeState(&pkcs7, WC_PKCS7_DECRYPT_KTRI_2); + + ret = wc_PKCS7_DecryptKtri(&pkcs7, in, inSz, &idx, decryptedKey, + &decryptedKeySz, &recipFound); + wc_PKCS7_FreeStream(&pkcs7); + return ret; +} + +static void wb_ktri2_seq_peek_rows(void) +{ + /* "30 04 AA BB CC DD": a 6-byte SEQUENCE, so GetSequence()/GetLength() + * succeed against a 6-byte view and the block reaches the guard. */ + static const byte seqSeed[] = { 0x30, 0x04, 0xAA, 0xBB }; + static byte seqTail[] = { 0xCC, 0xDD }; + static byte seqWhole[] = { 0x30, 0x04, 0xAA, 0xBB, 0xCC, 0xDD, + 0x00, 0x00 }; + int ret; + + WB_NOTE("wc_PKCS7_DecryptKtri(): KTRI_2 SEQUENCE peek with nothing" + " buffered [:12036 cond 0 false]"); + ret = wb_ktri2_seq_peek(NULL, 0, seqWhole, (word32)sizeof(seqWhole)); + WB_CHECK(ret != WC_NO_ERR_TRACE(WC_PKCS7_WANT_READ_E), + ":12036 stream->length == 0, guard does not fire"); + + WB_NOTE("wc_PKCS7_DecryptKtri(): KTRI_2 SEQUENCE peek with the internal" + " buffer topped up to MAX_SEQ_SZ [:12036 cond 0 true]"); + ret = wb_ktri2_seq_peek(seqSeed, (word32)sizeof(seqSeed), seqTail, + (word32)sizeof(seqTail)); + WB_CHECK(ret == WC_NO_ERR_TRACE(WC_PKCS7_WANT_READ_E), + ":12036 stream->length > 0 and short of the new expected"); +} +#else +static void wb_ktri2_seq_peek_rows(void) +{ + WB_NOTE("NO_PKCS7_STREAM; KTRI_2 SEQUENCE peek skipped"); +} +#endif /* !NO_PKCS7_STREAM */ + +/* ------------------------------------------------------------------------- * + * Section 19: PKCS7_VerifySignedData() WC_PKCS7_VERIFY_STAGE7, :8237 cond 0 + * (`ret == 0` ahead of the signature OCTET STRING tag test). + * + * This is NOT family (1): the statement between the state's + * wc_PKCS7_AddDataToStream() and the link is `if (idx >= pkiMsg2Sz) ret = + * BUFFER_E;` at :8232, which does write ret. The operand's false row therefore + * exists, and needs wc_PKCS7_ParseSignerInfo() to *succeed* while landing idx + * exactly on the end of the message -- a SignerInfo whose signatureAlgorithm + * is the last element in the buffer, with no signature behind it. No encoder + * in the tree emits that (signature is mandatory), and the truncation sweeps + * in Sections 1-8 and in test_pkcs7_mutate_whitebox.c cannot produce it either + * because cutting a real bundle there also invalidates the enclosing SEQUENCE + * and SET lengths, so the parse fails one stage earlier. + * + * So the stage is entered directly with a hand-built SignerInfo: + * SEQUENCE { INTEGER 1, SEQUENCE (IssuerAndSerialNumber, opaque to the + * parser -- it only records the bytes as the SID), AlgorithmIdentifier + * (sha-256), AlgorithmIdentifier (sha256WithRSAEncryption) } + * stream->expected is 0, so AddDataToStream() hands the buffer straight back + * at idx 0 (:357); stream->varThree is 1 so the `length > 0` operand of the + * enclosing :8230 guard holds, and stream->degenerate is 0. + * + * false row -- the buffer IS exactly the SignerInfo, so the parse leaves + * idx == pkiMsg2Sz, :8232 sets BUFFER_E and the decision is false. + * true row -- the same SignerInfo followed by a 4-byte OCTET STRING + * signature, so idx stops short, ret stays 0, the tag reads 0x04 and the + * decision is true. + * ------------------------------------------------------------------------- */ +#if !defined(NO_PKCS7_STREAM) && !defined(NO_RSA) && !defined(NO_SHA256) +/* offsets: [0..1] SignerInfo SEQ header, then version, SID, digestAlgorithm, + * signatureAlgorithm. 43 bytes total; the trailing 6 bytes are the OCTET + * STRING signature used only by the true row. */ +static byte wbStage7Si[] = { + 0x30, 0x29, /* SignerInfo SEQUENCE */ + 0x02, 0x01, 0x01, /* version 1 */ + 0x30, 0x06, /* IssuerAndSerial */ + 0x30, 0x00, /* issuer Name */ + 0x02, 0x02, 0x12, 0x34, /* serialNumber */ + 0x30, 0x0D, /* digestAlgorithm */ + 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, + 0x03, 0x04, 0x02, 0x01, /* id-sha256 */ + 0x05, 0x00, + 0x30, 0x0D, /* signatureAlgorithm */ + 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, + 0x0D, 0x01, 0x01, 0x0B, /* sha256WithRSA */ + 0x05, 0x00, + 0x04, 0x04, 0xDE, 0xAD, 0xBE, 0xEF /* signature (true row) */ +}; +#define WB_STAGE7_SI_ONLY 43 + +static int wb_stage7_signature(word32 inSz) +{ + wc_PKCS7* p = wc_PKCS7_New(NULL, INVALID_DEVID); + int ret; + + if (p == NULL) { + return MEMORY_E; + } + p->version = 1; + if (wc_PKCS7_CreateStream(p) != 0) { + wc_PKCS7_Free(p); + return MEMORY_E; + } + p->stream->expected = 0; /* AddDataToStream() returns `in` at idx 0 */ + p->stream->length = 0; + p->stream->idx = 0; + p->stream->varThree = 1; /* `length` operand of :8230 */ + p->stream->degenerate = 0; + p->stream->indefLen = 0; + wc_PKCS7_ChangeState(p, WC_PKCS7_VERIFY_STAGE7); + + ret = PKCS7_VerifySignedData(p, NULL, 0, wbStage7Si, inSz, NULL, 0); + wc_PKCS7_Free(p); + return ret; +} + +static void wb_stage7_signature_rows(void) +{ + int ret; + + WB_NOTE("PKCS7_VerifySignedData(): STAGE7 with the SignerInfo ending the" + " message [:8237 cond 0 false]"); + ret = wb_stage7_signature(WB_STAGE7_SI_ONLY); + WB_CHECK(ret == WC_NO_ERR_TRACE(BUFFER_E), + ":8232 idx == pkiMsg2Sz sets BUFFER_E before the tag test"); + + WB_NOTE("PKCS7_VerifySignedData(): STAGE7 with a signature OCTET STRING" + " behind the SignerInfo [:8237 cond 0 true]"); + ret = wb_stage7_signature((word32)sizeof(wbStage7Si)); + WB_CHECK(ret != WC_NO_ERR_TRACE(BUFFER_E), + ":8237 ret == 0 on arrival, the OCTET STRING tag is read"); +} +#else +static void wb_stage7_signature_rows(void) +{ + WB_NOTE("NO_PKCS7_STREAM/NO_RSA/NO_SHA256; STAGE7 signature-presence" + " rows skipped"); +} +#endif /* !NO_PKCS7_STREAM && !NO_RSA && !NO_SHA256 */ + +/* ------------------------------------------------------------------------- * + * main -- always returns 0 so the test harness keeps this variant's * coverage even if an individual sub-section's build config disables it. * ------------------------------------------------------------------------- */ int main(void) { int rngRet; + setvbuf(stdout, NULL, _IONBF, 0); printf("=== pkcs7 decode-chain white-box (Part 5) ===\n"); rngRet = wc_InitRng(&wbRng); @@ -1454,6 +2061,12 @@ int main(void) wb_octet_accum_chains(); wb_ecdsa_verify_results(); wb_verify_outer_shapes(); + wb_recipient_info_set_shapes(); + wb_parse_signer_info_nodegenerate(); + wb_rsa_spki_guards(); + wb_ktri_key_alg_dispatch(); + wb_ktri2_seq_peek_rows(); + wb_stage7_signature_rows(); if (rngRet == 0) { wc_FreeRng(&wbRng); diff --git a/tests/unit-mcdc/test_pkcs7_fault_whitebox.c b/tests/unit-mcdc/test_pkcs7_fault_whitebox.c index 5ceaaeba46..30020c27d5 100644 --- a/tests/unit-mcdc/test_pkcs7_fault_whitebox.c +++ b/tests/unit-mcdc/test_pkcs7_fault_whitebox.c @@ -1037,7 +1037,7 @@ static void wb_alloc_fault_encodestream(void) * Only compiled when the streaming state machine exists at all * (wc_PKCS7_CreateStream/GrowStream/FreeStream are themselves inside * `#ifndef NO_PKCS7_STREAM` in pkcs7.c, so they are simply not there to - * call under the no_stream campaign variant). + * call under the no_stream suite variant). * ------------------------------------------------------------------------- */ #ifndef NO_PKCS7_STREAM static void wb_growstream_bufsz(void) @@ -1080,7 +1080,7 @@ static void wb_growstream_bufsz(void) /* ------------------------------------------------------------------------- * * Section 11: wc_PKCS7_AddRecipient_KTRI() WOLFSSL_SMALL_STACK alloc-guard * `decoded == NULL || serial == NULL || keyAlgArray == NULL` [:9371]. This - * guard only exists in the WOLFSSL_SMALL_STACK build (small_stack campaign + * guard only exists in the WOLFSSL_SMALL_STACK build (small_stack suite * variant) -- in every other variant these three locals are plain stack * arrays and the line is not even compiled, so the sweep below is inert * (still safe) elsewhere. Allocation order is fixed by source order: serial @@ -1268,7 +1268,7 @@ int main(void) printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_pkcs7_mutate_whitebox.c b/tests/unit-mcdc/test_pkcs7_mutate_whitebox.c index 0978641e54..8bacdedfc1 100644 --- a/tests/unit-mcdc/test_pkcs7_mutate_whitebox.c +++ b/tests/unit-mcdc/test_pkcs7_mutate_whitebox.c @@ -1043,7 +1043,7 @@ int main(void) printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. */ + * entirely in the test harness. */ (void)wb_fail; return 0; } diff --git a/tests/unit-mcdc/test_pkcs7_whitebox.c b/tests/unit-mcdc/test_pkcs7_whitebox.c index 8805fee507..e73af2a31d 100644 --- a/tests/unit-mcdc/test_pkcs7_whitebox.c +++ b/tests/unit-mcdc/test_pkcs7_whitebox.c @@ -23,7 +23,7 @@ * First white-box MC/DC supplement for wolfcrypt/src/pkcs7.c (Part 5). * * 17.8k lines, 245/1058 conditions covered by tests/api at the start of this - * file's existence -- the largest deficit left in the campaign after asn.c. + * file's existence -- the largest deficit left in the harness after asn.c. * Most of the file-static parsing/encoding helpers guard against argument * combinations that every public wrapper already rejects before calling in * (NULL/size cross-checks), or are internal state machines (streaming, @@ -32,8 +32,8 @@ * drives those helpers by hand. * * Coverage is unioned by source line:col with the tests/api pkcs7 run in the - * per-module campaign; only conditions NOT already shown by tests/api are - * targeted below (cross-checked against campaign/reports/pkcs7/GAPS.md at + * per-module suite; only conditions NOT already shown by tests/api are + * targeted below (cross-checked against suite/reports/pkcs7/the uncovered-condition report at * the time of writing). * * NOT covered here (residual, needs follow-up): @@ -46,7 +46,7 @@ * - wc_PKCS7_DecryptKtri/Kari/Kekri/Pwri/Ori internal chains past the * first ASN.1 element (need a valid partial RecipientInfo body). * - wc_PKCS7_AddRecipient_KTRI's WOLFSSL_SMALL_STACK alloc-fail guard - * (needs fault-injection, deferred technique per campaign notes). + * (needs fault-injection, deferred technique per suite notes). * - ML-DSA SignedData sign/verify (WC_PKCS7_HAVE_MLDSA not defined in * this module's config base -- WOLFSSL_HAVE_MLDSA is off). * - wc_PKCS7_CertMatchesSignerInfo's IssuerAndSerialNumber compare @@ -2061,7 +2061,7 @@ int main(void) printf("done (%s)\n", wb_fail ? "with failures" : "ok"); /* Always return 0: a nonzero exit discards this variant's coverage - * entirely in the campaign harness. Failures are surfaced via the + * entirely in the test harness. Failures are surfaced via the * printed [FAIL] lines instead. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_poly1305_whitebox.c b/tests/unit-mcdc/test_poly1305_whitebox.c index 0a77f364ff..525114a910 100644 --- a/tests/unit-mcdc/test_poly1305_whitebox.c +++ b/tests/unit-mcdc/test_poly1305_whitebox.c @@ -30,11 +30,11 @@ * else poly1305_*_avx(...); * * Each of these is a single-condition branch (not a compound MC/DC decision: - * poly1305.c's own db/modules.json-measured MC/DC total is unaffected by + * poly1305.c's own the module registry-measured MC/DC total is unaffected by * which of these paths a given build takes), so this white-box does not - * change the campaign's covered/total counts. It is kept anyway, matching + * change the covered/total counts. It is kept anyway, matching * the intel-dispatch technique used by the aes/sha3 white-boxes and this - * campaign's chacha sibling, for FEATURE/branch-coverage evidence that the + * suite's chacha sibling, for FEATURE/branch-coverage evidence that the * AVX2-false (AVX1-only) side is reachable and correct: on an AVX2-capable * CI host, cpuid_get_flags_ex()'s real detection always takes the AVX2 * branch through the public API, so tests/api alone never demonstrates the @@ -45,7 +45,7 @@ * WC_CPUID_INITIALIZER. Forcing intel_flags to a real (non-initializer) * value before calling wc_Poly1305SetKey() makes it trust our forced value * instead of re-detecting. Crash-safety: this host has real AVX1 hardware - * (see db/modules.json poly1305 notes), so forcing intel_flags to "AVX1 + * (see the module registry poly1305 notes), so forcing intel_flags to "AVX1 * only" and letting the dispatch call the real poly1305_*_avx asm is always * safe: we never claim a capability the CPU lacks, only hide one it has. * @@ -402,7 +402,7 @@ int main(void) wb_poly1305_avx512_dispatch(); wb_poly1305_setkey_guard(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_puf_gf_whitebox.c b/tests/unit-mcdc/test_puf_gf_whitebox.c new file mode 100644 index 0000000000..bb5392b246 --- /dev/null +++ b/tests/unit-mcdc/test_puf_gf_whitebox.c @@ -0,0 +1,95 @@ +/* test_puf_gf_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 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC white-box supplement for wolfcrypt/src/puf.c -- the GF(2^7) helper. + * + * Closes puf.c 123:0, the last open condition in the module: + * + * static WC_INLINE byte gf_mul(byte a, byte b) + * { + * if (a == 0 || b == 0) + * return 0; + * + * `a == 0`'s independence pair needs the decision's TRUE outcome with `a` the + * deciding operand -- i.e. a == 0 -- beside the all-false row. The BCH decoder + * that reaches gf_mul() only multiplies syndrome/locator coefficients it has + * already tested for zero, so every call the KAT makes arrives with both + * operands non-zero and the decision is constant false. gf_mul() is + * file-static, so no caller outside puf.c can supply the missing row: it takes + * a TU that #includes puf.c. + * + * That is what this driver is. It rides the m33mu lane's white-box mechanism + * (variant "lane_whitebox"; lanes/m33mu/entry.sh + harness/build-fw.sh WB_SRC + * mode relink the firmware with puf.c's object replaced by this TU and main() + * routed here), which is also why it could not exist before: the lane's only + * driver used to be tests/unit-mcdc/test_puf_whitebox.c riding along as an + * UNinstrumented lane_extra_source, and that one can only call puf.c's public + * entry points. + * + * Three vectors, all in this one binary and all pure table arithmetic with no + * state, no allocation and no I/O: + * (a == 0, b != 0) -> decision true on the first operand + * (a != 0, b == 0) -> decision true on the second (already covered, kept so + * the pair is complete inside this binary) + * (a != 0, b != 0) -> the all-false row + * The same three are issued against gf_inv()'s single-condition guard so its + * zero arm is exercised here too. + */ + +#include + +#include + +#include + +int main(void) +{ + /* Unbuffered: on a timeout the process is killed and anything still + * buffered is lost, which reads as an empty log. */ + setvbuf(stdout, NULL, _IONBF, 0); + + printf("puf.c GF(2^7) white-box supplement\n"); + +#ifdef WOLFSSL_PUF + { + unsigned acc = 0; + int i; + + /* a == 0 with b non-zero: the row the BCH decoder never produces. */ + for (i = 1; i < 8; i++) { + acc += gf_mul(0, (byte)i); + acc += gf_mul((byte)i, 0); + acc += gf_mul((byte)i, (byte)(i + 1)); + acc += gf_inv((byte)i); + } + acc += gf_mul(0, 0); + acc += gf_inv(0); + printf(" [wb] gf_mul/gf_inv zero and non-zero operands issued (%u)\n", + acc); + } +#else + printf(" WOLFSSL_PUF not compiled; nothing to exercise\n"); +#endif + + /* Always 0: a nonzero exit discards this white-box row's coverage. */ + return 0; +} diff --git a/tests/unit-mcdc/test_pwdbased_whitebox.c b/tests/unit-mcdc/test_pwdbased_whitebox.c index 1280c307c2..5d05458ff3 100644 --- a/tests/unit-mcdc/test_pwdbased_whitebox.c +++ b/tests/unit-mcdc/test_pwdbased_whitebox.c @@ -71,7 +71,7 @@ * dead. An unarmed derivation is still driven below so this binary's own * MC/DC record carries the (T,T)/(T,F) rows next to the unreachable one. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with pwdbased.o removed. Not part of the wolfSSL build. */ diff --git a/tests/unit-mcdc/test_random_fault_whitebox.c b/tests/unit-mcdc/test_random_fault_whitebox.c index 651004613d..a40e0f6ff8 100644 --- a/tests/unit-mcdc/test_random_fault_whitebox.c +++ b/tests/unit-mcdc/test_random_fault_whitebox.c @@ -23,7 +23,7 @@ * ENTROPY / SEED-DERIVATION FAULT white-box supplement for * wolfcrypt/src/random.c. * - * This is the campaign's first RNG-failure injection driver. Two independent + * This is the first RNG-failure injection driver. Two independent * levers are combined here, and both generalise to any module that consumes * randomness -- see "REUSING THIS" at the bottom of this comment. * @@ -132,7 +132,7 @@ * generation, blinding, nonce derivation): install a staged/failing seed * callback and drive the module's public entry point twice. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with random.o removed. Not part of the wolfSSL build. */ diff --git a/tests/unit-mcdc/test_random_whitebox.c b/tests/unit-mcdc/test_random_whitebox.c index b466064d85..3cf934c904 100644 --- a/tests/unit-mcdc/test_random_whitebox.c +++ b/tests/unit-mcdc/test_random_whitebox.c @@ -22,7 +22,7 @@ /* White-box supplement for wolfcrypt/src/random.c. * * Two Hash_DRBG-core MC/DC leaves are structurally unreachable from the - * public wc_* API in this campaign, no matter what combination of public + * public wc_* API in this suite, no matter what combination of public * arguments a caller supplies: * * - Hash_gen()/Hash512_gen()'s "out != NULL && outSz != 0" guard around @@ -50,17 +50,17 @@ * 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 + * Two further the uncovered-condition report residual classes remain justified SKIPS, deliberately + * NOT chased by this white-box (per the 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 + * calls; this suite injects no allocation-failure fault (same * documented residual class as the rsa/sp-math allocation-failure - * branches -- see db/modules.json's "random" entry). + * branches -- see the module registry'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 @@ -649,7 +649,7 @@ static void wb_hash512_gen_alloc_guard(void) * BUILD-AXIS GUARD: that chain compiles exactly ONE wc_GenerateSeed body, and * the other arms neither share this guard nor, in the CUSTOM_RAND_GENERATE_BLOCK * case, define wc_GenerateSeed at all (that variant would not even link this - * TU). The condition below excludes every arm reachable from this campaign's + * TU). The condition below excludes every arm reachable from this suite's * variant set and from a host build, so the NULL vectors are only compiled * where the guard that catches them is. * ------------------------------------------------------------------------ */ @@ -718,7 +718,7 @@ int main(void) wb_hash512_gen_alloc_guard(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures are surfaced as skips, not test failures: the - * campaign treats a nonzero exit as a failed variant and discards its + * suite treats a nonzero exit as a failed variant and discards its * coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_rsa_fault_whitebox.c b/tests/unit-mcdc/test_rsa_fault_whitebox.c index 82887f10bd..2e26ff05ea 100644 --- a/tests/unit-mcdc/test_rsa_fault_whitebox.c +++ b/tests/unit-mcdc/test_rsa_fault_whitebox.c @@ -52,7 +52,7 @@ * sweep simply finds fewer heap sites to fault and the targets run to * completion), which is why it is safe to wire as a normal whitebox entry that * every variant compiles. ==> wire it with the small_stack -D (see the - * modules.json note at the end of this file's commit message). + * the module registry note at the end of this file's commit message). * * It #includes rsa.c directly (like the sibling test_rsa_whitebox.c and the * other unit-mcdc white-boxes) to reach the file-static wc_CompareDiffPQ / @@ -72,7 +72,7 @@ * the RSA verify-decrypt padding comparisons (3584/3595/3631/4071/4074) and the * *_KeyDecodeRaw / CheckProbablePrime_ex argument guards (5293/5914/5996/6004). * - * STRUCTURALLY UNSATISFIABLE (recorded in campaign/db/exclusions.json; line + * STRUCTURALLY UNSATISFIABLE (recorded in the exclusion record; line * numbers are rsa.c's). None of these is "hard to reach" -- each is an * argument that the missing row does not exist: * @@ -125,7 +125,7 @@ * ./test_rsa_fault_whitebox default: full fault-index sweep * ./test_rsa_fault_whitebox baseline only the unarmed valid ops (delta base) * ./test_rsa_fault_whitebox probe per-entry-point allocation counts - * (Default is the sweep so the campaign's run_whitebox harness -- which runs the + * (Default is the sweep so the run_whitebox harness -- which runs the * binary with NO arguments -- gets full coverage.) */ diff --git a/tests/unit-mcdc/test_rsa_whitebox.c b/tests/unit-mcdc/test_rsa_whitebox.c index 79dd9f8428..ebb69c8d57 100644 --- a/tests/unit-mcdc/test_rsa_whitebox.c +++ b/tests/unit-mcdc/test_rsa_whitebox.c @@ -10,13 +10,13 @@ * calling the helpers with both halves of each MC/DC independence pair. * * 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 + * source line:col in the per-module suite: + * llvm-cov computes MC/DC independence PER BINARY, and the * 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.sh's white-box step with the SAME MC/DC CFLAGS, + * Build: compiled by the coverage runner'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 rsa.o removed (this TU supplies * the instrumented rsa.c). NOT part of the wolfSSL build; not registered in @@ -1000,7 +1000,7 @@ int main(void) wb_public_encrypt_size_guard(); wb_private_decrypt_outlen(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_sakke_fault_whitebox.c b/tests/unit-mcdc/test_sakke_fault_whitebox.c index 0f4a48ddd1..4c694fee4f 100644 --- a/tests/unit-mcdc/test_sakke_fault_whitebox.c +++ b/tests/unit-mcdc/test_sakke_fault_whitebox.c @@ -48,7 +48,7 @@ * sakke_compute_point_r are reachable for direct armed calls, and so * llvm-cov attributes the coverage to sakke.c's own decisions. * - * Targeted GAPS.md residuals (err==0 FALSE half unless noted): + * Targeted the uncovered-condition report residuals (err==0 FALSE half unless noted): * 411 sakke_mulmod_base_add "(err==0) && map" (non-SP build only) * 536 wc_MakeSakkeKey "(err==0) && mp_iszero(..)" cond 0 only; * cond 1 (mp_iszero true, random scalar == 0) is crypto-unreachable. @@ -75,7 +75,7 @@ * ./test_sakke_fault_whitebox default: full fault-index sweeps * ./test_sakke_fault_whitebox baseline unarmed valid ops only (delta base) * ./test_sakke_fault_whitebox probe print per-target allocation counts - * The campaign run_whitebox harness runs the binary with NO arguments, so the + * The white-box harness runs the binary with NO arguments, so the * default is the productive full sweep. */ @@ -469,7 +469,7 @@ int main(int argc, char** argv) #ifndef MCDC_FA_UNAVAILABLE /* ============================================================ * Fault-index sweeps. K values sized from the probe run (see the - * modules.json note); each K over-sweeps its target's allocation + * the module registry note); each K over-sweeps its target's allocation * count by a margin (over-sweeping is harmless -- once n exceeds the * site count the target simply runs to completion). Fresh key state * is (re)built while DISARMED for every group whose target mutates @@ -496,6 +496,39 @@ int main(int argc, char** argv) } WB_NOTE("wc_MakeSakkeKey fault sweep done"); + /* --- wc_MakeSakkeKey 543: the master-secret retry loop + * while ((err == 0) && mp_iszero(wc_ecc_key_get_priv(..))) + * Both conditions need the loop to actually RETRY, i.e. a drawn scalar + * that reduces to zero -- a ~2^-1024 event that no heap fault and no + * seeded RNG can produce (mp_rand() is served from another TU, so the + * seeded-RNG macro never reaches it). The value-forcing mp_rand() + * interposer at the top of this file is the lever; sakke.c has exactly + * one mp_rand() call site, and both modes are ONE-SHOT so the second + * iteration draws real entropy and the loop terminates on real data. + * + * WB_SR_ZERO -> (T,T) decision TRUE, retries; the retry supplies the + * accepting (T,F) row -> closes idx1 and gives idx0 + * the TRUE-decision partner it needs + * WB_SR_FAIL -> (F,-) decision FALSE -> closes idx0 + * Both rows land in THIS binary alongside the ordinary (T,F). --- */ + { + int mode; + for (mode = WB_SR_ZERO; mode <= WB_SR_FAIL; mode++) { + SakkeKey zk; + XMEMSET(&zk, 0, sizeof(zk)); + if (wc_InitSakkeKey_ex(&zk, 128, ECC_SAKKE_1, NULL, + INVALID_DEVID) == 0) { + int e2; + wb_sr_mode = mode; + e2 = wc_MakeSakkeKey(&zk, &rng); + wb_sr_mode = WB_SR_OFF; + printf(" [wb] MakeSakkeKey rand mode %d -> %d\n", + mode, e2); + wc_FreeSakkeKey(&zk); + } + } + } + /* --- wc_MakeSakkeRsk: sweeps its own success chain. rsk output only; * key state (public key + master secret) untouched, so reuse key. * Fresh rsk point per iteration. --- */ @@ -544,6 +577,62 @@ int main(int argc, char** argv) * err==0 false half. Where mp_add/mp_mul_d never allocate (fixed * sp_int), the sweep is a no-op and these stay justified residuals. */ { + /* sakke_tplmod 1518 idx0 (`err == 0` FALSE) -- the THIRD of the + * helper's three sequential reductions: + * + * err = mp_mul_d(a, 3, r); (1511) + * if ((err == 0) && (mp_cmp(r,m) != MP_LT)) sub; (1512) + * if ((err == 0) && (mp_cmp(r,m) != MP_LT)) sub; (1515) + * if ((err == 0) && (mp_cmp(r,m) != MP_LT)) sub; (1518) + * + * 1512 and 1515 get their err!=0 halves for free -- an earlier + * mp_sub failing carries err forward -- but 1518 needs the mp_sub + * at 1516 to have RUN and FAILED, which needs an input where the + * first two reductions both fire. Every in-product caller passes + * a < m, so 3a < 3m and the third check is only ever reached with + * err == 0; a direct call on the file-static helper with a == m + * makes all three fire (300 -> 200 -> 100 -> 0). + * + * The heap lever cannot do it: under a fixed sp_int backend + * mp_sub() never allocates, so it never fails. mcdc_fault_mp.h's + * lever can -- interposed mp_* call 1 is the mp_mul_d, 2 is the + * mp_sub at 1513 and 3 is the mp_sub at 1516, so arming index 3 + * fails exactly that one and 1518 is reached with err != 0. + * + * The accepting (T,T) row at 1518 is the unarmed a == m call right + * before it; the (T,F) row comes from the a=40,m=100 tplmod calls + * in the sweep below (300 -> 200 -> 100 -> stop). */ + { + mp_int a, m, r; + int e0, e1; + mp_init(&a); mp_init(&m); mp_init(&r); + mp_set(&a, 100); mp_set(&m, 100); + e1 = sakke_tplmod(&a, &m, &r); /* 1518 (T,T) */ + mcdc_fm_arm(3); + e0 = sakke_tplmod(&a, &m, &r); /* 1518 (F,-) */ + mcdc_fm_disarm(); + printf(" [wb] sakke_tplmod 1518 vectors: (T,T)=%d armed=%d\n", + e1, e0); + mp_free(&a); mp_free(&m); mp_free(&r); + } + + /* sakke_addmod 1490 idx0, same shape but one reduction: a direct + * armed call is the only way to reach it with err != 0. */ + { + mp_int a, b, m, r; + int e0; + mp_init(&a); mp_init(&b); mp_init(&m); mp_init(&r); + mp_set(&a, 60); mp_set(&b, 70); mp_set(&m, 100); + mcdc_fm_arm(1); + e0 = sakke_addmod(&a, &b, &m, &r); /* 1490 (F,-) */ + mcdc_fm_disarm(); + (void)sakke_addmod(&a, &b, &m, &r); /* 1490 (T,T) */ + mp_set(&a, 10); mp_set(&b, 20); + (void)sakke_addmod(&a, &b, &m, &r); /* 1490 (T,F) */ + printf(" [wb] sakke_addmod 1490 armed=%d\n", e0); + mp_free(&a); mp_free(&b); mp_free(&m); mp_free(&r); + } + for (n = 1; n <= SAKKE_K_SMALLMP; n++) { mp_int a, b, m, r; mp_init(&a); mp_init(&b); mp_init(&m); mp_init(&r); diff --git a/tests/unit-mcdc/test_sha256_whitebox.c b/tests/unit-mcdc/test_sha256_whitebox.c index 3786f014ee..29b3feac5d 100644 --- a/tests/unit-mcdc/test_sha256_whitebox.c +++ b/tests/unit-mcdc/test_sha256_whitebox.c @@ -254,7 +254,7 @@ int main(void) wb_intel_dispatch(); wb_update_transform_err(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_she_whitebox.c b/tests/unit-mcdc/test_she_whitebox.c index a403adec8f..e710d6d3b3 100644 --- a/tests/unit-mcdc/test_she_whitebox.c +++ b/tests/unit-mcdc/test_she_whitebox.c @@ -63,7 +63,7 @@ * genuinely computed H_i, and either way the driver only inspects the returned * status, never the buffer. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with wc_she.o removed. Not part of the wolfSSL build. */ diff --git a/tests/unit-mcdc/test_signature_whitebox.c b/tests/unit-mcdc/test_signature_whitebox.c index 8b669c2f6e..9c11e74f89 100644 --- a/tests/unit-mcdc/test_signature_whitebox.c +++ b/tests/unit-mcdc/test_signature_whitebox.c @@ -75,11 +75,11 @@ * Nothing is read through plain_ptr on this vector -- the guard is FALSE, so * the XMEMCMP at line 313 is not executed. * - * If the campaign prefers not to admit interposition-driven evidence here, + * If the harness prefers not to admit interposition-driven evidence here, * the alternative is to EXCLUDE 311:21:311:42:1 (and, with it, the (T,F) * half only) on the argument above; V1/V2 still close idx0 on their own. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with signature.o removed. Not part of the wolfSSL build. */ diff --git a/tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c b/tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c index 3bd5e92373..b4f3e8dc41 100644 --- a/tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c +++ b/tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c @@ -22,7 +22,7 @@ /* * MC/DC hash-fault white-box supplement for wolfcrypt/src/wc_slhdsa.c. * - * campaign/reports/slhdsa/GAPS.md is almost entirely error propagation: + * suite/reports/slhdsa/the uncovered-condition report is almost entirely error propagation: * * if ((ret == 0) && (hdr != NULL)) -- PRF_msg / H_msg * if ((ret == 0) && (ctxSz > 0) && (ctx != NULL)) streaming chains @@ -41,7 +41,7 @@ * * WHERE THE INDEX HAS TO LAND * --------------------------- - * SLH-DSA sign is by far the most expensive operation in the campaign, so the + * SLH-DSA sign is by far the most expensive operation in the harness, so the * sweep is deliberately shaped: * * - a DENSE head (1..WB_DENSE) over every entry point. Almost all of the @@ -54,7 +54,7 @@ * densely than Sign, because verify is orders of magnitude cheaper. * * Every sweep also tests a CPU-time deadline, so the binary degrades to fewer - * points instead of being killed at the campaign's 600 s TEST_TIMEOUT -- a + * points instead of being killed at the 600 s TEST_TIMEOUT -- a * timeout is scored as a SILENT SKIP and would lose the whole file (HARD * RULE 2). * @@ -70,7 +70,7 @@ * slhdsakey_precompute_sha2_midstates()). * * NOT REACHABLE HERE (documented residuals, mirrored in - * campaign/db/exclusions.json): + * the exclusion record): * - `(ret == 0) && (n > 16)` / `(ret == 0) && (key->params->n > 16)`: the * second operand needs a category 3/5 parameter set, and this module's base * config compiles ONLY the 128-bit sets (WOLFSSL_SLHDSA_PARAM_NO_192/256 @@ -124,7 +124,7 @@ static int wb_fail = 0; #define WB_POINTS_VERIFY 128 #define WB_DEADLINE_S 170 -/* WALL clock, not clock(): the campaign runs several variants concurrently and +/* WALL clock, not clock(): the harness runs several variants concurrently and * TEST_TIMEOUT is 600 s of WALL time. Under that contention CPU time accrues * far slower than wall time, so a CPU-time budget would sail past the timeout * -- and a timed-out white-box is scored as a SILENT SKIP that loses the whole @@ -642,6 +642,6 @@ int main(void) #endif printf("done (%s)\n", wb_fail ? "with failures" : "ok"); - /* A non-zero exit makes the campaign discard this binary's coverage. */ + /* A non-zero exit makes the harness discard this binary's coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_slhdsa_whitebox.c b/tests/unit-mcdc/test_slhdsa_whitebox.c index 6d404544be..f6bef839f8 100644 --- a/tests/unit-mcdc/test_slhdsa_whitebox.c +++ b/tests/unit-mcdc/test_slhdsa_whitebox.c @@ -23,12 +23,12 @@ * mid-sign. Called directly on a stack HashAddress. * * 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): + * source line:col in the per-module suite: * llvm-cov computes MC/DC independence PER BINARY, and aggregate.sh ORs the * "independence shown" bit across binaries by key. Every pair below is * therefore completed *within this file*. * - * Build: compiled by run-mcdc-par.sh's white-box step with the SAME MC/DC + * Build: compiled by the coverage runner'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 wc_slhdsa.o removed * (this TU supplies the instrumented wc_slhdsa.c). NOT part of the wolfSSL @@ -145,11 +145,11 @@ static void wb_ha_encode(void) } /* ------------------------------------------------------------------------- * - * SHA-2 message-hash static functions: gap-closing supplement (see GAPS.md). + * SHA-2 message-hash static functions: gap-closing supplement (see the uncovered-condition report). * * 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 + * (every suite 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. @@ -433,7 +433,7 @@ int main(void) 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 + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_sp_arm32_whitebox.c b/tests/unit-mcdc/test_sp_arm32_whitebox.c index ba9f646887..1d51a83475 100644 --- a/tests/unit-mcdc/test_sp_arm32_whitebox.c +++ b/tests/unit-mcdc/test_sp_arm32_whitebox.c @@ -77,7 +77,7 @@ * This is a coverage-driving supplement, not a known-answer test: only "did * this fail outright" is checked, never a specific expected value. 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). + * line:col in the per-module suite. * * Build: compiled by lanes/qemu-entry.sh's white-box step with the SAME MC/DC * cross CFLAGS (--target=arm-linux-gnueabihf, -DWOLFSSL_SP_ARM32_ASM, @@ -119,6 +119,8 @@ #include "mcdc_fault_mutex.h" +#include "mcdc_fault_alloc.h" + #include #include #include @@ -129,6 +131,18 @@ static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) +/* Crafted-input driver shared with the SP host-backend white-boxes. The four + * ARM backends implement the SAME public API (sp_ecc_verify_, + * sp_ecc_sign_, sp_ecc_check_key_, sp_ModExp_, ...), so the same + * body drives them. It supplies two vectors this file's own drivers cannot: + * a verify whose public point is the Jacobian point at infinity (pZ == 0), + * which is the only way `(err == MP_OKAY) && sp__iszero_(p2->z)` goes + * true, and a sign with a zero private scalar against an all-zero hash, + * which makes s == 0 on EVERY attempt so the SP_ECC_MAX_SIG_GEN retry loop + * runs to exhaustion and leaves its `i > 0` operand false. Both are + * deterministic -- no RNG luck is involved, contrary to the note above. */ +#include "test_sp_crafted_common.h" + #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) @@ -2041,6 +2055,7 @@ int main(void) wb_run_dh_gaps(); wb_run_residual_extra_all(); wb_run_mod_inv(); + wb_spc_all(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); #else diff --git a/tests/unit-mcdc/test_sp_arm64_whitebox.c b/tests/unit-mcdc/test_sp_arm64_whitebox.c index 21ab7ed8ca..a564ae241f 100644 --- a/tests/unit-mcdc/test_sp_arm64_whitebox.c +++ b/tests/unit-mcdc/test_sp_arm64_whitebox.c @@ -121,6 +121,8 @@ #include "mcdc_fault_mutex.h" +#include "mcdc_fault_alloc.h" + #include #include #include @@ -131,6 +133,18 @@ static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) +/* Crafted-input driver shared with the SP host-backend white-boxes. The four + * ARM backends implement the SAME public API (sp_ecc_verify_, + * sp_ecc_sign_, sp_ecc_check_key_, sp_ModExp_, ...), so the same + * body drives them. It supplies two vectors this file's own drivers cannot: + * a verify whose public point is the Jacobian point at infinity (pZ == 0), + * which is the only way `(err == MP_OKAY) && sp__iszero_(p2->z)` goes + * true, and a sign with a zero private scalar against an all-zero hash, + * which makes s == 0 on EVERY attempt so the SP_ECC_MAX_SIG_GEN retry loop + * runs to exhaustion and leaves its `i > 0` operand false. Both are + * deterministic -- no RNG luck is involved, contrary to the note above. */ +#include "test_sp_crafted_common.h" + #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) @@ -1549,6 +1563,7 @@ int main(void) wb_run_rsa_dh_bounds(); wb_run_check_key_priv_all(); wb_run_mod_inv(); + wb_spc_all(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); #else diff --git a/tests/unit-mcdc/test_sp_arm_fault_common.h b/tests/unit-mcdc/test_sp_arm_fault_common.h index 81536de948..91c9c09ea8 100644 --- a/tests/unit-mcdc/test_sp_arm_fault_common.h +++ b/tests/unit-mcdc/test_sp_arm_fault_common.h @@ -46,9 +46,9 @@ * mean a whole extra cross build + emulator pass per lane. * * The including TU therefore defines WOLFSSL_SP_SMALL_STACK for ITSELF, before - * it #includes the sp_arm*.c under test. That is sound for this campaign and + * it #includes the sp_arm*.c under test. That is sound for this suite and * cheaper than a variant: - * - the lane's white-box recipe (campaign/lanes/qemu-entry.sh) compiles the + * - the lane's white-box recipe (suite/lanes/qemu-entry.sh) compiles the * wb TU with the library's own captured compile line and links it against * libwolfssl.a with the target file's object REMOVED, so the wb binary * contains exactly one copy of sp_arm*.c -- this one -- and there is no @@ -365,7 +365,7 @@ static void wb_fa_curve(int curveId, int fieldSz, const char* label, * its first operand's false row) but it must NOT be driven through * P-256 on sp_arm64.c: sp_256_mod_inv_4() there is hand-written * AArch64 assembly whose loop does not terminate for a == m, and the - * white-box hung until TEST_TIMEOUT killed it -- which the campaign + * white-box hung until TEST_TIMEOUT killed it -- which the harness * records as a silent skip of the whole row. The C mod_inv bodies are * reached instead by the direct sweep in the ordinary white-boxes * (wb_run_mod_inv), which can pick the curves it calls. */ diff --git a/tests/unit-mcdc/test_sp_armthumb_whitebox.c b/tests/unit-mcdc/test_sp_armthumb_whitebox.c index 225c319041..e5786c43d9 100644 --- a/tests/unit-mcdc/test_sp_armthumb_whitebox.c +++ b/tests/unit-mcdc/test_sp_armthumb_whitebox.c @@ -28,7 +28,7 @@ * (--target=arm-linux-gnueabihf -mthumb) and run under an ARM emulator * (qemu-arm). That is why this white-box is a LANE-only supplement (the * "qemu-armthumb" lane in db/lanes.json / the sp-arm-lanes "armthumb" variant - * in db/modules.json), never a native host build: the host x86-64 toolchain + * in the module registry), never a native host build: the host x86-64 toolchain * cannot even assemble the file. * * Unlike the asm-dispatch backends (sp_x86_64.c, the AArch64 armasm files), @@ -106,6 +106,8 @@ #include "mcdc_fault_mutex.h" +#include "mcdc_fault_alloc.h" + #include #include #include @@ -116,6 +118,18 @@ static int wb_fail = 0; #define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) +/* Crafted-input driver shared with the SP host-backend white-boxes. The four + * ARM backends implement the SAME public API (sp_ecc_verify_, + * sp_ecc_sign_, sp_ecc_check_key_, sp_ModExp_, ...), so the same + * body drives them. It supplies two vectors this file's own drivers cannot: + * a verify whose public point is the Jacobian point at infinity (pZ == 0), + * which is the only way `(err == MP_OKAY) && sp__iszero_(p2->z)` goes + * true, and a sign with a zero private scalar against an all-zero hash, + * which makes s == 0 on EVERY attempt so the SP_ECC_MAX_SIG_GEN retry loop + * runs to exhaustion and leaves its `i > 0` operand false. Both are + * deterministic -- no RNG luck is involved, contrary to the note above. */ +#include "test_sp_crafted_common.h" + #if defined(WOLFSSL_HAVE_SP_ECC) || defined(WOLFSSL_HAVE_SP_RSA) || \ defined(WOLFSSL_HAVE_SP_DH) @@ -661,7 +675,7 @@ static void wb_mod_top_bit_odd(mp_int* m, int bits) * false; base=2 with a non-all-ones modulus forces the shape * operand false; base=2 with an all-ones modulus gives the * all-true baseline (self-contained -- not relying on real DH - * traffic elsewhere in the campaign for this size). + * traffic elsewhere in the harness for this size). * 3. Leading-zero-strip loop: base=1 gives a result of 1 (every byte * but the last is 0, closing the "out[i]==0" operand's both * sides in one call); base=0 gives an all-zero result (closing @@ -1845,6 +1859,7 @@ int main(void) wb_run_gap_521(); wb_run_residual_extra_all(); wb_run_mod_inv(); + wb_spc_all(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); #else diff --git a/tests/unit-mcdc/test_sp_c32_whitebox.c b/tests/unit-mcdc/test_sp_c32_whitebox.c index f2fd036771..e40f24c6ef 100644 --- a/tests/unit-mcdc/test_sp_c32_whitebox.c +++ b/tests/unit-mcdc/test_sp_c32_whitebox.c @@ -68,9 +68,9 @@ * This is a coverage-driving supplement, not a known-answer test: only * "did this fail outright" is checked, never a specific expected value. * 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). + * by source line:col in the per-module suite. * - * Build: compiled by run-mcdc.sh's white-box step with the SAME MC/DC CFLAGS + * Build: compiled by the coverage runner's white-box step with the SAME MC/DC CFLAGS * (including -DSP_WORD_SIZE=32, which selects sp_c32.c's body), -DHAVE_CONFIG_H * and -I as the instrumented library, then linked against that * variant's libwolfssl.a with its sp_c32.o removed (this TU supplies the diff --git a/tests/unit-mcdc/test_sp_cortexm_crafted_whitebox.c b/tests/unit-mcdc/test_sp_cortexm_crafted_whitebox.c new file mode 100644 index 0000000000..dbcca5769a --- /dev/null +++ b/tests/unit-mcdc/test_sp_cortexm_crafted_whitebox.c @@ -0,0 +1,128 @@ +/* test_sp_cortexm_crafted_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 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC white-box supplement for the Cortex-M SP backend + * (wolfcrypt/src/sp_cortexm.c) -- crafted-input half. + * + * This is the fourth consumer of tests/unit-mcdc/test_sp_crafted_common.h, + * alongside sp_x86_64.c / sp_c64.c / sp_c32.c and the three qemu-user ARM + * lanes. All seven are implementations of ONE public API, so one body drives + * them; see that header for the full vector list and for the arguments behind + * the SP exclusion families A-G. + * + * It runs on the bare-metal m33mu lane through the lane white-box mechanism + * added in this pass (lib/lanes.sh forwards "lane_whitebox" for kind m33mu + * too; lanes/m33mu/entry.sh + harness/build-fw.sh WB_SRC mode relink the + * firmware with sp_cortexm.c's object replaced by this TU and main() routed + * here). Before that, sp_cortexm.c's only driver was the UNinstrumented + * lane_extra_source constructor in test_sp_cortexm_whitebox.c, which can call + * the file's external-linkage entry points but cannot supply an operand no + * public caller ever passes. + * + * WHAT IT CLOSES (sp_cortexm.c), both from the shared header's vectors: + * 42093:1 `for (i = SP_ECC_MAX_SIG_GEN; err == MP_OKAY && i > 0; i--)` in + * sp_ecc_sign_256() -- the `i > 0` operand's FALSE row. A zero + * private scalar with an all-zero hash makes s = (e + r*d)/k == 0 + * on EVERY attempt, so no attempt is accepted and the loop runs all + * SP_ECC_MAX_SIG_GEN times instead of leaving on `err`. r is + * non-zero and the iteration count is fixed, so nothing here + * depends on the RNG. + * 42989:0 and + * 42989:1 `if ((err == MP_OKAY) && sp_256_iszero_8(p2->z))` in + * sp_256_calc_vfy_point_8(). BOTH operands have to be driven in + * THIS one binary, which is why the TU turns + * WOLFSSL_SP_SMALL_STACK on below rather than leaving the `err` + * operand to the sibling fault white-box. For `A && B`, condition + * A's independence pair needs the decision's TRUE outcome, i.e. + * A = T *with* B = T -- and B is only ever true on the pZ == 0 + * vector, which the fault driver does not have. So: + * :1 a verify whose public point has pZ == 0. The point is then + * the Jacobian point at infinity and every step of the ladder + * keeps z == 0, so B goes true. + * :0 the shared header's allocation sweep over the same entry + * point (WB_SPC_EDGE_SIGNVERIFY, "no-op unless the variant + * sets WOLFSSL_SP_SMALL_STACK"): with the macro on, index 5 + * lets sp_ecc_verify_256's own two SP_ALLOC_VARs and + * sp_256_ecc_mulmod_base_8()'s two succeed and fails + * sp_256_ecc_mulmod_8()'s first, so `err` is MEMORY_E by the + * time the guard is evaluated. + * All of them are paired inside this binary by the ordinary sign/verify calls + * the same header makes just before them. + * + * NOT driven here: the `a == m` modular-inverse vector (verify with + * s == the curve order). test_sp_crafted_common.h already gates it off for + * every assembly backend, WOLFSSL_SP_ARM_CORTEX_M_ASM included -- + * sp_256_mod_inv_8() is hand-written Thumb assembly whose halving loop does + * not terminate on that input. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +/* Before ANY wolfSSL header, so sp_cortexm.c's own + * #ifdef WOLFSSL_SP_SMALL_STACK ... SP_ALLOC_VAR = XMALLOC + err + * arm of the SP_DECL_VAR/SP_ALLOC_VAR macro pair is the one compiled into this + * translation unit -- otherwise the SP temporaries are plain stack arrays, + * `err` is MP_OKAY from entry to exit and the shared header's allocation + * sweeps are inert. Sound for the same reasons spelled out in + * test_sp_arm_fault_common.h: the lane's white-box link contains exactly ONE + * copy of sp_cortexm.c -- this one -- so there is no ODR/ABI split with the + * rest of the firmware, no header or struct layout reacts to the macro, and it + * adds no decision to the file (SP_ALLOC_VAR/SP_FREE_VAR expand to + * single-condition ifs, which carry no MC/DC record), so the file's total + * stays 79 and the union with the other rows stays key-compatible. */ +#ifndef WOLFSSL_SP_SMALL_STACK + #define WOLFSSL_SP_SMALL_STACK +#endif + +#include + +#include + +#include +#include +#include +#include + +#include + +/* Pulls in mcdc_fault_alloc.h and the ecc/dh/random headers it needs itself, + * and defines wb_spc_all(). */ +#include "test_sp_crafted_common.h" + +int main(void) +{ + /* Unbuffered: on a timeout the process is killed and anything still + * buffered is lost, which reads as an empty log. */ + setvbuf(stdout, NULL, _IONBF, 0); + + printf("sp_cortexm.c crafted-input white-box supplement\n"); +#if defined(WOLFSSL_SP_ARM_CORTEX_M_ASM) + wb_spc_all(); + printf("done\n"); +#else + printf(" WOLFSSL_SP_ARM_CORTEX_M_ASM not defined; nothing to exercise\n"); +#endif + /* Always 0: a nonzero exit discards this white-box row's coverage. */ + return 0; +} diff --git a/tests/unit-mcdc/test_sp_cortexm_fault_whitebox.c b/tests/unit-mcdc/test_sp_cortexm_fault_whitebox.c new file mode 100644 index 0000000000..71e651a243 --- /dev/null +++ b/tests/unit-mcdc/test_sp_cortexm_fault_whitebox.c @@ -0,0 +1,84 @@ +/* test_sp_cortexm_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC white-box supplement for the Cortex-M SP backend + * (wolfcrypt/src/sp_cortexm.c) -- heap-fault half. + * + * Sibling of test_sp_arm64_fault_whitebox.c / _arm32_ / _armthumb_ and shares + * their body (tests/unit-mcdc/test_sp_arm_fault_common.h); see that header for + * why the `err == MP_OKAY` operand of this file's success chains is otherwise + * dead by construction, why this TU (not a new lane variant) turns + * WOLFSSL_SP_SMALL_STACK on, and what the allocation sweep does. + * + * WHY THIS FILE EXISTS ONLY NOW + * ---------------------------- + * sp_cortexm.c is measured on the bare-metal m33mu lane, and until this pass + * that lane had no white-box mechanism at all: lib/lanes.sh forwarded a + * variant's "lane_whitebox" rows to kind `qemu-user` only, because those lanes + * can relink against libwolfssl.a with the involved object trimmed. m33mu is a + * single static firmware link with no archive, so the only driver it could + * carry was tests/unit-mcdc/test_sp_cortexm_whitebox.c riding along as a + * lane_extra_source -- an UNinstrumented TU that can call sp_cortexm.c's + * external-linkage entry points but cannot change how the file itself is + * compiled. Nothing it did could make an SP temporary allocation exist, let + * alone fail. + * + * The lane now supports white-boxes properly: harness/build-fw.sh has a + * WB_SRC mode that relinks the firmware from the base build's object manifest + * with sp_cortexm.c's object replaced by THIS TU (compiled with clang MC/DC), + * and main() routed here. That is what makes WOLFSSL_SP_SMALL_STACK -- defined + * below, before the #include -- take effect: this TU holds the one and only + * compiled copy of sp_cortexm.c in the image, so there is no ODR/ABI split + * with the rest of the firmware (the macro only changes function-local + * storage inside the file; no header and no struct layout reacts to it). + * + * WHAT IT CLOSES (all in sp_cortexm.c, all the `err == MP_OKAY` operand): + * 39304:0 39307:0 39310:0 sp_ecc_mulmod_add_256(), three consecutive + * 40819:0 40822:0 40825:0 sp_ecc_mulmod_base_add_256(), three consecutive + * `if ((err == MP_OKAY) && (!inMont))`. inMont is passed 0 so the second + * operand stays true and the sweep is what moves err. + * 42989:0 `if ((err == MP_OKAY) && sp_256_iszero_8(p2->z))` + * in sp_256_calc_vfy_point_8(); err arrives non-MP_OKAY when the + * sp_256_ecc_mulmod_8() immediately above it fails its SP allocation. + * The ordinary (accepting) arrival of each of those guards is produced by the + * same driver's unarmed preparation calls, in this same binary. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +/* Before ANY wolfSSL header, so sp_cortexm.c's own + * #ifdef WOLFSSL_SP_SMALL_STACK ... SP_ALLOC_VAR = XMALLOC + err + * arm of the SP_DECL_VAR/SP_ALLOC_VAR macro pair is the one compiled into this + * translation unit. No header reacts to this macro, so it changes nothing but + * function-local storage inside the file under test. */ +#ifndef WOLFSSL_SP_SMALL_STACK + #define WOLFSSL_SP_SMALL_STACK +#endif + +#include + +#include + +#define SP_ARM_FAULT_LABEL "sp_cortexm.c" +#include "test_sp_arm_fault_common.h" diff --git a/tests/unit-mcdc/test_sp_cortexm_whitebox.c b/tests/unit-mcdc/test_sp_cortexm_whitebox.c index 7fbd3280ea..77a784c9c2 100644 --- a/tests/unit-mcdc/test_sp_cortexm_whitebox.c +++ b/tests/unit-mcdc/test_sp_cortexm_whitebox.c @@ -22,7 +22,7 @@ /* * MC/DC white-box supplement for the Cortex-M SP backend * (wolfcrypt/src/sp_cortexm.c), driven under the bare-metal m33mu emulator - * lane (campaign lane "m33mu", config configs/sp-arm-lanes/user_settings.cortexm.h). + * lane (the m33mu lane). * * LANE CONTRACT / WHY A CONSTRUCTOR (not the usual #include-the-.c whitebox): * The m33mu lane instruments sp_cortexm.c as its own clang TU and links it diff --git a/tests/unit-mcdc/test_sp_crafted_common.h b/tests/unit-mcdc/test_sp_crafted_common.h index 38f8ebd35b..e41474bd5e 100644 --- a/tests/unit-mcdc/test_sp_crafted_common.h +++ b/tests/unit-mcdc/test_sp_crafted_common.h @@ -126,7 +126,7 @@ * words in hand when the windowed loop's bit counter runs out, which is * the only vector of that loop's `i >= 0` operand. * - * NOT REACHABLE -- arguments, mirrored in EXCLUSIONS.md families A-G + * NOT REACHABLE -- arguments, mirrored in the exclusion record families A-G * ----------------------------------------------------------------- * A. `for (j=0; j_ecc_mulmod_stripe_. * Both bounds are compile-time arithmetic on the comb geometry, not @@ -211,9 +211,17 @@ * register quadruple that is identically zero and re-tests bit 0, so it * cannot terminate -- while appending a byte per iteration to a fixed * 0x208-byte stack buffer. It is not MC/DC-instrumented (it is assembly), - * so there is nothing to gain by driving it. See DEATHNOTE.md; the same - * defect is already recorded for sp_arm64.c. */ -#if defined(WOLFSSL_SP_X86_64_ASM) + * so there is nothing to gain by driving it. + * + * EVERY assembly backend has this shape, not just x86-64: the ARM lanes + * (sp_arm64.c, sp_arm32.c, sp_armthumb.c, sp_cortexm.c) hand-write the + * P-256 modular inverse for the same reason and the same defect is already + * recorded for sp_arm64.c. The guard therefore lists all of them -- a + * backend that reaches the `1` arm must have a C sp_256_mod_inv_(). Do + * NOT narrow this back to x86-64: the a == m vector does not return. */ +#if defined(WOLFSSL_SP_X86_64_ASM) || defined(WOLFSSL_SP_ARM64_ASM) || \ + defined(WOLFSSL_SP_ARM32_ASM) || defined(WOLFSSL_SP_ARM_THUMB_ASM) || \ + defined(WOLFSSL_SP_ARM_CORTEX_M_ASM) #define WB_SPC_MODINV_AM_256 0 #else #define WB_SPC_MODINV_AM_256 1 @@ -476,7 +484,7 @@ static void wb_spc_ecc_##BITS(void) \ key.pubkey.y, &one, &one, &smv, &res, NULL); \ } \ } -/* Three degenerate-operand vectors nothing else in the campaign produces. +/* Three degenerate-operand vectors nothing else in the harness produces. * * 1. sign with a zero private scalar and an all-zero hash. s is * (e + r*d) / k mod order, so e == 0 and d == 0 make s == 0 on EVERY @@ -814,7 +822,7 @@ static void wb_spc_all(void) { /* Referenced unconditionally: which of these the preprocessor leaves * with a live use depends on the variant, and an unused static is a - * warning this campaign's builds treat as noise to be avoided. */ + * warning this suite's builds treat as noise to be avoided. */ (void)wb_spc_digest; (void)wb_spc_zdigest; (void)wb_spc_make_modulus; diff --git a/tests/unit-mcdc/test_sp_fault_common.h b/tests/unit-mcdc/test_sp_fault_common.h index 57f9739bae..03b0a5a7de 100644 --- a/tests/unit-mcdc/test_sp_fault_common.h +++ b/tests/unit-mcdc/test_sp_fault_common.h @@ -28,7 +28,7 @@ * * if ((err == MP_OKAY) && ) * - * whose `err == MP_OKAY` operand has no false side in the campaign's builds. + * whose `err == MP_OKAY` operand has no false side in the builds. * The reason is not that the failure is hard to produce, it is that nothing in * the compiled code can produce it: SP_ALLOC_VAR is * @@ -47,7 +47,7 @@ * WOLFSSL_SP_SMALL_STACK. Elsewhere it runs the same operations with the * injector never armed, which costs one quick pass and keeps the file building * in every variant of the module (a white-box that fails to build is a silent - * skip, and the campaign has lost a module's evidence to that twice). + * skip, and the harness has lost a module's evidence to that twice). * * HOW * --- diff --git a/tests/unit-mcdc/test_sp_int_fault_whitebox.c b/tests/unit-mcdc/test_sp_int_fault_whitebox.c index a82556ff2d..b8d2c2aff1 100644 --- a/tests/unit-mcdc/test_sp_int_fault_whitebox.c +++ b/tests/unit-mcdc/test_sp_int_fault_whitebox.c @@ -38,7 +38,7 @@ * checked, whereas otherwise the temporaries are stack arrays and `err` cannot * change at all. * - * The campaign's sp-math module already builds a `small_stack` variant, so + * The suite's sp-math module already builds a `small_stack` variant, so * unlike the SP backends this needs no new configuration -- only this driver. * * METHOD @@ -76,7 +76,7 @@ * rather than evidence. The one exception is the invmod pair, which needs a * modulus of at least 1024 bits to select the division-based inverse. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with sp_int.o removed. Not part of the wolfSSL build. */ @@ -139,7 +139,7 @@ static const char* WB_M1024_EVEN = "00000000000000000000000000000000000000000000000000000000000062"; /* Which internal engines this configuration compiles. Mirrors sp_int.c's own - * guards so the TU builds under every campaign variant. */ + * guards so the TU builds under every suite variant. */ #if (defined(WOLFSSL_SP_MATH_ALL) && !defined(WOLFSSL_RSA_VERIFY_ONLY) && \ !defined(WOLFSSL_RSA_PUBLIC_ONLY)) || !defined(NO_DH) || \ defined(OPENSSL_ALL) diff --git a/tests/unit-mcdc/test_sp_int_whitebox.c b/tests/unit-mcdc/test_sp_int_whitebox.c index ec1c76f143..cd3646c9b2 100644 --- a/tests/unit-mcdc/test_sp_int_whitebox.c +++ b/tests/unit-mcdc/test_sp_int_whitebox.c @@ -16,13 +16,13 @@ * the public API). * * Coverage from this binary is unioned with the tests/api variant coverage - * by source line:col in the per-module campaign: llvm-cov computes MC/DC - * independence PER BINARY, and the campaign's aggregate.sh ORs the + * by source line:col in the per-module suite: llvm-cov computes MC/DC + * independence PER BINARY, and the 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 + * Build: compiled by the coverage runner'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 sp_int.o removed (this TU supplies * the instrumented sp_int.c). NOT part of the wolfSSL build; not registered @@ -37,12 +37,37 @@ * SMALL_STACK allocation-ceiling macros, and the 32-bit SP_WORD_SIZE axis). */ +/* settings.h FIRST, and before mcdc_seed_rng.h: that header decides whether it + * can build its SHAKE-256 stream by testing WOLFSSL_SHAKE256, and with no + * configuration in scope yet the test reads "no" and the header compiles + * itself down to inert stubs -- silently, with the driver still building and + * running. That is what happened on the first attempt at :19672:0 here. + * settings.h is include-guarded and idempotent, so pulling it in early costs + * nothing; sp_int.c below includes it again. */ +#include + +/* Deterministic RNG. Included BEFORE sp_int.c so its wc_RNG_GenerateBlock() + * call sites bind to the pinned SHAKE-256 stream while armed. Two things make + * this load-bearing for Class 23 below, not just cosmetic: + * - the stream is reproducible, so the fail-index sweep is reproducible; + * - the armed hook never enters the DRBG, so it makes NO allocation of its + * own. Under WOLFSSL_SMALL_STACK the real Hash_DRBG_Generate() does + * allocate, and one faulted allocation there puts the WC_RNG into its + * permanent DRBG_FAILED state -- after which every later sweep step dies + * at the first wc_RNG_GenerateBlock() instead of reaching the decision + * under test. That is exactly why the earlier real-entropy sweep never + * closed :19672:0. */ +#include "mcdc_seed_rng.h" + /* Pull sp_int.c in verbatim so its file-static helpers and the sp_int * struct's fields are in scope and instrumented in THIS binary. sp_int.c * includes settings.h (which picks up user_settings.h via * -DWOLFSSL_USER_SETTINGS) and sp_int.h itself. */ #include +#define MCDC_SR_IMPL +#include "mcdc_seed_rng.h" + #include "mcdc_fault_alloc.h" #include @@ -169,7 +194,7 @@ static void wb_set_d(sp_int* a, sp_int_digit v) /* a = 2^bits, written straight into the digit array. * * sp_mul_2d() would be the natural way to build these operands, but it is not - * compiled in every campaign variant (the reduced backend drops it), and this + * compiled in every suite variant (the reduced backend drops it), and this * TU has to build under all of them. Returns MP_VAL when the requested width * does not fit the compile-time digit ceiling so callers can skip that row. */ static int wb_pow2(sp_int* a, int bits) @@ -624,7 +649,7 @@ static void wb_invmod_negative(void) * if ((err == MP_OKAY) && (!sp_iszero(y))) err = MP_VAL; * * sp_invmod() only selects _sp_invmod_div() for a modulus of at least 1024 - * bits, and the campaign's API tests only ever ask for an inverse that + * bits, and the API tests only ever ask for an inverse that * exists, so the loop's leftover is always zero there. Ask for the inverse * of a value that shares a factor with the modulus instead. * ------------------------------------------------------------------------- */ @@ -788,7 +813,7 @@ static void wb_sp_backend_dispatch_one(int bits) * `(mBits == W) && sp_isodd(m) && (bBits <= W) && (eBits <= W)` the * indices are mBits, bBits, eBits, then sp_isodd's two halves. Index 3 * is therefore `m->used != 0`, which sp_exptmod_ex() has already - * rejected via sp_iszero(m); see EXCLUSIONS.md. */ + * rejected via sp_iszero(m); see the exclusion record. */ if ((wb_pow2(&b, bits) == MP_OKAY) && (sp_add_d(&b, 5, &b) == MP_OKAY)) { _sp_init_size(&r, SP_INT_DIGITS); (void)sp_exptmod_ex(&b, &e, 1, &m, &r); @@ -1630,6 +1655,11 @@ static void wb_gcd_r_small_b(void) * modular exponentiation it performs. Its false side (a composite rejected * on the result operand) has to be in the same binary. * ------------------------------------------------------------------------- */ +/* Pinned RNG seed for the randomised Miller-Rabin trial loop (Class 23). + * Recorded here because a seed that reaches a condition is a test vector: the + * next person has to be able to reproduce the result. */ +#define WB_PRIME_RNG_SEED 0x5eed0001UL + static void wb_prime_trial_alloc(void) { sp_int a; @@ -1709,15 +1739,13 @@ static void wb_prime_trial_alloc(void) (void)_sp_prime_random_trials(&a, 8, &res, &rng); wb_set_d(&a, (sp_int_digit)100160063ULL); (void)_sp_prime_random_trials(&a, 8, &res, &rng); - /* NOT CLOSED. The deterministic sibling's `err != MP_OKAY` operand - * closes on this sweep; this one does not, at any depth tried - * (n <= 30, 60, 120). The randomised loop draws a fresh candidate - * before every Miller-Rabin round and re-draws rejected ones, so - * the index of the round's own allocation is not a fixed offset - * from the arming point the way it is in _sp_prime_trials(). A - * pinned-seed RNG (mcdc_seed_rng.h) would make it one; it was not - * added for a single condition, and the condition is reported open - * rather than excluded -- it is not proven unreachable. */ + /* Real-entropy sweep. Kept for the rows it does reach (the + * wc_RNG_GenerateBlock() error break at :19666's neighbour), but it + * does NOT close :19672:0: the first faulted allocation lands inside + * Hash_DRBG_Generate() and leaves the WC_RNG permanently + * DRBG_FAILED, so every later index dies at the draw. Measured: for + * n >= 3 the call returns RNG_FAILURE_E with the RNG reporting a + * failure, never MP_MEM from the exponentiation. */ for (n = 1; n <= 30; n++) { wb_set_d(&a, (sp_int_digit)2147483647UL); mcdc_fa_arm_only(n); @@ -1726,6 +1754,43 @@ static void wb_prime_trial_alloc(void) } wc_FreeRng(&rng); } + + /* :19672:0 (`err != MP_OKAY` of the RANDOMISED trial loop), closed with a + * PINNED, allocation-free RNG on a fresh WC_RNG. + * + * PINNED INPUT (evidence): mcdc_seed_rng.h armed with seed 0x5eed0001; + * candidate a = 2^31 - 1 (prime) for the error rows and + * a = 100160063 = 10007 * 10009 (composite, both factors past the end of + * the small-prime table) for the *result == MP_NO row; trials = 8. + * + * With the armed hook the loop makes no allocation of its own before + * sp_prime_miller_rabin(), so allocation index 1 and 2 are this + * function's two ALLOC_SP_INT_ARRAYs (err set before the loop; the + * decision is not reached) and every index from 3 up lands inside the + * exponentiation, returning MP_MEM into `err` and breaking on THIS + * operand. The two un-armed calls above it in the same binary supply the + * (F,F) row (a prime, so the loop runs every round) and the (F,T) row (a + * composite, rejected on *result). */ + if (wc_InitRng(&rng) == 0) { + mcdc_sr_arm(WB_PRIME_RNG_SEED); + + wb_set_d(&a, (sp_int_digit)2147483647UL); + (void)_sp_prime_random_trials(&a, 8, &res, &rng); + wb_set_d(&a, (sp_int_digit)100160063ULL); + (void)_sp_prime_random_trials(&a, 8, &res, &rng); + + for (n = 1; n <= 24; n++) { + /* Rewind BEFORE arming: re-absorbing the seed allocates. */ + mcdc_sr_rewind(WB_PRIME_RNG_SEED); + wb_set_d(&a, (sp_int_digit)2147483647UL); + mcdc_fa_arm_only(n); + (void)_sp_prime_random_trials(&a, 8, &res, &rng); + mcdc_fa_disarm(); + } + + mcdc_sr_disarm(); + wc_FreeRng(&rng); + } #endif mcdc_fa_disarm(); @@ -1774,7 +1839,7 @@ int main(void) wb_gcd_r_small_b(); wb_prime_trial_alloc(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Setup failures are surfaced as skips, not test failures: the campaign + /* Setup failures are surfaced as skips, not test failures: the harness * treats a nonzero exit as a failed variant and discards its coverage. */ return 0; #endif diff --git a/tests/unit-mcdc/test_sp_x86_64_whitebox.c b/tests/unit-mcdc/test_sp_x86_64_whitebox.c index 81d184edf8..2f561414ee 100644 --- a/tests/unit-mcdc/test_sp_x86_64_whitebox.c +++ b/tests/unit-mcdc/test_sp_x86_64_whitebox.c @@ -32,7 +32,7 @@ * * * On any BMI2+ADX host (which is effectively every build/CI machine this - * campaign runs on) only the accelerated half of each such decision is ever + * suite runs on) only the accelerated half of each such decision is ever * taken by the ordinary tests/api-driven asm run, leaving the generic half * permanently uncovered -- roughly 374 decisions across the file. * @@ -48,11 +48,11 @@ * * Coverage from this binary is unioned with the tests/api variant coverage * (and with the normal, accelerated, asm run of this same file) 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 + * line:col in the per-module suite: + * llvm-cov computes MC/DC independence PER BINARY, and the * aggregate.sh ORs the "independence shown" bit across binaries by key. * - * Build: compiled by run-mcdc.sh's white-box step with the SAME MC/DC CFLAGS, + * Build: compiled by the coverage runner'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 sp_x86_64.o removed (this TU * supplies the instrumented sp_x86_64.c). NOT part of the wolfSSL build; not @@ -110,7 +110,7 @@ * would require calling the avx2 variant directly while ALSO forcing * cpuid_get_flags() to report BMI2/ADX absent for that one call, which * doesn't correspond to any state the real dispatch logic can reach. These - * are left uncovered here and logged as a residual/DEATHNOTE class rather + * are left uncovered here and logged as a residual class rather * than driven via an impossible-state call. * * ------------------------------------------------------------------------- @@ -557,7 +557,7 @@ static void wb_run_rsa_free(void) * "Group 15" 3072-bit prime from memory risks a transcription error, and * generating one at runtime via wc_DhGenerateParams(3072) is a slow * probable-safe-prime search that would meaningfully slow this binary - * down for a size this campaign only asks for "if convenient". The + * down for a size this suite only asks for "if convenient". The * generic sp_ModExp_3072/sp_DhExp_3072 decisions are still covered via * the RSA-3072 path above (same underlying generic Montgomery modexp * routines), so 2048-bit alone still exercises the DH-specific @@ -1458,7 +1458,7 @@ static void wb_run_dispatch_521(void) /* ----------------------------------------------------------------------- * * SAKKE (1024-bit): sp_1024_div_16/from_bin/to_mp. Niche feature, almost - * certainly not enabled in this campaign's builds -- WB_NOTE-skip if not. + * certainly not enabled in this suite's builds -- WB_NOTE-skip if not. * ----------------------------------------------------------------------- */ static void wb_run_dispatch_1024(void) { @@ -1506,7 +1506,7 @@ static void wb_run_dispatch(void) * * sp_ecc_mulmod_add_256/384/521(): each has * if ((err == MP_OKAY) && (!inMont)) { ... sp__mod_mul_norm_ ... } - * repeated for x/y/z (the ~36-conditions-across-3-curves the campaign + * repeated for x/y/z (the ~36-conditions-across-3-curves the harness * counts), plus a final `if (map) { ... }`. Driving all 4 (inMont, map) * combinations with a real curve point (from wc_ecc_make_key_ex()) as both * the multiplicand and the point to add covers every operand of both @@ -1529,7 +1529,7 @@ static void wb_run_dispatch(void) * from the file-static p_mod array, visible in this TU because * sp_x86_64.c is #included, not linked); false via a real coordinate * (which is always < the modulus). - * privm is passed as NULL throughout (this campaign only needs the public- + * privm is passed as NULL throughout (this suite only needs the public- * point guards, not the private-scalar-matches-point path, which is * already exercised for real keys by wb_run_ecc()). * @@ -1915,7 +1915,7 @@ int main(void) /* The dispatch decisions in sp_x86_64.c are `IS_INTEL_BMI2(f) && * IS_INTEL_ADX(f)` (two conditions) plus single-condition * `IS_INTEL_MOVBE(f)` checks. CRITICAL: llvm-cov computes MC/DC - * independence PER BINARY, and the campaign only ORs the resulting + * independence PER BINARY, and the harness only ORs the resulting * covered-bit across binaries -- it does NOT reconstruct an independence * pair from vectors spread over different binaries. So THIS binary must * itself observe all three vectors of `A && B` (TT, FT, TF). The ordinary diff --git a/tests/unit-mcdc/test_srp_whitebox.c b/tests/unit-mcdc/test_srp_whitebox.c new file mode 100644 index 0000000000..3a61397265 --- /dev/null +++ b/tests/unit-mcdc/test_srp_whitebox.c @@ -0,0 +1,184 @@ +/* test_srp_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC hash-fault white-box supplement for wolfcrypt/src/srp.c. + * + * THE ONE OPEN CONDITION + * ---------------------- + * srp.c:1057 if (!r && ConstantCompare(proof, digest, (int)size) != 0) + * + * `r` at that point comes from SrpHashFinal() (:1049), and -- on the server + * side only -- from the two SrpHashUpdate() calls at :1055-:1056. Those are + * file-static dispatchers straight onto wc_ShaFinal/wc_Sha256Final/ + * wc_Sha384Final/wc_Sha512Final, which cannot fail on a live, initialised + * context. So every reachable call from tests/api arrives here with r == 0 + * and only ever shows the operand TRUE: the (T,T) row (a wrong proof, giving + * SRP_VERIFY_E) and the (T,F) row (a correct proof) are both there, but the + * idx0 independence pair -- (T,T) against (F,.) -- is not. + * + * mcdc_fault_hash.h is the lever for exactly this shape: it + * macro-interposes the hash primitives for THIS translation unit only, before + * srp.c is #included, so mcdc_fh_arm(1) makes the very next primitive call + * (and every later one) return BAD_FUNC_ARG. SrpHashFinal() then propagates + * that into `r`, and :1057 is evaluated with `!r` FALSE while the decision + * short-circuits -- the missing half. The unarmed (T,T) partner is driven in + * the SAME binary immediately before it, which is what MC/DC needs: llvm-cov + * computes independence per binary and the harness only ORs the resulting + * bits by line:col. + * + * Note that wc_SrpVerifyPeersProof()'s SHA-256 proof hash is used here in its + * freshly initialised state (wc_SrpInit() runs SrpHashInit() on both proof + * contexts). No SRP handshake is needed to reach :1057 -- the decision does + * not depend on the session key, only on the digest/proof comparison -- and + * keeping the fixture to wc_SrpInit() makes every vector deterministic and + * cheap (no modexp, well inside TEST_TIMEOUT). tests/api/test_srp.c already + * carries the full handshake, including the corrupted-proof rejection. + * + * Build: compiled by the coverage runner'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 srp.o removed (this TU + * supplies the instrumented srp.c). NOT part of the wolfSSL build; not + * registered in tests/api. See tests/unit-mcdc/README.md. + */ + +#include "mcdc_fault_hash.h" + +/* Pull srp.c in verbatim so the file-static SrpHash* dispatchers are in scope + * and instrumented in THIS binary, and so the macros above rewrite srp.c's + * own primitive call sites. */ +#include + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#if defined(WOLFCRYPT_HAVE_SRP) && !defined(NO_SHA256) && \ + defined(MCDC_FH_HAVE_SHA256) + +/* ------------------------------------------------------------------------ * + * srp.c:1057 if (!r && ConstantCompare(proof, digest, (int)size) != 0) + * + * vector A (unarmed, wrong proof) -> (T,T) decision TRUE -> SRP_VERIFY_E + * vector B (unarmed, right proof) -> (T,F) decision FALSE -> 0 + * vector C (armed, any proof) -> (F,.) decision FALSE -> BAD_FUNC_ARG + * + * A against C is the idx0 independence pair; A against B is idx1's (already + * shown by tests/api, repeated here so this binary stands on its own). + * ------------------------------------------------------------------------ */ +static void wb_verify_peers_proof_hash_fault(void) +{ + Srp srp; + byte proof[WC_SHA256_DIGEST_SIZE]; + byte expect[WC_SHA256_DIGEST_SIZE]; + int ret; + + /* The proof context is the one wc_SrpInit() built and nothing has been + * absorbed into it, so the digest wc_SrpVerifyPeersProof() computes is + * SHA-256 over the empty message. Derive it the same way, unarmed. */ + { + wc_Sha256 sha; + + XMEMSET(expect, 0, sizeof(expect)); + if (wc_InitSha256(&sha) != 0) { + WB_NOTE("wc_InitSha256 failed; skipping srp proof vectors"); + return; + } + ret = wc_Sha256Final(&sha, expect); + wc_Sha256Free(&sha); + if (ret != 0) { + WB_NOTE("wc_Sha256Final failed; skipping srp proof vectors"); + return; + } + } + + /* Vector A: r == 0, proof mismatches -> both operands TRUE. */ + XMEMSET(&srp, 0, sizeof(srp)); + if (wc_SrpInit(&srp, SRP_TYPE_SHA256, SRP_CLIENT_SIDE) != 0) { + WB_NOTE("wc_SrpInit failed; skipping srp proof vectors"); + return; + } + XMEMSET(proof, 0, sizeof(proof)); + proof[0] = (byte)(expect[0] ^ 0x01); + ret = wc_SrpVerifyPeersProof(&srp, proof, (word32)sizeof(proof)); + if (ret != WC_NO_ERR_TRACE(SRP_VERIFY_E)) { + WB_NOTE("wrong proof was not rejected with SRP_VERIFY_E"); + wb_fail = 1; + } + wc_SrpTerm(&srp); + + /* Vector B: r == 0, proof matches -> idx0 TRUE, idx1 FALSE. */ + XMEMSET(&srp, 0, sizeof(srp)); + if (wc_SrpInit(&srp, SRP_TYPE_SHA256, SRP_CLIENT_SIDE) != 0) { + WB_NOTE("wc_SrpInit failed; skipping matching-proof vector"); + } + else { + XMEMCPY(proof, expect, sizeof(proof)); + ret = wc_SrpVerifyPeersProof(&srp, proof, (word32)sizeof(proof)); + if (ret != 0) { + WB_NOTE("matching proof was not accepted"); + wb_fail = 1; + } + wc_SrpTerm(&srp); + } + + /* Vector C: SrpHashFinal() fails, so :1057 is reached with r != 0 and + * `!r` is FALSE -- the half no live context can produce. Arm for exactly + * this one call so nothing else in the binary is faulted. */ + XMEMSET(&srp, 0, sizeof(srp)); + if (wc_SrpInit(&srp, SRP_TYPE_SHA256, SRP_CLIENT_SIDE) != 0) { + WB_NOTE("wc_SrpInit failed; skipping hash-fault vector"); + return; + } + XMEMSET(proof, 0, sizeof(proof)); + mcdc_fh_arm(1); + ret = wc_SrpVerifyPeersProof(&srp, proof, (word32)sizeof(proof)); + mcdc_fh_disarm(); + if (ret != WC_NO_ERR_TRACE(MCDC_FH_ERR)) { + WB_NOTE("faulted SrpHashFinal did not propagate out of " + "wc_SrpVerifyPeersProof"); + wb_fail = 1; + } + wc_SrpTerm(&srp); + + WB_NOTE("wc_SrpVerifyPeersProof !r / ConstantCompare pairs exercised"); +} + +#else /* !(WOLFCRYPT_HAVE_SRP && !NO_SHA256 && MCDC_FH_HAVE_SHA256) */ + +static void wb_verify_peers_proof_hash_fault(void) +{ + WB_NOTE("SRP or SHA-256 not compiled in; srp proof vectors skipped"); +} + +#endif + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("srp.c white-box supplement\n"); + wb_verify_peers_proof_hash_fault(); + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Setup issues are surfaced as skips; a nonzero exit would make the + * suite discard this variant's coverage. */ + return 0; +} diff --git a/tests/unit-mcdc/test_tfm_fault_whitebox.c b/tests/unit-mcdc/test_tfm_fault_whitebox.c new file mode 100644 index 0000000000..7a9e53215f --- /dev/null +++ b/tests/unit-mcdc/test_tfm_fault_whitebox.c @@ -0,0 +1,493 @@ +/* test_tfm_fault_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Second MC/DC white-box supplement for wolfcrypt/src/tfm.c (FASTMATH + * big-integer engine, bigint-tfm module). It complements + * test_tfm_whitebox.c, which reaches the file-static helpers; this TU targets + * the seven conditions that survived that driver, the tests/api + * DecisionCoverage extensions and the four native build variants, and which + * step 7 relabelled away from the (retired) "32-bit axis" residual class. + * + * Every section supplies BOTH halves of the targeted independence pair inside + * THIS binary - llvm-cov computes MC/DC per binary and the harness only + * unions the "independence shown" bit by source line:col, so a rejection + * vector without its accepting partner in the same binary proves nothing. + * + * --------------------------------------------------------------------------- + * 1. fp_exptmod / fp_exptmod_ex / fp_exptmod_nct, negative-exponent branch + * 2927:0, 3022:0, 3120:0 `if ((err == 0) && (P->sign == FP_NEG))` + * + * The recorded recipe for these ("an mcdc_fault_alloc.h row hugging + * fp_exptmod_ex") and the model in test_tfm_whitebox.c's + * wb_TfmExptModDecisionCoverage ("call C: invmod fails (gcd=7), err==0 F") + * are BOTH wrong about where `err` comes from. The decision sits INSIDE + * + * err = fp_invmod(&tmp[0], &tmp[1], &tmp[0]); + * if (err == FP_OKAY) { + * ... + * err = _fp_exptmod_ct/_nct(...); + * if ((err == 0) && (P->sign == FP_NEG)) { err = fp_add(Y, P, Y); } + * } + * + * so an fp_invmod failure never REACHES line 2927 - it skips the whole + * block. The only `err` the decision can see is the exponentiation + * engine's, and that engine must fail with the invmod having SUCCEEDED. + * + * No injector is needed. Both engines open with + * if ((err = fp_montgomery_setup(P, &mp)) != FP_OKAY) return err; + * and fp_montgomery_setup rejects an even modulus outright + * (tfm.c:3480 `b = a->dp[0]; if ((b & 1) == 0) return FP_VAL;`). A + * negative modulus with an EVEN magnitude therefore reaches the engine and + * fails it, while fp_invmod - which for an even modulus dispatches to + * fp_invmod_slow (tfm.c:1284) - still succeeds whenever gcd(G,|P|) == 1. + * G = 3, X = -3, P = -14: invmod(3,14) = 5, montgomery_setup(14) = FP_VAL, + * so `err == 0` is FALSE with the decision reached. P = -7 (odd) is the + * accepting partner: engine succeeds, P->sign == FP_NEG, decision TRUE. + * + * 2. fp_to_unsigned_bin_len trailing-significance check + * 3988:0 `if ((i == a->used - 1) && ((a->dp[i] >> j) != 0))` + * + * Crafted input, no injector. `i` is the digit cursor the copy loop stopped + * at. Its three reachable shapes are selected purely by the requested + * output length c against a->used: + * c == DIGIT_BIT/8 with used == 1: the loop consumes the whole digit and + * exits on `i < a->used`, leaving i == a->used, so `i == a->used - 1` + * is FALSE (cond0 F, cond1 unevaluated - the rejecting half); + * c == 2 with used == 1 and a < 2^16: loop exits on x < 0 with i == 0 == + * used-1 and no significant bits left (cond0 T, cond1 F); + * c == DIGIT_BIT/8 with used == 2: i stops one digit short and the top + * digit is nonzero (cond0 T, cond1 T -> FP_VAL). + * + * 3. fp_isprime_ex Miller-Rabin error propagation + * 5200:0 `if ((err != FP_OKAY) || (res == FP_NO))` + * + * Crafted input, no injector. fp_prime_miller_rabin() forwards the return + * of fp_exptmod(b, r, a, y), whose modulus is the CANDIDATE a; fp_exptmod + * rejects `P->used > (FP_SIZE/2)` at tfm.c:2857. A candidate wider than + * FP_SIZE/2 digits that still survives the 256-entry trial-division loop + * above therefore makes the very first Miller-Rabin round return FP_VAL. + * A power of 1621 (the first prime ABOVE primes[FP_PRIME_SIZE-1] == 1619) + * is coprime to every table entry by construction, so it passes trial + * division for free. The accepting partners are ordinary calls on 1621 + * (prime: err FP_OKAY, res FP_YES) and on 1621*1627 (composite that also + * survives trial division: err FP_OKAY, res FP_NO -> cond1's own pair). + * + * 4. fp_isprime_ex trial-division error propagation + * 5188:0 `if (res != MP_OKAY || d == 0)` + * + * fp_mod_d() is fp_div_d(), whose only non-FP_OKAY returns are FP_VAL for a + * zero divisor (primes[] contains none) and FP_MEM from its + * WC_ALLOC_VAR_EX scratch. That allocation exists ONLY under + * WOLFSSL_SMALL_STACK (types.h:983), so this operand is closable in the + * small_stack variant and there alone: mcdc_fault_alloc.h + a one-shot + * fail-index sweep. Under the other variants the sweep is inert (no + * allocation is issued inside fp_isprime_ex) and the calls simply run to + * completion, which is harmless. The all-FALSE partner is the unarmed call + * in section 3. + * + * 5. mp_prime_is_prime_ex random-base rejection + * 5319:0 `if (fp_cmp_d(b, 2) != FP_GT || fp_cmp(b, c) != FP_LT)` + * + * The base b is read from wc_RNG_GenerateBlock(), so `b <= 2` is a lottery + * - for the smallest candidate this loop will accept (11 bits, baseSz 2, + * top byte masked to 3 bits) it is a 3-in-2048 draw, and ASIL-D evidence + * cannot rest on a lottery. This TU interposes wc_RNG_GenerateBlock with a + * SCRIPTED byte stream (the mcdc_seed_rng.h macro trick, but with a chosen + * script rather than a SHAKE squeeze, because the target range is too + * narrow to hit by seed search and a script is reproducible by + * inspection): + * draw 1 -> b = 1 (cond0 TRUE via fp_cmp_d == FP_LT) + * draw 2 -> b = 2 (cond0 TRUE via fp_cmp_d == FP_EQ) + * draw 3 -> b = 2047 (cond0 FALSE, cond1 TRUE: b >= c == 1619) + * draw 4+ -> b = 5 (both FALSE: a real Miller-Rabin round, t--) + * The post-script filler is a valid base, so the `continue` loop always + * terminates. Disarmed, the hook is a straight pass-through to the real + * wc_RNG_GenerateBlock. + * --------------------------------------------------------------------------- + * + * Crash safety: the crafted vectors are ordinary fp_int values built through + * the fp_* API; the faulted vectors fail a single allocation whose caller + * returns FP_MEM before initialising anything (WC_ALLOC_VAR_EX and + * fp_prime_miller_rabin's XMALLOC both return immediately on NULL), and the + * harness never dereferences a faulted call's output. Operands are rebuilt + * while DISARMED on every iteration. + * + * Determinism: no wall clock, no live entropy on any measured path - the RNG + * hook replaces the only randomness tfm.c consumes. + * + * It #includes tfm.c directly (like every other unit-mcdc white-box) to be the + * single instrumented definition; the library's tfm.o is trimmed from the + * archive at link time. main() always returns 0 - a nonzero exit makes the + * suite discard the whole variant. + */ + +#include +#include + +/* Scripted RNG hook, declared while wc_RNG_GenerateBlock still means the real + * thing (random.h is already in above, so its prototype is not rewritten). */ +#ifndef WC_NO_RNG +static int mcdc_tfm_rng_block(WC_RNG* rng, byte* out, word32 sz); +#define wc_RNG_GenerateBlock(rng, out, sz) mcdc_tfm_rng_block((rng), (out), (sz)) +#endif + +#include + +#ifndef WC_NO_RNG +#undef wc_RNG_GenerateBlock +#endif + +#include "mcdc_fault_alloc.h" + +#include +#include + +#if defined(__GNUC__) || defined(__clang__) + #define WB_MAYBE_UNUSED __attribute__((unused)) +#else + #define WB_MAYBE_UNUSED +#endif + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +/* ------------------------------------------------------------------------ */ +/* scripted RNG (section 5) */ +/* ------------------------------------------------------------------------ */ +#ifndef WC_NO_RNG + +static int mcdc_tfm_rng_armed = 0; +static word32 mcdc_tfm_rng_pos = 0; + +/* Two bytes per draw; see section 5 of the header comment. */ +static const byte mcdc_tfm_rng_script[] = { + 0x00, 0x01, /* b = 1 */ + 0x00, 0x02, /* b = 2 */ + 0x07, 0xff /* b = 2047 */ +}; + +WB_MAYBE_UNUSED static void mcdc_tfm_rng_arm(void) +{ + mcdc_tfm_rng_armed = 1; + mcdc_tfm_rng_pos = 0; +} + +WB_MAYBE_UNUSED static void mcdc_tfm_rng_disarm(void) +{ + mcdc_tfm_rng_armed = 0; + mcdc_tfm_rng_pos = 0; +} + +static int mcdc_tfm_rng_block(WC_RNG* rng, byte* out, word32 sz) +{ + word32 i; + + if (!mcdc_tfm_rng_armed) + return wc_RNG_GenerateBlock(rng, out, sz); + + for (i = 0; i < sz; i++) { + if (mcdc_tfm_rng_pos < (word32)sizeof(mcdc_tfm_rng_script)) { + out[i] = mcdc_tfm_rng_script[mcdc_tfm_rng_pos++]; + } + else { + /* filler: a valid base (0x0005 big-endian) so the loop ends */ + out[i] = (byte)((i + 1 == sz) ? 0x05 : 0x00); + } + } + return 0; +} + +#endif /* !WC_NO_RNG */ + +#if !defined(USE_FAST_MATH) + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("tfm.c fault white-box: USE_FAST_MATH not defined, nothing to do\n"); + (void)wb_fail; + return 0; +} + +#else /* USE_FAST_MATH */ + +/* ------------------------------------------------------------------------ */ +/* 1. negative-exponent chain: err from the exponentiation engine (2927/3022/ */ +/* 3120 cond 0) */ +/* ------------------------------------------------------------------------ */ +#ifndef POSITIVE_EXP_ONLY +static void wb_exptmod_engine_failure(void) +{ + fp_int g, x, p, y; + + XMEMSET(&g, 0, sizeof(g)); + XMEMSET(&x, 0, sizeof(x)); + XMEMSET(&p, 0, sizeof(p)); + XMEMSET(&y, 0, sizeof(y)); + + /* --- accepting half: engine succeeds (err == 0 TRUE) and the modulus is + * negative (cond1 TRUE) -> the fp_add(Y, P, Y) fixup runs. --- */ + fp_set(&g, 3); fp_set(&x, 3); fp_setneg(&x); + fp_set(&p, 7); fp_setneg(&p); + printf(" [wb] fp_exptmod G=3 X=-3 P=-7 -> %d (expect 0)\n", + fp_exptmod(&g, &x, &p, &y)); + + fp_set(&g, 3); fp_set(&x, 3); fp_setneg(&x); + fp_set(&p, 7); fp_setneg(&p); + printf(" [wb] fp_exptmod_ex G=3 X=-3 P=-7 -> %d (expect 0)\n", + fp_exptmod_ex(&g, &x, x.used, &p, &y)); + + fp_set(&g, 3); fp_set(&x, 3); fp_setneg(&x); + fp_set(&p, 7); fp_setneg(&p); + printf(" [wb] fp_exptmod_nct G=3 X=-3 P=-7 -> %d (expect 0)\n", + fp_exptmod_nct(&g, &x, &p, &y)); + + /* --- rejecting half: |P| even, so fp_invmod still succeeds through + * fp_invmod_slow (gcd(3,14) == 1) but fp_montgomery_setup rejects the + * modulus inside _fp_exptmod_ct/_nct -> err == FP_VAL, cond0 FALSE. --- */ + fp_set(&g, 3); fp_set(&x, 3); fp_setneg(&x); + fp_set(&p, 14); fp_setneg(&p); + printf(" [wb] fp_exptmod G=3 X=-3 P=-14 -> %d (expect FP_VAL)\n", + fp_exptmod(&g, &x, &p, &y)); + + fp_set(&g, 3); fp_set(&x, 3); fp_setneg(&x); + fp_set(&p, 14); fp_setneg(&p); + printf(" [wb] fp_exptmod_ex G=3 X=-3 P=-14 -> %d (expect FP_VAL)\n", + fp_exptmod_ex(&g, &x, x.used, &p, &y)); + + fp_set(&g, 3); fp_set(&x, 3); fp_setneg(&x); + fp_set(&p, 14); fp_setneg(&p); + printf(" [wb] fp_exptmod_nct G=3 X=-3 P=-14 -> %d (expect FP_VAL)\n", + fp_exptmod_nct(&g, &x, &p, &y)); + + WB_NOTE("exptmod negative-exponent chain: engine-failure half exercised"); +} +#endif /* !POSITIVE_EXP_ONLY */ + +/* ------------------------------------------------------------------------ */ +/* 2. fp_to_unsigned_bin_len trailing check (3988 cond 0) */ +/* ------------------------------------------------------------------------ */ +#if DIGIT_BIT == 64 || DIGIT_BIT == 32 || DIGIT_BIT == 16 +static void wb_to_unsigned_bin_len_tail(void) +{ + fp_int a; + unsigned char buf[32]; + const int dbytes = (int)(DIGIT_BIT / 8); + + XMEMSET(&a, 0, sizeof(a)); + XMEMSET(buf, 0, sizeof(buf)); + + /* cond0 FALSE: the copy loop consumes every digit and stops on + * `i < a->used`, so i == a->used, one past `a->used - 1`. */ + fp_set(&a, 0x1234); + printf(" [wb] to_unsigned_bin_len(0x1234, %d) -> %d (expect 0, cond0 F)\n", + dbytes, fp_to_unsigned_bin_len(&a, buf, dbytes)); + + /* cond0 TRUE, cond1 FALSE: the loop stops on x < 0 with i == a->used - 1 + * and the remaining high bits of the current digit are zero. */ + fp_set(&a, 0x1234); + printf(" [wb] to_unsigned_bin_len(0x1234, 2) -> %d (expect 0, cond0 T" + " cond1 F)\n", fp_to_unsigned_bin_len(&a, buf, 2)); + + /* cond0 TRUE, cond1 TRUE: same stop, but a significant digit is left + * unwritten -> FP_VAL. */ + fp_set(&a, 1); + if (fp_mul_2d(&a, (int)DIGIT_BIT, &a) == FP_OKAY) + printf(" [wb] to_unsigned_bin_len(2^DIGIT_BIT, %d) -> %d (expect" + " FP_VAL, cond0 T cond1 T)\n", dbytes, + fp_to_unsigned_bin_len(&a, buf, dbytes)); + else + wb_fail = 1; + + WB_NOTE("fp_to_unsigned_bin_len trailing-significance vectors exercised"); +} +#endif /* DIGIT_BIT in {64,32,16} */ + +/* ------------------------------------------------------------------------ */ +/* 3./4. fp_isprime_ex (5200 cond 0, 5188 cond 0) */ +/* ------------------------------------------------------------------------ */ +#if !defined(NO_DH) || !defined(NO_DSA) || !defined(NO_RSA) || \ + defined(WOLFSSL_KEY_GEN) + +/* First prime strictly above primes[FP_PRIME_SIZE-1] (0x0653 == 1619), so any + * power of it survives the whole trial-division loop untouched. */ +#define WB_TFM_BIG_PRIME 1621 +#define WB_TFM_BIG_PRIME2 1627 + +/* a = WB_TFM_BIG_PRIME ^ k, k the smallest exponent with used > FP_SIZE/2. + * Returns 0 on success. */ +static int wb_build_oversized_candidate(fp_int* a) +{ + int guard = 0; + + fp_set(a, WB_TFM_BIG_PRIME); + while (a->used <= (FP_SIZE / 2)) { + if (fp_mul_d(a, WB_TFM_BIG_PRIME, a) != FP_OKAY) + return -1; + if (++guard > (FP_SIZE * DIGIT_BIT)) /* cannot loop forever */ + return -1; + } + return 0; +} + +static void wb_isprime_ex_vectors(void) +{ + fp_int a; + int res = 0; + int rc; + int n; + + XMEMSET(&a, 0, sizeof(a)); + + /* accepting half of BOTH decisions: a prime above the table. Every + * fp_mod_d returns MP_OKAY with d != 0 (5188 F,F) and every Miller-Rabin + * round returns FP_OKAY with res == FP_YES (5200 F,F). */ + fp_set(&a, WB_TFM_BIG_PRIME); + rc = fp_isprime_ex(&a, 8, &res); + printf(" [wb] fp_isprime_ex(1621) -> %d res %d (expect 0 / FP_YES)\n", + rc, res); + + /* 5188 cond1 TRUE: divisible by a table prime -> d == 0. */ + fp_set(&a, WB_TFM_BIG_PRIME); + if (fp_mul_d(&a, 3, &a) == FP_OKAY) + (void)fp_isprime_ex(&a, 8, &res); + + /* 5200 cond1 TRUE: composite that still survives trial division, so a + * Miller-Rabin round reports FP_NO with err == FP_OKAY. */ + fp_set(&a, WB_TFM_BIG_PRIME); + if (fp_mul_d(&a, WB_TFM_BIG_PRIME2, &a) == FP_OKAY) { + rc = fp_isprime_ex(&a, 8, &res); + printf(" [wb] fp_isprime_ex(1621*1627) -> %d res %d (expect 0 /" + " FP_NO)\n", rc, res); + } + + /* 5200 cond0 TRUE: candidate wider than FP_SIZE/2 digits. fp_exptmod + * inside the first Miller-Rabin round rejects it at tfm.c:2857 and + * fp_prime_miller_rabin forwards FP_VAL. */ + if (wb_build_oversized_candidate(&a) == 0) { + printf(" [wb] fp_isprime_ex(1621^k, used %d > FP_SIZE/2 %d) -> %d" + " (expect FP_VAL)\n", a.used, (int)(FP_SIZE / 2), + fp_isprime_ex(&a, 8, &res)); + } + else + wb_fail = 1; + + /* 5188 cond0 TRUE: the only failing return fp_mod_d has on this call site + * is FP_MEM out of fp_div_d's WC_ALLOC_VAR_EX, which exists only under + * WOLFSSL_SMALL_STACK. One-shot sweep so exactly one allocation fails per + * call and the rest of the run stays healthy; inert (a plain successful + * run) in variants where fp_isprime_ex allocates nothing. */ +#ifndef MCDC_FA_UNAVAILABLE + mcdc_fa_install(); + for (n = 1; n <= 4; n++) { + fp_set(&a, WB_TFM_BIG_PRIME); + mcdc_fa_arm_only(n); + (void)fp_isprime_ex(&a, 8, &res); + mcdc_fa_disarm(); + } + /* indices past the 256-entry trial-division loop land on fp_isprime_ex's + * own scratch and on fp_prime_miller_rabin's XMALLOC, a second (small + * stack only) route to 5200 cond0. */ + for (n = 255; n <= 262; n++) { + fp_set(&a, WB_TFM_BIG_PRIME); + mcdc_fa_arm_only(n); + (void)fp_isprime_ex(&a, 8, &res); + mcdc_fa_disarm(); + } + mcdc_fa_restore(); +#else + (void)n; + WB_NOTE("allocation lever unavailable; 5188:0 not attempted"); +#endif + + WB_NOTE("fp_isprime_ex trial-division / Miller-Rabin vectors exercised"); +} + +/* ------------------------------------------------------------------------ */ +/* 5. mp_prime_is_prime_ex random-base rejection (5319 cond 0) */ +/* ------------------------------------------------------------------------ */ +#if !defined(WC_NO_RNG) && !defined(FREESCALE_LTC_TFM) +static void wb_prime_is_prime_ex_base(void) +{ + WC_RNG rng; + mp_int a; + int res = 0; + int rc; + + XMEMSET(&a, 0, sizeof(a)); + + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; 5319:0 skipped"); + wb_fail = 1; + return; + } + + /* 1621: 11 bits -> baseSz 2, bitSz 3 (base[0] &= 7), c = a - 2 = 1619. + * The scripted stream walks b = 1, 2, 2047 and then a run of valid + * bases, so both operands of the base filter get both halves here. */ + fp_set(&a, WB_TFM_BIG_PRIME); + mcdc_tfm_rng_arm(); + rc = mp_prime_is_prime_ex(&a, 2, &res, &rng); + printf(" [wb] mp_prime_is_prime_ex(1621, scripted bases) -> %d res %d" + " (expect 0 / FP_YES)\n", rc, res); + mcdc_tfm_rng_disarm(); + + wc_FreeRng(&rng); + WB_NOTE("mp_prime_is_prime_ex scripted-base filter exercised"); +} +#endif /* !WC_NO_RNG && !FREESCALE_LTC_TFM */ + +#endif /* prime helpers compiled */ + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("tfm.c fault white-box MC/DC supplement\n"); + + if (wolfCrypt_Init() != 0) { + printf(" wolfCrypt_Init failed; nothing measured\n"); + return 0; + } + +#ifndef POSITIVE_EXP_ONLY + wb_exptmod_engine_failure(); +#endif +#if DIGIT_BIT == 64 || DIGIT_BIT == 32 || DIGIT_BIT == 16 + wb_to_unsigned_bin_len_tail(); +#endif +#if !defined(NO_DH) || !defined(NO_DSA) || !defined(NO_RSA) || \ + defined(WOLFSSL_KEY_GEN) + wb_isprime_ex_vectors(); +#if !defined(WC_NO_RNG) && !defined(FREESCALE_LTC_TFM) + wb_prime_is_prime_ex_base(); +#endif +#endif + + (void)wolfCrypt_Cleanup(); + + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Setup failures surface as skips, not failures: a nonzero exit makes the + * suite discard this variant's coverage. */ + return 0; +} + +#endif /* USE_FAST_MATH */ diff --git a/tests/unit-mcdc/test_tfm_whitebox.c b/tests/unit-mcdc/test_tfm_whitebox.c index dd418c4db3..3401b339e5 100644 --- a/tests/unit-mcdc/test_tfm_whitebox.c +++ b/tests/unit-mcdc/test_tfm_whitebox.c @@ -12,9 +12,9 @@ * 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). + * suite 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 + * Build: compiled by the coverage runner'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 @@ -22,7 +22,7 @@ * * 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). + * exit makes the harness discard the variant and its coverage). */ #include @@ -697,9 +697,17 @@ static void wb_TfmExptModDecisionCoverage(void) * 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). */ + * call C: G=7, X=-3, P=-7 (invmod fails (gcd=7)) + * Pair (A,B) isolates the P->sign operand (err==0 held true). + * + * CORRECTION (step 7): call C does NOT isolate the err==0 operand. The + * decision sits inside `if (err == FP_OKAY) { ... }`, so an fp_invmod + * failure skips the whole block and never reaches the test - `err` there + * can only come from _fp_exptmod_ct/_nct. Call C is kept because it does + * cover the invmod-failure return path, but the err==0 operand's FALSE + * half is closed in test_tfm_fault_whitebox.c, by an even |P| that lets + * fp_invmod succeed (via fp_invmod_slow) and then makes + * fp_montgomery_setup reject the modulus inside the engine. */ fp_set(&g, 3); fp_set(&x, 3); fp_setneg(&x); @@ -795,7 +803,7 @@ static void wb_TfmExptModDecisionCoverage(void) /* ------------------------------------------------------------------------ * Public-entry ARGUMENT-GUARD residuals. * - * campaign/reports/bigint-tfm/GAPS.md lists several multi-operand OR guards at + * suite/reports/bigint-tfm/the uncovered-condition report lists several multi-operand OR guards at * the top of public entry points whose operands the ordinary tests only ever * present all-false (they always pass valid arguments), so no operand's * independence pair is shown. Each is closed here by calling the entry point @@ -909,6 +917,7 @@ static void wb_entry_arg_guards(void) int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); printf("tfm.c white-box MC/DC supplement\n"); #if !defined(USE_FAST_MATH) printf(" USE_FAST_MATH not defined; nothing to exercise\n"); @@ -935,7 +944,7 @@ int main(void) wb_entry_arg_guards(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures surface as skips, not failures: a nonzero exit makes the - * campaign discard this variant's coverage. */ + * suite discard this variant's coverage. */ return 0; #endif } diff --git a/tests/unit-mcdc/test_tls13_null_whitebox.c b/tests/unit-mcdc/test_tls13_null_whitebox.c new file mode 100644 index 0000000000..6ba3b52b3b --- /dev/null +++ b/tests/unit-mcdc/test_tls13_null_whitebox.c @@ -0,0 +1,738 @@ +/* test_tls13_null_whitebox.c + * + * White-box MC/DC supplement for the POINTER-PRESENCE GUARDS of src/tls13.c. + * + * Companion to tests/unit-mcdc/test_tls13_whitebox.c, kept as a separate TU so + * the two can be extended independently; the harness unions their coverage by + * source line:col exactly as it unions the variant builds. + * + * SCOPE. Every decision driven here is a NULL / presence check on a pointer. + * The suite's disposition rule for that family is: + * + * - if the operand cannot vary even for a DIRECT caller, because a + * constructor or a callee postcondition fixes it, it is an entry in + * the exclusion record and NOT a test (e.g. `ssl->ctx != NULL`: + * wolfSSL_new() is the only constructor and rejects a NULL CTX); + * - if the operand cannot vary only because every IN-LIBRARY caller has + * already established it, it is reachable from a white-box and belongs + * here. That is what this file supplies. + * + * llvm-cov derives MC/DC independence PER BINARY, so for each decision below + * every row of its independence pairs -- including the "all operands false" + * row that the API tests also produce -- is driven inside THIS program. + * Nothing here leans on tests/api to complete a pair. + * + * DETERMINISM. No handshake, no network, no wall clock, no entropy beyond what + * wolfSSL_new() itself consumes. Every vector is a direct call with + * hand-supplied arguments, so consecutive runs are byte-identical. + * + * main() always returns 0: the harness scores a nonzero exit as a failed + * white-box and discards its whole coverage, so setup problems print a skip. + */ + +/* Pull tls13.c in verbatim so its file-static helpers are in scope and + * instrumented in THIS binary. tls13.c includes settings.h, which picks up + * user_settings.h via -DWOLFSSL_USER_SETTINGS. */ +#include + +#include + +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +/* Every group below needs a live WOLFSSL built through the public API. A + * CLIENT method is used for the same reason as in test_tls13_whitebox.c: + * wolfSSL_new() on a server WOLFSSL_CTX with no certificate loaded returns + * NULL, and loading one would tie this TU to the runner's certs/ tree. The two + * groups that need server-side behaviour flip ssl->options.side for the + * duration of a single call and restore it. */ +#if !defined(NO_TLS) && defined(WOLFSSL_TLS13) && !defined(WOLFCRYPT_ONLY) && \ + !defined(NO_WOLFSSL_CLIENT) + #define WBN_HAVE_SSL_FIXTURE +#endif + +#ifdef WBN_HAVE_SSL_FIXTURE + +static WOLFSSL_CTX* wbn_ctx = NULL; +static WOLFSSL* wbn_ssl = NULL; + +static int wbn_fixture_setup(void) +{ + wbn_ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); + if (wbn_ctx == NULL) + return 0; + wbn_ssl = wolfSSL_new(wbn_ctx); + if (wbn_ssl == NULL) + return 0; + return 1; +} + +static void wbn_fixture_teardown(void) +{ + if (wbn_ssl != NULL) wolfSSL_free(wbn_ssl); + if (wbn_ctx != NULL) wolfSSL_CTX_free(wbn_ctx); + wbn_ssl = NULL; wbn_ctx = NULL; +} + + +/* ------------------------------------------------------------------------- * + * GROUP 1 -- FreeScv13Args() / FreeDcv13Args(), the `args` presence guards. + * + * FreeScv13Args: if (args && args->sigData) [tls13.c ~:10318] + * if (args != NULL && args->frag != NULL) [~:10328] + * FreeDcv13Args: if (args && args->sigData != NULL) [~:11678] + * + * Both are file-static cleanup helpers with a SINGLE in-library call site each + * (the tail of SendTls13CertificateVerify / DoTls13CertificateVerify), and + * without WOLFSSL_ASYNC_CRYPT -- which this module's option list deliberately + * excludes -- the argument there is `Scv13Args args[1]` / `Dcv13Args args[1]`, + * i.e. the address of a stack object. The `ssl->async->freeArgs = ...` + * registration that could supply a different pointer is inside + * #ifdef WOLFSSL_ASYNC_CRYPT and is not compiled. So from the library the + * leading operand is invariant true and the trailing one only ever takes the + * value the send path happened to leave behind. + * + * The pointer arguments are ordinary parameters, not object invariants: a + * direct caller may legitimately pass NULL (that is what the guard is for) and + * may present an args block with the buffer either allocated or not. Three + * calls give both halves of all three decisions' pairs: + * + * args == NULL -> (F,-) decision false + * args != NULL, buffers NULL -> (T,F) decision false + * args != NULL, buffers allocated -> (T,T) decision true + * + * The third vector hands the helper real XMALLOC'd blocks tagged with the same + * DYNAMIC_TYPE_* the send path uses, so the XFREE it performs is the correct + * one and the pointers are nulled by the helper itself -- no double free. + * ------------------------------------------------------------------------- */ +#if (!defined(NO_RSA) || defined(HAVE_ECC) || defined(HAVE_ED25519) || \ + defined(HAVE_ED448) || defined(HAVE_FALCON) || \ + defined(WOLFSSL_HAVE_MLDSA) || defined(WOLFSSL_HAVE_SLHDSA)) && \ + !defined(NO_CERTS) && \ + (!defined(NO_WOLFSSL_SERVER) || !defined(WOLFSSL_NO_CLIENT_AUTH)) + #define WBN_HAVE_SCV_ARGS +#endif +#if (!defined(NO_RSA) || defined(HAVE_ECC) || defined(HAVE_ED25519) || \ + defined(HAVE_ED448) || defined(HAVE_FALCON) || \ + defined(WOLFSSL_HAVE_MLDSA) || defined(WOLFSSL_HAVE_SLHDSA)) && \ + !defined(NO_CERTS) + #define WBN_HAVE_DCV_ARGS +#endif + +static void wbn_free_args_guards(void) +{ +#if defined(WBN_HAVE_SCV_ARGS) || defined(WBN_HAVE_DCV_ARGS) + if (wbn_ssl == NULL) { + WB_NOTE("no ssl fixture; Free*13Args guards skipped"); + return; + } +#endif + +#ifdef WBN_HAVE_SCV_ARGS + { + Scv13Args scv; + + /* (F,-) on both decisions. */ + FreeScv13Args(wbn_ssl, NULL); + + /* (T,F) on both decisions. */ + XMEMSET(&scv, 0, sizeof(scv)); + FreeScv13Args(wbn_ssl, &scv); + + /* (T,T) on both decisions. */ + XMEMSET(&scv, 0, sizeof(scv)); + scv.sigData = (byte*)XMALLOC(16, wbn_ssl->heap, + DYNAMIC_TYPE_SIGNATURE); + scv.frag = (byte*)XMALLOC(16, wbn_ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (scv.sigData == NULL || scv.frag == NULL) { + XFREE(scv.sigData, wbn_ssl->heap, DYNAMIC_TYPE_SIGNATURE); + XFREE(scv.frag, wbn_ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + WB_NOTE("FreeScv13Args: allocation failed; (T,T) row skipped"); + } + else { + FreeScv13Args(wbn_ssl, &scv); + } + } + WB_NOTE("FreeScv13Args: args/sigData and args/frag guards driven with " + "both halves of every pair"); +#else + WB_NOTE("FreeScv13Args not compiled in this variant; skipped"); +#endif + +#ifdef WBN_HAVE_DCV_ARGS + { + Dcv13Args dcv; + + /* (F,-) */ + FreeDcv13Args(wbn_ssl, NULL); + + /* (T,F) */ + XMEMSET(&dcv, 0, sizeof(dcv)); + FreeDcv13Args(wbn_ssl, &dcv); + + /* (T,T) */ + XMEMSET(&dcv, 0, sizeof(dcv)); + dcv.sigData = (byte*)XMALLOC(16, wbn_ssl->heap, + DYNAMIC_TYPE_SIGNATURE); + if (dcv.sigData == NULL) { + WB_NOTE("FreeDcv13Args: allocation failed; (T,T) row skipped"); + } + else { + FreeDcv13Args(wbn_ssl, &dcv); + } + } + WB_NOTE("FreeDcv13Args: args/sigData guard driven with both halves of " + "its pair"); +#else + WB_NOTE("FreeDcv13Args not compiled in this variant; skipped"); +#endif +} + + +/* ------------------------------------------------------------------------- * + * GROUP 2 -- DoTls13ServerHello()'s entry guard. + * + * if (ssl == NULL || ssl->arrays == NULL) [tls13.c ~:5366] + * + * Same shape, and the same argument, as the eleven key-schedule guards already + * driven from test_tls13_whitebox.c: DoTls13ServerHello is reached only from + * DoTls13HandShakeMsgType, which has dereferenced `ssl` many times over, and + * only while the handshake is in progress -- FreeArrays() runs after it. From + * tests/api the decision is permanently (F,F). + * + * ssl == NULL -> (T,-) decision true + * ssl != NULL, arrays NULL -> (F,T) decision true + * ssl != NULL, arrays set -> (F,F) decision false + * + * The third vector runs the handler's body for real; it is given helloSz = 1, + * which the very next statement rejects with BUFFER_ERROR ("Protocol version + * length check"), so no record state is touched. arrays is nulled and restored + * rather than freed, so teardown is unaffected. + * ------------------------------------------------------------------------- */ +static void wbn_do_server_hello_entry_guard(void) +{ + byte input[4]; + word32 idx; + byte extMsgType; + Arrays* saved; + + if (wbn_ssl == NULL) { + WB_NOTE("no ssl fixture; DoTls13ServerHello entry guard skipped"); + return; + } + + XMEMSET(input, 0, sizeof(input)); + + /* (T,-) */ + idx = 0; extMsgType = server_hello; + (void)DoTls13ServerHello(NULL, input, &idx, 1, &extMsgType); + + /* (F,T) */ + saved = wbn_ssl->arrays; + wbn_ssl->arrays = NULL; + idx = 0; extMsgType = server_hello; + (void)DoTls13ServerHello(wbn_ssl, input, &idx, 1, &extMsgType); + wbn_ssl->arrays = saved; + + /* (F,F) -- bails out at the helloSz < OPAQUE16_LEN check. */ + if (wbn_ssl->arrays != NULL) { + idx = 0; extMsgType = server_hello; + (void)DoTls13ServerHello(wbn_ssl, input, &idx, 1, &extMsgType); + WB_NOTE("DoTls13ServerHello: ssl/arrays entry guard driven with all " + "three vectors"); + } + else { + WB_NOTE("DoTls13ServerHello: fixture has no arrays; (F,F) row " + "skipped"); + } +} + + +/* ------------------------------------------------------------------------- * + * GROUP 3 -- EchHashHelloInner()'s argument guard. + * + * if (ssl == NULL || ech == NULL) [tls13.c ~:3843] + * + * File-static; every in-library caller reaches it with an ssl it has just + * dereferenced and an ech taken from a TLSX whose data pointer was NULL + * checked one line earlier, so the decision is permanently (F,F) from a + * handshake. + * + * The (F,F) vector runs the body: with a zeroed WOLFSSL_ECH whose + * innerClientHelloLen is 0, the client arm writes a 4-byte handshake header + * into the function's own falseHeader and hashes it, allocating ssl->hsHashesEch + * on the way (released by wolfSSL_free). No record layer, no key material. + * ------------------------------------------------------------------------- */ +#ifdef HAVE_ECH +static void wbn_ech_hash_hello_inner_guard(void) +{ + WOLFSSL_ECH ech; + byte inner[4]; + + if (wbn_ssl == NULL) { + WB_NOTE("no ssl fixture; EchHashHelloInner guard skipped"); + return; + } + + XMEMSET(&ech, 0, sizeof(ech)); + XMEMSET(inner, 0, sizeof(inner)); + ech.innerClientHello = inner; + ech.innerClientHelloLen = 0; + + /* (T,-) */ + (void)EchHashHelloInner(NULL, &ech); + /* (F,T) */ + (void)EchHashHelloInner(wbn_ssl, NULL); + /* (F,F) */ + (void)EchHashHelloInner(wbn_ssl, &ech); + + WB_NOTE("EchHashHelloInner: ssl/ech argument guard driven with all three " + "vectors"); +} +#else +static void wbn_ech_hash_hello_inner_guard(void) +{ WB_NOTE("HAVE_ECH off in this variant; EchHashHelloInner skipped"); } +#endif + + +/* ------------------------------------------------------------------------- * + * GROUP 4 -- TlsCheckCookie()'s cookie-secret guards. + * + * if ((primary.buffer == NULL || primary.length == 0) + * && (secondary.buffer == NULL || secondary.length == 0)) [~:7134] + * if (primary.buffer != NULL && primary.length > 0) [~:7155] + * if (ret == the cookie-mismatch code && secondary.buffer != NULL + * && secondary.length > 0) [~:7164] + * + * TlsCheckCookie is WOLFSSL_LOCAL and is called only from the HelloRetryRequest + * cookie path, which a server enters only once wolfSSL_send_hrr_cookie() has + * installed a primary secret -- so the "missing secret" arms and the + * secondary/verify-only rotation arms never both occur on one live WOLFSSL. + * The four (buffer, length) shapes are ordinary buffer state, not an object + * invariant, so a direct call can present each of them. + * + * Vectors (P = primary, S = secondary), all with a 64-byte cookie so the + * `cookieSz < specs.hash_size + macSz` length check passes on an + * un-negotiated WOLFSSL (specs.hash_size is still 0, macSz is the digest + * size): + * + * v1 P.buf NULL S.buf NULL :7134 (T,-,T,-) true + * v2 P.buf set, len 32 S.buf NULL :7134 (F,F,-,-) false + * v3 P.buf set, len 0 S.buf NULL :7134 (F,T,T,-) true + * v4 P.buf NULL S.buf set, len 32 :7134 (T,-,F,F) false + * v5 P.buf NULL S.buf set, len 0 :7134 (T,-,F,T) true + * v6 P.buf set, len 0 S.buf set, len 32 :7134 (F,T,F,F) false + * v7 P.buf set, len 32 S.buf set, len 0 :7134 (F,F,-,-) false + * + * which pairs :7134's four operands as (v1,v2), (v3,v2), (v1,v4), (v5,v4). + * The vectors that get past :7134 then pair :7155 as (v2 -> (T,T)) against + * (v4 -> (F,-)) and (v6 -> (T,F)), and :7164's length operand as + * (v4 -> (T,T,T)) against (v7 -> (T,T,F)); the MAC never matches a random + * cookie, so `ret` carries the cookie-mismatch code on entry to :7164 every + * time. + * + * The secret buffers are file-static arrays assigned into ssl->buffers and + * cleared again before return, so wolfSSL_free() never XFREEs them. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SEND_HRR_COOKIE) && !defined(NO_WOLFSSL_SERVER) +static byte wbn_secret_pri[32]; +static byte wbn_secret_sec[32]; + +static void wbn_cookie_set(byte* pri, word32 priLen, byte* sec, word32 secLen) +{ + wbn_ssl->buffers.tls13CookieSecret.buffer = pri; + wbn_ssl->buffers.tls13CookieSecret.length = priLen; +#ifdef WOLFSSL_DTLS13 + wbn_ssl->buffers.tls13CookieSecretSecondary.buffer = sec; + wbn_ssl->buffers.tls13CookieSecretSecondary.length = secLen; +#else + (void)sec; (void)secLen; +#endif +} + +static void wbn_tls_check_cookie_guards(void) +{ + byte cookie[64]; + word32 i; + + if (wbn_ssl == NULL) { + WB_NOTE("no ssl fixture; TlsCheckCookie guards skipped"); + return; + } + + /* Fixed contents: the MAC comparison must fail identically every run. */ + for (i = 0; i < sizeof(cookie); i++) + cookie[i] = (byte)i; + for (i = 0; i < sizeof(wbn_secret_pri); i++) + wbn_secret_pri[i] = (byte)(0xA0 + i); + for (i = 0; i < sizeof(wbn_secret_sec); i++) + wbn_secret_sec[i] = (byte)(0x50 + i); + + wbn_cookie_set(NULL, 0, NULL, 0); /* v1 */ + (void)TlsCheckCookie(wbn_ssl, cookie, (word16)sizeof(cookie)); + + wbn_cookie_set(wbn_secret_pri, sizeof(wbn_secret_pri), NULL, 0); /* v2 */ + (void)TlsCheckCookie(wbn_ssl, cookie, (word16)sizeof(cookie)); + + wbn_cookie_set(wbn_secret_pri, 0, NULL, 0); /* v3 */ + (void)TlsCheckCookie(wbn_ssl, cookie, (word16)sizeof(cookie)); + +#ifdef WOLFSSL_DTLS13 + wbn_cookie_set(NULL, 0, wbn_secret_sec, sizeof(wbn_secret_sec)); /* v4 */ + (void)TlsCheckCookie(wbn_ssl, cookie, (word16)sizeof(cookie)); + + wbn_cookie_set(NULL, 0, wbn_secret_sec, 0); /* v5 */ + (void)TlsCheckCookie(wbn_ssl, cookie, (word16)sizeof(cookie)); + + wbn_cookie_set(wbn_secret_pri, 0, + wbn_secret_sec, sizeof(wbn_secret_sec)); /* v6 */ + (void)TlsCheckCookie(wbn_ssl, cookie, (word16)sizeof(cookie)); + + wbn_cookie_set(wbn_secret_pri, sizeof(wbn_secret_pri), + wbn_secret_sec, 0); /* v7 */ + (void)TlsCheckCookie(wbn_ssl, cookie, (word16)sizeof(cookie)); +#endif + + /* Detach the static secrets again so wolfSSL_free() has nothing to free. */ + wbn_cookie_set(NULL, 0, NULL, 0); + + WB_NOTE("TlsCheckCookie: primary/secondary cookie-secret guards driven " + "with both halves of every pair"); +} +#else +static void wbn_tls_check_cookie_guards(void) +{ WB_NOTE("WOLFSSL_SEND_HRR_COOKIE off in this variant; skipped"); } +#endif + + +/* ------------------------------------------------------------------------- * + * GROUP 5 -- SanityCheckTls13MsgReceived()'s DTLS 1.3 Connection ID arms. + * + * if (cidInfo == NULL || !cidInfo->negotiated) [tls13.c ~:14610] + * if (cidInfo->rx == NULL || cidInfo->rx->length == 0) [~:14623] + * if (cidInfo->tx == NULL || cidInfo->tx->length == 0) [~:14633] + * + * SanityCheckTls13MsgReceived is file-static and is a pure predicate over + * ssl->options / ssl->msgsReceived / ssl->dtlsCidInfo -- it has no side effect + * outside msgsReceived, which the CID arms do not touch, so it can be called + * repeatedly on one fixture. A live DTLS 1.3 peer only ever reaches these arms + * with a fully negotiated CIDInfo carrying non-empty ids, because the + * negotiation that allocates cidInfo is the same one that fills rx/tx; the + * "negotiated but empty" states RFC 9147 Section 9 tells the receiver to reject + * cannot be produced by wolfSSL as the peer. + * + * Vectors, all with options.dtls = 1 and handShakeState = HANDSHAKE_DONE so the + * two guards ahead of the rx/tx checks pass: + * + * cidInfo NULL -> :14610 (T,-) true + * cidInfo, negotiated 0 -> :14610 (F,T) true + * cidInfo, negotiated 1 -> :14610 (F,F) false, falls through + * with rx/tx NULL -> :14623/:14633 (T,-) true + * with rx/tx length 0 -> :14623/:14633 (F,T) true + * with rx/tx length 1 -> :14623/:14633 (F,F) false + * + * ConnectionID has a flexible array member, so each id is carved out of a byte + * buffer sized for the header plus one id byte. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_DTLS13) && defined(WOLFSSL_DTLS_CID) +static void wbn_sanity_check_cid_guards(void) +{ + byte rxBuf[sizeof(ConnectionID) + 4]; + byte txBuf[sizeof(ConnectionID) + 4]; + ConnectionID* rx = (ConnectionID*)rxBuf; + ConnectionID* tx = (ConnectionID*)txBuf; + CIDInfo cid; + CIDInfo* savedCid; + byte savedState; + byte savedDtls; + + if (wbn_ssl == NULL) { + WB_NOTE("no ssl fixture; SanityCheckTls13MsgReceived CID arms " + "skipped"); + return; + } + + XMEMSET(rxBuf, 0, sizeof(rxBuf)); + XMEMSET(txBuf, 0, sizeof(txBuf)); + XMEMSET(&cid, 0, sizeof(cid)); + + savedCid = wbn_ssl->dtlsCidInfo; + savedDtls = (byte)wbn_ssl->options.dtls; + savedState = wbn_ssl->options.handShakeState; + + wbn_ssl->options.dtls = 1; + wbn_ssl->options.handShakeState = HANDSHAKE_DONE; + + /* :14610 (T,-) */ + wbn_ssl->dtlsCidInfo = NULL; + (void)SanityCheckTls13MsgReceived(wbn_ssl, request_connection_id); + + /* :14610 (F,T) */ + cid.negotiated = 0; + wbn_ssl->dtlsCidInfo = &cid; + (void)SanityCheckTls13MsgReceived(wbn_ssl, request_connection_id); + + /* From here on the decision at :14610 is (F,F) and the rx/tx arms run. */ + cid.negotiated = 1; + + /* :14623 (T,-) and :14633 (T,-) */ + cid.rx = NULL; + cid.tx = NULL; + (void)SanityCheckTls13MsgReceived(wbn_ssl, request_connection_id); + (void)SanityCheckTls13MsgReceived(wbn_ssl, new_connection_id); + + /* :14623 (F,T) and :14633 (F,T) */ + rx->length = 0; + tx->length = 0; + cid.rx = rx; + cid.tx = tx; + (void)SanityCheckTls13MsgReceived(wbn_ssl, request_connection_id); + (void)SanityCheckTls13MsgReceived(wbn_ssl, new_connection_id); + + /* :14623 (F,F) and :14633 (F,F) */ + rx->length = 1; + tx->length = 1; + (void)SanityCheckTls13MsgReceived(wbn_ssl, request_connection_id); + (void)SanityCheckTls13MsgReceived(wbn_ssl, new_connection_id); + + wbn_ssl->dtlsCidInfo = savedCid; + wbn_ssl->options.dtls = savedDtls; + wbn_ssl->options.handShakeState = savedState; + + WB_NOTE("SanityCheckTls13MsgReceived: CID negotiated/rx/tx guards driven " + "with both halves of every pair"); +} +#else +static void wbn_sanity_check_cid_guards(void) +{ WB_NOTE("DTLS 1.3 CID off in this variant; skipped"); } +#endif + + +/* ------------------------------------------------------------------------- * + * GROUP 6 -- SetupOcspResp()'s two presence chains. + * + * if (extension == NULL && side == WOLFSSL_CLIENT_END + * && options.handShakeDone + * && TLSX_Find(ssl->ctx->extensions, TLSX_STATUS_REQUEST) != NULL) + * [tls13.c ~:9769] + * if (SSL_CM(ssl) != NULL && SSL_CM(ssl)->ocsp_stapling != NULL + * && SSL_CM(ssl)->ocsp_stapling->statusCb != NULL) + * [~:9786] + * + * SetupOcspResp is file-static and is called from the Certificate send path. + * The first chain is the post-handshake-client-auth re-staple: it is true only + * for a CLIENT that has completed its handshake, no longer carries the + * status_request extension on ssl->extensions, and whose CTX still does. Every + * operand of it is ordinary object state that this file sets directly. + * + * SSL_CM(ssl) (operand 0 of :9786) is NOT driven here: ssl->ctx is invariant + * non-NULL for a live WOLFSSL and ctx->cm is allocated by wolfSSL_CTX_new(), + * so that operand has no false row for any caller, direct or not. It is an + * exclusions.json entry, not a test. + * + * Call sequence -- the order matters, because each call can change the state + * the next one reads: + * + * A no ssl ext, side SERVER :9769 (T,F,-,-) false + * B no ssl ext, side CLIENT, !handShakeDone :9769 (T,T,F,-) false + * C no ssl ext, side CLIENT, handShakeDone, + * CTX has no status_request :9769 (T,T,T,F) false + * -- enable status_request on the CTX -- + * D same, CTX now has it :9769 (T,T,T,T) true + * ... which creates the ssl extension, so D also reaches + * :9786 with stapling not yet enabled (T,F,-) false + * -- wolfSSL_CTX_EnableOCSPStapling(): cm->ocsp_stapling allocated -- + * E ssl ext now present :9769 (F,-,-,-) false + * :9786 (T,T,F) false + * -- statusCb installed -- + * F ssl ext present :9769 (F,-,-,-) false + * :9786 (T,T,T) true + * + * pairing :9769's four operands against D and :9786's operands 1 and 2 + * against F. Calls A..C and E return before touching the certificate, D and E + * stop at the "Certificate buffer not set!" check (this fixture has no + * certificate), and F returns through the status callback, which answers + * WOLFSSL_OCSP_STATUS_CB_NOACK -- so nothing here parses a certificate or + * performs an OCSP lookup. + * ------------------------------------------------------------------------- */ +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) && \ + defined(WOLFSSL_POST_HANDSHAKE_AUTH) +static int wbn_status_cb(WOLFSSL* ssl, void* arg) +{ + (void)ssl; (void)arg; + /* NOACK: TLSX_CSR_SetResponseWithStatusCB() maps it to 0 without touching + * ssl->ocspCsrResp, so no response buffer is required. */ + return WOLFSSL_OCSP_STATUS_CB_NOACK; +} + +static void wbn_setup_ocsp_resp_guards(void) +{ + byte savedSide; + byte savedDone; + + if (wbn_ssl == NULL || wbn_ctx == NULL) { + WB_NOTE("no ssl fixture; SetupOcspResp guards skipped"); + return; + } + if (TLSX_Find(wbn_ssl->extensions, TLSX_STATUS_REQUEST) != NULL) { + WB_NOTE("SetupOcspResp: fixture already carries status_request; " + "skipped"); + return; + } + + savedSide = wbn_ssl->options.side; + savedDone = wbn_ssl->options.handShakeDone; + + /* A */ + wbn_ssl->options.side = WOLFSSL_SERVER_END; + wbn_ssl->options.handShakeDone = 0; + (void)SetupOcspResp(wbn_ssl); + + /* B */ + wbn_ssl->options.side = WOLFSSL_CLIENT_END; + (void)SetupOcspResp(wbn_ssl); + + /* C */ + wbn_ssl->options.handShakeDone = 1; + (void)SetupOcspResp(wbn_ssl); + + /* D -- CTX now carries the request extension. */ + if (wolfSSL_CTX_UseOCSPStapling(wbn_ctx, WOLFSSL_CSR_OCSP, 0) + == WOLFSSL_SUCCESS) { + (void)SetupOcspResp(wbn_ssl); + } + else { + WB_NOTE("SetupOcspResp: CTX status_request unavailable; the all-true " + "row of the re-staple chain was not driven"); + } + + /* E -- cm->ocsp_stapling allocated, no callback yet. */ + if (wolfSSL_CTX_EnableOCSPStapling(wbn_ctx) == WOLFSSL_SUCCESS && + SSL_CM(wbn_ssl) != NULL && + SSL_CM(wbn_ssl)->ocsp_stapling != NULL) { + (void)SetupOcspResp(wbn_ssl); + + /* F -- the same assignment wolfSSL_CTX_set_tlsext_status_cb() makes; + * done directly so this group does not depend on the compat-layer + * entry point being compiled. */ + SSL_CM(wbn_ssl)->ocsp_stapling->statusCb = wbn_status_cb; + SSL_CM(wbn_ssl)->ocsp_stapling->statusCbArg = NULL; + (void)SetupOcspResp(wbn_ssl); + SSL_CM(wbn_ssl)->ocsp_stapling->statusCb = NULL; + } + else { + WB_NOTE("SetupOcspResp: OCSP stapling unavailable; the ocsp_stapling " + "and statusCb rows were not driven"); + } + + wbn_ssl->options.side = savedSide; + wbn_ssl->options.handShakeDone = savedDone; + + WB_NOTE("SetupOcspResp: post-handshake re-staple chain and the " + "ocsp_stapling/statusCb chain driven"); +} +#else +static void wbn_setup_ocsp_resp_guards(void) +{ WB_NOTE("SetupOcspResp not compiled in this variant; skipped"); } +#endif + + +/* ------------------------------------------------------------------------- * + * GROUP 7 -- BuildTls13Message()'s non-sizeOnly argument guard. + * + * if (sizeOnly) { if (output || input) ... } [tls13.c ~:3339] + * else if (output == NULL || input == NULL) [~:3345] + * + * BuildTls13Message is WOLFSSL_LOCAL with seventeen in-library call sites, and + * every one of them that passes sizeOnly = 0 hands it a record buffer it has + * just reserved and a payload pointer into that same buffer, so from the whole + * library the decision is permanently (F,F) -- the `sizeOnly` sibling guard one + * line above (:3339) is the one that fires when a caller gets it wrong, and it + * is driven by the size probe in wolfssl_local_GetRecordSize(). + * + * output NULL -> (T,-) decision true + * output set, input NULL -> (F,T) decision true + * output set, input set -> (F,F) decision false + * + * The (F,F) vector is given outSz = 0, so control reaches the very next size + * check -- "Oops, want to write past output buffer size" -- and returns + * BUFFER_E before any record header is written or any AEAD state is touched. + * The fixture has no keys and none are needed. + * ------------------------------------------------------------------------- */ +static void wbn_build_message_arg_guard(void) +{ + byte out[8]; + byte in[8]; + + if (wbn_ssl == NULL) { + WB_NOTE("no ssl fixture; BuildTls13Message argument guard skipped"); + return; + } + + XMEMSET(out, 0, sizeof(out)); + XMEMSET(in, 0, sizeof(in)); + + /* :3345, the sizeOnly == 0 arm. */ + /* (T,-) */ + (void)BuildTls13Message(wbn_ssl, NULL, 0, in, 0, application_data, 0, 0, 0); + /* (F,T) */ + (void)BuildTls13Message(wbn_ssl, out, 0, NULL, 0, application_data, 0, 0, 0); + /* (F,F) -- stops at the `args->sz > outSz` check with BUFFER_E. */ + (void)BuildTls13Message(wbn_ssl, out, 0, in, 0, application_data, 0, 0, 0); + + /* :3339, the sizeOnly == 1 arm -- `if (output || input)`. Its (F,F) row is + * the ordinary size probe, which returns args->sz without writing + * anything; the two true rows are the mistaken-caller shapes the guard + * exists to catch. Driven here as well so this decision does not depend on + * a size-probe caller existing in some other test file. */ + /* (T,-) */ + (void)BuildTls13Message(wbn_ssl, out, 0, NULL, 0, application_data, 0, 1, 0); + /* (F,T) */ + (void)BuildTls13Message(wbn_ssl, NULL, 0, in, 0, application_data, 0, 1, 0); + /* (F,F) */ + (void)BuildTls13Message(wbn_ssl, NULL, 0, NULL, 0, application_data, 0, 1, 0); + + /* The probe wrote ssl->options.buildMsgState; put it back so nothing + * downstream inherits a half-built record state. */ + wbn_ssl->options.buildMsgState = BUILD_MSG_BEGIN; + + WB_NOTE("BuildTls13Message: both output/input argument guards driven with " + "all three vectors each"); +} + + +#endif /* WBN_HAVE_SSL_FIXTURE */ + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("tls13.c white-box MC/DC supplement -- pointer-presence guards\n"); + +#ifdef WBN_HAVE_SSL_FIXTURE + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + WB_NOTE("wolfSSL_Init failed; all groups skipped"); + } + else if (!wbn_fixture_setup()) { + WB_NOTE("could not build the WOLFSSL fixture; all groups skipped"); + } + else { + wbn_free_args_guards(); + wbn_do_server_hello_entry_guard(); + wbn_ech_hash_hello_inner_guard(); + wbn_tls_check_cookie_guards(); + wbn_sanity_check_cid_guards(); + wbn_setup_ocsp_resp_guards(); + wbn_build_message_arg_guard(); + } + + wbn_fixture_teardown(); + wolfSSL_Cleanup(); +#else + WB_NOTE("no TLS 1.3 client on this build axis; nothing to drive"); +#endif + + printf("done\n"); + /* Always 0: a nonzero exit is scored as a failed white-box and its + * coverage is discarded. */ + return 0; +} diff --git a/tests/unit-mcdc/test_tls13_whitebox.c b/tests/unit-mcdc/test_tls13_whitebox.c new file mode 100644 index 0000000000..00ba2bdd85 --- /dev/null +++ b/tests/unit-mcdc/test_tls13_whitebox.c @@ -0,0 +1,770 @@ +/* test_tls13_whitebox.c + * + * White-box MC/DC supplement for src/tls13.c. + * + * This is the FIRST white-box driver in the harness that targets a src/ file + * rather than wolfcrypt/src/*.c. The build contract is identical (see + * tests/unit-mcdc/README.md): this TU #includes the target .c verbatim, is + * compiled with the exact flags the instrumented library used for it, and is + * linked against that variant's libwolfssl.a with the target's own object + * removed, so this TU supplies the single (instrumented) definition. + * + * WHY A WHITE-BOX IS NEEDED HERE, given that src/ has almost no mutable file + * scope variables and all state hangs off WOLFSSL / WOLFSSL_CTX: + * tls13.c has ~80 `static` functions. What this TU buys is not access to + * hidden state but the ability to call those helpers with ARGUMENT + * COMBINATIONS NO PUBLIC CALLER PRODUCES -- the defensive guards that every + * in-library caller has already excluded before the callee runs. Those guards + * are real conditions in the coverage map and are unreachable from tests/api + * without editing library source. + * + * Coverage from this binary is unioned with the tests/api variant coverage by + * source line:col by the aggregate.sh, which ORs the "independence + * shown" bit across binaries. llvm-cov derives independence PER BINARY, so + * every MC/DC pair below is completed WITHIN THIS FILE; nothing here leans on + * the API tests to supply the other half of a pair. + * + * main() always returns 0: the harness treats a nonzero exit as a failed + * white-box and discards its coverage, so setup problems are printed as skips. + */ + +/* Pull tls13.c in verbatim so its file-static helpers are in scope and + * instrumented in THIS binary. tls13.c includes settings.h, which picks up + * user_settings.h via -DWOLFSSL_USER_SETTINGS. */ +#include + +#include + +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +/* The guard stack that encloses DecodeTls13SigAlg() in tls13.c: + * #if !defined(NO_TLS) && defined(WOLFSSL_TLS13) + * #ifndef WOLFCRYPT_ONLY + * #ifndef NO_CERTS + * #if !defined(NO_RSA) || defined(HAVE_ECC) || ... + * Reproduced verbatim so this file still compiles (as a no-op) on any build + * axis that does not compile the helper. */ +#if !defined(NO_TLS) && defined(WOLFSSL_TLS13) && !defined(WOLFCRYPT_ONLY) && \ + !defined(NO_CERTS) && \ + (!defined(NO_RSA) || defined(HAVE_ECC) || defined(HAVE_ED25519) || \ + defined(HAVE_ED448) || defined(HAVE_FALCON) || \ + defined(WOLFSSL_HAVE_MLDSA) || defined(WOLFSSL_HAVE_SLHDSA)) + #define WB_HAVE_DECODE_SIGALG +#endif + +/* ------------------------------------------------------------------------- * + * DecodeTls13SigAlg(): the two RSA-PSS minor-byte RANGE checks. + * + * if (input[1] >= RSA_PSS_RSAE_SHA256_MINOR && + * input[1] <= RSA_PSS_RSAE_SHA512_MINOR) [0x04 .. 0x06] + * else if (input[1] >= RSA_PSS_PSS_SHA256_MINOR && + * input[1] <= RSA_PSS_PSS_SHA512_MINOR) [0x09 .. 0x0B] + * + * DecodeTls13SigAlg is file-static and every in-library caller feeds it a + * signature algorithm that already passed the peer's advertised sig_algs + * negotiation, so the "major byte is 0x08 but the minor byte sits just outside + * a PSS range" combinations -- exactly the (T,F) halves of these two pairs -- + * never arrive from a handshake. Called directly here with all three vectors + * per decision: + * {0x08,0x05} -> (T,T) decision true + * {0x08,0x03} -> (F,-) decision false (short-circuits) pair for operand 0 + * {0x08,0x07} -> (T,F) decision false pair for operand 1 + * and the same shape one range up for the PSS-PSS check, which is only + * reached when the RSAE check is false. + * + * Pure function of a 2-byte buffer and two out-bytes: no WOLFSSL object, no + * allocation, no entropy. + * ------------------------------------------------------------------------- */ +#ifdef WB_HAVE_DECODE_SIGALG +static void wb_decode_tls13_sigalg(void) +{ + static const byte vec[6][2] = { + { NEW_SA_MAJOR, 0x05 }, /* RSAE range: T,T */ + { NEW_SA_MAJOR, 0x03 }, /* RSAE range: F,- ; PSS range: F,- */ + { NEW_SA_MAJOR, 0x07 }, /* RSAE range: T,F */ + { NEW_SA_MAJOR, 0x0A }, /* RSAE F,- then PSS range: T,T */ + { NEW_SA_MAJOR, 0x0C }, /* RSAE F,- then PSS range: T,F */ + { NEW_SA_MAJOR, 0x09 } /* PSS range lower edge: T,T */ + }; + byte input[2]; + byte hashAlgo; + byte hsType; + size_t i; + + for (i = 0; i < sizeof(vec) / sizeof(vec[0]); i++) { + input[0] = vec[i][0]; + input[1] = vec[i][1]; + hashAlgo = 0; + hsType = 0; + (void)DecodeTls13SigAlg(input, &hashAlgo, &hsType); + } + + WB_NOTE("DecodeTls13SigAlg: both PSS minor-byte range decisions driven " + "with both halves of each independence pair"); +} +#else +static void wb_decode_tls13_sigalg(void) +{ WB_NOTE("DecodeTls13SigAlg not compiled in this variant; skipped"); } +#endif + + +/* ------------------------------------------------------------------------- * + * Shared fixture: one WOLFSSL built through the public API. Nothing here + * performs a handshake -- the object exists only so the static helpers below + * can be called with a STRUCTURALLY VALID ssl (the "all operands false" half + * of each guard) as well as with the degenerate arguments no in-library + * caller ever produces. No entropy is consumed beyond what wolfSSL_new() + * itself does, no certificate or key file is read, and no wall-clock or + * network behaviour is involved, so the binary is deterministic. + * + * A CLIENT method is used because wolfSSL_new() on a server WOLFSSL_CTX with + * no certificate loaded fails, and depending on the on-disk certs/ tree would + * make this TU sensitive to the runner's working directory. The one place + * below that needs server-side behaviour flips ssl->options.side for the + * duration of a single call and restores it -- see wb_create_cookie_ext_guards. + * ------------------------------------------------------------------------- */ +#if !defined(NO_TLS) && defined(WOLFSSL_TLS13) && !defined(WOLFCRYPT_ONLY) && \ + !defined(NO_WOLFSSL_CLIENT) + #define WB_HAVE_SSL_FIXTURE +#endif + +#ifdef WB_HAVE_SSL_FIXTURE +static WOLFSSL_CTX* wb_ctx_c = NULL; +static WOLFSSL* wb_ssl_c = NULL; + +/* The ssl argument the WB_ARRAYS_GUARD macro varies between vectors. Kept in a + * file-scope variable so one macro can drive helpers with different + * signatures without re-stating each call three times. */ +static WOLFSSL* wb_s = NULL; + +static int wb_fixture_setup(void) +{ + wb_ctx_c = wolfSSL_CTX_new(wolfTLSv1_3_client_method()); + if (wb_ctx_c == NULL) + return 0; + wb_ssl_c = wolfSSL_new(wb_ctx_c); + if (wb_ssl_c == NULL) + return 0; + /* Every guard below reads ssl->arrays; a WOLFSSL that never got one is + * not a usable fixture. */ + if (wb_ssl_c->arrays == NULL) + return 0; + return 1; +} + +static void wb_fixture_teardown(void) +{ + if (wb_ssl_c != NULL) wolfSSL_free(wb_ssl_c); + if (wb_ctx_c != NULL) wolfSSL_CTX_free(wb_ctx_c); + wb_ssl_c = NULL; wb_ctx_c = NULL; +} + +/* Drive `if (ssl == NULL || ssl->arrays == NULL)` with all three vectors that + * unique-cause MC/DC needs, in THIS binary: + * + * ssl == NULL -> (T,-) decision true | pair for operand 0 + * ssl != NULL, arrays NULL -> (F,T) decision true | pair for operand 1 + * ssl != NULL, arrays set -> (F,F) decision false | shared partner + * + * EXPR must reference wb_s where the helper takes its WOLFSSL*. The third + * vector runs the helper's body for real; each helper below was chosen so that + * body is side-effect free on an un-negotiated WOLFSSL (specs.mac_algorithm is + * still 0, so the key schedule bails out with HASH_TYPE_E / BAD_FUNC_ARG + * before touching hsHashes or the record layer). Return values are + * deliberately ignored: the guard, not the outcome, is under test. + * + * arrays is nulled and restored rather than freed, so teardown is unaffected. + */ +#define WB_ARRAYS_GUARD(ssl, EXPR) \ + do { \ + Arrays* wb_saved = (ssl)->arrays; \ + wb_s = NULL; \ + (void)(EXPR); \ + wb_s = (ssl); \ + (ssl)->arrays = NULL; \ + (void)(EXPR); \ + (ssl)->arrays = wb_saved; \ + (void)(EXPR); \ + } while (0) + +/* ------------------------------------------------------------------------- * + * The TLS 1.3 key-schedule entry points and their `ssl == NULL || + * ssl->arrays == NULL` guards. + * + * DeriveBinderKey, DeriveBinderKeyResume, DeriveEarlyTrafficSecret, + * DeriveClient/ServerHandshakeSecret, DeriveClient/ServerTrafficSecret and + * DeriveExporterSecret are file-static; DeriveEarlySecret, + * DeriveHandshakeSecret and DeriveMasterSecret are WOLFSSL_LOCAL. Either way + * every in-library call site sits inside the handshake state machine, which + * cannot be entered at all without a WOLFSSL that already has its arrays -- + * FreeArrays() only runs once the handshake is complete and no key-schedule + * call follows it. So from tests/api the decision is *always* (F,F): both + * operands are stuck false and neither independence pair can ever be shown. + * Reaching (T,-) and (F,T) is precisely what compiling tls13.c into the test + * binary buys. + * ------------------------------------------------------------------------- */ +static void wb_key_schedule_null_guards(void) +{ + byte key[WC_MAX_DIGEST_SIZE]; + + if (wb_ssl_c == NULL) { + WB_NOTE("no ssl fixture; key-schedule guards skipped"); + return; + } + + XMEMSET(key, 0, sizeof(key)); + +#ifndef NO_PSK + WB_ARRAYS_GUARD(wb_ssl_c, DeriveBinderKey(wb_s, key)); +#endif +#if defined(HAVE_SESSION_TICKET) && \ + (!defined(NO_WOLFSSL_CLIENT) || !defined(NO_WOLFSSL_SERVER)) + WB_ARRAYS_GUARD(wb_ssl_c, DeriveBinderKeyResume(wb_s, key)); +#endif +#ifdef WOLFSSL_EARLY_DATA + WB_ARRAYS_GUARD(wb_ssl_c, + DeriveEarlyTrafficSecret(wb_s, key, WOLFSSL_CLIENT_END)); +#endif + WB_ARRAYS_GUARD(wb_ssl_c, DeriveClientHandshakeSecret(wb_s, key)); + WB_ARRAYS_GUARD(wb_ssl_c, DeriveServerHandshakeSecret(wb_s, key)); + WB_ARRAYS_GUARD(wb_ssl_c, DeriveClientTrafficSecret(wb_s, key)); + WB_ARRAYS_GUARD(wb_ssl_c, DeriveServerTrafficSecret(wb_s, key)); +#ifdef HAVE_KEYING_MATERIAL + WB_ARRAYS_GUARD(wb_ssl_c, DeriveExporterSecret(wb_s, key)); +#endif + WB_ARRAYS_GUARD(wb_ssl_c, DeriveEarlySecret(wb_s)); + WB_ARRAYS_GUARD(wb_ssl_c, DeriveHandshakeSecret(wb_s)); + WB_ARRAYS_GUARD(wb_ssl_c, DeriveMasterSecret(wb_s)); + + WB_NOTE("key-schedule ssl/arrays guards driven with all three vectors"); +} + +/* ------------------------------------------------------------------------- * + * BuildTls13HandshakeHmac(): `if (ssl == NULL || key == NULL || hash == NULL)` + * + * Three operands, so unique-cause MC/DC needs four vectors. The only two + * callers (SendTls13Finished / DoTls13Finished) pass ssl plus two automatic + * buffers, so all three operands are permanently false from tests/api. + * + * (T,-,-) ssl NULL + * (F,T,-) key NULL + * (F,F,T) hash NULL + * (F,F,F) all supplied -> body runs; specs.mac_algorithm is 0 on an + * un-negotiated WOLFSSL, so the switch takes `default:` and returns + * BAD_FUNC_ARG before dereferencing ssl->hsHashes. + * ------------------------------------------------------------------------- */ +static void wb_build_handshake_hmac_guard(void) +{ + byte key[WC_MAX_DIGEST_SIZE]; + byte hash[WC_MAX_DIGEST_SIZE]; + word32 hashSz = 0; + + if (wb_ssl_c == NULL) { + WB_NOTE("no ssl fixture; BuildTls13HandshakeHmac guard skipped"); + return; + } + + XMEMSET(key, 0, sizeof(key)); + XMEMSET(hash, 0, sizeof(hash)); + + (void)BuildTls13HandshakeHmac(NULL, key, hash, &hashSz); + (void)BuildTls13HandshakeHmac(wb_ssl_c, NULL, hash, &hashSz); + (void)BuildTls13HandshakeHmac(wb_ssl_c, key, NULL, &hashSz); + (void)BuildTls13HandshakeHmac(wb_ssl_c, key, hash, &hashSz); + + WB_NOTE("BuildTls13HandshakeHmac argument guard driven with all four " + "vectors"); +} + +/* ------------------------------------------------------------------------- * + * CreateCookieExt(): the two argument/state guards. + * + * if (hash == NULL || hashSz == 0) -> BAD_FUNC_ARG + * if (cookieSecret.buffer == NULL || cookieSecret.length == 0) + * -> COOKIE_ERROR + * + * Both callers (SendTls13ServerHello's HRR path and DoTls13ClientHello) reach + * CreateCookieExt only after the server has a cookie secret and a computed + * transcript hash, so from tests/api every operand of both decisions is stuck + * false. Driven here directly: + * + * hash guard: (NULL, 32) / (hash, 0) / (hash, 32) + * secret guard: no secret -> (T,-); secret with length forced to 0 -> (F,T); + * secret as installed -> (F,F), which runs the HMAC and the + * TLSX_Cookie_Use() that follows. + * + * The length field is forced to 0 and restored rather than freeing the + * buffer, so wolfSSL_free() still releases it exactly once. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_SEND_HRR_COOKIE) && !defined(NO_WOLFSSL_SERVER) +static void wb_create_cookie_ext_guards(void) +{ + static const byte secret[32] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f, + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17, + 0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f + }; + byte hash[WC_SHA256_DIGEST_SIZE]; + TLSX* exts = NULL; + word32 savedLen; + int savedSide; + int ret; + + if (wb_ssl_c == NULL) { + WB_NOTE("no ssl fixture; CreateCookieExt guards skipped"); + return; + } + + XMEMSET(hash, 0x5a, sizeof(hash)); + + /* hash guard, all three vectors. The third falls through to the cookie + * secret guard, which on a fresh server is (T,-): no secret installed. */ + (void)CreateCookieExt(wb_ssl_c, NULL, (word16)sizeof(hash), &exts, + TLS13_BYTE, TLS_AES_128_GCM_SHA256); + (void)CreateCookieExt(wb_ssl_c, hash, 0, &exts, + TLS13_BYTE, TLS_AES_128_GCM_SHA256); + (void)CreateCookieExt(wb_ssl_c, hash, (word16)sizeof(hash), &exts, + TLS13_BYTE, TLS_AES_128_GCM_SHA256); + + /* wolfSSL_send_hrr_cookie() is server-only; the fixture is a client + * because a server WOLFSSL_CTX with no certificate cannot be instantiated. + * The side is flipped for exactly this call and restored immediately, so + * wolfSSL_free() still tears the object down along the client path. */ + savedSide = wb_ssl_c->options.side; + wb_ssl_c->options.side = WOLFSSL_SERVER_END; + ret = wolfSSL_send_hrr_cookie(wb_ssl_c, secret, (unsigned int)sizeof(secret)); + wb_ssl_c->options.side = savedSide; + + if (ret != WOLFSSL_SUCCESS || + wb_ssl_c->buffers.tls13CookieSecret.buffer == NULL) { + WB_NOTE("could not install a cookie secret; secret guard partial"); + TLSX_FreeAll(exts, wb_ssl_c->heap); + return; + } + + /* buffer non-NULL, length 0 -> (F,T) */ + savedLen = wb_ssl_c->buffers.tls13CookieSecret.length; + wb_ssl_c->buffers.tls13CookieSecret.length = 0; + (void)CreateCookieExt(wb_ssl_c, hash, (word16)sizeof(hash), &exts, + TLS13_BYTE, TLS_AES_128_GCM_SHA256); + wb_ssl_c->buffers.tls13CookieSecret.length = savedLen; + + /* buffer non-NULL, length non-zero -> (F,F); runs the HMAC + cookie use */ + (void)CreateCookieExt(wb_ssl_c, hash, (word16)sizeof(hash), &exts, + TLS13_BYTE, TLS_AES_128_GCM_SHA256); + + TLSX_FreeAll(exts, wb_ssl_c->heap); + + WB_NOTE("CreateCookieExt hash and cookie-secret guards driven with both " + "halves of each independence pair"); +} +#else +static void wb_create_cookie_ext_guards(void) +{ WB_NOTE("CreateCookieExt not compiled in this variant; skipped"); } +#endif + +#else /* !WB_HAVE_SSL_FIXTURE */ +static void wb_key_schedule_null_guards(void) +{ WB_NOTE("no WOLFSSL fixture on this build axis; skipped"); } +static void wb_build_handshake_hmac_guard(void) +{ WB_NOTE("no WOLFSSL fixture on this build axis; skipped"); } +static void wb_create_cookie_ext_guards(void) +{ WB_NOTE("no WOLFSSL fixture on this build axis; skipped"); } +#endif /* WB_HAVE_SSL_FIXTURE */ + + +/* ------------------------------------------------------------------------- * + * SanityCheckTls13MsgReceived(): the handshake-message ordering matrix. + * + * This file-static predicate is the densest single cluster of open conditions + * in tls13.c. It is a PURE function of ssl->options, ssl->msgsReceived, + * ssl->earlyData and ssl->certReqCtx -- it allocates nothing, reads no buffer + * and performs no crypto -- yet from tests/api most of its operand + * combinations are unreachable, because reaching a given message type at all + * means the state machine has already forced the very fields the decision + * tests. A handshake that is far enough along to deliver a Finished, for + * instance, cannot simultaneously have mutualAuth set and havePeerCert clear. + * + * Calling it directly with a synthesised WOLFSSL state is the only way to + * complete these pairs. Each vector below restores the fixture to a known base + * first (wb_sc_reset), so the vectors are order-independent and the fixture is + * handed back to wolfSSL_free() in its initial shape. + * ------------------------------------------------------------------------- */ +#ifdef WB_HAVE_SSL_FIXTURE +static void wb_sc_reset(void) +{ + WOLFSSL* ssl = wb_ssl_c; + + XMEMSET(&ssl->msgsReceived, 0, sizeof(ssl->msgsReceived)); + ssl->options.side = WOLFSSL_CLIENT_END; + ssl->options.clientState = NULL_STATE; + ssl->options.serverState = NULL_STATE; + ssl->options.connectState = CONNECT_BEGIN; + ssl->options.pskNegotiated = 0; + ssl->options.postHandshakeAuth = 0; + ssl->options.verifyPeer = 0; + ssl->options.verifyPostHandshake = 0; + ssl->options.mutualAuth = 0; + ssl->options.havePeerCert = 0; + ssl->options.havePeerVerify = 0; + ssl->options.dtls = 0; + ssl->options.downgrade = 0; + ssl->options.minDowngrade = 0; + ssl->certReqCtx = NULL; +#ifdef WOLFSSL_EARLY_DATA + ssl->earlyData = no_early_data; +#endif +} + +#define WB_SC(type) (void)SanityCheckTls13MsgReceived(wb_ssl_c, (byte)(type)) + +static void wb_sanity_check_client_hello(void) +{ +#ifndef NO_WOLFSSL_SERVER + WOLFSSL* ssl = wb_ssl_c; + + /* 2nd ClientHello duplicate check: + * got_client_hello == 1 && serverState != SERVER_HELLO_RETRY_REQUEST_COMPLETE + * Reached only on the server with clientState < CLIENT_HELLO_COMPLETE. A + * server that has seen one ClientHello has, by construction, either + * answered it (clientState advances) or emitted an HRR, so the (T,F) and + * (F,-) partners never coexist with (T,T) in one live handshake. */ + wb_sc_reset(); ssl->options.side = WOLFSSL_SERVER_END; + ssl->msgsReceived.got_client_hello = 1; /* (T,T) -> dup */ + WB_SC(client_hello); + + wb_sc_reset(); ssl->options.side = WOLFSSL_SERVER_END; /* (F,-) -> accept */ + WB_SC(client_hello); + + wb_sc_reset(); ssl->options.side = WOLFSSL_SERVER_END; + ssl->msgsReceived.got_client_hello = 1; + ssl->options.serverState = SERVER_HELLO_RETRY_REQUEST_COMPLETE; /* (T,F) */ + WB_SC(client_hello); +#endif +} + +static void wb_sanity_check_certificate(void) +{ + WOLFSSL* ssl = wb_ssl_c; + + /* Client-side ordering guard: + * side == CLIENT && serverState != SERVER_ENCRYPTED_EXTENSIONS_COMPLETE */ +#ifndef NO_WOLFSSL_CLIENT + wb_sc_reset(); /* (T,T) -> ooo */ + WB_SC(certificate); + + wb_sc_reset(); + ssl->options.serverState = SERVER_ENCRYPTED_EXTENSIONS_COMPLETE; /* (T,F) */ + WB_SC(certificate); +#endif + +#ifndef NO_WOLFSSL_SERVER + /* Server-side ordering guard: + * side == SERVER && clientState != CLIENT_HELLO_COMPLETE && + * serverState < SERVER_FINISHED_COMPLETE + * The (F,-,-) partner is the client vector just above, which also supplies + * the (T,F) half of the client guard -- one call, two pairs. */ + wb_sc_reset(); ssl->options.side = WOLFSSL_SERVER_END; /* (T,T,T) -> ooo */ + WB_SC(certificate); + + wb_sc_reset(); ssl->options.side = WOLFSSL_SERVER_END; + ssl->options.clientState = CLIENT_HELLO_COMPLETE; /* (T,F,-) */ + WB_SC(certificate); + + wb_sc_reset(); ssl->options.side = WOLFSSL_SERVER_END; + ssl->options.serverState = SERVER_FINISHED_COMPLETE; /* (T,T,F) */ + WB_SC(certificate); + + wb_sc_reset(); + ssl->options.serverState = SERVER_ENCRYPTED_EXTENSIONS_COMPLETE; + WB_SC(certificate); /* (F,-,-) */ +#endif +} + +#ifndef NO_WOLFSSL_CLIENT +static void wb_sanity_check_certificate_request(void) +{ + WOLFSSL* ssl = wb_ssl_c; + + /* Ordering guard: + * serverState != SERVER_ENCRYPTED_EXTENSIONS_COMPLETE && + * (serverState < SERVER_FINISHED_COMPLETE || + * clientState != CLIENT_FINISHED_COMPLETE) */ + wb_sc_reset(); /* (T,T,-) -> ooo */ + WB_SC(certificate_request); + + wb_sc_reset(); + ssl->options.serverState = SERVER_ENCRYPTED_EXTENSIONS_COMPLETE; /* (F,-,-) */ + WB_SC(certificate_request); + + wb_sc_reset(); + ssl->options.serverState = SERVER_FINISHED_COMPLETE; /* (T,F,T) -> ooo */ + WB_SC(certificate_request); + + /* (T,F,F): the post-handshake window. Also the only way to reach the + * post_handshake_auth guard below with its first two operands true. */ + wb_sc_reset(); + ssl->options.serverState = SERVER_FINISHED_COMPLETE; + ssl->options.clientState = CLIENT_FINISHED_COMPLETE; + ssl->options.postHandshakeAuth = 1; /* pha guard (T,T,F) */ + WB_SC(certificate_request); + + wb_sc_reset(); + ssl->options.serverState = SERVER_FINISHED_COMPLETE; + ssl->options.clientState = CLIENT_FINISHED_COMPLETE; + ssl->options.postHandshakeAuth = 0; /* pha guard (T,T,T) */ + WB_SC(certificate_request); + + /* Duplicate guard: + * got_certificate_request && clientState != CLIENT_FINISHED_COMPLETE */ + wb_sc_reset(); + ssl->options.serverState = SERVER_ENCRYPTED_EXTENSIONS_COMPLETE; + ssl->msgsReceived.got_certificate_request = 1; /* (T,T) -> dup */ + WB_SC(certificate_request); + + wb_sc_reset(); + ssl->options.serverState = SERVER_ENCRYPTED_EXTENSIONS_COMPLETE; + ssl->msgsReceived.got_certificate_request = 1; + ssl->options.clientState = CLIENT_FINISHED_COMPLETE; /* (T,F) */ + WB_SC(certificate_request); + /* (F,-) is any of the accepting vectors above. */ +} +#else +static void wb_sanity_check_certificate_request(void) { } +#endif + +static void wb_sanity_check_finished(void) +{ + WOLFSSL* ssl = wb_ssl_c; + CertReqCtx reqCtx; + + XMEMSET(&reqCtx, 0, sizeof(reqCtx)); + +#if !defined(NO_WOLFSSL_SERVER) && defined(WOLFSSL_EARLY_DATA) + /* Server early-data guard: + * earlyData == process_early_data && !dtls && !WOLFSSL_IS_QUIC(ssl) */ + wb_sc_reset(); + ssl->options.side = WOLFSSL_SERVER_END; + ssl->options.serverState = SERVER_FINISHED_COMPLETE; + ssl->options.clientState = CLIENT_HELLO_COMPLETE; + ssl->earlyData = process_early_data; /* (T,T,T) -> ooo */ + WB_SC(finished); + + wb_sc_reset(); + ssl->options.side = WOLFSSL_SERVER_END; + ssl->options.serverState = SERVER_FINISHED_COMPLETE; + ssl->options.clientState = CLIENT_HELLO_COMPLETE; /* (F,-,-) */ + WB_SC(finished); + + wb_sc_reset(); + ssl->options.side = WOLFSSL_SERVER_END; + ssl->options.serverState = SERVER_FINISHED_COMPLETE; + ssl->options.clientState = CLIENT_HELLO_COMPLETE; + ssl->earlyData = process_early_data; + ssl->options.dtls = 1; /* (T,F,-) */ + WB_SC(finished); + ssl->options.dtls = 0; +#endif + + /* The three peer-certificate guards. All are reached on the server with + * serverState >= SERVER_FINISHED_COMPLETE and clientState >= + * CLIENT_HELLO_COMPLETE, or on the client with serverState == + * SERVER_CERT_VERIFY_COMPLETE. pskNegotiated must be 0 for the block to be + * entered at all. */ +#define WB_SC_FIN_SERVER() \ + do { wb_sc_reset(); \ + ssl->options.side = WOLFSSL_SERVER_END; \ + ssl->options.serverState = SERVER_FINISHED_COMPLETE; \ + ssl->options.clientState = CLIENT_HELLO_COMPLETE; \ + } while (0) +#define WB_SC_FIN_CLIENT() \ + do { wb_sc_reset(); \ + ssl->options.side = WOLFSSL_CLIENT_END; \ + ssl->options.clientState = CLIENT_HELLO_COMPLETE; \ + ssl->options.serverState = SERVER_CERT_VERIFY_COMPLETE; \ + } while (0) + +#ifndef NO_WOLFSSL_SERVER + /* Guard 1: verifyPeer && + * (!verifyPostHandshake || + * (side == SERVER && certReqCtx != NULL)) && + * !got_certificate */ + WB_SC_FIN_SERVER(); ssl->options.verifyPeer = 1; /* (T,T,-,-,T) */ + WB_SC(finished); + + WB_SC_FIN_SERVER(); /* (F,-,-,-,-) */ + WB_SC(finished); + + WB_SC_FIN_SERVER(); ssl->options.verifyPeer = 1; + ssl->msgsReceived.got_certificate = 1; /* (T,T,-,-,F) */ + WB_SC(finished); + +#ifdef WOLFSSL_POST_HANDSHAKE_AUTH + WB_SC_FIN_SERVER(); ssl->options.verifyPeer = 1; + ssl->options.verifyPostHandshake = 1; + ssl->certReqCtx = &reqCtx; /* (T,F,T,T,T) */ + WB_SC(finished); + ssl->certReqCtx = NULL; + + WB_SC_FIN_SERVER(); ssl->options.verifyPeer = 1; + ssl->options.verifyPostHandshake = 1; /* (T,F,T,F,-) */ + WB_SC(finished); +#endif +#endif /* !NO_WOLFSSL_SERVER */ + +#ifndef NO_WOLFSSL_CLIENT +#ifdef WOLFSSL_POST_HANDSHAKE_AUTH + /* (T,F,F,-,-): a client never satisfies the side == SERVER operand. */ + WB_SC_FIN_CLIENT(); ssl->options.verifyPeer = 1; + ssl->options.verifyPostHandshake = 1; + ssl->msgsReceived.got_certificate = 1; + WB_SC(finished); +#endif +#endif + +#ifndef NO_WOLFSSL_SERVER + /* Guard 2: (mutualAuth || (side == CLIENT && verifyPeer)) && + * !havePeerCert */ + WB_SC_FIN_SERVER(); ssl->options.mutualAuth = 1; /* (T,-,-,T) */ + WB_SC(finished); + + WB_SC_FIN_SERVER(); ssl->options.mutualAuth = 1; + ssl->options.havePeerCert = 1; /* (T,-,-,F) */ + WB_SC(finished); + + WB_SC_FIN_SERVER(); /* (F,F,-,-) */ + WB_SC(finished); +#endif + +#ifndef NO_WOLFSSL_CLIENT + WB_SC_FIN_CLIENT(); ssl->options.verifyPeer = 1; + ssl->msgsReceived.got_certificate = 1; /* (F,T,T,T) */ + WB_SC(finished); + + WB_SC_FIN_CLIENT(); /* (F,T,F,-) */ + WB_SC(finished); +#endif + +#ifndef NO_WOLFSSL_SERVER + /* Guard 3: (mutualAuth || verifyPeer) && havePeerCert && !havePeerVerify */ + WB_SC_FIN_SERVER(); ssl->options.mutualAuth = 1; + ssl->options.havePeerCert = 1; /* (T,-,T,T) */ + WB_SC(finished); + + WB_SC_FIN_SERVER(); ssl->options.mutualAuth = 1; + ssl->options.havePeerCert = 1; + ssl->options.havePeerVerify = 1; /* (T,-,T,F) */ + WB_SC(finished); + + /* mutualAuth clear, verifyPeer set: guard 2's subexpression is false on + * the server, so guard 3 is reached with cond0 false and cond1 true. */ + WB_SC_FIN_SERVER(); ssl->options.verifyPeer = 1; + ssl->msgsReceived.got_certificate = 1; + ssl->options.havePeerCert = 1; /* (F,T,T,T) */ + WB_SC(finished); + + WB_SC_FIN_SERVER(); ssl->options.verifyPeer = 1; + ssl->msgsReceived.got_certificate = 1; /* (F,T,F,-) */ + WB_SC(finished); + + WB_SC_FIN_SERVER(); /* (F,F,-,-) */ + WB_SC(finished); +#endif + +#undef WB_SC_FIN_SERVER +#undef WB_SC_FIN_CLIENT + (void)reqCtx; +} + +#if defined(WOLFSSL_DTLS13) && !defined(WOLFSSL_NO_TLS12) +static void wb_sanity_check_hello_verify_request(void) +{ + WOLFSSL* ssl = wb_ssl_c; + + /* Ordering guard: + * serverState >= SERVER_HELLO_RETRY_REQUEST_COMPLETE || + * connectState != CLIENT_HELLO_SENT */ + wb_sc_reset(); ssl->options.dtls = 1; + ssl->options.serverState = SERVER_HELLO_RETRY_REQUEST_COMPLETE; /* (T,-) */ + WB_SC(hello_verify_request); + + wb_sc_reset(); ssl->options.dtls = 1; /* (F,T) */ + WB_SC(hello_verify_request); + + /* (F,F) falls through to the downgrade guard: + * !downgrade || minDowngrade < DTLSv1_2_MINOR */ + wb_sc_reset(); ssl->options.dtls = 1; + ssl->options.connectState = CLIENT_HELLO_SENT; /* (F,F); (T,-) */ + WB_SC(hello_verify_request); + + wb_sc_reset(); ssl->options.dtls = 1; + ssl->options.connectState = CLIENT_HELLO_SENT; + ssl->options.downgrade = 1; + ssl->options.minDowngrade = 0; /* (F,T) */ + WB_SC(hello_verify_request); + + wb_sc_reset(); ssl->options.dtls = 1; + ssl->options.connectState = CLIENT_HELLO_SENT; + ssl->options.downgrade = 1; + ssl->options.minDowngrade = DTLSv1_2_MINOR; /* (F,F) -> accept */ + WB_SC(hello_verify_request); + + wb_sc_reset(); +} +#else +static void wb_sanity_check_hello_verify_request(void) { } +#endif + +static void wb_sanity_check_msgs(void) +{ + if (wb_ssl_c == NULL) { + WB_NOTE("no ssl fixture; SanityCheckTls13MsgReceived skipped"); + return; + } + + wb_sanity_check_client_hello(); + wb_sanity_check_certificate(); + wb_sanity_check_certificate_request(); + wb_sanity_check_finished(); + wb_sanity_check_hello_verify_request(); + wb_sc_reset(); + + WB_NOTE("SanityCheckTls13MsgReceived ordering matrix driven"); +} +#undef WB_SC +#else +static void wb_sanity_check_msgs(void) +{ WB_NOTE("no WOLFSSL fixture on this build axis; skipped"); } +#endif /* WB_HAVE_SSL_FIXTURE */ + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("tls13.c white-box MC/DC supplement\n"); + +#ifdef WB_HAVE_SSL_FIXTURE + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + WB_NOTE("wolfSSL_Init failed; fixture-based groups skipped"); + } + else if (!wb_fixture_setup()) { + WB_NOTE("could not build the WOLFSSL fixtures; groups skipped"); + } +#endif + + wb_decode_tls13_sigalg(); + wb_key_schedule_null_guards(); + wb_build_handshake_hmac_guard(); + wb_create_cookie_ext_guards(); + wb_sanity_check_msgs(); + +#ifdef WB_HAVE_SSL_FIXTURE + wb_fixture_teardown(); + wolfSSL_Cleanup(); +#endif + + printf("done\n"); + /* Always 0: a nonzero exit is scored as a failed white-box and its + * coverage is discarded. */ + return 0; +} diff --git a/tests/unit-mcdc/test_tsp_fault_whitebox.c b/tests/unit-mcdc/test_tsp_fault_whitebox.c index 60873314c4..0c255f820c 100644 --- a/tests/unit-mcdc/test_tsp_fault_whitebox.c +++ b/tests/unit-mcdc/test_tsp_fault_whitebox.c @@ -24,7 +24,7 @@ * * tests/api/test_tsp.c and tests/unit-mcdc/test_tsp_whitebox.c together drive * the module's happy paths and most static-helper argument guards. The - * campaign's GAPS.md still lists a residual set of MC/DC independence pairs + * suite's the uncovered-condition report still lists a residual set of MC/DC independence pairs * as unshown in the union of all variant runs; this file targets those, * providing BOTH rows of each pair in this one binary (MC/DC independence is * computed per binary, not merged across separately-compiled #include @@ -71,7 +71,7 @@ * tsa_cert_der_2048/tsa_key_der_2048 (real RSA signature over real * SignedAttributes). * - * DEATHNOTE claim check (asn_tsp.c:1433, condition index 0, + * dead-condition claim check (asn_tsp.c:1433, condition index 0, * `GetASNTag(signers, &idx, &tag, signersSz) < 0`, inside * TspCheckOneSignerInfo()'s `while ((ret == 0) && (idx < signersSz))` loop): * CONFIRMED dead at this call site. GetASNTag() only fails when @@ -86,7 +86,7 @@ * test_tsp_whitebox.c's header for why that would be unnecessary anyway: * none of the residuals here are in one of its file-static helpers). * - * Targeted residuals, by GAPS.md line (17 NULL/argument-guard conditions): + * Targeted residuals, by the uncovered-condition report line (17 NULL/argument-guard conditions): * tsp.c:797 idx0,idx1 - SetGenTimeAsTime ts==NULL / ValidateGmtime * tsp.c:1033 idx3,idx5 - SetFromRequest policySz==0 / serialSz==0 * tsp.c:1175 idx4 - CheckRequest nonce content mismatch @@ -97,6 +97,18 @@ * tsp.c:1763 idx0,idx3 - Tsp_CheckSignerCert no-EKU / EKU-not-critical * tsp.c:1773 idx0,idx2 - Tsp_CheckSignerCert no-KU / KU-zero-bits * tsp.c:2162 idx0,idx1 - TspResponse_Verify cm != NULL + * tsp.c:2167 idx1 - TspResponse_Verify cert != NULL. The public + * entry points make this operand invariantly true: + * wc_TspResponse_Verify() rejects a NULL cert up + * front, and wc_TspResponse_VerifyWithCm() -- the + * only caller that passes cert == NULL -- passes a + * non-NULL cm, so the cm arm above consumes the + * ret == 0 arrival. Called directly (this TU has + * the static in scope) the operand pairs: cm == + * NULL with cert == NULL gives the decision's + * false row, and cm == NULL with a *different* + * trusted certificate gives its true row, both on + * the same verified token. * Plus allocation err-chain coverage (mcdc_fault_alloc.h fault sweep) over * wc_TspTstInfo_SignWithPkcs7()'s tstDer/attribs XMALLOC calls, and two cheap * bonus rows opportunistic with the above (tsp.c:939 SetNonce loop entry, @@ -113,7 +125,7 @@ * GeneralName (no mock needed) * tsp.c:2135 idx2 - TspResponse_Verify tokenSz==0 with a token * - * STRUCTURALLY UNSATISFIABLE (recorded in campaign/db/exclusions.json): + * STRUCTURALLY UNSATISFIABLE (recorded in the exclusion record): * - tsp.c:2167 idx3 `pkcs7->verifyCert == NULL`: this else-if only runs * with ret == 0, and wc_TspTstInfo_VerifyWithPKCS7() sets ret to * TSP_VERIFY_E whenever pkcs7->verifyCert is NULL, so the operand is @@ -1292,6 +1304,19 @@ static void wb_response_verify_cm(void) } } + /* 2167 idx1 true row: cm==NULL and a non-NULL trusted certificate that + * is NOT the signer. The token carries the TSA's own certificate, so + * wc_TspTstInfo_VerifyWithPKCS7() still returns 0 and the else-if is + * reached with ret==0; the pin comparison then fails on the length + * operand. Paired with the cert==NULL calls above, which reach the same + * else-if with ret==0 and take its false side on this very operand. */ + ret = TspResponse_Verify(&resp, client_cert_der_2048, + (word32)sizeof_client_cert_der_2048, NULL, NULL); + if (ret != WC_NO_ERR_TRACE(TSP_VERIFY_E)) { + WB_NOTE("TspResponse_Verify(wrong trusted cert) did not reject"); + wb_fail = 1; + } + /* 2162 idx1 true: ret==0 with cm != NULL. An empty (no trust anchors) * certificate manager still exercises the decision's TRUE row; the * ensuing chain verify is expected to fail (untrusted signer), which is @@ -1333,7 +1358,7 @@ int main(void) wb_response_verify_cm(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Always return 0: a nonzero exit makes the campaign discard the whole + /* Always return 0: a nonzero exit makes the harness discard the whole * variant's coverage, including the parts that did succeed. */ (void)wb_fail; return 0; diff --git a/tests/unit-mcdc/test_tsp_whitebox.c b/tests/unit-mcdc/test_tsp_whitebox.c index 8dde3c27a6..db75385860 100644 --- a/tests/unit-mcdc/test_tsp_whitebox.c +++ b/tests/unit-mcdc/test_tsp_whitebox.c @@ -38,7 +38,7 @@ * Class 9 wc_TspTstInfo_Encode() accuracy micros!=0 ............... 1 cond * Class 10 TspCheckSigningCertAttr() cert-hash mismatch ............ 1 cond * Class 11 TspCheckOneSignerInfo() SignerInfo SET walk ............. 4 conds - * Total newly exercised: 30 conditions (of 58 in the campaign's GAPS.md). + * Total newly exercised: 30 conditions (of 58 in the uncovered-condition report). * * Documented residuals (not exercised here; time-boxed out of this pass - * each needs either a fault only reachable through a platform-specific @@ -55,7 +55,7 @@ * fixture with "no EKU extension at all" or "EKU present but not * critical" was found in certs_test.h within this pass's time budget * (tsa_bad_ku_cert_der_2048 and tsa_extra_eku_cert_der_2048 cover other - * operands of the same two decisions, already outside GAPS.md). + * operands of the same two decisions, already outside the uncovered-condition report). * - tsp.c:1854 wc_TspTstInfo_VerifyWithPKCS7() contentType-OID mismatch, * tsp.c:2162/:2167/:2179/:2188/:2230 TspResponse_Verify()'s cm/cert/ * contentSz/cleanup decisions past a successful token verify: all @@ -73,7 +73,7 @@ * technique as test_pkcs12_parse_whitebox.c's Class 3), but the ASN * template's exact byte offsets were not worked out in this pass. * - * STRUCTURALLY UNSATISFIABLE (recorded in campaign/db/exclusions.json): + * STRUCTURALLY UNSATISFIABLE (recorded in the exclusion record): * - asn_tsp.c:1021 idx1 `length >= 2`, idx2 `length <= 5`. Defence in depth * behind the ASN.1 template. GetASN_Items() stores the item's FULL length * (asn.c:1948) before stepping over a BIT STRING's unused-bits byte, then @@ -702,7 +702,7 @@ int main(void) wb_check_signing_cert_attr(); wb_check_one_signer_info(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); - /* Always return 0: a nonzero exit makes the campaign discard the whole + /* Always return 0: a nonzero exit makes the harness discard the whole * variant's coverage, including the parts that did succeed. */ return 0; } diff --git a/tests/unit-mcdc/test_wc_encrypt_whitebox.c b/tests/unit-mcdc/test_wc_encrypt_whitebox.c index 906b077bd2..9d1ef7734c 100644 --- a/tests/unit-mcdc/test_wc_encrypt_whitebox.c +++ b/tests/unit-mcdc/test_wc_encrypt_whitebox.c @@ -60,7 +60,7 @@ * if (version == PKCS5v2 || version == PKCS12v1) * desIv = cbcIv; * Both operands need three rows in one binary, and the row that never - * occurs in the campaign's API-level runs is the all-false one, because + * occurs in the API-level runs is the all-false one, because * the PBE_*_DES ids are only ever reached with a PKCS#12 or PKCS#5 v2.0 * encoding. The third version value has to be one the version switch * above still accepts, otherwise ret != 0 and the cipher switch is never @@ -76,7 +76,7 @@ * both inside the range wc_PBKDF1() just wrote, so no uninitialised or * out-of-bounds read. Input is 32 bytes, a whole number of DES blocks. * - * Build: compiled by the campaign's white-box step with the same MC/DC CFLAGS + * Build: compiled by the white-box step with the same MC/DC CFLAGS * as the instrumented library, then linked against that variant's * libwolfssl.a with wc_encrypt.o removed. Not part of the wolfSSL build. */ diff --git a/tests/unit-mcdc/test_wc_lms_impl_whitebox.c b/tests/unit-mcdc/test_wc_lms_impl_whitebox.c index 2c2ac291fc..e0359e1e96 100644 --- a/tests/unit-mcdc/test_wc_lms_impl_whitebox.c +++ b/tests/unit-mcdc/test_wc_lms_impl_whitebox.c @@ -236,7 +236,7 @@ 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 + /* Setup/skip conditions are surfaced as notes, not failures: the harness * 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. */ diff --git a/tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c b/tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c index 3b311db1f5..6258dea18e 100644 --- a/tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c +++ b/tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c @@ -24,8 +24,8 @@ * 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). + * suite variants and are skipped here (see notes below and the final + * per-line residual list in the notes). * * 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 @@ -56,7 +56,7 @@ static int wb_fail = 0; /* Tree height for the two drivers that build a real multi-level HSS key. * Keygen cost is 2^h OTS keys per subtree per level, so height 5 made those * two setups alone take ~320s under MC/DC instrumentation -- over the - * campaign's 600s TEST_TIMEOUT once variants run concurrently, which scored + * suite's 600s TEST_TIMEOUT once variants run concurrently, which scored * the whole file as a skip and cost every decision in it. * * wb_make_params() bypasses wc_LmsKey_SetParameters() and lmsType is only @@ -1066,7 +1066,7 @@ static void wb_hss_full_cycle(void) {} int main(void) { - /* Unbuffered: if a driver overruns the campaign's TEST_TIMEOUT the + /* Unbuffered: if a driver overruns the TEST_TIMEOUT the * harness SIGKILLs this process, and anything still sitting in stdio's * buffer is lost -- which reports as an empty log and no clue where it * stopped. */ @@ -1088,7 +1088,7 @@ int main(void) 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 + * process failures: the harness 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 index 54e8f853d3..41409587cc 100644 --- a/tests/unit-mcdc/test_wc_mldsa_whitebox.c +++ b/tests/unit-mcdc/test_wc_mldsa_whitebox.c @@ -44,7 +44,7 @@ * 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. + * the binary always returns 0 so the harness keeps the variant. */ /* SAVE_VECTOR_REGISTERS2() gates every SIMD dispatch in this file. In a @@ -1198,7 +1198,7 @@ static void wb_arg_guards(void) * if ((ret == 0) && valid) { ... } * * guards: `valid` goes false when a decoded hint is malformed, a norm check - * fails, or the recomputed commitment differs. A campaign that only ever + * fails, or the recomputed commitment differs. A suite that only ever * verifies signatures it just produced sees valid == 1 at every one of them, * so the operand is undriven -- and a genuinely corrupt signature is the * ordinary, in-spec way to drive it. @@ -1301,8 +1301,119 @@ static void wb_verify_invalid(void) } #endif +/* ------------------------------------------------------------------------- * + * wc_MlDsaKey_CheckKey()'s s1/s2 coefficient range check (wc_mldsa.c:12517, + * :12522, :12523). + * + * for (c = 0; c < (word32)(params->l * MLDSA_N); c++) { + * if (s1[c] < -eta || s1[c] > eta) { ret = PUBLIC_KEY_E; break; } + * } + * for (c = 0; (ret == 0) && (c < (word32)(params->k * MLDSA_N)); c++) { + * if (s2[c] < -eta || s2[c] > eta) { ret = PUBLIC_KEY_E; break; } + * } + * + * Every key the API can hand this function was either generated (s1/s2 are in + * range by construction) or decoded through mldsa_check_eta_range(), which + * rejects an out-of-range nibble/3-bit group before the key is marked set. So + * from tests/api the two `< -eta` operands only ever take their FALSE side and + * the s2 loop header's `ret == 0` operand only ever takes its TRUE side. + * + * The vector is a MUTATED private key blob: a good key is generated, then the + * first byte of the packed s1 (or s2) region of key->k is forced to 0xFF. The + * eta unpackers read `eta - t` from an unsigned bit field -- t is a 3-bit + * group for eta 2 and a nibble for eta 4 (mldsa_decode_eta_2_bits_c / + * mldsa_decode_eta_4_bits_c) -- so 0xFF decodes the first coefficient as + * 2 - 7 = -5 or 4 - 15 = -11, out of range on the LOW side for either + * parameter set. The un-mutated CheckKey call in the same binary supplies the + * all-false row of both `||` decisions and the `ret == 0` TRUE row; the s1 + * mutation supplies the s2 header's `ret == 0` FALSE row (the s1 loop has + * already set PUBLIC_KEY_E when that header is next evaluated). + * + * The HIGH side (`s1[c] > eta`) is NOT driven here and cannot be: the unpack + * is `eta - t` with t unsigned, so the decoded coefficient never exceeds eta. + * Both `> eta` operands are recorded in the exclusion record. + * ------------------------------------------------------------------------- */ +#if defined(WOLFSSL_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_CHECK_KEY) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ + !defined(WOLFSSL_MLDSA_ASSIGN_KEY) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) +static void wb_check_key_range(void) +{ + wc_MlDsaKey key; + byte seed[MLDSA_SEED_SZ]; + byte* kp; + byte* s1p; + byte* s2p; + byte savedS1; + byte savedS2; + int ret; +#ifndef WOLFSSL_NO_ML_DSA_44 + const int level = WC_ML_DSA_44; /* smallest set: fastest under cov */ +#elif !defined(WOLFSSL_NO_ML_DSA_65) + const int level = WC_ML_DSA_65; +#else + const int level = WC_ML_DSA_87; +#endif + + XMEMSET(seed, 0x27, sizeof(seed)); + + if (wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID) != 0) { + WB_NOTE("CheckKey range rows skipped (init failed)"); + return; + } + if ((wc_MlDsaKey_SetParams(&key, level) != 0) || + (wc_MlDsaKey_MakeKeyFromSeed(&key, seed) != 0)) { + wc_MlDsaKey_Free(&key); + WB_NOTE("CheckKey range rows skipped (keygen unavailable)"); + return; + } + + /* All-false row of both range decisions, and the s2 header's ret == 0 + * true row: a well-formed key. */ + ret = wc_MlDsaKey_CheckKey(&key); + if (ret != 0) { + WB_NOTE("wc_MlDsaKey_CheckKey rejected a freshly generated key"); + } + + kp = (byte*)key.k; + s1p = kp + MLDSA_PUB_SEED_SZ + MLDSA_K_SZ + MLDSA_TR_SZ; + s2p = s1p + key.params->s1EncSz; + savedS1 = s1p[0]; + savedS2 = s2p[0]; + + /* s1[0] out of range on the low side: :12517 idx0 true, and the s2 loop + * header (:12522 idx0) is then evaluated with ret != 0. */ + s1p[0] = 0xFF; + ret = wc_MlDsaKey_CheckKey(&key); + if (ret == 0) { + WB_NOTE("CheckKey accepted an out-of-range s1 coefficient"); + } + s1p[0] = savedS1; + + /* s2[0] out of range: the s1 loop runs clean, the s2 header is true, and + * :12523 idx0 takes its true side. */ + s2p[0] = 0xFF; + ret = wc_MlDsaKey_CheckKey(&key); + if (ret == 0) { + WB_NOTE("CheckKey accepted an out-of-range s2 coefficient"); + } + s2p[0] = savedS2; + + wc_MlDsaKey_Free(&key); + WB_NOTE("CheckKey s1/s2 range rows exercised (12517, 12522, 12523)"); +} +#else +static void wb_check_key_range(void) +{ + WB_NOTE("CheckKey range rows skipped (not compiled in this variant)"); +} +#endif + int main(void) { + /* Unbuffered: on a timeout the process is killed and anything still + * buffered is lost, which reads as an empty log. */ + setvbuf(stdout, NULL, _IONBF, 0); + printf("wc_mldsa.c white-box MC/DC supplement\n"); #if !defined(WOLFSSL_HAVE_MLDSA) printf(" ML-DSA not enabled; nothing to exercise\n"); @@ -1339,6 +1450,7 @@ int main(void) wb_gen_lane_rows(); wb_arg_guards(); wb_verify_invalid(); + wb_check_key_range(); printf("done (%d note%s)\n", wb_notes, (wb_notes == 1) ? "" : "s"); return 0; #endif diff --git a/tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c b/tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c index bdf4d904e7..28bdd08779 100644 --- a/tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c +++ b/tests/unit-mcdc/test_wc_mlkem_poly_whitebox.c @@ -37,12 +37,12 @@ * - 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. + * and butterfly loops driven when the harness 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 + * arrays); prints skips and returns 0 on any unexpected result so the harness * keeps the variant. */ @@ -355,7 +355,7 @@ static void wb_transform(void) * 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. + * axis, which this suite 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. @@ -526,7 +526,7 @@ static const int wb_kem_types[] = { /* One key generation only: the matrix generators and their rejection-sampling * loops all hang off wc_MlKemKey_MakeKey(), and keeping the per-pass work to a * single keygen is what lets the lane sweep below afford ~100 passes inside the - * campaign's wall-clock budget. */ + * suite's wall-clock budget. */ static void wb_run_keygen(WC_RNG* rng, int type) { MlKemKey key; @@ -881,7 +881,7 @@ int main(void) wb_rej_uniform(); wb_transform(); if (wb_fail) { - /* Do not fail the campaign variant on a behavioural surprise; the + /* Do not fail the harness variant on a behavioural surprise; the * coverage is still valid. Report and exit 0. */ printf(" [wb] note: one or more sanity checks were unexpected\n"); } diff --git a/tests/unit-mcdc/test_wc_port_whitebox.c b/tests/unit-mcdc/test_wc_port_whitebox.c index e794159b33..54b82eb7c9 100644 --- a/tests/unit-mcdc/test_wc_port_whitebox.c +++ b/tests/unit-mcdc/test_wc_port_whitebox.c @@ -34,7 +34,7 @@ * * wc_accept_cloexec()'s guard `if (errno != ENOSYS && errno != EINVAL)` * (wc_port.c ~:5684) can only see errno == ENOSYS on a kernel that does not - * implement accept4(). Every host this campaign runs on does, so the first + * implement accept4(). Every host this suite runs on does, so the first * operand has no reachable independence pair from the outside. * * The white-box TU #includes wc_port.c directly, so accept4() can be @@ -296,7 +296,7 @@ int main(void) wb_strnstr(); wb_cloexec_wrappers(); printf(" [wb] failures: %d\n", wb_fail); - /* Always 0: a non-zero exit makes the campaign harness discard the + /* Always 0: a non-zero exit makes the test harness discard the * whole variant rather than record its coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c b/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c index fd55d0fb12..6239fc996a 100644 --- a/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c +++ b/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c @@ -35,6 +35,24 @@ * 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. + * + * What this file deliberately does NOT cover - both are in the + * exclusion ledger (the exclusion record, + * the exclusion record#condition-level-exclusions): + * + * 2465:2 and 4131:2 - the "c <= 4" operand of WC_IDX_INVALID's mixed + * 32/64-bit arm, i.e. "((c > 4) && IDX64_INVALID(..)) || ((c <= 4) && + * IDX32_INVALID(..))" with c = params->idx_len. It is the exact logical + * negation of the "c > 4" operand, which is evaluated on every arrival that + * gets past "ret == 0", so every vector that flips it flips "c > 4" too and + * no independence pair exists. It is not dead: it IS evaluated, and false, + * whenever c > 4 is true and IDX64_INVALID is false (a live height-40 key). + * The other four operands of both decisions ARE driven here - "ret == 0" + * false from a forged idx_len of 2, "c > 4"/IDX64_INVALID from live and + * retired XMSSMT-SHA2_40/8_256 keys, IDX32_INVALID from live and retired + * XMSSMT-SHA2_20/2_256 keys. Beware the index: these five conditions all + * share one macro-expansion location, and llvm-cov's export order is NOT + * source order there - index 2 is "c <= 4" and index 3 is IDX64_INVALID. */ #include @@ -198,7 +216,7 @@ static void wb_param_roundtrip(WC_RNG* rng, const char* paramStr) * * `(idx_len > 4)` and `(idx_len <= 4)` are exact logical complements of one * parameter, so the second of them has no independence pair by construction; - * that residual is recorded in campaign/db/exclusions.json. + * that residual is recorded in the exclusion record. */ static void wb_exhausted_index(WC_RNG* rng, const char* paramStr, int doSign) { @@ -254,12 +272,14 @@ static void wb_exhausted_index(WC_RNG* rng, const char* paramStr, int doSign) /* The FALSE row for this parameter set: one ordinary signature, so the * decision is evaluated with a valid index and the same idx_len. Under - * WOLFSSL_WC_XMSS_SMALL a height-40 signature recomputes every subtree and - * does not fit the campaign's TEST_TIMEOUT -- a timed-out white-box is - * scored as a SILENT SKIP and would lose the whole file -- so that one row - * is skipped there. wc_XmssKey_SigsLeft() is cheap in every build and - * still supplies the live-index row for wc_xmss_sigsleft()'s copy of the - * same macro. */ + * WOLFSSL_WC_XMSS_SMALL a height-40 signature recomputes every subtree, so + * it is a single signature per key here and the four-vector allocator + * sweep below (which has nothing to fail in the small build) is compiled + * out; the measured white-box runtime is in the run log. This row is what + * pairs IDX64_INVALID inside the small build's own wc_xmssmt_sign() + * (2455): its retired-index partner alone shows only the true side. + * wc_XmssKey_SigsLeft() is cheap in every build and still supplies the + * live-index row for wc_xmss_sigsleft()'s copy of the same macro. */ if (doSign) { sigSz = sigLen; if (wc_XmssKey_Sign(&key, sig, &sigSz, msg, (int)sizeof(msg)) != 0) { @@ -283,6 +303,7 @@ static void wb_exhausted_index(WC_RNG* rng, const char* paramStr, int doSign) * wc_xmssmt_sign() reach the guard with ret != 0. A short dense sweep -- * a vector count, not a clock -- covers the handful of allocations the * sign path makes. */ +#ifndef WOLFSSL_WC_XMSS_SMALL if (doSign) { int n; @@ -294,7 +315,22 @@ static void wb_exhausted_index(WC_RNG* rng, const char* paramStr, int doSign) mcdc_fa_disarm(); } mcdc_fa_restore(); + /* The first injected failure moves the XmssKey to WC_XMSS_STATE_BAD + * and wc_XmssKey_Sign()/_SigsLeft() then short-circuit in wc_xmss.c + * WITHOUT calling into this file - which silently dropped every + * retired-index row below (measured: the 64-bit arm of the macro at + * 2455/4117 was left unpaired in each non-small variant). The + * injected failures happen before any secret-key mutation, so the + * persisted key is still consistent; put the handle back in a good + * state so the rows that follow actually reach wc_xmss_impl.c. */ + key.state = WC_XMSS_STATE_OK; } +#else + /* wc_xmss_impl.c's only XMALLOC is the BDS-state allocation, which lives + * in the non-small block, so the injector has nothing to fail here: the + * sweep would only burn four height-40 recompute signatures. */ + (void)0; +#endif /* The TRUE row: retire the persisted index. Both entry points reload the * secret key through the read callback, so this is all that is needed. */ @@ -371,11 +407,7 @@ static void wb_run(void) #endif #if defined(WC_XMSS_SHA256) && (WOLFSSL_XMSS_MAX_HEIGHT >= 40) && \ (!defined(WOLFSSL_XMSS_MIN_HEIGHT) || (WOLFSSL_XMSS_MIN_HEIGHT <= 40)) -#ifdef WOLFSSL_WC_XMSS_SMALL - wb_exhausted_index(&rng, "XMSSMT-SHA2_40/8_256", 0); -#else wb_exhausted_index(&rng, "XMSSMT-SHA2_40/8_256", 1); -#endif #endif wc_FreeRng(&rng); @@ -1156,11 +1188,12 @@ static void wb_full_cycle_d1(void) /* 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. */ + * reusing the just-exhausted sk above, whose index field wc_xmssmt_sign() + * has XMEMSET to all-0xFF: 2^h - 1 is the smallest value the check must + * reject and is the one this test is after. (The all-0xFF marker used to + * read back as "valid" because IDX32_INVALID's "(idx+1)>>h" overflowed to + * 0 - a real defect, fixed in "wolfcrypt: xmss exhausted-key index marker + * wrapped and re-enabled signing".) */ if (exhausted) { byte idxSk[2048]; @@ -1217,6 +1250,220 @@ static void wb_full_cycle_d1(void) } #endif /* !WOLFSSL_XMSS_VERIFY_ONLY && !WOLFSSL_WC_XMSS_SMALL */ +/******************************************** + * 3995-3997 (3981-3983 before the exhausted-marker fix moved the file): + * wc_xmssmt_sign_next_idx()'s + * "if ((ret == 0) && (i > 0) && (updates > 0) && + * (idx_tree < ((XmssIdx)1 << (h - (hs * (i + 1))))) && + * (bds[alt_i].next < ((XmssIdx)1 << h)))" + * + * condIndex 0 ("ret == 0") and condIndex 4 ("bds[alt_i].next < (1 << h)") + * have no independence pair from ordinary signing: + * + * - condIndex 4: BdsState.next is a *sub*tree leaf counter. It is reset to 0 + * at a subtree boundary and wc_xmss_bds_update() itself stops incrementing + * it at (1 << sub_h), while the bound tested here is (1 << h) with h the + * FULL tree height. On every reachable signing vector next <= 2^sub_h <= + * 2^h, so the operand is true. It is only false when the value loaded out + * of the persisted secret key (a 24-bit big-endian field, wc_xmss_bds_ + * state_load() at 2743) is already >= 2^h - i.e. a corrupted/forged + * private key, which is exactly the case this defensive guard exists for. + * Here that state is forged directly: the BDS array is loaded from a COPY + * of a good secret key and every state's "next" is set to 1 << h before + * wc_xmssmt_sign_next_idx() is called. Memory-safe: "next" is read in + * exactly two places - this guard, and wc_xmss_bds_update()'s own + * "next < (1 << sub_h)" entry test, which the forged value turns into a + * no-op. It indexes nothing. + * + * - condIndex 0: reaching this guard with ret != 0 needs the *same* loop + * iteration's wc_xmss_bds_auth_path() (or wc_xmss_bds_treehash_updates()) + * to have failed; a failure in any earlier iteration leaves through the + * for-header instead. Both only fail on a NULL BDS sub-buffer or a digest + * failure. Forged the same way: bds[BDS_IDX(...)].keep is NULLed, so + * wc_xmss_bds_auth_path() takes its "(bds->keep == NULL) || + * (bds->authPath == NULL)" bail-out, sets state->ret = WC_FAILURE and + * returns before touching anything. + * + * Both forged rows are driven against the ORDINARY, all-operands-true row + * from a real wc_xmssmt_sign() at the same index, in this same binary, so + * the independence pairs are complete here and do not lean on any other + * build. Parameters are hand-built with the same per-layer geometry as + * XMSSMT-SHA2_40/8_256 (sub_h = 5, bds_k = 0, so updates = 2) but only + * h = 20 / d = 4, which keeps keygen and eight signatures cheap. + ********************************************/ +#if !defined(WOLFSSL_XMSS_VERIFY_ONLY) && !defined(WOLFSSL_WC_XMSS_SMALL) && \ + defined(WC_XMSS_SHA256) +static byte wb_ni_sk[16384]; +static byte wb_ni_skCopy[16384]; +static byte wb_ni_sig[16384]; + +static void wb_sign_next_idx_rows(void) +{ + XmssParams params; + XmssState state; + byte seed[3 * 32]; + byte pk[160]; + static const byte msg[] = "xmss whitebox next-idx message"; + int ret; + int k; + + wb_params_init(¶ms, WC_HASH_TYPE_SHA256, 32, 32, 20, 4, 4, 0); + if ((params.sk_len > (word32)sizeof(wb_ni_sk)) || + (params.sig_len > (word32)sizeof(wb_ni_sig))) { + WB_NOTE("next-idx rows: scratch too small; skipped"); + return; + } + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("next-idx rows: state init failed; skipped"); + return; + } + + XMEMSET(seed, 0x33, sizeof(seed)); + XMEMSET(wb_ni_sk, 0, sizeof(wb_ni_sk)); + XMEMSET(pk, 0, sizeof(pk)); + + ret = wc_xmssmt_keygen(&state, seed, wb_ni_sk, pk); + if (ret != 0) { + WB_NOTE("next-idx rows: keygen failed; skipped"); + wb_state_free(&state); + return; + } + + for (k = 0; k < 8; k++) { + BdsState* bds; + byte* wots_sigs; + int j; + + /* Row A - forged BDS state: every state's next is already past the + * full-tree bound, so condIndex 4 is false with condIndex 0..3 true. */ + XMEMCPY(wb_ni_skCopy, wb_ni_sk, sizeof(wb_ni_skCopy)); + bds = NULL; + wots_sigs = NULL; + if (wc_xmss_bds_state_alloc(¶ms, &bds, state.heap) == 0) { + if (wc_xmss_bds_state_load(&state, wb_ni_skCopy, bds, + &wots_sigs) == 0) { + for (j = 0; j < 2 * (int)params.d - 1; j++) { + bds[j].next = (word32)1U << params.h; + } + state.ret = 0; + (void)wc_xmssmt_sign_next_idx(&state, bds, (XmssIdx)k, + wots_sigs, wb_ni_skCopy); + state.ret = 0; + } + wc_xmss_bds_state_free(bds, state.heap); + } + + /* Row B - forged BDS state: the working state's keep buffer is NULL, + * so this iteration's wc_xmss_bds_auth_path() fails and the guard is + * reached with ret != 0 (condIndex 0 false). */ + XMEMCPY(wb_ni_skCopy, wb_ni_sk, sizeof(wb_ni_skCopy)); + bds = NULL; + wots_sigs = NULL; + if (wc_xmss_bds_state_alloc(¶ms, &bds, state.heap) == 0) { + if (wc_xmss_bds_state_load(&state, wb_ni_skCopy, bds, + &wots_sigs) == 0) { + bds[BDS_IDX((XmssIdx)k, 0, params.sub_h, params.d)].keep = + NULL; + state.ret = 0; + (void)wc_xmssmt_sign_next_idx(&state, bds, (XmssIdx)k, + wots_sigs, wb_ni_skCopy); + state.ret = 0; + } + wc_xmss_bds_state_free(bds, state.heap); + } + + /* Row C - the ordinary all-true row: a real signature at the same + * index, whose wc_xmssmt_sign() runs wc_xmssmt_sign_next_idx() + * unforged. */ + XMEMSET(wb_ni_sig, 0, sizeof(wb_ni_sig)); + ret = wc_xmssmt_sign(&state, msg, (word32)sizeof(msg), wb_ni_sk, + wb_ni_sig); + if (ret != 0) { + WB_NOTE("next-idx rows: ordinary sign failed"); + wb_fail = 1; + break; + } + } + + wb_state_free(&state); + WB_NOTE("next-idx forged-BDS rows exercised"); +} +#else +static void wb_sign_next_idx_rows(void) +{ + WB_NOTE("BDS signing path not compiled in; wb_sign_next_idx_rows " + "skipped"); +} +#endif /* !WOLFSSL_XMSS_VERIFY_ONLY && !WOLFSSL_WC_XMSS_SMALL && + * WC_XMSS_SHA256 */ + +/******************************************** + * 2465 (WOLFSSL_WC_XMSS_SMALL's wc_xmssmt_sign() only): + * "if ((ret == 0) && (WC_IDX_INVALID(idx, params->idx_len, params->h)))" + * condIndex 0's false side. The only assignment to ret before the guard is + * WC_IDX_DECODE's trailing "else { ret = NOT_COMPILED_IN; }", which fires + * only when params->idx_len is none of 3, 4, 5 and 8 - and every set in + * wc_xmss_alg[] uses one of those four, so no key can produce it. A + * hand-built parameter set with idx_len == 2 does, exactly as + * wb_full_cycle_d1() already does for wc_xmss_sigsleft()'s copy of the same + * macro. + * + * Memory-safe: before the guard the function only zeroes state->addr and + * copies idx_len (2) bytes sk->sig; sk_seed/pk_seed/sig_r are pointer + * arithmetic that is never dereferenced on this path, and with ret != 0 + * every later block is gated by "if (ret == 0)" down to "return ret", so + * nothing is allocated or hashed. The buffers are still sized past + * params.sk_len so even the unused interior pointers stay in-object. + * + * Only built for WOLFSSL_WC_XMSS_SMALL: the non-small wc_xmssmt_sign() + * (4055) allocates and loads the BDS state from sk *before* its own index + * check and decodes with xmss_idx_decode(), which has no NOT_COMPILED_IN + * arm - it would neither reach this decision nor be memory-safe with a + * forged parameter set. + ********************************************/ +#if defined(WOLFSSL_WC_XMSS_SMALL) && !defined(WOLFSSL_XMSS_VERIFY_ONLY) && \ + defined(WC_XMSS_SHA256) +static void wb_smallmt_bad_idx_len(void) +{ + XmssParams params; + XmssState state; + byte sk[1024]; + byte sig[1024]; + static const byte msg[] = "xmss whitebox bad idx_len message"; + int ret; + + wb_params_init(¶ms, WC_HASH_TYPE_SHA256, 32, 32, 4, 1, 2, 0); + if (params.sk_len > (word32)sizeof(sk)) { + WB_NOTE("bad idx_len row: scratch too small; skipped"); + return; + } + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("bad idx_len row: state init failed; skipped"); + return; + } + + XMEMSET(sk, 0, sizeof(sk)); + XMEMSET(sig, 0, sizeof(sig)); + + ret = wc_xmssmt_sign(&state, msg, (word32)sizeof(msg), sk, sig); + if (ret != WC_NO_ERR_TRACE(NOT_COMPILED_IN)) { + WB_NOTE("bad idx_len row: wc_xmssmt_sign did not reject an " + "unsupported idx_len"); + wb_fail = 1; + } + + wb_state_free(&state); + WB_NOTE("small-path unsupported idx_len row exercised"); +} +#else +static void wb_smallmt_bad_idx_len(void) +{ + WB_NOTE("small XMSS^MT signing path not compiled in; " + "wb_smallmt_bad_idx_len skipped"); +} +#endif /* WOLFSSL_WC_XMSS_SMALL && !WOLFSSL_XMSS_VERIFY_ONLY && + * WC_XMSS_SHA256 */ + #else /* WOLFSSL_HAVE_XMSS */ static void wb_hash_family_pairs(void) @@ -1243,6 +1490,14 @@ static void wb_full_cycle_d1(void) { WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); } +static void wb_sign_next_idx_rows(void) +{ + WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); +} +static void wb_smallmt_bad_idx_len(void) +{ + WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); +} #endif /* WOLFSSL_HAVE_XMSS */ @@ -1256,6 +1511,8 @@ int main(void) wb_bds_auth_path(); wb_full_cycle_d2(); wb_full_cycle_d1(); + wb_sign_next_idx_rows(); + wb_smallmt_bad_idx_len(); 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 index d012f2d644..4cb9eb36e6 100644 --- a/tests/unit-mcdc/test_wolfentropy_whitebox.c +++ b/tests/unit-mcdc/test_wolfentropy_whitebox.c @@ -45,7 +45,7 @@ * 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. + * the harness 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 @@ -70,6 +70,51 @@ #include "mcdc_fault_mutex.h" +/* ---- SHA3-256 interposer, for the startup health test's noise fill ------- + * + * Entropy_GetNoise() is a file-static in wolfentropy.c itself, so it cannot be + * macro-interposed the way mcdc_fault_hash.h interposes primitives that live + * in another translation unit: a macro on its name renames the DEFINITION as + * well as the call sites and changes nothing. But every failure it can report + * originates in Entropy_MemUse(), whose only fallible operations are + * wc_Sha3_256_Update()/wc_Sha3_256_Final() on the shared conditioner -- and + * those DO come from sha3.o in the archive. Refusing one of them is therefore + * the reachable equivalent: Entropy_MemUse() -> Entropy_GetNoise() -> + * Entropy_HealthTest_Startup() propagates it into `ret` before the sample + * loop at :781 is ever evaluated. mcdc_fault_hash.h does not carry SHA-3 (no + * white-box has needed it before), so the wrapper is local to this TU. + * + * The wrapper is defined BEFORE the macro exists, so it still reaches the + * real primitive; ordering is load-bearing, exactly as in mcdc_fault_hash.h. + * + * libwolfssl_sources.h has to come first (mcdc_fault_mutex.h deliberately + * includes nothing, so no configuration has been read yet): without it + * WOLFSSL_SHA3 is undefined at this point, the whole interposer is + * preprocessed away, and wb_startup_noise_fail() below -- which is guarded on + * the same macro but sits AFTER wolfentropy.c has pulled settings.h in -- + * refers to a wb_sha3_refuse that does not exist. That is a compile failure, + * which the harness scores as a SILENT SKIP. + */ +#include +#include +#include + +#ifdef WOLFSSL_SHA3 +#include + +static int wb_sha3_refuse = 0; + +static int wb_Sha3_256_Update(wc_Sha3* sha3, const byte* data, word32 len) +{ + if (wb_sha3_refuse) { + return WC_NO_ERR_TRACE(BAD_FUNC_ARG); + } + return wc_Sha3_256_Update(sha3, data, len); +} + +#define wc_Sha3_256_Update(s, d, l) wb_Sha3_256_Update((s), (d), (l)) +#endif /* WOLFSSL_SHA3 */ + #include #define MCDC_FM_IMPL @@ -192,7 +237,7 @@ static void wb_collect_path(void) * -- 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). */ + * steady-state call in this suite, e.g. wb_collect_path() above). */ static void wb_startup_retrigger(void) { int ret; @@ -280,6 +325,54 @@ static void wb_get_loop_early_exit(void) Entropy_Final(); } +/* Entropy_HealthTest_Startup()'s sample loop: + * "for (i = 0; (ret == 0) && (i < ENTROPY_INITIAL_COUNT); i++)" + * On a healthy host the loop always runs to completion, so idx0 ("ret == 0") + * only ever shows TRUE and the loop is only ever left through idx1. `ret` is + * assignable inside the loop (the two health tests at :782-:786), but real + * MemUse jitter never trips REP_CUTOFF/PROP_CUTOFF, and the noise buffer is + * filled by the file-static Entropy_GetNoise() which cannot be rigged + * directly. The reachable lever is the one thing Entropy_GetNoise() depends on + * from outside this file: refuse the conditioner's SHA3-256 update, so + * Entropy_MemUse() fails, Entropy_GetNoise() returns that error at :779, and + * the loop condition is evaluated once with ret != 0 -- idx0 FALSE, the + * missing half. The healthy call immediately before it (same binary) supplies + * (T,T) and, at i == ENTROPY_INITIAL_COUNT, (T,F). */ +#ifdef WOLFSSL_SHA3 +static void wb_startup_noise_fail(void) +{ + int ret; + + if (Entropy_Init() != 0) { + WB_NOTE("Entropy_Init failed; skipping startup noise-failure vector"); + return; + } + + /* Armed: the first conditioner update inside Entropy_MemUse() refuses, so + * Entropy_GetNoise() never fills the buffer. */ + wb_sha3_refuse = 1; + ret = Entropy_HealthTest_Startup(); + wb_sha3_refuse = 0; + if (ret == 0) { + WB_NOTE("refused SHA3 update did not fail the startup health test"); + wb_fail = 1; + } + + /* Unarmed partner in the same binary, which also leaves the health-test + * globals primed for whatever runs next. */ + if (Entropy_HealthTest_Startup() != 0) { + WB_NOTE("healthy startup health test failed (skip, not fail)"); + } + + Entropy_Final(); + + WB_NOTE("Entropy_HealthTest_Startup ret==0 pair exercised"); +} +#else +static void wb_startup_noise_fail(void) +{ WB_NOTE("WOLFSSL_SHA3 off; startup noise-failure vector skipped"); } +#endif /* WOLFSSL_SHA3 */ + #else /* !HAVE_ENTROPY_MEMUSE */ static void wb_repetition(void) @@ -292,6 +385,8 @@ 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"); } +static void wb_startup_noise_fail(void) +{ WB_NOTE("HAVE_ENTROPY_MEMUSE not compiled in; skipped startup noise fail"); } #endif /* HAVE_ENTROPY_MEMUSE */ @@ -364,9 +459,10 @@ int main(void) wb_proportion(); wb_startup_retrigger(); wb_get_loop_early_exit(); + wb_startup_noise_fail(); wb_entropy_get_mutex(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup issues are surfaced as skips; a nonzero exit would make the - * campaign discard this variant's coverage. */ + * suite discard this variant's coverage. */ return 0; } diff --git a/tests/unit-mcdc/test_wolfmath_whitebox.c b/tests/unit-mcdc/test_wolfmath_whitebox.c new file mode 100644 index 0000000000..bd4f8c8752 --- /dev/null +++ b/tests/unit-mcdc/test_wolfmath_whitebox.c @@ -0,0 +1,213 @@ +/* test_wolfmath_whitebox.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * MC/DC RNG-scripting white-box supplement for wolfcrypt/src/wolfmath.c. + * + * THE TWO OPEN CONDITIONS + * ----------------------- + * wolfmath.c:211 while ((ret == MP_OKAY) && (a->dp[a->used - 1] == 0)) + * + * This is mp_rand()'s "ensure the top digit is not zero" retry loop. Driven by + * a real RNG it is only ever evaluated once, as (T,F): ret is MP_OKAY because + * the preceding wc_RNG_GenerateBlock() succeeded, and the top digit is zero + * with probability 2^-64 (2^-32 on a 32-bit build), so: + * + * idx1 (a->dp[a->used - 1] == 0) has no TRUE row -- the loop body is never + * entered at all; + * idx0 (ret == MP_OKAY) has no FALSE row -- `ret` can only become non-OKAY + * INSIDE the loop body, from mp_get_rand_digit(), which is only reached + * once idx1 has already been TRUE. + * + * Both halves therefore hang off the same lever: the randomness mp_rand() + * consumes. Waiting for the real RNG to hand out an all-zero top digit is not + * an option for ASIL-D evidence (it is a 2^-64 lottery, and rule 3 of this + * suite forbids evidence that depends on a live draw), so this TU scripts + * the stream instead. + * + * HOW -- MACRO INTERPOSITION ON wc_RNG_GenerateBlock() + * --------------------------------------------------- + * random.h is included and the hook declared FIRST, then wc_RNG_GenerateBlock + * is #defined to the hook, and only then is wolfmath.c #included. Both of + * wolfmath.c's call sites (mp_rand()'s block fill and mp_get_rand_digit()'s + * single-digit redraw) are rewritten; every other translation unit in the + * archive keeps the real function. The hook's own body sits after an #undef, + * so it still reaches the real RNG when the script is idle. + * + * PER-CONDITION VECTOR MAP (all three rows in THIS binary) + * -------------------------------------------------------- + * WB_RNG_FIXED block fill = 0xa5.. -> top digit != 0 + * loop evaluated once: (T,F) -> decision FALSE + * WB_RNG_ZERO block fill = 0x00.. -> top digit == 0 + * 1st evaluation: (T,T) -> decision TRUE, body runs + * mp_get_rand_digit()'s redraw is then refused, so + * 2nd evaluation: (F,.) -> decision FALSE + * WB_RNG_REAL pass-through, so an ordinary mp_rand() is also measured. + * + * (T,T) against (F,.) is idx0's independence pair; (T,T) against (T,F) is + * idx1's. Both are completed inside this binary, which is what MC/DC needs: + * llvm-cov computes independence per binary and the harness only ORs the + * resulting bits by line:col. + * + * Build: compiled by the coverage runner'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 wolfmath.o removed + * (this TU supplies the instrumented wolfmath.c). NOT part of the wolfSSL + * build; not registered in tests/api. See tests/unit-mcdc/README.md. + */ + +#include +#include + +/* Declare the hook explicitly rather than relying on the macro to rewrite + * random.h's own prototype: if anything drags random.h in first, the include + * guard skips that prototype, the hook is never declared, and every call site + * inside wolfmath.c fails to compile -- which the harness scores as a SILENT + * SKIP (see the same note in mcdc_seed_rng.h). */ +static int wb_wm_rng_block(WC_RNG* rng, byte* out, word32 sz); + +#define wc_RNG_GenerateBlock(rng, out, sz) wb_wm_rng_block((rng), (out), (sz)) + +/* Pull wolfmath.c in verbatim so it is instrumented in THIS binary and sees + * the interposer above. */ +#include + +#undef wc_RNG_GenerateBlock + +#include + +static int wb_fail = 0; +#define WB_NOTE(msg) do { printf(" [wb] %s\n", (msg)); } while (0) + +#define WB_RNG_REAL 0 /* pass through to the real RNG */ +#define WB_RNG_FIXED 1 /* every draw is 0xa5.. (top digit non-zero) */ +#define WB_RNG_ZERO 2 /* first draw all-zero, every later draw fails */ + +static int wb_rng_mode = WB_RNG_REAL; +static int wb_rng_calls = 0; + +static int wb_wm_rng_block(WC_RNG* rng, byte* out, word32 sz) +{ + if (wb_rng_mode == WB_RNG_REAL) { + return wc_RNG_GenerateBlock(rng, out, sz); + } + + wb_rng_calls++; + + if (wb_rng_mode == WB_RNG_FIXED) { + XMEMSET(out, 0xa5, sz); + return 0; + } + + /* WB_RNG_ZERO: hand mp_rand() a block whose top digit is zero, then + * refuse the redraw mp_get_rand_digit() makes from inside the loop. */ + if (wb_rng_calls == 1) { + XMEMSET(out, 0, sz); + return 0; + } + return WC_NO_ERR_TRACE(RNG_FAILURE_E); +} + +#if !defined(WC_NO_RNG) && (!defined(NO_BIG_INT) || defined(WOLFSSL_SP_MATH)) + +static void wb_mp_rand_top_digit(void) +{ + mp_int a; + WC_RNG rng; + int ret; + /* Two digits is the smallest shape that still exercises the a->used + * indexing; mp_rand() rejects digits <= 0. */ + const int digits = 2; + + if (mp_init(&a) != MP_OKAY) { + WB_NOTE("mp_init failed; skipping mp_rand vectors"); + return; + } + if (wc_InitRng(&rng) != 0) { + WB_NOTE("wc_InitRng failed; skipping mp_rand vectors"); + mp_clear(&a); + return; + } + + /* Row 1 -- ordinary draw, pass-through: the loop is evaluated once and + * both operands come from the real RNG. */ + ret = mp_rand(&a, digits, &rng); + if (ret != MP_OKAY) { + WB_NOTE("mp_rand with the real RNG failed"); + wb_fail = 1; + } + + /* Row 2 -- (T,F): a scripted non-zero top digit, so the loop is entered + * zero times deterministically (the real-RNG row above cannot be relied + * on for this: it is a draw, not a vector). */ + wb_rng_mode = WB_RNG_FIXED; + wb_rng_calls = 0; + ret = mp_rand(&a, digits, &rng); + wb_rng_mode = WB_RNG_REAL; + if (ret != MP_OKAY) { + WB_NOTE("mp_rand with a scripted non-zero fill failed"); + wb_fail = 1; + } + + /* Row 3+4 -- (T,T) then (F,.): an all-zero block enters the loop, and the + * mp_get_rand_digit() redraw inside it is refused, so the very next + * evaluation of the same decision has ret != MP_OKAY. */ + wb_rng_mode = WB_RNG_ZERO; + wb_rng_calls = 0; + ret = mp_rand(&a, digits, &rng); + wb_rng_mode = WB_RNG_REAL; + if (ret != WC_NO_ERR_TRACE(RNG_FAILURE_E)) { + WB_NOTE("refused top-digit redraw did not propagate out of mp_rand"); + wb_fail = 1; + } + if (wb_rng_calls < 2) { + WB_NOTE("mp_rand never re-drew the top digit; loop was not entered"); + wb_fail = 1; + } + + wc_FreeRng(&rng); + mp_clear(&a); + + WB_NOTE("mp_rand top-digit retry loop pairs exercised"); +} + +#else + +static void wb_mp_rand_top_digit(void) +{ + /* Keep the interposer referenced so it is never an unused static in a + * variant that compiles mp_rand() out. */ + (void)&wb_wm_rng_block; + WB_NOTE("WC_NO_RNG or no big-int math; mp_rand vectors skipped"); +} + +#endif /* !WC_NO_RNG && (!NO_BIG_INT || WOLFSSL_SP_MATH) */ + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("wolfmath.c white-box supplement\n"); + wb_mp_rand_top_digit(); + printf("done (%s)\n", wb_fail ? "with skips" : "ok"); + /* Setup issues are surfaced as skips; a nonzero exit would make the + * suite discard this variant's coverage. */ + return 0; +} diff --git a/tests/unit-mcdc/test_xmss_fault_whitebox.c b/tests/unit-mcdc/test_xmss_fault_whitebox.c index 01e553b430..27c0c07a19 100644 --- a/tests/unit-mcdc/test_xmss_fault_whitebox.c +++ b/tests/unit-mcdc/test_xmss_fault_whitebox.c @@ -26,7 +26,7 @@ * * None of these decisions need a real XMSS keypair. Every guard chain here * is closed with the SMALLEST built-in parameter set (XMSS-SHA2_10_256, - * height 10 - available under every campaign variant since + * height 10 - available under every suite variant since * WOLFSSL_XMSS_MIN_HEIGHT defaults to 10) purely for its XmssParams fields * (sig_len/pk_len/sk_len); wc_XmssKey_SetParamStr() never generates keys, so * it costs nothing. key->state is then poked directly (this file #includes @@ -57,7 +57,7 @@ * This #includes wc_xmss.c directly (like the sibling wc_xmss_impl.c * white-box) so key->state and the other private fields are reachable. * - * Invocation: no arguments; runs the full sweep (the campaign's + * Invocation: no arguments; runs the full sweep (the * run_whitebox harness invokes the binary with none). */ @@ -908,7 +908,7 @@ static void wb_import_pub_raw_guard(void) * 907: same function's * "if (labelLen == 0 || labelLen > XMSS_MAX_LABEL_LEN)" * WOLF_PRIVATE_KEY_ID is auto-enabled by settings.h whenever - * HAVE_PK_CALLBACKS is set (true for every campaign variant here), so this + * HAVE_PK_CALLBACKS is set (true for every suite variant here), so this * is not a dead gate in practice. ********************************************/ static void wb_init_id_label(void) diff --git a/tests/unit-mcdc/test_xmss_hash_fault_whitebox.c b/tests/unit-mcdc/test_xmss_hash_fault_whitebox.c index 27f8956b69..fe7f511c58 100644 --- a/tests/unit-mcdc/test_xmss_hash_fault_whitebox.c +++ b/tests/unit-mcdc/test_xmss_hash_fault_whitebox.c @@ -22,7 +22,7 @@ /* * MC/DC hash-fault white-box supplement for wolfcrypt/src/wc_xmss_impl.c. * - * campaign/reports/xmss/GAPS.md is entirely error-propagation: + * suite/reports/xmss/the uncovered-condition report is entirely error-propagation: * * for (i = 1; (ret == 0) && (i < params->wots_len); i++) -- WOTS+ chain * for (i = 0; (ret == 0) && (i < params->d); i++) -- subtree loops @@ -91,7 +91,7 @@ static int wb_fail = 0; #define WB_POINTS 192 #define WB_DEADLINE_S 170 -/* WALL clock, not clock(): the campaign runs several variants concurrently and +/* WALL clock, not clock(): the harness runs several variants concurrently and * TEST_TIMEOUT is 600 s of WALL time. Under that contention CPU time accrues * far slower than wall time, so a CPU-time budget would sail past the timeout * -- and a timed-out white-box is scored as a SILENT SKIP that loses the whole @@ -450,6 +450,6 @@ int main(void) #endif printf("done (%s)\n", wb_fail ? "with failures" : "ok"); - /* A non-zero exit makes the campaign discard this binary's coverage. */ + /* A non-zero exit makes the harness discard this binary's coverage. */ return 0; } diff --git a/wolfcrypt/src/dsa.c b/wolfcrypt/src/dsa.c index ae1a1a7a8a..b40b0b974e 100644 --- a/wolfcrypt/src/dsa.c +++ b/wolfcrypt/src/dsa.c @@ -278,7 +278,16 @@ int wc_MakeDsaKey(WC_RNG *rng, DsaKey *dsa) if (err == MP_OKAY) #endif + { + /* Map an init failure to MP_INIT_E, the code the cleanup below uses to + * mean "nothing here was constructed". mp_init_multi() reports the + * backend's own error (MP_MEM from the heap backends) and, on failure, + * leaves every argument either cleared or never touched, so the + * cleanup must not mp_clear() any of them. */ err = mp_init_multi(&dsa->x, &dsa->y, tmpQ, NULL, NULL, NULL); + if (err != MP_OKAY) + err = MP_INIT_E; + } if (err == MP_OKAY) { do { @@ -320,7 +329,7 @@ int wc_MakeDsaKey(WC_RNG *rng, DsaKey *dsa) if (err == MP_OKAY) dsa->type = DSA_PRIVATE; - if (err != MP_OKAY) { + if ((err != MP_OKAY) && (err != WC_NO_ERR_TRACE(MP_INIT_E))) { mp_forcezero(&dsa->x); mp_clear(&dsa->y); } @@ -333,11 +342,13 @@ int wc_MakeDsaKey(WC_RNG *rng, DsaKey *dsa) #if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_NO_MALLOC) XFREE(cBuf, dsa->heap, DYNAMIC_TYPE_TMP_BUFFER); if (tmpQ != NULL) { - mp_clear(tmpQ); + if (err != WC_NO_ERR_TRACE(MP_INIT_E)) + mp_clear(tmpQ); XFREE(tmpQ, dsa->heap, DYNAMIC_TYPE_TMP_BUFFER); } #else - mp_clear(tmpQ); + if (err != WC_NO_ERR_TRACE(MP_INIT_E)) + mp_clear(tmpQ); #endif return err; @@ -420,7 +431,15 @@ int wc_MakeDsaParameters(WC_RNG *rng, int modulus_size, DsaKey *dsa) if (err == MP_OKAY) #endif + { + /* Map an init failure to MP_INIT_E: the cleanup below already keys off + * that code to skip mp_clear() on objects the failed init never + * constructed, but mp_init_multi() returns the backend's own error + * (MP_MEM from the heap backends), so the guard never fired. */ err = mp_init_multi(tmp, tmp2, &dsa->p, &dsa->q, &dsa->g, 0); + if (err != MP_OKAY) + err = MP_INIT_E; + } if (err == MP_OKAY) err = mp_read_unsigned_bin(tmp2, buf, (word32)(msize - qsize)); diff --git a/wolfcrypt/src/wc_xmss_impl.c b/wolfcrypt/src/wc_xmss_impl.c index 7cd27a90e4..688f19ba87 100644 --- a/wolfcrypt/src/wc_xmss_impl.c +++ b/wolfcrypt/src/wc_xmss_impl.c @@ -194,13 +194,17 @@ do { \ } while (0) /* Check whether index is valid. + * + * Written as "i >= 2^h - 1" for the same reason as IDX32_INVALID: the + * previous "((i + 1) >> (h - 32)) != 0" form wrapped at i == 2^64 - 1, the + * exhausted-key marker of an 8-byte index field (the h = 60 parameter sets). * * @param [in] i Index to check. * @param [in] c Count of bytes i was encoded in. * @param [in] h Full tree Height. */ #define IDX64_INVALID(i, c, h) \ - ((w64GetHigh32(w64Add32(i, 1, NULL)) >> ((h) - 32)) != 0) + w64GTE(i, w64Sub32(w64ShiftLeft(w64From32(0, 1), h), 1, NULL)) /* Set 64-bit index as hash address value for tree. * @@ -251,13 +255,19 @@ do { \ } while (0) /* Check whether 32-bit index is valid. + * + * Written as "i >= 2^h - 1" rather than "((i + 1) >> h) != 0": the two agree + * everywhere except at i == 0xFFFFFFFF, where i + 1 wraps to 0 and the shift + * form reports the index VALID. 0xFFFFFFFF is exactly the exhausted-key + * marker this file writes into a 4-byte index field, so the shift form let a + * retired key be signed with again. * * @param [in] i Index to check. * @param [in] c Count of bytes i was encoded in. * @param [in] h Full tree Height. */ #define IDX32_INVALID(i, c, h) \ - ((((i) + 1) >> (h)) != 0) + ((i) >= ((((word32)1U) << (h)) - 1U)) /* Set 32-bit index as hash address value for tree. * @@ -3714,13 +3724,17 @@ static void xmss_idx_decode(XmssIdx* idx, word8 c, const unsigned char* a) } /* Check whether index is valid. + * + * Written as "i >= 2^h - 1" rather than "((i + 1) >> h) != 0": with XmssIdx + * 32 bits wide (WOLFSSL_XMSS_MAX_HEIGHT <= 32) the increment wraps at the + * all-ones exhausted-key marker and the shift form reports it valid. * * @param [in] i Index to check. * @param [in] h Full tree Height. */ static int xmss_idx_invalid(XmssIdx i, word8 h) { - return ((i + 1) >> h) != 0; + return i >= ((((XmssIdx)1) << h) - 1); } /* Get tree and leaf index from index. diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index ec68a86093..5e84c4cd02 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -23766,46 +23766,40 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t aeskeywrap_test(void) /* Drive wc_AesKeyWrap_ex/wc_AesKeyUnWrap_ex directly with a caller Aes; the * KAT loop above already covers every vector via the key-based wrappers. */ { - Aes* aes = (Aes*)XMALLOC(sizeof(Aes), HEAP_HINT, DYNAMIC_TYPE_AES); - if (aes == NULL) - return WC_TEST_RET_ENC_NC; + /* Aes must not come from XMALLOC here: struct Aes carries ALIGN16 + * members, so _Alignof(Aes) is 16 under the default --enable-aligndata, + * while malloc() only guarantees 8 on 32-bit targets. A local gets the + * type's alignment from the compiler. See wc_AesSetIV(), which clang + * lowers to an alignment-qualified NEON store on armv8-a+crypto. */ + Aes aes[1]; XMEMSET(output, 0, sizeof(output)); XMEMSET(plain, 0, sizeof(plain)); - if (wc_AesInit(aes, HEAP_HINT, devId) != 0) { - XFREE(aes, HEAP_HINT, DYNAMIC_TYPE_AES); + if (wc_AesInit(aes, HEAP_HINT, devId) != 0) return WC_TEST_RET_ENC_NC; - } if (wc_AesSetKey(aes, test_wrap[0].kek, test_wrap[0].kekLen, NULL, AES_ENCRYPTION) != 0) { wc_AesFree(aes); - XFREE(aes, HEAP_HINT, DYNAMIC_TYPE_AES); return WC_TEST_RET_ENC_NC; } wrapSz = wc_AesKeyWrap_ex(aes, test_wrap[0].data, test_wrap[0].dataLen, output, sizeof(output), NULL); wc_AesFree(aes); if ( (wrapSz < 0) || (wrapSz != (int)test_wrap[0].verifyLen) || - XMEMCMP(output, test_wrap[0].verify, test_wrap[0].verifyLen) != 0) { - XFREE(aes, HEAP_HINT, DYNAMIC_TYPE_AES); + XMEMCMP(output, test_wrap[0].verify, test_wrap[0].verifyLen) != 0) return WC_TEST_RET_ENC_NC; - } - if (wc_AesInit(aes, HEAP_HINT, devId) != 0) { - XFREE(aes, HEAP_HINT, DYNAMIC_TYPE_AES); + if (wc_AesInit(aes, HEAP_HINT, devId) != 0) return WC_TEST_RET_ENC_NC; - } if (wc_AesSetKey(aes, test_wrap[0].kek, test_wrap[0].kekLen, NULL, AES_DECRYPTION) != 0) { wc_AesFree(aes); - XFREE(aes, HEAP_HINT, DYNAMIC_TYPE_AES); return WC_TEST_RET_ENC_NC; } plainSz = wc_AesKeyUnWrap_ex(aes, output, (word32)wrapSz, plain, sizeof(plain), NULL); wc_AesFree(aes); - XFREE(aes, HEAP_HINT, DYNAMIC_TYPE_AES); if ( (plainSz < 0) || (plainSz != (int)test_wrap[0].dataLen) || XMEMCMP(plain, test_wrap[0].data, test_wrap[0].dataLen) != 0) return WC_TEST_RET_ENC_NC; diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 7569a5cb2a..844e739cda 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -53,7 +53,10 @@ #ifdef HAVE_POLY1305 #include #endif -#if defined(HAVE_CHACHA) && defined(HAVE_POLY1305) && defined(OPENSSL_EXTRA) +#if defined(HAVE_CHACHA) && defined(HAVE_POLY1305) + /* Not OPENSSL_EXTRA-only: the TLS record layer calls the persistent-key + * helpers wc_ChaCha20Poly1305_{Encrypt,Decrypt}_ex(), so this header has + * to be visible whenever the ChaCha20-Poly1305 suites are built. */ #include #endif #ifdef HAVE_ARIA @@ -7010,14 +7013,15 @@ struct SystemCryptoPolicy { do { \ (err) = wolfSSL_ERR_peek_last_error(); \ if (wolfSSL_ERR_GET_LIB(err) == WOLFSSL_ERR_LIB_PEM && \ - wolfSSL_ERR_GET_REASON(err) == -WOLFSSL_PEM_R_NO_START_LINE_E) { \ + wolfSSL_ERR_GET_REASON(err) == \ + -WC_NO_ERR_TRACE(WOLFSSL_PEM_R_NO_START_LINE_E)) { \ unsigned long peekErr; \ do { \ wc_RemoveErrorNode(-1); \ peekErr = wolfSSL_ERR_peek_last_error(); \ } while (wolfSSL_ERR_GET_LIB(peekErr) == WOLFSSL_ERR_LIB_PEM && \ wolfSSL_ERR_GET_REASON(peekErr) == \ - -WOLFSSL_PEM_R_NO_START_LINE_E); \ + -WC_NO_ERR_TRACE(WOLFSSL_PEM_R_NO_START_LINE_E)); \ } \ } while(0) #else