From f6499686b1831942a91d58674101448aee3d1f6d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 10:50:20 +0200 Subject: [PATCH 01/60] tests: add an mc/dc white-box driver for src/dtls.c Every uncovered condition in dtls.c is in a file-static helper on the stateless path, where the public API fixes most arguments: no caller can ask CreateDtls12Cookie for a NULL secret or FindExtByType for a length that overruns its own vector. The dtls group already runs 87 of 103 and still leaves 46 of 56 uncovered, so the API limit is reached before the file is. Drives CreateDtls12Cookie, FindExtByType, ClientHelloSanityCheck, TlsCheckSupportedVersion and DtlsCidGetSize with paired vectors in one binary. dtls.c 10/56 -> 16/56. --- tests/include.am | 1 + tests/unit-mcdc/test_dtls_whitebox.c | 261 +++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 tests/unit-mcdc/test_dtls_whitebox.c diff --git a/tests/include.am b/tests/include.am index c19499a3f64..b56f2a9e8fa 100644 --- a/tests/include.am +++ b/tests/include.am @@ -144,6 +144,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_curve25519_whitebox.c \ tests/unit-mcdc/test_dh_fault_whitebox.c \ tests/unit-mcdc/test_dsa_fault_whitebox.c \ + tests/unit-mcdc/test_dtls_whitebox.c \ tests/unit-mcdc/test_ecc_fault_whitebox.c \ tests/unit-mcdc/test_ecc_whitebox.c \ tests/unit-mcdc/test_eccsi_fault_whitebox.c \ diff --git a/tests/unit-mcdc/test_dtls_whitebox.c b/tests/unit-mcdc/test_dtls_whitebox.c new file mode 100644 index 00000000000..fc4e52ac152 --- /dev/null +++ b/tests/unit-mcdc/test_dtls_whitebox.c @@ -0,0 +1,261 @@ +/* test_dtls_whitebox.c -- MC/DC white-box driver for src/dtls.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-1301, USA + */ + +/* WHY A WHITE-BOX FOR THIS FILE. + * + * Every uncovered condition in src/dtls.c sits in a file-static helper on the + * stateless (cookie exchange) path: CreateDtls12Cookie, CheckDtlsCookie, + * FindExtByType, TlsCheckSupportedVersion, ClientHelloSanityCheck, + * FindPskSuiteFromExt, SendStatelessReplyDtls13, DtlsCidGetSize. The public + * API reaches them only by feeding a crafted ClientHello through a real + * handshake, which fixes most of their arguments: a caller cannot ask + * CreateDtls12Cookie for a NULL secret, or FindExtByType for a length that + * overruns its own vector, because the code above them never produces those. + * The independence pairs therefore do not exist from outside, and the API + * tests that do reach this file were measured first -- the dtls group runs 87 + * of 103 and still leaves 46 of 56 conditions uncovered. + * + * This driver includes the .c and calls those static helpers directly, which is the + * same justification the campaign uses for the 200 static functions in tls.c. + * + * Rules this file must satisfy, each learned the hard way: + * - main() ALWAYS returns 0. A non-zero exit marks the variant failed and + * discards its entire profile, including the parts that worked. + * - Vectors come in pairs in ONE binary. A rejection with no accepting + * partner demonstrates no independence pair and adds no coverage. + * - It compiles under every variant of the module, with a skip stub for the + * configurations that do not build the file at all -- a driver that fails + * to compile is scored a silent skip, not a failure. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif +#include +#include + +#if defined(WOLFSSL_DTLS) && !defined(WOLFCRYPT_ONLY) + +#include +#include +#include +#include + +/* The unit under test. Its object is removed from the archive by the runner so + * these definitions are the ones that link. */ +#include + +static int g_checks; + +#define WB_NOTE(what) do { g_checks++; (void)(what); } while (0) + +/* ------------------------------------------------------------------ helpers */ + +/* A CH whose vectors are all empty and whose pv is caller supplied. Callers + * fill in only the field the vector under test depends on, so an unrelated + * field can never be what actually drove the branch. */ +static void wb_ch_init(WolfSSL_CH* ch, ProtocolVersion* pv) +{ + XMEMSET(ch, 0, sizeof(*ch)); + ch->pv = pv; +} + +/* ---------------------------------------------- CreateDtls12Cookie :237 */ +/* `if (secret == NULL || secretSz == 0)` + * + * Both operands need an independence pair, so three vectors: NULL secret with + * a non-zero size isolates operand 0, a real secret with size 0 isolates + * operand 1, and a real secret with a real size is the accepting partner that + * makes both pairs count. Without the third, neither operand has a pair and + * the rejections prove nothing. */ +static void wb_create_dtls12_cookie(WOLFSSL* ssl) +{ + ProtocolVersion pv; + WolfSSL_CH ch; + byte cookie[DTLS_COOKIE_SZ]; + static const byte secret[] = { 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a }; + const byte random[RAN_LEN] = { 0 }; + + pv.major = DTLS_MAJOR; + pv.minor = DTLSv1_2_MINOR; + wb_ch_init(&ch, &pv); + ch.random = random; + + WB_NOTE(CreateDtls12Cookie(ssl, &ch, NULL, sizeof(secret), cookie)); + WB_NOTE(CreateDtls12Cookie(ssl, &ch, secret, 0, cookie)); + WB_NOTE(CreateDtls12Cookie(ssl, &ch, secret, sizeof(secret), cookie)); +} + +/* ------------------------------------------------- FindExtByType :402 */ +/* `if (idx > exts.size || ...)` -- the overrun guard. + * + * A well formed extension block never trips this; the caller above always + * hands FindExtByType a vector whose declared length matches its buffer. The + * rejecting vector is a block whose inner extension length runs past the end + * of the block that contains it, which is exactly what a hostile ClientHello + * carries and what no in-tree caller constructs. */ +static void wb_find_ext_by_type(void) +{ + WolfSSL_ConstVector found; + WolfSSL_ConstVector exts; + int tlsxFound = 0; + /* type 0x002b (supported_versions), length 0x0002, body 2 bytes: valid. */ + static const byte ok[] = { 0x00, 0x2b, 0x00, 0x02, 0x03, 0x04 }; + /* Same header, but the length claims 0x00ff with only 2 bytes present. */ + static const byte overrun[] = { 0x00, 0x2b, 0x00, 0xff, 0x03, 0x04 }; + + exts.elements = ok; + exts.size = (word32)sizeof(ok); + WB_NOTE(FindExtByType(&found, TLSX_SUPPORTED_VERSIONS, exts, &tlsxFound)); + + exts.elements = overrun; + exts.size = (word32)sizeof(overrun); + WB_NOTE(FindExtByType(&found, TLSX_SUPPORTED_VERSIONS, exts, &tlsxFound)); +} + +/* ----------------------------------------- ClientHelloSanityCheck :984 */ +/* `if (ch->pv->minor != DTLSv1_2_MINOR && ch->pv->minor != DTLS_MINOR)` + * + * Three minors give both operands a pair: DTLSv1_2_MINOR takes the first + * operand false, DTLS_MINOR takes the first true and the second false, and a + * version that is neither takes both true. A handshake only ever produces the + * first, which is why this needs driving directly. */ +static void wb_client_hello_sanity(void) +{ + ProtocolVersion pv; + WolfSSL_CH ch; + const byte minors[3] = { DTLSv1_2_MINOR, DTLS_MINOR, 0x0f }; + size_t i; + + for (i = 0; i < sizeof(minors) / sizeof(minors[0]); i++) { + pv.major = DTLS_MAJOR; + pv.minor = minors[i]; + wb_ch_init(&ch, &pv); + WB_NOTE(ClientHelloSanityCheck(&ch, 0)); + } +} + +/* ------------------------------------------ TlsCheckSupportedVersion :541 */ +/* `if (!tlsxFound || tlsxSupportedVersions.elements == NULL)` + * + * Operand 0 is isolated by an extension block with no supported_versions in + * it; the accepting partner carries one. */ +static void wb_check_supported_version(WOLFSSL* ssl) +{ + ProtocolVersion pv; + WolfSSL_CH ch; + byte isTls13 = 0; + /* supported_versions carrying a single TLS 1.3 entry */ + static const byte with_sv[] = { 0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04 }; + /* server_name (0x0000) only: parses cleanly, but no supported_versions */ + static const byte without_sv[] = { 0x00, 0x00, 0x00, 0x01, 0x00 }; + + pv.major = DTLS_MAJOR; + pv.minor = DTLSv1_2_MINOR; + + wb_ch_init(&ch, &pv); + ch.extension.elements = without_sv; + ch.extension.size = (word32)sizeof(without_sv); + WB_NOTE(TlsCheckSupportedVersion(ssl, &ch, &isTls13)); + + wb_ch_init(&ch, &pv); + ch.extension.elements = with_sv; + ch.extension.size = (word32)sizeof(with_sv); + WB_NOTE(TlsCheckSupportedVersion(ssl, &ch, &isTls13)); +} + +/* ------------------------------------------------- DtlsCidGetSize :1146 */ +/* `if (ssl == NULL || size == NULL)` -- both operands, plus the accepting + * partner with a real ssl and a real out pointer. */ +static void wb_cid_get_size(WOLFSSL* ssl) +{ + unsigned int sz = 0; + + WB_NOTE(DtlsCidGetSize(NULL, &sz, 1)); + WB_NOTE(DtlsCidGetSize(ssl, NULL, 1)); + WB_NOTE(DtlsCidGetSize(ssl, &sz, 1)); + WB_NOTE(DtlsCidGetSize(ssl, &sz, 0)); +} + +/* ---------------------------------------------------------------- main */ + +int main(void) +{ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + /* Every bail point says so. An earlier revision returned 0 silently when + * the fixture could not be built, and the harness scored it 0/56 -- a + * driver that runs, exits clean and covers nothing looks exactly like a + * driver with nothing to say. It has to be possible to tell those apart + * from the log alone. */ + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("dtls white-box: wolfSSL_Init failed\n"); + goto done; + } + + /* CLIENT method deliberately. A server WOLFSSL needs a certificate and key + * before wolfSSL_new() will hand one back, and this driver has no business + * loading credentials to reach argument guards that never look at them. + * The static helpers driven here take ssl only to read heap/version fields. */ +#ifndef NO_WOLFSSL_CLIENT + ctx = wolfSSL_CTX_new(wolfDTLS_client_method()); +#else + ctx = wolfSSL_CTX_new(wolfDTLS_server_method()); +#endif + if (ctx == NULL) { + printf("dtls white-box: CTX_new failed\n"); + goto done; + } + wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_NONE, NULL); + ssl = wolfSSL_new(ctx); + if (ssl == NULL) { + printf("dtls white-box: wolfSSL_new failed\n"); + goto done; + } + + wb_create_dtls12_cookie(ssl); + wb_find_ext_by_type(); + wb_client_hello_sanity(); + wb_check_supported_version(ssl); + wb_cid_get_size(ssl); + + printf("dtls white-box: %d vectors driven\n", g_checks); + +done: + if (ssl != NULL) + wolfSSL_free(ssl); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + /* Always 0: a non-zero exit discards the whole variant's coverage. */ + return 0; +} + +#else /* !WOLFSSL_DTLS || WOLFCRYPT_ONLY */ + +int main(void) +{ + printf("dtls white-box: skipped (WOLFSSL_DTLS not built)\n"); + return 0; +} + +#endif From d0093e28aeda3e9f0799b0812ed46016fc318ed8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 12:08:04 +0200 Subject: [PATCH 02/60] tests: white-box for src/crl.c, and fix the include order in both crl.c measured 3 of 48: there is no crl test group, CRL lives in certman, and certman runs 11 of 36 on this option list. Drives CheckCertCRLCm's (serial == NULL || serialSz == 0) && serialHash == NULL guard with the four vectors MC/DC needs. crl.c 3/48 -> 6/48. Both drivers were including the target .c before wolfssl/options.h. config.h carries no feature macros, so the smoke build compiled them with WOLFSSL_DTLS and HAVE_CRL undefined: they took their skip stubs, exited 0, and were recorded as passing while testing nothing. options.h now comes first, which is the rule AGENTS.md already states. Under the smoke build they now drive 22 and 4 vectors respectively. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 3 + tests/unit-mcdc/test_crl_whitebox.c | 146 +++++++++++++++++++++++++++ tests/unit-mcdc/test_dtls_whitebox.c | 105 +++++++++++++++++-- 4 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 tests/unit-mcdc/test_crl_whitebox.c diff --git a/tests/include.am b/tests/include.am index b56f2a9e8fa..7df055aded2 100644 --- a/tests/include.am +++ b/tests/include.am @@ -140,6 +140,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_chacha20_poly1305_whitebox.c \ tests/unit-mcdc/test_chacha_whitebox.c \ tests/unit-mcdc/test_cmac_whitebox.c \ + tests/unit-mcdc/test_crl_whitebox.c \ tests/unit-mcdc/test_cryptocb_whitebox.c \ tests/unit-mcdc/test_curve25519_whitebox.c \ tests/unit-mcdc/test_dh_fault_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index de097482f0a..5a4322ce003 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -4,7 +4,9 @@ test_blake2s_whitebox test_chacha20_poly1305_whitebox test_chacha_whitebox test_cmac_whitebox +test_crl_whitebox test_cryptocb_whitebox +test_dtls_whitebox test_eccsi_fault_whitebox test_eccsi_whitebox test_ed25519_whitebox @@ -26,6 +28,7 @@ test_memory_whitebox test_mldsa_fault_whitebox test_mldsa_hash_fault_whitebox test_mlkem_fault_whitebox +test_ocsp_whitebox test_pkcs12_fault_whitebox test_poly1305_whitebox test_puf_gf_whitebox diff --git a/tests/unit-mcdc/test_crl_whitebox.c b/tests/unit-mcdc/test_crl_whitebox.c new file mode 100644 index 00000000000..96b40a1b912 --- /dev/null +++ b/tests/unit-mcdc/test_crl_whitebox.c @@ -0,0 +1,146 @@ +/* test_crl_whitebox.c -- MC/DC white-box driver for src/crl.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-1301, USA + */ + +/* WHY A WHITE-BOX FOR THIS FILE. + * + * src/crl.c measured 3 of 48 conditions at intake -- the lowest in the whole + * campaign -- and the reason is driver reach, not difficulty. There is no `crl` + * test group: CRL work lives inside the `certman` group, which runs 11 of its + * 36 tests on the campaign option list because the rest are gated on the + * OpenSSL compat layer that this option list deliberately excludes as a build + * fact. So most of crl.c is never entered at all from tests/api. + * + * What remains is reachable only by calling the file's own helpers with + * argument combinations the public API never produces: a revoked-cert lookup + * with neither a serial nor a serial hash, a CertManager with a missing-CRL + * callback installed but no CRL loaded, a serial that matches in length but + * not in content. This driver does that directly. + * + * Rules, same as the other drivers in this directory: + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Every rejecting vector has its accepting partner in THIS binary. + * - Bail paths print, so a driver that covers nothing is distinguishable + * from a driver that had nothing to say. + */ + +/* crl.c first and nothing before it: it includes settings.h, which picks up + * user_settings.h under the campaign builds and options.h under the + * --enable-all smoke build. Putting settings.h or options.h ahead of it breaks + * the header order for one of the two. */ +/* options.h FIRST, before any other wolfSSL header. Under the campaign's + * --enable-usersettings builds it just defines WOLFSSL_USER_SETTINGS and + * settings.h then reads user_settings.h; under the --enable-all smoke build it + * is where every feature macro actually lives. Getting this wrong is silent in + * the worst way: without it the smoke build compiled this driver with + * WOLFSSL_CRL undefined, so it took the skip stub, exited 0, and was recorded + * as a passing entry in smoke-expected.txt while testing nothing at all. */ +#include + +#include + +#include +#include + +#if defined(HAVE_CRL) && !defined(WOLFCRYPT_ONLY) && !defined(NO_CERTS) + +static int g_checks; +#define WB_NOTE(what) do { g_checks++; (void)(what); } while (0) + +/* ------------------------------------------------ CheckCertCRLCm :607 */ +/* `if ((serial == NULL || serialSz == 0) && serialHash == NULL)` + * + * Three operands, so four vectors. The inner OR short-circuits, so operand 1 + * (serialSz == 0) is only reachable with a non-NULL serial, and operand 2 + * (serialHash == NULL) is only reachable when the inner OR is true. + * + * serial=NULL sz=n hash=set -> op0 true, op2 false (op2 pair) + * serial=NULL sz=n hash=NULL -> op0 true, op2 true (op0 pair) + * serial=set sz=0 hash=NULL -> op0 false, op1 true, op2 true (op1 pair) + * serial=set sz=n hash=NULL -> op0 false, op1 false (accepting) + * + * No public caller can ask for this: wolfSSL_CertManagerCheckCRL and the + * internal CheckCertCRL both derive serial and serialSz from a DecodedCert + * that always has both, so the guard is dead from outside and its operands + * have no independence pair there. */ +static void wb_check_cert_crl_ex(WOLFSSL_CERT_MANAGER* cm) +{ + byte serial[] = { 0x01, 0x02, 0x03 }; + byte hash[SIGNER_DIGEST_SIZE]; + byte issuerHash[SIGNER_DIGEST_SIZE]; + + XMEMSET(hash, 0x11, sizeof(hash)); + XMEMSET(issuerHash, 0x22, sizeof(issuerHash)); + + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, NULL, (int)sizeof(serial), + hash, NULL, 0, NULL, cm)); + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, NULL, (int)sizeof(serial), + NULL, NULL, 0, NULL, cm)); + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, 0, + NULL, NULL, 0, NULL, cm)); + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, (int)sizeof(serial), + NULL, NULL, 0, NULL, cm)); +} + +/* ---------------------------------------------------------- main */ + +int main(void) +{ + WOLFSSL_CERT_MANAGER* cm = NULL; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("crl white-box: wolfSSL_Init failed\n"); + goto done; + } + cm = wolfSSL_CertManagerNew(); + if (cm == NULL) { + printf("crl white-box: CertManagerNew failed\n"); + goto done; + } + if (wolfSSL_CertManagerEnableCRL(cm, WOLFSSL_CRL_CHECK) + != WOLFSSL_SUCCESS) { + printf("crl white-box: EnableCRL failed\n"); + goto done; + } + if (cm->crl == NULL) { + printf("crl white-box: no CRL context after EnableCRL\n"); + goto done; + } + + wb_check_cert_crl_ex(cm); + + printf("crl white-box: %d vectors driven\n", g_checks); + +done: + if (cm != NULL) + wolfSSL_CertManagerFree(cm); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else /* !HAVE_CRL */ + +int main(void) +{ + printf("crl white-box: skipped (HAVE_CRL not built)\n"); + return 0; +} + +#endif diff --git a/tests/unit-mcdc/test_dtls_whitebox.c b/tests/unit-mcdc/test_dtls_whitebox.c index fc4e52ac152..8f9cba98211 100644 --- a/tests/unit-mcdc/test_dtls_whitebox.c +++ b/tests/unit-mcdc/test_dtls_whitebox.c @@ -46,22 +46,33 @@ * to compile is scored a silent skip, not a failure. */ -#ifdef HAVE_CONFIG_H - #include -#endif -#include +/* Pull dtls.c in verbatim so its file-static helpers are in scope and + * instrumented in THIS binary. Its object is removed from the archive by the + * runner, so these definitions are the ones that link. + * + * This include comes FIRST and nothing precedes it. dtls.c includes settings.h + * itself, which picks up user_settings.h under the campaign's + * -DWOLFSSL_USER_SETTINGS builds and options.h under the --enable-all smoke + * build. Including settings.h or options.h ahead of it gets the header order + * wrong for one of those two configurations: an earlier revision of this file + * did exactly that and failed to compile under gcc/--enable-all with + * "MAX_EX_DATA undeclared", behind a "No configuration for wolfSSL detected, + * check header order" warning. */ +/* options.h FIRST, before any other wolfSSL header. Under the campaign's + * --enable-usersettings builds it just defines WOLFSSL_USER_SETTINGS and + * settings.h then reads user_settings.h; under the --enable-all smoke build it + * is where every feature macro actually lives. Getting this wrong is silent in + * the worst way: without it the smoke build compiled this driver with + * WOLFSSL_DTLS undefined, so it took the skip stub, exited 0, and was recorded + * as a passing entry in smoke-expected.txt while testing nothing at all. */ #include -#if defined(WOLFSSL_DTLS) && !defined(WOLFCRYPT_ONLY) +#include -#include -#include #include #include -/* The unit under test. Its object is removed from the archive by the runner so - * these definitions are the ones that link. */ -#include +#if defined(WOLFSSL_DTLS) && !defined(WOLFCRYPT_ONLY) static int g_checks; @@ -195,6 +206,79 @@ static void wb_cid_get_size(WOLFSSL* ssl) WB_NOTE(DtlsCidGetSize(ssl, &sz, 0)); } +/* --------------------------------------- SendStatelessReplyDtls13 :851 */ +/* `if (!haveKS || !haveSA || !haveSG)` + * + * RFC 8446 section 9.2: a ClientHello that is not resuming must carry + * key_share, signature_algorithms AND supported_groups. The three flags are set + * purely by whether FindExtByType locates each extension in ch->extension, so + * all three operands are drivable by presenting extension blocks that omit one + * at a time -- no PSK, no handshake state, no IO. + * + * Four vectors, which is the minimum for MC/DC over a three-operand OR chain: + * all three present takes every operand false and is the accepting partner; + * then each of the three is dropped in turn, and because || short-circuits, the + * omitted one is the first operand that can be true in its vector. Dropping KS + * isolates operand 0; dropping SA needs KS present so operand 0 is false first; + * dropping SG needs both KS and SA present. + * + * A real handshake cannot produce these: a conforming client always sends all + * three, and a non-conforming one is rejected before this point by the record + * and cookie layers. That is the whole reason this lives in a white-box. */ +static void wb_stateless_reply_have_flags(WOLFSSL* ssl) +{ + ProtocolVersion pv; + WolfSSL_CH ch; + size_t i; + + /* Minimal well-formed extension bodies. Content beyond the header does not + * matter for the presence flags -- each parser is entered, and a parse + * failure exits before line 851 without touching the flags, so a vector + * that failed to parse would show up as a MISSING pair rather than a false + * pass. */ + static const byte ext_sa[] = { /* signature_algorithms 0x000d */ + 0x00, 0x0d, 0x00, 0x04, 0x00, 0x02, 0x08, 0x04 }; + static const byte ext_sg[] = { /* supported_groups 0x000a */ + 0x00, 0x0a, 0x00, 0x04, 0x00, 0x02, 0x00, 0x17 }; + static const byte ext_ks[] = { /* key_share 0x0033 */ + 0x00, 0x33, 0x00, 0x06, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00 }; + + /* one row per vector: which of KS / SA / SG to include */ + static const struct { byte ks, sa, sg; const char* what; } rows[] = { + { 1, 1, 1, "all three present -> every operand false" }, + { 0, 1, 1, "no key_share -> operand 0 true" }, + { 1, 0, 1, "no sig_algs -> operand 1 true" }, + { 1, 1, 0, "no supported_grps -> operand 2 true" }, + }; + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + byte exts[sizeof(ext_sa) + sizeof(ext_sg) + sizeof(ext_ks)]; + word32 n = 0; + byte suite[2]; + + if (rows[i].sa) { XMEMCPY(exts + n, ext_sa, sizeof(ext_sa)); + n += (word32)sizeof(ext_sa); } + if (rows[i].sg) { XMEMCPY(exts + n, ext_sg, sizeof(ext_sg)); + n += (word32)sizeof(ext_sg); } + if (rows[i].ks) { XMEMCPY(exts + n, ext_ks, sizeof(ext_ks)); + n += (word32)sizeof(ext_ks); } + + pv.major = DTLS_MAJOR; + pv.minor = DTLSv1_3_MINOR; + wb_ch_init(&ch, &pv); + /* An even suite size is required by the prologue; two bytes is the + * smallest legal ClientHello cipher-suite list. */ + suite[0] = 0x13; suite[1] = 0x01; /* TLS_AES_128_GCM_SHA256 */ + ch.cipherSuite.elements = suite; + ch.cipherSuite.size = 2; + ch.extension.elements = exts; + ch.extension.size = n; + + WB_NOTE(rows[i].what); + WB_NOTE(SendStatelessReplyDtls13(ssl, &ch)); + } +} + /* ---------------------------------------------------------------- main */ int main(void) @@ -237,6 +321,7 @@ int main(void) wb_client_hello_sanity(); wb_check_supported_version(ssl); wb_cid_get_size(ssl); + wb_stateless_reply_have_flags(ssl); printf("dtls white-box: %d vectors driven\n", g_checks); From 5f568aca5b5358d9f01be7145e9056f3ff550a66 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 12:10:47 +0200 Subject: [PATCH 03/60] tests: white-box for src/ocsp.c The ocsp group runs 3 of its 8 tests on the campaign option list, so most of ocsp.c is never entered from tests/api. GetOcspEntry's cache-match compares an entry against the request by issuer hash and issuer key hash; a caller coming through the public API builds both from the same certificate, so neither operand has a false case from outside and the loop body needs a seeded cache to execute at all. ocsp.c 7/47 -> 9/47. --- tests/include.am | 1 + tests/unit-mcdc/test_ocsp_whitebox.c | 153 +++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 tests/unit-mcdc/test_ocsp_whitebox.c diff --git a/tests/include.am b/tests/include.am index 7df055aded2..994634d5c6a 100644 --- a/tests/include.am +++ b/tests/include.am @@ -177,6 +177,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_mlkem_fault_whitebox.c \ tests/unit-mcdc/test_mlkem_poly_hash_fault_whitebox.c \ tests/unit-mcdc/test_pkcs12_fault_whitebox.c \ + tests/unit-mcdc/test_ocsp_whitebox.c \ tests/unit-mcdc/test_pkcs12_parse_whitebox.c \ tests/unit-mcdc/test_pkcs12_whitebox.c \ tests/unit-mcdc/test_pkcs7_arg_whitebox.c \ diff --git a/tests/unit-mcdc/test_ocsp_whitebox.c b/tests/unit-mcdc/test_ocsp_whitebox.c new file mode 100644 index 00000000000..18b07876dcd --- /dev/null +++ b/tests/unit-mcdc/test_ocsp_whitebox.c @@ -0,0 +1,153 @@ +/* test_ocsp_whitebox.c -- MC/DC white-box driver for src/ocsp.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-1301, USA + */ + +/* WHY A WHITE-BOX FOR THIS FILE. + * + * src/ocsp.c measured 7 of 47 conditions at intake. The `ocsp` group runs 3 of + * its 8 tests on the campaign option list -- the rest are gated on the OpenSSL + * compat layer this option list excludes as a build fact -- so most of the file + * is never entered from tests/api at all. + * + * The conditions that remain are lookup-table comparisons inside file-static + * helpers: matching a cached OCSP entry by issuer hash, and matching a status + * by serial. A caller coming through the public API always presents a + * consistent (issuerHash, serial) pair derived from a real DecodedCert, so the + * "same length, different content" and "different hash" cases -- exactly the + * ones an attacker controls -- have no independence pair from outside. + * + * Rules, same as the sibling drivers: + * - options.h FIRST, before any other wolfSSL header, or the smoke build + * compiles this with the feature macros undefined and it silently becomes + * a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Every rejecting vector has its accepting partner in THIS binary. + * - Bail paths print, so "covered nothing" is distinguishable from + * "had nothing to say". + */ + +#include + +#include + +#include +#include + +#if defined(HAVE_OCSP) && !defined(WOLFCRYPT_ONLY) && !defined(NO_CERTS) + +static int g_checks; +#define WB_NOTE(what) do { g_checks++; (void)(what); } while (0) + +/* ------------------------------------------------ FindStatus / entry :243 */ +/* `if (XMEMCMP((*entry)->issuerHash, request->issuerHash, ...) == 0 && ...)` + * + * Both operands need a pair, and the second is only reachable when the first + * matches. Three vectors: issuer hash differing (operand 0 false), issuer hash + * matching with the second discriminator differing (operand 0 true, operand 1 + * false), and both matching (the accepting partner). + * + * The public path builds request and entry from the same certificate, so + * outside this binary the two hashes are equal by construction and operand 0 + * has no false case at all. */ +static void wb_entry_match(WOLFSSL_OCSP* ocsp) +{ + OcspRequest req; + OcspEntry seeded; + OcspEntry* found = NULL; + + /* Seed the cache with one entry so the loop body executes. GetOcspEntry + * walks ocsp->ocspList and compares each node against the request; with an + * empty list the loop never runs and the comparison is never evaluated, + * which is exactly why driving this from the public API proves nothing. */ + XMEMSET(&seeded, 0, sizeof(seeded)); + XMEMSET(seeded.issuerHash, 0xAA, OCSP_DIGEST_SIZE); + XMEMSET(seeded.issuerKeyHash, 0xCC, OCSP_DIGEST_SIZE); + seeded.next = NULL; + ocsp->ocspList = &seeded; + + /* Vector 1: issuer hash differs -> operand 0 false, operand 1 not reached. + * Pairs with vector 3. */ + XMEMSET(&req, 0, sizeof(req)); + XMEMSET(req.issuerHash, 0xBB, OCSP_DIGEST_SIZE); + XMEMSET(req.issuerKeyHash, 0xCC, OCSP_DIGEST_SIZE); + WB_NOTE(GetOcspEntry(ocsp, &req, &found)); + + /* Vector 2: issuer hash equal, key hash differs -> operand 0 true, + * operand 1 false. Pairs with vector 3 on operand 1. */ + ocsp->ocspList = &seeded; + seeded.next = NULL; + XMEMSET(req.issuerHash, 0xAA, OCSP_DIGEST_SIZE); + XMEMSET(req.issuerKeyHash, 0xDD, OCSP_DIGEST_SIZE); + found = NULL; + WB_NOTE(GetOcspEntry(ocsp, &req, &found)); + + /* Vector 3: both equal -> the accepting partner that completes both pairs. */ + ocsp->ocspList = &seeded; + seeded.next = NULL; + XMEMSET(req.issuerKeyHash, 0xCC, OCSP_DIGEST_SIZE); + found = NULL; + WB_NOTE(GetOcspEntry(ocsp, &req, &found)); + + /* Detach the stack entry before the CertManager frees the list, or the + * teardown walks into this frame. GetOcspEntry appends a heap node when it + * finds no match, so drop whatever it linked on as well. */ + ocsp->ocspList = NULL; +} + +/* ---------------------------------------------------------- main */ + +int main(void) +{ + WOLFSSL_CERT_MANAGER* cm = NULL; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("ocsp white-box: wolfSSL_Init failed\n"); + goto done; + } + cm = wolfSSL_CertManagerNew(); + if (cm == NULL) { + printf("ocsp white-box: CertManagerNew failed\n"); + goto done; + } + if (wolfSSL_CertManagerEnableOCSP(cm, 0) != WOLFSSL_SUCCESS) { + printf("ocsp white-box: EnableOCSP failed\n"); + goto done; + } + + wb_entry_match(cm->ocsp); + + printf("ocsp white-box: %d vectors driven\n", g_checks); + +done: + if (cm != NULL) + wolfSSL_CertManagerFree(cm); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else /* !HAVE_OCSP */ + +int main(void) +{ + printf("ocsp white-box: skipped (HAVE_OCSP not built)\n"); + return 0; +} + +#endif From d9f8aa90a59ad9b145d2a21f9b733f4edbfcd25d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 16:59:13 +0200 Subject: [PATCH 04/60] tests: white-box the cipher suite table in src/internal.c InitSuites is a long table of 'tls1_2 && haveX && haveAES128' rows over twelve have* flags. No handshake can drive it: every caller derives those flags from what the build compiled in, so on one binary they are constant and no operand has an independence pair. A one-at-a-time sweep from an all-ones baseline, over six protocol versions and both sides, gives the pair for every operand of every row in n+1 calls instead of 2^n. 156 calls, internal.c 532/1730 -> 573/1722. Also extends the crl driver to CheckCertCRLCm's cm != NULL && cm->cbMissingCRL and cm != NULL && cm->crlCb guards, whose operand 0 is true by construction from wolfSSL_CertManagerCheckCRL. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + tests/unit-mcdc/test_crl_whitebox.c | 27 +++ .../unit-mcdc/test_internal_suites_whitebox.c | 174 ++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 tests/unit-mcdc/test_internal_suites_whitebox.c diff --git a/tests/include.am b/tests/include.am index 994634d5c6a..7e9b7d8a569 100644 --- a/tests/include.am +++ b/tests/include.am @@ -165,6 +165,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_integer_fault_whitebox.c \ tests/unit-mcdc/test_integer_whitebox.c \ tests/unit-mcdc/test_kdf_hash_fault_whitebox.c \ + tests/unit-mcdc/test_internal_suites_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 \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index 5a4322ce003..e2c66e56dd6 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -19,6 +19,7 @@ test_hpke_fault_whitebox test_hpke_whitebox test_integer_fault_whitebox test_integer_whitebox +test_internal_suites_whitebox test_lms_bds_whitebox test_lms_fault_whitebox test_lms_hash_fault_whitebox diff --git a/tests/unit-mcdc/test_crl_whitebox.c b/tests/unit-mcdc/test_crl_whitebox.c index 96b40a1b912..96609851768 100644 --- a/tests/unit-mcdc/test_crl_whitebox.c +++ b/tests/unit-mcdc/test_crl_whitebox.c @@ -99,6 +99,32 @@ static void wb_check_cert_crl_ex(WOLFSSL_CERT_MANAGER* cm) NULL, NULL, 0, NULL, cm)); } + +/* --------------------------------------------- CheckCertCRLCm :668, :686 */ +/* `if (cm != NULL && cm->cbMissingCRL)` and + * `if (cm != NULL && cm->crlCb && ...)` + * + * Both operands of each need a pair. A CertManager reaches this code only + * through wolfSSL_CertManagerCheckCRL, which never passes NULL, so operand 0 + * is true by construction from outside and its false case is unreachable + * there. Calling directly gives both: once with cm NULL, once with a real cm + * that has no callback installed, once with the callback installed. */ +static void wb_missing_crl_callbacks(WOLFSSL_CERT_MANAGER* cm) +{ + byte serial[] = { 0x0a, 0x0b }; + byte issuerHash[SIGNER_DIGEST_SIZE]; + + XMEMSET(issuerHash, 0x33, sizeof(issuerHash)); + + /* cm NULL: operand 0 false for both decisions. */ + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, (int)sizeof(serial), + NULL, NULL, 0, NULL, NULL)); + + /* real cm, no callbacks installed: operand 0 true, operand 1 false. */ + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, (int)sizeof(serial), + NULL, NULL, 0, NULL, cm)); +} + /* ---------------------------------------------------------- main */ int main(void) @@ -125,6 +151,7 @@ int main(void) } wb_check_cert_crl_ex(cm); + wb_missing_crl_callbacks(cm); printf("crl white-box: %d vectors driven\n", g_checks); diff --git a/tests/unit-mcdc/test_internal_suites_whitebox.c b/tests/unit-mcdc/test_internal_suites_whitebox.c new file mode 100644 index 00000000000..568395708b3 --- /dev/null +++ b/tests/unit-mcdc/test_internal_suites_whitebox.c @@ -0,0 +1,174 @@ +/* test_internal_suites_whitebox.c -- MC/DC white-box driver for the cipher + * suite table in src/internal.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-1301, USA + */ + +/* WHY A WHITE-BOX FOR THIS. + * + * InitSuites() is one long table of decisions of the shape + * + * if (tls1_2 && haveECC && haveAES128) { suites[idx++] = ...; } + * + * over twelve have* parameters and a protocol version. It is the densest + * single cluster of uncovered conditions in internal.c. + * + * A handshake cannot drive it. Every caller -- InitSSL_Suites, + * InitCtxSuitesWithMutex, AllocateSuites -- derives the have* flags from what + * the build compiled in and what the CTX was loaded with, so on any one binary + * they are near enough constant: a build with ECC gives haveECC=1 in every + * call it ever makes. The operands therefore have no independence pair from + * outside, however many handshakes are run. + * + * The vector set is a one-at-a-time sweep from an all-ones baseline. For a + * decision `A && B && C`, the all-ones call takes it true, and the call with + * exactly one flag cleared takes that operand false with the others true -- + * which is precisely the independence pair MC/DC asks for, for every operand + * of every decision in the table, from n+1 calls rather than 2^n. + * + * The version is swept too, because tls, tls1_2 and tls1_3 are derived from + * pv and appear as the leading operand of most rows. + * + * Rules, same as the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Bail paths print, so "covered nothing" is distinguishable from + * "had nothing to say". + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) + +static int g_calls; + +/* The twelve have* parameters, in the order InitSuites takes them. Index into + * this to clear exactly one per call. */ +enum { + F_RSA = 0, F_PSK, F_DH, F_ECDSASIG, F_ECC, F_STATICRSA, F_STATICECC, + F_ANON, F_NULL, F_AES128, F_SHA1, F_RC4, F_COUNT +}; + +static const char* const kFlagName[F_COUNT] = { + "haveRSA", "havePSK", "haveDH", "haveECDSAsig", "haveECC", "haveStaticRSA", + "haveStaticECC", "haveAnon", "haveNull", "haveAES128", "haveSHA1", "haveRC4" +}; + +static void wb_call(ProtocolVersion pv, const word16 f[F_COUNT], int side) +{ + /* Suites is large and InitSuites appends from idx 0, so a fresh zeroed + * struct per call keeps each vector independent of the last. */ + Suites* suites = (Suites*)XMALLOC(sizeof(Suites), NULL, + DYNAMIC_TYPE_SUITES); + if (suites == NULL) + return; + XMEMSET(suites, 0, sizeof(*suites)); + InitSuites(suites, pv, 2048 / 8, + f[F_RSA], f[F_PSK], f[F_DH], f[F_ECDSASIG], f[F_ECC], + f[F_STATICRSA], f[F_STATICECC], f[F_ANON], f[F_NULL], + f[F_AES128], f[F_SHA1], f[F_RC4], side); + g_calls++; + XFREE(suites, NULL, DYNAMIC_TYPE_SUITES); +} + +static void wb_sweep_version(ProtocolVersion pv, const char* what) +{ + word16 f[F_COUNT]; + int i, side; + const int sides[2] = { WOLFSSL_CLIENT_END, WOLFSSL_SERVER_END }; + + for (side = 0; side < 2; side++) { + /* All ones: takes every AND chain in the table true, and is the + * accepting partner for every operand cleared below. */ + for (i = 0; i < F_COUNT; i++) + f[i] = 1; + wb_call(pv, f, sides[side]); + + /* One at a time: this operand false, all others true. */ + for (i = 0; i < F_COUNT; i++) { + int j; + for (j = 0; j < F_COUNT; j++) + f[j] = 1; + f[i] = 0; + wb_call(pv, f, sides[side]); + } + } + (void)what; + (void)kFlagName; +} + +/* ---------------------------------------------------------- main */ + +int main(void) +{ + ProtocolVersion pv; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal suites white-box: wolfSSL_Init failed\n"); + goto done; + } + + /* tls is false, so the leading operand of every `tls && ...` row is + * exercised false with the rest true. */ + pv.major = SSLv3_MAJOR; pv.minor = SSLv3_MINOR; + wb_sweep_version(pv, "SSLv3"); + + /* tls true, tls1_2 false. */ + pv.major = SSLv3_MAJOR; pv.minor = TLSv1_MINOR; + wb_sweep_version(pv, "TLSv1.0"); + + /* tls1_2 true, tls1_3 false: the bulk of the table. */ + pv.major = SSLv3_MAJOR; pv.minor = TLSv1_2_MINOR; + wb_sweep_version(pv, "TLSv1.2"); + + /* tls1_3 true. */ + pv.major = SSLv3_MAJOR; pv.minor = TLSv1_3_MINOR; + wb_sweep_version(pv, "TLSv1.3"); + +#ifdef WOLFSSL_DTLS + /* The DTLS branch inverts the minor comparison, so both DTLS versions are + * needed to pair the operands inside `pv.major == DTLS_MAJOR`. */ + pv.major = DTLS_MAJOR; pv.minor = DTLSv1_2_MINOR; + wb_sweep_version(pv, "DTLSv1.2"); + pv.major = DTLS_MAJOR; pv.minor = DTLS_MINOR; + wb_sweep_version(pv, "DTLSv1.3"); +#endif + + printf("internal suites white-box: %d InitSuites calls\n", g_calls); + +done: + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else /* WOLFCRYPT_ONLY || NO_TLS */ + +int main(void) +{ + printf("internal suites white-box: skipped (TLS not built)\n"); + return 0; +} + +#endif From 404492c13531f1f8c06e5bf57e41f0f04cb40a66 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 17:03:39 +0200 Subject: [PATCH 05/60] tests: group the orphaned ocsp tests and bound-check DecodeUrl hosts Eight ocsp tests were registered with a bare TEST_DECL and no group, and ApiTest_RunGroup selects on group != NULL, so none of them has ever run under --group. Among them are both OCSP-to-CRL fallback tests and the DecodeUrl CR/LF injection test. Grouping them takes the ocsp group from 3 of 8 running to 9 of 17, and src/wolfio.c from 18/87 to 43/87. Adds test_wolfIO_DecodeUrl_host_bounds for the two host-parsing loops, whose four operands are each ended by a different vector: a bracketed IPv6 literal with no closing bracket, one cut short by a NUL, a host running to the end of the buffer, and a host longer than the item cap, with well-formed URLs as the accepting partners. Those are attacker-controlled shapes that no in-tree caller produces. --- tests/api.c | 17 ++++---- tests/api/test_ocsp.c | 91 +++++++++++++++++++++++++++++++++++++++++++ tests/api/test_ocsp.h | 1 + 3 files changed, 101 insertions(+), 8 deletions(-) diff --git a/tests/api.c b/tests/api.c index 3618a957733..ed4ca862ae0 100644 --- a/tests/api.c +++ b/tests/api.c @@ -42104,14 +42104,15 @@ TEST_CASE testCases[] = { TEST_DECL_GROUP("ocsp", test_ocsp_certid_dup), TEST_DECL_GROUP("ocsp", test_ocsp_resp_find_status_serial_prefix), TEST_DECL(test_ocsp_tls_cert_cb), - TEST_DECL(test_ocsp_status_request_v2_multi_revoked_single), - TEST_DECL(test_ocsp_cert_unknown_crl_fallback), - TEST_DECL(test_ocsp_cert_unknown_crl_fallback_nonleaf), - TEST_DECL(test_ocsp_no_url_policy), - TEST_DECL(test_tls13_nonblock_ocsp_low_mfl), - TEST_DECL(test_ocsp_ctx_request_cache), - TEST_DECL(test_ocsp_responder), - TEST_DECL(test_wolfIO_DecodeUrl_crlf_reject), + TEST_DECL_GROUP("ocsp", test_ocsp_status_request_v2_multi_revoked_single), + TEST_DECL_GROUP("ocsp", test_ocsp_cert_unknown_crl_fallback), + TEST_DECL_GROUP("ocsp", test_ocsp_cert_unknown_crl_fallback_nonleaf), + TEST_DECL_GROUP("ocsp", test_ocsp_no_url_policy), + TEST_DECL_GROUP("ocsp", test_tls13_nonblock_ocsp_low_mfl), + TEST_DECL_GROUP("ocsp", test_ocsp_ctx_request_cache), + TEST_DECL_GROUP("ocsp", test_ocsp_responder), + TEST_DECL_GROUP("ocsp", test_wolfIO_DecodeUrl_crlf_reject), + TEST_DECL_GROUP("ocsp", test_wolfIO_DecodeUrl_host_bounds), TEST_TLS_DECLS, TEST_TLS_BOUNDS_DECLS, TEST_TLS_MSGTYPE_DECLS, diff --git a/tests/api/test_ocsp.c b/tests/api/test_ocsp.c index ccac1491fb9..1fa553b915e 100644 --- a/tests/api/test_ocsp.c +++ b/tests/api/test_ocsp.c @@ -2374,6 +2374,97 @@ int test_ocsp_responder(void) #if defined(HAVE_HTTP_CLIENT) /* A peer-supplied AIA/CRL URL must not be able to smuggle CR/LF into the * outbound OCSP/CRL HTTP request (header injection / request splitting). */ +/* MC/DC vectors for wolfIO_DecodeUrl's host-parsing loops. + * + * The bracketed-IPv6 loop and the plain-host loop each carry four operands: + * + * while (i < MAX_URL_ITEM_SIZE-1 && cur < urlSz && url[cur] != 0 && + * url[cur] != ']') (or != ':' && != '/') + * + * and the bracket path is followed by + * + * if (cur >= urlSz || url[cur] != ']') + * + * The existing CR/LF tests drive the injection guard but always terminate the + * host normally, so three of the four loop operands never take the value that + * ends the loop, and the unterminated-bracket rejection never fires. Each + * vector below stops the loop on a DIFFERENT operand, which is what gives each + * one its independence pair; the well-formed URLs at the end are the accepting + * partners. + * + * These are the shapes an attacker controls -- an unterminated literal, a host + * that runs to the end of the buffer, a host longer than the item cap -- and + * none of them is produced by any in-tree caller, which is why they were + * uncovered. */ +int test_wolfIO_DecodeUrl_host_bounds(void) +{ + EXPECT_DECLS; +#if defined(HAVE_OCSP) || defined(HAVE_CRL_IO) + /* wolfio.c keeps MAX_URL_ITEM_SIZE private (src/wolfio.c), so mirror the + * value here rather than reaching into the implementation. Only the + * "longer than the cap" vector depends on it, and it just has to exceed + * the real cap for the first loop operand to go false. */ + #define WOLFIO_URL_ITEM_CAP 80 + char domainName[WOLFIO_URL_ITEM_CAP]; + char path[WOLFIO_URL_ITEM_CAP]; + word16 port; + int i; + char longHost[WOLFIO_URL_ITEM_CAP + 32]; + /* bracketed IPv6 with no closing ']' -- loop ends on cur < urlSz going + * false, then the terminator check rejects. */ + const char* v6Unterminated = "http://[::1"; + /* bracketed IPv6 whose host hits a NUL before ']' -- loop ends on + * url[cur] != 0 going false. */ + static const char v6Nul[] = "http://[::1\0]/ocsp"; + /* well-formed bracketed literal: loop ends on url[cur] != ']' going + * false, and the terminator check accepts. The accepting partner. */ + const char* v6Good = "http://[::1]:8080/ocsp"; + /* plain host running to the end of the buffer with no ':' or '/' -- loop + * ends on cur < urlSz. */ + const char* hostEof = "http://ocsp.example.com"; + /* plain host terminated by '/' and by ':' respectively: the accepting + * partners for the last two operands of the plain-host loop. */ + const char* hostSlash = "http://ocsp.example.com/ocsp"; + const char* hostColon = "http://ocsp.example.com:8080/ocsp"; + + /* A host longer than the item cap ends the loop on the FIRST operand, + * i < MAX_URL_ITEM_SIZE-1, which nothing else in the suite reaches. */ + XSTRNCPY(longHost, "http://", sizeof(longHost)); + for (i = 7; i < (int)sizeof(longHost) - 2; i++) + longHost[i] = 'a'; + longHost[sizeof(longHost) - 2] = '/'; + longHost[sizeof(longHost) - 1] = '\0'; + + /* --- rejecting vectors, one per loop-exit operand --- */ + ExpectIntLT(wolfIO_DecodeUrl(v6Unterminated, (int)XSTRLEN(v6Unterminated), + domainName, path, &port), 0); + ExpectIntLT(wolfIO_DecodeUrl(v6Nul, (int)sizeof(v6Nul) - 1, + domainName, path, &port), 0); + + /* --- accepting partners --- */ + ExpectIntEQ(wolfIO_DecodeUrl(v6Good, (int)XSTRLEN(v6Good), + domainName, path, &port), 0); + ExpectIntEQ(wolfIO_DecodeUrl(hostEof, (int)XSTRLEN(hostEof), + domainName, path, &port), 0); + ExpectIntEQ(wolfIO_DecodeUrl(hostSlash, (int)XSTRLEN(hostSlash), + domainName, path, &port), 0); + ExpectIntEQ(wolfIO_DecodeUrl(hostColon, (int)XSTRLEN(hostColon), + domainName, path, &port), 0); + + /* Cap-length host: whatever the parser decides, the point is that the + * first loop operand goes false, so no return value is asserted beyond + * it not crashing. */ + (void)wolfIO_DecodeUrl(longHost, (int)XSTRLEN(longHost), + domainName, path, &port); + + /* Null and zero-length arguments: both operands of + * `if (url == NULL || urlSz == 0)`, with the accepting partner above. */ + ExpectIntLT(wolfIO_DecodeUrl(NULL, 10, domainName, path, &port), 0); + ExpectIntLT(wolfIO_DecodeUrl(hostSlash, 0, domainName, path, &port), 0); +#endif + return EXPECT_RESULT(); +} + int test_wolfIO_DecodeUrl_crlf_reject(void) { EXPECT_DECLS; diff --git a/tests/api/test_ocsp.h b/tests/api/test_ocsp.h index 5b1b4000c8b..a9e85ba908f 100644 --- a/tests/api/test_ocsp.h +++ b/tests/api/test_ocsp.h @@ -40,5 +40,6 @@ int test_ocsp_responder(void); int test_ocsp_ancestor_responder_rejected(void); int test_ocsp_forged_responder_cert_rejected(void); int test_wolfIO_DecodeUrl_crlf_reject(void); +int test_wolfIO_DecodeUrl_host_bounds(void); #endif /* WOLFSSL_TEST_OCSP_H */ From ef2eac6bf38009219d146b37017ca56337814f06 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 17:14:50 +0200 Subject: [PATCH 06/60] tests: drive the crl missing-callback guards to a true decision The previous vectors reached CheckCertCRLCm's cm != NULL && cm->cbMissingCRL and cm != NULL && cm->crlCb chains and covered nothing, because every vector took the decision false: once by short-circuit on a NULL cm, once because no callback was installed. An operand that changes value without changing the decision outcome has no independence pair. Installs both callbacks so the decisions go true, and drives the error callback's own return value both ways for the third operand of the :686 chain, plus a url longer than the 256-byte stack buffer for the copy guard inside the :668 body. crl.c 6/48 -> 11/48. --- tests/unit-mcdc/test_crl_whitebox.c | 91 +++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/tests/unit-mcdc/test_crl_whitebox.c b/tests/unit-mcdc/test_crl_whitebox.c index 96609851768..6e311a9e2f5 100644 --- a/tests/unit-mcdc/test_crl_whitebox.c +++ b/tests/unit-mcdc/test_crl_whitebox.c @@ -102,27 +102,98 @@ static void wb_check_cert_crl_ex(WOLFSSL_CERT_MANAGER* cm) /* --------------------------------------------- CheckCertCRLCm :668, :686 */ /* `if (cm != NULL && cm->cbMissingCRL)` and - * `if (cm != NULL && cm->crlCb && ...)` + * `if (cm != NULL && cm->crlCb && cm->crlCb(ret, crl, cm, cm->crlCbCtx))` * - * Both operands of each need a pair. A CertManager reaches this code only - * through wolfSSL_CertManagerCheckCRL, which never passes NULL, so operand 0 - * is true by construction from outside and its false case is unreachable - * there. Calling directly gives both: once with cm NULL, once with a real cm - * that has no callback installed, once with the callback installed. */ + * Reached only when foundEntry == 0, i.e. no CRL matched -- the CRL_MISSING + * path, which is where a caller is told the check could not be completed. + * + * An earlier version of this driver passed cm == NULL and cm != NULL with no + * callbacks installed, and gained nothing. Both evaluations take the decision + * FALSE -- once by short-circuit and once because the callback pointer is + * NULL -- so operand 0 changed value without changing the outcome, which is + * not an independence pair. MC/DC needs the DECISION to flip, so at least one + * vector has to install the callback and drive the decision true. + * + * The full set, per decision: + * cm == NULL -> op0 false, decision false + * cm != NULL, callback NULL -> op0 true, op1 false, decision false + * cm != NULL, callback installed -> op0 true, op1 true, decision TRUE + * + * and for :686 a third operand, the callback's own return value, needs one + * vector returning zero and one returning non-zero. + * + * No public caller can produce the cm == NULL case: every path into this + * function comes from a CertManager. */ + +static int g_missingCalled; +static int g_crlCbCalled; +static int g_crlCbResult; + +static void wb_missing_crl_cb(const char* url) +{ + g_missingCalled++; + (void)url; +} + +static int wb_crl_err_cb(int ret, WOLFSSL_CRL* crl, WOLFSSL_CERT_MANAGER* cm, + void* ctx) +{ + g_crlCbCalled++; + (void)ret; (void)crl; (void)cm; (void)ctx; + return g_crlCbResult; /* drives operand 2 of the :686 chain */ +} + static void wb_missing_crl_callbacks(WOLFSSL_CERT_MANAGER* cm) { byte serial[] = { 0x0a, 0x0b }; byte issuerHash[SIGNER_DIGEST_SIZE]; + const char* url = "http://crl.example.com/root.crl"; XMEMSET(issuerHash, 0x33, sizeof(issuerHash)); - /* cm NULL: operand 0 false for both decisions. */ + /* 1. cm NULL: operand 0 false for both decisions. */ WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, (int)sizeof(serial), - NULL, NULL, 0, NULL, NULL)); + NULL, (const byte*)url, (int)XSTRLEN(url), NULL, + NULL)); - /* real cm, no callbacks installed: operand 0 true, operand 1 false. */ + /* 2. real cm, no callbacks: operand 0 true, operand 1 false. */ + cm->cbMissingCRL = NULL; + cm->crlCb = NULL; WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, (int)sizeof(serial), - NULL, NULL, 0, NULL, cm)); + NULL, (const byte*)url, (int)XSTRLEN(url), NULL, + cm)); + + /* 3. both callbacks installed, error cb returns 0: takes :668 TRUE (the + * partner that gives operands 0 and 1 their pairs) and :686 to its third + * operand, false. */ + cm->cbMissingCRL = wb_missing_crl_cb; + cm->crlCb = wb_crl_err_cb; + g_crlCbResult = 0; + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, (int)sizeof(serial), + NULL, (const byte*)url, (int)XSTRLEN(url), NULL, + cm)); + + /* 4. error cb returns non-zero: :686 operand 2 true, decision TRUE, which + * is the override-the-CRL-error path. */ + g_crlCbResult = 1; + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, (int)sizeof(serial), + NULL, (const byte*)url, (int)XSTRLEN(url), NULL, + cm)); + + /* 5. a url longer than the 256-byte stack buffer takes the "CRL url too + * long" arm of the copy guard inside the :668 body, which the short url + * above leaves unexercised. */ + { + char longUrl[300]; + XMEMSET(longUrl, 'u', sizeof(longUrl) - 1); + longUrl[sizeof(longUrl) - 1] = '\0'; + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, + (int)sizeof(serial), NULL, (const byte*)longUrl, + (int)sizeof(longUrl) - 1, NULL, cm)); + } + + cm->cbMissingCRL = NULL; + cm->crlCb = NULL; } /* ---------------------------------------------------------- main */ From d73917c85b849753c4c64168aed2d7498a8cd2f2 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 17:31:54 +0200 Subject: [PATCH 07/60] tests: drive the crl load and store paths with a real CRL BufferLoadCRL and BufferStoreCRL are argument-validated with wide OR chains and then branch on DER vs PEM. Every in-tree caller passes a real CRL and a real type, so the rejecting side of each operand is unreachable from outside, while the accepting side needs a genuinely parsed entry: a hand-built one has no toBeSigned or signature, so it can only take the :1036 guard true and leaves the whole store path uncovered. Loads certs/crl/crl.der through the real loader and lets the CRL context own the entry, then stores it as DER, as PEM, into an undersized buffer and with a size query. crl.c 11/48 -> 18/48. --- tests/unit-mcdc/test_crl_whitebox.c | 101 ++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/unit-mcdc/test_crl_whitebox.c b/tests/unit-mcdc/test_crl_whitebox.c index 6e311a9e2f5..49102e23bef 100644 --- a/tests/unit-mcdc/test_crl_whitebox.c +++ b/tests/unit-mcdc/test_crl_whitebox.c @@ -196,6 +196,106 @@ static void wb_missing_crl_callbacks(WOLFSSL_CERT_MANAGER* cm) cm->crlCb = NULL; } + +/* ------------------------------- BufferLoadCRL :913 / BufferStoreCRL :1011, + * :1036, :1044, :1081, :1105, :1146 */ +/* The load and store entry points are argument-validated with wide OR chains + * + * if (crl == NULL || buff == NULL || sz <= 0) + * if (crl == NULL || inOutSz == NULL) + * if (ent == NULL || tbs == NULL || tbsSz == 0 || sig == NULL || sigSz == 0) + * + * and then branch on the encoding + * + * if (ret == 0 && type == WOLFSSL_FILETYPE_ASN1) + * else if (ret == 0 && type == WOLFSSL_FILETYPE_PEM) + * + * Every in-tree caller passes a real CRL and a real type, so the rejecting + * side of each operand is unreachable from outside, while the ACCEPTING side + * needs a genuinely parsed CRL entry -- a hand-built one does not have + * toBeSigned or signature populated, so it can only ever take the :1036 guard + * true and would leave the whole store path uncovered. + * + * That is the lesson from the ocsp sibling: build the fixture with the real + * loader and let the library own it, rather than assembling structs on the + * stack and linking them into lists the library frees. + * + * Both encodings are driven because the type operand of :1081 and :1146 needs + * a pair, and certs/crl carries both a DER and a PEM of the same CRL. */ +static void wb_buffer_load_store(WOLFSSL_CERT_MANAGER* cm) +{ + static const char* kDer = "certs/crl/crl.der"; + static const char* kPem = "certs/crl/crl.pem"; + byte der[4096]; + long n = 0; + int loaded = 0; + XFILE f; + + /* ---- BufferLoadCRL :913, one vector per operand plus the partner ---- */ + f = XFOPEN(kDer, "rb"); + if (f != XBADFILE) { + n = (long)XFREAD(der, 1, sizeof(der), f); + XFCLOSE(f); + } + if (n <= 0) { + /* Without the file the accepting partner does not exist, so the + * rejecting vectors below would prove nothing. Say so rather than + * report a pass. */ + printf("crl white-box: %s unreadable, load/store vectors skipped\n", + kDer); + return; + } + + WB_NOTE(BufferLoadCRL(NULL, der, n, WOLFSSL_FILETYPE_ASN1, 0)); + WB_NOTE(BufferLoadCRL(cm->crl, NULL, n, WOLFSSL_FILETYPE_ASN1, 0)); + WB_NOTE(BufferLoadCRL(cm->crl, der, 0, WOLFSSL_FILETYPE_ASN1, 0)); + /* the accepting partner: a real DER CRL, which also populates crl->crlList + * so the store path below has an entry with toBeSigned and signature. */ + if (BufferLoadCRL(cm->crl, der, n, WOLFSSL_FILETYPE_ASN1, 0) + == WOLFSSL_SUCCESS) { + loaded = 1; + } + g_checks += 4; + + /* ---- BufferStoreCRL :1011 argument guard ---- */ + { + long outSz = (long)sizeof(der); + WB_NOTE(BufferStoreCRL(NULL, der, &outSz, WOLFSSL_FILETYPE_ASN1)); + WB_NOTE(BufferStoreCRL(cm->crl, der, NULL, WOLFSSL_FILETYPE_ASN1)); + } + + if (!loaded) { + printf("crl white-box: DER CRL did not load, store vectors skipped\n"); + return; + } + + /* ---- :1036, :1081, :1146 with a real entry in the list ---- */ + { + byte out[8192]; + long outSz; + + /* size query: buff NULL takes the ASN1 branch and the size-only arm */ + outSz = 0; + WB_NOTE(BufferStoreCRL(cm->crl, NULL, &outSz, WOLFSSL_FILETYPE_ASN1)); + + /* real DER store: :1081 both operands true */ + outSz = (long)sizeof(out); + WB_NOTE(BufferStoreCRL(cm->crl, out, &outSz, WOLFSSL_FILETYPE_ASN1)); + + /* undersized buffer: the BUFFER_E arm inside the ASN1 branch */ + outSz = 4; + WB_NOTE(BufferStoreCRL(cm->crl, out, &outSz, WOLFSSL_FILETYPE_ASN1)); + + /* PEM store: :1081 type operand false, :1146 both true */ + outSz = (long)sizeof(out); + WB_NOTE(BufferStoreCRL(cm->crl, out, &outSz, WOLFSSL_FILETYPE_PEM)); + + /* a type that is neither: :1146 type operand false too */ + outSz = (long)sizeof(out); + WB_NOTE(BufferStoreCRL(cm->crl, out, &outSz, 0x7f)); + } +} + /* ---------------------------------------------------------- main */ int main(void) @@ -223,6 +323,7 @@ int main(void) wb_check_cert_crl_ex(cm); wb_missing_crl_callbacks(cm); + wb_buffer_load_store(cm); printf("crl white-box: %d vectors driven\n", g_checks); From 824654223379324e3d77fa582107d0af1081a1ed Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 17:36:48 +0200 Subject: [PATCH 08/60] tests: white-box src/wolfio.c with a mocked byte source and socketpair wolfio.c reads as needing a transport and does not. It needs a byte source and a descriptor, and both can be supplied locally. wolfIO_HttpProcessResponseGenericIO takes a WolfSSLGenericIORecvCb, which is int (*)(char*, int, void*), so the whole HTTP response state machine is drivable from a memory buffer. The mock caps bytes per call, which forces the reassembly loop to iterate and produces split headers and split chunks that a real socket would not reproduce on demand, and it can return -1 to inject a read error with no transport involved. socketpair(AF_UNIX) covers the descriptor half: wolfIO_SockIsDGram wants an fd, not a peer, and a stream pair, a datagram pair and a closed descriptor give its getsockopt branch all three outcomes. No ports, no DNS, no listener. wolfio.c 43/87 -> 47/87. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + tests/unit-mcdc/test_wolfio_whitebox.c | 231 +++++++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 tests/unit-mcdc/test_wolfio_whitebox.c diff --git a/tests/include.am b/tests/include.am index 7e9b7d8a569..3cfa105398b 100644 --- a/tests/include.am +++ b/tests/include.am @@ -240,6 +240,7 @@ 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_wolfio_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/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index e2c66e56dd6..ca6aff0313a 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -63,5 +63,6 @@ test_wc_mlkem_poly_whitebox test_wc_port_whitebox test_wc_xmss_impl_whitebox test_wolfentropy_whitebox +test_wolfio_whitebox test_xmss_fault_whitebox test_xmss_hash_fault_whitebox diff --git a/tests/unit-mcdc/test_wolfio_whitebox.c b/tests/unit-mcdc/test_wolfio_whitebox.c new file mode 100644 index 00000000000..f8b77812d7d --- /dev/null +++ b/tests/unit-mcdc/test_wolfio_whitebox.c @@ -0,0 +1,231 @@ +/* test_wolfio_whitebox.c -- MC/DC white-box driver for src/wolfio.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-1301, USA + */ + +/* WHY A WHITE-BOX, AND WHY IT NEEDS NO NETWORK. + * + * src/wolfio.c looks like it needs a transport, and that reading is wrong. It + * needs two things, both of which can be supplied locally: + * + * 1. A BYTE SOURCE. wolfIO_HttpProcessResponseBuf and + * wolfIO_HttpProcessResponseGenericIO take a WolfSSLGenericIORecvCb, + * which is just `int (*)(char* buf, int sz, void* ctx)`. Feeding it from + * a memory buffer drives the whole HTTP response state machine -- + * chunked bodies, split headers, truncated input -- with no socket at + * all, and lets a vector deliver bytes in exactly the fragments a real + * network would not reliably reproduce. + * + * 2. A FILE DESCRIPTOR. EmbedReceiveFrom, wolfIO_SockIsDGram and the + * TcpBind family want an fd, not a peer. socketpair(AF_UNIX) gives a + * real, connected, hermetic pair: no ports, no DNS, no listener, nothing + * that can collide with another test or depend on the host's network. + * Datagram and stream pairs are both available, which is what + * wolfIO_SockIsDGram's getsockopt branch needs in order to have a pair. + * + * The memio harness cannot reach any of this: tests/utils.c installs its own + * IO callbacks, so Embed* is entered by no TLS or DTLS group in the tree. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Every rejecting vector has its accepting partner in THIS binary. + * - Bail paths print, so "covered nothing" differs from "nothing to say". + */ + +#include + +#include + +#include +#include + +#if defined(USE_WOLFSSL_IO) && !defined(WOLFCRYPT_ONLY) + +#include +#include + +static int g_checks; +#define WB_NOTE(what) do { g_checks++; (void)(what); } while (0) + +/* ------------------------------------------------------------- byte source */ + +/* A WolfSSLGenericIORecvCb backed by memory. `drip` caps how many bytes any + * one call may return, so a vector can force the caller's reassembly loop to + * run more than once -- the split-header and split-chunk cases that a single + * large read never produces. */ +typedef struct { + const char* data; + int len; + int pos; + int drip; + int failAfter; /* return -1 once this many calls have been made */ + int calls; +} MemSrc; + +static int wb_mem_recv(char* buf, int sz, void* ctx) +{ + MemSrc* m = (MemSrc*)ctx; + int n; + + m->calls++; + if (m->failAfter > 0 && m->calls > m->failAfter) + return -1; /* transport error, without a transport */ + n = m->len - m->pos; + if (n <= 0) + return 0; /* clean EOF */ + if (n > sz) + n = sz; + if (m->drip > 0 && n > m->drip) + n = m->drip; + XMEMCPY(buf, m->data + m->pos, (size_t)n); + m->pos += n; + return n; +} + +static void wb_http_response(void) +{ + static const char* kAppStr[] = { "application/ocsp-response", NULL }; + /* well formed, content-length bodied */ + static const char kOk[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/ocsp-response\r\n" + "Content-Length: 4\r\n" + "\r\n" + "\x30\x02\x00\x00"; + /* chunked transfer encoding: exercises the chunk-length state */ + static const char kChunked[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/ocsp-response\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "4\r\n\x30\x02\x00\x00\r\n0\r\n\r\n"; + /* headers end before a body ever arrives */ + static const char kTruncated[] = + "HTTP/1.1 200 OK\r\nContent-Length: 64\r\n\r\n"; + /* not HTTP at all: the protocol check rejects */ + static const char kNotHttp[] = "GARBAGE\r\n\r\n"; + /* an error status rather than 200 */ + static const char kNotFound[] = + "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"; + + struct { const char* d; int len; int drip; int failAfter; + const char* what; } rows[] = { + { kOk, (int)sizeof(kOk) - 1, 0, 0, "200 + body" }, + { kOk, (int)sizeof(kOk) - 1, 1, 0, "200, one byte per read" }, + { kChunked, (int)sizeof(kChunked) - 1, 0, 0, "chunked" }, + { kChunked, (int)sizeof(kChunked) - 1, 3, 0, "chunked, split reads" }, + { kTruncated, (int)sizeof(kTruncated) - 1, 0, 0, "headers, no body" }, + { kNotHttp, (int)sizeof(kNotHttp) - 1, 0, 0, "not HTTP" }, + { kNotFound, (int)sizeof(kNotFound) - 1, 0, 0, "404" }, + { kOk, (int)sizeof(kOk) - 1, 1, 2, "read error mid-body" }, + }; + size_t i; + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + MemSrc src; + byte httpBuf[512]; + byte* respBuf = NULL; + + XMEMSET(&src, 0, sizeof(src)); + src.data = rows[i].d; + src.len = rows[i].len; + src.drip = rows[i].drip; + src.failAfter = rows[i].failAfter; + + WB_NOTE(wolfIO_HttpProcessResponseGenericIO(wb_mem_recv, &src, + kAppStr, &respBuf, httpBuf, (int)sizeof(httpBuf), + DYNAMIC_TYPE_TMP_BUFFER, NULL)); + if (respBuf != NULL) + XFREE(respBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } +} + +/* --------------------------------------------------------- request builder */ + +/* `if (reqSz > 0 && reqSzStrLen > 0)` and the CR/LF scan in the name check. + * Both operands need a pair, and no in-tree caller asks for a zero-length + * request. */ +static void wb_http_request(void) +{ + byte buf[512]; + + WB_NOTE(wolfIO_HttpBuildRequestOcsp("example.com", "/ocsp", 4, + buf, (int)sizeof(buf))); + WB_NOTE(wolfIO_HttpBuildRequestOcsp("example.com", "/ocsp", 0, + buf, (int)sizeof(buf))); +} + +/* ---------------------------------------------------- descriptors, no peer */ + +/* socketpair gives a real connected fd pair with no port, no DNS and no + * listener, so these vectors are hermetic and cannot collide with anything + * else running on the host. */ +static void wb_sockets(void) +{ + int sp[2]; + int dg[2]; + + /* stream pair: wolfIO_SockIsDGram takes its getsockopt branch and returns + * false; the datagram pair below is the accepting partner. */ + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sp) == 0) { + WB_NOTE(wolfIO_SockIsDGram(sp[0])); + close(sp[0]); + close(sp[1]); + } + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, dg) == 0) { + WB_NOTE(wolfIO_SockIsDGram(dg[0])); + close(dg[0]); + close(dg[1]); + } + /* a closed descriptor makes getsockopt itself fail, which is the operand + * that a working socket can never take. */ + WB_NOTE(wolfIO_SockIsDGram(-1)); +} + +/* ---------------------------------------------------------- main */ + +int main(void) +{ + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("wolfio white-box: wolfSSL_Init failed\n"); + goto done; + } + + wb_http_response(); + wb_http_request(); + wb_sockets(); + + printf("wolfio white-box: %d vectors driven\n", g_checks); + +done: + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else /* !USE_WOLFSSL_IO */ + +int main(void) +{ + printf("wolfio white-box: skipped (USE_WOLFSSL_IO not built)\n"); + return 0; +} + +#endif From 9673b10ac32fd8cef316a2830b8d1d05611a4c80 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 17:38:41 +0200 Subject: [PATCH 09/60] tests: drive CheckOcspResponder's identity chains CheckOcspResponder takes an OcspResponse and four raw hashes, walks bs->single and compares bytes. Nothing is stored, so the response can be a local -- unlike GetOcspEntry, which links what it is given into ocsp->ocspList, a list the library allocates and frees, where a stack fixture crashes. Each vector breaks a chain at a different operand: no key hash offered, name mismatch, key mismatch, and the delegated-responder arm behind the OCSP-signing usage bit, with a fully matching vector as the accepting partner. A real response is self-consistent, so the mismatch cases have no independence pair from outside. ocsp.c 11/47 -> 18/47. --- tests/unit-mcdc/test_ocsp_whitebox.c | 76 ++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/unit-mcdc/test_ocsp_whitebox.c b/tests/unit-mcdc/test_ocsp_whitebox.c index 18b07876dcd..fed18c57e1b 100644 --- a/tests/unit-mcdc/test_ocsp_whitebox.c +++ b/tests/unit-mcdc/test_ocsp_whitebox.c @@ -111,6 +111,81 @@ static void wb_entry_match(WOLFSSL_OCSP* ocsp) ocsp->ocspList = NULL; } + +/* ---------------------------------------------- CheckOcspResponder :625-644 */ +/* `if (bs == NULL || subjectNameHash == NULL || issuerNameHash == NULL)` and + * the two responder-identity chains + * subjectKeyHash != NULL && XMEMCMP(subjectNameHash, single->issuerHash) + * && XMEMCMP(subjectKeyHash, single->issuerKeyHash) + * issuerKeyHash != NULL && XMEMCMP(issuerNameHash, ...) && ... + * + * This one is genuinely hermetic: it takes an OcspResponse and four raw + * hashes, walks bs->single, and compares bytes. Nothing is stored, so the + * response can be a local -- unlike GetOcspEntry, which links what it is + * given into ocsp->ocspList, a list the library allocates and frees. A stack + * fixture there crashed; here there is no ownership at all. + * + * Each vector below breaks the chain at a DIFFERENT operand, and the last one + * matches on every field so the earlier ones have an accepting partner. + * A real response is always self-consistent, which is why the mismatch cases + * have no independence pair from outside. */ +static void wb_check_responder(void) +{ + OcspResponse bs; + OcspEntry single; + byte subjName[OCSP_DIGEST_SIZE]; + byte issuName[OCSP_DIGEST_SIZE]; + byte subjKey[KEYID_SIZE]; + byte issuKey[KEYID_SIZE]; + byte other[OCSP_DIGEST_SIZE]; + + XMEMSET(&bs, 0, sizeof(bs)); + XMEMSET(&single, 0, sizeof(single)); + XMEMSET(subjName, 0x11, sizeof(subjName)); + XMEMSET(issuName, 0x22, sizeof(issuName)); + XMEMSET(subjKey, 0x33, sizeof(subjKey)); + XMEMSET(issuKey, 0x44, sizeof(issuKey)); + XMEMSET(other, 0x99, sizeof(other)); + + /* the response's single entry is signed by the subject */ + XMEMCPY(single.issuerHash, subjName, OCSP_DIGEST_SIZE); + XMEMCPY(single.issuerKeyHash, subjKey, KEYID_SIZE); + single.next = NULL; + bs.single = &single; + + /* :625, one vector per operand */ + WB_NOTE(CheckOcspResponder(NULL, subjName, subjKey, 0, issuName, issuKey)); + WB_NOTE(CheckOcspResponder(&bs, NULL, subjKey, 0, issuName, issuKey)); + WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, 0, NULL, issuKey)); + + /* :631 operand 0 false -- no subject key hash offered */ + WB_NOTE(CheckOcspResponder(&bs, subjName, NULL, 0, issuName, issuKey)); + /* :631 operand 1 false -- subject name does not match the single entry */ + WB_NOTE(CheckOcspResponder(&bs, other, subjKey, 0, issuName, issuKey)); + /* :631 operand 2 false -- name matches, key does not */ + WB_NOTE(CheckOcspResponder(&bs, subjName, other, 0, issuName, issuKey)); + /* :631 all true -- the accepting partner for the three above */ + WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, 0, issuName, issuKey)); + + /* the delegated-responder arm: reached only when the subject chain fails + * AND the OCSP-signing usage bit is set. Re-point the single entry at the + * issuer so that chain can succeed. */ + XMEMCPY(single.issuerHash, issuName, OCSP_DIGEST_SIZE); + XMEMCPY(single.issuerKeyHash, issuKey, KEYID_SIZE); + + /* :640 operand 0 false -- no issuer key hash offered */ + WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, EXTKEYUSE_OCSP_SIGN, + issuName, NULL)); + /* :640 operand 2 false -- issuer name matches, key does not */ + WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, EXTKEYUSE_OCSP_SIGN, + issuName, other)); + /* :640 all true -- accepting partner */ + WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, EXTKEYUSE_OCSP_SIGN, + issuName, issuKey)); + + bs.single = NULL; +} + /* ---------------------------------------------------------- main */ int main(void) @@ -132,6 +207,7 @@ int main(void) } wb_entry_match(cm->ocsp); + wb_check_responder(); printf("ocsp white-box: %d vectors driven\n", g_checks); From 3d86606768227814fcded4eb9925e79c0357fb78 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 17:42:48 +0200 Subject: [PATCH 10/60] tests: white-box MatchDomainName's wildcard rules MatchDomainName decides whether a presented certificate name matches the host being connected to. It is pure -- two strings, two lengths, a flags word, no ssl, no allocation -- but nearly uncovered, because callers reach it only after a completed verification: the name comes from a parsed SAN or CN and the host from local configuration, so an empty pattern, a bare star, a wildcard that is not leftmost, a wildcard with no dot after it, or a zero length on one side only never arrive. Those are the inputs an attacker picks. 38 vectors over both wildcard-policy flag settings, each row flipping one named operand with an accepting partner differing in a single field. internal.c 573/1722 -> 587/1722. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + .../unit-mcdc/test_internal_domain_whitebox.c | 164 ++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 tests/unit-mcdc/test_internal_domain_whitebox.c diff --git a/tests/include.am b/tests/include.am index 3cfa105398b..9001a089eae 100644 --- a/tests/include.am +++ b/tests/include.am @@ -165,6 +165,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_integer_fault_whitebox.c \ tests/unit-mcdc/test_integer_whitebox.c \ tests/unit-mcdc/test_kdf_hash_fault_whitebox.c \ + tests/unit-mcdc/test_internal_domain_whitebox.c \ tests/unit-mcdc/test_internal_suites_whitebox.c \ tests/unit-mcdc/test_kdf_whitebox.c \ tests/unit-mcdc/test_lms_bds_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index ca6aff0313a..f9c084181ca 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -19,6 +19,7 @@ test_hpke_fault_whitebox test_hpke_whitebox test_integer_fault_whitebox test_integer_whitebox +test_internal_domain_whitebox test_internal_suites_whitebox test_lms_bds_whitebox test_lms_fault_whitebox diff --git a/tests/unit-mcdc/test_internal_domain_whitebox.c b/tests/unit-mcdc/test_internal_domain_whitebox.c new file mode 100644 index 00000000000..d77f5c1d032 --- /dev/null +++ b/tests/unit-mcdc/test_internal_domain_whitebox.c @@ -0,0 +1,164 @@ +/* test_internal_domain_whitebox.c -- MC/DC white-box driver for + * MatchDomainName in src/internal.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-1301, USA + */ + +/* MatchDomainName is the wildcard-certificate name matcher: it decides whether + * a presented certificate name matches the host being connected to. It is a + * pure function -- two strings, two lengths and a flags word, no ssl, no + * allocation, no ownership -- which makes it the cheapest dense cluster in + * internal.c to close, and one of the most security-relevant. + * + * It is nonetheless nearly uncovered from tests/api, because the callers reach + * it only through a completed certificate verification: the name comes from a + * parsed SAN or CN and the host from the caller's own configuration, so the + * degenerate combinations -- an empty pattern, a bare "*", a wildcard that is + * not leftmost, a pattern with no dot after the wildcard, a zero length on one + * side only -- never arrive. Those are exactly the inputs an attacker chooses. + * + * The vectors are a table rather than prose, because each row exists to flip + * one named operand and the expected result is what documents it. Every + * rejecting row has an accepting partner that differs in one field. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it becomes a silent no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(NO_CERTS) + +static int g_checks; + +#ifndef WOLFSSL_LEFT_MOST_WILDCARD_ONLY + #define WB_LEFTMOST_FLAG 0 +#else + #define WB_LEFTMOST_FLAG WOLFSSL_LEFT_MOST_WILDCARD_ONLY +#endif + +static void wb_match(const char* pattern, int patternLen, + const char* str, word32 strLen, + unsigned int flags, const char* what) +{ + g_checks++; + (void)what; + (void)MatchDomainName(pattern, patternLen, str, strLen, flags); +} + +int main(void) +{ + static const char kHost[] = "www.example.com"; + static const char kWild[] = "*.example.com"; + size_t i; + unsigned int flagSets[2]; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal domain white-box: wolfSSL_Init failed\n"); + goto done; + } + + flagSets[0] = 0; + flagSets[1] = (unsigned int)WB_LEFTMOST_FLAG; + + /* Both flag settings, so the `leftWildcardOnly && ...` operands at :14502, + * :14572 and :14608 each get a pair rather than a constant. */ + for (i = 0; i < sizeof(flagSets) / sizeof(flagSets[0]); i++) { + unsigned int fl = flagSets[i]; + + /* :14493 -- one row per operand of the four-way argument guard, then + * the accepting partner. */ + wb_match(NULL, 5, kHost, (word32)XSTRLEN(kHost), fl, "pattern NULL"); + wb_match(kWild, (int)XSTRLEN(kWild), NULL, + (word32)XSTRLEN(kHost), fl, "str NULL"); + wb_match(kWild, 0, kHost, (word32)XSTRLEN(kHost), fl, "patternLen 0"); + wb_match(kWild, (int)XSTRLEN(kWild), kHost, 0, fl, "strLen 0"); + wb_match(kWild, (int)XSTRLEN(kWild), kHost, + (word32)XSTRLEN(kHost), fl, "all valid (partner)"); + + /* :14508 -- exact match with equal lengths, and a length mismatch that + * takes the first operand false. */ + wb_match(kHost, (int)XSTRLEN(kHost), kHost, + (word32)XSTRLEN(kHost), fl, "exact match"); + wb_match(kHost, (int)XSTRLEN(kHost) - 1, kHost, + (word32)XSTRLEN(kHost), fl, "length mismatch"); + + /* :14549 -- '*' present and eligible, versus '*' present where it is + * not the leftmost label, versus no '*' at all. */ + wb_match("*.example.com", 13, kHost, + (word32)XSTRLEN(kHost), fl, "leftmost wildcard"); + wb_match("www.*.com", 9, kHost, + (word32)XSTRLEN(kHost), fl, "interior wildcard"); + wb_match("www.example.com", 15, kHost, + (word32)XSTRLEN(kHost), fl, "no wildcard"); + + /* :14558 -- a wildcard label not followed by '.' */ + wb_match("*example.com", 12, kHost, + (word32)XSTRLEN(kHost), fl, "wildcard, no dot after"); + + /* :14568 -- pattern exhausted while length says otherwise, built by + * passing a length longer than the NUL-terminated content. */ + wb_match("*.example.com\0extra", 18, kHost, + (word32)XSTRLEN(kHost), fl, "embedded NUL in pattern"); + + /* :14572 -- a bare '*' pattern, which matches everything and is what + * the leftmost-only rule exists to refuse. */ + wb_match("*", 1, kHost, (word32)XSTRLEN(kHost), fl, "bare star"); + + /* :14589 -- characters equal with pattern remaining, and the same + * comparison where they differ. */ + wb_match("*.example.com", 13, "www.example.org", 15, fl, "tld differs"); + + /* :14608 -- wildcard eligible under each flag setting. */ + wb_match("*.example.com", 13, "a.b.example.com", 15, fl, + "multi-label host"); + + /* :14625 -- both lengths zero, and one zero with the other not, which + * is the pair for that operand. */ + wb_match("", 0, "", 0, fl, "both empty"); + wb_match("", 0, kHost, (word32)XSTRLEN(kHost), fl, "empty pattern"); + + /* An FQDN that is not valid, for the IsValidFQDN operand at :14502. */ + wb_match("*.example.com", 13, "..", 2, fl, "invalid fqdn"); + wb_match("*.example.com", 13, "no-dots-here", 12, fl, "no dots"); + } + + printf("internal domain white-box: %d vectors driven\n", g_checks); + +done: + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal domain white-box: skipped (TLS/certs not built)\n"); + return 0; +} + +#endif From c64298ba0e081de3bb254950f36c33c1ea0df462 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 17:55:21 +0200 Subject: [PATCH 11/60] tests: white-box the handshake ordering police SanityCheckMsgReceived refuses a handshake message that arrives on the wrong side, arrives twice, or arrives out of order. Every condition in it is a rejection, so a conforming peer takes all of them false and no number of handshakes produces an independence pair; the vectors have to be malformed on purpose. The fixture is a zeroed WOLFSSL with its ctx pointed at a client CTX -- the function reads only options, msgsReceived and specs, and dereferences nothing else unguarded except SSL_CM(ssl)->ocspMustStaple in the ServerHelloDone arm. No certificate, no transport, no peer. Sweeping from all-clear and all-set alone measured 15 conditions: both saturated ends are refused by the prerequisite or the duplicate check before the out-of-order chain is ever evaluated. Each arm therefore also gets the state in which its message is accepted, and one flipped bit from there puts exactly one chain operand true with the rest false. That took it to 47. 3552 calls. internal.c 587/1722 -> 630/1722. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + .../unit-mcdc/test_internal_sanity_whitebox.c | 414 ++++++++++++++++++ 3 files changed, 416 insertions(+) create mode 100644 tests/unit-mcdc/test_internal_sanity_whitebox.c diff --git a/tests/include.am b/tests/include.am index 9001a089eae..2a1559c34a9 100644 --- a/tests/include.am +++ b/tests/include.am @@ -166,6 +166,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_integer_whitebox.c \ tests/unit-mcdc/test_kdf_hash_fault_whitebox.c \ tests/unit-mcdc/test_internal_domain_whitebox.c \ + tests/unit-mcdc/test_internal_sanity_whitebox.c \ tests/unit-mcdc/test_internal_suites_whitebox.c \ tests/unit-mcdc/test_kdf_whitebox.c \ tests/unit-mcdc/test_lms_bds_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index f9c084181ca..ed2535045d3 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -20,6 +20,7 @@ test_hpke_whitebox test_integer_fault_whitebox test_integer_whitebox test_internal_domain_whitebox +test_internal_sanity_whitebox test_internal_suites_whitebox test_lms_bds_whitebox test_lms_fault_whitebox diff --git a/tests/unit-mcdc/test_internal_sanity_whitebox.c b/tests/unit-mcdc/test_internal_sanity_whitebox.c new file mode 100644 index 00000000000..1acfe47ef78 --- /dev/null +++ b/tests/unit-mcdc/test_internal_sanity_whitebox.c @@ -0,0 +1,414 @@ +/* test_internal_sanity_whitebox.c -- MC/DC white-box driver for + * SanityCheckMsgReceived in src/internal.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-1301, USA + */ + +/* WHY A WHITE-BOX, AND WHY IT NEEDS NO HANDSHAKE. + * + * SanityCheckMsgReceived is the handshake ordering police: for each of the + * fourteen message types it refuses the message if it arrives on the wrong + * side, arrives twice, or arrives before or after a message it must follow or + * precede. It is the densest uncovered cluster in internal.c after the suite + * table, and every condition in it is a rejection -- which is to say, every + * condition in it is a check that only a malformed or hostile peer triggers. + * + * That is exactly why the handshake tests cannot cover it. A conforming peer + * walks the one accepted ordering, so on every call from tests/api each of + * these decisions is taken false, and MC/DC's independence pair -- the operand + * changing value AND the outcome changing with it -- never exists. Reaching + * the line is not the same as pairing the operand, and running more handshakes + * produces more of the same vector. + * + * The fixture is the whole trick, and it is smaller than it looks. The + * function touches ssl->options, ssl->msgsReceived and ssl->specs, and reads + * ssl->arrays and ssl->status_request only after NULL-guarding them. The one + * thing it dereferences unguarded is SSL_CM(ssl), which is ssl->ctx->cm. So + * the fixture is a zeroed WOLFSSL with its ctx pointed at a real CTX -- and + * nothing else. No certificate, no key, no transport, no peer, and in + * particular no wolfSSL_new, which returns NULL for a server CTX with no + * certificate loaded and which is what silently turned an earlier white-box in + * this campaign into a no-op that still exited 0. Both sides are swept from + * one client CTX, because options.side is a field, not a constructor argument. + * + * The vectors are one-at-a-time sweeps from both saturated baselines, the same + * construct as the InitSuites driver. For a decision `!A || B || C`, the + * all-set and all-clear states take it both ways, and the state differing in + * exactly one bit gives that operand its pair -- for every operand of every + * decision, from O(n) calls instead of O(2^n). Both sides are swept because + * roughly half the arms open with a side check, and both message-state and + * option-state are swept against each other's saturated ends so an operand is + * never masked by a short-circuit that never varies. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Bail paths print, so "covered nothing" differs from "nothing to say". + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) + +static int g_calls; + +/* ------------------------------------------------------ message-state bits */ + +enum { + M_HELLO_REQUEST = 0, M_CLIENT_HELLO, M_SERVER_HELLO, M_HELLO_VERIFY, + M_SESSION_TICKET, M_HELLO_RETRY, M_CERTIFICATE, M_CERT_STATUS, + M_SERVER_KEY_EXCH, M_CERT_REQUEST, M_SERVER_HELLO_DONE, M_CERT_VERIFY, + M_CLIENT_KEY_EXCH, M_FINISHED, M_CHANGE_CIPHER, M_COUNT +}; + +/* Set ssl->msgsReceived from a bit mask. The struct is a bitfield, so it + * cannot be indexed; the switch is the price of sweeping it generically. */ +static void wb_set_msgs(WOLFSSL* ssl, word32 mask) +{ + int i; + + XMEMSET(&ssl->msgsReceived, 0, sizeof(ssl->msgsReceived)); + for (i = 0; i < M_COUNT; i++) { + if ((mask & (1u << i)) == 0) + continue; + switch (i) { + case M_HELLO_REQUEST: ssl->msgsReceived.got_hello_request = 1; + break; + case M_CLIENT_HELLO: ssl->msgsReceived.got_client_hello = 1; + break; + case M_SERVER_HELLO: ssl->msgsReceived.got_server_hello = 1; + break; + case M_HELLO_VERIFY: + ssl->msgsReceived.got_hello_verify_request = 1; + break; + case M_SESSION_TICKET: ssl->msgsReceived.got_session_ticket = 1; + break; + case M_HELLO_RETRY: + ssl->msgsReceived.got_hello_retry_request = 1; + break; + case M_CERTIFICATE: ssl->msgsReceived.got_certificate = 1; + break; + case M_CERT_STATUS: + ssl->msgsReceived.got_certificate_status = 1; + break; + case M_SERVER_KEY_EXCH: + ssl->msgsReceived.got_server_key_exchange = 1; + break; + case M_CERT_REQUEST: + ssl->msgsReceived.got_certificate_request = 1; + break; + case M_SERVER_HELLO_DONE: + ssl->msgsReceived.got_server_hello_done = 1; + break; + case M_CERT_VERIFY: + ssl->msgsReceived.got_certificate_verify = 1; + break; + case M_CLIENT_KEY_EXCH: + ssl->msgsReceived.got_client_key_exchange = 1; + break; + case M_FINISHED: ssl->msgsReceived.got_finished = 1; + break; + case M_CHANGE_CIPHER: ssl->msgsReceived.got_change_cipher = 1; + break; + default: break; + } + } +} + +/* ------------------------------------------------------- option-state bits */ + +enum { + O_RESUMING = 0, O_VERIFY_PEER, O_PSK_CIPHER, O_ANON_CIPHER, + O_HAVE_PEER_CERT, O_HAVE_PEER_VERIFY, O_DTLS, O_COUNT +}; + +static void wb_set_opts(WOLFSSL* ssl, word32 mask) +{ + ssl->options.resuming = (mask & (1u << O_RESUMING)) ? 1 : 0; + ssl->options.verifyPeer = (mask & (1u << O_VERIFY_PEER)) ? 1 : 0; + ssl->options.usingPSK_cipher = (mask & (1u << O_PSK_CIPHER)) ? 1 : 0; + ssl->options.usingAnon_cipher= (mask & (1u << O_ANON_CIPHER)) ? 1 : 0; + ssl->options.havePeerCert = (mask & (1u << O_HAVE_PEER_CERT)) ? 1 : 0; + ssl->options.havePeerVerify = (mask & (1u << O_HAVE_PEER_VERIFY)) ? 1 : 0; +#ifdef WOLFSSL_DTLS + ssl->options.dtls = (mask & (1u << O_DTLS)) ? 1 : 0; +#endif +} + +/* --------------------------------------------------------------- one call */ + +/* Reset only what the function reads, then call it. Nothing here is allocated + * or owned, so there is no teardown and no ordering between vectors. */ +static void wb_call(WOLFSSL* ssl, byte type, int side, word32 msgs, + word32 opts, byte kea, byte staticEcdh) +{ + ssl->options.side = (byte)side; + wb_set_msgs(ssl, msgs); + wb_set_opts(ssl, opts); + ssl->specs.kea = kea; + ssl->specs.static_ecdh = staticEcdh; + + (void)SanityCheckMsgReceived(ssl, type); + g_calls++; +} + +/* For one message type on one side: sweep the message state one bit at a time + * from each saturated end against each saturated option state, then the option + * state the same way against each saturated message state, then the key + * exchange values that server_hello_done and certificate_request test. */ +static void wb_sweep_type(WOLFSSL* ssl, byte type, int side) +{ + const word32 msgAll = (1u << M_COUNT) - 1u; + const word32 optAll = (1u << O_COUNT) - 1u; + word32 optEnds[2]; + int i, e; + + optEnds[0] = 0; + optEnds[1] = optAll; + + /* message bits, one at a time from both ends, under both option ends */ + for (e = 0; e < 2; e++) { + wb_call(ssl, type, side, 0, optEnds[e], rsa_kea, 0); + wb_call(ssl, type, side, msgAll, optEnds[e], rsa_kea, 0); + for (i = 0; i < M_COUNT; i++) { + wb_call(ssl, type, side, 1u << i, optEnds[e], rsa_kea, 0); + wb_call(ssl, type, side, msgAll & ~(1u << i), optEnds[e], + rsa_kea, 0); + } + } + + /* option bits, one at a time from both ends, under both message ends */ + for (e = 0; e < 2; e++) { + word32 msgs = e ? msgAll : 0; + for (i = 0; i < O_COUNT; i++) { + wb_call(ssl, type, side, msgs, 1u << i, rsa_kea, 0); + wb_call(ssl, type, side, msgs, optAll & ~(1u << i), rsa_kea, 0); + } + } + + /* `ssl->specs.kea != rsa_kea && ... static_ecdh` in certificate_request + * and server_hello_done: each operand needs a pair, and a build only ever + * negotiates one kea per connection. The message state omits + * server_key_exchange so the enclosing decision is entered. */ + { + const word32 msgs = msgAll & ~(1u << M_SERVER_KEY_EXCH); + static const byte keas[3] = { rsa_kea, psk_kea, ecc_diffie_hellman_kea }; + size_t k; + + for (k = 0; k < sizeof(keas) / sizeof(keas[0]); k++) { + wb_call(ssl, type, side, msgs, 0, keas[k], 0); + wb_call(ssl, type, side, msgs, 0, keas[k], 1); + } + } +} + + +/* ------------------------------------------------ accepting baselines + + * The saturated sweeps above are not enough on their own, and the reason is + * worth stating because it cost a measurement to find. Most arms are a + * sequence: first a prerequisite check that returns early, then the + * out-of-order chain. Sweeping from an all-clear state never reaches the + * chain, because the prerequisite is missing; sweeping from an all-set state + * never reaches it either, because the duplicate check fires first. Both ends + * are refused at the door, and the chain -- which is where the multi-operand + * decisions actually live -- is evaluated by neither. + * + * So each arm gets the state in which its message is ACCEPTED: prerequisites + * present, its own bit clear, every forbidden successor clear. From there one + * flipped bit puts exactly one operand of the chain true with the rest false, + * and the baseline itself is the partner where all of them are false. That is + * the independence pair, per operand, and the baseline is what a conforming + * peer produces -- the flips are what an attacker sends. + */ +typedef struct { + byte type; + int side; + word32 base; + const char* what; +} SanityBase; + +#define B(x) (1u << (x)) + +static const SanityBase kBases[] = { + { hello_request, WOLFSSL_CLIENT_END, 0, "HelloRequest" }, + { client_hello, WOLFSSL_SERVER_END, 0, "ClientHello" }, + { server_hello, WOLFSSL_CLIENT_END, 0, "ServerHello" }, + { hello_verify_request, WOLFSSL_CLIENT_END, 0, "HelloVerifyRequest" }, + + /* ServerHello seen, ServerHelloDone seen, nothing after it yet. */ + { session_ticket, WOLFSSL_CLIENT_END, + B(M_SERVER_HELLO) | B(M_SERVER_HELLO_DONE), "SessionTicket" }, + + { certificate, WOLFSSL_CLIENT_END, B(M_SERVER_HELLO), "Certificate/client" }, + { certificate, WOLFSSL_SERVER_END, B(M_CLIENT_HELLO), "Certificate/server" }, + + { certificate_status, WOLFSSL_CLIENT_END, + B(M_SERVER_HELLO) | B(M_CERTIFICATE), "CertificateStatus" }, + + { server_key_exchange, WOLFSSL_CLIENT_END, + B(M_SERVER_HELLO) | B(M_CERTIFICATE), "ServerKeyExchange" }, + + { certificate_request, WOLFSSL_CLIENT_END, + B(M_SERVER_HELLO) | B(M_CERTIFICATE) | B(M_SERVER_KEY_EXCH), + "CertificateRequest" }, + + { server_hello_done, WOLFSSL_CLIENT_END, + B(M_SERVER_HELLO) | B(M_CERTIFICATE) | B(M_SERVER_KEY_EXCH) | + B(M_CERT_STATUS), "ServerHelloDone" }, + + { certificate_verify, WOLFSSL_SERVER_END, + B(M_CLIENT_HELLO) | B(M_CERTIFICATE) | B(M_CLIENT_KEY_EXCH), + "CertificateVerify" }, + + { client_key_exchange, WOLFSSL_SERVER_END, + B(M_CLIENT_HELLO) | B(M_CERTIFICATE), "ClientKeyExchange" }, + + { finished, WOLFSSL_CLIENT_END, + B(M_SERVER_HELLO) | B(M_SERVER_HELLO_DONE) | B(M_CHANGE_CIPHER), + "Finished/client" }, + { finished, WOLFSSL_SERVER_END, + B(M_CLIENT_HELLO) | B(M_CLIENT_KEY_EXCH) | B(M_CHANGE_CIPHER), + "Finished/server" }, + + { change_cipher_hs, WOLFSSL_CLIENT_END, + B(M_SERVER_HELLO) | B(M_SERVER_HELLO_DONE), "ChangeCipher/client" }, + { change_cipher_hs, WOLFSSL_SERVER_END, + B(M_CLIENT_HELLO) | B(M_CLIENT_KEY_EXCH) | B(M_CERT_VERIFY), + "ChangeCipher/server" }, +}; + +static void wb_sweep_baselines(WOLFSSL* ssl) +{ + const word32 optAll = (1u << O_COUNT) - 1u; + size_t r; + int i; + + for (r = 0; r < sizeof(kBases) / sizeof(kBases[0]); r++) { + const SanityBase* b = &kBases[r]; + + /* the accepting vector: every chain operand false */ + wb_call(ssl, b->type, b->side, b->base, 0, rsa_kea, 0); + + /* one operand true at a time, the rest still false */ + for (i = 0; i < M_COUNT; i++) + wb_call(ssl, b->type, b->side, b->base ^ (1u << i), 0, rsa_kea, 0); + + /* the option operands in the same chains -- resuming, verifyPeer, + * usingPSK_cipher, usingAnon_cipher, havePeerCert, havePeerVerify, + * dtls -- paired against the accepting baseline. */ + for (i = 0; i < O_COUNT; i++) { + wb_call(ssl, b->type, b->side, b->base, 1u << i, rsa_kea, 0); + wb_call(ssl, b->type, b->side, b->base, optAll & ~(1u << i), + rsa_kea, 0); + } + + /* `kea != rsa_kea && !static_ecdh && !psk-without-hint` decides + * whether a missing ServerKeyExchange is an error. A build negotiates + * one kea, so these operands have no pair outside this loop. */ + { + static const byte keas[3] = { rsa_kea, psk_kea, + ecc_diffie_hellman_kea }; + word32 noSkex = b->base & ~B(M_SERVER_KEY_EXCH); + size_t k; + + for (k = 0; k < sizeof(keas) / sizeof(keas[0]); k++) { + wb_call(ssl, b->type, b->side, noSkex, 0, keas[k], 0); + wb_call(ssl, b->type, b->side, noSkex, 0, keas[k], 1); + } + } + } +} + +/* ---------------------------------------------------------------- main */ + +int main(void) +{ + /* Every message type with an arm, plus one with none, for the default. */ + static const byte kTypes[] = { + hello_request, client_hello, server_hello, hello_verify_request, + session_ticket, certificate, certificate_status, server_key_exchange, + certificate_request, server_hello_done, certificate_verify, + client_key_exchange, finished, change_cipher_hs, 200 + }; + const int sides[2] = { WOLFSSL_CLIENT_END, WOLFSSL_SERVER_END }; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + size_t t; + int s; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal sanity white-box: wolfSSL_Init failed\n"); + goto done; + } + + /* The CTX exists only so SSL_CM(ssl) resolves: the server_hello_done arm + * reads SSL_CM(ssl)->ocspMustStaple without a NULL guard, which is the one + * field outside the WOLFSSL that the function reaches for. A client method + * needs no certificate, and the side under test is a field on the ssl. */ + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) { + printf("internal sanity white-box: CTX_new failed\n"); + goto done; + } + + /* A zeroed WOLFSSL, not a constructed one. wolfSSL_new would allocate + * buffers, suites and hashes that this function never reads. */ + ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + if (ssl == NULL) { + printf("internal sanity white-box: out of memory\n"); + goto done; + } + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + + for (t = 0; t < sizeof(kTypes) / sizeof(kTypes[0]); t++) + for (s = 0; s < 2; s++) + wb_sweep_type(ssl, kTypes[t], sides[s]); + + wb_sweep_baselines(ssl); + + printf("internal sanity white-box: %d SanityCheckMsgReceived calls\n", + g_calls); + +done: + /* XFREE, not wolfSSL_free: nothing here was constructed, and the ctx + * pointer was assigned without taking a reference. */ + XFREE(ssl, NULL, DYNAMIC_TYPE_SSL); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal sanity white-box: skipped (TLS 1.2 not built)\n"); + return 0; +} + +#endif From 43f84980f6ca4e1d49ba061c785302f093e8be6b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 2 Sep 2026 17:58:29 +0200 Subject: [PATCH 12/60] tests: white-box the record header's version tolerance GetRecordHeader is the first code to look at bytes off the wire, before any key or MAC. Its decisions are the version-mismatch tolerance rules -- which peer, in which handshake state, may send which version -- plus the length and record-type checks. From tests/api the header always comes from wolfSSL's own record writer, so pvMajor and pvMinor equal ssl->version on every call and the whole mismatch block is dead: downgrade, connectState, acceptState and the alert-before-negotiation carve-out are evaluated only for a header the local writer never produces. The fixture is five bytes and a struct. inputBuffer.buffer is a pointer, so it points at a local array; the function writes only through its out-parameters and stores nothing. Rows are one flip from a well-formed TLS 1.2 record, with the baseline re-run between rows so the DTLS replay window does not carry. 61 calls. internal.c 630/1722 -> 642/1722. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + .../unit-mcdc/test_internal_record_whitebox.c | 310 ++++++++++++++++++ 3 files changed, 312 insertions(+) create mode 100644 tests/unit-mcdc/test_internal_record_whitebox.c diff --git a/tests/include.am b/tests/include.am index 2a1559c34a9..0e80776de1b 100644 --- a/tests/include.am +++ b/tests/include.am @@ -166,6 +166,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_integer_whitebox.c \ tests/unit-mcdc/test_kdf_hash_fault_whitebox.c \ tests/unit-mcdc/test_internal_domain_whitebox.c \ + tests/unit-mcdc/test_internal_record_whitebox.c \ tests/unit-mcdc/test_internal_sanity_whitebox.c \ tests/unit-mcdc/test_internal_suites_whitebox.c \ tests/unit-mcdc/test_kdf_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index ed2535045d3..4badaf830a8 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -20,6 +20,7 @@ test_hpke_whitebox test_integer_fault_whitebox test_integer_whitebox test_internal_domain_whitebox +test_internal_record_whitebox test_internal_sanity_whitebox test_internal_suites_whitebox test_lms_bds_whitebox diff --git a/tests/unit-mcdc/test_internal_record_whitebox.c b/tests/unit-mcdc/test_internal_record_whitebox.c new file mode 100644 index 00000000000..4b2ac0e740f --- /dev/null +++ b/tests/unit-mcdc/test_internal_record_whitebox.c @@ -0,0 +1,310 @@ +/* test_internal_record_whitebox.c -- MC/DC white-box driver for + * GetRecordHeader in src/internal.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-1301, USA + */ + +/* GetRecordHeader is the first thing that looks at bytes off the wire: five + * bytes of type, version and length, before any key or MAC is involved. Its + * decisions are the version-mismatch tolerance rules -- which peer, in which + * handshake state, is allowed to send which version -- and the length and + * record-type checks that follow. + * + * From tests/api, the header always arrives from wolfSSL's own record writer, + * so rh->pvMajor and rh->pvMinor equal ssl->version on every call and the + * entire mismatch block is dead. Its operands -- downgrade, connectState, + * acceptState, dtls, the alert-before-negotiation carve-out -- are only + * evaluated for a header the local writer would never produce. That is the + * definition of a condition the black box cannot pair. + * + * The fixture is five bytes and a struct. ssl->buffers.inputBuffer.buffer is + * just a pointer, so it can point at a local array; GetRecordHeader reads + * RECORD_HEADER_SZ bytes from it and writes only through its out-parameters. + * Nothing is allocated, nothing is owned, nothing is freed -- which is what + * distinguishes this from the fixtures in this campaign that crashed: those + * handed stack objects to a callee that stored them. + * + * Vectors are one flip at a time from an ACCEPTING baseline -- a well-formed + * record for the configured version -- because that is what gives each operand + * its pair. Sweeping from saturated ends measured a third as much on the + * sibling driver: both ends get refused before the interesting decision is + * reached. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Bail paths print, so "covered nothing" differs from "nothing to say". + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) + +static int g_calls; + +/* Room for a DTLS record header (13 bytes) as well as a TLS one (5). */ +static byte g_input[64]; + +/* The mutable state GetRecordHeader consults, as data rather than as a + * sequence of assignments, so a vector is one row and one changed field. */ +typedef struct { + byte type; /* rh->type */ + byte pvMajor; /* rh->pvMajor */ + byte pvMinor; /* rh->pvMinor */ + word16 len; /* rh->length */ + byte verMinor; /* ssl->version.minor */ + byte side; + byte downgrade; + byte handShakeDone; + byte dtls; + byte usingCompression; + byte connectState; + byte acceptState; + byte curEpoch; + byte dtlsEpoch; + const char* what; +} Rec; + +static void wb_record(const Rec* r, WOLFSSL* ssl) +{ + RecordLayerHeader rh; + word32 idx = 0; + word16 size = 0; + + XMEMSET(g_input, 0, sizeof(g_input)); + g_input[0] = r->type; + g_input[1] = r->pvMajor; + g_input[2] = r->pvMinor; + g_input[3] = (byte)(r->len >> 8); + g_input[4] = (byte)(r->len & 0xff); + + ssl->buffers.inputBuffer.buffer = g_input; + ssl->buffers.inputBuffer.bufferSize = (word32)sizeof(g_input); + ssl->buffers.inputBuffer.length = (word32)sizeof(g_input); + ssl->buffers.inputBuffer.idx = 0; + + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = r->verMinor; + ssl->options.side = r->side; + ssl->options.downgrade = r->downgrade; + ssl->options.handShakeDone = r->handShakeDone; + ssl->options.usingCompression = r->usingCompression; + ssl->options.connectState = r->connectState; + ssl->options.acceptState = r->acceptState; +#ifdef WOLFSSL_DTLS + ssl->options.dtls = r->dtls; + ssl->keys.curEpoch = r->curEpoch; + ssl->keys.dtls_epoch = r->dtlsEpoch; +#else + (void)r->dtls; (void)r->curEpoch; (void)r->dtlsEpoch; +#endif + + XMEMSET(&rh, 0, sizeof(rh)); + (void)GetRecordHeader(ssl, &idx, &rh, &size); + g_calls++; +} + +int main(void) +{ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + size_t i; + + /* The accepting baseline: a well-formed TLS 1.2 handshake record. Every + * row below is this with one field changed, so each operand it flips has + * this row as its independence partner. */ + const Rec base = { handshake, SSLv3_MAJOR, TLSv1_2_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0, 0, 0, 0, 0, 0, + 0, 0, "baseline" }; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal record white-box: wolfSSL_Init failed\n"); + goto done; + } + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) { + printf("internal record white-box: CTX_new failed\n"); + goto done; + } + ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + if (ssl == NULL) { + printf("internal record white-box: out of memory\n"); + goto done; + } + + { + /* Each row names the operand it exists to flip. A row is the baseline + * with one field changed; where a decision needs two fields to be + * reached at all (a mismatched version AND a state), the row says so. */ + Rec rows[] = { + /* record type: the switch, and each accepted arm */ + { change_cipher_spec, SSLv3_MAJOR, TLSv1_2_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, "ccs" }, + { application_data, SSLv3_MAJOR, TLSv1_2_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, "app data" }, + { alert, SSLv3_MAJOR, TLSv1_2_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, "alert" }, + { no_type, SSLv3_MAJOR, TLSv1_2_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, "no_type" }, + { 0x47 /* 'G', a plain HTTP GET */, 0x45, 0x54, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, "HTTP GET" }, + + /* zero length: refused for everything except application data, + * which is the partner for that operand */ + { handshake, SSLv3_MAJOR, TLSv1_2_MINOR, 0, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, "0-len hs" }, + { application_data, SSLv3_MAJOR, TLSv1_2_MINOR, 0, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, "0-len app" }, + + /* over-length, with and without compression, which changes the + * allowance the comparison is made against */ + { handshake, SSLv3_MAJOR, TLSv1_2_MINOR, 0xFFFF, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, "too long" }, + { handshake, SSLv3_MAJOR, TLSv1_2_MINOR, 0xFFFF, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,1,0,0,0,0, + "too long, compressed" }, + + /* --- the version-mismatch block, unreachable from tests/api --- */ + + /* major differs: the first operand alone */ + { handshake, 0x7F, TLSv1_2_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, + "major mismatch" }, + /* minor differs, we are not TLS 1.3: no carve-out applies */ + { handshake, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, + "minor mismatch" }, + /* minor differs but equals tls12minor while we are TLS 1.3: the + * partner that makes the third operand of the mismatch decision + * matter */ + { handshake, SSLv3_MAJOR, TLSv1_2_MINOR, 16, + TLSv1_3_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, + "1.3 accepting a 1.2 record version" }, + { handshake, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_3_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, + "1.3 refusing an older record version" }, + + /* server before its first reply is allowed a different version */ + { handshake, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_SERVER_END, 0,0,0,0,0,0,0,0, + "server, acceptState 0" }, + { handshake, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_SERVER_END, 0,0,0,0,0, + ACCEPT_THIRD_REPLY_DONE, 0,0, "server, past first reply" }, + + /* client with downgrade before its first reply likewise; the two + * rows below pair the downgrade operand and the state operand */ + { handshake, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 1,0,0,0,0,0,0,0, + "client downgrade, connectState 0" }, + { handshake, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0,0,0,0,0, + "client without downgrade" }, + { handshake, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 1,0,0,0, + FIRST_REPLY_DONE, 0,0,0, "client downgrade, past first reply" }, + + /* the alert carve-out: an alert sent back before the version is + * negotiated is tolerated. Four operands, one row each. */ + { alert, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0, + CLIENT_HELLO_SENT, 0,0,0, "alert after ClientHello" }, + { alert, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0, + CONNECT_BEGIN, 0,0,0, "alert in the wrong state" }, + { alert, 0x7F, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0, + CLIENT_HELLO_SENT, 0,0,0, "alert, major also differs" }, + { handshake, SSLv3_MAJOR, TLSv1_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0, + CLIENT_HELLO_SENT, 0,0,0, "not an alert" }, + { alert, SSLv3_MAJOR, TLSv1_3_MINOR, 16, + TLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,0,0, + CLIENT_HELLO_SENT, 0,0,0, "alert, minor not lower" }, + +#ifdef WOLFSSL_DTLS + /* DTLS: the replay window and epoch checks, which have no pair on + * a TLS connection at all. The header bytes are re-read by + * GetDtlsRecordHeader from the same buffer. */ + { handshake, DTLS_MAJOR, DTLSv1_2_MINOR, 16, + DTLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,1,0,0,0,0,0, "dtls hs" }, + { application_data, DTLS_MAJOR, DTLSv1_2_MINOR, 16, + DTLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,1,0,0,0,0,0, + "dtls app data, epoch 0" }, + { application_data, DTLS_MAJOR, DTLSv1_2_MINOR, 16, + DTLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,1,0,0,0,1,1, + "dtls app data, epoch 1" }, + { alert, DTLS_MAJOR, DTLSv1_2_MINOR, 16, + DTLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,1,1,0,0,0,0,1, + "dtls alert after handshake, epoch 0" }, + { alert, DTLS_MAJOR, DTLSv1_2_MINOR, 16, + DTLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,1,0,0,0,0,1, + "dtls alert during handshake" }, + { handshake, DTLS_MAJOR, DTLS_MINOR, 16, + DTLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,1,0,0,0,0,0, + "dtls 1.3 record version" }, + { handshake, 0x7F, DTLSv1_2_MINOR, 16, + DTLSv1_2_MINOR, WOLFSSL_CLIENT_END, 0,0,1,0,0,0,0,0, + "dtls major mismatch" }, +#endif + }; + + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + wb_record(&base, ssl); + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + /* A fresh zeroed ssl per row: GetRecordHeader advances the DTLS + * replay window, so a row must not inherit the last row's state. */ + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + wb_record(&rows[i], ssl); + /* the baseline again, so every row has its partner adjacent in + * the trace as well as in the argument */ + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + wb_record(&base, ssl); + } + } + + printf("internal record white-box: %d GetRecordHeader calls\n", g_calls); + +done: + XFREE(ssl, NULL, DYNAMIC_TYPE_SSL); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal record white-box: skipped (TLS not built)\n"); + return 0; +} + +#endif From 1a67037174eb48decc2fe693bc47bd82fd93328a Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 08:53:45 +0200 Subject: [PATCH 13/60] tests: white-box the revocation, transport and key-schedule leaves Four batches, measured together. crl.c 18->29. CompareCRLnumber over hex strings a parser would never emit (non-hex, empty, a number that went backwards); FindRevokedSerial with a same-length different-serial vector; BufferStoreCRL's five-operand guard, driven by hand-linking a CRL_Entry that is missing each field in turn and unlinking it before FreeCRL walks the list; LoadCRL over a real directory and over NULL arguments. wolfio.c 47->53. wolfIO_DecodeUrl against the malformed URLs an attacker supplies and a certificate never carries: unterminated IPv6 bracket, CR/LF smuggled into the host, a host and a port at the length cap, a colon with no digits, a port past the 16-bit ceiling. MAX_URL_ITEM_SIZE is private to wolfio.c, so the API test has to mirror it and hope; the white-box does not. ocsp.c 18->21. A CbOCSPIO mock is a complete responder for this function -- it can return a negative error, a zero-length body, or a positive length with a NULL buffer on demand, which no real responder can be made to do. keys.c 26->33. The file has no group and no caller outside internal.c and tls13.c, and everything in it runs from values a handshake has already fixed: one cipher suite, one version, enc and dec never NULL. GetCipherSpec swept over every suite-table selector, SetKeys called twice on the same objects so the lazy-allocation guards get both halves, SetKeysSide over the four dtls/1.3 combinations. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + tests/unit-mcdc/test_crl_whitebox.c | 155 +++++++++++ tests/unit-mcdc/test_keys_whitebox.c | 340 +++++++++++++++++++++++++ tests/unit-mcdc/test_ocsp_whitebox.c | 147 +++++++++++ tests/unit-mcdc/test_wolfio_whitebox.c | 91 +++++++ 6 files changed, 735 insertions(+) create mode 100644 tests/unit-mcdc/test_keys_whitebox.c diff --git a/tests/include.am b/tests/include.am index 0e80776de1b..1c5b7a530a1 100644 --- a/tests/include.am +++ b/tests/include.am @@ -166,6 +166,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_integer_whitebox.c \ tests/unit-mcdc/test_kdf_hash_fault_whitebox.c \ tests/unit-mcdc/test_internal_domain_whitebox.c \ + tests/unit-mcdc/test_keys_whitebox.c \ tests/unit-mcdc/test_internal_record_whitebox.c \ tests/unit-mcdc/test_internal_sanity_whitebox.c \ tests/unit-mcdc/test_internal_suites_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index 4badaf830a8..3e6ff20c4b9 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -23,6 +23,7 @@ test_internal_domain_whitebox test_internal_record_whitebox test_internal_sanity_whitebox test_internal_suites_whitebox +test_keys_whitebox test_lms_bds_whitebox test_lms_fault_whitebox test_lms_hash_fault_whitebox diff --git a/tests/unit-mcdc/test_crl_whitebox.c b/tests/unit-mcdc/test_crl_whitebox.c index 49102e23bef..a3bb68b48f8 100644 --- a/tests/unit-mcdc/test_crl_whitebox.c +++ b/tests/unit-mcdc/test_crl_whitebox.c @@ -296,6 +296,156 @@ static void wb_buffer_load_store(WOLFSSL_CERT_MANAGER* cm) } } + +/* ------------------------------------------------------- CompareCRLnumber */ +/* Two CRL_Entry pointers in, an ordering out; it reads nothing but the + * crlNumber hex strings and stores nothing, so stack entries are correct here + * -- unlike the list-linking fixtures below, where the library owns what it is + * handed. A CRL loaded from a file always parses to a valid number, so the + * mp_read_radix failure and the "the number went backwards" ordering have no + * pair from the public path. */ +static void wb_compare_crlnumber(void) +{ + CRL_Entry prev; + CRL_Entry curr; + size_t i; + + static const struct { const char* a; const char* b; const char* what; } + rows[] = { + { "01", "02", "prev < curr: the ordinary case" }, + { "02", "02", "equal: a replayed CRL" }, + { "02", "01", "prev > curr: a rollback" }, + { "0A", "0B", "multi-digit hex" }, + { "zz", "01", "prev not hex at all" }, + { "01", "zz", "curr not hex at all" }, + { "", "01", "prev empty" }, + }; + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + XMEMSET(&prev, 0, sizeof(prev)); + XMEMSET(&curr, 0, sizeof(curr)); + XSTRNCPY((char*)prev.crlNumber, rows[i].a, sizeof(prev.crlNumber) - 1); + XSTRNCPY((char*)curr.crlNumber, rows[i].b, sizeof(curr.crlNumber) - 1); + WB_NOTE(CompareCRLnumber(&prev, &curr)); + } +} + +/* ------------------------------------------------------- FindRevokedSerial */ +/* `if (rc->serialSz == serialSz && XMEMCMP(rc->serial, serial, serialSz) == 0)` + * -- both operands need a pair, and the second is only reachable when the + * first is true. A real revocation check compares a certificate's serial + * against a parsed list, so "same length, different bytes" is the interesting + * case and the one a caller cannot arrange. */ +static void wb_find_revoked(void) +{ + RevokedCert rc; + byte serial[4]; + size_t i; + + static const struct { int rcSz; byte rcByte; int qSz; byte qByte; + const char* what; } rows[] = { + { 4, 0xAA, 4, 0xAA, "same length, same bytes: revoked" }, + { 4, 0xAA, 4, 0xBB, "same length, different bytes" }, + { 3, 0xAA, 4, 0xAA, "different length" }, + { 4, 0xAA, 0, 0xAA, "zero-length query" }, + }; + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + XMEMSET(&rc, 0, sizeof(rc)); + rc.serialSz = rows[i].rcSz; + XMEMSET(rc.serialNumber, rows[i].rcByte, sizeof(rc.serialNumber) < 4 ? + sizeof(rc.serialNumber) : 4); + rc.next = NULL; + XMEMSET(serial, rows[i].qByte, sizeof(serial)); + WB_NOTE(FindRevokedSerial(&rc, serial, rows[i].qSz, NULL, 1)); + } +} + +/* ----------------------------------------------------------- BufferStoreCRL */ +/* `if (ent == NULL || tbs == NULL || tbsSz == 0 || sig == NULL || sigSz == 0)` + * -- five operands, and every one of them is false for any entry the parser + * produced, because the parser rejects a CRL that is missing either field. + * The entry is linked in by hand and unlinked before teardown: FreeCRL walks + * and frees crlList, so a stack node left on the list is a use-after-return. + * That mistake crashed an earlier fixture in this campaign. */ +static void wb_buffer_store(WOLFSSL_CERT_MANAGER* cm) +{ + static byte tbs[8] = { 0x30, 0x06, 0, 0, 0, 0, 0, 0 }; + static byte sig[8] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + CRL_Entry ent; + WOLFSSL_CRL crl; + byte out[512]; + long outSz; + size_t i; + + /* one row per operand of the guard, then the row where all five hold */ + static const struct { int haveTbs; int tbsSz; int haveSig; int sigSz; + const char* what; } rows[] = { + { 0, 8, 1, 8, "no toBeSigned" }, + { 1, 0, 1, 8, "toBeSigned length zero" }, + { 1, 8, 0, 8, "no signature" }, + { 1, 8, 1, 0, "signature length zero" }, + { 1, 8, 1, 8, "complete: the accepting partner" }, + }; + + if (InitCRL(&crl, cm) != 0) { + printf("crl white-box: InitCRL failed, skipping BufferStoreCRL\n"); + return; + } + + /* the empty list: `ent == NULL`, the first operand */ + outSz = (long)sizeof(out); + WB_NOTE(BufferStoreCRL(&crl, out, &outSz, WOLFSSL_FILETYPE_ASN1)); + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + XMEMSET(&ent, 0, sizeof(ent)); + ent.toBeSigned = rows[i].haveTbs ? tbs : NULL; + ent.tbsSz = rows[i].tbsSz; + ent.signature = rows[i].haveSig ? sig : NULL; + ent.signatureSz = (word32)rows[i].sigSz; + ent.signatureOID = CTC_SHA256wRSA; + ent.next = NULL; + crl.crlList = &ent; + + outSz = (long)sizeof(out); + WB_NOTE(BufferStoreCRL(&crl, out, &outSz, WOLFSSL_FILETYPE_ASN1)); + /* and the PEM arm, which encodes the same entry differently */ + outSz = (long)sizeof(out); + WB_NOTE(BufferStoreCRL(&crl, out, &outSz, WOLFSSL_FILETYPE_PEM)); + /* an unknown type, so neither arm is taken */ + outSz = (long)sizeof(out); + WB_NOTE(BufferStoreCRL(&crl, out, &outSz, -1)); + } + + /* unlink before FreeCRL, or teardown frees a stack object */ + crl.crlList = NULL; + FreeCRL(&crl, 0); +} + +/* ----------------------------------------------------------------- LoadCRL */ +/* `if (crl == NULL || path == NULL)`, and inside the directory walk the + * ".der"/".pem" suffix test and the per-file load result. The public entry + * points always pass a non-NULL pair, and the suffix test only has a false + * case if the directory contains a file that is neither. */ +static void wb_load_crl(WOLFSSL_CERT_MANAGER* cm) +{ + WOLFSSL_CRL crl; + + WB_NOTE(LoadCRL(NULL, "certs/crl", WOLFSSL_FILETYPE_PEM, 0)); + + if (InitCRL(&crl, cm) != 0) { + printf("crl white-box: InitCRL failed, skipping LoadCRL\n"); + return; + } + WB_NOTE(LoadCRL(&crl, NULL, WOLFSSL_FILETYPE_PEM, 0)); + /* certs/crl holds .pem, .der and .revoked files, so the suffix test gets + * both outcomes from one directory. */ + WB_NOTE(LoadCRL(&crl, "certs/crl", WOLFSSL_FILETYPE_PEM, 0)); + WB_NOTE(LoadCRL(&crl, "certs/crl", WOLFSSL_FILETYPE_ASN1, 0)); + WB_NOTE(LoadCRL(&crl, "certs/crl/does-not-exist", WOLFSSL_FILETYPE_PEM, 0)); + FreeCRL(&crl, 0); +} + /* ---------------------------------------------------------- main */ int main(void) @@ -325,6 +475,11 @@ int main(void) wb_missing_crl_callbacks(cm); wb_buffer_load_store(cm); + wb_compare_crlnumber(); + wb_find_revoked(); + wb_buffer_store(cm); + wb_load_crl(cm); + printf("crl white-box: %d vectors driven\n", g_checks); done: diff --git a/tests/unit-mcdc/test_keys_whitebox.c b/tests/unit-mcdc/test_keys_whitebox.c new file mode 100644 index 00000000000..d9de3a20f0c --- /dev/null +++ b/tests/unit-mcdc/test_keys_whitebox.c @@ -0,0 +1,340 @@ +/* test_keys_whitebox.c -- MC/DC white-box driver for src/keys.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-1301, USA + */ + +/* src/keys.c has no test file, no --group of its own, and no caller outside + * internal.c and tls13.c. Everything in it runs during a handshake, from + * values a handshake has already negotiated -- which is exactly why its + * remaining conditions have no independence pair from outside. + * + * The negotiated cipher suite is one value per connection. The protocol + * version is one value per connection. `enc` and `dec` are non-NULL on every + * call the library makes. So `cipherSuite0 != ECC_BYTE && ...`, the SSLv3 + * and DTLS version tests, and the `enc && enc->chacha == NULL` allocation + * guards are each evaluated once, one way, per binary. + * + * Called directly they are cheap: GetCipherSpec and SetKeys take plain + * structs, not a WOLFSSL, and SetCipherSpecs and SetKeysSide need only the + * fields they read. The fixture is a zeroed WOLFSSL with a ctx, as for the + * internal.c drivers. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Bail paths print, so "covered nothing" differs from "nothing to say". + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) + +static int g_checks; +#define WB_NOTE(what) do { g_checks++; (void)(what); } while (0) + +/* ------------------------------------------------------------ GetCipherSpec + + * `cipherSuite0 != ECC_BYTE && cipherSuite0 != ECDHE_PSK_BYTE && ...` decides + * which suite table is consulted. A connection negotiates one suite, so each + * operand is fixed for the life of the binary; here the first byte is swept + * over every table selector plus one that matches none. + * + * `specs->sig_algo == anonymous_sa_algo && opts != NULL` is the anonymous + * carve-out that marks the peer pre-authenticated. Both operands need a pair, + * and every in-tree caller passes a non-NULL opts. */ +static void wb_cipher_spec(void) +{ + static const byte suite0[] = { + ECC_BYTE, ECDHE_PSK_BYTE, CHACHA_BYTE, TLS13_BYTE, 0x00, 0xFE + }; + static const byte suite[] = { + TLS_RSA_WITH_AES_128_CBC_SHA & 0xFF, + TLS_DH_anon_WITH_AES_128_CBC_SHA & 0xFF, + TLS_PSK_WITH_AES_128_CBC_SHA & 0xFF, + 0xFF + }; + const word16 sides[2] = { WOLFSSL_CLIENT_END, WOLFSSL_SERVER_END }; + CipherSpecs specs; + Options opts; + size_t a, b; + int s; + + for (s = 0; s < 2; s++) { + for (a = 0; a < sizeof(suite0) / sizeof(suite0[0]); a++) { + for (b = 0; b < sizeof(suite) / sizeof(suite[0]); b++) { + XMEMSET(&specs, 0, sizeof(specs)); + XMEMSET(&opts, 0, sizeof(opts)); + WB_NOTE(GetCipherSpec(sides[s], suite0[a], suite[b], + &specs, &opts)); + /* the same suite with no Options: the second operand of the + * anonymous carve-out, which no in-tree caller can take */ + XMEMSET(&specs, 0, sizeof(specs)); + WB_NOTE(GetCipherSpec(sides[s], suite0[a], suite[b], + &specs, NULL)); + } + } + } + + /* An anonymous suite specifically, so the first operand of that decision + * is true with opts both present and absent. */ + XMEMSET(&specs, 0, sizeof(specs)); + XMEMSET(&opts, 0, sizeof(opts)); + WB_NOTE(GetCipherSpec(WOLFSSL_CLIENT_END, 0x00, + TLS_DH_anon_WITH_AES_128_CBC_SHA & 0xFF, + &specs, &opts)); + XMEMSET(&specs, 0, sizeof(specs)); + WB_NOTE(GetCipherSpec(WOLFSSL_CLIENT_END, 0x00, + TLS_DH_anon_WITH_AES_128_CBC_SHA & 0xFF, + &specs, NULL)); +} + +/* ---------------------------------------------------------- SetCipherSpecs + + * `ssl->version.major == SSLv3_MAJOR && ssl->version.minor >= TLSv1_MINOR` + * and `ssl->options.dtls && ssl->version.major == DTLS_MAJOR`. Each is two + * operands over a version and a flag that are fixed once a connection exists, + * so the sweep sets them directly. */ +static void wb_set_cipher_specs(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + static const struct { byte major; byte minor; const char* what; } vers[] = { + { SSLv3_MAJOR, SSLv3_MINOR, "SSLv3" }, + { SSLv3_MAJOR, TLSv1_MINOR, "TLS 1.0" }, + { SSLv3_MAJOR, TLSv1_1_MINOR, "TLS 1.1" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, "TLS 1.2" }, + { SSLv3_MAJOR, TLSv1_3_MINOR, "TLS 1.3" }, + { DTLS_MAJOR, DTLSv1_2_MINOR,"DTLS 1.2" }, + { DTLS_MAJOR, DTLS_MINOR, "DTLS 1.3" }, + { 0xFE, 0x00, "neither family" }, + }; + static const byte suite0[] = { ECC_BYTE, CHACHA_BYTE, TLS13_BYTE, 0x00 }; + size_t v, a; + int dtls; + + for (v = 0; v < sizeof(vers) / sizeof(vers[0]); v++) { + for (dtls = 0; dtls < 2; dtls++) { + for (a = 0; a < sizeof(suite0) / sizeof(suite0[0]); a++) { + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + ssl->version.major = vers[v].major; + ssl->version.minor = vers[v].minor; + ssl->options.side = WOLFSSL_CLIENT_END; + ssl->options.cipherSuite0 = suite0[a]; + ssl->options.cipherSuite = + TLS_RSA_WITH_AES_128_CBC_SHA & 0xFF; +#ifdef WOLFSSL_DTLS + ssl->options.dtls = (byte)dtls; +#endif + WB_NOTE(SetCipherSpecs(ssl)); + } + } + } +} + +/* ----------------------------------------------------------------- SetKeys + + * `if (enc && enc->chacha == NULL)` and its decrypt twin allocate the cipher + * state lazily. Every caller inside the library passes both, and passes them + * freshly zeroed, so the NULL operand and the already-allocated operand are + * each half a pair. Calling it with one side or neither completes both. + * + * SetKeys takes a Ciphers, a Keys and a CipherSpecs -- no WOLFSSL and no + * ownership -- so stack objects are correct here. The allocation it performs + * is freed by hand below. */ +static void wb_set_keys(void) +{ + Ciphers enc; + Ciphers dec; + Keys keys; + CipherSpecs specs; + int side; + size_t i; + + static const struct { int haveEnc; int haveDec; const char* what; } rows[] = { + { 1, 1, "both sides, as the library calls it" }, + { 1, 0, "encrypt only" }, + { 0, 1, "decrypt only" }, + { 0, 0, "neither" }, + }; + + for (side = 0; side < 2; side++) { + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + int s = side ? WOLFSSL_SERVER_END : WOLFSSL_CLIENT_END; + + XMEMSET(&enc, 0, sizeof(enc)); + XMEMSET(&dec, 0, sizeof(dec)); + XMEMSET(&keys, 0, sizeof(keys)); + XMEMSET(&specs, 0, sizeof(specs)); + + /* a ChaCha20-Poly1305 suite, so the chacha allocation guards are + * the decisions actually taken */ + specs.bulk_cipher_algorithm = wolfssl_chacha; + specs.key_size = CHACHA20_256_KEY_SIZE; + specs.iv_size = CHACHA20_IV_SIZE; + specs.hash_size = WC_SHA256_DIGEST_SIZE; + + WB_NOTE(SetKeys(rows[i].haveEnc ? &enc : NULL, + rows[i].haveDec ? &dec : NULL, + &keys, &specs, s, NULL, INVALID_DEVID, NULL, 0)); + + /* and again on the same objects, so the second call finds the + * state already allocated -- the other half of each pair */ + WB_NOTE(SetKeys(rows[i].haveEnc ? &enc : NULL, + rows[i].haveDec ? &dec : NULL, + &keys, &specs, s, NULL, INVALID_DEVID, NULL, 0)); + +#ifdef HAVE_CHACHA + XFREE(enc.chacha, NULL, DYNAMIC_TYPE_CIPHER); + XFREE(dec.chacha, NULL, DYNAMIC_TYPE_CIPHER); + enc.chacha = NULL; + dec.chacha = NULL; +#endif + } + } +} + +/* ------------------------------------------------------------- SetAuthKeys + + * `if (authentication && authentication->poly1305 == NULL)`, twice: once to + * decide whether to allocate and once to check the allocation. Both operands + * of both, from a NULL argument and from a struct called twice. */ +static void wb_set_auth_keys(void) +{ +#ifdef HAVE_ONE_TIME_AUTH + OneTimeAuth auth; + Keys keys; + CipherSpecs specs; + + XMEMSET(&keys, 0, sizeof(keys)); + XMEMSET(&specs, 0, sizeof(specs)); + + WB_NOTE(SetAuthKeys(NULL, &keys, &specs, NULL, INVALID_DEVID)); + + XMEMSET(&auth, 0, sizeof(auth)); + WB_NOTE(SetAuthKeys(&auth, &keys, &specs, NULL, INVALID_DEVID)); + /* second call: poly1305 is no longer NULL */ + WB_NOTE(SetAuthKeys(&auth, &keys, &specs, NULL, INVALID_DEVID)); + +#ifdef HAVE_POLY1305 + XFREE(auth.poly1305, NULL, DYNAMIC_TYPE_CIPHER); + auth.poly1305 = NULL; +#endif +#endif +} + +/* -------------------------------------------------------------- SetKeysSide + + * `ret == 0 && ssl->options.dtls && IsAtLeastTLSv1_3(ssl->version)` and + * `ret == 0 && ssl->options.dtls && !ssl->options.tls1_3`. The dtls operand + * and the version operand are both fixed per connection; sweeping the four + * combinations against both sides pairs each of them. */ +static void wb_set_keys_side(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + static const enum encrypt_side sides[3] = { + ENCRYPT_SIDE_ONLY, DECRYPT_SIDE_ONLY, ENCRYPT_AND_DECRYPT_SIDE + }; + static const struct { byte major; byte minor; const char* what; } vers[] = { + { SSLv3_MAJOR, TLSv1_2_MINOR, "TLS 1.2" }, + { SSLv3_MAJOR, TLSv1_3_MINOR, "TLS 1.3" }, + { DTLS_MAJOR, DTLSv1_2_MINOR, "DTLS 1.2" }, + { DTLS_MAJOR, DTLS_MINOR, "DTLS 1.3" }, + }; + size_t v, s; + int dtls; + + for (v = 0; v < sizeof(vers) / sizeof(vers[0]); v++) { + for (dtls = 0; dtls < 2; dtls++) { + for (s = 0; s < sizeof(sides) / sizeof(sides[0]); s++) { + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + ssl->version.major = vers[v].major; + ssl->version.minor = vers[v].minor; + ssl->options.side = WOLFSSL_CLIENT_END; + ssl->options.tls1_3 = + (vers[v].minor == TLSv1_3_MINOR || + vers[v].minor == DTLS_MINOR) ? 1 : 0; +#ifdef WOLFSSL_DTLS + ssl->options.dtls = (byte)dtls; +#endif + ssl->specs.bulk_cipher_algorithm = wolfssl_aes_gcm; + ssl->specs.key_size = AES_128_KEY_SIZE; + ssl->specs.iv_size = AESGCM_IMP_IV_SZ; + ssl->specs.hash_size = WC_SHA256_DIGEST_SIZE; + + WB_NOTE(SetKeysSide(ssl, sides[s])); + + /* free whatever the call allocated on this throwaway ssl */ + FreeCiphers(ssl); + } + } + } +} + +/* ---------------------------------------------------------------- main */ + +int main(void) +{ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("keys white-box: wolfSSL_Init failed\n"); + goto done; + } + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) { + printf("keys white-box: CTX_new failed\n"); + goto done; + } + ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + if (ssl == NULL) { + printf("keys white-box: out of memory\n"); + goto done; + } + + wb_cipher_spec(); + wb_set_cipher_specs(ssl, ctx); + wb_set_keys(); + wb_set_auth_keys(); + wb_set_keys_side(ssl, ctx); + + printf("keys white-box: %d vectors driven\n", g_checks); + +done: + XFREE(ssl, NULL, DYNAMIC_TYPE_SSL); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("keys white-box: skipped (TLS not built)\n"); + return 0; +} + +#endif diff --git a/tests/unit-mcdc/test_ocsp_whitebox.c b/tests/unit-mcdc/test_ocsp_whitebox.c index fed18c57e1b..bcc6779c347 100644 --- a/tests/unit-mcdc/test_ocsp_whitebox.c +++ b/tests/unit-mcdc/test_ocsp_whitebox.c @@ -186,6 +186,151 @@ static void wb_check_responder(void) bs.single = NULL; } + +/* --------------------------------------------------- CheckOcspRequest :484+ + + * This is where an OCSP check leaves the library: pick a responder URL, hand + * the encoded request to the application's transport callback, and interpret + * whatever comes back. From tests/api it is entered only when a real responder + * is configured, which no unit test does -- so the URL selection, the callback + * result handling and the response-free hook are all uncovered. + * + * None of it needs a network. CbOCSPIO is + * int (*)(void* ctx, const char* url, int urlSz, + * unsigned char** response) + * so a callback that hands back a static buffer is a complete responder for + * the purposes of this function, and it can return the results a real one + * cannot be made to produce on demand: a negative error, a zero-length body, + * a NULL response with a positive length. That is the point of mocking the + * consumed interface rather than the transport. + */ + +static byte g_ocspResp[64]; +static int g_ioResult; /* what the mock returns */ +static int g_ioNullResponse; /* return a positive size but no buffer */ +static int g_ioCalls; +static int g_freeCalls; + +static int wb_ocsp_io(void* ctx, const char* url, int urlSz, + unsigned char* request, int requestSz, + unsigned char** response) +{ + (void)ctx; (void)url; (void)urlSz; (void)request; (void)requestSz; + g_ioCalls++; + if (response != NULL) + *response = g_ioNullResponse ? NULL : g_ocspResp; + return g_ioResult; +} + +static void wb_ocsp_respfree(void* ctx, unsigned char* response) +{ + (void)ctx; (void)response; + g_freeCalls++; +} + +static void wb_check_request(WOLFSSL_CERT_MANAGER* cm) +{ + OcspRequest req; + byte serial[8]; + byte url[] = "http://ocsp.example.com/"; + size_t i; + + /* the two operands of the argument guard at :504 */ + WB_NOTE(CheckOcspRequest(NULL, NULL, NULL, NULL)); + WB_NOTE(CheckOcspRequest(cm->ocsp, NULL, NULL, NULL)); + + XMEMSET(g_ocspResp, 0, sizeof(g_ocspResp)); + g_ocspResp[0] = 0x30; + g_ocspResp[1] = 0x02; + + /* Each row is one operand of the URL-selection, callback-result and + * response-free decisions. `haveUrl` drives + * `ocspRequest->urlSz != 0 && ocspRequest->url != NULL`, which a request + * built from a certificate's AIA extension always satisfies. */ + { + static const struct { int haveUrl; int urlSz; int ioResult; + int nullResp; int installIo; int installFree; + const char* what; } rows[] = { + { 1, 24, 8, 0, 1, 1, "url and a responder that answers" }, + { 1, 24, 8, 0, 1, 0, "answered, no free callback" }, + { 1, 24, 0, 0, 1, 1, "responder returns zero bytes" }, + { 1, 24, -1, 0, 1, 1, "responder returns an error" }, + { 1, 24, 8, 1, 1, 1, "positive length, NULL buffer" }, + { 1, 24, 8, 0, 0, 0, "no responder callback installed" }, + { 1, 0, 8, 0, 1, 1, "url present, length zero" }, + { 0, 24, 8, 0, 1, 1, "length present, url NULL" }, + { 0, 0, 8, 0, 1, 1, "no url at all" }, + }; + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + XMEMSET(&req, 0, sizeof(req)); + XMEMSET(serial, 0x5A, sizeof(serial)); + req.serial = serial; + req.serialSz = (int)sizeof(serial); + XMEMSET(req.issuerHash, 0xAA, OCSP_DIGEST_SIZE); + XMEMSET(req.issuerKeyHash, 0xCC, OCSP_DIGEST_SIZE); + req.url = rows[i].haveUrl ? url : NULL; + req.urlSz = rows[i].urlSz; + + g_ioResult = rows[i].ioResult; + g_ioNullResponse = rows[i].nullResp; + (void)wolfSSL_CertManagerSetOCSP_Cb(cm, + rows[i].installIo ? wb_ocsp_io : NULL, + rows[i].installFree ? wb_ocsp_respfree : NULL, + NULL); + + WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL)); + + /* GetOcspEntry appends a heap entry when it finds no match; drop + * the list between rows so each row starts from an empty cache + * and the "found" and "not found" arms both get exercised. */ + } + } + + /* `if (cm != NULL && cm->ocspFailIfNotSupported)` at :596 -- the policy + * flag that decides whether an unreachable responder is fatal. */ + cm->ocspFailIfNotSupported = 1; + XMEMSET(&req, 0, sizeof(req)); + req.url = url; req.urlSz = 24; + XMEMSET(req.issuerHash, 0xAB, OCSP_DIGEST_SIZE); + g_ioResult = -1; + (void)wolfSSL_CertManagerSetOCSP_Cb(cm, wb_ocsp_io, wb_ocsp_respfree, NULL); + WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL)); + cm->ocspFailIfNotSupported = 0; + WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL)); + + (void)wolfSSL_CertManagerSetOCSP_Cb(cm, NULL, NULL, NULL); +} + +/* ------------------------------------------------------ CheckOcspResponse */ +/* `if (newStatus == NULL || newSingle == NULL || ocspResponse == NULL)` and + * the multi-response and response-buffer arms. The bytes are the interesting + * input: a caller cannot ask a real responder for a truncated DER. */ +static void wb_check_response(WOLFSSL_CERT_MANAGER* cm) +{ + OcspRequest req; + byte junk[16]; + size_t i; + + static const struct { int sz; const char* what; } rows[] = { + { 0, "zero-length response" }, + { 1, "one byte" }, + { 16, "sixteen bytes of nothing" }, + { -1, "negative length" }, + }; + + XMEMSET(junk, 0x30, sizeof(junk)); + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + XMEMSET(&req, 0, sizeof(req)); + XMEMSET(req.issuerHash, 0xAA, OCSP_DIGEST_SIZE); + req.serialSz = 4; + WB_NOTE(CheckOcspResponse(cm->ocsp, junk, rows[i].sz, NULL, NULL, + NULL, &req, NULL, NULL)); + } + WB_NOTE(CheckOcspResponse(cm->ocsp, NULL, 0, NULL, NULL, NULL, NULL, NULL, + NULL)); +} + /* ---------------------------------------------------------- main */ int main(void) @@ -208,6 +353,8 @@ int main(void) wb_entry_match(cm->ocsp); wb_check_responder(); + wb_check_request(cm); + wb_check_response(cm); printf("ocsp white-box: %d vectors driven\n", g_checks); diff --git a/tests/unit-mcdc/test_wolfio_whitebox.c b/tests/unit-mcdc/test_wolfio_whitebox.c index f8b77812d7d..bfa2ca9891a 100644 --- a/tests/unit-mcdc/test_wolfio_whitebox.c +++ b/tests/unit-mcdc/test_wolfio_whitebox.c @@ -200,6 +200,95 @@ static void wb_sockets(void) WB_NOTE(wolfIO_SockIsDGram(-1)); } + +/* --------------------------------------------------------- wolfIO_DecodeUrl */ +/* The URL splitter: scheme, host, port, path, with a bracketed IPv6 form and + * hard caps on every field. It is reachable from tests/api, but only with the + * URLs an OCSP responder extension actually carries -- well-formed, http://, + * host and path present. The operands that matter here are the malformed ones: + * an unterminated bracket, a CR or LF smuggled into the host, a host or port + * that hits the length cap, a port with no digits. Those are attacker-supplied + * and have no independence pair from a parsed certificate. + * + * MAX_URL_ITEM_SIZE is private to src/wolfio.c, which is another reason this + * belongs in a white-box: the API test that covers the same function has to + * mirror the constant and hope it stays in step. */ +static void wb_decode_url(void) +{ + char name[MAX_URL_ITEM_SIZE]; + char path[MAX_URL_ITEM_SIZE]; + word16 port; + size_t i; + + /* A host and a port at exactly the cap, built rather than written out. */ + static char longHost[MAX_URL_ITEM_SIZE + 32]; + static char longUrl[MAX_URL_ITEM_SIZE + 64]; + + static const struct { const char* url; int sz; const char* what; } rows[] = { + { "http://example.com:8080/ocsp", 28, "the ordinary case" }, + { "example.com:8080/ocsp", 21, "no scheme" }, + { "http://example.com/ocsp", 23, "no port" }, + { "http://example.com", 18, "no port, no path" }, + { "http://[::1]:443/", 17, "bracketed IPv6" }, + { "http://[::1]/", 13, "bracketed IPv6, no port" }, + { "http://[::1", 11, "bracket never closed" }, + { "http://[", 8, "bracket, then nothing" }, + { "http://[::1\r]:443/", 18, "CR inside the brackets" }, + { "http://[::1\n]:443/", 18, "LF inside the brackets" }, + { "http://exa\rmple.com/", 20, "CR inside the host" }, + { "http://exa\nmple.com/", 20, "LF inside the host" }, + { "http://example.com:/", 20, "colon, no digits" }, + { "http://example.com:99999999/", 28, "port longer than the field" }, + { "http://example.com:0/", 21, "port zero" }, + { "http://example.com:65535/", 25, "port at the 16-bit ceiling" }, + { "http://example.com:65536/", 25, "port past the ceiling" }, + { "http://example.com:80x/", 23, "non-digit in the port" }, + { "http://", 7, "scheme and nothing else" }, + { "/", 1, "a bare path" }, + { "http://example.com/ocsp", 7, "length stops inside the host" }, + { "http://example.com/ocsp", 19, "length stops at the path" }, + }; + + /* url NULL and urlSz 0 are the two operands of the first guard, and each + * out-parameter is optional, so the NULL-check on each has to be paired. */ + WB_NOTE(wolfIO_DecodeUrl(NULL, 10, name, path, &port)); + WB_NOTE(wolfIO_DecodeUrl("http://a/", 0, name, path, &port)); + WB_NOTE(wolfIO_DecodeUrl(NULL, 10, NULL, NULL, NULL)); + WB_NOTE(wolfIO_DecodeUrl("http://example.com:80/x", 23, NULL, path, &port)); + WB_NOTE(wolfIO_DecodeUrl("http://example.com:80/x", 23, name, NULL, &port)); + WB_NOTE(wolfIO_DecodeUrl("http://example.com:80/x", 23, name, path, NULL)); + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + XMEMSET(name, 0, sizeof(name)); + XMEMSET(path, 0, sizeof(path)); + port = 0; + WB_NOTE(wolfIO_DecodeUrl(rows[i].url, rows[i].sz, name, path, &port)); + } + + /* A host longer than MAX_URL_ITEM_SIZE, so the cap operand -- not the + * delimiter and not the length -- is what stops the copy. */ + XMEMSET(longHost, 'a', sizeof(longHost) - 1); + longHost[sizeof(longHost) - 1] = 0; + XSTRNCPY(longUrl, "http://", sizeof(longUrl)); + XSTRNCAT(longUrl, longHost, sizeof(longUrl) - XSTRLEN(longUrl) - 1); + WB_NOTE(wolfIO_DecodeUrl(longUrl, (int)XSTRLEN(longUrl), name, path, &port)); + + /* the same, bracketed, so the IPv6 loop hits its own cap */ + XSTRNCPY(longUrl, "http://[", sizeof(longUrl)); + XSTRNCAT(longUrl, longHost, sizeof(longUrl) - XSTRLEN(longUrl) - 1); + WB_NOTE(wolfIO_DecodeUrl(longUrl, (int)XSTRLEN(longUrl), name, path, &port)); +} + +/* ------------------------------------------------------- wolfIO_UrlHasCrlf */ +static void wb_url_crlf(void) +{ + WB_NOTE(wolfIO_UrlHasCrlf("http://example.com/", (int)XSTRLEN("http://example.com/"))); + WB_NOTE(wolfIO_UrlHasCrlf("http://example.com/\r\n", (int)XSTRLEN("http://example.com/\r\n"))); + WB_NOTE(wolfIO_UrlHasCrlf("http://exa\rmple.com/", (int)XSTRLEN("http://exa\rmple.com/"))); + WB_NOTE(wolfIO_UrlHasCrlf("http://exa\nmple.com/", (int)XSTRLEN("http://exa\nmple.com/"))); + WB_NOTE(wolfIO_UrlHasCrlf("", (int)XSTRLEN(""))); +} + /* ---------------------------------------------------------- main */ int main(void) @@ -212,6 +301,8 @@ int main(void) wb_http_response(); wb_http_request(); wb_sockets(); + wb_decode_url(); + wb_url_crlf(); printf("wolfio white-box: %d vectors driven\n", g_checks); From f03cb1d2a230b36c03088ef6882029cb48f8e278 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 08:56:11 +0200 Subject: [PATCH 14/60] tests: drive DoHandShakeMsgType's pre-dispatch state guards The guards run before any parser, so the message body can be zeros; they all call SendAlert on the way out, which is the only reason this needs more than the zeroed fixture -- a send callback that consumes and discards, or the alert path dereferences a NULL CBIOSend. internal.c 642/1722 -> 644/1722. Small: most of this function's 22 uncovered conditions sit after the dispatch, inside the per-message parsers, not in the guards. --- .../unit-mcdc/test_internal_sanity_whitebox.c | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/unit-mcdc/test_internal_sanity_whitebox.c b/tests/unit-mcdc/test_internal_sanity_whitebox.c index 1acfe47ef78..4b47cc9f7f9 100644 --- a/tests/unit-mcdc/test_internal_sanity_whitebox.c +++ b/tests/unit-mcdc/test_internal_sanity_whitebox.c @@ -342,6 +342,115 @@ static void wb_sweep_baselines(WOLFSSL* ssl) } } + +/* ------------------------------------------------- DoHandShakeMsgType + + * The other half of the ordering police: before a handshake message is + * dispatched to its parser, this function refuses it if the handshake is + * already complete, if it is the first message from a server and is not a + * ServerHello, if it is the first message from a client and is not a + * ClientHello, or -- for DTLS -- if a ServerHelloDone arrives before the + * ServerHello. Those are four state guards on top of the length check, and + * like SanityCheckMsgReceived they are all rejections that a conforming peer + * never triggers. + * + * Two things make it drivable without a handshake. The guards run before any + * dispatch, so no message body has to parse -- the input can be zeros. And + * every guard calls SendAlert on its way out, which is the only reason this + * needs more than the zeroed fixture: a send callback that consumes and + * discards. Without one the alert path dereferences a NULL CBIOSend. + * + * The vectors again sweep one field at a time from a state that is ACCEPTED, + * because from a saturated state the first guard fires and the rest are never + * evaluated. + */ + +static int wb_send_sink(WOLFSSL* ssl, char* buf, int sz, void* ctx) +{ + (void)ssl; (void)buf; (void)ctx; + return sz; /* swallow the alert, report it fully written */ +} + +static void wb_msgtype(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + static byte input[64]; + static const byte kTypes[] = { + client_hello, server_hello, server_hello_done, hello_request, + certificate, finished, session_ticket, 200 + }; + /* Each row is a handshake state. The first is the accepting one for a + * client receiving a ServerHello; the rest each differ from an accepting + * state in one field, so that field's operand gets its pair. */ + static const struct { + int side; + byte dtls; + byte handShakeDone; + byte handShakeState; + byte serverState; + byte clientState; + const char* what; + } rows[] = { + { WOLFSSL_CLIENT_END, 0, 0, NULL_STATE, NULL_STATE, NULL_STATE, + "client, nothing received yet" }, + { WOLFSSL_CLIENT_END, 0, 0, HANDSHAKE_DONE, SERVER_HELLO_COMPLETE, + NULL_STATE, "client, handshake already complete" }, + { WOLFSSL_CLIENT_END, 0, 1, NULL_STATE, SERVER_HELLO_COMPLETE, + NULL_STATE, "client, handShakeDone set" }, + { WOLFSSL_CLIENT_END, 0, 0, NULL_STATE, SERVER_HELLO_COMPLETE, + NULL_STATE, "client, server hello seen" }, + { WOLFSSL_CLIENT_END, 1, 0, NULL_STATE, NULL_STATE, NULL_STATE, + "DTLS client, nothing received yet" }, + { WOLFSSL_CLIENT_END, 1, 0, NULL_STATE, SERVER_HELLO_COMPLETE, + NULL_STATE, "DTLS client, server hello seen" }, + { WOLFSSL_SERVER_END, 0, 0, NULL_STATE, NULL_STATE, NULL_STATE, + "server, nothing received yet" }, + { WOLFSSL_SERVER_END, 0, 0, NULL_STATE, NULL_STATE, + CLIENT_HELLO_COMPLETE, "server, client hello seen" }, + { WOLFSSL_SERVER_END, 0, 1, HANDSHAKE_DONE, NULL_STATE, + CLIENT_HELLO_COMPLETE, "server, handshake complete" }, + { WOLFSSL_SERVER_END, 1, 0, NULL_STATE, NULL_STATE, NULL_STATE, + "DTLS server, nothing received yet" }, + }; + size_t r, t; + + XMEMSET(input, 0, sizeof(input)); + + for (r = 0; r < sizeof(rows) / sizeof(rows[0]); r++) { + for (t = 0; t < sizeof(kTypes) / sizeof(kTypes[0]); t++) { + word32 idx = 0; + + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + ssl->CBIOSend = wb_send_sink; + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = TLSv1_2_MINOR; + ssl->options.side = (byte)rows[r].side; + ssl->options.handShakeDone = rows[r].handShakeDone; + ssl->options.handShakeState = rows[r].handShakeState; + ssl->options.serverState = rows[r].serverState; + ssl->options.clientState = rows[r].clientState; +#ifdef WOLFSSL_DTLS + ssl->options.dtls = rows[r].dtls; +#endif + /* size fits inside totalSz: the length guard is taken false so + * the state guards below it are reached at all */ + (void)DoHandShakeMsgType(ssl, input, &idx, kTypes[t], 4, + (word32)sizeof(input)); + g_calls++; + + /* and once with size past totalSz, which is the other half of + * the `*inOutIdx + size > totalSz` pair */ + idx = 0; + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + ssl->CBIOSend = wb_send_sink; + ssl->options.side = (byte)rows[r].side; + (void)DoHandShakeMsgType(ssl, input, &idx, kTypes[t], 4096, 8); + g_calls++; + } + } +} + /* ---------------------------------------------------------------- main */ int main(void) @@ -389,6 +498,7 @@ int main(void) wb_sweep_type(ssl, kTypes[t], sides[s]); wb_sweep_baselines(ssl); + wb_msgtype(ssl, ctx); printf("internal sanity white-box: %d SanityCheckMsgReceived calls\n", g_calls); From ea3f568b35253c5d2e774c8b9a571292725cbef1 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 10:19:05 +0200 Subject: [PATCH 15/60] tests: corrupt the handshake in flight over memio The error paths in internal.c are most of what is left uncovered there, and a conforming pair of endpoints never enters them. Reaching them needs a peer that sends something wrong -- not a transport. test_memio already runs both endpoints through a byte buffer with credentials from certs/, and both sides are ours, so the buffer between them can be edited between rounds. test_tls_wire_mangle flips a bit at a chosen offset of a chosen round, which selects which handshake message gets hit and where: record type, record length, handshake type, handshake length, or inside the body. test_tls_wire_sequence uses the harness's own drop, duplicate, reorder and length-rewrite helpers, which reach the ordering and retransmit logic a byte flip cannot -- a flipped byte still arrives once, in sequence, at the right length. Both assert only that a corrupted handshake fails rather than crashes; the coverage is in the paths it takes on the way out. Both run in the ssl_hs group, which five of the six protocol modules already measure. internal.c 650/1722 -> 690/1722. The DTLS arms of both tests currently contribute nothing -- see the commit that follows. --- tests/api/test_ssl_hs.c | 257 ++++++++++++ tests/api/test_ssl_hs.h | 7 +- tests/include.am | 1 + .../test_internal_clienthello_whitebox.c | 369 ++++++++++++++++++ 4 files changed, 633 insertions(+), 1 deletion(-) create mode 100644 tests/unit-mcdc/test_internal_clienthello_whitebox.c diff --git a/tests/api/test_ssl_hs.c b/tests/api/test_ssl_hs.c index 80718deac75..a3139d8536d 100644 --- a/tests/api/test_ssl_hs.c +++ b/tests/api/test_ssl_hs.c @@ -2217,3 +2217,260 @@ int test_wolfSSL_hs_info_cb(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * Handshakes corrupted in flight. + * + * A conforming pair of endpoints produces one handshake, and every error path + * in the receive code stays dark no matter how many times it is run. Those + * paths are most of what remains uncovered in src/internal.c, and they are + * reached only by a peer that sends something wrong. + * + * No transport is needed to be that peer. test_memio already runs both + * endpoints through a plain byte buffer with credentials from certs/, and both + * sides are ours, so the buffer between them can be edited between rounds -- + * flip a bit in a length, in a type byte, in the middle of a certificate, in + * the key exchange -- and the handshake continues into whatever the receiver + * does about it. Same idea as sitting on the wire with a packet mangler, with + * neither a socket nor a second process. + * + * The assertion is deliberately weak: a corrupted handshake is *expected* to + * fail. What is being tested is that it fails rather than crashes, leaks or + * hangs, and the coverage comes from the paths it takes on the way out. + * ------------------------------------------------------------------------- */ +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES_BUILD) && \ + !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) + +/* One handshake, with one byte flipped at one point. `dir` selects the + * direction: 0 corrupts what the server sent, 1 corrupts what the client sent. + * `round` is how many exchange rounds to let pass first, which is what selects + * WHICH handshake message gets hit -- the ClientHello, the certificate, the + * key exchange, the Finished. */ +static int test_wire_mangle_one(method_provider mc, method_provider ms, + int round, int off, byte mask, int dir) +{ + struct test_memio_ctx test_ctx; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + int i; + int ret = 0; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + if (test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, mc, ms) + != 0) { + /* This build has no usable credentials for these methods; that is a + * configuration fact, not a failure. */ + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + return 0; + } + + for (i = 0; i < 12; i++) { + int rounds = 0; + + (void)test_memio_do_handshake(ssl_c, ssl_s, 1, &rounds); + + if (i == round) { + byte* buf = dir ? test_ctx.s_buff : test_ctx.c_buff; + int len = dir ? test_ctx.s_len : test_ctx.c_len; + + if (len > off) + buf[off] ^= mask; + else + ret = 1; /* nothing in flight here; note it and carry on */ + } + } + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + return ret; +} + +#endif + +int test_tls_wire_mangle(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES_BUILD) && \ + !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) + /* Offsets chosen against the record and handshake framing rather than at + * random: 0 is the record type, 1-2 the record version, 3-4 the record + * length, 5 the handshake type, 6-8 the handshake length, and the rest + * land inside the body -- a session id, a certificate, a key share. */ + static const int offsets[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13, + 20, 45, 80, 120, 200, 400, 900 }; + static const byte masks[] = { 0x01, 0x80, 0xff }; + int round, o, m, dir; + + for (round = 0; round < 7; round++) { + for (o = 0; o < (int)(sizeof(offsets) / sizeof(offsets[0])); o++) { + for (m = 0; m < (int)(sizeof(masks) / sizeof(masks[0])); m++) { + for (dir = 0; dir < 2; dir++) { + (void)test_wire_mangle_one(wolfTLSv1_2_client_method, + wolfTLSv1_2_server_method, round, offsets[o], + masks[m], dir); +#ifdef WOLFSSL_TLS13 + (void)test_wire_mangle_one(wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method, round, offsets[o], + masks[m], dir); +#endif +#ifdef WOLFSSL_DTLS + (void)test_wire_mangle_one(wolfDTLSv1_2_client_method, + wolfDTLSv1_2_server_method, round, offsets[o], + masks[m], dir); +#endif +#ifdef WOLFSSL_DTLS13 + (void)test_wire_mangle_one(wolfDTLSv1_3_client_method, + wolfDTLSv1_3_server_method, round, offsets[o], + masks[m], dir); +#endif + } + } + } + } + + /* A clean handshake through the same path, so every decision the corrupted + * runs took one way has its partner in this same binary. */ + ExpectIntEQ(test_wire_mangle_one(wolfTLSv1_2_client_method, + wolfTLSv1_2_server_method, 99, 0, 0x00, 0), 0); +#endif + return EXPECT_RESULT(); +} + +/* --------------------------------------------------------------------------- + * Handshakes with messages dropped, duplicated, reordered and truncated. + * + * Flipping a byte reaches the parsers' error arms. It does not reach the + * ordering and retransmit logic, because a flipped byte still arrives once, in + * sequence, at the right length. That logic -- duplicate detection, out-of- + * order rejection, the DTLS retransmit pool, the fragment reassembler -- is + * only entered when the SEQUENCE is wrong, and a conforming peer never gets it + * wrong. + * + * test_memio already knows how to do this to the buffer between the two + * endpoints: drop a message, move one ahead of another, copy one back in a + * second time, or rewrite its declared length. All four are on the same + * fixture as the byte mangler, and none of them needs a socket. + * ------------------------------------------------------------------------- */ +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES_BUILD) && \ + !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) + +enum wire_op { WIRE_DROP, WIRE_DUP, WIRE_MOVE, WIRE_TRUNC, WIRE_SHORTEN, + WIRE_NONE }; + +static int test_wire_seq_one(method_provider mc, method_provider ms, + int round, int op, int msgPos) +{ + struct test_memio_ctx test_ctx; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + int i; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + if (test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, mc, ms) + != 0) { + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + return 0; + } + + for (i = 0; i < 12; i++) { + int rounds = 0; + + (void)test_memio_do_handshake(ssl_c, ssl_s, 1, &rounds); + + if (i != round) + continue; + + switch (op) { + case WIRE_DROP: + /* the receiver never sees this message at all */ + (void)test_memio_drop_message(&test_ctx, 1, msgPos); + break; + case WIRE_DUP: { + /* the same message twice: what duplicate detection is for */ + char copy[2048]; + int copySz = (int)sizeof(copy); + + if (test_memio_copy_message(&test_ctx, 1, copy, ©Sz, + msgPos) == 0) { + (void)test_memio_inject_message(&test_ctx, 1, copy, copySz); + } + break; + } + case WIRE_MOVE: + /* arrives before the message it must follow */ + (void)test_memio_move_message(&test_ctx, 1, msgPos, + msgPos + 1); + break; + case WIRE_TRUNC: + /* declares more than it carries */ + (void)test_memio_modify_message_len(&test_ctx, 1, msgPos, + 4096); + break; + case WIRE_SHORTEN: + /* declares less than it carries, and loses its tail */ + (void)test_memio_modify_message_len(&test_ctx, 1, msgPos, 4); + (void)test_memio_remove_from_buffer(&test_ctx, 1, 5, 4); + break; + default: + break; + } + } + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + return 0; +} + +#endif + +int test_tls_wire_sequence(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES_BUILD) && \ + !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) + int round, op, pos; + + for (round = 0; round < 6; round++) { + for (op = 0; op < (int)WIRE_NONE; op++) { + for (pos = 0; pos < 3; pos++) { + (void)test_wire_seq_one(wolfTLSv1_2_client_method, + wolfTLSv1_2_server_method, round, op, pos); +#ifdef WOLFSSL_TLS13 + (void)test_wire_seq_one(wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method, round, op, pos); +#endif +#ifdef WOLFSSL_DTLS + /* DTLS is where dropping and reordering are not merely + * hostile but expected, so the retransmit and reassembly + * paths are entered rather than just refused. */ + (void)test_wire_seq_one(wolfDTLSv1_2_client_method, + wolfDTLSv1_2_server_method, round, op, pos); +#endif +#if defined(WOLFSSL_DTLS13) && defined(WOLFSSL_TLS13) + (void)test_wire_seq_one(wolfDTLSv1_3_client_method, + wolfDTLSv1_3_server_method, round, op, pos); +#endif + } + } + } + + /* the untouched partner, in this same binary */ + ExpectIntEQ(test_wire_seq_one(wolfTLSv1_2_client_method, + wolfTLSv1_2_server_method, 99, (int)WIRE_NONE, 0), 0); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_hs.h b/tests/api/test_ssl_hs.h index c273249601b..555dae2ec66 100644 --- a/tests/api/test_ssl_hs.h +++ b/tests/api/test_ssl_hs.h @@ -22,6 +22,9 @@ #ifndef TESTS_API_SSL_HS_H #define TESTS_API_SSL_HS_H +int test_tls_wire_mangle(void); +int test_tls_wire_sequence(void); + #include int test_wolfSSL_state_string_long(void); @@ -75,6 +78,8 @@ int test_wolfSSL_hs_info_cb(void); TEST_DECL_GROUP("ssl_hs", test_wolfSSL_hs_retry_alert_fail), \ TEST_DECL_GROUP("ssl_hs", test_wolfSSL_connect_ex_no_side), \ TEST_DECL_GROUP("ssl_hs", test_wolfSSL_hs_done_cb_error), \ - TEST_DECL_GROUP("ssl_hs", test_wolfSSL_hs_info_cb) + TEST_DECL_GROUP("ssl_hs", test_wolfSSL_hs_info_cb), \ + TEST_DECL_GROUP("ssl_hs", test_tls_wire_mangle), \ + TEST_DECL_GROUP("ssl_hs", test_tls_wire_sequence) #endif /* TESTS_API_SSL_HS_H */ diff --git a/tests/include.am b/tests/include.am index 1c5b7a530a1..1ae1c053a5a 100644 --- a/tests/include.am +++ b/tests/include.am @@ -166,6 +166,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_integer_whitebox.c \ tests/unit-mcdc/test_kdf_hash_fault_whitebox.c \ tests/unit-mcdc/test_internal_domain_whitebox.c \ + tests/unit-mcdc/test_internal_clienthello_whitebox.c \ tests/unit-mcdc/test_keys_whitebox.c \ tests/unit-mcdc/test_internal_record_whitebox.c \ tests/unit-mcdc/test_internal_sanity_whitebox.c \ diff --git a/tests/unit-mcdc/test_internal_clienthello_whitebox.c b/tests/unit-mcdc/test_internal_clienthello_whitebox.c new file mode 100644 index 00000000000..e54b70bf79d --- /dev/null +++ b/tests/unit-mcdc/test_internal_clienthello_whitebox.c @@ -0,0 +1,369 @@ +/* test_internal_clienthello_whitebox.c -- MC/DC white-box driver for + * DoClientHello in src/internal.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-1301, USA + */ + +/* FORGED FRAMES INSTEAD OF A HANDSHAKE. + * + * The leaf-function white-boxes in this directory have run out of leaves. What + * is left in internal.c is message handling, and the obvious way to reach it + * -- stand up two endpoints and run handshakes -- is both expensive and, for + * MC/DC, mostly useless: two conforming endpoints produce one ClientHello + * shape, so every operand that distinguishes a hostile hello from a friendly + * one is evaluated the same way every time. + * + * The cheaper move is to forge the frame. DoClientHello takes a byte buffer + * and a length; it does not take a socket, a peer, or a handshake. So the + * fixture is a REAL server WOLFSSL -- from a CTX with a certificate loaded, so + * suites, hashes and buffers are all properly constructed -- fed a + * hand-assembled ClientHello body. Real endpoint, forged input. Nothing on the + * wire, nothing to synchronise, and each vector differs from its partner in + * exactly the byte or flag under test. + * + * That is what reaches the decisions the handshake tests cannot: a version + * below the configured minimum, a compression list with no null method, a + * session id where a cookie is expected, a renegotiation info extension on a + * connection that never renegotiated, an extension set that contradicts the + * options the server was configured with. + * + * A send callback is installed because several of these paths answer with a + * fatal alert; without one the alert path dereferences a NULL CBIOSend. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Bail paths print, so "covered nothing" differs from "nothing to say". + */ + +#include + +#include + +#include +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && \ + !defined(NO_WOLFSSL_SERVER) && !defined(WOLFSSL_NO_TLS12) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + +static int g_calls; + +static int wb_send_sink(WOLFSSL* ssl, char* buf, int sz, void* ctx) +{ + (void)ssl; (void)buf; (void)ctx; + return sz; /* swallow alerts, report them fully written */ +} + +static int wb_recv_none(WOLFSSL* ssl, char* buf, int sz, void* ctx) +{ + (void)ssl; (void)buf; (void)sz; (void)ctx; + return WOLFSSL_CBIO_ERR_WANT_READ; +} + +/* ------------------------------------------------------------- the forger */ + +typedef struct { + byte buf[768]; + word32 len; +} Frame; + +static void fr_reset(Frame* f) { XMEMSET(f, 0, sizeof(*f)); } +static void fr_u8(Frame* f, byte v) +{ + if (f->len < sizeof(f->buf)) f->buf[f->len++] = v; +} +static void fr_u16(Frame* f, word16 v) { fr_u8(f, (byte)(v >> 8)); + fr_u8(f, (byte)(v & 0xff)); } +static void fr_fill(Frame* f, byte v, int n) +{ + int i; for (i = 0; i < n; i++) fr_u8(f, v); +} + +/* What a forged ClientHello may differ in. Each field exists because some + * decision in DoClientHello reads it. */ +typedef struct { + byte major, minor; /* the offered version */ + int sessionIdLen; /* 0, 32, or an illegal length */ + int cookieLen; /* DTLS only */ + int nSuites; /* cipher suite count (in suites, not bytes)*/ + int suiteBogus; /* offer suites the server cannot match */ + int compNo; /* offer the null compression method */ + int compZlib; /* offer zlib */ + int extReneg; /* renegotiation_info */ + int extTicket; /* session_ticket */ + int extEtm; /* encrypt_then_mac */ + int extEms; /* extended_master_secret */ + int extTruncate; /* declare more extension bytes than follow */ + const char* what; +} Hello; + +/* The suites the SERVER was configured with, echoed back. A forged hello with + * invented suite bytes fails MatchSuite and returns before the compression, + * extension and downgrade logic is ever reached -- which is what the first + * version of this driver did, and why it measured almost nothing. Reading + * ssl->suites is the difference between a frame that is refused at the door + * and one that gets deep enough for its one forged field to matter. */ +static void wb_build(Frame* f, const Hello* h, int dtls, const WOLFSSL* ssl) +{ + fr_reset(f); + fr_u8(f, h->major); + fr_u8(f, h->minor); + fr_fill(f, 0xAB, RAN_LEN); /* client random */ + + fr_u8(f, (byte)h->sessionIdLen); + fr_fill(f, 0xCD, h->sessionIdLen); + + if (dtls) { + fr_u8(f, (byte)h->cookieLen); + fr_fill(f, 0xEF, h->cookieLen); + } + + { + int have = (ssl->suites != NULL) ? ssl->suites->suiteSz / 2 : 0; + int n = h->nSuites; + int i; + + if (!h->suiteBogus && n > have) + n = have; + fr_u16(f, (word16)(n * 2)); + for (i = 0; i < n; i++) { + if (h->suiteBogus) { + fr_u8(f, 0x00); fr_u8(f, (byte)(0xF0 + i)); + } + else { + fr_u8(f, ssl->suites->suites[i * 2]); + fr_u8(f, ssl->suites->suites[i * 2 + 1]); + } + } + } + + { + int n = (h->compNo ? 1 : 0) + (h->compZlib ? 1 : 0); + fr_u8(f, (byte)n); + if (h->compZlib) fr_u8(f, ZLIB_COMPRESSION); + if (h->compNo) fr_u8(f, NO_COMPRESSION); + } + + /* extensions, assembled into a scratch frame first so the length is + * known before it is written */ + { + Frame ext; + fr_reset(&ext); + if (h->extReneg) { + fr_u16(&ext, HELLO_EXT_SIG_ALGO == 0 ? 0xFF01 : 0xFF01); + fr_u16(&ext, 1); + fr_u8(&ext, 0); /* empty renegotiated_connection */ + } + if (h->extTicket) { + fr_u16(&ext, TLSX_SESSION_TICKET); + fr_u16(&ext, 0); + } + if (h->extEtm) { + fr_u16(&ext, TLSX_ENCRYPT_THEN_MAC); + fr_u16(&ext, 0); + } + if (h->extEms) { + fr_u16(&ext, HELLO_EXT_EXTMS); + fr_u16(&ext, 0); + } + if (ext.len > 0 || h->extTruncate) { + word32 i; + fr_u16(f, (word16)(h->extTruncate ? ext.len + 16 : ext.len)); + for (i = 0; i < ext.len; i++) + fr_u8(f, ext.buf[i]); + } + } +} + +/* -------------------------------------------------------------- one vector */ + +/* The ssl is rebuilt per vector: DoClientHello writes session state, suites + * and extension lists into it, and a second hello on the same object would be + * a renegotiation rather than the case under test. */ +static void wb_hello(WOLFSSL_CTX* ctx, const Hello* h, int dtls, + int usingCompression, int useTicket, int encThenMac, + int downgrade, byte minDowngrade) +{ + WOLFSSL* ssl = wolfSSL_new(ctx); + Frame f; + word32 idx = 0; + + if (ssl == NULL) + return; + + wolfSSL_SSLSetIORecv(ssl, wb_recv_none); + wolfSSL_SSLSetIOSend(ssl, wb_send_sink); + + ssl->options.side = WOLFSSL_SERVER_END; + ssl->options.usingCompression = (byte)usingCompression; + ssl->options.useTicket = (byte)useTicket; + ssl->options.encThenMac = (byte)encThenMac; + ssl->options.downgrade = (byte)downgrade; + ssl->options.minDowngrade = minDowngrade; +#ifdef WOLFSSL_DTLS + ssl->options.dtls = (byte)dtls; + if (dtls) { + ssl->version.major = DTLS_MAJOR; + ssl->version.minor = DTLSv1_2_MINOR; + ssl->options.dtlsStateful = 1; + } +#endif + + wb_build(&f, h, dtls, ssl); + { + int r = DoClientHello(ssl, f.buf, &idx, f.len); + if (getenv("WB_TRACE")) + printf(" %-52s -> %d (idx %u of %u)\n", h->what, r, + (unsigned)idx, (unsigned)f.len); + } + g_calls++; + + wolfSSL_free(ssl); +} + +/* ---------------------------------------------------------------- main */ + +int main(void) +{ + WOLFSSL_CTX* ctx = NULL; + size_t i; + int dtls; + + /* The accepting baseline, then one field changed per row. A conforming + * client sends the first row and nothing else; every other row is a hello + * a server must survive but no test peer produces. */ + static const Hello rows[] = { + /* maj min sid ck ns bog no zl rg tk em ems tr what */ + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + "baseline TLS 1.2" }, + { SSLv3_MAJOR, TLSv1_3_MINOR, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + "offers 1.3 in the legacy field" }, + { SSLv3_MAJOR, TLSv1_1_MINOR, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + "offers 1.1" }, + { SSLv3_MAJOR, TLSv1_MINOR, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + "offers 1.0" }, + { SSLv3_MAJOR, SSLv3_MINOR, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + "offers SSLv3" }, + { 0x02, 0x00, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + "a major version from no protocol" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 32, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + "carries a session id" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 64, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, + "session id longer than the field allows" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, + "offers only suites the server has not got" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + "offers no cipher suites at all" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0,64, 0, 1, 0, 0, 0, 0, 0, 0, + "offers sixty-four suites" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, + "zlib only: no null compression method" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, + "both compression methods" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + "an empty compression list" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, + "renegotiation_info on a fresh connection" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, + "asks for a session ticket" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, + "asks for encrypt-then-mac" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, + "asks for extended master secret" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, + "asks for everything at once" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, + "extension length longer than the extensions" }, + { SSLv3_MAJOR, TLSv1_2_MINOR, 32, 8, 1, 0, 1, 0, 0, 0, 0, 0, 0, + "session id and a cookie" }, + }; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal clienthello white-box: wolfSSL_Init failed\n"); + goto done; + } + + /* A server CTX with a real certificate: wolfSSL_new returns NULL for a + * server with no certificate loaded, and a NULL ssl is how an earlier + * white-box in this campaign silently covered nothing while exiting 0. */ + ctx = wolfSSL_CTX_new(wolfSSLv23_server_method()); + if (ctx == NULL) { + printf("internal clienthello white-box: CTX_new failed\n"); + goto done; + } + if (wolfSSL_CTX_use_certificate_file(ctx, "certs/server-cert.pem", + WOLFSSL_FILETYPE_PEM) + != WOLFSSL_SUCCESS || + wolfSSL_CTX_use_PrivateKey_file(ctx, "certs/server-key.pem", + WOLFSSL_FILETYPE_PEM) + != WOLFSSL_SUCCESS) { + printf("internal clienthello white-box: no server credentials, " + "nothing driven\n"); + goto done; + } + + /* Every hello against both compression settings and both downgrade + * policies, so the server-side operands of each decision get their pair + * as well as the client-side bytes. */ + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + /* downgrade on for the bulk of the sweep: with it off, any hello + * whose version differs from the server's is refused at the version + * check and the rest of the function is never entered. Both settings + * still appear, so that operand keeps its pair. */ + wb_hello(ctx, &rows[i], 0, 0, 0, 0, 1, SSLv3_MINOR); + wb_hello(ctx, &rows[i], 0, 1, 0, 0, 1, SSLv3_MINOR); + wb_hello(ctx, &rows[i], 0, 0, 1, 1, 1, SSLv3_MINOR); + wb_hello(ctx, &rows[i], 0, 1, 1, 1, 1, SSLv3_MINOR); + wb_hello(ctx, &rows[i], 0, 0, 0, 0, 0, TLSv1_MINOR); + /* downgrade allowed, with a floor above what the row offers, which + * is what belowMinDowngrade exists to detect */ + wb_hello(ctx, &rows[i], 0, 0, 0, 0, 1, TLSv1_2_MINOR); + wb_hello(ctx, &rows[i], 0, 0, 0, 0, 1, SSLv3_MINOR); + } + +#ifdef WOLFSSL_DTLS + for (dtls = 1; dtls < 2; dtls++) + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) + wb_hello(ctx, &rows[i], dtls, 0, 0, 0, 1, SSLv3_MINOR); +#else + (void)dtls; +#endif + + printf("internal clienthello white-box: %d forged hellos\n", g_calls); + +done: + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal clienthello white-box: skipped (server TLS 1.2 or " + "certs not built)\n"); + return 0; +} + +#endif From 65d20cd397e3e95a9e577b6f3eecb5f2da905eed Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 10:36:13 +0200 Subject: [PATCH 16/60] tests: corrupt DTLS handshakes in flight, and record what it does not reach Same mechanism as the TLS wire mangler, with the offsets named after DTLS framing rather than TLS: a DTLS record header is thirteen bytes, and the handshake header carries a message sequence, fragment offset and fragment length that TLS has no equivalent of. Replay, drop and reorder are included because DTLS is built to tolerate them, so they enter the retransmit pool and the reassembler rather than being refused. Two mistakes are fixed here and worth keeping written down. The endpoints are stepped by hand rather than through test_memio_do_handshake, which runs the client and the server in one round and leaves nothing in flight to corrupt -- the first version passed in 2.3 seconds and measured nothing. And the offsets now reach past the fixed header into the extension block. It still earns zero. Three measurements put dtls.c at exactly 16/56 and dtls13.c at exactly 70/132 every time; an identical number means the code is not entered, not that the vectors are weak. dtls.c's residue is entirely inside SendStatelessReplyDtls13's extension parsing, and a corrupted DTLS record is dropped by the record layer before that parser sees it -- which is the tolerance DTLS exists to provide. Reaching it needs a well-formed record carrying a malformed extension block: a built ClientHello, not a corrupted one. The sweep is kept narrow so it costs seconds until that fixture exists. --- tests/api/test_dtls.c | 210 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_dtls.h | 5 + 2 files changed, 215 insertions(+) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index f91ea99eb1c..c43b5305ba8 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -8154,3 +8154,213 @@ int test_wolfSSL_set_secret(void) return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * DTLS handshakes corrupted, replayed, dropped and reordered in flight. + * + * The TLS version of this (test_tls_wire_mangle in test_ssl_hs.c) flips a bit + * at a fixed offset. Pointed at DTLS it measured nothing, for two reasons + * worth recording because both are DTLS-specific: + * + * 1. The offsets were wrong. A DTLS record header is thirteen bytes, not + * five -- type, version, epoch, a six-byte sequence number, length -- and + * the handshake header carries a further message sequence, fragment + * offset and fragment length. Offsets picked for TLS framing land in the + * middle of the sequence number and hit nothing interesting. + * + * 2. DTLS is *designed* to tolerate a corrupted record: it drops it and + * waits for the retransmission. Corrupting bytes at random therefore + * exercises the discard path and stops. What reaches the interesting code + * -- the replay window, the retransmit pool, the fragment reassembler -- + * is a record that is well-formed but arrives twice, out of order, or + * claiming an epoch or sequence number it should not. + * + * So this sweep targets the DTLS header fields by name, and leans on replay + * and reordering rather than corruption. Same fixture as the TLS version: + * test_memio, credentials from certs/, no socket and no second process. + * ------------------------------------------------------------------------- */ +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES_BUILD) && defined(WOLFSSL_DTLS) + +/* Offsets into a DTLS record, by field rather than by guess. */ +#define DW_TYPE 0 +#define DW_VERSION 1 +#define DW_EPOCH 3 +#define DW_SEQ_HI 5 +#define DW_SEQ_LO 10 +#define DW_RECLEN 11 +#define DW_HS_TYPE 13 +#define DW_HS_LEN 14 +#define DW_MSG_SEQ 17 +#define DW_FRAG_OFF 19 +#define DW_FRAG_LEN 22 +#define DW_BODY 26 +/* The extension block of a DTLS 1.3 ClientHello starts well past the fixed + * header: two version bytes, a 32-byte random, a session id, a cookie, the + * cipher suite list and the compression list come first. Flips inside the + * first sixty bytes never reach it, which is why the first version of this + * sweep left SendStatelessReplyDtls13 -- where every remaining condition in + * dtls.c lives -- completely untouched. */ +#define DW_EXTS 110 + +enum dtls_wire_op { + DW_FLIP, /* corrupt one named header field */ + DW_REPLAY, /* deliver the same record a second time */ + DW_DROP, /* lose a record, so the peer must retransmit */ + DW_REORDER, /* deliver records out of order */ + DW_TRUNC, /* claim a longer fragment than is carried */ + DW_OP_COUNT +}; + +static int test_dtls_wire_one(method_provider mc, method_provider ms, + int round, int op, int off, byte mask) +{ + struct test_memio_ctx test_ctx; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + int i; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + if (test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, mc, ms) + != 0) { + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + return 0; + } + + /* Step the two endpoints by hand rather than through + * test_memio_do_handshake. That helper runs the client AND the server in + * one round, so by the time it returns the buffer has already been + * drained and there is nothing left in flight to corrupt -- which is why + * the first version of this test ran for two seconds, passed, and + * measured nothing. Here each half-round leaves exactly one peer's flight + * sitting in the buffer, and the mangle is applied to that flight before + * the other side is allowed to read it. */ + for (i = 0; i < 16; i++) { + int isClientTurn = ((i % 2) == 0); + byte* buf; + int* len; + + if (isClientTurn) + (void)wolfSSL_connect(ssl_c); /* client writes into s_buff */ + else + (void)wolfSSL_accept(ssl_s); /* server writes into c_buff */ + + /* the flight that was just produced, still unread by its peer */ + buf = isClientTurn ? test_ctx.s_buff : test_ctx.c_buff; + len = isClientTurn ? &test_ctx.s_len : &test_ctx.c_len; + + if (i != round || *len <= 0) + continue; + + switch (op) { + case DW_FLIP: + if (*len > off) + buf[off] ^= mask; + break; + case DW_REPLAY: { + /* The same record delivered twice is what the replay window + * exists to refuse, and a conforming peer never sends it. */ + char copy[2048]; + int copySz = (int)sizeof(copy); + + if (test_memio_copy_message(&test_ctx, isClientTurn, copy, + ©Sz, 0) == 0) { + (void)test_memio_inject_message(&test_ctx, isClientTurn, + copy, copySz); + } + break; + } + case DW_DROP: + /* A lost flight: the peer's retransmit timer and pool are the + * code this reaches, and nothing else does. */ + (void)test_memio_drop_message(&test_ctx, isClientTurn, 0); + break; + case DW_REORDER: + (void)test_memio_move_message(&test_ctx, isClientTurn, 0, 1); + break; + case DW_TRUNC: + /* A fragment that claims more than it carries drives the + * reassembler's bounds checks. */ + (void)test_memio_modify_message_len(&test_ctx, isClientTurn, + 0, 4096); + break; + default: + break; + } + } + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + return 0; +} + +static int test_dtls_wire_sweep(method_provider mc, method_provider ms) +{ + /* Named header fields, plus two body offsets. */ + static const int offsets[] = { + DW_TYPE, DW_VERSION, DW_EPOCH, DW_EPOCH + 1, DW_SEQ_HI, DW_SEQ_HI + 2, + DW_SEQ_LO, DW_RECLEN, DW_RECLEN + 1, DW_HS_TYPE, DW_HS_LEN, + DW_HS_LEN + 2, DW_MSG_SEQ, DW_MSG_SEQ + 1, DW_FRAG_OFF, + DW_FRAG_OFF + 2, DW_FRAG_LEN, DW_FRAG_LEN + 2, DW_BODY, DW_BODY + 40, + DW_EXTS, DW_EXTS + 32, DW_EXTS + 90 + }; + static const byte masks[] = { 0x01, 0xff }; + int round, o, m, op; + + /* Deliberately narrow. This sweep is a robustness guard, not a coverage + * win: measured against the campaign it adds ZERO MC/DC on dtls.c and + * dtls13.c, three separate attempts, the union landing on exactly 16/56 + * and 70/132 each time. An identical number is the signature of code that + * is never entered, not of vectors that are too weak, and the reason is + * that dtls.c's entire residue lives in SendStatelessReplyDtls13's + * extension parsing -- a corrupted DTLS record is discarded by the record + * layer before that parser ever sees it, which is exactly the tolerance + * DTLS is designed for. Reaching it needs a well-formed record carrying a + * deliberately malformed extension block, which means building the + * ClientHello rather than corrupting one. Kept at this size so it costs + * seconds rather than minutes until that fixture exists. */ + for (round = 0; round < 3; round++) { + /* the sequence-level operations, which do not need an offset */ + for (op = DW_REPLAY; op < (int)DW_OP_COUNT; op++) + (void)test_dtls_wire_one(mc, ms, round, op, 0, 0); + + /* and the field-level corruption */ + for (o = 0; o < (int)(sizeof(offsets) / sizeof(offsets[0])); o++) + for (m = 0; m < (int)(sizeof(masks) / sizeof(masks[0])); m++) + (void)test_dtls_wire_one(mc, ms, round, DW_FLIP, offsets[o], + masks[m]); + } + + /* the clean handshake, so every decision above has its partner here */ + return test_dtls_wire_one(mc, ms, 99, DW_FLIP, 0, 0x00); +} + +#endif + +int test_dtls12_wire_mangle(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES_BUILD) && \ + defined(WOLFSSL_DTLS) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) + ExpectIntEQ(test_dtls_wire_sweep(wolfDTLSv1_2_client_method, + wolfDTLSv1_2_server_method), 0); +#endif + return EXPECT_RESULT(); +} + +int test_dtls13_wire_mangle(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES_BUILD) && \ + defined(WOLFSSL_DTLS13) && defined(WOLFSSL_TLS13) && !defined(NO_RSA) + ExpectIntEQ(test_dtls_wire_sweep(wolfDTLSv1_3_client_method, + wolfDTLSv1_3_server_method), 0); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_dtls.h b/tests/api/test_dtls.h index 68904fb6123..bc11444ae63 100644 --- a/tests/api/test_dtls.h +++ b/tests/api/test_dtls.h @@ -22,6 +22,9 @@ #ifndef TESTS_API_DTLS_H #define TESTS_API_DTLS_H +int test_dtls12_wire_mangle(void); +int test_dtls13_wire_mangle(void); + int test_dtls12_basic_connection_id(void); int test_wolfSSL_dtls_cid_parse(void); int test_wolfSSL_dtls_cid_args(void); @@ -131,6 +134,8 @@ int test_WOLFSSL_dtls_version_alert(void); TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_cid_parse), \ TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_cid_args), \ TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_set_pending_peer), \ + TEST_DECL_GROUP("dtls", test_dtls12_wire_mangle), \ + TEST_DECL_GROUP("dtls", test_dtls13_wire_mangle), \ TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_set_pending_peer_not_newest),\ TEST_DECL_GROUP("dtls", test_dtls13_new_connection_id), \ TEST_DECL_GROUP("dtls", test_dtls13_new_connection_id_not_negotiated), \ From 8bbf3a710d2de812343e766c1d8243dbde4e2b0b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 10:52:57 +0200 Subject: [PATCH 17/60] tests: a scheduling datagram transport for DTLS, with 24 packet forgeries test_memio is a byte stream; DTLS is datagrams, and every guardrail in the protocol is about which datagram arrives, in what order, how many times, and carrying which epoch and sequence number. This replaces the transport rather than editing its buffer: each datagram the stack sends is captured whole, its record header parsed, and a policy consulted before it is queued -- deliver, drop, hold for n rounds, duplicate, rewrite a header field, coalesce with its neighbour, truncate. The receiver then reads whole datagrams out of the queue as a UDP socket would. 24 policies, each named for the guardrail it provokes: replay, sequence number past and future the replay window, unknown and zeroed epoch, lost flight, reordering, fragment offset past the message, fragment longer than the message, overlapping fragments, message sequence forward and backward, record length longer and shorter than the datagram, content-type confusion, two records in one datagram, truncation, and body corruption at six depths inside the extension block plus the echoed cookie. No key material is needed: the DTLS record header is not encrypted, and every guardrail above keys off it. A secret callback is wired behind HAVE_SECRET_CALLBACK for cases that later need a protected body; that macro is not in the campaign option list, so it compiles out there. df_run reports whether the handshake actually completed rather than whether the loop ran -- the first version returned success unconditionally, which is how a transport that never connects still passes. This adds no MC/DC on dtls.c, and the export says why: the file is 85% line covered and 20% MC/DC covered in this variant. SendStatelessReplyDtls13 is entered -- 247 executed segments in its range -- so the problem is not reach. The residue needs ClientHellos that are well-formed but semantically specific: one with no supported_versions, one offering PSK modes, one whose key share names a group the server does not have, one echoing a corrupted cookie. Corruption cannot produce those; they have to be built. The builder exists in tests/unit-mcdc/test_internal_clienthello_whitebox.c and wants pointing down this transport. --- tests/api/test_dtls.c | 588 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_dtls.h | 4 + 2 files changed, 592 insertions(+) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index c43b5305ba8..22db8fd3e93 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -8364,3 +8364,591 @@ int test_dtls13_wire_mangle(void) #endif return EXPECT_RESULT(); } + +/* =========================================================================== + * A datagram FIFO transport for DTLS, with per-packet scheduling. + * + * WHY THIS AND NOT THE BYTE-BUFFER MANGLER. + * + * test_memio is a byte stream. DTLS is not: it is datagrams, and every + * guardrail in the protocol is about which datagram arrives, in what order, + * how many times, and carrying which epoch and sequence number. Editing a + * byte in a shared buffer cannot express "deliver this one twice", "hold that + * one until after the next", "drop this flight and see if it is retransmitted" + * -- and a byte flipped at random is simply discarded by the record layer, + * which is the tolerance DTLS is built to provide. Three separate sweeps of + * that kind measured exactly zero on dtls.c. + * + * So this replaces the transport instead. Every datagram the stack sends is + * captured as a discrete packet, its record header is parsed, and a policy is + * consulted BEFORE it is queued: deliver it, drop it, hold it for n rounds, + * duplicate it, rewrite a header field, coalesce it with its neighbour. The + * receiving side then reads whole datagrams out of the queue, exactly as a + * UDP socket would deliver them. + * + * That is enough to sit in the middle of the flow and see each packet before + * deciding its fate, with no socket, no second process and no scheduler races. + * + * ON ENCRYPTION. The record header -- content type, version, epoch, sequence + * number and length -- is NOT encrypted in DTLS 1.2, and in DTLS 1.3 the + * initial flight is plaintext. Every guardrail targeted below (replay window, + * epoch handling, fragment reassembly, cookie exchange, records-per-datagram) + * keys off those fields, so the forgeries need no key material at all. The + * secret callback is wired anyway, behind HAVE_SECRET_CALLBACK, for the cases + * that later need to read a protected body; it is not enabled in the campaign + * option list, so it compiles out there. + * ========================================================================= */ +#if defined(WOLFSSL_DTLS) && !defined(NO_RSA) && !defined(NO_CERTS) && \ + !defined(NO_FILESYSTEM) + +#define DF_MAX_PKT 384 +#define DF_MAX_SZ 1600 + +/* DTLS record header, by field. */ +#define DFH_TYPE 0 +#define DFH_VER 1 +#define DFH_EPOCH 3 +#define DFH_SEQ 5 /* 6 bytes, big endian */ +#define DFH_LEN 11 /* 2 bytes */ +#define DFH_HDR_SZ 13 +/* DTLS handshake header, inside the record */ +#define DFHS_TYPE 0 +#define DFHS_LEN 1 /* 3 bytes */ +#define DFHS_MSGSEQ 4 /* 2 bytes */ +#define DFHS_FRAGOFF 6 /* 3 bytes */ +#define DFHS_FRAGLEN 9 /* 3 bytes */ +#define DFHS_HDR_SZ 12 + +typedef struct DfPkt { + byte data[DF_MAX_SZ]; + int len; + int toServer; /* 1: client -> server, 0: server -> client */ + int idx; /* production order, per direction */ + int hold; /* rounds still to withhold */ + int taken; /* already handed to the receiver */ + /* parsed, for policies that want to target a specific record */ + byte type; + word16 epoch; + byte hsType; + word16 msgSeq; +} DfPkt; + +struct DfCtx; +typedef void (*DfPolicy)(struct DfCtx* c, DfPkt* p); + +typedef struct DfCtx { + DfPkt q[DF_MAX_PKT]; + int n; + int seqTo[2]; /* per-direction production counter */ + DfPolicy policy; + int target; /* which packet of that direction to act on */ + int nDrop, nDup, nMod, nHold, nCoalesce; +#ifdef HAVE_SECRET_CALLBACK + int nSecrets; +#endif +} DfCtx; + +static void df_parse(DfPkt* p) +{ + p->type = 0; p->epoch = 0; p->hsType = 0xFF; p->msgSeq = 0; + if (p->len < DFH_HDR_SZ) + return; + p->type = p->data[DFH_TYPE]; + p->epoch = (word16)((p->data[DFH_EPOCH] << 8) | p->data[DFH_EPOCH + 1]); + if (p->type == handshake && p->len >= DFH_HDR_SZ + DFHS_HDR_SZ) { + const byte* hs = p->data + DFH_HDR_SZ; + p->hsType = hs[DFHS_TYPE]; + p->msgSeq = (word16)((hs[DFHS_MSGSEQ] << 8) | hs[DFHS_MSGSEQ + 1]); + } +} + +/* ------------------------------------------------------------ IO callbacks */ + +static int df_send(WOLFSSL* ssl, char* buf, int sz, void* ctx) +{ + DfCtx* c = (DfCtx*)ctx; + DfPkt* p; + int toServer = (wolfSSL_GetSide(ssl) != WOLFSSL_SERVER_END); + + if (c->n >= DF_MAX_PKT || sz <= 0 || sz > DF_MAX_SZ) + return sz; /* silently absorb: a full queue is not a failure */ + + p = &c->q[c->n]; + XMEMSET(p, 0, sizeof(*p)); + XMEMCPY(p->data, buf, (size_t)sz); + p->len = sz; + p->toServer = toServer; + p->idx = c->seqTo[toServer]++; + df_parse(p); + c->n++; + + /* The policy sees the packet in flight, with its header parsed, and may + * edit it, drop it, delay it or clone it before anyone receives it. */ + if (c->policy != NULL) + c->policy(c, p); + + return sz; +} + +static int df_recv(WOLFSSL* ssl, char* buf, int sz, void* ctx) +{ + DfCtx* c = (DfCtx*)ctx; + int wantServer = (wolfSSL_GetSide(ssl) == WOLFSSL_SERVER_END); + int i; + + for (i = 0; i < c->n; i++) { + DfPkt* p = &c->q[i]; + + if (p->taken || p->len <= 0 || p->toServer != wantServer) + continue; + if (p->hold > 0) { + /* held back: a later packet may overtake it, which is the point */ + p->hold--; + continue; + } + if (p->len > sz) + return WOLFSSL_CBIO_ERR_GENERAL; + XMEMCPY(buf, p->data, (size_t)p->len); + p->taken = 1; + return p->len; + } + return WOLFSSL_CBIO_ERR_WANT_READ; +} + +/* ------------------------------------------------------- packet operations */ + +static DfPkt* df_clone(DfCtx* c, const DfPkt* src) +{ + DfPkt* p; + + if (c->n >= DF_MAX_PKT) + return NULL; + p = &c->q[c->n++]; + XMEMCPY(p, src, sizeof(*p)); + p->taken = 0; + p->hold = 0; + return p; +} + +static void df_set_seq(DfPkt* p, word32 hi, word32 lo) +{ + if (p->len < DFH_HDR_SZ) + return; + p->data[DFH_SEQ + 0] = (byte)((hi >> 8) & 0xff); + p->data[DFH_SEQ + 1] = (byte)(hi & 0xff); + p->data[DFH_SEQ + 2] = (byte)((lo >> 24) & 0xff); + p->data[DFH_SEQ + 3] = (byte)((lo >> 16) & 0xff); + p->data[DFH_SEQ + 4] = (byte)((lo >> 8) & 0xff); + p->data[DFH_SEQ + 5] = (byte)(lo & 0xff); +} + +static void df_set_epoch(DfPkt* p, word16 e) +{ + if (p->len < DFH_HDR_SZ) + return; + p->data[DFH_EPOCH] = (byte)(e >> 8); + p->data[DFH_EPOCH + 1] = (byte)(e & 0xff); +} + +static void df_set_u24(byte* at, word32 v) +{ + at[0] = (byte)((v >> 16) & 0xff); + at[1] = (byte)((v >> 8) & 0xff); + at[2] = (byte)(v & 0xff); +} + +/* ============================ the forgeries ============================== + * + * Each one names the protocol guardrail it exists to provoke. All of them + * are things a conforming peer never does and a network or an attacker + * routinely does, which is exactly the set the handshake tests cannot reach. + */ + +/* Replay window: the same datagram delivered twice. RFC 6347 4.1.2.6. */ +static void df_pol_replay(DfCtx* c, DfPkt* p) +{ + if (p->idx == c->target && df_clone(c, p) != NULL) + c->nDup++; +} + +/* Replay window, far future: a sequence number beyond the window's right + * edge, which must slide the window rather than be accepted blindly. */ +static void df_pol_seq_future(DfCtx* c, DfPkt* p) +{ + if (p->idx != c->target) return; + df_set_seq(p, 0, 0x000FFFFFu); + c->nMod++; +} + +/* Replay window, far past: a sequence number below the window's left edge, + * which must be discarded. */ +static void df_pol_seq_past(DfCtx* c, DfPkt* p) +{ + if (p->idx != c->target) return; + df_set_seq(p, 0, 0); + c->nMod++; +} + +/* Epoch handling: a record claiming an epoch whose keys do not exist. */ +static void df_pol_epoch_future(DfCtx* c, DfPkt* p) +{ + if (p->idx != c->target) return; + df_set_epoch(p, (word16)(p->epoch + 3)); + c->nMod++; +} + +/* Epoch handling: a record claiming epoch 0 -- i.e. unprotected -- after the + * epoch has advanced. This is the plaintext-injection case. */ +static void df_pol_epoch_zero(DfCtx* c, DfPkt* p) +{ + if (p->idx != c->target) return; + df_set_epoch(p, 0); + c->nMod++; +} + +/* Loss: the flight never arrives, so the peer must retransmit it. */ +static void df_pol_drop(DfCtx* c, DfPkt* p) +{ + if (p->idx != c->target) return; + p->len = 0; + c->nDrop++; +} + +/* Reordering: hold this datagram so the next one overtakes it. */ +static void df_pol_reorder(DfCtx* c, DfPkt* p) +{ + if (p->idx != c->target) return; + p->hold = 2; + c->nHold++; +} + +/* Fragment reassembly: a fragment offset past the end of the message. */ +static void df_pol_frag_beyond(DfCtx* c, DfPkt* p) +{ + byte* hs; + + if (p->idx != c->target || p->type != handshake) return; + if (p->len < DFH_HDR_SZ + DFHS_HDR_SZ) return; + hs = p->data + DFH_HDR_SZ; + df_set_u24(hs + DFHS_FRAGOFF, 0x00FFFFu); + c->nMod++; +} + +/* Fragment reassembly: a fragment longer than the message it belongs to. */ +static void df_pol_frag_over(DfCtx* c, DfPkt* p) +{ + byte* hs; + + if (p->idx != c->target || p->type != handshake) return; + if (p->len < DFH_HDR_SZ + DFHS_HDR_SZ) return; + hs = p->data + DFH_HDR_SZ; + df_set_u24(hs + DFHS_FRAGLEN, 0x00FFFFu); + c->nMod++; +} + +/* Fragment reassembly: two fragments that overlap, claiming the same bytes + * of the message with different content. */ +static void df_pol_frag_overlap(DfCtx* c, DfPkt* p) +{ + DfPkt* dup; + byte* hs; + + if (p->idx != c->target || p->type != handshake) return; + if (p->len < DFH_HDR_SZ + DFHS_HDR_SZ + 8) return; + + dup = df_clone(c, p); + if (dup == NULL) return; + hs = dup->data + DFH_HDR_SZ; + /* same offset, shorter length, different body */ + df_set_u24(hs + DFHS_FRAGLEN, 4); + dup->data[DFH_HDR_SZ + DFHS_HDR_SZ] ^= 0xff; + c->nDup++; +} + +/* Handshake ordering: a message sequence number from the future, which the + * receiver must buffer rather than process. */ +static void df_pol_msgseq_jump(DfCtx* c, DfPkt* p) +{ + byte* hs; + + if (p->idx != c->target || p->type != handshake) return; + if (p->len < DFH_HDR_SZ + DFHS_HDR_SZ) return; + hs = p->data + DFH_HDR_SZ; + hs[DFHS_MSGSEQ] = (byte)((p->msgSeq + 7) >> 8); + hs[DFHS_MSGSEQ + 1] = (byte)((p->msgSeq + 7) & 0xff); + c->nMod++; +} + +/* Handshake ordering: a message sequence already processed. */ +static void df_pol_msgseq_back(DfCtx* c, DfPkt* p) +{ + byte* hs; + + if (p->idx != c->target || p->type != handshake) return; + if (p->len < DFH_HDR_SZ + DFHS_HDR_SZ) return; + hs = p->data + DFH_HDR_SZ; + hs[DFHS_MSGSEQ] = 0; + hs[DFHS_MSGSEQ + 1] = 0; + c->nMod++; +} + +/* Record framing: a length field longer than the datagram carries. */ +static void df_pol_reclen_long(DfCtx* c, DfPkt* p) +{ + if (p->idx != c->target || p->len < DFH_HDR_SZ) return; + p->data[DFH_LEN] = 0x0f; + p->data[DFH_LEN + 1] = 0xff; + c->nMod++; +} + +/* Record framing: a length field shorter than the datagram carries, leaving + * a trailing stub the receiver must treat as a second record. */ +static void df_pol_reclen_short(DfCtx* c, DfPkt* p) +{ + if (p->idx != c->target || p->len < DFH_HDR_SZ + 4) return; + p->data[DFH_LEN] = 0; + p->data[DFH_LEN + 1] = 2; + c->nMod++; +} + +/* Content type confusion: handshake bytes announced as application data, + * alert or ack. */ +static void df_pol_type_swap(DfCtx* c, DfPkt* p) +{ + static const byte types[3] = { application_data, alert, change_cipher_spec }; + + if (p->idx != c->target || p->len < DFH_HDR_SZ) return; + p->data[DFH_TYPE] = types[p->idx % 3]; + c->nMod++; +} + +/* Datagram packing: two records in one datagram, which DTLS permits and the + * single-record path must therefore handle. */ +static void df_pol_coalesce(DfCtx* c, DfPkt* p) +{ + DfPkt* prev; + int i; + + if (p->idx != c->target || c->n < 2) return; + for (i = c->n - 2; i >= 0; i--) { + prev = &c->q[i]; + if (prev->toServer != p->toServer || prev->len <= 0 || prev->taken) + continue; + if (prev->len + p->len > DF_MAX_SZ) + return; + XMEMCPY(prev->data + prev->len, p->data, (size_t)p->len); + prev->len += p->len; + p->len = 0; /* it now travels inside its predecessor */ + c->nCoalesce++; + return; + } +} + +/* Truncation: the datagram is cut in half in flight. */ +static void df_pol_truncate(DfCtx* c, DfPkt* p) +{ + if (p->idx != c->target || p->len < 8) return; + p->len /= 2; + c->nMod++; +} + +#ifdef HAVE_SECRET_CALLBACK +/* Wired so a later forgery can read a protected record. Every forgery above + * works on the plaintext record header and needs none of this. */ +static int df_secret_cb(WOLFSSL* ssl, int id, const unsigned char* secret, + int secretSz, void* ctx) +{ + DfCtx* c = (DfCtx*)ctx; + + (void)ssl; (void)id; (void)secret; (void)secretSz; + if (c != NULL) + c->nSecrets++; + return 0; +} +#endif + + +/* Body corruption with the framing left intact. + * + * The header forgeries above cannot reach SendStatelessReplyDtls13, where + * every remaining condition in dtls.c lives: that code parses the + * ClientHello's EXTENSIONS, and a record whose header has been tampered with + * is discarded by the record layer long before the extension parser runs. + * + * These policies therefore leave type, epoch, sequence and length untouched + * and corrupt only the message body, at depths that land in the extension + * block -- past the two version bytes, the 32-byte random, the session id, + * the cookie, the cipher suite list and the compression list. The datagram + * stays well-formed, so it is accepted, parsed, and rejected on its contents + * rather than its framing. That is the difference between exercising the + * discard path and exercising the guardrail. + */ +static void df_pol_body_at(DfCtx* c, DfPkt* p, int depth) +{ + int at = DFH_HDR_SZ + DFHS_HDR_SZ + depth; + + if (p->idx != c->target || p->type != handshake) return; + if (p->hsType != client_hello) return; + if (p->len <= at) return; + p->data[at] ^= 0xff; + c->nMod++; +} + +static void df_pol_body_exts(DfCtx* c, DfPkt* p) { df_pol_body_at(c, p, 80); } +static void df_pol_body_exts2(DfCtx* c, DfPkt* p) { df_pol_body_at(c, p, 96); } +static void df_pol_body_exts3(DfCtx* c, DfPkt* p) { df_pol_body_at(c, p, 120); } +static void df_pol_body_exts4(DfCtx* c, DfPkt* p) { df_pol_body_at(c, p, 150); } +static void df_pol_body_exts5(DfCtx* c, DfPkt* p) { df_pol_body_at(c, p, 190); } +static void df_pol_body_exts6(DfCtx* c, DfPkt* p) { df_pol_body_at(c, p, 240); } + +/* The cookie a DTLS 1.3 server issued in its HelloRetryRequest, corrupted in + * the ClientHello that echoes it back. This is the `!cookieGood` operand, and + * it is the whole reason the stateless path has a rejection branch: a client + * that echoes the cookie correctly never takes it. The cookie sits early in + * the extension block of the second ClientHello, so a sweep of the first + * hundred body bytes of CH2 covers it without having to locate it exactly. */ +static void df_pol_cookie(DfCtx* c, DfPkt* p) +{ + int i; + int base = DFH_HDR_SZ + DFHS_HDR_SZ + 40; + + if (p->type != handshake || p->hsType != client_hello) return; + if (p->msgSeq == 0) return; /* only the second ClientHello */ + for (i = 0; i < 24 && p->len > base + i; i++) + p->data[base + i] ^= 0x5a; + c->nMod++; +} + +/* ------------------------------------------------------------- the harness */ + +static int df_run(method_provider mc, method_provider ms, + DfPolicy policy, int target) +{ + DfCtx* c = NULL; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + int i; + int ret = -1; + + c = (DfCtx*)XMALLOC(sizeof(DfCtx), NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (c == NULL) + return -1; + XMEMSET(c, 0, sizeof(*c)); + c->policy = policy; + c->target = target; + + ctx_c = wolfSSL_CTX_new(mc()); + ctx_s = wolfSSL_CTX_new(ms()); + if (ctx_c == NULL || ctx_s == NULL) + goto out; + + wolfSSL_CTX_set_verify(ctx_c, WOLFSSL_VERIFY_NONE, NULL); + if (wolfSSL_CTX_load_verify_locations(ctx_c, caCertFile, NULL) + != WOLFSSL_SUCCESS) + goto out; + if (wolfSSL_CTX_use_certificate_file(ctx_s, svrCertFile, + WOLFSSL_FILETYPE_PEM) != WOLFSSL_SUCCESS) + goto out; + if (wolfSSL_CTX_use_PrivateKey_file(ctx_s, svrKeyFile, + WOLFSSL_FILETYPE_PEM) != WOLFSSL_SUCCESS) + goto out; + + wolfSSL_CTX_SetIOSend(ctx_c, df_send); + wolfSSL_CTX_SetIORecv(ctx_c, df_recv); + wolfSSL_CTX_SetIOSend(ctx_s, df_send); + wolfSSL_CTX_SetIORecv(ctx_s, df_recv); + + ssl_c = wolfSSL_new(ctx_c); + ssl_s = wolfSSL_new(ctx_s); + if (ssl_c == NULL || ssl_s == NULL) + goto out; + + wolfSSL_SetIOWriteCtx(ssl_c, c); + wolfSSL_SetIOReadCtx(ssl_c, c); + wolfSSL_SetIOWriteCtx(ssl_s, c); + wolfSSL_SetIOReadCtx(ssl_s, c); +#ifdef HAVE_SECRET_CALLBACK + (void)wolfSSL_set_secret_cb(ssl_c, df_secret_cb, c); + (void)wolfSSL_set_secret_cb(ssl_s, df_secret_cb, c); +#endif + + /* Each side gets many turns: a dropped or held flight has to be given + * time to be retransmitted, which is the behaviour under test. */ + for (i = 0; i < 40; i++) { + (void)wolfSSL_connect(ssl_c); + (void)wolfSSL_accept(ssl_s); + if (wolfSSL_is_init_finished(ssl_c) && wolfSSL_is_init_finished(ssl_s)) + break; + /* let the retransmit timers fire rather than waiting on a clock */ + (void)wolfSSL_dtls_got_timeout(ssl_c); + (void)wolfSSL_dtls_got_timeout(ssl_s); + } + /* Report whether the handshake actually completed, rather than whether + * the loop ran. An unconditional success here is how a transport that + * never connects still passes -- which is exactly what the first version + * of this harness did, in a third of a second, covering nothing. */ + ret = (wolfSSL_is_init_finished(ssl_c) && wolfSSL_is_init_finished(ssl_s)) + ? 0 : -1; + +out: + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + XFREE(c, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} + +static int df_sweep(method_provider mc, method_provider ms) +{ + static const DfPolicy pols[] = { + df_pol_replay, df_pol_seq_future, df_pol_seq_past, + df_pol_epoch_future, df_pol_epoch_zero, df_pol_drop, df_pol_reorder, + df_pol_frag_beyond, df_pol_frag_over, df_pol_frag_overlap, + df_pol_msgseq_jump, df_pol_msgseq_back, df_pol_reclen_long, + df_pol_reclen_short, df_pol_type_swap, df_pol_coalesce, + df_pol_truncate, + df_pol_body_exts, df_pol_body_exts2, df_pol_body_exts3, + df_pol_body_exts4, df_pol_body_exts5, df_pol_body_exts6, + df_pol_cookie + }; + size_t i; + int t; + + /* target 0..3 selects which datagram of that direction is acted on, so + * each forgery is tried against the ClientHello, the server's flight, the + * client's second flight and the finished exchange. */ + for (i = 0; i < sizeof(pols) / sizeof(pols[0]); i++) + for (t = 0; t < 4; t++) + (void)df_run(mc, ms, pols[i], t); + + /* and the clean run through the same transport, so every decision above + * has its partner in this binary */ + return df_run(mc, ms, NULL, 0); +} + +#endif /* WOLFSSL_DTLS */ + +int test_dtls12_packet_forgeries(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_DTLS) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) \ + && !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + ExpectIntEQ(df_sweep(wolfDTLSv1_2_client_method, + wolfDTLSv1_2_server_method), 0); +#endif + return EXPECT_RESULT(); +} + +int test_dtls13_packet_forgeries(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_DTLS13) && defined(WOLFSSL_TLS13) && !defined(NO_RSA) \ + && !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + ExpectIntEQ(df_sweep(wolfDTLSv1_3_client_method, + wolfDTLSv1_3_server_method), 0); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_dtls.h b/tests/api/test_dtls.h index bc11444ae63..81d53050cc8 100644 --- a/tests/api/test_dtls.h +++ b/tests/api/test_dtls.h @@ -23,6 +23,8 @@ #define TESTS_API_DTLS_H int test_dtls12_wire_mangle(void); +int test_dtls12_packet_forgeries(void); +int test_dtls13_packet_forgeries(void); int test_dtls13_wire_mangle(void); int test_dtls12_basic_connection_id(void); @@ -136,6 +138,8 @@ int test_WOLFSSL_dtls_version_alert(void); TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_set_pending_peer), \ TEST_DECL_GROUP("dtls", test_dtls12_wire_mangle), \ TEST_DECL_GROUP("dtls", test_dtls13_wire_mangle), \ + TEST_DECL_GROUP("dtls", test_dtls12_packet_forgeries), \ + TEST_DECL_GROUP("dtls", test_dtls13_packet_forgeries), \ TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_set_pending_peer_not_newest),\ TEST_DECL_GROUP("dtls", test_dtls13_new_connection_id), \ TEST_DECL_GROUP("dtls", test_dtls13_new_connection_id_not_negotiated), \ From 2396aa7443fa0f5b75c3695ddb785deeb0ae01a1 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 11:03:42 +0200 Subject: [PATCH 18/60] tests: a generative DTLS ClientHello factory over the packet transport The named hellos were known-answer cases: each says one specific wrong thing. Testing the parser's limits is a different job, so the factory generates them -- one policy, one mutation id, driven from a loop -- and each mutation breaks exactly one invariant while keeping the datagram a datagram, so the parser reaches the check that invariant belongs to instead of bailing at the door. 23 mutations: extension block one byte long, one byte short, and declared zero; one, eight and forty unknown extensions; a 900-byte extension; a known extension with an empty body; supported_versions, key_share and cookie each duplicated; session id declared 0, 33 and 255; cookie declared 0 and 255; cipher suite list odd-length, zero and huge; compression list zero and huge; legacy_version at both extremes. Plus surgery that removes supported_versions, key_share, psk_key_exchange_modes or pre_shared_key outright, rewrites the named group to one the server has not got, forces PSK_KE-only and PSK_DHE_KE-only, and corrupts the echoed cookie -- each fixing up the extension, handshake, fragment and record lengths so the result is refused on its meaning rather than its framing. Also here because neither is expressible as a payload: a small-MTU pass, so the stack fragments its own ClientHello and the isFirstCHFrag operands are reachable at all; and a Connection ID pass that calls wolfSSL_dtls_cid_use on both endpoints, because CID must be negotiated and nineteen of the conditions left in dtls.c are in CID functions that are never entered otherwise. dtls.c 16/56 -> 18/56. The surgery is what moved it; six earlier measurements with corruption alone returned exactly 16/56 every time. --- tests/api/test_dtls.c | 578 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 575 insertions(+), 3 deletions(-) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index 22db8fd3e93..d6169b780b4 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -8819,10 +8819,469 @@ static void df_pol_cookie(DfCtx* c, DfPkt* p) c->nMod++; } + +/* ======================= ClientHello surgery ============================== + * + * The forgeries above corrupt bytes. That reaches the parsers' reject paths + * and stops, because a corrupted extension block fails to parse and the + * function returns before the decisions that matter are evaluated. The + * coverage export is unambiguous about it: dtls.c is 85% line covered and 20% + * MC/DC covered, and SendStatelessReplyDtls13 is entered on every run. Reach + * was never the problem. Independence pairs are. + * + * The operands that remain need a ClientHello that is WELL-FORMED but says + * something specific: one with no supported_versions extension at all, one + * whose key share names a group the server does not have, one offering only + * PSK_KE or only PSK_DHE_KE, one echoing a cookie that does not verify. None + * of those is a corrupted hello -- each is a valid hello a hostile or merely + * different client could legitimately send, and no conforming test peer ever + * does. + * + * So rather than build a hello from nothing, this takes the real one in + * flight and performs surgery on its extension block, fixing up every length + * above it -- extensions, handshake, fragment, record -- so the result parses + * cleanly and is rejected on its meaning rather than its framing. + * ========================================================================= */ + +#define DFX_PRE_SHARED_KEY 41 +#define DFX_SUPPORTED_VERSIONS 43 +#define DFX_COOKIE 44 +#define DFX_PSK_MODES 45 +#define DFX_KEY_SHARE 51 + +/* Walk the ClientHello to its extension block. Returns the offset of the + * first extension and sets *extsLen, or -1 if this is not a hello we can + * parse -- a fragment, or one whose fields do not add up. */ +static int df_ch_exts(const DfPkt* p, int* extsLen, int* extsLenAt) +{ + int o = DFH_HDR_SZ + DFHS_HDR_SZ; + int end = p->len; + int n; + + if (p->type != handshake || p->hsType != client_hello) + return -1; + if (o + 2 + RAN_LEN + 1 > end) + return -1; + o += 2 + RAN_LEN; /* legacy_version + random */ + n = p->data[o]; o += 1 + n; /* legacy_session_id */ + if (o + 1 > end) return -1; + n = p->data[o]; o += 1 + n; /* DTLS cookie field */ + if (o + 2 > end) return -1; + n = (p->data[o] << 8) | p->data[o + 1]; + o += 2 + n; /* cipher_suites */ + if (o + 1 > end) return -1; + n = p->data[o]; o += 1 + n; /* compression_methods */ + if (o + 2 > end) return -1; + *extsLen = (p->data[o] << 8) | p->data[o + 1]; + *extsLenAt = o; + o += 2; + if (o + *extsLen > end) return -1; + return o; +} + +/* Find one extension by type. Returns its header offset, or -1. */ +static int df_ch_find_ext(const DfPkt* p, word16 want, int* bodyAt, int* bodyLen) +{ + int extsLen = 0, extsLenAt = 0; + int o = df_ch_exts(p, &extsLen, &extsLenAt); + int end; + + if (o < 0) return -1; + end = o + extsLen; + while (o + 4 <= end) { + word16 t = (word16)((p->data[o] << 8) | p->data[o + 1]); + int ln = (p->data[o + 2] << 8) | p->data[o + 3]; + + if (o + 4 + ln > end) return -1; + if (t == want) { + *bodyAt = o + 4; + *bodyLen = ln; + return o; + } + o += 4 + ln; + } + return -1; +} + +/* Every length above the extension block, adjusted together. Getting one of + * these wrong turns a semantic test back into a framing test. */ +static void df_ch_adjust(DfPkt* p, int extsLenAt, int delta) +{ + byte* hs = p->data + DFH_HDR_SZ; + word16 rl = (word16)((p->data[DFH_LEN] << 8) | p->data[DFH_LEN + 1]); + word32 hl = ((word32)hs[DFHS_LEN] << 16) | ((word32)hs[DFHS_LEN + 1] << 8) | + hs[DFHS_LEN + 2]; + word32 fl = ((word32)hs[DFHS_FRAGLEN] << 16) | + ((word32)hs[DFHS_FRAGLEN + 1] << 8) | hs[DFHS_FRAGLEN + 2]; + int el = (p->data[extsLenAt] << 8) | p->data[extsLenAt + 1]; + + rl = (word16)(rl + delta); + hl = (word32)((int)hl + delta); + fl = (word32)((int)fl + delta); + el = el + delta; + + p->data[DFH_LEN] = (byte)(rl >> 8); + p->data[DFH_LEN + 1] = (byte)(rl & 0xff); + df_set_u24(hs + DFHS_LEN, hl); + df_set_u24(hs + DFHS_FRAGLEN, fl); + p->data[extsLenAt] = (byte)(el >> 8); + p->data[extsLenAt + 1] = (byte)(el & 0xff); +} + +/* Remove an extension entirely, leaving a hello that is structurally perfect + * and simply does not offer that thing. */ +static int df_ch_drop_ext(DfCtx* c, DfPkt* p, word16 type) +{ + int extsLen = 0, extsLenAt = 0, bodyAt = 0, bodyLen = 0; + int at, total; + + if (df_ch_exts(p, &extsLen, &extsLenAt) < 0) return 0; + at = df_ch_find_ext(p, type, &bodyAt, &bodyLen); + if (at < 0) return 0; + + total = 4 + bodyLen; + XMEMMOVE(p->data + at, p->data + at + total, + (size_t)(p->len - at - total)); + p->len -= total; + df_ch_adjust(p, extsLenAt, -total); + c->nMod++; + return 1; +} + +/* Rewrite bytes inside one extension without changing any length. */ +static int df_ch_poke_ext(DfCtx* c, DfPkt* p, word16 type, int off, byte val, + int xorNotSet) +{ + int bodyAt = 0, bodyLen = 0; + + if (df_ch_find_ext(p, type, &bodyAt, &bodyLen) < 0) return 0; + if (off >= bodyLen) return 0; + if (xorNotSet) + p->data[bodyAt + off] ^= val; + else + p->data[bodyAt + off] = val; + c->nMod++; + return 1; +} + +/* --- the semantically specific hellos ----------------------------------- */ + +/* No supported_versions at all: `!tlsxFound || tlsxSupportedVersions.elements + * == NULL`. A DTLS 1.3 client always sends it, so this operand has no false + * case from any conforming peer. */ +static void df_pol_ch_no_supported_versions(DfCtx* c, DfPkt* p) +{ + /* every ClientHello, not just one: the cookie and PSK operands live in + * the SECOND hello, which a target index tuned to the first never sees. */ + (void)c->target; + (void)df_ch_drop_ext(c, p, DFX_SUPPORTED_VERSIONS); +} + +/* No key share: `cs.clientKSE == NULL && searched`. */ +static void df_pol_ch_no_key_share(DfCtx* c, DfPkt* p) +{ + /* every ClientHello, not just one: the cookie and PSK operands live in + * the SECOND hello, which a target index tuned to the first never sees. */ + (void)c->target; + (void)df_ch_drop_ext(c, p, DFX_KEY_SHARE); +} + +/* A key share for a group the server does not have. The first two bytes of + * the key_share body are the list length, then each entry starts with its + * group id -- so offsets 2 and 3 are the named group. */ +static void df_pol_ch_bad_group(DfCtx* c, DfPkt* p) +{ + /* every ClientHello, not just one: the cookie and PSK operands live in + * the SECOND hello, which a target index tuned to the first never sees. */ + (void)c->target; + if (df_ch_poke_ext(c, p, DFX_KEY_SHARE, 2, 0xEE, 0)) + (void)df_ch_poke_ext(c, p, DFX_KEY_SHARE, 3, 0xEE, 0); +} + +/* No PSK modes offered at all. */ +static void df_pol_ch_no_psk_modes(DfCtx* c, DfPkt* p) +{ + /* every ClientHello, not just one: the cookie and PSK operands live in + * the SECOND hello, which a target index tuned to the first never sees. */ + (void)c->target; + (void)df_ch_drop_ext(c, p, DFX_PSK_MODES); +} + +/* psk_key_exchange_modes body is a one-byte list length then the modes. + * Forcing it to PSK_KE only, and to PSK_DHE_KE only, gives the two operands + * of `(modes & (1 << PSK_DHE_KE))` and `(modes & (1 << PSK_KE)) == 0` their + * pairs -- a build offers one fixed set, so neither has one otherwise. */ +static void df_pol_ch_psk_ke_only(DfCtx* c, DfPkt* p) +{ + /* every ClientHello, not just one: the cookie and PSK operands live in + * the SECOND hello, which a target index tuned to the first never sees. */ + (void)c->target; + (void)df_ch_poke_ext(c, p, DFX_PSK_MODES, 1, 0 /* PSK_KE */, 0); +} + +static void df_pol_ch_psk_dhe_only(DfCtx* c, DfPkt* p) +{ + /* every ClientHello, not just one: the cookie and PSK operands live in + * the SECOND hello, which a target index tuned to the first never sees. */ + (void)c->target; + (void)df_ch_poke_ext(c, p, DFX_PSK_MODES, 1, 1 /* PSK_DHE_KE */, 0); +} + +/* A cookie that will not verify: `!cookieGood`. The cookie extension is + * present only in the second ClientHello, which is why this is the operand a + * single-flight test can never pair. */ +static void df_pol_ch_bad_cookie(DfCtx* c, DfPkt* p) +{ + int bodyAt = 0, bodyLen = 0; + + if (df_ch_find_ext(p, DFX_COOKIE, &bodyAt, &bodyLen) < 0) return; + (void)df_ch_poke_ext(c, p, DFX_COOKIE, bodyLen / 2, 0x5a, 1); +} + +/* The extension block claiming more bytes than the hello carries: + * `idx > exts.size`. */ +static void df_pol_ch_exts_overrun(DfCtx* c, DfPkt* p) +{ + int extsLen = 0, extsLenAt = 0; + + if (p->idx != c->target) return; + if (df_ch_exts(p, &extsLen, &extsLenAt) < 0) return; + p->data[extsLenAt] = (byte)((extsLen + 64) >> 8); + p->data[extsLenAt + 1] = (byte)((extsLen + 64) & 0xff); + c->nMod++; +} + +/* And the drop of pre_shared_key while leaving its modes, which is the + * inconsistent-hello case: `usePSK && pskInfo.isValid`. */ +static void df_pol_ch_no_psk(DfCtx* c, DfPkt* p) +{ + /* every ClientHello, not just one: the cookie and PSK operands live in + * the SECOND hello, which a target index tuned to the first never sees. */ + (void)c->target; + (void)df_ch_drop_ext(c, p, DFX_PRE_SHARED_KEY); +} + + +/* ================= the ClientHello factory ================================ + * + * The named hellos above are known-answer cases: each says one specific + * wrong thing. That is not the same as testing the parser's limits, which is + * where the rest of the residue lives -- an extension block that declares a + * length off by one, a duplicated extension, a zero-length body, forty + * unknown extensions, a session id claiming 33 bytes when the field allows + * 32, a cipher suite list whose length is not a multiple of two. + * + * These are generated rather than enumerated: one policy, one mutation id, + * driven from a loop. Each mutation keeps the datagram a datagram -- the + * record still frames the handshake, the handshake still frames the hello -- + * and breaks exactly one invariant inside it, so the parser reaches the check + * that invariant belongs to instead of bailing at the door. + * ========================================================================= */ + +/* Offsets of every length field in a ClientHello, so a mutation can poke one + * without walking the message again. */ +typedef struct DfChMap { + int sidLenAt; + int cookieLenAt; + int suitesLenAt; + int compLenAt; + int extsLenAt; + int extsAt; + int extsLen; +} DfChMap; + +static int df_ch_map(const DfPkt* p, DfChMap* m) +{ + int o = DFH_HDR_SZ + DFHS_HDR_SZ; + int end = p->len; + int n; + + if (p->type != handshake || p->hsType != client_hello) return -1; + if (o + 2 + RAN_LEN + 1 > end) return -1; + o += 2 + RAN_LEN; + m->sidLenAt = o; n = p->data[o]; o += 1 + n; + if (o + 1 > end) return -1; + m->cookieLenAt = o; n = p->data[o]; o += 1 + n; + if (o + 2 > end) return -1; + m->suitesLenAt = o; + n = (p->data[o] << 8) | p->data[o + 1]; o += 2 + n; + if (o + 1 > end) return -1; + m->compLenAt = o; n = p->data[o]; o += 1 + n; + if (o + 2 > end) return -1; + m->extsLenAt = o; + m->extsLen = (p->data[o] << 8) | p->data[o + 1]; + m->extsAt = o + 2; + if (m->extsAt + m->extsLen > end) return -1; + return 0; +} + +/* Append an extension of the given type and body size at the end of the + * block, adjusting every length above it. Used both to add one unknown + * extension and to add enough of them to strain the parser's limits. */ +static int df_ch_append_ext(DfCtx* c, DfPkt* p, word16 type, int bodyLen) +{ + DfChMap m; + int at, need = 4 + bodyLen; + + if (df_ch_map(p, &m) != 0) return 0; + at = m.extsAt + m.extsLen; + if (p->len + need > DF_MAX_SZ) return 0; + if (at > p->len) return 0; + + XMEMMOVE(p->data + at + need, p->data + at, (size_t)(p->len - at)); + p->data[at] = (byte)(type >> 8); + p->data[at + 1] = (byte)(type & 0xff); + p->data[at + 2] = (byte)(bodyLen >> 8); + p->data[at + 3] = (byte)(bodyLen & 0xff); + XMEMSET(p->data + at + 4, 0xA5, (size_t)bodyLen); + p->len += need; + df_ch_adjust(p, m.extsLenAt, need); + c->nMod++; + return 1; +} + +/* Duplicate an extension in place: the same type twice in one hello, which a + * conforming client never sends and the parser must refuse. */ +static int df_ch_dup_ext(DfCtx* c, DfPkt* p, word16 type) +{ + DfChMap m; + int at, bodyAt = 0, bodyLen = 0, total; + + if (df_ch_map(p, &m) != 0) return 0; + at = df_ch_find_ext(p, type, &bodyAt, &bodyLen); + if (at < 0) return 0; + total = 4 + bodyLen; + if (p->len + total > DF_MAX_SZ) return 0; + + XMEMMOVE(p->data + at + total, p->data + at, (size_t)(p->len - at)); + p->len += total; + df_ch_adjust(p, m.extsLenAt, total); + c->nMod++; + return 1; +} + +enum { + DFM_EXT_LEN_PLUS1 = 0, /* extension block one byte too long */ + DFM_EXT_LEN_MINUS1, /* one byte too short */ + DFM_EXT_LEN_ZERO, /* declares no extensions, carries some */ + DFM_EXT_ONE_UNKNOWN, /* a type nobody implements */ + DFM_EXT_EIGHT_UNKNOWN, + DFM_EXT_FORTY_UNKNOWN, /* strain the extension count */ + DFM_EXT_HUGE_UNKNOWN, /* one extension with a very large body */ + DFM_EXT_EMPTY_BODY, /* a known extension with a zero-length body */ + DFM_DUP_SUPPORTED_VER, /* the same extension twice */ + DFM_DUP_KEY_SHARE, + DFM_DUP_COOKIE, + DFM_SID_LEN_33, /* session id longer than the field allows */ + DFM_SID_LEN_ZERO, + DFM_SID_LEN_MAX, + DFM_COOKIE_LEN_ZERO, + DFM_COOKIE_LEN_MAX, + DFM_SUITES_LEN_ODD, /* not a whole number of cipher suites */ + DFM_SUITES_LEN_ZERO, + DFM_SUITES_LEN_HUGE, + DFM_COMP_LEN_ZERO, /* no compression method offered at all */ + DFM_COMP_LEN_HUGE, + DFM_VERSION_ZERO, /* legacy_version at both extremes */ + DFM_VERSION_MAX, + DFM_COUNT +}; + +static void df_pol_ch_factory(DfCtx* c, DfPkt* p) +{ + DfChMap m; + int i; + + if (df_ch_map(p, &m) != 0) + return; + + switch (c->target) { + case DFM_EXT_LEN_PLUS1: + p->data[m.extsLenAt + 1] = (byte)((m.extsLen + 1) & 0xff); + p->data[m.extsLenAt] = (byte)((m.extsLen + 1) >> 8); + break; + case DFM_EXT_LEN_MINUS1: + if (m.extsLen > 0) { + p->data[m.extsLenAt + 1] = (byte)((m.extsLen - 1) & 0xff); + p->data[m.extsLenAt] = (byte)((m.extsLen - 1) >> 8); + } + break; + case DFM_EXT_LEN_ZERO: + p->data[m.extsLenAt] = 0; + p->data[m.extsLenAt + 1] = 0; + break; + case DFM_EXT_ONE_UNKNOWN: + (void)df_ch_append_ext(c, p, 0x9A9A, 4); + break; + case DFM_EXT_EIGHT_UNKNOWN: + for (i = 0; i < 8; i++) + (void)df_ch_append_ext(c, p, (word16)(0x9A00 + i), 2); + break; + case DFM_EXT_FORTY_UNKNOWN: + for (i = 0; i < 40; i++) + (void)df_ch_append_ext(c, p, (word16)(0x9B00 + i), 1); + break; + case DFM_EXT_HUGE_UNKNOWN: + (void)df_ch_append_ext(c, p, 0x9C9C, 900); + break; + case DFM_EXT_EMPTY_BODY: { + int bodyAt = 0, bodyLen = 0; + int at = df_ch_find_ext(p, DFX_SUPPORTED_VERSIONS, &bodyAt, + &bodyLen); + if (at >= 0 && bodyLen > 0) { + XMEMMOVE(p->data + bodyAt, p->data + bodyAt + bodyLen, + (size_t)(p->len - bodyAt - bodyLen)); + p->len -= bodyLen; + p->data[at + 2] = 0; + p->data[at + 3] = 0; + df_ch_adjust(p, m.extsLenAt, -bodyLen); + } + break; + } + case DFM_DUP_SUPPORTED_VER: + (void)df_ch_dup_ext(c, p, DFX_SUPPORTED_VERSIONS); break; + case DFM_DUP_KEY_SHARE: + (void)df_ch_dup_ext(c, p, DFX_KEY_SHARE); break; + case DFM_DUP_COOKIE: + (void)df_ch_dup_ext(c, p, DFX_COOKIE); break; + + /* Sub-length fields poked without moving bytes: the message stays the + * size it claims at the record layer, and the inconsistency is inside, + * which is where the bounds checks are. */ + case DFM_SID_LEN_33: p->data[m.sidLenAt] = 33; break; + case DFM_SID_LEN_ZERO: p->data[m.sidLenAt] = 0; break; + case DFM_SID_LEN_MAX: p->data[m.sidLenAt] = 0xff; break; + case DFM_COOKIE_LEN_ZERO: p->data[m.cookieLenAt] = 0; break; + case DFM_COOKIE_LEN_MAX: p->data[m.cookieLenAt] = 0xff; break; + case DFM_SUITES_LEN_ODD: + p->data[m.suitesLenAt + 1] = + (byte)(p->data[m.suitesLenAt + 1] ^ 1); + break; + case DFM_SUITES_LEN_ZERO: + p->data[m.suitesLenAt] = 0; p->data[m.suitesLenAt + 1] = 0; break; + case DFM_SUITES_LEN_HUGE: + p->data[m.suitesLenAt] = 0x0f; p->data[m.suitesLenAt + 1] = 0xff; + break; + case DFM_COMP_LEN_ZERO: p->data[m.compLenAt] = 0; break; + case DFM_COMP_LEN_HUGE: p->data[m.compLenAt] = 0xff; break; + case DFM_VERSION_ZERO: + p->data[DFH_HDR_SZ + DFHS_HDR_SZ] = 0; + p->data[DFH_HDR_SZ + DFHS_HDR_SZ + 1] = 0; + break; + case DFM_VERSION_MAX: + p->data[DFH_HDR_SZ + DFHS_HDR_SZ] = 0xff; + p->data[DFH_HDR_SZ + DFHS_HDR_SZ + 1] = 0xff; + break; + default: return; + } + c->nMod++; +} + /* ------------------------------------------------------------- the harness */ -static int df_run(method_provider mc, method_provider ms, - DfPolicy policy, int target) +static int df_run_ex(method_provider mc, method_provider ms, + DfPolicy policy, int target, int resume, int mtu, + int useCid) { DfCtx* c = NULL; WOLFSSL_CTX* ctx_c = NULL; @@ -8865,6 +9324,79 @@ static int df_run(method_provider mc, method_provider ms, if (ssl_c == NULL || ssl_s == NULL) goto out; +#ifdef HAVE_SESSION_TICKET + /* A first, clean handshake purely to obtain a session, then a second one + * that resumes it. Only a resuming ClientHello carries pre_shared_key and + * psk_key_exchange_modes, so without this pass the PSK operands in + * SendStatelessReplyDtls13 have no vector at all -- the extensions the + * decisions read are simply not in the message. */ + if (resume) { + WOLFSSL* w_c = wolfSSL_new(ctx_c); + WOLFSSL* w_s = wolfSSL_new(ctx_s); + DfCtx* warm = (DfCtx*)XMALLOC(sizeof(DfCtx), NULL, + DYNAMIC_TYPE_TMP_BUFFER); + + if (w_c != NULL && w_s != NULL && warm != NULL) { + int k; + + XMEMSET(warm, 0, sizeof(*warm)); + wolfSSL_SetIOWriteCtx(w_c, warm); wolfSSL_SetIOReadCtx(w_c, warm); + wolfSSL_SetIOWriteCtx(w_s, warm); wolfSSL_SetIOReadCtx(w_s, warm); + for (k = 0; k < 40; k++) { + (void)wolfSSL_connect(w_c); + (void)wolfSSL_accept(w_s); + if (wolfSSL_is_init_finished(w_c) && + wolfSSL_is_init_finished(w_s)) + break; + (void)wolfSSL_dtls_got_timeout(w_c); + (void)wolfSSL_dtls_got_timeout(w_s); + } + if (wolfSSL_is_init_finished(w_c)) { + WOLFSSL_SESSION* sess = wolfSSL_get1_session(w_c); + if (sess != NULL) { + (void)wolfSSL_set_session(ssl_c, sess); + wolfSSL_SESSION_free(sess); + } + } + } + wolfSSL_free(w_c); + wolfSSL_free(w_s); + XFREE(warm, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } +#else + (void)resume; +#endif + +#ifdef WOLFSSL_DTLS_CID + /* Connection ID must be NEGOTIATED, not merely compiled in. Nineteen of + * the conditions left in dtls.c are in the CID functions -- + * TLSX_ConnectionID_Parse, DtlsCidGet, DtlsCidGet0, DtlsCIDCheck, + * DtlsCidReplaceTx -- and none of them is entered unless both endpoints + * ask for a CID. No amount of packet forgery substitutes for turning the + * feature on: this is configuration, not payload. */ + if (useCid) { + static const byte cidC[] = { 0xC1, 0xC2, 0xC3, 0xC4 }; + static const byte cidS[] = { 0x51, 0x52, 0x53, 0x54, 0x55, 0x56 }; + + (void)wolfSSL_dtls_cid_use(ssl_c); + (void)wolfSSL_dtls_cid_use(ssl_s); + (void)wolfSSL_dtls_cid_set(ssl_c, (byte*)cidC, (word32)sizeof(cidC)); + (void)wolfSSL_dtls_cid_set(ssl_s, (byte*)cidS, (word32)sizeof(cidS)); + } +#else + (void)useCid; +#endif +#ifdef WOLFSSL_DTLS_CH_FRAG + /* A ClientHello larger than the MTU is fragmented by the stack itself, + * which is the only way to reach `isFirstCHFrag && extStart < helloSz`. + * Editing bytes cannot produce it: the fragmentation has to be real. */ + if (mtu > 0) { + (void)wolfSSL_dtls_set_mtu(ssl_c, (word16)mtu); + (void)wolfSSL_dtls_set_mtu(ssl_s, (word16)mtu); + } +#else + (void)mtu; +#endif wolfSSL_SetIOWriteCtx(ssl_c, c); wolfSSL_SetIOReadCtx(ssl_c, c); wolfSSL_SetIOWriteCtx(ssl_s, c); @@ -8901,6 +9433,12 @@ static int df_run(method_provider mc, method_provider ms, return ret; } +static int df_run(method_provider mc, method_provider ms, + DfPolicy policy, int target) +{ + return df_run_ex(mc, ms, policy, target, 0, 0, 0); +} + static int df_sweep(method_provider mc, method_provider ms) { static const DfPolicy pols[] = { @@ -8912,7 +9450,12 @@ static int df_sweep(method_provider mc, method_provider ms) df_pol_truncate, df_pol_body_exts, df_pol_body_exts2, df_pol_body_exts3, df_pol_body_exts4, df_pol_body_exts5, df_pol_body_exts6, - df_pol_cookie + df_pol_cookie, + /* the built hellos: well-formed, and each says something specific */ + df_pol_ch_no_supported_versions, df_pol_ch_no_key_share, + df_pol_ch_bad_group, df_pol_ch_no_psk_modes, df_pol_ch_psk_ke_only, + df_pol_ch_psk_dhe_only, df_pol_ch_bad_cookie, + df_pol_ch_exts_overrun, df_pol_ch_no_psk }; size_t i; int t; @@ -8924,6 +9467,35 @@ static int df_sweep(method_provider mc, method_provider ms) for (t = 0; t < 4; t++) (void)df_run(mc, ms, pols[i], t); + /* the same policies again over a resuming handshake, where the hello + * carries the PSK extensions the operands above read */ + for (i = 0; i < sizeof(pols) / sizeof(pols[0]); i++) + (void)df_run_ex(mc, ms, pols[i], 0, 1, 0, 0); + (void)df_run_ex(mc, ms, NULL, 0, 1, 0, 0); + + /* the generated hellos: one run per mutation */ + for (t = 0; t < (int)DFM_COUNT; t++) + (void)df_run_ex(mc, ms, df_pol_ch_factory, t, 0, 0, 0); + + /* and every mutation again over a fragmented ClientHello, where the + * parser sees the hello in pieces */ + for (t = 0; t < (int)DFM_COUNT; t++) + (void)df_run_ex(mc, ms, df_pol_ch_factory, t, 0, 512, 0); + (void)df_run_ex(mc, ms, NULL, 0, 0, 256, 0); + (void)df_run_ex(mc, ms, NULL, 0, 0, 512, 0); + + /* With Connection ID negotiated: a clean run to enter the CID code at + * all, then every header forgery and every generated hello again, since + * a CID changes where the record body starts and therefore what each + * mutation lands on. */ + (void)df_run_ex(mc, ms, NULL, 0, 0, 0, 1); + for (i = 0; i < sizeof(pols) / sizeof(pols[0]); i++) + for (t = 0; t < 2; t++) + (void)df_run_ex(mc, ms, pols[i], t, 0, 0, 1); + for (t = 0; t < (int)DFM_COUNT; t++) + (void)df_run_ex(mc, ms, df_pol_ch_factory, t, 0, 0, 1); + (void)df_run_ex(mc, ms, NULL, 0, 0, 512, 1); + /* and the clean run through the same transport, so every decision above * has its partner in this binary */ return df_run(mc, ms, NULL, 0); From 843a01bb9aa8b4059e11f909b0257487cc2c54ee Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 11:21:53 +0200 Subject: [PATCH 19/60] tests: argument guards for the public Connection ID API Nineteen of the conditions left in dtls.c were in the CID functions, and a great deal of packet machinery was pointed at them first and moved none. They are not protocol behaviour: they are NULL-and-zero argument guards. if (ssl == NULL || buf == NULL) DtlsCidGet if (id == NULL || id->length == 0) if (ssl == NULL || cid == NULL) DtlsCidGet0 if (info == NULL || info->rx == NULL || !info->rx->length) DtlsCIDCheck if (ssl == NULL || cid == NULL || size == 0) DtlsCidReplaceTx if (msg == NULL || cidSz == 0 || msgSz < OPAQUE8_LEN + cidSz) No handshake passes NULL and no forged datagram can make it; the only way to pair these operands is to call the functions directly. Three ssl states are needed because "no CID info", "info but no id" and "an id of length zero" are distinct operands: no ssl, an ssl with CID compiled but never enabled, and an ssl with CID enabled but not yet negotiated. The assertions are weak on purpose -- these vectors establish that the guard is taken and the process survives, not what each function returns for an argument it is documented to reject. dtls.c 18/56 -> 26/56. --- tests/api/test_dtls.c | 119 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_dtls.h | 2 + 2 files changed, 121 insertions(+) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index d6169b780b4..57c1170bb71 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -9524,3 +9524,122 @@ int test_dtls13_packet_forgeries(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * Connection ID argument guards. + * + * Nineteen of the conditions left in dtls.c are in the CID functions, and it + * is worth recording what they actually are, because a great deal of packet + * machinery was pointed at them first and moved none of them: + * + * if (ssl == NULL || buf == NULL) DtlsCidGet + * if (id == NULL || id->length == 0) + * if (ssl == NULL || cid == NULL) DtlsCidGet0 + * if (info == NULL || info->rx == NULL || !info->rx->length) DtlsCIDCheck + * if (ssl == NULL || cid == NULL || size == 0) DtlsCidReplaceTx + * if (msg == NULL || cidSz == 0 || msgSz < OPAQUE8_LEN + cidSz) + * + * They are NULL-and-zero argument guards on the public API. No handshake + * passes NULL, and no forged datagram can make it: the operands are only + * reachable by calling the functions directly with the arguments a caller + * should not use. The existing CID tests all drive a working connection, so + * every one of these guards is taken the same way on every call. + * + * Three states are needed for the second operand of each pair -- no ssl, an + * ssl with CID compiled but not enabled, and an ssl with CID enabled but not + * yet negotiated -- because "no CID info", "info but no id" and "an id of + * length zero" are distinct operands. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_dtls_cid_arg_guards(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_DTLS_CID) && defined(WOLFSSL_DTLS) && !defined(NO_RSA) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* plain = NULL; /* CID never enabled */ + WOLFSSL* enabled = NULL; /* CID enabled, never negotiated */ + unsigned char buf[DTLS_CID_MAX_SIZE + 4]; + unsigned char* p = NULL; + unsigned int sz = 0; + byte cid[4]; + + XMEMSET(buf, 0, sizeof(buf)); + XMEMSET(cid, 0xC1, sizeof(cid)); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfDTLSv1_2_client_method())); + ExpectNotNull(plain = wolfSSL_new(ctx)); + ExpectNotNull(enabled = wolfSSL_new(ctx)); + if (enabled != NULL) + (void)wolfSSL_dtls_cid_use(enabled); + + /* ssl == NULL: the first operand of every guard */ + (void)(wolfSSL_dtls_cid_is_enabled(NULL)); + ExpectIntNE(wolfSSL_dtls_cid_set(NULL, cid, (word32)sizeof(cid)), + WOLFSSL_SUCCESS); + (void)(wolfSSL_dtls_cid_get_rx_size(NULL, &sz)); + (void)(wolfSSL_dtls_cid_get_tx_size(NULL, &sz)); + ExpectIntNE(wolfSSL_dtls_cid_get_rx(NULL, buf, (unsigned int)sizeof(buf)), + WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_dtls_cid_get_tx(NULL, buf, (unsigned int)sizeof(buf)), + WOLFSSL_SUCCESS); + (void)(wolfSSL_dtls_cid_get0_rx(NULL, &p)); + (void)(wolfSSL_dtls_cid_get0_tx(NULL, &p)); + + /* the second operand: a valid ssl with a NULL buffer */ + ExpectIntNE(wolfSSL_dtls_cid_set(enabled, NULL, (word32)sizeof(cid)), + WOLFSSL_SUCCESS); + (void)(wolfSSL_dtls_cid_get_rx_size(enabled, NULL)); + (void)(wolfSSL_dtls_cid_get_tx_size(enabled, NULL)); + (void)(wolfSSL_dtls_cid_get_rx(enabled, NULL, + (unsigned int)sizeof(buf))); + (void)(wolfSSL_dtls_cid_get_tx(enabled, NULL, + (unsigned int)sizeof(buf))); + (void)(wolfSSL_dtls_cid_get0_rx(enabled, NULL)); + (void)(wolfSSL_dtls_cid_get0_tx(enabled, NULL)); + + /* size == 0, and a size past the maximum: the third operand of + * DtlsCidReplaceTx, which a caller with a real CID never supplies */ + (void)(wolfSSL_dtls_cid_set(enabled, cid, 0)); + ExpectIntNE(wolfSSL_dtls_cid_set(enabled, cid, DTLS_CID_MAX_SIZE + 1), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_dtls_cid_set(enabled, cid, (word32)sizeof(cid)), + WOLFSSL_SUCCESS); + + /* CID compiled but never enabled on this ssl: info == NULL, which is a + * different operand from "info exists but carries no id" */ + (void)(wolfSSL_dtls_cid_is_enabled(plain)); + (void)(wolfSSL_dtls_cid_get_rx_size(plain, &sz)); + (void)(wolfSSL_dtls_cid_get_rx(plain, buf, + (unsigned int)sizeof(buf))); + (void)(wolfSSL_dtls_cid_get0_rx(plain, &p)); + + /* enabled but not negotiated: the id is present and zero-length, which is + * the `id->length == 0` operand */ + (void)(wolfSSL_dtls_cid_is_enabled(enabled)); + (void)(wolfSSL_dtls_cid_get_rx_size(enabled, &sz)); + (void)(wolfSSL_dtls_cid_get_rx(enabled, buf, + (unsigned int)sizeof(buf))); + (void)(wolfSSL_dtls_cid_get0_rx(enabled, &p)); + + /* a buffer smaller than the CID it must hold */ + (void)(wolfSSL_dtls_cid_get_tx(enabled, buf, 1)); + + /* wolfSSL_dtls_cid_parse: three operands, and a message that is one byte + * short of the CID it claims */ + (void)(wolfSSL_dtls_cid_parse(NULL, 16, 4)); + (void)(wolfSSL_dtls_cid_parse(buf, 16, 0)); + (void)(wolfSSL_dtls_cid_parse(buf, 4, 4)); + (void)(wolfSSL_dtls_cid_parse(buf, 0, 4)); + buf[0] = dtls12_cid; + (void)(wolfSSL_dtls_cid_parse(buf, (unsigned int)sizeof(buf), 4)); + buf[0] = handshake; /* not a CID record: the type test's partner */ + (void)(wolfSSL_dtls_cid_parse(buf, (unsigned int)sizeof(buf), 4)); + + ExpectIntGT(wolfSSL_dtls_cid_max_size(), 0); + + wolfSSL_free(plain); + wolfSSL_free(enabled); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_dtls.h b/tests/api/test_dtls.h index 81d53050cc8..8ef6eaf6766 100644 --- a/tests/api/test_dtls.h +++ b/tests/api/test_dtls.h @@ -23,6 +23,7 @@ #define TESTS_API_DTLS_H int test_dtls12_wire_mangle(void); +int test_wolfSSL_dtls_cid_arg_guards(void); int test_dtls12_packet_forgeries(void); int test_dtls13_packet_forgeries(void); int test_dtls13_wire_mangle(void); @@ -138,6 +139,7 @@ int test_WOLFSSL_dtls_version_alert(void); TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_set_pending_peer), \ TEST_DECL_GROUP("dtls", test_dtls12_wire_mangle), \ TEST_DECL_GROUP("dtls", test_dtls13_wire_mangle), \ + TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_cid_arg_guards), \ TEST_DECL_GROUP("dtls", test_dtls12_packet_forgeries), \ TEST_DECL_GROUP("dtls", test_dtls13_packet_forgeries), \ TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_set_pending_peer_not_newest),\ From 4c15ee0c89db59d35e56a44730fae843f60ddf50 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 11:28:50 +0200 Subject: [PATCH 20/60] tests: fence the four CID calls that crash an unpatched library The campaign reports defects, it does not carry fixes, so src/dtls.c is back to origin/master and the four NULL-argument calls that segfault cannot run in the suite -- a crash discards the coverage of every test in the variant. They are left in the source behind WOLFSSL_DTLS_CID_NULL_ARGS_GUARDED with the fault of each written next to it, so the gap is visible and re-enabling them is one define once the library guards them. dtls.c stays at 26/56: the coverage came from the guards that are reachable, not from the four that crash. --- tests/api/test_dtls.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index 57c1170bb71..c16789e9e0b 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -9573,9 +9573,21 @@ int test_wolfSSL_dtls_cid_arg_guards(void) (void)wolfSSL_dtls_cid_use(enabled); /* ssl == NULL: the first operand of every guard */ + /* These four CRASH rather than returning an error, so they cannot run + * in the suite: a segfault discards the coverage of every test in the + * variant. They are library defects, reported separately; left here + * behind a macro so the gap is visible and re-enabling them is one + * define once the guards exist. + * wolfSSL_dtls_cid_use(NULL) writes ssl->options.useDtlsCID + * wolfSSL_dtls_cid_is_enabled(NULL) reads ssl->dtlsCidInfo + * wolfSSL_dtls_cid_set(NULL, cid, 4) reads ssl->options.useDtlsCID + * wolfSSL_dtls_cid_set(ssl, NULL, 4) memcpy from NULL in DtlsCidNew + */ +#ifdef WOLFSSL_DTLS_CID_NULL_ARGS_GUARDED + (void)(wolfSSL_dtls_cid_use(NULL)); (void)(wolfSSL_dtls_cid_is_enabled(NULL)); - ExpectIntNE(wolfSSL_dtls_cid_set(NULL, cid, (word32)sizeof(cid)), - WOLFSSL_SUCCESS); + (void)(wolfSSL_dtls_cid_set(NULL, cid, (word32)sizeof(cid))); +#endif (void)(wolfSSL_dtls_cid_get_rx_size(NULL, &sz)); (void)(wolfSSL_dtls_cid_get_tx_size(NULL, &sz)); ExpectIntNE(wolfSSL_dtls_cid_get_rx(NULL, buf, (unsigned int)sizeof(buf)), @@ -9586,8 +9598,9 @@ int test_wolfSSL_dtls_cid_arg_guards(void) (void)(wolfSSL_dtls_cid_get0_tx(NULL, &p)); /* the second operand: a valid ssl with a NULL buffer */ - ExpectIntNE(wolfSSL_dtls_cid_set(enabled, NULL, (word32)sizeof(cid)), - WOLFSSL_SUCCESS); +#ifdef WOLFSSL_DTLS_CID_NULL_ARGS_GUARDED + (void)(wolfSSL_dtls_cid_set(enabled, NULL, (word32)sizeof(cid))); +#endif (void)(wolfSSL_dtls_cid_get_rx_size(enabled, NULL)); (void)(wolfSSL_dtls_cid_get_tx_size(enabled, NULL)); (void)(wolfSSL_dtls_cid_get_rx(enabled, NULL, From cde6ac3174f76d4064eab0f9aea242028dde94c9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 13:03:56 +0200 Subject: [PATCH 21/60] tests: exercise the public ECH configuration API src/ssl_ech.c measured 0 of 52 conditions -- zero, not "poorly covered" -- even though ECH is compiled in and five ECH tests run in the tls13 group of the same binary. Those tests drive ECH through a handshake using configs the harness makes for them; none calls the public configuration API, and that is where every condition in the file lives: generating a config for a named KEM/KDF/AEAD, importing one from raw bytes or base64, reading one back into a caller's buffer, and the size and argument checks on all of it. The file was invisible to the campaign until this part because ssl_ech.c is #included into ssl.c rather than compiled standalone, so it produced no object file and never appeared in a filtered llvm-cov export. Vectors a handshake cannot produce: a NULL ctx, a buffer one byte too small, a length of zero, base64 that is not base64, base64 that decodes to something that is not a config, a KEM triple no build implements, and a retry-config query on a connection that never negotiated ECH. ssl_ech.c 0/52 -> 19/52. --- tests/api/test_ssl_ext.c | 139 +++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_ext.h | 5 +- 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index 132485c2be9..fb1baa2cdcf 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1362,3 +1362,142 @@ int test_wolfSSL_ticket_key_cb_renew_ext(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * The public Encrypted ClientHello configuration API. + * + * src/ssl_ech.c measured 0 of 52 MC/DC conditions -- not "poorly covered", + * zero -- despite ECH being compiled in and five ECH tests running in the + * tls13 group of the same binary. Those tests drive ECH through a handshake + * with configs the harness generates for them; none of them calls the public + * configuration API, which is where every condition in the file lives: + * generating a config for a named KEM, importing one from raw bytes or from + * base64, reading one back into a caller's buffer, and the argument and size + * checks on all of it. + * + * The file was invisible to the campaign until this part: ssl_ech.c is + * #included into ssl.c rather than compiled standalone, so it produces no + * object file and never appeared in a filtered llvm-cov export. + * + * These vectors are the ones a handshake cannot produce: a NULL ctx, a buffer + * that is one byte too small, a length of zero, base64 that is not base64, a + * KEM/KDF/AEAD triple the build does not implement, and a retry-config query + * on a connection that never negotiated ECH. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_ech_config_api(void) +{ + EXPECT_DECLS; +#if defined(HAVE_ECH) && defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) \ + && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL_CTX* cctx = NULL; + WOLFSSL* ssl = NULL; + byte cfg[512]; + byte small[4]; + word32 cfgSz = (word32)sizeof(cfg); + word32 smallSz = (word32)sizeof(small); + word32 zero = 0; + char b64[1024]; + word32 b64Sz = (word32)sizeof(b64); + + XMEMSET(cfg, 0, sizeof(cfg)); + XMEMSET(b64, 0, sizeof(b64)); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectNotNull(cctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + + /* --- generation: the argument guards, then a real config ------------ */ + ExpectIntNE(wolfSSL_CTX_GenerateEchConfig(NULL, "example.com", 0, 0, 0), + WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_GenerateEchConfig(ctx, NULL, 0, 0, 0), + WOLFSSL_SUCCESS); + /* a KEM/KDF/AEAD triple no build implements: the lookup's failure arm */ + ExpectIntNE(wolfSSL_CTX_GenerateEchConfig(ctx, "example.com", + 0xFFFF, 0xFFFF, 0xFFFF), WOLFSSL_SUCCESS); + /* the accepting partner: defaults */ + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(ctx, "example.com", 0, 0, 0), + WOLFSSL_SUCCESS); + + /* --- reading it back: the size negotiation -------------------------- */ + ExpectIntNE(wolfSSL_CTX_GetEchConfigs(NULL, cfg, &cfgSz), + WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_GetEchConfigs(ctx, cfg, NULL), WOLFSSL_SUCCESS); + /* output NULL with a size pointer is the "how big is it?" call */ + cfgSz = 0; + (void)wolfSSL_CTX_GetEchConfigs(ctx, NULL, &cfgSz); + /* a buffer that cannot hold it: the LENGTH_ERROR arm, which a caller + * sizing from the previous call never takes */ + (void)wolfSSL_CTX_GetEchConfigs(ctx, small, &smallSz); + /* and a size of zero with a real buffer */ + (void)wolfSSL_CTX_GetEchConfigs(ctx, cfg, &zero); + cfgSz = (word32)sizeof(cfg); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(ctx, cfg, &cfgSz), WOLFSSL_SUCCESS); + ExpectIntGT(cfgSz, 0); + + /* --- importing raw bytes on the client ------------------------------ */ + ExpectIntNE(wolfSSL_CTX_SetEchConfigs(NULL, cfg, cfgSz), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_SetEchConfigs(cctx, NULL, cfgSz), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_SetEchConfigs(cctx, cfg, 0), WOLFSSL_SUCCESS); + /* truncated: well-formed prefix, impossible length */ + (void)wolfSSL_CTX_SetEchConfigs(cctx, cfg, 2); + (void)wolfSSL_CTX_SetEchConfigs(cctx, cfg, cfgSz / 2); + ExpectIntEQ(wolfSSL_CTX_SetEchConfigs(cctx, cfg, cfgSz), WOLFSSL_SUCCESS); + + /* --- the base64 path, which has its own decode failure arms --------- */ + ExpectIntNE(wolfSSL_CTX_SetEchConfigsBase64(NULL, b64, b64Sz), + WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_SetEchConfigsBase64(cctx, NULL, b64Sz), + WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_SetEchConfigsBase64(cctx, b64, 0), + WOLFSSL_SUCCESS); + /* not base64 at all */ + XSTRNCPY(b64, "!!!!not base64!!!!", sizeof(b64)); + (void)wolfSSL_CTX_SetEchConfigsBase64(cctx, b64, + (word32)XSTRLEN(b64)); + /* valid base64 that decodes to something that is not an ECH config */ + XSTRNCPY(b64, "AAAAAAAAAAAAAAAAAAAAAAAA", sizeof(b64)); + (void)wolfSSL_CTX_SetEchConfigsBase64(cctx, b64, + (word32)XSTRLEN(b64)); + + /* --- the enable switches, both ways --------------------------------- */ + wolfSSL_CTX_SetEchEnable(ctx, 0); + wolfSSL_CTX_SetEchEnable(ctx, 1); + wolfSSL_CTX_SetEchEnableTrialDecrypt(ctx, 1); + wolfSSL_CTX_SetEchEnableTrialDecrypt(ctx, 0); + + /* --- the per-connection API ----------------------------------------- */ + ExpectNotNull(ssl = wolfSSL_new(cctx)); + ExpectIntNE(wolfSSL_SetEchConfigs(NULL, cfg, cfgSz), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_SetEchConfigs(ssl, NULL, cfgSz), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_SetEchConfigs(ssl, cfg, 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_SetEchConfigs(ssl, cfg, cfgSz), WOLFSSL_SUCCESS); + + ExpectIntNE(wolfSSL_SetEchConfigsBase64(ssl, NULL, b64Sz), + WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_SetEchConfigsBase64(ssl, b64, 0), WOLFSSL_SUCCESS); + + cfgSz = (word32)sizeof(cfg); + (void)wolfSSL_GetEchConfigs(NULL, cfg, &cfgSz); + (void)wolfSSL_GetEchConfigs(ssl, cfg, NULL); + (void)wolfSSL_GetEchConfigs(ssl, cfg, &cfgSz); + + /* Retry configs on a connection that never negotiated ECH: the arm a + * successful handshake cannot reach, because there is nothing to retry. */ + cfgSz = (word32)sizeof(cfg); + (void)wolfSSL_GetEchRetryConfigs(NULL, cfg, &cfgSz); + (void)wolfSSL_GetEchRetryConfigs(ssl, cfg, NULL); + (void)wolfSSL_GetEchRetryConfigs(ssl, cfg, &cfgSz); + smallSz = (word32)sizeof(small); + (void)wolfSSL_GetEchRetryConfigs(ssl, small, &smallSz); + + wolfSSL_SetEchEnable(ssl, 0); + wolfSSL_SetEchEnable(ssl, 1); + wolfSSL_SetEchEnableTrialDecrypt(ssl, 1); + wolfSSL_SetEchEnableTrialDecrypt(ssl, 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(cctx); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_ext.h b/tests/api/test_ssl_ext.h index ee3da3a2108..637831c79e2 100644 --- a/tests/api/test_ssl_ext.h +++ b/tests/api/test_ssl_ext.h @@ -22,6 +22,8 @@ #ifndef TESTS_API_SSL_EXT_H #define TESTS_API_SSL_EXT_H +int test_wolfSSL_ech_config_api(void); + int test_wolfSSL_NoTicketTLSv12_ext(void); int test_wolfSSL_CTX_UseMaxFragment_ext(void); int test_wolfSSL_CTX_num_tickets_ext(void); @@ -106,6 +108,7 @@ int test_wolfSSL_ticket_key_cb_renew_ext(void); TEST_DECL_GROUP("ssl_ext", \ test_wolfSSL_get_secure_renegotiation_support_ext), \ TEST_DECL_GROUP("ssl_ext", test_wolfSSL_set_alpn_protos_badlen_ext), \ - TEST_DECL_GROUP("ssl_ext", test_wolfSSL_ticket_key_cb_renew_ext) + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_ticket_key_cb_renew_ext), \ + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_ech_config_api) #endif /* TESTS_API_SSL_EXT_H */ From 7e316795c3b9b035075b7ef10ca3e1a59c820373 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 13:09:23 +0200 Subject: [PATCH 22/60] tests: argument guards across the newly-visible ssl_api surface 342 of the 646 uncovered conditions in the files declared this part mention NULL -- 88% in ssl_api_cert.c, 85% in ssl_api_crl_ocsp.c, 81% in ssl_api_ext.c, 78% in x509.c. They are not protocol behaviour; they are the checks each entry point makes on its own arguments, and every existing test passes arguments that are valid, so each decision is taken the same way on every call. Two sweeps: the certificate configuration API (mutual auth, verify depth, client and server certificate type lists, expected raw public keys, the verify callbacks) and the CRL/OCSP configuration API (enable/disable, the callback setters, file, directory and buffer loads), each called with a NULL object, a NULL buffer, a zero length and an over-long length, and then with valid arguments so every guard has its partner. ssl_api_cert.c 13/38 -> 20/38. ssl_api_crl_ocsp.c 0/28 -> 1/28: the CRL and OCSP entry points did not respond the way the density predicted even though HAVE_CRL and HAVE_OCSP are both on in this build, so where those 28 conditions actually sit is still an open question. --- tests/api/test_ssl_cert.c | 208 ++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_cert.h | 7 +- 2 files changed, 214 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 44073b5f611..b4216e535e6 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -1499,6 +1499,214 @@ int test_wolfSSL_cert_unload(void) return EXPECT_RESULT(); } +/* --------------------------------------------------------------------------- + * Argument guards across the newly-visible public API surface. + * + * src/ssl_api_cert.c, ssl_api_crl_ocsp.c and their siblings are #included into + * ssl.c rather than compiled standalone, so they produced no object file and + * no module declared them until this part of the campaign. Now that they are + * measured, the shape of what is missing is unambiguous: 342 of the 646 + * uncovered conditions across these files mention NULL, and in the densest of + * them it is 85-100%. + * + * They are not protocol behaviour. They are the checks each entry point makes + * on its own arguments, and the existing tests all pass arguments that are + * valid -- so every one of these decisions is taken the same way on every + * call, and none of the operands has an independence pair. + * + * A caller that queries or configures a CTX or an SSL it has not created yet, + * or passes a zero length, or asks for an output through a NULL pointer, is + * doing something ordinary and wrong. That is what these vectors are. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_cert_api_arg_guards(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + int tp = 0; + const char certTypes[] = { WOLFSSL_CERT_TYPE_X509 }; + unsigned char spki[8]; + + XMEMSET(spki, 0, sizeof(spki)); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* --- mutual auth and verify depth: NULL object, valid argument ------ */ + ExpectIntNE(wolfSSL_CTX_mutual_auth(NULL, 1), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_mutual_auth(NULL, 1), WOLFSSL_SUCCESS); + (void)wolfSSL_CTX_mutual_auth(ctx, 1); + (void)wolfSSL_CTX_mutual_auth(ctx, 0); + (void)wolfSSL_mutual_auth(ssl, 1); + (void)wolfSSL_mutual_auth(ssl, 0); + + ExpectNull(wolfSSL_CTX_GetCertManager(NULL)); + ExpectNotNull(wolfSSL_CTX_GetCertManager(ctx)); + + wolfSSL_CTX_set_verify_depth(NULL, 4); + wolfSSL_CTX_set_verify_depth(ctx, 4); + (void)wolfSSL_CTX_get_verify_depth(NULL); + (void)wolfSSL_CTX_get_verify_depth(ctx); + (void)wolfSSL_get_verify_depth(NULL); + (void)wolfSSL_get_verify_depth(ssl); + + /* --- certificate type lists: NULL object, NULL buffer, bad length --- */ + (void)wolfSSL_CTX_set_client_cert_type(NULL, certTypes, + (int)sizeof(certTypes)); + (void)wolfSSL_CTX_set_server_cert_type(NULL, certTypes, + (int)sizeof(certTypes)); + (void)wolfSSL_set_client_cert_type(NULL, certTypes, + (int)sizeof(certTypes)); + (void)wolfSSL_set_server_cert_type(NULL, certTypes, + (int)sizeof(certTypes)); + /* NULL list with a non-zero length, and a list longer than allowed: + * both are refusals a correct caller never triggers */ + (void)wolfSSL_CTX_set_client_cert_type(ctx, NULL, 1); + (void)wolfSSL_CTX_set_client_cert_type(ctx, certTypes, 0); + (void)wolfSSL_CTX_set_client_cert_type(ctx, certTypes, 99); + (void)wolfSSL_set_server_cert_type(ssl, NULL, 1); + (void)wolfSSL_set_server_cert_type(ssl, certTypes, 0); + (void)wolfSSL_set_server_cert_type(ssl, certTypes, 99); + /* the accepting partners */ + (void)wolfSSL_CTX_set_client_cert_type(ctx, certTypes, + (int)sizeof(certTypes)); + (void)wolfSSL_set_server_cert_type(ssl, certTypes, + (int)sizeof(certTypes)); + + /* negotiated type read back before any handshake, and through NULL */ + (void)wolfSSL_get_negotiated_client_cert_type(NULL, &tp); + (void)wolfSSL_get_negotiated_server_cert_type(NULL, &tp); + (void)wolfSSL_get_negotiated_client_cert_type(ssl, NULL); + (void)wolfSSL_get_negotiated_server_cert_type(ssl, NULL); + (void)wolfSSL_get_negotiated_client_cert_type(ssl, &tp); + (void)wolfSSL_get_negotiated_server_cert_type(ssl, &tp); + + /* --- raw public key expectations ------------------------------------ */ + (void)wolfSSL_CTX_set_expected_rpk(NULL, spki, (word32)sizeof(spki)); + (void)wolfSSL_set_expected_rpk(NULL, spki, (word32)sizeof(spki)); + (void)wolfSSL_CTX_set_expected_rpk(ctx, NULL, (word32)sizeof(spki)); + (void)wolfSSL_set_expected_rpk(ssl, NULL, (word32)sizeof(spki)); + (void)wolfSSL_CTX_set_expected_rpk(ctx, spki, 0); + (void)wolfSSL_set_expected_rpk(ssl, spki, 0); + (void)wolfSSL_CTX_set_expected_rpk(ctx, spki, (word32)sizeof(spki)); + (void)wolfSSL_set_expected_rpk(ssl, spki, (word32)sizeof(spki)); + (void)wolfSSL_CTX_clear_expected_rpk(NULL); + (void)wolfSSL_clear_expected_rpk(NULL); + (void)wolfSSL_CTX_clear_expected_rpk(ctx); + (void)wolfSSL_clear_expected_rpk(ssl); + + /* --- verify configuration through NULL objects ---------------------- */ + wolfSSL_CTX_set_verify(NULL, WOLFSSL_VERIFY_PEER, NULL); + wolfSSL_set_verify(NULL, WOLFSSL_VERIFY_PEER, NULL); + wolfSSL_set_verify_result(NULL, 0); + wolfSSL_CTX_SetCertCbCtx(NULL, NULL); + wolfSSL_SetCertCbCtx(NULL, NULL); + /* and the same on real objects, so each guard has its partner */ + wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(ssl, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify_result(ssl, 0); + wolfSSL_CTX_SetCertCbCtx(ctx, NULL); + wolfSSL_SetCertCbCtx(ssl, NULL); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* The CRL and OCSP configuration entry points, same argument-guard rationale. + * 24 of ssl_api_crl_ocsp.c's 28 uncovered conditions mention NULL. */ +int test_wolfSSL_crl_ocsp_api_arg_guards(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + +#ifdef HAVE_CRL + ExpectIntNE(wolfSSL_EnableCRL(NULL, 0), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_DisableCRL(NULL), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_EnableCRL(NULL, 0), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_DisableCRL(NULL), WOLFSSL_SUCCESS); + (void)wolfSSL_CTX_EnableCRL(ctx, 0); + (void)wolfSSL_CTX_DisableCRL(ctx); + (void)wolfSSL_EnableCRL(ssl, 0); + (void)wolfSSL_DisableCRL(ssl); + + ExpectIntNE(wolfSSL_SetCRL_Cb(NULL, NULL), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_SetCRL_Cb(NULL, NULL), WOLFSSL_SUCCESS); + (void)wolfSSL_SetCRL_Cb(ssl, NULL); + (void)wolfSSL_CTX_SetCRL_Cb(ctx, NULL); + ExpectIntNE(wolfSSL_SetCRL_ErrorCb(NULL, NULL, NULL), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_SetCRL_ErrorCb(NULL, NULL, NULL), WOLFSSL_SUCCESS); + (void)wolfSSL_SetCRL_ErrorCb(ssl, NULL, NULL); + (void)wolfSSL_CTX_SetCRL_ErrorCb(ctx, NULL, NULL); + +#ifdef HAVE_CRL_IO + ExpectIntNE(wolfSSL_SetCRL_IOCb(NULL, NULL), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CTX_SetCRL_IOCb(NULL, NULL), WOLFSSL_SUCCESS); + (void)wolfSSL_SetCRL_IOCb(ssl, NULL); + (void)wolfSSL_CTX_SetCRL_IOCb(ctx, NULL); +#endif + +#if !defined(NO_FILESYSTEM) && !defined(NO_WOLFSSL_DIR) + /* NULL object, NULL path, and a path that does not exist: three + * different refusals, none of which a working configuration produces */ + (void)wolfSSL_LoadCRL(NULL, "certs/crl", WOLFSSL_FILETYPE_PEM, 0); + (void)wolfSSL_CTX_LoadCRL(NULL, "certs/crl", WOLFSSL_FILETYPE_PEM, 0); + (void)wolfSSL_LoadCRL(ssl, NULL, WOLFSSL_FILETYPE_PEM, 0); + (void)wolfSSL_CTX_LoadCRL(ctx, NULL, WOLFSSL_FILETYPE_PEM, 0); + (void)wolfSSL_CTX_LoadCRL(ctx, "certs/no-such-dir", + WOLFSSL_FILETYPE_PEM, 0); + (void)wolfSSL_LoadCRLFile(NULL, "certs/crl/crl.pem", + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CTX_LoadCRLFile(NULL, "certs/crl/crl.pem", + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_LoadCRLFile(ssl, NULL, WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CTX_LoadCRLFile(ctx, NULL, WOLFSSL_FILETYPE_PEM); +#endif + + /* buffer loads: NULL object, NULL buffer, zero length, bad type */ + (void)wolfSSL_CTX_LoadCRLBuffer(NULL, (const unsigned char*)"x", 1, + WOLFSSL_FILETYPE_ASN1); + (void)wolfSSL_LoadCRLBuffer(NULL, (const unsigned char*)"x", 1, + WOLFSSL_FILETYPE_ASN1); + (void)wolfSSL_CTX_LoadCRLBuffer(ctx, NULL, 1, WOLFSSL_FILETYPE_ASN1); + (void)wolfSSL_LoadCRLBuffer(ssl, NULL, 1, WOLFSSL_FILETYPE_ASN1); + (void)wolfSSL_CTX_LoadCRLBuffer(ctx, (const unsigned char*)"x", 0, + WOLFSSL_FILETYPE_ASN1); + (void)wolfSSL_LoadCRLBuffer(ssl, (const unsigned char*)"x", 0, + WOLFSSL_FILETYPE_ASN1); +#endif /* HAVE_CRL */ + +#ifdef HAVE_OCSP + ExpectIntNE(wolfSSL_EnableOCSP(NULL, 0), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_DisableOCSP(NULL), WOLFSSL_SUCCESS); + (void)wolfSSL_EnableOCSP(ssl, 0); + (void)wolfSSL_DisableOCSP(ssl); + (void)wolfSSL_SetOCSP_OverrideURL(NULL, "http://ocsp.example.com/"); + (void)wolfSSL_SetOCSP_OverrideURL(ssl, NULL); + (void)wolfSSL_SetOCSP_OverrideURL(ssl, "http://ocsp.example.com/"); + (void)wolfSSL_SetOCSP_Cb(NULL, NULL, NULL, NULL); + (void)wolfSSL_SetOCSP_Cb(ssl, NULL, NULL, NULL); +#ifdef HAVE_CERTIFICATE_STATUS_REQUEST + ExpectIntNE(wolfSSL_EnableOCSPStapling(NULL), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_DisableOCSPStapling(NULL), WOLFSSL_SUCCESS); + (void)wolfSSL_EnableOCSPStapling(ssl); + (void)wolfSSL_DisableOCSPStapling(ssl); +#endif +#endif /* HAVE_OCSP */ + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + #if !defined(NO_CERTS) && !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) /* One row of the verify-mode mapping table: the mode handed to diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index 151895bbab6..03bcf60c71b 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -22,6 +22,9 @@ #ifndef TESTS_API_SSL_CERT_H #define TESTS_API_SSL_CERT_H +int test_wolfSSL_cert_api_arg_guards(void); +int test_wolfSSL_crl_ocsp_api_arg_guards(void); + int test_wolfSSL_get_verify_mode(void); int test_wolfSSL_CTX_get_verify_mode(void); int test_wolfSSL_get_verify_callback(void); @@ -101,6 +104,8 @@ int test_wolfSSL_verify_post_handshake_defers(void); TEST_DECL_GROUP("ssl_cert", \ test_wolfSSL_verify_empty_server_cert), \ TEST_DECL_GROUP("ssl_cert", \ - test_wolfSSL_verify_post_handshake_defers) + test_wolfSSL_verify_post_handshake_defers), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_cert_api_arg_guards), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_crl_ocsp_api_arg_guards) #endif /* TESTS_API_SSL_CERT_H */ From a3ccf97b856de279c6859cc62232c59fef675775 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 13:47:48 +0200 Subject: [PATCH 23/60] tests: OCSP stapling accessors and a mocked CRL transport The previous sweep moved this file by one condition because it guessed which functions held the gaps. Reading them settles it: not the enable/disable calls, but the stapling request and response accessors, whose operands are of three kinds and none reachable from a client that staples successfully. ssl->options.side != WOLFSSL_CLIENT_END needs a SERVER object; a ctx->method->side != WOLFSSL_CLIENT_END client-side test never has one ssl->ocspProducedDateFormat != ASN_UTC_TIME needs a response that was ... != ASN_GENERALIZED_TIME never processed, or one carrying a GeneralizedTime idx >= XELEM_CNT(ssl->ocspCsrResp), len < 0 an out-of-range slot So the vectors are a server CTX and a server SSL passed to the client-only entry points, the producedDate accessor called before any response exists and again with each format set directly, and the output-pointer and buffer-size operands underneath. CRL gets the transport treatment as well: wolfSSL_SetCRL_IOCb installs the CbCrlIO the library consumes when a certificate names a distribution point, and mocking it -- the CRL equivalent of the CbOCSPIO mock in the ocsp white-box -- returns the outcomes a real distribution point cannot be asked for on demand. ssl_api_crl_ocsp.c 1/28 -> 13/28. --- tests/api/test_ssl_cert.c | 147 ++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_cert.h | 6 +- 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index b4216e535e6..bd878d2abc9 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2539,3 +2539,150 @@ int test_wolfSSL_verify_post_handshake_defers(void) #endif return EXPECT_RESULT(); } + +static int test_crl_io_mock(WOLFSSL_CRL* crl, const char* url, int urlSz) +{ + (void)crl; (void)url; (void)urlSz; + g_crlIoCalls++; + return g_crlIoResult; +} + +int test_wolfSSL_crl_io_mock(void) +{ + EXPECT_DECLS; +#if defined(HAVE_CRL) && defined(HAVE_CRL_IO) && !defined(NO_CERTS) && \ + !defined(NO_WOLFSSL_CLIENT) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + int i; + static const int results[] = { 0, -1, 1 }; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectIntEQ(wolfSSL_CTX_EnableCRL(ctx, WOLFSSL_CRL_CHECK), + WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* the guard operands first */ + (void)wolfSSL_SetCRL_IOCb(NULL, test_crl_io_mock); + (void)wolfSSL_CTX_SetCRL_IOCb(NULL, test_crl_io_mock); + (void)wolfSSL_SetCRL_IOCb(ssl, NULL); + (void)wolfSSL_CTX_SetCRL_IOCb(ctx, NULL); + + /* then the callback installed, returning each of the outcomes a + * distribution point can produce */ + for (i = 0; i < (int)(sizeof(results) / sizeof(results[0])); i++) { + g_crlIoResult = results[i]; + g_crlIoCalls = 0; + (void)wolfSSL_CTX_SetCRL_IOCb(ctx, test_crl_io_mock); + (void)wolfSSL_SetCRL_IOCb(ssl, test_crl_io_mock); + /* loading a certificate whose CRL is missing is what drives the + * callback; the load itself is allowed to fail */ + (void)wolfSSL_CTX_load_verify_locations(ctx, caCertFile, NULL); + } + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* --------------------------------------------------------------------------- + * OCSP stapling accessors, and CRL delivered through a mocked transport. + * + * The previous argument-guard sweep moved ssl_api_crl_ocsp.c by one condition, + * because it guessed at which functions held the gaps. Reading them settles + * it: they are not the enable/disable calls, they are the stapling request + * and response accessors, and their operands are of three kinds -- + * + * ssl->options.side != WOLFSSL_CLIENT_END a SERVER object, which no + * ctx->method->side != WOLFSSL_CLIENT_END client-side test can supply + * + * ssl->ocspProducedDateFormat != ASN_UTC_TIME a response that was never + * processed, or one whose + * producedDate is a + * GeneralizedTime + * + * idx >= XELEM_CNT(ssl->ocspCsrResp), len < 0 an out-of-range slot + * + * None of them is reachable from a working client that staples successfully, + * which is the only shape the existing tests have. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_ocsp_stapling_accessors(void) +{ + EXPECT_DECLS; +#if defined(HAVE_OCSP) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + WOLFSSL_CTX* cctx = NULL; /* client */ + WOLFSSL_CTX* sctx = NULL; /* server: the side operand's partner */ + WOLFSSL* cssl = NULL; + WOLFSSL* sssl = NULL; + + ExpectNotNull(cctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(sctx = wolfSSL_CTX_new(wolfSSLv23_server_method())); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(sctx, svrCertFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(sctx, svrKeyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + ExpectNotNull(cssl = wolfSSL_new(cctx)); + ExpectNotNull(sssl = wolfSSL_new(sctx)); + + /* --- the stapling request calls, once per operand ------------------- */ + (void)wolfSSL_UseOCSPStapling(NULL, WOLFSSL_CSR_OCSP, 0); + (void)wolfSSL_CTX_UseOCSPStapling(NULL, WOLFSSL_CSR_OCSP, 0); + /* a SERVER object: side != CLIENT_END, which is the operand a client-only + * test leaves permanently false */ + (void)wolfSSL_UseOCSPStapling(sssl, WOLFSSL_CSR_OCSP, 0); + (void)wolfSSL_CTX_UseOCSPStapling(sctx, WOLFSSL_CSR_OCSP, 0); + /* the accepting partners */ + (void)wolfSSL_UseOCSPStapling(cssl, WOLFSSL_CSR_OCSP, 0); + (void)wolfSSL_CTX_UseOCSPStapling(cctx, WOLFSSL_CSR_OCSP, 0); + +#ifdef HAVE_CERTIFICATE_STATUS_REQUEST_V2 + (void)wolfSSL_UseOCSPStaplingV2(NULL, WOLFSSL_CSR2_OCSP, 0); + (void)wolfSSL_CTX_UseOCSPStaplingV2(NULL, WOLFSSL_CSR2_OCSP, 0); + (void)wolfSSL_UseOCSPStaplingV2(sssl, WOLFSSL_CSR2_OCSP, 0); + (void)wolfSSL_CTX_UseOCSPStaplingV2(sctx, WOLFSSL_CSR2_OCSP, 0); + (void)wolfSSL_UseOCSPStaplingV2(cssl, WOLFSSL_CSR2_OCSP, 0); + (void)wolfSSL_CTX_UseOCSPStaplingV2(cctx, WOLFSSL_CSR2_OCSP, 0); +#endif + +#ifndef NO_ASN_TIME + /* --- the producedDate accessor -------------------------------------- */ + { + byte when[32]; + int fmt = 0; + + XMEMSET(when, 0, sizeof(when)); + (void)wolfSSL_get_ocsp_producedDate(NULL, when, sizeof(when), &fmt); + /* no response processed: ocspProducedDateFormat is neither UTC nor + * generalized, which is the pair for both operands at :879 */ + (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), &fmt); + /* the output-pointer operands, reached only once a format is set */ + (void)wolfSSL_get_ocsp_producedDate(cssl, NULL, sizeof(when), &fmt); + (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), NULL); + /* a buffer too small to hold the date */ + (void)wolfSSL_get_ocsp_producedDate(cssl, when, 1, &fmt); + + /* Drive the format operands directly. A stapled response carrying a + * GeneralizedTime rather than a UTCTime is legal, rare, and not + * something the test responder emits -- so this is the only way the + * second half of that decision is ever taken. */ + cssl->ocspProducedDateFormat = ASN_UTC_TIME; + (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), &fmt); + (void)wolfSSL_get_ocsp_producedDate(cssl, NULL, sizeof(when), &fmt); + (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), NULL); + (void)wolfSSL_get_ocsp_producedDate(cssl, when, 1, &fmt); + cssl->ocspProducedDateFormat = ASN_GENERALIZED_TIME; + (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), &fmt); + cssl->ocspProducedDateFormat = 0; + } +#endif + + wolfSSL_free(cssl); + wolfSSL_free(sssl); + wolfSSL_CTX_free(cctx); + wolfSSL_CTX_free(sctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index 03bcf60c71b..b1d2c056a77 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -24,6 +24,8 @@ int test_wolfSSL_cert_api_arg_guards(void); int test_wolfSSL_crl_ocsp_api_arg_guards(void); +int test_wolfSSL_ocsp_stapling_accessors(void); +int test_wolfSSL_crl_io_mock(void); int test_wolfSSL_get_verify_mode(void); int test_wolfSSL_CTX_get_verify_mode(void); @@ -106,6 +108,8 @@ int test_wolfSSL_verify_post_handshake_defers(void); TEST_DECL_GROUP("ssl_cert", \ test_wolfSSL_verify_post_handshake_defers), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_cert_api_arg_guards), \ - TEST_DECL_GROUP("ssl_cert", test_wolfSSL_crl_ocsp_api_arg_guards) + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_crl_ocsp_api_arg_guards), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_ocsp_stapling_accessors), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_crl_io_mock) #endif /* TESTS_API_SSL_CERT_H */ From a405ccd2e8f46915d09031b4c666b68762b773cd Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 14:46:04 +0200 Subject: [PATCH 24/60] tests: null-argument burn-down across the public API Null-guards are the largest remaining type: 749 of 2855 uncovered conditions, 26%. Splitting them by enclosing function is the part that matters for planning, because it bounds what an API suite can do at all: 246 in public wolfSSL_* / wc_* functions reachable by calling them 503 in file-static helpers white-box only These vectors take the public third across the extension, DTLS, session and record APIs: an object never created, an output pointer that is NULL, a zero length with a real buffer, each followed by the same call made correctly so the guard has its partner. ssl_api_ext.c 24->30, ssl_api_rw.c 35->37, ssl_api_dtls.c 3->5, ssl.c 7->8. The yield per call is low because most of the 246 sit behind guards that a correctly-typed call already satisfies; what is left needs the specific wrong argument each function checks for, read from the source rather than guessed. The SESSION_* accessors are absent entirely -- they live behind the OpenSSL compatibility layer this option list excludes as a build fact. --- tests/api/test_ssl_ext.c | 158 +++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_ext.h | 6 +- 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index fb1baa2cdcf..170e02bc39e 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1501,3 +1501,161 @@ int test_wolfSSL_ech_config_api(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * Null-argument burn-down across the public API. + * + * A taxonomy of the 2855 conditions still uncovered puts null-guards at 749 -- + * the largest single type, 26% of everything left. Splitting them by enclosing + * function settles what API tests can and cannot do about it: + * + * 246 in public wolfSSL_* / wc_* functions <- these, reachable by call + * 503 in file-static helpers <- white-box only + * + * So an API suite can address a third of the category and no more; the rest is + * structurally out of reach from outside the library. These vectors take the + * public third across the extension, DTLS, session and record APIs. + * + * Every call here is a caller mistake a working program does not make: an + * object that was never created, an output pointer that is NULL, a length of + * zero paired with a real buffer. Each is followed by the same call made + * correctly, so the guard has its independence partner in this binary. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_api_null_burndown(void) +{ + EXPECT_DECLS; +#if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + char* proto = NULL; + word16 protoSz = 0; + unsigned int sz = 0; + byte buf[64]; + + XMEMSET(buf, 0, sizeof(buf)); + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* --- extension API: ssl_api_ext.c ----------------------------------- */ +#ifdef HAVE_SNI + (void)wolfSSL_UseSNI(NULL, WOLFSSL_SNI_HOST_NAME, "a", 1); + (void)wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, NULL, 1); + (void)wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, "a", 0); + (void)wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, "a", 1); + (void)wolfSSL_CTX_UseSNI(NULL, WOLFSSL_SNI_HOST_NAME, "a", 1); + (void)wolfSSL_CTX_UseSNI(ctx, WOLFSSL_SNI_HOST_NAME, NULL, 1); + (void)wolfSSL_SNI_GetRequest(NULL, WOLFSSL_SNI_HOST_NAME, NULL); + (void)wolfSSL_SNI_GetRequest(ssl, WOLFSSL_SNI_HOST_NAME, NULL); +#endif +#ifdef HAVE_ALPN + (void)wolfSSL_UseALPN(NULL, "h2", 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); + (void)wolfSSL_UseALPN(ssl, NULL, 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); + (void)wolfSSL_UseALPN(ssl, "h2", 0, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); + (void)wolfSSL_UseALPN(ssl, "h2", 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); + (void)wolfSSL_ALPN_GetProtocol(NULL, &proto, &protoSz); + (void)wolfSSL_ALPN_GetProtocol(ssl, NULL, &protoSz); + (void)wolfSSL_ALPN_GetProtocol(ssl, &proto, NULL); + (void)wolfSSL_ALPN_GetProtocol(ssl, &proto, &protoSz); +#endif +#ifdef HAVE_TRUSTED_CA + (void)wolfSSL_UseTrustedCA(NULL, WOLFSSL_TRUSTED_CA_PRE_AGREED, NULL, 0); + (void)wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_X509_NAME, NULL, 4); + (void)wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_PRE_AGREED, NULL, 0); +#endif +#ifdef HAVE_MAX_FRAGMENT + (void)wolfSSL_UseMaxFragment(NULL, WOLFSSL_MFL_2_9); + (void)wolfSSL_UseMaxFragment(ssl, 0); + (void)wolfSSL_UseMaxFragment(ssl, 0xFF); + (void)wolfSSL_UseMaxFragment(ssl, WOLFSSL_MFL_2_9); + (void)wolfSSL_CTX_UseMaxFragment(NULL, WOLFSSL_MFL_2_9); +#endif +#ifdef HAVE_SUPPORTED_CURVES + (void)wolfSSL_UseSupportedCurve(NULL, WOLFSSL_ECC_SECP256R1); + (void)wolfSSL_UseSupportedCurve(ssl, 0); + (void)wolfSSL_UseSupportedCurve(ssl, WOLFSSL_ECC_SECP256R1); + (void)wolfSSL_CTX_UseSupportedCurve(NULL, WOLFSSL_ECC_SECP256R1); +#endif + + /* --- DTLS API on a non-DTLS ssl: ssl_api_dtls.c --------------------- */ +#ifdef WOLFSSL_DTLS + (void)wolfSSL_dtls_get_current_timeout(NULL); + (void)wolfSSL_dtls_get_current_timeout(ssl); + (void)wolfSSL_dtls_set_timeout_init(NULL, 1); + (void)wolfSSL_dtls_set_timeout_init(ssl, -1); + (void)wolfSSL_dtls_set_timeout_init(ssl, 1); + (void)wolfSSL_dtls_got_timeout(NULL); + (void)wolfSSL_dtls_got_timeout(ssl); + (void)wolfSSL_dtls_retransmit(NULL); + (void)wolfSSL_dtls_retransmit(ssl); + sz = (unsigned int)sizeof(buf); + (void)wolfSSL_dtls_get_peer(NULL, buf, &sz); + (void)wolfSSL_dtls_get_peer(ssl, NULL, &sz); + (void)wolfSSL_dtls_get_peer(ssl, buf, NULL); + (void)wolfSSL_dtls_get_peer(ssl, buf, &sz); + (void)wolfSSL_dtls_set_pending_peer(NULL, buf, (unsigned int)sizeof(buf)); + (void)wolfSSL_dtls_set_pending_peer(ssl, NULL, (unsigned int)sizeof(buf)); + (void)wolfSSL_dtls_set_pending_peer(ssl, buf, 0); + (void)wolfSSL_dtls(NULL); + (void)wolfSSL_dtls(ssl); +#endif + + /* --- read/write status: ssl_api_rw.c -------------------------------- */ + (void)wolfSSL_want_read(NULL); + (void)wolfSSL_want_read(ssl); + (void)wolfSSL_want_write(NULL); + (void)wolfSSL_want_write(ssl); + (void)wolfSSL_pending(NULL); + (void)wolfSSL_pending(ssl); + + /* --- cipher and curve name lookups: ssl.c --------------------------- */ + (void)wolfSSL_get_curve_name(NULL); + (void)wolfSSL_get_curve_name(ssl); + (void)wolfSSL_get_cipher_name(NULL); + (void)wolfSSL_get_cipher_name(ssl); + (void)wolfSSL_get_cipher(NULL); + (void)wolfSSL_get_cipher(ssl); + (void)wolfSSL_get_version(NULL); + (void)wolfSSL_get_version(ssl); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* Session objects: 41 null-guards, the densest public surface left. Every one + * is a caller reading from or duplicating a session it does not have. */ +int test_wolfSSL_session_null_burndown(void) +{ + EXPECT_DECLS; +#if !defined(NO_SESSION_CACHE) && !defined(NO_WOLFSSL_CLIENT) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + WOLFSSL_SESSION* sess = NULL; + /* The SESSION_* accessors (master_key, id, is_setup, time) live behind + * the OpenSSL compatibility layer, which this option list excludes as a + * build fact, so they are not callable here. What remains is the native + * session API. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* a session that was never established */ + (void)wolfSSL_get_session(NULL); + (void)wolfSSL_get1_session(NULL); + (void)wolfSSL_SESSION_dup(NULL); + wolfSSL_SESSION_free(NULL); + (void)wolfSSL_set_session(NULL, NULL); + (void)wolfSSL_set_session(ssl, NULL); + + /* and against a real, unestablished session where one exists */ + sess = wolfSSL_get1_session(ssl); + if (sess != NULL) { + (void)wolfSSL_SESSION_dup(sess); + wolfSSL_SESSION_free(sess); + } + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_ext.h b/tests/api/test_ssl_ext.h index 637831c79e2..527d929c0da 100644 --- a/tests/api/test_ssl_ext.h +++ b/tests/api/test_ssl_ext.h @@ -23,6 +23,8 @@ #define TESTS_API_SSL_EXT_H int test_wolfSSL_ech_config_api(void); +int test_wolfSSL_api_null_burndown(void); +int test_wolfSSL_session_null_burndown(void); int test_wolfSSL_NoTicketTLSv12_ext(void); int test_wolfSSL_CTX_UseMaxFragment_ext(void); @@ -109,6 +111,8 @@ int test_wolfSSL_ticket_key_cb_renew_ext(void); test_wolfSSL_get_secure_renegotiation_support_ext), \ TEST_DECL_GROUP("ssl_ext", test_wolfSSL_set_alpn_protos_badlen_ext), \ TEST_DECL_GROUP("ssl_ext", test_wolfSSL_ticket_key_cb_renew_ext), \ - TEST_DECL_GROUP("ssl_ext", test_wolfSSL_ech_config_api) + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_ech_config_api), \ + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_api_null_burndown), \ + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_session_null_burndown) #endif /* TESTS_API_SSL_EXT_H */ From a1a7bd4591fb082c277a0d7a5018d22ed2701e37 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 14:51:24 +0200 Subject: [PATCH 25/60] tests: null vectors aimed at named operands, not first arguments The previous pass sprayed NULL at the first argument of everything and returned 11 conditions for about a hundred calls. A guard like if ((ssl == NULL) || (p == NULL) || (g == NULL)) has three operands, and a NULL in the first slot pairs only the first -- the other two are never evaluated because the chain short-circuits. Each operand needs its own call with every other argument valid, and the ledger already says which operand of which decision is missing. Taking the vectors from GAPS.md instead of from the function name: 26 conditions from one test, against 11 from the spray. ssl_api_ext.c 30->38, ssl_load.c 22->34, ssl_sess.c 27->32, ssl.c 8->11. --- tests/api/test_ssl_ext.c | 171 +++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_ext.h | 4 +- 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index 170e02bc39e..c8b842d1434 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1659,3 +1659,174 @@ int test_wolfSSL_session_null_burndown(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * Null-guard vectors aimed at named operands. + * + * The previous pass sprayed NULL at the first argument of everything and + * returned 11 conditions for a hundred calls. The reason is that a guard like + * + * if ((ssl == NULL) || (p == NULL) || (g == NULL)) + * + * has three operands, and a NULL in the first slot pairs only the first: the + * other two are never evaluated. Each operand needs its own call, with every + * other argument valid. + * + * So these vectors come from the ledger rather than from guesswork -- one call + * per uncovered operand of each guard, followed by the all-valid call that is + * their shared partner. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_api_null_operands(void) +{ + EXPECT_DECLS; +#if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[64]; + word32 bufSz = (word32)sizeof(buf); + int iSz = (int)sizeof(buf); + + XMEMSET(buf, 0, sizeof(buf)); + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* --- SetTmpDH: (ssl|ctx == NULL) || (p == NULL) || (g == NULL) ------ */ +#if !defined(NO_DH) && !defined(WOLFSSL_NO_TLS12) + { + static const byte p[] = { 0x00, 0x01 }; + static const byte g[] = { 0x02 }; + + (void)wolfSSL_SetTmpDH(NULL, p, (int)sizeof(p), g, (int)sizeof(g)); + (void)wolfSSL_SetTmpDH(ssl, NULL, (int)sizeof(p), g, (int)sizeof(g)); + (void)wolfSSL_SetTmpDH(ssl, p, (int)sizeof(p), NULL, (int)sizeof(g)); + (void)wolfSSL_SetTmpDH(ssl, p, 0, g, (int)sizeof(g)); + (void)wolfSSL_SetTmpDH(ssl, p, (int)sizeof(p), g, 0); + (void)wolfSSL_SetTmpDH(ssl, p, (int)sizeof(p), g, (int)sizeof(g)); + + (void)wolfSSL_CTX_SetTmpDH(NULL, p, (int)sizeof(p), g, (int)sizeof(g)); + (void)wolfSSL_CTX_SetTmpDH(ctx, NULL, (int)sizeof(p), g, + (int)sizeof(g)); + (void)wolfSSL_CTX_SetTmpDH(ctx, p, (int)sizeof(p), NULL, + (int)sizeof(g)); + (void)wolfSSL_CTX_SetTmpDH(ctx, p, 0, g, (int)sizeof(g)); + (void)wolfSSL_CTX_SetTmpDH(ctx, p, (int)sizeof(p), g, 0); + (void)wolfSSL_CTX_SetTmpDH(ctx, p, (int)sizeof(p), g, (int)sizeof(g)); + } +#endif + + /* --- load_verify_locations_ex: ctx, then (file == NULL && path == NULL), + * which is a compound operand a caller giving either one never takes --- */ +#ifndef NO_FILESYSTEM + (void)wolfSSL_CTX_load_verify_locations_ex(NULL, caCertFile, NULL, 0); + (void)wolfSSL_CTX_load_verify_locations_ex(ctx, NULL, NULL, 0); + (void)wolfSSL_CTX_load_verify_locations_ex(ctx, caCertFile, NULL, 0); + (void)wolfSSL_CTX_load_verify_locations(NULL, caCertFile, NULL); + (void)wolfSSL_CTX_load_verify_locations(ctx, NULL, NULL); +#endif + + /* --- export_keying_material: ssl, out, label, and the context pair --- */ +#ifdef HAVE_KEYING_MATERIAL + (void)wolfSSL_export_keying_material(NULL, buf, sizeof(buf), + "label", 5, NULL, 0, 0); + (void)wolfSSL_export_keying_material(ssl, NULL, sizeof(buf), + "label", 5, NULL, 0, 0); + (void)wolfSSL_export_keying_material(ssl, buf, sizeof(buf), + NULL, 5, NULL, 0, 0); + /* use_context set with a NULL context: the operand pair a caller that + * passes both or neither cannot produce */ + (void)wolfSSL_export_keying_material(ssl, buf, sizeof(buf), + "label", 5, NULL, 0, 1); + (void)wolfSSL_export_keying_material(ssl, buf, sizeof(buf), + "label", 5, buf, 4, 1); + (void)wolfSSL_export_keying_material(ssl, buf, sizeof(buf), + "label", 5, NULL, 0, 0); +#endif + + /* --- SetServerID: ssl, id, then len <= 0 ---------------------------- */ +#ifndef NO_SESSION_CACHE + (void)wolfSSL_SetServerID(NULL, buf, iSz, 0); + (void)wolfSSL_SetServerID(ssl, NULL, iSz, 0); + (void)wolfSSL_SetServerID(ssl, buf, 0, 0); + (void)wolfSSL_SetServerID(ssl, buf, -1, 0); + (void)wolfSSL_SetServerID(ssl, buf, iSz, 0); + /* SetSession: ssl, session, then a session that exists but is not set up */ + (void)wolfSSL_SetSession(NULL, NULL); + (void)wolfSSL_SetSession(ssl, NULL); +#endif + + /* --- ALPN peer protocol: ssl, list, listSz -------------------------- */ +#ifdef HAVE_ALPN + { + char* list = NULL; + word16 listSz = 0; + + (void)wolfSSL_ALPN_GetPeerProtocol(NULL, &list, &listSz); + (void)wolfSSL_ALPN_GetPeerProtocol(ssl, NULL, &listSz); + (void)wolfSSL_ALPN_GetPeerProtocol(ssl, &list, NULL); + (void)wolfSSL_ALPN_GetPeerProtocol(ssl, &list, &listSz); + if (list != NULL) + XFREE(list, NULL, DYNAMIC_TYPE_TLSX); + } +#endif + + /* --- SNI from a raw ClientHello buffer ------------------------------ */ +#ifdef HAVE_SNI + { + byte hello[64]; + word32 outSz = (word32)sizeof(buf); + + XMEMSET(hello, 0, sizeof(hello)); + /* one operand of `clientHello != NULL && helloSz > 0 && sni != NULL + * && inOutSz != NULL` per call */ + (void)wolfSSL_SNI_GetFromBuffer(NULL, (word32)sizeof(hello), + WOLFSSL_SNI_HOST_NAME, buf, &outSz); + (void)wolfSSL_SNI_GetFromBuffer(hello, 0, + WOLFSSL_SNI_HOST_NAME, buf, &outSz); + (void)wolfSSL_SNI_GetFromBuffer(hello, (word32)sizeof(hello), + WOLFSSL_SNI_HOST_NAME, NULL, &outSz); + (void)wolfSSL_SNI_GetFromBuffer(hello, (word32)sizeof(hello), + WOLFSSL_SNI_HOST_NAME, buf, NULL); + (void)wolfSSL_SNI_GetFromBuffer(hello, (word32)sizeof(hello), + WOLFSSL_SNI_HOST_NAME, buf, &outSz); + } +#endif + + /* --- trusted CA: the (certId != NULL) || (certIdSz != 0) pair ------- */ +#ifdef HAVE_TRUSTED_CA + (void)wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_PRE_AGREED, buf, 0); + (void)wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_PRE_AGREED, NULL, 4); + (void)wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_KEY_SHA1, buf, + (word32)sizeof(buf)); +#endif + + /* --- DTLS peer: `peer != NULL && peerSz != NULL` --------------------- */ +#ifdef WOLFSSL_DTLS + bufSz = (word32)sizeof(buf); + (void)wolfSSL_dtls_get_peer(ssl, NULL, &bufSz); + (void)wolfSSL_dtls_get_peer(ssl, buf, NULL); + (void)wolfSSL_dtls_get_peer(ssl, buf, &bufSz); + /* got_timeout on a connection that is not DTLS: the second operand */ + (void)wolfSSL_dtls_got_timeout(ssl); +#endif + + /* --- cipher suite lookup by name: name, then the output pointers ---- */ + { + byte c0 = 0, c1 = 0; + + (void)wolfSSL_get_cipher_suite_from_name(NULL, &c0, &c1, NULL); + (void)wolfSSL_get_cipher_suite_from_name("TLS13-AES128-GCM-SHA256", + NULL, &c1, NULL); + (void)wolfSSL_get_cipher_suite_from_name("TLS13-AES128-GCM-SHA256", + &c0, NULL, NULL); + (void)wolfSSL_get_cipher_suite_from_name("no-such-suite", + &c0, &c1, NULL); + (void)wolfSSL_get_cipher_suite_from_name("TLS13-AES128-GCM-SHA256", + &c0, &c1, NULL); + } + + (void)bufSz; (void)iSz; + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_ext.h b/tests/api/test_ssl_ext.h index 527d929c0da..3e61ba5b5b5 100644 --- a/tests/api/test_ssl_ext.h +++ b/tests/api/test_ssl_ext.h @@ -25,6 +25,7 @@ int test_wolfSSL_ech_config_api(void); int test_wolfSSL_api_null_burndown(void); int test_wolfSSL_session_null_burndown(void); +int test_wolfSSL_api_null_operands(void); int test_wolfSSL_NoTicketTLSv12_ext(void); int test_wolfSSL_CTX_UseMaxFragment_ext(void); @@ -113,6 +114,7 @@ int test_wolfSSL_ticket_key_cb_renew_ext(void); TEST_DECL_GROUP("ssl_ext", test_wolfSSL_ticket_key_cb_renew_ext), \ TEST_DECL_GROUP("ssl_ext", test_wolfSSL_ech_config_api), \ TEST_DECL_GROUP("ssl_ext", test_wolfSSL_api_null_burndown), \ - TEST_DECL_GROUP("ssl_ext", test_wolfSSL_session_null_burndown) + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_session_null_burndown), \ + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_api_null_operands) #endif /* TESTS_API_SSL_EXT_H */ From 9853cbc2a57c9d2f51973a79981db85c67de62b4 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 14:56:07 +0200 Subject: [PATCH 26/60] tests: give each guard the object its accepting half needs x509.c and ssl_api_dtls.c sat at the bottom of the newly-visible surface for the same reason, and it was not that their guards are hard to reach. It is that the ACCEPTING half of each pair needs an object no test in the group had. x509.c every accessor is x509 == NULL || outSz == NULL || ..., so the NULL half is trivial and the valid half needs a parsed certificate. No test here was holding one. 0/37 -> 12/37. ssl_api_dtls.c every guard is ssl == NULL || !ssl->options.dtls, so the second operand needs a DTLS connection; every test in this group used a TLS one, leaving that operand constant. 5/53 -> 11/53. Both fixtures are cheap -- a certificate from certs/, and a WOLFSSL made from a DTLS method -- and neither needs a peer. The pattern generalises: where a file sits near zero, look for the object its guards compare against before concluding the conditions are hard. --- tests/api/test_ssl_cert.c | 154 ++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_cert.h | 6 +- 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index bd878d2abc9..0368a7f5687 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2686,3 +2686,157 @@ int test_wolfSSL_ocsp_stapling_accessors(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * X509 accessors and the DTLS API, each with the object its guard needs. + * + * Two files sat at the bottom of the newly-visible surface for the same + * reason, and it is not that their guards are hard to reach -- it is that the + * ACCEPTING half of each pair needs an object the existing tests do not have. + * + * x509.c every accessor is `x509 == NULL || outSz == NULL || ...`, so the + * NULL half is trivial and the valid half needs a parsed + * certificate. There was no test holding one. + * + * ssl_api_dtls.c every guard is `ssl == NULL || !ssl->options.dtls`, so the + * second operand needs a DTLS connection. Every test in this group + * used a TLS one, which leaves that operand constant. + * + * Both fixtures are cheap: a certificate loaded from certs/, and a WOLFSSL + * made from a DTLS method. Neither needs a peer. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_x509_accessor_guards(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && defined(WOLFSSL_CERT_GEN) + WOLFSSL_X509* x509 = NULL; + byte buf[2048]; + int iSz = (int)sizeof(buf); + word32 wSz = (word32)sizeof(buf); + const byte* der = NULL; + int derSz = 0; + + XMEMSET(buf, 0, sizeof(buf)); + + /* the NULL half of every guard, before any fixture exists */ + (void)wolfSSL_X509_get_der(NULL, &derSz); + (void)wolfSSL_X509_get_serial_number(NULL, buf, &iSz); + (void)wolfSSL_X509_get_signature(NULL, buf, &iSz); + (void)wolfSSL_X509_get_next_altname(NULL); + (void)wolfSSL_X509_load_certificate_file(NULL, WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_X509_load_certificate_file("certs/no-such-file.pem", + WOLFSSL_FILETYPE_PEM); + + /* the accepting half: a real parsed certificate */ + x509 = wolfSSL_X509_load_certificate_file(svrCertFile, + WOLFSSL_FILETYPE_PEM); + if (x509 != NULL) { + /* second operand of each guard: valid object, NULL output */ + (void)wolfSSL_X509_get_der(x509, NULL); + (void)wolfSSL_X509_get_serial_number(x509, buf, NULL); + (void)wolfSSL_X509_get_serial_number(x509, NULL, &iSz); + (void)wolfSSL_X509_get_signature(x509, buf, NULL); + /* a buffer too small for the signature: the size operand, which a + * caller sizing from the query call never takes */ + iSz = 1; + (void)wolfSSL_X509_get_signature(x509, buf, &iSz); + /* the query form: NULL buffer with a size pointer */ + iSz = 0; + (void)wolfSSL_X509_get_signature(x509, NULL, &iSz); + iSz = (int)sizeof(buf); + (void)wolfSSL_X509_get_signature(x509, buf, &iSz); + + derSz = 0; + der = wolfSSL_X509_get_der(x509, &derSz); + (void)der; + + iSz = (int)sizeof(buf); + (void)wolfSSL_X509_get_serial_number(x509, buf, &iSz); + (void)wolfSSL_X509_get_next_altname(x509); + (void)wolfSSL_X509_notBefore(x509); + (void)wolfSSL_X509_notAfter(x509); + (void)wolfSSL_X509_version(x509); + +#ifdef OPENSSL_EXTRA + (void)wolfSSL_X509_check_host(x509, NULL, 0, 0, NULL); + (void)wolfSSL_X509_check_host(NULL, "example.com", 11, 0, NULL); +#endif + wSz = (word32)sizeof(buf); + (void)wolfSSL_X509_get_pubkey_buffer(x509, buf, (int*)&wSz); + (void)wolfSSL_X509_get_pubkey_buffer(x509, NULL, (int*)&wSz); + (void)wolfSSL_X509_get_pubkey_buffer(x509, buf, NULL); + + wolfSSL_X509_free(x509); + } + (void)wSz; +#endif + return EXPECT_RESULT(); +} + +int test_wolfSSL_dtls_api_on_dtls_object(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_DTLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) + WOLFSSL_CTX* dctx = NULL; /* the object the second operand needs */ + WOLFSSL_CTX* tctx = NULL; /* a TLS one, for the operand's other half */ + WOLFSSL* dssl = NULL; + WOLFSSL* tssl = NULL; + byte peer[64]; + unsigned int peerSz = (unsigned int)sizeof(peer); + + XMEMSET(peer, 0, sizeof(peer)); + ExpectNotNull(dctx = wolfSSL_CTX_new(wolfDTLSv1_2_client_method())); + ExpectNotNull(tctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(dssl = wolfSSL_new(dctx)); + ExpectNotNull(tssl = wolfSSL_new(tctx)); + + /* `ssl == NULL || !ssl->options.dtls` -- three vectors, one per outcome */ + (void)wolfSSL_dtls_got_timeout(NULL); + (void)wolfSSL_dtls_got_timeout(tssl); /* not a DTLS connection */ + (void)wolfSSL_dtls_got_timeout(dssl); /* the accepting partner */ + (void)wolfSSL_dtls_retransmit(NULL); + (void)wolfSSL_dtls_retransmit(tssl); + (void)wolfSSL_dtls_retransmit(dssl); + (void)wolfSSL_dtls_get_current_timeout(tssl); + (void)wolfSSL_dtls_get_current_timeout(dssl); + (void)wolfSSL_dtls(tssl); + (void)wolfSSL_dtls(dssl); + + /* peer accessors on a connection that has no peer set yet */ + peerSz = (unsigned int)sizeof(peer); + (void)wolfSSL_dtls_get_peer(dssl, peer, &peerSz); + (void)wolfSSL_dtls_get_peer(dssl, NULL, &peerSz); + (void)wolfSSL_dtls_get_peer(dssl, peer, NULL); + (void)wolfSSL_dtls_set_pending_peer(dssl, peer, 0); + (void)wolfSSL_dtls_set_pending_peer(dssl, NULL, + (unsigned int)sizeof(peer)); + (void)wolfSSL_dtls_set_pending_peer(dssl, peer, + (unsigned int)sizeof(peer)); + /* and again now that a peer exists, so the `peer.sa != NULL` operand + * gets both values */ + (void)wolfSSL_dtls_set_pending_peer(dssl, peer, + (unsigned int)sizeof(peer)); + + /* MTU: `ctx == NULL || newMtu > MAX_RECORD_SIZE`, both operands */ +#ifdef WOLFSSL_DTLS_MTU + (void)wolfSSL_CTX_dtls_set_mtu(NULL, 512); + (void)wolfSSL_CTX_dtls_set_mtu(dctx, 0xFFFF); + (void)wolfSSL_CTX_dtls_set_mtu(dctx, 512); + (void)wolfSSL_dtls_set_mtu(dssl, 0xFFFF); + (void)wolfSSL_dtls_set_mtu(dssl, 512); +#endif + +#ifdef WOLFSSL_DTLS13 + (void)wolfSSL_dtls13_has_pending_msg(dssl); + (void)wolfSSL_dtls13_use_quick_timeout(dssl); + wolfSSL_dtls13_set_send_more_acks(dssl, 1); + wolfSSL_dtls13_set_send_more_acks(dssl, 0); +#endif + + wolfSSL_free(dssl); + wolfSSL_free(tssl); + wolfSSL_CTX_free(dctx); + wolfSSL_CTX_free(tctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index b1d2c056a77..e9795b0ad03 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -26,6 +26,8 @@ int test_wolfSSL_cert_api_arg_guards(void); int test_wolfSSL_crl_ocsp_api_arg_guards(void); int test_wolfSSL_ocsp_stapling_accessors(void); int test_wolfSSL_crl_io_mock(void); +int test_wolfSSL_x509_accessor_guards(void); +int test_wolfSSL_dtls_api_on_dtls_object(void); int test_wolfSSL_get_verify_mode(void); int test_wolfSSL_CTX_get_verify_mode(void); @@ -110,6 +112,8 @@ int test_wolfSSL_verify_post_handshake_defers(void); TEST_DECL_GROUP("ssl_cert", test_wolfSSL_cert_api_arg_guards), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_crl_ocsp_api_arg_guards), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_ocsp_stapling_accessors), \ - TEST_DECL_GROUP("ssl_cert", test_wolfSSL_crl_io_mock) + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_crl_io_mock), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_x509_accessor_guards), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_dtls_api_on_dtls_object) #endif /* TESTS_API_SSL_CERT_H */ From a52d92f8107ef61e9a8fe83f5fcab5ac76792cca Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 15:01:26 +0200 Subject: [PATCH 27/60] tests: file-load failure arms, including a FIFO for the unseekable case ssl_misc.c is 11 conditions of file-helper guards -- a seek that fails, a length that is zero or absurd, a read shorter than promised -- all in static helpers, but every public load-from-file entry point runs through them, so the vector is the FILE rather than the argument. An empty file gives sz <= 0; a directory and a nonexistent name give the open and read failures; a file that is not a certificate exercises the parse failure after a successful read. The unseekable case needs a FIFO. fopen() on it succeeds, so the XBADFILE arm is passed, and then fseek() fails with ESPIPE because a pipe has no position -- the exact shape the guard is written for, and nothing on disk imitates it. The FIFO is opened read-write by the test first so the library's fopen() cannot block; a hanging test costs the variant as surely as a crash. ssl_misc.c 0/11 -> 4/11, ssl_load.c 34/155 -> 35/155. The short-read arm (XFREAD returning fewer bytes than the seek promised) is still open and is not reachable this way: once the seek fails the function returns before reading, and a regular file always reads what it promised. That one needs fault injection below the C library, not a different file type. --- tests/api/test_ssl_cert.c | 163 ++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_cert.h | 6 +- 2 files changed, 168 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 0368a7f5687..6951499c93d 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -34,6 +34,13 @@ #include #include +#if defined(__unix__) && !defined(NO_FILESYSTEM) + #include + #include + #include + #include +#endif + /* Tests for the certificate APIs in src/ssl_api_cert.c (moved from ssl.c). */ /* Test reading back the verification mode from an object. @@ -2840,3 +2847,159 @@ int test_wolfSSL_dtls_api_on_dtls_object(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * Certificate loading from files that are wrong in file-system ways. + * + * ssl_misc.c is 0/11 and every one of its conditions is in a static file + * helper -- wolfssl_file_len and wolfssl_read_file_static -- guarding against + * a seek that fails, a length that is zero or absurd, and a read that returns + * fewer bytes than the length promised: + * + * if ((ret == 0) && ((sz > MAX_WOLFSSL_FILE_SIZE) || (sz <= 0L))) + * if ((ret == 0) && ((file = XFOPEN(fname, "rb")) == XBADFILE)) + * if ((ret == 0) && (XFREAD(...) != sz)) + * + * They are static, but they are not out of reach: every public load-from-file + * entry point runs through them, so the vector is the FILE rather than the + * argument. An empty file gives sz <= 0; a directory passed where a file is + * expected gives a seek or read that fails; a name that does not exist gives + * XBADFILE. None of those is something a working configuration supplies, and + * no existing test supplies them either. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_load_pathological_files(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_WOLFSSL_CLIENT) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL_CERT_MANAGER* cm = NULL; + const char* emptyFile = "test-empty-cert.tmp"; + XFILE f = XBADFILE; + + /* an empty file: the `sz <= 0` operand, which no real certificate has */ + f = XFOPEN(emptyFile, "wb"); + if (f != XBADFILE) + XFCLOSE(f); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + + /* XBADFILE: a name that does not exist */ + (void)wolfSSL_CTX_load_verify_locations(ctx, "no-such-file.pem", NULL); + (void)wolfSSL_CTX_use_certificate_file(ctx, "no-such-file.pem", + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CTX_use_PrivateKey_file(ctx, "no-such-file.pem", + WOLFSSL_FILETYPE_PEM); + + /* sz <= 0: the empty file */ + (void)wolfSSL_CTX_load_verify_locations(ctx, emptyFile, NULL); + (void)wolfSSL_CTX_use_certificate_file(ctx, emptyFile, + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CTX_use_PrivateKey_file(ctx, emptyFile, + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CTX_use_certificate_chain_file(ctx, emptyFile); + + /* a directory where a file is expected: the seek and read failure arms */ + (void)wolfSSL_CTX_load_verify_locations(ctx, "certs", NULL); + (void)wolfSSL_CTX_use_certificate_file(ctx, "certs", + WOLFSSL_FILETYPE_PEM); + + /* a file that exists and is not a certificate at all */ + (void)wolfSSL_CTX_load_verify_locations(ctx, "Makefile", NULL); + + /* the accepting partner, so every operand above has one */ + (void)wolfSSL_CTX_load_verify_locations(ctx, caCertFile, NULL); + + /* the same set through the CertManager, which has its own copies of the + * load paths */ + cm = wolfSSL_CertManagerNew(); + if (cm != NULL) { + (void)wolfSSL_CertManagerLoadCA(cm, "no-such-file.pem", NULL); + (void)wolfSSL_CertManagerLoadCA(cm, emptyFile, NULL); + (void)wolfSSL_CertManagerLoadCA(cm, "certs", NULL); + (void)wolfSSL_CertManagerLoadCA(cm, caCertFile, NULL); + (void)wolfSSL_CertManagerVerify(cm, "no-such-file.pem", + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CertManagerVerify(cm, emptyFile, WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CertManagerVerify(cm, svrCertFile, + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CertManagerUnloadCAs(cm); + (void)wolfSSL_CertManagerUnloadCAs(NULL); + wolfSSL_CertManagerFree(cm); + } + (void)wolfSSL_CertManagerNew_ex(NULL); + + wolfSSL_CTX_free(ctx); + (void)remove(emptyFile); +#endif + return EXPECT_RESULT(); +} + +/* --------------------------------------------------------------------------- + * The file-helper failure arms, reached with a FIFO. + * + * Three of ssl_misc.c's conditions cannot be produced by any regular file: + * + * if ((ret == 0) && (XFSEEK(fp, 0, SEEK_END) != 0)) seek failed + * if ((ret == 0) && (XFREAD(...) != (size_t)sz)) short read + * + * A regular file always seeks and always reads what it promised. A FIFO does + * neither: fopen() succeeds, so the XBADFILE arm is passed, and then fseek() + * fails with ESPIPE because a pipe has no position. That is the exact shape + * the guard is written for, and nothing on disk can imitate it. + * + * The FIFO is opened read-write by the test before the library touches it, so + * the library's fopen() cannot block waiting for a writer -- a test that hangs + * costs the whole variant just as surely as one that crashes. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_load_from_fifo(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(NO_WOLFSSL_CLIENT) && defined(__unix__) + WOLFSSL_CTX* ctx = NULL; + const char* fifo = "test-cert-fifo.tmp"; + int fd = -1; + + (void)remove(fifo); + if (mkfifo(fifo, 0600) != 0) { + /* no FIFO support here; that is a platform fact, not a failure */ + return EXPECT_RESULT(); + } + /* Hold it open both ways so the library's fopen() returns immediately + * and there is something to read. */ + fd = open(fifo, O_RDWR | O_NONBLOCK); + if (fd >= 0) { + const char junk[] = "-----BEGIN CERTIFICATE-----\n"; + ssize_t w = write(fd, junk, sizeof(junk) - 1); + (void)w; + } + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + if (fd >= 0) { + /* fopen succeeds, fseek fails: the seek-failure arm */ + (void)wolfSSL_CTX_load_verify_locations(ctx, fifo, NULL); + (void)wolfSSL_CTX_use_certificate_file(ctx, fifo, + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CTX_use_PrivateKey_file(ctx, fifo, + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CTX_use_certificate_chain_file(ctx, fifo); + { + WOLFSSL_CERT_MANAGER* cm = wolfSSL_CertManagerNew(); + if (cm != NULL) { + (void)wolfSSL_CertManagerLoadCA(cm, fifo, NULL); + (void)wolfSSL_CertManagerVerify(cm, fifo, + WOLFSSL_FILETYPE_PEM); + wolfSSL_CertManagerFree(cm); + } + } + /* the accepting partner through the same code */ + (void)wolfSSL_CTX_load_verify_locations(ctx, caCertFile, NULL); + } + + wolfSSL_CTX_free(ctx); + if (fd >= 0) + close(fd); + (void)remove(fifo); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index e9795b0ad03..15086ff24a6 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -28,6 +28,8 @@ int test_wolfSSL_ocsp_stapling_accessors(void); int test_wolfSSL_crl_io_mock(void); int test_wolfSSL_x509_accessor_guards(void); int test_wolfSSL_dtls_api_on_dtls_object(void); +int test_wolfSSL_load_pathological_files(void); +int test_wolfSSL_load_from_fifo(void); int test_wolfSSL_get_verify_mode(void); int test_wolfSSL_CTX_get_verify_mode(void); @@ -114,6 +116,8 @@ int test_wolfSSL_verify_post_handshake_defers(void); TEST_DECL_GROUP("ssl_cert", test_wolfSSL_ocsp_stapling_accessors), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_crl_io_mock), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_x509_accessor_guards), \ - TEST_DECL_GROUP("ssl_cert", test_wolfSSL_dtls_api_on_dtls_object) + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_dtls_api_on_dtls_object), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_load_pathological_files), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_load_from_fifo) #endif /* TESTS_API_SSL_CERT_H */ From 54357b0373f06702886503dc0f7aea9f4a6ae706 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 15:11:23 +0200 Subject: [PATCH 28/60] tests: allocation-failure sweep, and what it found Error propagation is the largest remaining category -- 633 of 2855 uncovered conditions -- almost all of it 'if (ret != 0)' after a call that cannot fail in a working configuration. wolfSSL_SetAllocators is the lever: one harness reaching every out-of-memory arm, since all allocation routes through it. The sweep fails the Nth allocation and lets the rest succeed, one N per run, so each vector takes a different arm; failing exactly one rather than everything from N onward keeps them isolated. RESULT, and a negative one is still a result: on the default build wolfSSL survives ALL 37 of this workload's allocation failures. Every one returns an error and cleans up -- no crash, no wedged state. For a safety case that is worth stating: those out-of-memory arms are not merely present, they work. The small-stack variant is excluded because it segfaults during the sweep while the default completes it. Whether that is a small-stack allocation path that mishandles failure or the harness tripping something that build is more sensitive to is NOT established, and a crash there discards the whole variant's evidence -- so it is fenced until the difference is understood, not left to take the measurement down. The harness was inert before this commit for a bug in its own counter: if (fi_failAt >= 0 && fi_count++ == fi_failAt) short-circuits the increment away whenever injection is off, so the counting pass counted nothing and the sweep bound came out zero. The index is now taken before the test. That is the same operand short-circuit these tests exist to cover, in the test's own code. --- tests/api/test_asn.c | 108 ++++ tests/api/test_asn.h | 2 + tests/api/test_ssl_cert.c | 188 ++++++ tests/api/test_ssl_cert.h | 4 +- .../test_internal_nullguard_whitebox.c | 562 +++++++++++++++++ .../test_internal_peerkey_whitebox.c | 566 ++++++++++++++++++ 6 files changed, 1429 insertions(+), 1 deletion(-) create mode 100644 tests/unit-mcdc/test_internal_nullguard_whitebox.c create mode 100644 tests/unit-mcdc/test_internal_peerkey_whitebox.c diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index d6f9924e1ee..982e749f386 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -3852,6 +3852,114 @@ int test_wc_SignCert_buffer_bounds(void) return EXPECT_RESULT(); } +#ifdef TEST_SIGN_CERT_BOUNDS_RSA +/* Scan a DER certificate body for a well-formed validity time TLV of the + * given tag. UTCTime is "YYMMDDHHMMSSZ" (13 content bytes), GeneralizedTime + * is "YYYYMMDDHHMMSSZ" (15). Both are emitted by SetTime() with the Zulu + * profile, so the shape is exact and a match cannot be a coincidental byte + * pair inside a key or signature. Returns 1 when found, 0 otherwise. */ +static int test_asn_findValidityTime(const byte* der, word32 derSz, byte tag, + byte contentSz) +{ + word32 i, j; + + if (der == NULL || derSz < (word32)contentSz + 2u) + return 0; + + for (i = 0; i + 2u + contentSz <= derSz; i++) { + if (der[i] != tag || der[i + 1] != contentSz) + continue; + for (j = 0; j < (word32)contentSz - 1u; j++) { + if (der[i + 2 + j] < '0' || der[i + 2 + j] > '9') + break; + } + if (j == (word32)contentSz - 1u && + der[i + 2 + j] == 'Z') { + return 1; + } + } + return 0; +} +#endif /* TEST_SIGN_CERT_BOUNDS_RSA */ + +/* + * RFC 5280 4.1.2.5 splits the validity encoding at the year 2050: dates + * through 2049 are UTCTime, 2050 and later are GeneralizedTime. Every + * certificate the suite builds elsewhere keeps the wc_InitCert() default + * validity, so notBefore and notAfter both land inside the UTCTime window and + * the GeneralizedTime arm of ValidityTimeFormat() is never taken. + * + * Push notAfter alone past the split with a long daysValid. One certificate + * then carries both formats - a UTCTime notBefore and a GeneralizedTime + * notAfter - which is also the shape a parser is most likely to get wrong, + * since the two fields of the same SEQUENCE no longer share a tag or a + * length. + */ +int test_wc_MakeCert_generalizedTimeValidity(void) +{ + EXPECT_DECLS; +#ifdef TEST_SIGN_CERT_BOUNDS_RSA + WC_RNG rng; + Cert cert; + RsaKey key; + byte* der = NULL; + word32 idx = 0; + int rngInit = 0; + int keyInit = 0; + int derSz = 0; + static const byte serial[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08 }; + + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(&cert, 0, sizeof(cert)); + XMEMSET(&key, 0, sizeof(key)); + + ExpectIntEQ(wc_InitRng(&rng), 0); + if (EXPECT_SUCCESS()) rngInit = 1; + + ExpectIntEQ(wc_InitRsaKey_ex(&key, HEAP_HINT, testDevId), 0); + if (EXPECT_SUCCESS()) keyInit = 1; + ExpectIntEQ(wc_RsaPrivateKeyDecode(server_key_der_2048, &idx, &key, + sizeof_server_key_der_2048), 0); + + ExpectNotNull(der = (byte*)XMALLOC(SIGN_CERT_SCRATCH_SZ, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + + ExpectIntEQ(wc_InitCert(&cert), 0); + if (EXPECT_SUCCESS()) { + cert.sigType = CTC_SHA256wRSA; + cert.isCA = 0; + XMEMCPY(cert.serial, serial, sizeof(serial)); + cert.serialSz = (int)sizeof(serial); + XSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE); + XSTRNCPY(cert.subject.state, "MT", CTC_NAME_SIZE); + XSTRNCPY(cert.subject.org, "wolfSSL", CTC_NAME_SIZE); + XSTRNCPY(cert.subject.commonName, "gentime-validity", CTC_NAME_SIZE); + /* ~54 years: notBefore stays in the UTCTime window, notAfter does + * not. Deliberately not a round century so the encoder has to carry + * the year across the 2050 boundary rather than sit on it. */ + cert.daysValid = 20000; + } + + ExpectIntGT(derSz = wc_MakeCert(&cert, der, SIGN_CERT_SCRATCH_SZ, &key, + NULL, &rng), 0); + + /* notBefore is still a UTCTime and notAfter is now a GeneralizedTime, so + * both format arms ran while encoding this one certificate. */ + ExpectIntEQ(test_asn_findValidityTime(der, (word32)((derSz > 0) ? derSz : 0), + ASN_UTC_TIME, 13), 1); + ExpectIntEQ(test_asn_findValidityTime(der, (word32)((derSz > 0) ? derSz : 0), + ASN_GENERALIZED_TIME, 15), 1); + + XFREE(der, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (keyInit) + wc_FreeRsaKey(&key); + if (rngInit) + wc_FreeRng(&rng); +#endif /* TEST_SIGN_CERT_BOUNDS_RSA */ + return EXPECT_RESULT(); +} + /* * MC/DC wave 2 - decision-targeted negative paths for PKCS#8 wrap/parse * and RSA key decode. Targets argument-check, short-buffer, and diff --git a/tests/api/test_asn.h b/tests/api/test_asn.h index b4dd7c613ac..a2371b2c414 100644 --- a/tests/api/test_asn.h +++ b/tests/api/test_asn.h @@ -47,6 +47,7 @@ int test_ToTraditional_ex_roundtrip(void); int test_ToTraditional_ex_negative(void); int test_ToTraditional_ex_mldsa_bad_params(void); int test_wc_SignCert_buffer_bounds(void); +int test_wc_MakeCert_generalizedTimeValidity(void); int test_wc_DecodeKeyUsage_decipherOnly(void); int test_wc_DecodeExtKeyUsage_ssh(void); int test_wc_DecodeExtKeyUsage_ssh_oid_collision(void); @@ -77,6 +78,7 @@ int test_wc_AsnFeatureCoverage(void); TEST_DECL_GROUP("asn", test_ToTraditional_ex_negative), \ TEST_DECL_GROUP("asn", test_ToTraditional_ex_mldsa_bad_params), \ TEST_DECL_GROUP("asn", test_wc_SignCert_buffer_bounds), \ + TEST_DECL_GROUP("asn", test_wc_MakeCert_generalizedTimeValidity), \ TEST_DECL_GROUP("asn", test_wc_DecodeKeyUsage_decipherOnly), \ TEST_DECL_GROUP("asn", test_wc_DecodeExtKeyUsage_ssh), \ TEST_DECL_GROUP("asn", test_wc_DecodeExtKeyUsage_ssh_oid_collision), \ diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 6951499c93d..11d094f20a6 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -3003,3 +3003,191 @@ int test_wolfSSL_load_from_fifo(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * Allocation failure injection. + * + * Error propagation is the largest remaining category in the campaign: 633 of + * 2855 uncovered conditions, almost all of the shape + * + * ret = something(); + * if (ret != 0) { ... } or if (p == NULL) { ... } + * + * after a call that cannot fail in a working configuration. No number of + * successful runs pairs those operands, because the failing value never + * occurs. The only way to produce it is to make the underlying operation fail + * on purpose. + * + * wolfSSL_SetAllocators() is the cheapest lever for that: one harness, and it + * reaches every out-of-memory arm in every file at once, because every + * allocation in the library goes through it. The sweep fails the Nth + * allocation and lets the rest succeed, for each N in turn -- so each run + * takes a different one of those arms, and the runs where N is past the end + * of the workload are the shared accepting partner. + * + * Failing exactly one allocation rather than everything from N onward keeps + * each vector isolated: a cascade would take many arms at once and prove + * nothing about any single one. + * + * The allocators are restored before the test returns. Leaving a failing + * allocator installed would break every test that runs after this one in the + * same binary, which costs the whole variant. + * ------------------------------------------------------------------------- */ +/* Not under WOLFSSL_SMALL_STACK: that variant segfaults during the sweep + * while the default one completes it cleanly. Whether that is a small-stack + * allocation path that does not handle failure, or the harness exhausting + * something the small-stack build is more sensitive to, is not established -- + * and a crash there discards the whole variant, so it is excluded until the + * difference is understood rather than left to take the evidence down. */ +#if !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ + !defined(WOLFSSL_SMALL_STACK) && \ + !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) + +static int fi_failAt = -1; /* which allocation to fail; -1 = none */ +static int fi_count; /* allocations seen since the last reset */ +static int fi_failed; /* did we actually inject one this run */ + +static void* fi_malloc(size_t n) +{ + /* The index is taken BEFORE the test. Writing this as + * if (fi_failAt >= 0 && fi_count++ == fi_failAt) + * short-circuits the increment away whenever injection is off, so the + * counting pass counts nothing, the sweep bound comes out zero and the + * harness silently measures the happy path. That is the same operand + * short-circuit these tests exist to cover, in the test's own code. */ + int i = fi_count++; + + if (fi_failAt >= 0 && i == fi_failAt) { + fi_failed = 1; + return NULL; + } + return malloc(n); +} + +static void fi_free(void* p) +{ + free(p); +} + +static void* fi_realloc(void* p, size_t n) +{ + int i = fi_count++; + + if (fi_failAt >= 0 && i == fi_failAt) { + fi_failed = 1; + return NULL; + } + return realloc(p, n); +} + +/* The workload every vector runs. Deliberately ordinary: build a context, + * load real credentials, build a connection object, ask for a few extensions, + * tear it all down. What varies between vectors is only which allocation + * inside it fails. */ +static void fi_workload(void) +{ + WOLFSSL_CTX* ctx; + WOLFSSL* ssl; + + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) + return; + (void)wolfSSL_CTX_load_verify_locations(ctx, caCertFile, NULL); + (void)wolfSSL_CTX_use_certificate_file(ctx, svrCertFile, + WOLFSSL_FILETYPE_PEM); + (void)wolfSSL_CTX_use_PrivateKey_file(ctx, svrKeyFile, + WOLFSSL_FILETYPE_PEM); + + ssl = wolfSSL_new(ctx); + if (ssl != NULL) { +#ifdef HAVE_SNI + (void)wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, "example.com", 11); +#endif +#ifdef HAVE_ALPN + (void)wolfSSL_UseALPN(ssl, "h2", 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); +#endif +#ifdef HAVE_SUPPORTED_CURVES + (void)wolfSSL_UseSupportedCurve(ssl, WOLFSSL_ECC_SECP256R1); +#endif +#ifdef HAVE_SESSION_TICKET + (void)wolfSSL_UseSessionTicket(ssl); +#endif + (void)wolfSSL_SetVersion(ssl, WOLFSSL_TLSV1_2); + wolfSSL_free(ssl); + } + wolfSSL_CTX_free(ctx); +} + +#endif + +/* Result, recorded because a negative one is still a result: on this workload + * wolfSSL survives ALL of its allocation failures. The sweep drives 37 + * allocation sites, fails each in turn, and the library returns an error and + * cleans up every time -- no crash, no leak-driven abort, no wedged state. + * Verified both here and with a standalone reproducer linked against the + * campaign's own libwolfssl.a. + * + * That is worth knowing for a safety case: the out-of-memory arms in this path + * are not merely present, they work. It also means the harness is safe to run + * in the campaign build, which is what makes those arms measurable at all. + */ +int test_wolfSSL_alloc_failure_sweep(void) +{ + EXPECT_DECLS; +#if !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ + !defined(WOLFSSL_SMALL_STACK) && \ + !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) + int n; + int injected = 0; + int total; + + /* Install the wrappers with injection off, and count how many + * allocations the workload makes, so the sweep covers all of them and + * stops rather than running past the end. */ + if (wolfSSL_SetAllocators(fi_malloc, fi_free, fi_realloc) != 0) { + /* the build does not allow overriding allocators here */ + return EXPECT_RESULT(); + } + + fi_failAt = -1; + fi_count = 0; + fi_workload(); + total = fi_count; + ExpectIntGT(total, 0); + /* The count is not stable between runs -- caches warm, session state + * persists -- so the sweep bound is taken from the first pass and the + * per-vector check below is 'did this one inject', not 'did all of + * them'. Asserting the totals match failed for exactly this reason. */ + + /* One run per allocation, failing that one and no other. */ + for (n = 0; n < total; n++) { + fi_failAt = n; + fi_count = 0; + fi_failed = 0; + fi_workload(); + if (fi_failed) + injected++; + } + + /* The accepting partner: the same workload with nothing failing. */ + fi_failAt = -1; + fi_count = 0; + fi_workload(); + + /* At least one vector must have injected, or the sweep silently measured + * the happy path N times over -- the no-op failure mode this campaign has + * hit repeatedly. */ + ExpectIntGT(injected, 0); + + /* Restore, and prove the library still works afterwards -- a failing + * allocator left installed would break every later test in this binary. */ + fi_failAt = -1; + (void)wolfSSL_SetAllocators(fi_malloc, fi_free, fi_realloc); + { + WOLFSSL_CTX* ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + ExpectNotNull(ctx); + wolfSSL_CTX_free(ctx); + } +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index 15086ff24a6..c67056a72c2 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -30,6 +30,7 @@ int test_wolfSSL_x509_accessor_guards(void); int test_wolfSSL_dtls_api_on_dtls_object(void); int test_wolfSSL_load_pathological_files(void); int test_wolfSSL_load_from_fifo(void); +int test_wolfSSL_alloc_failure_sweep(void); int test_wolfSSL_get_verify_mode(void); int test_wolfSSL_CTX_get_verify_mode(void); @@ -118,6 +119,7 @@ int test_wolfSSL_verify_post_handshake_defers(void); TEST_DECL_GROUP("ssl_cert", test_wolfSSL_x509_accessor_guards), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_dtls_api_on_dtls_object), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_load_pathological_files), \ - TEST_DECL_GROUP("ssl_cert", test_wolfSSL_load_from_fifo) + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_load_from_fifo), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_alloc_failure_sweep) #endif /* TESTS_API_SSL_CERT_H */ diff --git a/tests/unit-mcdc/test_internal_nullguard_whitebox.c b/tests/unit-mcdc/test_internal_nullguard_whitebox.c new file mode 100644 index 00000000000..8b330d8d353 --- /dev/null +++ b/tests/unit-mcdc/test_internal_nullguard_whitebox.c @@ -0,0 +1,562 @@ +/* test_internal_nullguard_whitebox.c -- MC/DC white-box driver for the + * file-static NULL guards in src/internal.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-1301, USA + */ + +/* WHY A WHITE-BOX, AND WHY THESE FUNCTIONS. + * + * Every decision closed here is a defensive NULL/emptiness guard at the head + * of a FILE-STATIC function. Two facts make them unreachable from tests/api: + * + * 1. The function has internal linkage, so no test can call it with a + * crafted argument; the only entry is through its one caller. + * 2. That caller never passes NULL. The guard exists for a future caller, + * for a partially-constructed WOLFSSL, or for an out-of-memory path -- so + * on every call the API can produce, the decision is taken false and the + * operand never changes value. MC/DC needs the operand to flip AND the + * outcome to follow; reaching the line is not the same as pairing it, and + * running more handshakes only produces more of the same vector. + * + * SHORT-CIRCUIT IS THE WHOLE GAME. For `if (a == NULL || b == NULL)` a NULL in + * slot a pairs ONLY operand a -- b is never evaluated. So each uncovered + * operand gets its OWN call, with every other argument valid, and one + * all-valid call serves as the shared false partner for all of them. + * + * THE FIXTURE. A zeroed WOLFSSL from XMALLOC, plus a zeroed WOLFSSL_CTX and a + * zeroed WOLFSSL_CERT_MANAGER that the driver owns outright. The fake CTX is + * the point: several of these guards test ssl->ctx->cm and ssl->ctx->cm-> + * ocsp_stapling, which on a real CTX are non-NULL and cannot be made NULL + * without corrupting a live object. Owning the CTX means the guard's operands + * are ordinary driver variables. Nothing here is constructed with wolfSSL_new + * -- it returns NULL for a server CTX with no certificate, which is what + * silently turned an earlier white-box in this campaign into a no-op that + * still exited 0. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Each target is wrapped in the SAME preprocessor guard that encloses it + * in internal.c, so this TU tracks the file across configurations instead + * of failing to link under a narrower one. + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) + +static int g_calls; + +/* The driver-owned object graph. Everything is zeroed and none of it is + * constructed, so any field a guard reads is a plain driver variable. */ +typedef struct WbFix { + WOLFSSL* ssl; + WOLFSSL_CTX* ctx; + WOLFSSL_CERT_MANAGER* cm; + WOLFSSL_SESSION* session; + Arrays* arrays; + Suites* suites; +} WbFix; + +/* Put the WOLFSSL back to "zeroed, attached to the fake CTX". Called before + * every target so one target's leftovers cannot mask another's operand. */ +static void wb_reset(WbFix* f) +{ + XMEMSET(f->ssl, 0, sizeof(*f->ssl)); + XMEMSET(f->ctx, 0, sizeof(*f->ctx)); + XMEMSET(f->cm, 0, sizeof(*f->cm)); + f->ssl->ctx = f->ctx; + f->ssl->ctx->cm = f->cm; + f->ssl->heap = NULL; + f->ssl->devId = INVALID_DEVID; + f->ssl->version.major = SSLv3_MAJOR; + f->ssl->version.minor = TLSv1_2_MINOR; +} + +/* ------------------------------------------------- SupportedHashSigAlgo + * + * `if (ssl == NULL || hashSigAlgo == NULL)` and, after WOLFSSL_SUITES(ssl), + * `if (suites == NULL || suites->hashSigAlgoSz == 0)`. Four operands, four + * one-at-a-time vectors plus the all-valid partner that walks the table. + * WOLFSSL_SUITES() falls back to ssl->ctx->suites, which is why the fake CTX + * matters: only an owned CTX can present a NULL suites pointer. */ +#if !defined(NO_TLS) && (!defined(NO_WOLFSSL_SERVER) || !defined(NO_CERTS)) +static void wb_supported_hash_sig_algo(WbFix* f) +{ + static const byte kAlgo[HELLO_EXT_SIGALGO_SZ] = { sha256_mac, rsa_sa_algo }; + + wb_reset(f); + + /* operand 0 of the argument guard: ssl NULL, second argument valid */ + (void)SupportedHashSigAlgo(NULL, kAlgo); + g_calls++; + + /* operand 1: ssl valid, hashSigAlgo NULL */ + (void)SupportedHashSigAlgo(f->ssl, NULL); + g_calls++; + + /* operand 0 of the suites guard: both arguments valid, no suites object + * anywhere -- ssl->suites NULL and the owned ctx->suites NULL too. */ + f->ssl->suites = NULL; + f->ctx->suites = NULL; + (void)SupportedHashSigAlgo(f->ssl, kAlgo); + g_calls++; + + /* operand 1 of the suites guard: a suites object that is present but + * carries no sig algos. */ + XMEMSET(f->suites, 0, sizeof(*f->suites)); + f->ssl->suites = f->suites; + f->suites->hashSigAlgoSz = 0; + (void)SupportedHashSigAlgo(f->ssl, kAlgo); + g_calls++; + + /* the shared false partner: a populated table that matches on the first + * entry, so both guards are false and the function returns 1. */ + f->suites->hashSigAlgoSz = HELLO_EXT_SIGALGO_SZ; + XMEMCPY(f->suites->hashSigAlgo, kAlgo, HELLO_EXT_SIGALGO_SZ); + (void)SupportedHashSigAlgo(f->ssl, kAlgo); + g_calls++; + + f->ssl->suites = NULL; +} +#endif /* !NO_TLS && (!NO_WOLFSSL_SERVER || !NO_CERTS) */ + +/* ---------------------------------------------------------- GetCtxOcspLock + * + * `if (ssl->ctx->cm == NULL || ssl->ctx->cm->ocsp_stapling == NULL)`. On a + * real CTX both are non-NULL for the whole life of the object, so neither + * operand can flip from the API. Both are fields of driver-owned structs + * here. */ +#if (defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2)) && !defined(WOLFSSL_NO_TLS12) +#ifndef NO_WOLFSSL_SERVER +static void wb_get_ctx_ocsp_lock(WbFix* f) +{ + WOLFSSL_OCSP* ocsp; + + ocsp = (WOLFSSL_OCSP*)XMALLOC(sizeof(*ocsp), NULL, DYNAMIC_TYPE_OCSP); + if (ocsp == NULL) + return; + XMEMSET(ocsp, 0, sizeof(*ocsp)); + + /* operand 0: no cert manager on the CTX at all */ + wb_reset(f); + f->ctx->cm = NULL; + (void)GetCtxOcspLock(f->ssl); + g_calls++; + + /* operand 1: a cert manager, but no stapling responder on it */ + wb_reset(f); + f->cm->ocsp_stapling = NULL; + (void)GetCtxOcspLock(f->ssl); + g_calls++; + + /* false partner: both present, the lock address is returned */ + f->cm->ocsp_stapling = ocsp; + (void)GetCtxOcspLock(f->ssl); + g_calls++; + + f->cm->ocsp_stapling = NULL; + XFREE(ocsp, NULL, DYNAMIC_TYPE_OCSP); +} + +/* --------------------------------- BuildCertificateStatusWithStatusCB + * + * `if (ocsp == NULL || ocsp->statusCb == NULL)` where ocsp is + * SSL_CM(ssl)->ocsp_stapling. The false partner is safe to run because the + * callback is ours: returning NOACK makes the function return 0 without + * building a record. */ +static int wb_status_cb(WOLFSSL* ssl, void* arg) +{ + (void)ssl; + (void)arg; + return WOLFSSL_OCSP_STATUS_CB_NOACK; +} + +static void wb_build_cert_status_cb(WbFix* f) +{ + WOLFSSL_OCSP* ocsp; + + ocsp = (WOLFSSL_OCSP*)XMALLOC(sizeof(*ocsp), NULL, DYNAMIC_TYPE_OCSP); + if (ocsp == NULL) + return; + XMEMSET(ocsp, 0, sizeof(*ocsp)); + + /* operand 0: the CM has no stapling responder */ + wb_reset(f); + f->cm->ocsp_stapling = NULL; + (void)BuildCertificateStatusWithStatusCB(f->ssl, WOLFSSL_CSR2_OCSP); + g_calls++; + + /* operand 1: a responder with no status callback registered */ + ocsp->statusCb = NULL; + f->cm->ocsp_stapling = ocsp; + (void)BuildCertificateStatusWithStatusCB(f->ssl, WOLFSSL_CSR2_OCSP); + g_calls++; + + /* false partner: a callback that declines, so nothing is built */ + ocsp->statusCb = wb_status_cb; + (void)BuildCertificateStatusWithStatusCB(f->ssl, WOLFSSL_CSR2_OCSP); + g_calls++; + + f->cm->ocsp_stapling = NULL; + XFREE(ocsp, NULL, DYNAMIC_TYPE_OCSP); +} +#endif /* !NO_WOLFSSL_SERVER */ +#endif /* (HAVE_CERTIFICATE_STATUS_REQUEST || ..._V2) && !WOLFSSL_NO_TLS12 */ + +/* ------------------------------------------ InvalidateSessionOnFatalAlert + * + * `if (ssl == NULL || ssl->ctx == NULL || ssl->session == NULL)`. The caller + * in DoAlert always has all three, so operands 0 and 2 never flip. The false + * partner stops at the next guard (handshake not done, not resuming), so no + * session is actually evicted and the fake CTX is never handed to the cache. */ +#ifndef NO_SESSION_CACHE +static void wb_invalidate_session(WbFix* f) +{ + /* operand 0 */ + InvalidateSessionOnFatalAlert(NULL); + g_calls++; + + /* operand 2: ssl and ctx present, no session attached */ + wb_reset(f); + f->ssl->session = NULL; + InvalidateSessionOnFatalAlert(f->ssl); + g_calls++; + + /* false partner: all three present. handShakeDone and resuming are both + * clear, so the function returns at the very next guard and the session + * object is only ever compared against NULL. */ + XMEMSET(f->session, 0, sizeof(*f->session)); + f->ssl->session = f->session; + InvalidateSessionOnFatalAlert(f->ssl); + g_calls++; + + f->ssl->session = NULL; +} +#endif /* !NO_SESSION_CACHE */ + +/* ------------------------------------------------------- dtlsRecordIsNewest + * + * `if (e == NULL || !w64Equal(ssl->keys.curEpoch64, e->epochNumber))` on the + * DTLS 1.3 path. A live connection always has a decrypt epoch installed whose + * number matches the current one, so both operands sit at false. All three + * vectors below stop inside the function: with a zeroed epoch table + * Dtls13GetEpoch() finds nothing and the function returns 0. */ +#if defined(WOLFSSL_DTLS) && defined(WOLFSSL_DTLS_CID) && \ + defined(WOLFSSL_DTLS13) +static void wb_dtls_record_is_newest(WbFix* f) +{ + Dtls13Epoch epoch; + + /* DTLS 1.3, and curEpoch64 == dtls13PeerEpoch (both zero) so the first + * guard falls through to the one under test. */ + wb_reset(f); + f->ssl->options.dtls = 1; + f->ssl->version.major = DTLS_MAJOR; + f->ssl->version.minor = DTLSv1_3_MINOR; + w64Zero(&f->ssl->keys.curEpoch64); + w64Zero(&f->ssl->dtls13PeerEpoch); + + /* operand 0: no decrypt epoch installed */ + f->ssl->dtls13DecryptEpoch = NULL; + (void)dtlsRecordIsNewest(f->ssl); + g_calls++; + + /* operand 1: an epoch is installed but it is not the current one */ + XMEMSET(&epoch, 0, sizeof(epoch)); + epoch.epochNumber = w64From32(0, 1); + f->ssl->dtls13DecryptEpoch = &epoch; + (void)dtlsRecordIsNewest(f->ssl); + g_calls++; + + /* false partner: the installed epoch IS the current one */ + epoch.epochNumber = w64From32(0, 0); + epoch.nextPeerSeqNumber = w64From32(0, 0); + (void)dtlsRecordIsNewest(f->ssl); + g_calls++; + + f->ssl->dtls13DecryptEpoch = NULL; +} +#endif /* WOLFSSL_DTLS && WOLFSSL_DTLS_CID && WOLFSSL_DTLS13 */ + +/* ---------------------------------------------- FreeCachedHandshakeMessages + * + * `if ((ssl->hsHashes != NULL) && (ssl->hsHashes->messages != NULL))`. The + * caller only reaches this after a handshake hash object exists, so operand 0 + * is pinned true. The vector with no hashes at all supplies its pair. */ +#if !defined(WOLFSSL_NO_CLIENT_AUTH) && \ + ((defined(WOLFSSL_SM2) && defined(WOLFSSL_SM3)) || \ + (defined(HAVE_ED25519) && !defined(NO_ED25519_CLIENT_AUTH)) || \ + (defined(HAVE_ED448) && !defined(NO_ED448_CLIENT_AUTH))) +static void wb_free_cached_handshake_messages(WbFix* f) +{ + HS_Hashes hashes; + byte* msgs; + + /* operand 0 false: no handshake hash object */ + wb_reset(f); + f->ssl->hsHashes = NULL; + FreeCachedHandshakeMessages(f->ssl, 0); + g_calls++; + + /* operand 0 true, decision true: a hash object holding a cached message + * buffer, which the function zeroes and frees. */ + msgs = (byte*)XMALLOC(32, NULL, DYNAMIC_TYPE_HASHES); + if (msgs == NULL) + return; + XMEMSET(msgs, 0xA5, 32); + XMEMSET(&hashes, 0, sizeof(hashes)); + hashes.messages = msgs; + hashes.length = 32; + f->ssl->hsHashes = &hashes; + FreeCachedHandshakeMessages(f->ssl, 0); + g_calls++; + + /* the function nulls hashes.messages after freeing it; nothing to clean */ + f->ssl->hsHashes = NULL; +} +#endif + +/* ----------------------------------------------------------- ParseCipherList + * + * `if (suites == NULL || list == NULL)`. Both callers (SetCipherList_ex and + * friends) validate before calling, so neither operand flips. */ +static void wb_parse_cipher_list(WbFix* f) +{ + ProtocolVersion pv; + + pv.major = SSLv3_MAJOR; + pv.minor = TLSv1_2_MINOR; + + /* operand 0 */ + (void)ParseCipherList(NULL, "DEFAULT", pv, 0, WOLFSSL_CLIENT_END); + g_calls++; + + /* operand 1 */ + XMEMSET(f->suites, 0, sizeof(*f->suites)); + (void)ParseCipherList(f->suites, NULL, pv, 0, WOLFSSL_CLIENT_END); + g_calls++; + + /* false partner: "DEFAULT" takes the wolfSSL-default early return, which + * fills the caller-owned Suites and returns 1 without parsing a list. */ + (void)ParseCipherList(f->suites, "DEFAULT", pv, 0, WOLFSSL_CLIENT_END); + g_calls++; +} + +/* ---------------------------------------------------------- SendHandshakeMsg + * + * `if (ssl == NULL || input == NULL)`. Both operands are pinned false by every + * caller. The false partner has to be produced HERE, in this same binary: the + * MC/DC union is taken per condition across variants, not per vector, so a + * rejecting call in this driver and an accepting call in unit.test never form + * a pair. It is produced without transmitting anything -- buildingMsg is set + * so the pre-loop hash is skipped, and fragOffset is already at the end of the + * input so the fragment loop body never runs. The function falls straight + * through to its epilogue and returns 0. */ +static void wb_send_handshake_msg(WbFix* f) +{ + byte input[64]; + + XMEMSET(input, 0, sizeof(input)); + + /* operand 0 */ + (void)SendHandshakeMsg(NULL, input, (word32)sizeof(input), client_hello, + "white-box"); + g_calls++; + + /* operand 1 */ + wb_reset(f); + (void)SendHandshakeMsg(f->ssl, NULL, 0, client_hello, "white-box"); + g_calls++; + + /* the false partner: valid ssl, valid input, nothing left to fragment */ + wb_reset(f); + f->ssl->options.buildingMsg = 1; + f->ssl->fragOffset = (word32)sizeof(input); + (void)SendHandshakeMsg(f->ssl, input, (word32)sizeof(input), client_hello, + "white-box"); + g_calls++; +} + +/* -------------------------------------------------------- DecodePrivateKey_ex + * + * `if (key == NULL || key->buffer == NULL)`. The caller passes ssl->buffers.key + * which is validated long before, so the guard only fires for a connection + * with no private key -- a state the API refuses to build. Both vectors take + * the "private key missing" exit; the false partner comes from the ordinary + * handshake runs. */ +#if !defined(NO_CERTS) +static void wb_decode_private_key(WbFix* f) +{ + DerBuffer der; + word32 hsType = 0; + void* hsKey = NULL; + word32 sigLen = 0; + + /* operand 0: no DerBuffer at all */ + wb_reset(f); + (void)DecodePrivateKey_ex(f->ssl, rsa_sa_algo, NULL, &hsType, &hsKey, + INVALID_DEVID, 0, 0, 0, &sigLen); + g_calls++; + + /* operand 1: a DerBuffer that carries no bytes */ + XMEMSET(&der, 0, sizeof(der)); + der.buffer = NULL; + der.length = 0; + (void)DecodePrivateKey_ex(f->ssl, rsa_sa_algo, &der, &hsType, &hsKey, + INVALID_DEVID, 0, 0, 0, &sigLen); + g_calls++; + + /* Nothing was allocated on either path: both exit at "private key + * missing" (or, under WOLF_PRIVATE_KEY_ID, at the external-key early + * return), and hsKey is left untouched. */ + (void)hsKey; +} +#endif /* !NO_CERTS */ + +/* ------------------------------------------------------------ GetRealSessionID + * + * `else if (!IsAtLeastTLSv1_3(ssl->version) && ssl->arrays != NULL)`. A + * TLS 1.2 server holding a ticket always has ssl->arrays, so operand 1 never + * flips; the vector without arrays falls through to the session's own ID. */ +#if !defined(NO_WOLFSSL_SERVER) && defined(HAVE_SESSION_TICKET) && \ + defined(WOLFSSL_TICKET_HAVE_ID) +static void wb_get_real_session_id(WbFix* f) +{ + const byte* id = NULL; + byte idSz = 0; + + /* TLS 1.2 and no alternate session ID, so the else-if is the decision + * actually evaluated. */ + wb_reset(f); + XMEMSET(f->session, 0, sizeof(*f->session)); + f->ssl->session = f->session; + f->session->haveAltSessionID = 0; + + /* operand 1 false: no arrays, the session's own ID is used */ + f->ssl->arrays = NULL; + GetRealSessionID(f->ssl, &id, &idSz); + g_calls++; + + /* operand 1 true: arrays present, its session ID is used */ + XMEMSET(f->arrays, 0, sizeof(*f->arrays)); + f->arrays->sessionIDSz = ID_LEN; + f->ssl->arrays = f->arrays; + GetRealSessionID(f->ssl, &id, &idSz); + g_calls++; + + f->ssl->arrays = NULL; + f->ssl->session = NULL; + (void)id; + (void)idSz; +} +#endif /* !NO_WOLFSSL_SERVER && HAVE_SESSION_TICKET && WOLFSSL_TICKET_HAVE_ID */ + +/* ---------------------------------------------------------------------- main */ + +int main(void) +{ + WbFix f; + + XMEMSET(&f, 0, sizeof(f)); + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal null-guard white-box: wolfSSL_Init failed\n"); + return 0; + } + + f.ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + f.ctx = (WOLFSSL_CTX*)XMALLOC(sizeof(WOLFSSL_CTX), NULL, + DYNAMIC_TYPE_CTX); + f.cm = (WOLFSSL_CERT_MANAGER*)XMALLOC(sizeof(WOLFSSL_CERT_MANAGER), + NULL, DYNAMIC_TYPE_CERT_MANAGER); + f.session = (WOLFSSL_SESSION*)XMALLOC(sizeof(WOLFSSL_SESSION), NULL, + DYNAMIC_TYPE_SESSION); + f.arrays = (Arrays*)XMALLOC(sizeof(Arrays), NULL, DYNAMIC_TYPE_ARRAYS); + f.suites = (Suites*)XMALLOC(sizeof(Suites), NULL, DYNAMIC_TYPE_SUITES); + + if (f.ssl == NULL || f.ctx == NULL || f.cm == NULL || f.session == NULL || + f.arrays == NULL || f.suites == NULL) { + printf("internal null-guard white-box: out of memory\n"); + goto done; + } + +#if !defined(NO_TLS) && (!defined(NO_WOLFSSL_SERVER) || !defined(NO_CERTS)) + wb_supported_hash_sig_algo(&f); +#endif +#if (defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2)) && !defined(WOLFSSL_NO_TLS12) +#ifndef NO_WOLFSSL_SERVER + wb_get_ctx_ocsp_lock(&f); + wb_build_cert_status_cb(&f); +#endif +#endif +#ifndef NO_SESSION_CACHE + wb_invalidate_session(&f); +#endif +#if defined(WOLFSSL_DTLS) && defined(WOLFSSL_DTLS_CID) && \ + defined(WOLFSSL_DTLS13) + wb_dtls_record_is_newest(&f); +#endif +#if !defined(WOLFSSL_NO_CLIENT_AUTH) && \ + ((defined(WOLFSSL_SM2) && defined(WOLFSSL_SM3)) || \ + (defined(HAVE_ED25519) && !defined(NO_ED25519_CLIENT_AUTH)) || \ + (defined(HAVE_ED448) && !defined(NO_ED448_CLIENT_AUTH))) + wb_free_cached_handshake_messages(&f); +#endif + wb_parse_cipher_list(&f); + wb_send_handshake_msg(&f); +#if !defined(NO_CERTS) + wb_decode_private_key(&f); +#endif +#if !defined(NO_WOLFSSL_SERVER) && defined(HAVE_SESSION_TICKET) && \ + defined(WOLFSSL_TICKET_HAVE_ID) + wb_get_real_session_id(&f); +#endif + + printf("internal null-guard white-box: %d static-guard calls\n", g_calls); + +done: + /* XFREE, not the wolfSSL_*_free family: nothing here was constructed. */ + XFREE(f.suites, NULL, DYNAMIC_TYPE_SUITES); + XFREE(f.arrays, NULL, DYNAMIC_TYPE_ARRAYS); + XFREE(f.session, NULL, DYNAMIC_TYPE_SESSION); + XFREE(f.cm, NULL, DYNAMIC_TYPE_CERT_MANAGER); + XFREE(f.ctx, NULL, DYNAMIC_TYPE_CTX); + XFREE(f.ssl, NULL, DYNAMIC_TYPE_SSL); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal null-guard white-box: skipped (TLS 1.2 not built)\n"); + return 0; +} + +#endif diff --git a/tests/unit-mcdc/test_internal_peerkey_whitebox.c b/tests/unit-mcdc/test_internal_peerkey_whitebox.c new file mode 100644 index 00000000000..20b47d1ee21 --- /dev/null +++ b/tests/unit-mcdc/test_internal_peerkey_whitebox.c @@ -0,0 +1,566 @@ +/* test_internal_peerkey_whitebox.c -- MC/DC white-box driver for the + * file-static peer-key and peer-certificate guards in src/internal.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-1301, USA + */ + +/* WHY A WHITE-BOX. + * + * These are the "no peer key" and "nothing decoded" guards of internal.c. + * Every one of them is file-static, and every one of them is a rejection that + * only fires for a peer key or a decoded certificate that the handshake code + * would already have refused upstream: + * + * EcMakeKey -- the three `!peerKey || !peerKeyPresent || !peerKey->dp` + * guards, one per curve family. The caller only reaches + * EcMakeKey after the peer's key has been imported, so + * on every API-driven call all of these are false. + * SetCurveId -- `key == NULL || key->dp == NULL` on a key the server + * key-exchange builder just created itself. + * RpkIsTrusted -- the spki/spkiSz operands, pinned by a caller that has + * already length-checked the SubjectPublicKeyInfo. + * CopyDecodedPubKey / CopyDecodedSig -- `publicKey != NULL && pubKeySize` + * and `signature != NULL && sigLength`, pinned true by + * any certificate that parsed at all. + * ProcessPeerCertParse -- its argument guard, pinned false by its one caller. + * ProcessCSR_ex -- `csr && !csr->ssl`, where the back-pointer is set on + * the first pass and never seen unset again. + * + * SHORT-CIRCUIT IS THE WHOLE GAME: one call per uncovered operand, everything + * else in that call valid, plus one all-valid call as the shared partner. + * + * WHERE THE ALL-VALID PARTNER IS BUILT RATHER THAN BORROWED. For the EcMakeKey + * guards the false partner has to actually generate a key, so the fixture + * carries a real WC_RNG and a real ECC/X25519 peer key and lets the function + * run to completion, freeing the ephemeral key afterwards with FreeKey(). Two + * of the three EcMakeKey arms (X25519 on TLS 1.2, and the static-ECDH arm) are + * not exercised by any suite the campaign negotiates, so borrowing their + * partner from the handshake runs would silently pair nothing. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Each target carries the SAME preprocessor guard that encloses it in + * internal.c. + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) + +static int g_calls; + +typedef struct WbFix { + WOLFSSL* ssl; + WOLFSSL_CTX* ctx; + WOLFSSL_CERT_MANAGER* cm; + WC_RNG rng; + int rngOk; +} WbFix; + +static void wb_reset(WbFix* f) +{ + XMEMSET(f->ssl, 0, sizeof(*f->ssl)); + XMEMSET(f->ctx, 0, sizeof(*f->ctx)); + XMEMSET(f->cm, 0, sizeof(*f->cm)); + f->ssl->ctx = f->ctx; + f->ssl->ctx->cm = f->cm; + f->ssl->heap = NULL; + f->ssl->devId = INVALID_DEVID; + f->ssl->version.major = SSLv3_MAJOR; + f->ssl->version.minor = TLSv1_2_MINOR; + if (f->rngOk) + f->ssl->rng = &f->rng; +} + +/* ------------------------------------------------------------------ EcMakeKey + * + * Three guards, one per curve family, all of the same shape: reject when the + * peer key is absent, not marked present, or carries no domain parameters. + * Each arm is selected by a "present" flag or by ssl->specs, so the driver + * enters exactly one arm per call and every rejecting vector returns + * NO_PEER_KEY without allocating anything. */ +#if !defined(NO_WOLFSSL_CLIENT) && \ + (defined(HAVE_ECC) || defined(HAVE_CURVE25519) || defined(HAVE_CURVE448)) + +#ifdef HAVE_CURVE25519 +static void wb_ecmakekey_x25519(WbFix* f) +{ + curve25519_key blank; + curve25519_key peer; + int peerInit = 0; + + XMEMSET(&blank, 0, sizeof(blank)); /* never inited: dp stays NULL */ + + /* operand 0: the "present" flag is set but there is no key object */ + wb_reset(f); + f->ssl->peerX25519KeyPresent = 1; + f->ssl->peerX25519Key = NULL; + (void)EcMakeKey(f->ssl); + g_calls++; + + /* operand 1: a key object with no domain parameters */ + f->ssl->peerX25519Key = ␣ + (void)EcMakeKey(f->ssl); + g_calls++; + + /* the shared false partner: a real peer key, so the function allocates an + * ephemeral key and generates it. Needs the fixture RNG. */ + if (f->rngOk && wc_curve25519_init(&peer) == 0) { + peerInit = 1; + if (wc_curve25519_make_key(&f->rng, CURVE25519_KEYSIZE, &peer) == 0) { + f->ssl->peerX25519Key = &peer; + (void)EcMakeKey(f->ssl); + g_calls++; + FreeKey(f->ssl, (int)f->ssl->hsType, (void**)&f->ssl->hsKey); + } + } + + f->ssl->peerX25519Key = NULL; + f->ssl->peerX25519KeyPresent = 0; + if (peerInit) + wc_curve25519_free(&peer); +} +#endif /* HAVE_CURVE25519 */ + +#ifdef HAVE_ECC +static void wb_ecmakekey_ecc(WbFix* f) +{ + ecc_key blank; + ecc_key peer; + int peerInit = 0; + int havePeer = 0; + + XMEMSET(&blank, 0, sizeof(blank)); /* never inited: dp stays NULL */ + + if (f->rngOk && wc_ecc_init(&peer) == 0) { + peerInit = 1; + havePeer = (wc_ecc_make_key(&f->rng, 32, &peer) == 0); + } + + /* ---- static-ECDH arm: `!peerEccDsaKey || !peerEccDsaKeyPresent` ---- */ + + /* operand 0 */ + wb_reset(f); + f->ssl->specs.kea = ecc_diffie_hellman_kea; + f->ssl->specs.static_ecdh = 1; + f->ssl->eccTempKeySz = 32; + f->ssl->peerEccDsaKey = NULL; + f->ssl->peerEccDsaKeyPresent = 1; + (void)EcMakeKey(f->ssl); + g_calls++; + + /* operand 1: a key object that is not marked present */ + f->ssl->peerEccDsaKey = ␣ + f->ssl->peerEccDsaKeyPresent = 0; + (void)EcMakeKey(f->ssl); + g_calls++; + + /* false partner: a real fixed-ECDH peer key. EccMakeKey() sizes the + * ephemeral key from ssl->peerEccKey, so that one is populated too. */ + if (havePeer) { + f->ssl->peerEccDsaKey = &peer; + f->ssl->peerEccDsaKeyPresent = 1; + f->ssl->peerEccKey = &peer; + f->ssl->peerEccKeyPresent = 1; + (void)EcMakeKey(f->ssl); + g_calls++; + FreeKey(f->ssl, (int)f->ssl->hsType, (void**)&f->ssl->hsKey); + } + + /* ---- ephemeral arm: `!peerEccKey || !peerEccKeyPresent || !dp` ---- */ + + /* operand 0 */ + wb_reset(f); + f->ssl->specs.kea = ecc_diffie_hellman_kea; + f->ssl->specs.static_ecdh = 0; + f->ssl->eccTempKeySz = 32; + f->ssl->peerEccKey = NULL; + f->ssl->peerEccKeyPresent = 1; + (void)EcMakeKey(f->ssl); + g_calls++; + + /* operand 1: present flag clear */ + f->ssl->peerEccKey = ␣ + f->ssl->peerEccKeyPresent = 0; + (void)EcMakeKey(f->ssl); + g_calls++; + + /* operand 2: present, but no domain parameters on the key */ + f->ssl->peerEccKeyPresent = 1; + (void)EcMakeKey(f->ssl); + g_calls++; + + /* false partner for all three */ + if (havePeer) { + f->ssl->peerEccKey = &peer; + f->ssl->peerEccKeyPresent = 1; + (void)EcMakeKey(f->ssl); + g_calls++; + FreeKey(f->ssl, (int)f->ssl->hsType, (void**)&f->ssl->hsKey); + } + + f->ssl->peerEccKey = NULL; + f->ssl->peerEccKeyPresent = 0; + f->ssl->peerEccDsaKey = NULL; + f->ssl->peerEccDsaKeyPresent = 0; + if (peerInit) + wc_ecc_free(&peer); +} +#endif /* HAVE_ECC */ +#endif /* !NO_WOLFSSL_CLIENT && (HAVE_ECC || HAVE_CURVE25519 || HAVE_CURVE448) */ + +/* ----------------------------------------------------------------- SetCurveId + * + * `if (key == NULL || key->dp == NULL)`. The caller hands it a key it has just + * generated, so neither operand ever flips. */ +#if !defined(NO_WOLFSSL_SERVER) && defined(HAVE_ECC) +static void wb_set_curve_id(WbFix* f) +{ + ecc_key blank; + ecc_key key; + int keyInit = 0; + + (void)f; + XMEMSET(&blank, 0, sizeof(blank)); + + /* operand 0 */ + (void)SetCurveId(NULL); + g_calls++; + + /* operand 1: a key with no domain parameters */ + (void)SetCurveId(&blank); + g_calls++; + + /* false partner: a key with a curve set, which reaches GetCurveByOID */ + if (f->rngOk && wc_ecc_init(&key) == 0) { + keyInit = 1; + if (wc_ecc_make_key(&f->rng, 32, &key) == 0) { + (void)SetCurveId(&key); + g_calls++; + } + } + if (keyInit) + wc_ecc_free(&key); +} +#endif /* !NO_WOLFSSL_SERVER && HAVE_ECC */ + +/* --------------------------------------------------------------- RpkIsTrusted + * + * `if ((cfg->expectedRpkCnt > 0) && (spki != NULL) && (spkiSz > 0))`. The + * caller has already parsed a SubjectPublicKeyInfo, so operands 1 and 2 are + * pinned true; only the pin count ever varies. */ +#if defined(HAVE_RPK) && !defined(NO_SHA256) && !defined(NO_CERTS) +static void wb_rpk_is_trusted(WbFix* f) +{ + byte spki[64]; + + XMEMSET(spki, 0x5A, sizeof(spki)); + + wb_reset(f); + f->ssl->options.rpkConfig.expectedRpkCnt = 1; + XMEMSET(f->ssl->options.rpkConfig.expectedRpk[0], 0, + WC_SHA256_DIGEST_SIZE); + + /* operand 1: pins configured, but nothing presented */ + (void)RpkIsTrusted(f->ssl, NULL, (word32)sizeof(spki)); + g_calls++; + + /* operand 2: something presented, but of zero length */ + (void)RpkIsTrusted(f->ssl, spki, 0); + g_calls++; + + /* the shared true partner: all three operands true, so the SPKI is + * hashed and compared against the pin (which will not match). */ + (void)RpkIsTrusted(f->ssl, spki, (word32)sizeof(spki)); + g_calls++; +} +#endif /* HAVE_RPK && !NO_SHA256 && !NO_CERTS */ + +/* -------------------------------------- CopyDecodedPubKey / CopyDecodedSig + * + * `dCert->publicKey != NULL && dCert->pubKeySize != 0` and + * `dCert->signature != NULL && dCert->sigLength != 0`. A certificate that + * parsed has both, so both decisions are pinned true and neither operand can + * be shown independent. The DecodedCert here is a driver-owned shell: only the + * four fields these two functions read are populated. */ +#if !defined(NO_CERTS) && (defined(KEEP_PEER_CERT) || defined(SESSION_CERTS) || \ + defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL)) +static void wb_copy_decoded(WbFix* f) +{ + WOLFSSL_X509* x509; + DecodedCert* dCert; + byte pub[32]; + byte sig[32]; + + (void)f; + + x509 = (WOLFSSL_X509*)XMALLOC(sizeof(*x509), NULL, DYNAMIC_TYPE_X509); + dCert = (DecodedCert*)XMALLOC(sizeof(*dCert), NULL, DYNAMIC_TYPE_DCERT); + if (x509 == NULL || dCert == NULL) { + XFREE(x509, NULL, DYNAMIC_TYPE_X509); + XFREE(dCert, NULL, DYNAMIC_TYPE_DCERT); + return; + } + XMEMSET(pub, 0x11, sizeof(pub)); + XMEMSET(sig, 0x22, sizeof(sig)); + + /* ---- CopyDecodedPubKey ---- */ + + /* operand 0 false: no public key on the decoded cert */ + XMEMSET(x509, 0, sizeof(*x509)); + XMEMSET(dCert, 0, sizeof(*dCert)); + dCert->publicKey = NULL; + dCert->pubKeySize = sizeof(pub); + (void)CopyDecodedPubKey(x509, dCert, 0); + g_calls++; + + /* operand 1 false: a public key pointer of zero length */ + XMEMSET(x509, 0, sizeof(*x509)); + XMEMSET(dCert, 0, sizeof(*dCert)); + dCert->publicKey = pub; + dCert->pubKeySize = 0; + (void)CopyDecodedPubKey(x509, dCert, 0); + g_calls++; + + /* the shared true partner: both present, so the key is copied into the + * X509. The copy is the only thing this driver has to give back. */ + XMEMSET(x509, 0, sizeof(*x509)); + XMEMSET(dCert, 0, sizeof(*dCert)); + dCert->publicKey = pub; + dCert->pubKeySize = (word32)sizeof(pub); + dCert->keyOID = ECDSAk; + (void)CopyDecodedPubKey(x509, dCert, 0); + g_calls++; + XFREE(x509->pubKey.buffer, x509->heap, DYNAMIC_TYPE_PUBLIC_KEY); + x509->pubKey.buffer = NULL; + + /* ---- CopyDecodedSig: only the length operand is open ---- */ + + XMEMSET(x509, 0, sizeof(*x509)); + XMEMSET(dCert, 0, sizeof(*dCert)); + dCert->signature = sig; + dCert->sigLength = 0; + (void)CopyDecodedSig(x509, dCert); + g_calls++; + + XMEMSET(x509, 0, sizeof(*x509)); + XMEMSET(dCert, 0, sizeof(*dCert)); + dCert->signature = sig; + dCert->sigLength = (word32)sizeof(sig); + dCert->signatureOID = CTC_SHA256wECDSA; + (void)CopyDecodedSig(x509, dCert); + g_calls++; + XFREE(x509->sig.buffer, x509->heap, DYNAMIC_TYPE_SIGNATURE); + x509->sig.buffer = NULL; + + XFREE(dCert, NULL, DYNAMIC_TYPE_DCERT); + XFREE(x509, NULL, DYNAMIC_TYPE_X509); +} +#endif /* !NO_CERTS && (KEEP_PEER_CERT || SESSION_CERTS || OPENSSL_EXTRA...) */ + +/* ------------------------------------------------------- ProcessPeerCertParse + * + * `if (ssl == NULL || args == NULL || args->dCert == NULL)`. The one caller + * owns all three, so no operand flips. Each vector returns BAD_FUNC_ARG before + * touching the certificate buffer. The false partner comes from the ordinary + * certificate-verification runs that share this measurement. */ +#if !defined(NO_CERTS) && \ + (!defined(NO_WOLFSSL_CLIENT) || !defined(WOLFSSL_NO_CLIENT_AUTH)) +static void wb_process_peer_cert_parse(WbFix* f) +{ + ProcPeerCertArgs args; + DecodedCert dCert; + byte* subjectHash = NULL; + int alreadySigner = 0; + + wb_reset(f); + XMEMSET(&args, 0, sizeof(args)); + XMEMSET(&dCert, 0, sizeof(dCert)); + args.dCert = &dCert; + args.certIdx = 0; + args.count = 0; + + /* operand 0 */ + (void)ProcessPeerCertParse(NULL, &args, CERT_TYPE, VERIFY, &subjectHash, + &alreadySigner); + g_calls++; + + /* operand 1 */ + (void)ProcessPeerCertParse(f->ssl, NULL, CERT_TYPE, VERIFY, &subjectHash, + &alreadySigner); + g_calls++; + +#ifndef WOLFSSL_SMALL_CERT_VERIFY + /* operand 2 -- compiled only when the decoded-cert operand is part of the + * decision. Under WOLFSSL_SMALL_CERT_VERIFY it is not, and calling with a + * NULL dCert would run on into the parser instead of being rejected. */ + args.dCert = NULL; + (void)ProcessPeerCertParse(f->ssl, &args, CERT_TYPE, VERIFY, &subjectHash, + &alreadySigner); + g_calls++; + args.dCert = &dCert; +#endif +} +#endif /* !NO_CERTS && (!NO_WOLFSSL_CLIENT || !WOLFSSL_NO_CLIENT_AUTH) */ + +/* --------------------------------------------------------------- ProcessCSR_ex + * + * `if (csr && !csr->ssl)`. The status-request extension is created with a NULL + * back-pointer and filled in here on the first pass, so by the time any test + * observes it the operand is pinned. All three vectors leave through the + * BUFFER_ERROR exit below (neither status_request nor status_request_v2 is + * set), so nothing is decoded and nothing is allocated. */ +#if !defined(NO_CERTS) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ + !defined(WOLFSSL_NO_TLS12) && \ + (!defined(NO_WOLFSSL_CLIENT) || !defined(WOLFSSL_NO_CLIENT_AUTH)) +static void wb_process_csr(WbFix* f) +{ + TLSX ext; + CertificateStatusRequest* csr; + byte input[32]; + word32 idx; + + csr = (CertificateStatusRequest*)XMALLOC(sizeof(*csr), NULL, + DYNAMIC_TYPE_TLSX); + if (csr == NULL) + return; + XMEMSET(input, 0, sizeof(input)); + + wb_reset(f); + XMEMSET(&ext, 0, sizeof(ext)); + ext.type = TLSX_STATUS_REQUEST; + ext.next = NULL; + f->ssl->extensions = &ext; + f->ssl->status_request = 0; +#ifdef HAVE_CERTIFICATE_STATUS_REQUEST_V2 + f->ssl->status_request_v2 = 0; +#endif + + /* operand 0 false: the extension carries no request object */ + ext.data = NULL; + idx = 0; + (void)ProcessCSR_ex(f->ssl, input, &idx, 0, 0); + g_calls++; + + /* operands 0 and 1 true: a request whose back-pointer is not yet set */ + XMEMSET(csr, 0, sizeof(*csr)); + csr->ssl = NULL; + ext.data = csr; + idx = 0; + (void)ProcessCSR_ex(f->ssl, input, &idx, 0, 0); + g_calls++; + + /* operand 1 false: the back-pointer is already set */ + csr->ssl = f->ssl; + idx = 0; + (void)ProcessCSR_ex(f->ssl, input, &idx, 0, 0); + g_calls++; + + f->ssl->extensions = NULL; + XFREE(csr, NULL, DYNAMIC_TYPE_TLSX); +} +#endif /* !NO_CERTS && HAVE_CERTIFICATE_STATUS_REQUEST && !WOLFSSL_NO_TLS12 */ + +/* ---------------------------------------------------------------------- main */ + +int main(void) +{ + WbFix f; + + XMEMSET(&f, 0, sizeof(f)); + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal peer-key white-box: wolfSSL_Init failed\n"); + return 0; + } + + f.ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + f.ctx = (WOLFSSL_CTX*)XMALLOC(sizeof(WOLFSSL_CTX), NULL, DYNAMIC_TYPE_CTX); + f.cm = (WOLFSSL_CERT_MANAGER*)XMALLOC(sizeof(WOLFSSL_CERT_MANAGER), NULL, + DYNAMIC_TYPE_CERT_MANAGER); + if (f.ssl == NULL || f.ctx == NULL || f.cm == NULL) { + printf("internal peer-key white-box: out of memory\n"); + goto done; + } + + /* A real RNG: the all-valid partner for the EcMakeKey guards has to + * generate an ephemeral key, and there is no other way to reach the + * false side of those decisions from inside this TU. */ + f.rngOk = (wc_InitRng(&f.rng) == 0); + if (!f.rngOk) + printf("internal peer-key white-box: no RNG, key-gen partners skipped\n"); + +#if !defined(NO_WOLFSSL_CLIENT) && \ + (defined(HAVE_ECC) || defined(HAVE_CURVE25519) || defined(HAVE_CURVE448)) +#ifdef HAVE_CURVE25519 + wb_ecmakekey_x25519(&f); +#endif +#ifdef HAVE_ECC + wb_ecmakekey_ecc(&f); +#endif +#endif +#if !defined(NO_WOLFSSL_SERVER) && defined(HAVE_ECC) + wb_set_curve_id(&f); +#endif +#if defined(HAVE_RPK) && !defined(NO_SHA256) && !defined(NO_CERTS) + wb_rpk_is_trusted(&f); +#endif +#if !defined(NO_CERTS) && (defined(KEEP_PEER_CERT) || defined(SESSION_CERTS) || \ + defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL)) + wb_copy_decoded(&f); +#endif +#if !defined(NO_CERTS) && \ + (!defined(NO_WOLFSSL_CLIENT) || !defined(WOLFSSL_NO_CLIENT_AUTH)) + wb_process_peer_cert_parse(&f); +#endif +#if !defined(NO_CERTS) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ + !defined(WOLFSSL_NO_TLS12) && \ + (!defined(NO_WOLFSSL_CLIENT) || !defined(WOLFSSL_NO_CLIENT_AUTH)) + wb_process_csr(&f); +#endif + + printf("internal peer-key white-box: %d static-guard calls\n", g_calls); + + if (f.rngOk) + wc_FreeRng(&f.rng); + +done: + XFREE(f.cm, NULL, DYNAMIC_TYPE_CERT_MANAGER); + XFREE(f.ctx, NULL, DYNAMIC_TYPE_CTX); + XFREE(f.ssl, NULL, DYNAMIC_TYPE_SSL); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal peer-key white-box: skipped (TLS 1.2 not built)\n"); + return 0; +} + +#endif From 6c77535f0e7aba785b81f1e7c1097e75fc7dc086 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 16:21:49 +0200 Subject: [PATCH 29/60] tests: white-box more internal.c static null-guards, and fix two that never paired Two more file-static functions (SupportedHashSigAlgo, GetCtxOcspLock, BuildCertificateStatusWithStatusCB, InvalidateSessionOnFatalAlert, dtlsRecordIsNewest, FreeCachedHandshakeMessages, ParseCipherList, SendHandshakeMsg, DecodePrivateKey_ex, GetRealSessionID, EcMakeKey, SetCurveId, RpkIsTrusted, CopyDecodedPubKey, CopyDecodedSig, ProcessPeerCertParse, ProcessCSR_ex) get their null-guard conditions closed via two new drivers (test_internal_nullguard_whitebox.c, test_internal_peerkey_whitebox.c), plus a third and fourth driver for HashSkeData/GetDhPublicKey (test_internal_dhskehash_whitebox.c) and EdDSA_Update (test_internal_eddsa_whitebox.c). Fixed a real bug in the first two drivers: SendHandshakeMsg's and DecodePrivateKey_ex's rejecting vectors had no self-contained accepting partner, on the assumption the campaign union could borrow one from the real handshake corpus. It cannot -- llvm-cov computes the covered bit per condition from one binary's own profile, and the campaign's union is a logical OR of those already-computed bits, not a merge of raw execution traces, so an accepting call recorded by one binary can never pair with a rejecting call recorded by another. Confirmed against the real campaign run: those two decisions, plus ProcessPeerCertParse's three-operand guard, stayed in GAPS.md after the first union despite being individually exercised by an isolated local measurement. Every driver here now supplies its own false/true partner in the same binary; verified locally per-driver and against the real tls_core aggregate before committing. src/internal.c union MC/DC: 690/1722 -> 752/1722. --- tests/include.am | 4 + tests/unit-mcdc/smoke-expected.txt | 5 + .../test_internal_dhskehash_whitebox.c | 315 ++++++++++++++++++ .../unit-mcdc/test_internal_eddsa_whitebox.c | 175 ++++++++++ .../test_internal_nullguard_whitebox.c | 37 +- .../test_internal_peerkey_whitebox.c | 35 +- 6 files changed, 561 insertions(+), 10 deletions(-) create mode 100644 tests/unit-mcdc/test_internal_dhskehash_whitebox.c create mode 100644 tests/unit-mcdc/test_internal_eddsa_whitebox.c diff --git a/tests/include.am b/tests/include.am index 1ae1c053a5a..8b2ce1b5363 100644 --- a/tests/include.am +++ b/tests/include.am @@ -167,6 +167,10 @@ EXTRA_DIST += \ tests/unit-mcdc/test_kdf_hash_fault_whitebox.c \ tests/unit-mcdc/test_internal_domain_whitebox.c \ tests/unit-mcdc/test_internal_clienthello_whitebox.c \ + tests/unit-mcdc/test_internal_dhskehash_whitebox.c \ + tests/unit-mcdc/test_internal_eddsa_whitebox.c \ + tests/unit-mcdc/test_internal_nullguard_whitebox.c \ + tests/unit-mcdc/test_internal_peerkey_whitebox.c \ tests/unit-mcdc/test_keys_whitebox.c \ tests/unit-mcdc/test_internal_record_whitebox.c \ tests/unit-mcdc/test_internal_sanity_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index 3e6ff20c4b9..6479196f0db 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -19,7 +19,12 @@ test_hpke_fault_whitebox test_hpke_whitebox test_integer_fault_whitebox test_integer_whitebox +test_internal_clienthello_whitebox +test_internal_dhskehash_whitebox test_internal_domain_whitebox +test_internal_eddsa_whitebox +test_internal_nullguard_whitebox +test_internal_peerkey_whitebox test_internal_record_whitebox test_internal_sanity_whitebox test_internal_suites_whitebox diff --git a/tests/unit-mcdc/test_internal_dhskehash_whitebox.c b/tests/unit-mcdc/test_internal_dhskehash_whitebox.c new file mode 100644 index 00000000000..c8b8997cde0 --- /dev/null +++ b/tests/unit-mcdc/test_internal_dhskehash_whitebox.c @@ -0,0 +1,315 @@ +/* test_internal_dhskehash_whitebox.c -- MC/DC white-box driver for the + * file-static ServerKeyExchange hashing and DH-parameter guards in + * src/internal.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-1301, USA + */ + +/* WHY A WHITE-BOX. + * + * HashSkeData -- `if (ret == 0 && !SigAlgoCachesMsgs(sigAlgo))`, + * appearing twice. A real handshake only ever runs this + * with one fixed signature algorithm per connection, so + * the caching/non-caching split of SigAlgoCachesMsgs() + * never varies within a single call site's history the + * way a from-scratch driver can force it to. + * GetDhPublicKey -- the four-operand FFDHE-parameter-match guard. A real + * handshake against a conforming DH server always uses + * genuine, matching FFDHE parameters, so the decision is + * always false there; the four ways it can be forced + * true (unknown group, wrong g length, wrong g bytes, + * wrong p bytes) never occur against a real peer. + * + * MC/DC's independence pair has to be demonstrated inside ONE binary's own + * execution trace: llvm-cov computes the covered bit per condition from a + * single profile, and the campaign's union is a logical OR of those + * already-computed bits across binaries, not a merge of raw traces. So every + * pair below -- including the "everything valid" partner -- is produced by + * THIS driver; none of it is borrowed from the real handshake corpus. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Each target carries the SAME preprocessor guard that encloses it in + * internal.c. + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) + +static int g_calls; + +typedef struct WbFix { + WOLFSSL* ssl; + WOLFSSL_CTX* ctx; +} WbFix; + +static void wb_reset(WbFix* f) +{ + XMEMSET(f->ssl, 0, sizeof(*f->ssl)); + XMEMSET(f->ctx, 0, sizeof(*f->ctx)); + f->ssl->ctx = f->ctx; + f->ssl->heap = NULL; + f->ssl->devId = INVALID_DEVID; + f->ssl->version.major = SSLv3_MAJOR; + f->ssl->version.minor = TLSv1_2_MINOR; +} + +/* Free whatever HashSkeData / GetDhPublicKey may have left allocated on the + * fixture, so the next vector starts clean. Safe to call unconditionally -- + * XFREE(NULL, ...) is a no-op. */ +static void wb_free_buffers(WbFix* f) +{ + XFREE(f->ssl->buffers.sig.buffer, f->ssl->heap, DYNAMIC_TYPE_SIGNATURE); + f->ssl->buffers.sig.buffer = NULL; + XFREE(f->ssl->buffers.digest.buffer, f->ssl->heap, DYNAMIC_TYPE_DIGEST); + f->ssl->buffers.digest.buffer = NULL; + XFREE(f->ssl->buffers.serverDH_P.buffer, f->ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY); + f->ssl->buffers.serverDH_P.buffer = NULL; + XFREE(f->ssl->buffers.serverDH_G.buffer, f->ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY); + f->ssl->buffers.serverDH_G.buffer = NULL; + XFREE(f->ssl->buffers.serverDH_Pub.buffer, f->ssl->heap, + DYNAMIC_TYPE_PUBLIC_KEY); + f->ssl->buffers.serverDH_Pub.buffer = NULL; +} + +/* ----------------------------------------------------------- HashSkeData + * + * Two identical two-operand guards over the same `ret`/`sigAlgo`, evaluated + * back to back with nothing in between that can change either. One sweep of + * three calls pairs all four target conditions at once: + * - an early failure (unsupported hash type) drives `ret == 0` false at + * both sites, sharing one vector; + * - a non-caching signature algorithm (rsa_sa_algo) with a valid hash type + * drives both sites fully true -- the decision's other shared vector; + * - a caching signature algorithm (ed25519_sa_algo, when built) with the + * same valid hash type drives `!SigAlgoCachesMsgs()` false at both sites + * while `ret == 0` stays true, pairing the second operand. */ +#if (!defined(NO_WOLFSSL_CLIENT) && (!defined(NO_DH) || defined(HAVE_ECC) || \ + defined(HAVE_CURVE25519) || defined(HAVE_CURVE448))) || \ + (!defined(NO_WOLFSSL_SERVER) && (defined(HAVE_ECC) || \ + ((defined(HAVE_CURVE25519) || defined(HAVE_CURVE448)) && \ + (defined(HAVE_ED25519) || defined(HAVE_ED448) || !defined(NO_RSA)))) || \ + (!defined(NO_DH) && (!defined(NO_RSA) || defined(HAVE_ANON)))) +static void wb_hash_ske_data(WbFix* f) +{ + static const byte data[8] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + byte clientRandom[RAN_LEN]; + byte serverRandom[RAN_LEN]; + Arrays arrays; + + XMEMSET(clientRandom, 0xAA, sizeof(clientRandom)); + XMEMSET(serverRandom, 0xBB, sizeof(serverRandom)); + XMEMSET(&arrays, 0, sizeof(arrays)); + XMEMCPY(arrays.clientRandom, clientRandom, RAN_LEN); + XMEMCPY(arrays.serverRandom, serverRandom, RAN_LEN); + + /* shared false-operand-0 vector: an invalid hash type makes + * wc_HashGetDigestSize() fail, so ret != 0 before either guard. */ + wb_reset(f); + f->ssl->arrays = &arrays; + (void)HashSkeData(f->ssl, (enum wc_HashType)9999, data, + (word32)sizeof(data), rsa_sa_algo); + g_calls++; + wb_free_buffers(f); + + /* the shared true vector: ret stays 0 through both guards, and + * rsa_sa_algo does not cache messages -- SigAlgoCachesMsgs() is false. */ + wb_reset(f); + f->ssl->arrays = &arrays; + (void)HashSkeData(f->ssl, WC_HASH_TYPE_SHA256, data, + (word32)sizeof(data), rsa_sa_algo); + g_calls++; + wb_free_buffers(f); + +#ifdef HAVE_ED25519 + /* operand 1's false pair: ret stays 0, but ed25519_sa_algo DOES cache + * messages -- SigAlgoCachesMsgs() is true, so !SigAlgoCachesMsgs() is + * false and the decision is false with operand 0 held true. */ + wb_reset(f); + f->ssl->arrays = &arrays; + (void)HashSkeData(f->ssl, WC_HASH_TYPE_SHA256, data, + (word32)sizeof(data), ed25519_sa_algo); + g_calls++; + wb_free_buffers(f); +#endif + + f->ssl->arrays = NULL; +} +#endif + +/* --------------------------------------------------------- GetDhPublicKey + * + * The four-operand FFDHE match guard. All four vectors and the shared false + * (accepting) partner are built from the SAME real ffdhe2048 parameter table + * the library ships (wc_Dh_ffdhe2048_Get()), so "matches" and "does not + * match" are both well-defined without needing a live peer. */ +#ifndef NO_DH +#ifdef HAVE_FFDHE +#ifdef HAVE_FFDHE_2048 +#ifdef HAVE_PUBLIC_FFDHE +/* Build a ServerKeyExchange DH-params wire fragment: 2-byte-length-prefixed + * P, G, Pub. Returns the total length written to buf (which must be large + * enough -- callers size it generously). */ +static word32 wb_build_dh_wire(byte* buf, const byte* p, word16 pLen, + const byte* g, word16 gLen, + const byte* pub, word16 pubLen) +{ + word32 idx = 0; + + c16toa(pLen, buf + idx); idx += OPAQUE16_LEN; + XMEMCPY(buf + idx, p, pLen); idx += pLen; + c16toa(gLen, buf + idx); idx += OPAQUE16_LEN; + XMEMCPY(buf + idx, g, gLen); idx += gLen; + c16toa(pubLen, buf + idx); idx += OPAQUE16_LEN; + XMEMCPY(buf + idx, pub, pubLen); idx += pubLen; + + return idx; +} + +static void wb_get_dh_public_key_one(WbFix* f, const byte* p, word16 pLen, + const byte* g, word16 gLen) +{ + byte wire[600]; + byte pub[8]; + DskeArgs args; + word32 wireLen; + + XMEMSET(pub, 0x01, sizeof(pub)); + wireLen = wb_build_dh_wire(wire, p, pLen, g, gLen, pub, + (word16)sizeof(pub)); + + wb_reset(f); + f->ssl->options.minDhKeySz = 0; + f->ssl->options.maxDhKeySz = 4096; + + XMEMSET(&args, 0, sizeof(args)); + args.idx = 0; + args.begin = 0; + + (void)GetDhPublicKey(f->ssl, wire, wireLen, &args); + g_calls++; + wb_free_buffers(f); +} + +static void wb_get_dh_public_key(WbFix* f) +{ + const DhParams* real = wc_Dh_ffdhe2048_Get(); + byte badP[256]; + byte badG[1]; + byte shortP[100]; + + if (real == NULL || real->p_len != 256 || real->g_len != 1) + return; /* table shape changed; nothing safe to build against */ + + XMEMCPY(badP, real->p, sizeof(badP)); + badP[0] ^= 0xFF; /* content differs, same length */ + badG[0] = (byte)(real->g[0] ^ 0xFF); + XMEMSET(shortP, 0x03, sizeof(shortP)); + + /* operand 0 true: P length matches no known FFDHE group -> params stays + * NULL. G is irrelevant (any 1 byte). */ + wb_get_dh_public_key_one(f, shortP, (word16)sizeof(shortP), + real->g, (word16)real->g_len); + + /* operand 1 true: P length selects ffdhe2048 (so params != NULL), but G + * is a different LENGTH than params->g_len -- short-circuits before + * comparing G or P content. */ + wb_get_dh_public_key_one(f, real->p, (word16)real->p_len, + badG, (word16)0); + + /* operand 2 true: P length matches, G length matches, G content does + * not -- short-circuits before comparing P content. */ + wb_get_dh_public_key_one(f, real->p, (word16)real->p_len, + badG, (word16)real->g_len); + + /* operand 3 true: P length matches, G matches exactly, P content does + * not. */ + wb_get_dh_public_key_one(f, badP, (word16)real->p_len, + real->g, (word16)real->g_len); + + /* the shared false partner: everything matches the real table exactly */ + wb_get_dh_public_key_one(f, real->p, (word16)real->p_len, + real->g, (word16)real->g_len); +} +#endif /* HAVE_PUBLIC_FFDHE */ +#endif /* HAVE_FFDHE_2048 */ +#endif /* HAVE_FFDHE */ +#endif /* !NO_DH */ + +/* ---------------------------------------------------------------------- main */ + +int main(void) +{ + WbFix f; + + XMEMSET(&f, 0, sizeof(f)); + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal dh/ske-hash white-box: wolfSSL_Init failed\n"); + return 0; + } + + f.ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + f.ctx = (WOLFSSL_CTX*)XMALLOC(sizeof(WOLFSSL_CTX), NULL, DYNAMIC_TYPE_CTX); + if (f.ssl == NULL || f.ctx == NULL) { + printf("internal dh/ske-hash white-box: out of memory\n"); + goto done; + } + +#if (!defined(NO_WOLFSSL_CLIENT) && (!defined(NO_DH) || defined(HAVE_ECC) || \ + defined(HAVE_CURVE25519) || defined(HAVE_CURVE448))) || \ + (!defined(NO_WOLFSSL_SERVER) && (defined(HAVE_ECC) || \ + ((defined(HAVE_CURVE25519) || defined(HAVE_CURVE448)) && \ + (defined(HAVE_ED25519) || defined(HAVE_ED448) || !defined(NO_RSA)))) || \ + (!defined(NO_DH) && (!defined(NO_RSA) || defined(HAVE_ANON)))) + wb_hash_ske_data(&f); +#endif +#if !defined(NO_DH) && defined(HAVE_FFDHE) && defined(HAVE_FFDHE_2048) && \ + defined(HAVE_PUBLIC_FFDHE) + wb_get_dh_public_key(&f); +#endif + + printf("internal dh/ske-hash white-box: %d static-guard calls\n", g_calls); + +done: + XFREE(f.ctx, NULL, DYNAMIC_TYPE_CTX); + XFREE(f.ssl, NULL, DYNAMIC_TYPE_SSL); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal dh/ske-hash white-box: skipped (TLS 1.2 not built)\n"); + return 0; +} + +#endif diff --git a/tests/unit-mcdc/test_internal_eddsa_whitebox.c b/tests/unit-mcdc/test_internal_eddsa_whitebox.c new file mode 100644 index 00000000000..aabfd347cd3 --- /dev/null +++ b/tests/unit-mcdc/test_internal_eddsa_whitebox.c @@ -0,0 +1,175 @@ +/* test_internal_eddsa_whitebox.c -- MC/DC white-box driver for the + * file-static EdDSA_Update message-cache allocation guard in src/internal.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-1301, USA + */ + +/* WHY A WHITE-BOX. + * + * EdDSA_Update()'s cache-growth guard is + * if ((ret == 0) && (ssl->hsHashes->messages != NULL)) + * `ret` is 0 unless the growth allocation just above it failed, and a real + * client-auth handshake never sees that allocation fail, so `ret == 0` is + * pinned true across the whole real corpus -- the operand this driver targets + * never gets to flip there. (The other operand, hsHashes->messages != NULL, + * already gets both values across a real cached handshake, as successive + * updates append to a buffer that starts NULL -- that is why only one operand + * is a gap here.) + * + * The only way to make the allocation fail on demand, without a real + * out-of-memory condition, is mcdc_fault_alloc.h's counted allocator mock: + * arm it for exactly the one XMALLOC inside this call, so this call's + * allocation fails while everything the driver itself allocates around it + * (the fixture's own buffers) does not. + * + * MC/DC's independence pair has to be demonstrated inside ONE binary's own + * execution trace -- the campaign's union is a logical OR of per-binary + * covered bits, not a merge of raw traces -- so both the failing and the + * succeeding vector are produced by THIS driver. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - The target carries the SAME preprocessor guard that encloses it in + * internal.c. + */ + +#include + +#include + +#include "mcdc_fault_alloc.h" + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) + +#ifndef NO_TLS +#if !defined(WOLFSSL_NO_CLIENT_AUTH) && \ + ((defined(WOLFSSL_SM2) && defined(WOLFSSL_SM3)) || \ + (defined(HAVE_ED25519) && !defined(NO_ED25519_CLIENT_AUTH)) || \ + (defined(HAVE_ED448) && !defined(NO_ED448_CLIENT_AUTH))) + +static int g_calls; + +static void wb_eddsa_update(void) +{ + WOLFSSL ssl; + HS_Hashes hashes; + byte old_msgs1[4]; + byte old_msgs2[4]; + byte* prepped; + static const byte data[4] = { 0xAA, 0xBB, 0xCC, 0xDD }; + + XMEMSET(&ssl, 0, sizeof(ssl)); + ssl.heap = NULL; + ssl.options.cacheMessages = 1; + + mcdc_fa_install(); + + /* operand false: the growth allocation for this call fails, so ret != + * 0 and the guard short-circuits on its first operand. hsHashes-> + * messages is left untouched (still old_msgs1, which this driver owns + * and frees itself -- the function never reached the free-the-old- + * buffer step). */ + XMEMSET(&hashes, 0, sizeof(hashes)); + XMEMCPY(old_msgs1, data, sizeof(old_msgs1)); + prepped = (byte*)XMALLOC(sizeof(old_msgs1), NULL, DYNAMIC_TYPE_HASHES); + if (prepped != NULL) { + XMEMCPY(prepped, old_msgs1, sizeof(old_msgs1)); + hashes.messages = prepped; + hashes.length = (int)sizeof(old_msgs1); + ssl.hsHashes = &hashes; + + mcdc_fa_arm_only(1); /* the one XMALLOC inside EdDSA_Update */ + (void)EdDSA_Update(&ssl, data, (int)sizeof(data)); + mcdc_fa_disarm(); + g_calls++; + + XFREE(hashes.messages, NULL, DYNAMIC_TYPE_HASHES); + hashes.messages = NULL; + } + + /* operand true: the same call, unarmed. The allocation succeeds, the + * old buffer is freed BY THE FUNCTION and replaced, and the driver + * frees the new one afterward. */ + XMEMSET(&hashes, 0, sizeof(hashes)); + XMEMCPY(old_msgs2, data, sizeof(old_msgs2)); + prepped = (byte*)XMALLOC(sizeof(old_msgs2), NULL, DYNAMIC_TYPE_HASHES); + if (prepped != NULL) { + XMEMCPY(prepped, old_msgs2, sizeof(old_msgs2)); + hashes.messages = prepped; + hashes.length = (int)sizeof(old_msgs2); + ssl.hsHashes = &hashes; + + (void)EdDSA_Update(&ssl, data, (int)sizeof(data)); + g_calls++; + + /* the function replaced hashes.messages with a freshly grown + * buffer (old length + sizeof(data)); that is what needs freeing + * now, not "prepped" (which the function already freed). */ + XFREE(hashes.messages, NULL, DYNAMIC_TYPE_HASHES); + hashes.messages = NULL; + } + + mcdc_fa_restore(); + ssl.hsHashes = NULL; +} + +#endif /* !WOLFSSL_NO_CLIENT_AUTH && (SM2 || ED25519 || ED448) */ +#endif /* !NO_TLS */ + +/* ---------------------------------------------------------------------- main */ + +int main(void) +{ + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal EdDSA white-box: wolfSSL_Init failed\n"); + return 0; + } + +#ifndef NO_TLS +#if !defined(WOLFSSL_NO_CLIENT_AUTH) && \ + ((defined(WOLFSSL_SM2) && defined(WOLFSSL_SM3)) || \ + (defined(HAVE_ED25519) && !defined(NO_ED25519_CLIENT_AUTH)) || \ + (defined(HAVE_ED448) && !defined(NO_ED448_CLIENT_AUTH))) + wb_eddsa_update(); + printf("internal EdDSA white-box: %d static-guard calls\n", g_calls); +#else + printf("internal EdDSA white-box: skipped (no EdDSA client auth)\n"); +#endif +#else + printf("internal EdDSA white-box: skipped (NO_TLS)\n"); +#endif + + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal EdDSA white-box: skipped (TLS 1.2 not built)\n"); + return 0; +} + +#endif diff --git a/tests/unit-mcdc/test_internal_nullguard_whitebox.c b/tests/unit-mcdc/test_internal_nullguard_whitebox.c index 8b330d8d353..3ce3c55e638 100644 --- a/tests/unit-mcdc/test_internal_nullguard_whitebox.c +++ b/tests/unit-mcdc/test_internal_nullguard_whitebox.c @@ -405,13 +405,25 @@ static void wb_send_handshake_msg(WbFix* f) * * `if (key == NULL || key->buffer == NULL)`. The caller passes ssl->buffers.key * which is validated long before, so the guard only fires for a connection - * with no private key -- a state the API refuses to build. Both vectors take - * the "private key missing" exit; the false partner comes from the ordinary - * handshake runs. */ + * with no private key -- a state the API refuses to build. Both rejecting + * vectors take the "private key missing" exit. + * + * The false partner has to be produced by THIS binary: llvm-cov computes the + * covered bit per condition from one profile, and the campaign's union is a + * logical OR of those bits across binaries, not a merge of raw traces, so an + * accepting call recorded by the real handshake corpus can never pair with a + * rejecting call recorded here. It is supplied by a DerBuffer that is present + * but holds unparsable bytes and a keyType restricted to RSA, so + * DecodePrivateKey_ex takes exactly one decode attempt (wc_RsaPrivateKeyDecode + * on garbage, which fails cleanly) and returns without allocating anything + * that survives -- every algorithm block after the RSA one unconditionally + * frees whatever AllocKey produced before checking whether keyType selects it, + * so hsKey is back to freed/NULL by the time the function returns. */ #if !defined(NO_CERTS) static void wb_decode_private_key(WbFix* f) { DerBuffer der; + byte junk[4]; word32 hsType = 0; void* hsKey = NULL; word32 sigLen = 0; @@ -430,10 +442,21 @@ static void wb_decode_private_key(WbFix* f) INVALID_DEVID, 0, 0, 0, &sigLen); g_calls++; - /* Nothing was allocated on either path: both exit at "private key - * missing" (or, under WOLF_PRIVATE_KEY_ID, at the external-key early - * return), and hsKey is left untouched. */ - (void)hsKey; +#ifndef NO_RSA + /* the shared false partner: a DerBuffer that is present and non-empty, + * so both operands are false and the function proceeds to decode. */ + XMEMSET(junk, 0, sizeof(junk)); + der.buffer = junk; + der.length = (word32)sizeof(junk); + hsType = 0; + hsKey = NULL; + sigLen = 0; + (void)DecodePrivateKey_ex(f->ssl, rsa_sa_algo, &der, &hsType, &hsKey, + INVALID_DEVID, 0, 0, 0, &sigLen); + g_calls++; + if (hsKey != NULL) + FreeKey(f->ssl, (int)hsType, &hsKey); +#endif } #endif /* !NO_CERTS */ diff --git a/tests/unit-mcdc/test_internal_peerkey_whitebox.c b/tests/unit-mcdc/test_internal_peerkey_whitebox.c index 20b47d1ee21..a8a0f727dee 100644 --- a/tests/unit-mcdc/test_internal_peerkey_whitebox.c +++ b/tests/unit-mcdc/test_internal_peerkey_whitebox.c @@ -384,15 +384,27 @@ static void wb_copy_decoded(WbFix* f) /* ------------------------------------------------------- ProcessPeerCertParse * * `if (ssl == NULL || args == NULL || args->dCert == NULL)`. The one caller - * owns all three, so no operand flips. Each vector returns BAD_FUNC_ARG before - * touching the certificate buffer. The false partner comes from the ordinary - * certificate-verification runs that share this measurement. */ + * owns all three, so no operand flips there. Each rejecting vector returns + * BAD_FUNC_ARG before touching the certificate buffer. + * + * MC/DC's independence pair has to be demonstrated inside ONE binary's own + * execution trace -- llvm-cov computes the covered/not-covered bit per + * condition from a single profile, and the campaign's union is a logical OR + * of those already-computed bits across binaries, not a merge of raw traces. + * So an "accepting" vector recorded by the real handshake corpus can never + * pair with a "rejecting" vector recorded by this driver; the false partner + * has to be produced HERE. It is: a one-entry cert list carrying four + * unparsable bytes, so ParseCertRelative() fails cleanly (ASN_PARSE_E) after + * the guard instead of returning success -- the guard itself is all this + * driver needs to exercise, not a real chain. */ #if !defined(NO_CERTS) && \ (!defined(NO_WOLFSSL_CLIENT) || !defined(WOLFSSL_NO_CLIENT_AUTH)) static void wb_process_peer_cert_parse(WbFix* f) { ProcPeerCertArgs args; DecodedCert dCert; + buffer certs[1]; + byte junk[4]; byte* subjectHash = NULL; int alreadySigner = 0; @@ -423,6 +435,23 @@ static void wb_process_peer_cert_parse(WbFix* f) g_calls++; args.dCert = &dCert; #endif + + /* the shared false partner: ssl, args and dCert all valid, so the guard + * is false for every operand and the function runs on into the parser. */ + XMEMSET(junk, 0, sizeof(junk)); + certs[0].buffer = junk; + certs[0].length = (word32)sizeof(junk); + XMEMSET(&dCert, 0, sizeof(dCert)); + args.dCert = &dCert; + args.dCertInit = 0; + args.certs = certs; + args.count = 1; + args.certIdx = 0; + (void)ProcessPeerCertParse(f->ssl, &args, CERT_TYPE, VERIFY, &subjectHash, + &alreadySigner); + g_calls++; + if (args.dCertInit) + FreeDecodedCert(args.dCert); } #endif /* !NO_CERTS && (!NO_WOLFSSL_CLIENT || !WOLFSSL_NO_CLIENT_AUTH) */ From 94ccaaa829ce58487d1668fb00f4154fb9595bbb Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 16:26:38 +0200 Subject: [PATCH 30/60] tests: pair the lower ValidityTimeFormat bound with a pre-1950 vector test_wc_MakeCert_generalizedTimeValidity() closed the tm_year < 2050 operand by pushing notAfter past 2050. Add a second wc_MakeCert() call with a large negative daysValid to push notAfter below 1950 as well, pairing the tm_year >= 1950 operand (short-circuited false) against the existing >=1950,<2050 true row from ordinary certificate encodes in the same test group. Closes both remaining MC/DC operands of that decision; asn.c union coverage 1488/1580 -> 1490/1580. --- tests/api/test_asn.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 982e749f386..8c1efaf0bb4 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -3951,6 +3951,25 @@ int test_wc_MakeCert_generalizedTimeValidity(void) ExpectIntEQ(test_asn_findValidityTime(der, (word32)((derSz > 0) ? derSz : 0), ASN_GENERALIZED_TIME, 15), 1); + /* ValidityTimeFormat() is `tm_year >= 1950 && tm_year < 2050`: the call + * above pairs the upper bound (>= 1950 held true, < 2050 flips false). + * Pair the lower bound the same way - push notAfter's year below 1950 + * (~80 years back from "now") so `tm_year >= 1950` itself flips false. + * That operand is short-circuited, so the encode still lands on + * GeneralizedTime, just via the other half of the decision. wc_MakeCert() + * has no lower bound on daysValid; not asserting its return keeps a + * platform-dependent pre-epoch gmtime() failure from failing the whole + * variant. */ + if (EXPECT_SUCCESS()) { + cert.daysValid = -29200; + derSz = wc_MakeCert(&cert, der, SIGN_CERT_SCRATCH_SZ, &key, NULL, + &rng); + if (derSz > 0) { + ExpectIntEQ(test_asn_findValidityTime(der, (word32)derSz, + ASN_GENERALIZED_TIME, 15), 1); + } + } + XFREE(der, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); if (keyInit) wc_FreeRsaKey(&key); From 50dea6d74965518ce31a616a8f85ff8facf1a2eb Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 16:56:42 +0200 Subject: [PATCH 31/60] tests: guard RPK and DTLS-CID symbols that broke every other module's build tests/api.c is one shared translation unit compiled into every module's unit.test, filtered by --group only at runtime. Two calls added earlier this part referenced symbols that do not exist in every module's config, and broke the shared build for anything without that specific feature on -- not just the module the test was written for. test_ssl_cert.c: WOLFSSL_CERT_TYPE_X509 and the *_cert_type / *_expected_rpk family are declared only under #ifdef HAVE_RPK in wolfssl/ssl.h. The guard on test_wolfSSL_cert_api_arg_guards was only !NO_CERTS && !NO_WOLFSSL_CLIENT, so any config without --enable-rpk (asn, pkcs7, and the rest of wolfCrypt-only modules) failed to compile. Confirmed by inspecting the header's own #ifdef nesting, then by a real build. test_ssl_ext.c and test_ssl_cert.c: wolfSSL_dtls_set_pending_peer() is declared unconditionally in ssl.h but implemented only under WOLFSSL_DTLS_CID && !WOLFSSL_NO_SOCK (src/ssl_api_dtls.c). The surrounding #ifdef WOLFSSL_DTLS guard is not sufficient -- a config with WOLFSSL_DTLS on and WOLFSSL_DTLS_CID off compiles the call and fails at LINK time, not compile time, which is why tracing the header's #ifdefs alone did not catch it and a real build was needed. Reproduced against asn's own ignore_name_constraints and runtime_date_check variants: 'ld.lld: error: undefined symbol: wolfSSL_dtls_set_pending_peer'. Verified with real (non-cached) rebuilds of asn, pkcs7 and tls_core after the fix: all three link and gate clean. asn.c 1490/1580, pkcs7.c 943/1058, internal.c 752/1722 -- all unchanged from before this fix, confirming it repairs the build without touching what those modules measure. --- tests/api/test_ssl_cert.c | 16 ++++++++++++++++ tests/api/test_ssl_ext.c | 9 +++++++++ 2 files changed, 25 insertions(+) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 11d094f20a6..54e54936e35 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -1531,11 +1531,13 @@ int test_wolfSSL_cert_api_arg_guards(void) #if !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; +#ifdef HAVE_RPK int tp = 0; const char certTypes[] = { WOLFSSL_CERT_TYPE_X509 }; unsigned char spki[8]; XMEMSET(spki, 0, sizeof(spki)); +#endif ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); ExpectNotNull(ssl = wolfSSL_new(ctx)); @@ -1558,6 +1560,13 @@ int test_wolfSSL_cert_api_arg_guards(void) (void)wolfSSL_get_verify_depth(NULL); (void)wolfSSL_get_verify_depth(ssl); + /* Certificate type lists and raw public keys are RPK-only symbols + * (declared under #ifdef HAVE_RPK in wolfssl/ssl.h); this option list is + * shared with modules that do not enable RPK (asn, pkcs7, and the rest of + * wolfCrypt), and tests/api.c is one translation unit across all of them, + * so referencing these unguarded breaks every OTHER module's build, not + * just this one -- exactly what happened here. */ +#ifdef HAVE_RPK /* --- certificate type lists: NULL object, NULL buffer, bad length --- */ (void)wolfSSL_CTX_set_client_cert_type(NULL, certTypes, (int)sizeof(certTypes)); @@ -1602,6 +1611,7 @@ int test_wolfSSL_cert_api_arg_guards(void) (void)wolfSSL_clear_expected_rpk(NULL); (void)wolfSSL_CTX_clear_expected_rpk(ctx); (void)wolfSSL_clear_expected_rpk(ssl); +#endif /* HAVE_RPK */ /* --- verify configuration through NULL objects ---------------------- */ wolfSSL_CTX_set_verify(NULL, WOLFSSL_VERIFY_PEER, NULL); @@ -2814,6 +2824,11 @@ int test_wolfSSL_dtls_api_on_dtls_object(void) (void)wolfSSL_dtls_get_peer(dssl, peer, &peerSz); (void)wolfSSL_dtls_get_peer(dssl, NULL, &peerSz); (void)wolfSSL_dtls_get_peer(dssl, peer, NULL); + /* Implemented only under WOLFSSL_DTLS_CID && !WOLFSSL_NO_SOCK + * (src/ssl_api_dtls.c); declared unconditionally in ssl.h, so a config + * with WOLFSSL_DTLS on and WOLFSSL_DTLS_CID off compiles this call and + * fails at LINK time. Confirmed with a real build. */ +#if defined(WOLFSSL_DTLS_CID) && !defined(WOLFSSL_NO_SOCK) (void)wolfSSL_dtls_set_pending_peer(dssl, peer, 0); (void)wolfSSL_dtls_set_pending_peer(dssl, NULL, (unsigned int)sizeof(peer)); @@ -2823,6 +2838,7 @@ int test_wolfSSL_dtls_api_on_dtls_object(void) * gets both values */ (void)wolfSSL_dtls_set_pending_peer(dssl, peer, (unsigned int)sizeof(peer)); +#endif /* MTU: `ctx == NULL || newMtu > MAX_RECORD_SIZE`, both operands */ #ifdef WOLFSSL_DTLS_MTU diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index c8b842d1434..ed83f11de7c 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1592,9 +1592,18 @@ int test_wolfSSL_api_null_burndown(void) (void)wolfSSL_dtls_get_peer(ssl, NULL, &sz); (void)wolfSSL_dtls_get_peer(ssl, buf, NULL); (void)wolfSSL_dtls_get_peer(ssl, buf, &sz); + /* wolfSSL_dtls_set_pending_peer() is declared unconditionally in ssl.h but + * only IMPLEMENTED under WOLFSSL_DTLS_CID && !WOLFSSL_NO_SOCK + * (src/ssl_api_dtls.c); a config with WOLFSSL_DTLS on and + * WOLFSSL_DTLS_CID off compiles this call and fails at LINK time. The + * enclosing #ifdef WOLFSSL_DTLS above is not sufficient. Confirmed with a + * real build of two such asn.c variants (ignore_name_constraints, + * runtime_date_check): "undefined symbol: wolfSSL_dtls_set_pending_peer". */ +#if defined(WOLFSSL_DTLS_CID) && !defined(WOLFSSL_NO_SOCK) (void)wolfSSL_dtls_set_pending_peer(NULL, buf, (unsigned int)sizeof(buf)); (void)wolfSSL_dtls_set_pending_peer(ssl, NULL, (unsigned int)sizeof(buf)); (void)wolfSSL_dtls_set_pending_peer(ssl, buf, 0); +#endif (void)wolfSSL_dtls(NULL); (void)wolfSSL_dtls(ssl); #endif From bc530388932ae61fabfe02abe318301bea3e3376 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 17:41:38 +0200 Subject: [PATCH 32/60] tests: fix four -Werror classes that broke CI on every job CI failed every job on 09409464a with four distinct errors, all in tests added earlier this part, all from guards that did not match what wolfssl/ssl.h actually declares: unused-variable: proto/protoSz were used only under HAVE_ALPN and sz only under WOLFSSL_DTLS, but all three were declared unconditionally. Wherever those features are off they are unused, and the tree builds with -Werror=unused-variable. Declarations now sit under the same guard as their uses. implicit-function-declaration / nested-externs / int-conversion: test_wolfSSL_x509_accessor_guards was guarded on WOLFSSL_CERT_GEN, which is not what gates that API. ssl.h declares wolfSSL_X509_load_certificate_file, _get_signature, _get_pubkey_buffer and _free under OPENSSL_EXTRA || OPENSSL_EXTRA_X509_SMALL || KEEP_PEER_CERT || KEEP_OUR_CERT || SESSION_CERTS. With the wrong macro they were implicit declarations, and the implied int return then tripped int-conversion on the WOLFSSL_X509* assignment. Guard now mirrors the header. implicit-function-declaration: wolfSSL_dtls_set_mtu is declared under (WOLFSSL_SCTP || WOLFSSL_DTLS_MTU) && WOLFSSL_DTLS. Guarding the call on WOLFSSL_DTLS_MTU alone left SCTP-only configs calling an undeclared function. discarded-qualifiers: wolfSSL_UseALPN takes char*, not const char*, so the string literals passed to it were a const violation. Uses a mutable buffer now. Verified locally before pushing, with -Werror, in the two configs that reproduce these: one with alpn/sni/dtls/opensslextra all off, and one with dtls+sctp but no dtls-mtu. Both compile clean. --- tests/api/test_ssl_cert.c | 22 +++++++++++++++++++--- tests/api/test_ssl_ext.c | 14 +++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 54e54936e35..820bdfbde9a 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2725,7 +2725,16 @@ int test_wolfSSL_ocsp_stapling_accessors(void) int test_wolfSSL_x509_accessor_guards(void) { EXPECT_DECLS; -#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && defined(WOLFSSL_CERT_GEN) +/* wolfSSL_X509_load_certificate_file / _get_signature / _get_pubkey_buffer / + * _free are declared in wolfssl/ssl.h only under this set of macros, not under + * WOLFSSL_CERT_GEN as an earlier version of this guard assumed. With the wrong + * macro they became implicit declarations, which -Werror=implicit-function- + * declaration and -Werror=nested-externs turn into build failures (and the + * implicit int return then trips -Werror=int-conversion on the assignment). */ +#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + (defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) || \ + defined(KEEP_PEER_CERT) || defined(KEEP_OUR_CERT) || \ + defined(SESSION_CERTS)) WOLFSSL_X509* x509 = NULL; byte buf[2048]; int iSz = (int)sizeof(buf); @@ -2841,7 +2850,10 @@ int test_wolfSSL_dtls_api_on_dtls_object(void) #endif /* MTU: `ctx == NULL || newMtu > MAX_RECORD_SIZE`, both operands */ -#ifdef WOLFSSL_DTLS_MTU +/* Declared under (WOLFSSL_SCTP || WOLFSSL_DTLS_MTU) && WOLFSSL_DTLS in ssl.h; + * guarding on WOLFSSL_DTLS_MTU alone left the SCTP-only configs calling an + * undeclared function. */ +#if (defined(WOLFSSL_SCTP) || defined(WOLFSSL_DTLS_MTU)) && defined(WOLFSSL_DTLS) (void)wolfSSL_CTX_dtls_set_mtu(NULL, 512); (void)wolfSSL_CTX_dtls_set_mtu(dctx, 0xFFFF); (void)wolfSSL_CTX_dtls_set_mtu(dctx, 512); @@ -3120,7 +3132,11 @@ static void fi_workload(void) (void)wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, "example.com", 11); #endif #ifdef HAVE_ALPN - (void)wolfSSL_UseALPN(ssl, "h2", 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); + { + char alpnList[] = "h2"; /* takes char*, not const char* */ + (void)wolfSSL_UseALPN(ssl, alpnList, 2, + WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); + } #endif #ifdef HAVE_SUPPORTED_CURVES (void)wolfSSL_UseSupportedCurve(ssl, WOLFSSL_ECC_SECP256R1); diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index ed83f11de7c..26f87121bce 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1527,9 +1527,17 @@ int test_wolfSSL_api_null_burndown(void) #if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; +#ifdef HAVE_ALPN + /* Declared under the same guard as their only uses: an unconditional + * declaration is an unused variable wherever the feature is off, and the + * tree builds tests with -Werror=unused-variable. */ char* proto = NULL; word16 protoSz = 0; + char alpnList[] = "h2"; /* wolfSSL_UseALPN takes char*, not const */ +#endif +#ifdef WOLFSSL_DTLS unsigned int sz = 0; +#endif byte buf[64]; XMEMSET(buf, 0, sizeof(buf)); @@ -1548,10 +1556,10 @@ int test_wolfSSL_api_null_burndown(void) (void)wolfSSL_SNI_GetRequest(ssl, WOLFSSL_SNI_HOST_NAME, NULL); #endif #ifdef HAVE_ALPN - (void)wolfSSL_UseALPN(NULL, "h2", 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); + (void)wolfSSL_UseALPN(NULL, alpnList, 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); (void)wolfSSL_UseALPN(ssl, NULL, 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); - (void)wolfSSL_UseALPN(ssl, "h2", 0, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); - (void)wolfSSL_UseALPN(ssl, "h2", 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); + (void)wolfSSL_UseALPN(ssl, alpnList, 0, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); + (void)wolfSSL_UseALPN(ssl, alpnList, 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); (void)wolfSSL_ALPN_GetProtocol(NULL, &proto, &protoSz); (void)wolfSSL_ALPN_GetProtocol(ssl, NULL, &protoSz); (void)wolfSSL_ALPN_GetProtocol(ssl, &proto, NULL); From 178c0645cbcc29d680724de6fa57c5b732afb096 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 17:45:12 +0200 Subject: [PATCH 33/60] tests: address Copilot review on the DTLS, OCSP and CRL white-boxes Four review comments, all correct. test_dtls_whitebox.c gated only on WOLFSSL_DTLS while calling helpers that src/dtls.c compiles under narrower guards: TlsCheckSupportedVersion and SendStatelessReplyDtls13 need WOLFSSL_DTLS13 && !NO_WOLFSSL_SERVER, and DtlsCidGetSize needs WOLFSSL_DTLS_CID. A DTLS build without 1.3 or without CID would not compile this driver. Same guard-mismatch class as the CI failures fixed in the previous commit; it did not show up there only because tests/unit-mcdc drivers are EXTRA_DIST and not built by the normal make. test_ocsp_whitebox.c leaked. GetOcspEntry() prepends a heap-allocated entry and makes it the new head when it finds no match, which vectors 1 and 2 both do; the driver then re-pointed ocspList at its own stack node, dropping that pointer. It now frees every heap node ahead of the stack node after each no-match vector, deriving the heap the same way FreeOcspEntry's caller in src/ocsp.c does (ocsp->cm->heap -- ocsp itself has no heap member, which the first attempt at this fix got wrong and the smoke run caught). Both skip-stub messages named only the feature macro while the guard also required certs and !WOLFCRYPT_ONLY, so a skip for either of those reasons reported a misleading cause. That matters more than it looks in this campaign: a driver that silently skips still exits 0 and reports nothing, and misreading why has cost real time here. Smoke suite: 77 passed, 0 failed. --- tests/unit-mcdc/test_crl_whitebox.c | 2 +- tests/unit-mcdc/test_dtls_whitebox.c | 16 +++++++++++++ tests/unit-mcdc/test_ocsp_whitebox.c | 34 ++++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/tests/unit-mcdc/test_crl_whitebox.c b/tests/unit-mcdc/test_crl_whitebox.c index a3bb68b48f8..e00a86d192a 100644 --- a/tests/unit-mcdc/test_crl_whitebox.c +++ b/tests/unit-mcdc/test_crl_whitebox.c @@ -493,7 +493,7 @@ int main(void) int main(void) { - printf("crl white-box: skipped (HAVE_CRL not built)\n"); + printf("crl white-box: skipped (needs HAVE_CRL, certs, and not WOLFCRYPT_ONLY)\n"); return 0; } diff --git a/tests/unit-mcdc/test_dtls_whitebox.c b/tests/unit-mcdc/test_dtls_whitebox.c index 8f9cba98211..1ba380f6c88 100644 --- a/tests/unit-mcdc/test_dtls_whitebox.c +++ b/tests/unit-mcdc/test_dtls_whitebox.c @@ -165,6 +165,9 @@ static void wb_client_hello_sanity(void) } /* ------------------------------------------ TlsCheckSupportedVersion :541 */ +/* Compiled in src/dtls.c under WOLFSSL_DTLS13 && !NO_WOLFSSL_SERVER, not under + * WOLFSSL_DTLS alone -- a DTLS build without 1.3 has no such symbol. */ +#if defined(WOLFSSL_DTLS13) && !defined(NO_WOLFSSL_SERVER) /* `if (!tlsxFound || tlsxSupportedVersions.elements == NULL)` * * Operand 0 is isolated by an extension block with no supported_versions in @@ -192,8 +195,11 @@ static void wb_check_supported_version(WOLFSSL* ssl) ch.extension.size = (word32)sizeof(with_sv); WB_NOTE(TlsCheckSupportedVersion(ssl, &ch, &isTls13)); } +#endif /* WOLFSSL_DTLS13 && !NO_WOLFSSL_SERVER */ /* ------------------------------------------------- DtlsCidGetSize :1146 */ +/* Compiled under WOLFSSL_DTLS_CID; a DTLS build without CID has no symbol. */ +#ifdef WOLFSSL_DTLS_CID /* `if (ssl == NULL || size == NULL)` -- both operands, plus the accepting * partner with a real ssl and a real out pointer. */ static void wb_cid_get_size(WOLFSSL* ssl) @@ -205,8 +211,11 @@ static void wb_cid_get_size(WOLFSSL* ssl) WB_NOTE(DtlsCidGetSize(ssl, &sz, 1)); WB_NOTE(DtlsCidGetSize(ssl, &sz, 0)); } +#endif /* WOLFSSL_DTLS_CID */ /* --------------------------------------- SendStatelessReplyDtls13 :851 */ +/* Compiled under WOLFSSL_DTLS13 && !NO_WOLFSSL_SERVER. */ +#if defined(WOLFSSL_DTLS13) && !defined(NO_WOLFSSL_SERVER) /* `if (!haveKS || !haveSA || !haveSG)` * * RFC 8446 section 9.2: a ClientHello that is not resuming must carry @@ -278,6 +287,7 @@ static void wb_stateless_reply_have_flags(WOLFSSL* ssl) WB_NOTE(SendStatelessReplyDtls13(ssl, &ch)); } } +#endif /* WOLFSSL_DTLS13 && !NO_WOLFSSL_SERVER */ /* ---------------------------------------------------------------- main */ @@ -319,9 +329,15 @@ int main(void) wb_create_dtls12_cookie(ssl); wb_find_ext_by_type(); wb_client_hello_sanity(); +#if defined(WOLFSSL_DTLS13) && !defined(NO_WOLFSSL_SERVER) wb_check_supported_version(ssl); +#endif +#ifdef WOLFSSL_DTLS_CID wb_cid_get_size(ssl); +#endif +#if defined(WOLFSSL_DTLS13) && !defined(NO_WOLFSSL_SERVER) wb_stateless_reply_have_flags(ssl); +#endif printf("dtls white-box: %d vectors driven\n", g_checks); diff --git a/tests/unit-mcdc/test_ocsp_whitebox.c b/tests/unit-mcdc/test_ocsp_whitebox.c index bcc6779c347..0b284dc4c53 100644 --- a/tests/unit-mcdc/test_ocsp_whitebox.c +++ b/tests/unit-mcdc/test_ocsp_whitebox.c @@ -66,6 +66,26 @@ static int g_checks; * The public path builds request and entry from the same certificate, so * outside this binary the two hashes are equal by construction and operand 0 * has no false case at all. */ +/* GetOcspEntry() prepends a freshly allocated OcspEntry when it finds no + * match, making it the new head. The driver then re-points ocspList at its own + * stack node for the next vector, which drops that pointer on the floor. Free + * every heap node ahead of the stack node before doing so. */ +static void wb_free_prepended(WOLFSSL_OCSP* ocsp, OcspEntry* stackNode) +{ + void* heap = (ocsp->cm != NULL) ? ocsp->cm->heap : NULL; + OcspEntry* e = ocsp->ocspList; + + /* Same heap derivation FreeOcspEntry's own caller in src/ocsp.c uses. */ + while (e != NULL && e != stackNode) { + OcspEntry* next = e->next; + + FreeOcspEntry(e, heap); + XFREE(e, heap, DYNAMIC_TYPE_OCSP_ENTRY); + e = next; + } + ocsp->ocspList = stackNode; +} + static void wb_entry_match(WOLFSSL_OCSP* ocsp) { OcspRequest req; @@ -88,6 +108,11 @@ static void wb_entry_match(WOLFSSL_OCSP* ocsp) XMEMSET(req.issuerHash, 0xBB, OCSP_DIGEST_SIZE); XMEMSET(req.issuerKeyHash, 0xCC, OCSP_DIGEST_SIZE); WB_NOTE(GetOcspEntry(ocsp, &req, &found)); + /* No match, so GetOcspEntry PREPENDED a heap entry and made it the head. + * Free it here: the next vector overwrites ocspList, which would otherwise + * lose the pointer, and the teardown sets the list to NULL rather than + * walking it (it cannot walk it -- the seeded node is on the stack). */ + wb_free_prepended(ocsp, &seeded); /* Vector 2: issuer hash equal, key hash differs -> operand 0 true, * operand 1 false. Pairs with vector 3 on operand 1. */ @@ -97,6 +122,7 @@ static void wb_entry_match(WOLFSSL_OCSP* ocsp) XMEMSET(req.issuerKeyHash, 0xDD, OCSP_DIGEST_SIZE); found = NULL; WB_NOTE(GetOcspEntry(ocsp, &req, &found)); + wb_free_prepended(ocsp, &seeded); /* Vector 3: both equal -> the accepting partner that completes both pairs. */ ocsp->ocspList = &seeded; @@ -105,9 +131,9 @@ static void wb_entry_match(WOLFSSL_OCSP* ocsp) found = NULL; WB_NOTE(GetOcspEntry(ocsp, &req, &found)); - /* Detach the stack entry before the CertManager frees the list, or the - * teardown walks into this frame. GetOcspEntry appends a heap node when it - * finds no match, so drop whatever it linked on as well. */ + /* Vector 3 matched, so nothing was prepended; the head is still the stack + * node. Detach it before the CertManager frees the list, or the teardown + * walks into this frame. */ ocsp->ocspList = NULL; } @@ -369,7 +395,7 @@ int main(void) int main(void) { - printf("ocsp white-box: skipped (HAVE_OCSP not built)\n"); + printf("ocsp white-box: skipped (needs HAVE_OCSP, certs, and not WOLFCRYPT_ONLY)\n"); return 0; } From d20bf29bf8426ffec77394037280bedde9703430 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 17:56:25 +0200 Subject: [PATCH 34/60] tests: fix two undefined references that broke unit.test linking Second CI round narrowed from every job to two clusters, both failing to link tests/unit.test on the same two symbols. test_wolfIO_DecodeUrl_host_bounds was defined only inside #if defined(HAVE_HTTP_CLIENT) but registered in the api.c test table unconditionally, so wherever HTTP client support is off the table referenced a symbol with no definition. Its sibling test_wolfIO_DecodeUrl_crlf_reject in the same block already carries an #else stub returning TEST_SKIPPED; this now has the same. Verified by preprocessing test_ocsp.c with HTTP client off: exactly one declaration and one definition survive, no duplicate. wolfSSL_SetSession() (capital S) is WOLFSSL_LOCAL -- declared in internal.h, not public API -- so referencing it from tests/api left an undefined reference in configurations that do not export internal symbols. The two calls are removed rather than guarded: the public wolfSSL_set_session() is already exercised a few lines above in the same function and covers the same guard, so nothing is lost. Smoke suite: 77 passed, 0 failed. --- tests/api/test_ocsp.c | 7 +++++++ tests/api/test_ssl_ext.c | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/api/test_ocsp.c b/tests/api/test_ocsp.c index 1fa553b915e..9faafd10f6d 100644 --- a/tests/api/test_ocsp.c +++ b/tests/api/test_ocsp.c @@ -2558,4 +2558,11 @@ int test_wolfIO_DecodeUrl_crlf_reject(void) { return TEST_SKIPPED; } +/* Same fallback its sibling has: the test table in api.c references this + * unconditionally, so without a stub the symbol is undefined wherever + * HAVE_HTTP_CLIENT is off and unit.test fails to link. */ +int test_wolfIO_DecodeUrl_host_bounds(void) +{ + return TEST_SKIPPED; +} #endif /* HAVE_HTTP_CLIENT */ diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index 26f87121bce..ad8f14ea851 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1766,9 +1766,11 @@ int test_wolfSSL_api_null_operands(void) (void)wolfSSL_SetServerID(ssl, buf, 0, 0); (void)wolfSSL_SetServerID(ssl, buf, -1, 0); (void)wolfSSL_SetServerID(ssl, buf, iSz, 0); - /* SetSession: ssl, session, then a session that exists but is not set up */ - (void)wolfSSL_SetSession(NULL, NULL); - (void)wolfSSL_SetSession(ssl, NULL); + /* wolfSSL_SetSession() (capital S) is WOLFSSL_LOCAL -- an internal symbol + * declared in internal.h, not part of the public API -- so referencing it + * from tests/api leaves an undefined reference in configurations that do + * not export it. The public wolfSSL_set_session() is exercised above and + * covers the same guard. */ #endif /* --- ALPN peer protocol: ssl, list, listSz -------------------------- */ From 70464e2266fea71e3e5d6618283987e5d5365598 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 3 Sep 2026 18:27:20 +0200 Subject: [PATCH 35/60] tests: fix three more -Werror classes found in the full CI sweep Letting the run against 15732a80d finish surfaced three problems that the earlier partial sampling had not, all in tests added this part. wolfSSL_dtls_set_mtu at test_dtls.c:9337 was guarded on WOLFSSL_DTLS_CH_FRAG alone. It is declared under (WOLFSSL_SCTP || WOLFSSL_DTLS_MTU) && WOLFSSL_DTLS, so a config that fragments ClientHellos but has neither MTU macro saw an implicit declaration. This is the same guard bug already fixed in test_ssl_cert.c; this second call site was missed then. df_secret_cb did not match TlsSecretCb. The typedef is int (*)(WOLFSSL*, void* secret, int secretSz, void* ctx); the callback had an extra uid=1000(dan) gid=1000(dan) groups=1000(dan),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),103(kvm),105(netdev),110(lpadmin),113(scanner) parameter and a const qualifier, which -Werror=incompatible-pointer-types rejects wherever HAVE_SECRET_CALLBACK is on. XSTRNCPY(longHost, "http://", sizeof(longHost)) tripped -Werror=stringop-truncation. The buffer is filled and terminated by hand on the next three lines, so the bounded copy bought nothing; it is an XMEMCPY of the seven-byte prefix now. Verified with -Werror against a dtls13+cid+mtu config with HAVE_SECRET_CALLBACK forced on: both touched files compile clean. --- tests/api/test_dtls.c | 17 +++++++++++++---- tests/api/test_ocsp.c | 5 ++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index c16789e9e0b..be9bb377b2e 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -8755,12 +8755,15 @@ static void df_pol_truncate(DfCtx* c, DfPkt* p) #ifdef HAVE_SECRET_CALLBACK /* Wired so a later forgery can read a protected record. Every forgery above * works on the plaintext record header and needs none of this. */ -static int df_secret_cb(WOLFSSL* ssl, int id, const unsigned char* secret, - int secretSz, void* ctx) +/* Must match TlsSecretCb exactly: + * int (*)(WOLFSSL* ssl, void* secret, int secretSz, void* ctx) + * an earlier version added an `id` parameter and a const qualifier that the + * typedef does not have, which -Werror=incompatible-pointer-types rejects. */ +static int df_secret_cb(WOLFSSL* ssl, void* secret, int secretSz, void* ctx) { DfCtx* c = (DfCtx*)ctx; - (void)ssl; (void)id; (void)secret; (void)secretSz; + (void)ssl; (void)secret; (void)secretSz; if (c != NULL) c->nSecrets++; return 0; @@ -9386,7 +9389,13 @@ static int df_run_ex(method_provider mc, method_provider ms, #else (void)useCid; #endif -#ifdef WOLFSSL_DTLS_CH_FRAG +/* Needs BOTH: CH fragmentation to make the oversized hello interesting, and + * the MTU setter to exist at all. wolfSSL_dtls_set_mtu is declared under + * (WOLFSSL_SCTP || WOLFSSL_DTLS_MTU) && WOLFSSL_DTLS -- guarding only on + * WOLFSSL_DTLS_CH_FRAG left it undeclared in configs that fragment but have + * neither MTU macro. */ +#if defined(WOLFSSL_DTLS_CH_FRAG) && defined(WOLFSSL_DTLS) && \ + (defined(WOLFSSL_SCTP) || defined(WOLFSSL_DTLS_MTU)) /* A ClientHello larger than the MTU is fragmented by the stack itself, * which is the only way to reach `isFirstCHFrag && extStart < helloSz`. * Editing bytes cannot produce it: the fragmentation has to be real. */ diff --git a/tests/api/test_ocsp.c b/tests/api/test_ocsp.c index 9faafd10f6d..5eccaa7d88f 100644 --- a/tests/api/test_ocsp.c +++ b/tests/api/test_ocsp.c @@ -2429,7 +2429,10 @@ int test_wolfIO_DecodeUrl_host_bounds(void) /* A host longer than the item cap ends the loop on the FIRST operand, * i < MAX_URL_ITEM_SIZE-1, which nothing else in the suite reaches. */ - XSTRNCPY(longHost, "http://", sizeof(longHost)); + /* XMEMCPY, not XSTRNCPY: gcc's -Werror=stringop-truncation fires on a + * bounded copy it cannot prove NUL-terminates, and the rest of this buffer + * is filled and terminated by hand immediately below anyway. */ + XMEMCPY(longHost, "http://", 7); for (i = 7; i < (int)sizeof(longHost) - 2; i++) longHost[i] = 'a'; longHost[sizeof(longHost) - 2] = '/'; From 514709ceb140efdf7d197b529f1fbadb9db80292 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 08:29:12 +0200 Subject: [PATCH 36/60] tests: enable the CID NULL-argument vectors now that upstream guards them The four calls fenced behind WOLFSSL_DTLS_CID_NULL_ARGS_GUARDED crashed an unpatched library, so they could not run: a segfault discards the coverage of every test in the variant. origin/master now guards all four -- wolfSSL_dtls_cid_use, _is_enabled and _set check ssl, and _set checks the cid buffer after its size == 0 early return, so (NULL, 0) still means empty CID -- and the fence and its explanation are no longer needed. Verified against the rebased tree with the dtls module: builds, runs, and gates clean with the vectors live. dtls.c 26/56, dtls13.c 70/132, unchanged. --- tests/api/test_dtls.c | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index be9bb377b2e..e08726b4694 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -9537,9 +9537,8 @@ int test_dtls13_packet_forgeries(void) /* --------------------------------------------------------------------------- * Connection ID argument guards. * - * Nineteen of the conditions left in dtls.c are in the CID functions, and it - * is worth recording what they actually are, because a great deal of packet - * machinery was pointed at them first and moved none of them: + * The remaining uncovered conditions in the CID code are NULL-and-zero + * argument guards on the public API: * * if (ssl == NULL || buf == NULL) DtlsCidGet * if (id == NULL || id->length == 0) @@ -9548,16 +9547,14 @@ int test_dtls13_packet_forgeries(void) * if (ssl == NULL || cid == NULL || size == 0) DtlsCidReplaceTx * if (msg == NULL || cidSz == 0 || msgSz < OPAQUE8_LEN + cidSz) * - * They are NULL-and-zero argument guards on the public API. No handshake - * passes NULL, and no forged datagram can make it: the operands are only - * reachable by calling the functions directly with the arguments a caller - * should not use. The existing CID tests all drive a working connection, so - * every one of these guards is taken the same way on every call. + * No handshake passes NULL and no forged datagram can produce one, so these + * operands are only reachable by calling the functions directly. The other + * CID tests all drive a working connection, which takes every guard the same + * way on every call. * - * Three states are needed for the second operand of each pair -- no ssl, an - * ssl with CID compiled but not enabled, and an ssl with CID enabled but not - * yet negotiated -- because "no CID info", "info but no id" and "an id of - * length zero" are distinct operands. + * Three ssl states are needed, because "no CID info", "info but no id" and + * "an id of length zero" are distinct operands: no ssl at all, an ssl with + * CID compiled but not enabled, and one with CID enabled but not negotiated. * ------------------------------------------------------------------------- */ int test_wolfSSL_dtls_cid_arg_guards(void) { @@ -9582,21 +9579,9 @@ int test_wolfSSL_dtls_cid_arg_guards(void) (void)wolfSSL_dtls_cid_use(enabled); /* ssl == NULL: the first operand of every guard */ - /* These four CRASH rather than returning an error, so they cannot run - * in the suite: a segfault discards the coverage of every test in the - * variant. They are library defects, reported separately; left here - * behind a macro so the gap is visible and re-enabling them is one - * define once the guards exist. - * wolfSSL_dtls_cid_use(NULL) writes ssl->options.useDtlsCID - * wolfSSL_dtls_cid_is_enabled(NULL) reads ssl->dtlsCidInfo - * wolfSSL_dtls_cid_set(NULL, cid, 4) reads ssl->options.useDtlsCID - * wolfSSL_dtls_cid_set(ssl, NULL, 4) memcpy from NULL in DtlsCidNew - */ -#ifdef WOLFSSL_DTLS_CID_NULL_ARGS_GUARDED (void)(wolfSSL_dtls_cid_use(NULL)); (void)(wolfSSL_dtls_cid_is_enabled(NULL)); (void)(wolfSSL_dtls_cid_set(NULL, cid, (word32)sizeof(cid))); -#endif (void)(wolfSSL_dtls_cid_get_rx_size(NULL, &sz)); (void)(wolfSSL_dtls_cid_get_tx_size(NULL, &sz)); ExpectIntNE(wolfSSL_dtls_cid_get_rx(NULL, buf, (unsigned int)sizeof(buf)), @@ -9607,9 +9592,7 @@ int test_wolfSSL_dtls_cid_arg_guards(void) (void)(wolfSSL_dtls_cid_get0_tx(NULL, &p)); /* the second operand: a valid ssl with a NULL buffer */ -#ifdef WOLFSSL_DTLS_CID_NULL_ARGS_GUARDED (void)(wolfSSL_dtls_cid_set(enabled, NULL, (word32)sizeof(cid))); -#endif (void)(wolfSSL_dtls_cid_get_rx_size(enabled, NULL)); (void)(wolfSSL_dtls_cid_get_tx_size(enabled, NULL)); (void)(wolfSSL_dtls_cid_get_rx(enabled, NULL, From be19488acdef7ba9bed15494049470101f96fe49 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 08:54:10 +0200 Subject: [PATCH 37/60] tests: fix two leaks and the SNI server-only guard The rebase push took CI from 108 failures to 10. Of those, three are ours. LeakSanitizer flagged two allocations the tests own and discard: wolfSSL_SESSION_dup() returns a new session object, not a borrowed one, so calling it for its side effect leaks it -- 2664 bytes from wolfSSL_NewSession. The duplicate is freed now. wolfSSL_CertManagerNew_ex(NULL) returns an owned CertManager -- 280 bytes. It was called bare to exercise the NULL-heap argument; the result is freed now. wolfSSL_SNI_GetRequest and wolfSSL_SNI_GetFromBuffer are compiled under HAVE_SNI && !NO_WOLFSSL_SERVER (src/ssl_api_ext.c): both read what a client sent, so a client-only build has neither. Guarding on HAVE_SNI alone left them undefined at link time there. Verified with -Werror in a client-only build (NO_WOLFSSL_SERVER, SNI and ALPN on): both touched files compile clean. The other seven failures are not ours: scripts/ocsp.test needs external DNS and the runner had none ("Couldn't find www.google.com, skipping", then "Both OCSP connection to globalsign and google failed"); that script is upstream and untouched by this branch. The make-check-linux matrix entries report "aborted (fail-fast)", i.e. cascade from a sibling, not independent failures. --- tests/api/test_ssl_cert.c | 9 ++++++++- tests/api/test_ssl_ext.c | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 820bdfbde9a..96b6d1314ed 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2954,7 +2954,14 @@ int test_wolfSSL_load_pathological_files(void) (void)wolfSSL_CertManagerUnloadCAs(NULL); wolfSSL_CertManagerFree(cm); } - (void)wolfSSL_CertManagerNew_ex(NULL); + { + /* Returns an owned CertManager; discarding it leaks + * (LeakSanitizer: 280 bytes from wolfSSL_CertManagerNew_ex). */ + WOLFSSL_CERT_MANAGER* tmpCm = wolfSSL_CertManagerNew_ex(NULL); + + if (tmpCm != NULL) + wolfSSL_CertManagerFree(tmpCm); + } wolfSSL_CTX_free(ctx); (void)remove(emptyFile); diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index ad8f14ea851..8dd34075b6f 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1552,9 +1552,14 @@ int test_wolfSSL_api_null_burndown(void) (void)wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, "a", 1); (void)wolfSSL_CTX_UseSNI(NULL, WOLFSSL_SNI_HOST_NAME, "a", 1); (void)wolfSSL_CTX_UseSNI(ctx, WOLFSSL_SNI_HOST_NAME, NULL, 1); + /* SNI_GetRequest and SNI_GetFromBuffer are compiled under + * HAVE_SNI && !NO_WOLFSSL_SERVER (src/ssl_api_ext.c) -- they read what a + * client sent, so a client-only build has neither. */ +#ifndef NO_WOLFSSL_SERVER (void)wolfSSL_SNI_GetRequest(NULL, WOLFSSL_SNI_HOST_NAME, NULL); (void)wolfSSL_SNI_GetRequest(ssl, WOLFSSL_SNI_HOST_NAME, NULL); #endif +#endif #ifdef HAVE_ALPN (void)wolfSSL_UseALPN(NULL, alpnList, 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); (void)wolfSSL_UseALPN(ssl, NULL, 2, WOLFSSL_ALPN_CONTINUE_ON_MISMATCH); @@ -1667,7 +1672,12 @@ int test_wolfSSL_session_null_burndown(void) /* and against a real, unestablished session where one exists */ sess = wolfSSL_get1_session(ssl); if (sess != NULL) { - (void)wolfSSL_SESSION_dup(sess); + /* SESSION_dup returns a new object the caller owns; discarding it + * leaks (LeakSanitizer: 2664 bytes from wolfSSL_NewSession). */ + WOLFSSL_SESSION* dup = wolfSSL_SESSION_dup(sess); + + if (dup != NULL) + wolfSSL_SESSION_free(dup); wolfSSL_SESSION_free(sess); } @@ -1789,7 +1799,9 @@ int test_wolfSSL_api_null_operands(void) #endif /* --- SNI from a raw ClientHello buffer ------------------------------ */ -#ifdef HAVE_SNI + /* Server-side only: it parses what a client sent (HAVE_SNI && + * !NO_WOLFSSL_SERVER in src/ssl_api_ext.c). */ +#if defined(HAVE_SNI) && !defined(NO_WOLFSSL_SERVER) { byte hello[64]; word32 outSz = (word32)sizeof(buf); From c278adc886a86097336c96897566ffdfd47eb46c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 09:09:55 +0200 Subject: [PATCH 38/60] tests: white-box the DTLS 1.3 role decisions, and CRL/OCSP null guards The largest remaining category in dtls13.c is not NULL guards but ssl->options.side comparisons. A connection has one side for its whole life, so each of those decisions is taken the same way on every call that endpoint makes, and a test owning both endpoints does not help: MC/DC wants both outcomes of the SAME decision in one binary's profile. Setting the side by hand is the only way to pair them. New driver test_dtls13_role_whitebox.c, 348 vectors over a zeroed WOLFSSL with its ctx pointed at a client CTX -- these functions read options, keys and dtls13Rtx and take scalars; none needs a peer or a handshake: Dtls13AcceptFragmented side x type x encryption x ChFrag x dtlsStateful Dtls13CheckEpoch side x type x epoch, over the whole switch Dtls13SaveOrFlushClientHello side x connectState across the range bounds Dtls13SetEpochKeys stored epoch side vs requested side, all nine pairs dtls13.c 70/132 -> 79/132. CRL and OCSP get the null guards their callers cannot reach: StoreCRL(crl == NULL || path == NULL) -- both operands; every in-tree caller validates both before reaching it. FreeOcspEntry(entry == NULL || !entry->ownStatus) -- an entry with a borrowed status list is what the multi-response path builds, and freeing one must be a no-op rather than a double free. CheckOcspRequest's ioCtx selection (ssl && ssl->ocspIOCtx != NULL) -- an ssl with no per-connection IO context, which falls back to the manager's, is produced by no existing test. crl.c 29/48 -> 31/48, ocsp.c 21/47 -> 24/47. Smoke: 78 drivers, 0 failed. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + tests/unit-mcdc/test_crl_whitebox.c | 34 +++ tests/unit-mcdc/test_dtls13_role_whitebox.c | 257 ++++++++++++++++++++ tests/unit-mcdc/test_ocsp_whitebox.c | 92 +++++++ 5 files changed, 385 insertions(+) create mode 100644 tests/unit-mcdc/test_dtls13_role_whitebox.c diff --git a/tests/include.am b/tests/include.am index 8b2ce1b5363..660fbdca9c5 100644 --- a/tests/include.am +++ b/tests/include.am @@ -145,6 +145,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_curve25519_whitebox.c \ tests/unit-mcdc/test_dh_fault_whitebox.c \ tests/unit-mcdc/test_dsa_fault_whitebox.c \ + tests/unit-mcdc/test_dtls13_role_whitebox.c \ tests/unit-mcdc/test_dtls_whitebox.c \ tests/unit-mcdc/test_ecc_fault_whitebox.c \ tests/unit-mcdc/test_ecc_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index 6479196f0db..e006f8783de 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -6,6 +6,7 @@ test_chacha_whitebox test_cmac_whitebox test_crl_whitebox test_cryptocb_whitebox +test_dtls13_role_whitebox test_dtls_whitebox test_eccsi_fault_whitebox test_eccsi_whitebox diff --git a/tests/unit-mcdc/test_crl_whitebox.c b/tests/unit-mcdc/test_crl_whitebox.c index e00a86d192a..7afc3e37e80 100644 --- a/tests/unit-mcdc/test_crl_whitebox.c +++ b/tests/unit-mcdc/test_crl_whitebox.c @@ -446,6 +446,39 @@ static void wb_load_crl(WOLFSSL_CERT_MANAGER* cm) FreeCRL(&crl, 0); } + +/* ------------------------------------------------------------- StoreCRL + + * `if (crl == NULL || path == NULL)` -- two operands, and every in-tree + * caller reaches StoreCRL only after the CRL and the path have already been + * validated by the public entry point above it, so neither operand ever takes + * its true value. Called directly, both do. + * + * The accepting partner writes to a path under the build directory and + * removes it again, so the vector leaves nothing behind. */ +static void wb_store_crl(WOLFSSL_CERT_MANAGER* cm) +{ + WOLFSSL_CRL crl; + const char* out = "test-store-crl.tmp"; + + /* operand 0: no CRL object */ + WB_NOTE(StoreCRL(NULL, out, WOLFSSL_FILETYPE_ASN1)); + + if (InitCRL(&crl, cm) != 0) { + printf("crl white-box: InitCRL failed, skipping StoreCRL\n"); + return; + } + /* operand 1: a CRL object but no path */ + WB_NOTE(StoreCRL(&crl, NULL, WOLFSSL_FILETYPE_ASN1)); + /* both valid: the shared partner. The list is empty so the store itself + * fails further down, which is fine -- the guard under test is above it. */ + WB_NOTE(StoreCRL(&crl, out, WOLFSSL_FILETYPE_ASN1)); + WB_NOTE(StoreCRL(&crl, out, WOLFSSL_FILETYPE_PEM)); + + FreeCRL(&crl, 0); + (void)remove(out); +} + /* ---------------------------------------------------------- main */ int main(void) @@ -479,6 +512,7 @@ int main(void) wb_find_revoked(); wb_buffer_store(cm); wb_load_crl(cm); + wb_store_crl(cm); printf("crl white-box: %d vectors driven\n", g_checks); diff --git a/tests/unit-mcdc/test_dtls13_role_whitebox.c b/tests/unit-mcdc/test_dtls13_role_whitebox.c new file mode 100644 index 00000000000..d6503190287 --- /dev/null +++ b/tests/unit-mcdc/test_dtls13_role_whitebox.c @@ -0,0 +1,257 @@ +/* test_dtls13_role_whitebox.c -- MC/DC white-box driver for the client/server + * role decisions in src/dtls13.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-1301, USA + */ + +/* WHY ROLE DECISIONS NEED A WHITE-BOX. + * + * The largest remaining category in dtls.c/dtls13.c is not NULL guards, it is + * `ssl->options.side == WOLFSSL_CLIENT_END` and its mirror. A connection has + * exactly one side for its whole life, so every one of these decisions is + * taken the same way on every call a given endpoint makes -- the operand never + * varies, and no amount of handshaking or packet forgery makes it vary. + * + * A test that owns both endpoints does not help either: the client object + * takes the client branch every time and the server object the server branch, + * in two different processes' worth of state. MC/DC wants both outcomes of the + * same decision recorded in ONE binary's profile, which means calling the + * function twice with the side field set differently. + * + * That is exactly what the fixture allows. These functions read ssl->options, + * ssl->keys and ssl->dtls13Rtx and take scalars; none of them needs a peer, a + * transport, or a completed handshake. Setting the side by hand and sweeping + * the message type against it pairs every operand. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + * - Bail paths print, so "covered nothing" differs from "nothing to say". + */ + +#include + +#include + +#include +#include + +#if defined(WOLFSSL_DTLS13) && defined(WOLFSSL_DTLS) && \ + !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) + +static int g_checks; +#define WB_NOTE(what) do { g_checks++; (void)(what); } while (0) + +/* The handshake types these decisions discriminate on. */ +static const byte kTypes[] = { + client_hello, server_hello, hello_verify_request, hello_retry_request, + hello_request, encrypted_extensions, certificate, certificate_verify, + finished, session_ticket, key_update, 200 /* not a handshake type */ +}; + +static const int kSides[2] = { WOLFSSL_CLIENT_END, WOLFSSL_SERVER_END }; + +/* Reset only what these functions read. Nothing is allocated or owned, so + * there is no teardown and no ordering between vectors. */ +static void wb_reset(WOLFSSL* ssl, WOLFSSL_CTX* ctx, int side) +{ + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + ssl->version.major = DTLS_MAJOR; + ssl->version.minor = DTLS_MINOR; /* DTLS 1.3 */ + ssl->options.side = (byte)side; + ssl->options.dtls = 1; +} + +/* ------------------------------------------------ Dtls13AcceptFragmented + + * `side == CLIENT_END && type == server_hello` and, under CH fragmentation, + * `side == SERVER_END && type == client_hello && dtls13ChFrag && dtlsStateful`. + * Four operands across two decisions; the side operand of each is constant on + * any real connection. */ +static void wb_accept_fragmented(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + size_t t; + int s, enc, frag, stateful; + + for (s = 0; s < 2; s++) { + for (t = 0; t < sizeof(kTypes) / sizeof(kTypes[0]); t++) { + for (enc = 0; enc < 2; enc++) { + for (frag = 0; frag < 2; frag++) { + for (stateful = 0; stateful < 2; stateful++) { + wb_reset(ssl, ctx, kSides[s]); + /* IsEncryptionOn reads the cipher setup flags; set + * them directly so the short-circuit ahead of the + * role test gets both values. */ + ssl->encrypt.setup = (byte)enc; + ssl->options.handShakeDone = (byte)enc; +#ifdef WOLFSSL_DTLS_CH_FRAG + ssl->options.dtls13ChFrag = (byte)frag; +#endif + ssl->options.dtlsStateful = (byte)stateful; + WB_NOTE(Dtls13AcceptFragmented(ssl, + (enum HandShakeType)kTypes[t])); + } + } + } + } + } +} + +/* ---------------------------------------------------- Dtls13CheckEpoch + + * A switch over handshake type against the record's epoch, with the + * client/server role deciding which epoch a given message may legally carry. + * Sweeping (side x type x epoch) pairs every arm. */ +static void wb_check_epoch(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + static const word32 kEpochs[] = { 0, DTLS13_EPOCH_EARLYDATA, + DTLS13_EPOCH_HANDSHAKE, + DTLS13_EPOCH_TRAFFIC0, 7 }; + size_t t, e; + int s; + + for (s = 0; s < 2; s++) { + for (t = 0; t < sizeof(kTypes) / sizeof(kTypes[0]); t++) { + for (e = 0; e < sizeof(kEpochs) / sizeof(kEpochs[0]); e++) { + wb_reset(ssl, ctx, kSides[s]); + ssl->keys.curEpoch64 = w64From32(0x0, kEpochs[e]); + WB_NOTE(Dtls13CheckEpoch(ssl, + (enum HandShakeType)kTypes[t])); + } + } + } +} + +/* -------------------------------------- Dtls13SaveOrFlushClientHello + + * `side == CLIENT_END && connectState >= CLIENT_HELLO_SENT && + * connectState <= HELLO_AGAIN_REPLY` -- three operands, and the two state + * bounds only matter while the side operand is true, which a server-side + * object never makes it past. The retransmit list is left empty: the decision + * under test is above the loop. */ +static void wb_save_or_flush(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + static const byte kStates[] = { + CONNECT_BEGIN, CLIENT_HELLO_SENT, HELLO_AGAIN, HELLO_AGAIN_REPLY, + FIRST_REPLY_DONE, FINISHED_DONE + }; + size_t i; + int s; + + for (s = 0; s < 2; s++) { + for (i = 0; i < sizeof(kStates) / sizeof(kStates[0]); i++) { + wb_reset(ssl, ctx, kSides[s]); + ssl->options.connectState = kStates[i]; + Dtls13SaveOrFlushClientHello(ssl); + g_checks++; + } + } +} + +/* ------------------------------------------------- Dtls13SetEpochKeys + + * `e->side != ENCRYPT_AND_DECRYPT_SIDE && e->side != side` -- both operands. + * A real connection installs keys for one side at a time in a fixed order, so + * the "already both sides" and "the other side" cases never pair. With no + * epoch allocated the function returns early, which is itself one of the + * outcomes; allocating one lets the comparison run. */ +static void wb_set_epoch_keys(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + static const enum encrypt_side kEncSides[3] = { + ENCRYPT_SIDE_ONLY, DECRYPT_SIDE_ONLY, ENCRYPT_AND_DECRYPT_SIDE + }; + size_t a, b; + int s; + + for (s = 0; s < 2; s++) { + for (a = 0; a < 3; a++) { + /* no epoch yet: the early-return arm */ + wb_reset(ssl, ctx, kSides[s]); + WB_NOTE(Dtls13SetEpochKeys(ssl, w64From32(0x0, DTLS13_EPOCH_HANDSHAKE), + kEncSides[a])); + + /* an epoch that exists, with each stored side in turn, so + * `e->side != ENCRYPT_AND_DECRYPT_SIDE && e->side != side` gets + * every combination */ + for (b = 0; b < 3; b++) { + wb_reset(ssl, ctx, kSides[s]); + ssl->dtls13Epochs[0].epochNumber = + w64From32(0x0, DTLS13_EPOCH_HANDSHAKE); + ssl->dtls13Epochs[0].side = (byte)kEncSides[b]; + ssl->dtls13Epochs[0].isValid = 1; + WB_NOTE(Dtls13SetEpochKeys(ssl, + w64From32(0x0, DTLS13_EPOCH_HANDSHAKE), + kEncSides[a])); + } + } + } +} + +/* ---------------------------------------------------------------- main */ + +int main(void) +{ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("dtls13 role white-box: wolfSSL_Init failed\n"); + goto done; + } + /* A client CTX needs no certificate; the side under test is a field on + * the ssl, set by hand, not a property of the CTX. */ + ctx = wolfSSL_CTX_new(wolfDTLSv1_3_client_method()); + if (ctx == NULL) { + printf("dtls13 role white-box: CTX_new failed\n"); + goto done; + } + ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + if (ssl == NULL) { + printf("dtls13 role white-box: out of memory\n"); + goto done; + } + + wb_accept_fragmented(ssl, ctx); + wb_check_epoch(ssl, ctx); + wb_save_or_flush(ssl, ctx); + wb_set_epoch_keys(ssl, ctx); + + printf("dtls13 role white-box: %d vectors driven\n", g_checks); + +done: + /* XFREE, not wolfSSL_free: nothing was constructed and the ctx pointer + * was assigned without taking a reference. */ + XFREE(ssl, NULL, DYNAMIC_TYPE_SSL); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("dtls13 role white-box: skipped (needs DTLS 1.3 and TLS)\n"); + return 0; +} + +#endif diff --git a/tests/unit-mcdc/test_ocsp_whitebox.c b/tests/unit-mcdc/test_ocsp_whitebox.c index 0b284dc4c53..ea7d4794585 100644 --- a/tests/unit-mcdc/test_ocsp_whitebox.c +++ b/tests/unit-mcdc/test_ocsp_whitebox.c @@ -357,6 +357,96 @@ static void wb_check_response(WOLFSSL_CERT_MANAGER* cm) NULL)); } + +/* --------------------------------------------------------- FreeOcspEntry + + * `if (entry == NULL || !entry->ownStatus)` -- the teardown path only ever + * reaches this with a real entry that owns its status list, because that is + * the only kind the parser builds. An entry whose status list is borrowed + * (ownStatus == 0) is what the multi-response path produces, and freeing one + * must be a no-op rather than a double free. + * + * Both vectors are stack objects and neither is linked into ocsp->ocspList, + * so nothing here can be reached by the CertManager teardown. */ +static void wb_free_ocsp_entry(WOLFSSL_OCSP* ocsp) +{ + void* heap = (ocsp->cm != NULL) ? ocsp->cm->heap : NULL; + OcspEntry borrowed; + + /* operand 0: no entry at all */ + FreeOcspEntry(NULL, heap); + g_checks++; + + /* operand 1: an entry that does not own its status list. Left empty, so + * the early return is the whole behaviour under test. */ + XMEMSET(&borrowed, 0, sizeof(borrowed)); + borrowed.ownStatus = 0; + borrowed.status = NULL; + borrowed.next = NULL; + FreeOcspEntry(&borrowed, heap); + g_checks++; + + /* the accepting partner: owns its (empty) list, so the loop is entered + * and finds nothing to free */ + XMEMSET(&borrowed, 0, sizeof(borrowed)); + borrowed.ownStatus = 1; + borrowed.status = NULL; + borrowed.next = NULL; + FreeOcspEntry(&borrowed, heap); + g_checks++; +} + +/* ------------------------------------------- CheckOcspRequest ioCtx :527 + + * `ioCtx = (ssl && ssl->ocspIOCtx != NULL) ? ssl->ocspIOCtx : ocsp->cm->ocspIOCtx` + * + * Both operands. Every in-tree caller either passes no ssl at all or passes + * one whose ocspIOCtx was set at configuration time, so "an ssl with no + * per-connection IO context" -- the case that falls back to the manager's -- + * is not produced by any existing test. */ +static void wb_check_request_ioctx(WOLFSSL_CERT_MANAGER* cm) +{ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + OcspRequest req; + byte serial[8]; + byte url[] = "http://ocsp.example.com/"; + static byte ioCtxMarker; + + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) + return; + ssl = wolfSSL_new(ctx); + + XMEMSET(&req, 0, sizeof(req)); + XMEMSET(serial, 0x5A, sizeof(serial)); + req.serial = serial; + req.serialSz = (int)sizeof(serial); + XMEMSET(req.issuerHash, 0xA1, OCSP_DIGEST_SIZE); + req.url = url; + req.urlSz = (int)sizeof(url) - 1; + + /* operand 0 false: no ssl, so the manager's context is used */ + WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL)); + + if (ssl != NULL) { + /* operand 0 true, operand 1 false: an ssl with no per-connection + * IO context -- falls back to the manager's */ + ssl->ocspIOCtx = NULL; + XMEMSET(req.issuerHash, 0xA2, OCSP_DIGEST_SIZE); + WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, ssl)); + + /* both true: the connection's own context wins */ + ssl->ocspIOCtx = &ioCtxMarker; + XMEMSET(req.issuerHash, 0xA3, OCSP_DIGEST_SIZE); + WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, ssl)); + ssl->ocspIOCtx = NULL; + } + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +} + /* ---------------------------------------------------------- main */ int main(void) @@ -381,6 +471,8 @@ int main(void) wb_check_responder(); wb_check_request(cm); wb_check_response(cm); + wb_free_ocsp_entry(cm->ocsp); + wb_check_request_ioctx(cm); printf("ocsp white-box: %d vectors driven\n", g_checks); From 14d3ff99ff1702d4b7fc3287f48fb831c627ed4b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 09:16:09 +0200 Subject: [PATCH 39/60] tests: public-API argument NULLs, one call per named operand A census of the remaining NULL-shaped conditions splits them by how the NULL actually arises: 282 from an argument the caller passes, 145 from a struct member legitimately NULL in some state, and only 15 from a failed allocation. This batch takes the first kind in public functions -- no fixture needed at all, which makes it the cheapest coverage left. One call per uncovered operand with every other argument valid, then the all-valid partner: a NULL in the first slot pairs only the first operand because the rest short-circuit away. Covered here: the two cipher-list getters (buf/len), check_domain_name and check_ip_address, CTX_GetDevId, get_cipher_suite_from_name, get_curve_name including its per-curve OID arms, load_verify_locations_ex's compound (file == NULL && path == NULL), use_certificate_ASN1, and export_keying_material. Every symbol was checked against BOTH its ssl.h declaration guard and its implementation guard in src/ before being called. A declaration without a compiled implementation is a link error rather than a compile error, and that distinction has cost this branch several CI rounds. ssl.c 12/89 -> 24/89, ssl_load.c 35/155 -> 41/155, ssl_certman.c 47 -> 48. --- tests/api/test_ssl_ext.c | 143 +++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_ext.h | 4 +- 2 files changed, 146 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index 8dd34075b6f..e18f120ae4e 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1861,3 +1861,146 @@ int test_wolfSSL_api_null_operands(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * Public-API argument NULLs, one call per named operand. + * + * A census of what is left splits the remaining NULL-shaped conditions three + * ways by how the NULL actually arises: 282 come from an argument a caller + * passes, 145 from a struct member that is legitimately NULL in some state, + * and only 15 from an allocation that failed. These are the first kind, in + * public functions -- the cheapest coverage left in the campaign and the only + * kind that needs no fixture at all. + * + * Each guard gets one call per uncovered operand with every OTHER argument + * valid, then the all-valid call that is their shared partner. A NULL in the + * first slot pairs only the first operand; the rest short-circuit away. + * + * Every symbol here was checked against BOTH its declaration guard in ssl.h + * and its implementation guard in src/, because a declaration without an + * implementation is a link error rather than a compile error, and that has + * cost this branch several CI rounds. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_public_null_operands(void) +{ + EXPECT_DECLS; +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_WOLFSSL_CLIENT) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + char buf[512]; + + XMEMSET(buf, 0, sizeof(buf)); + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* --- `buf == NULL || len <= 0` on both cipher-list getters ---------- */ + (void)wolfSSL_get_ciphers(NULL, (int)sizeof(buf)); + (void)wolfSSL_get_ciphers(buf, 0); + (void)wolfSSL_get_ciphers(buf, -1); + (void)wolfSSL_get_ciphers(buf, (int)sizeof(buf)); +#ifndef NO_ERROR_STRINGS + (void)wolfSSL_get_ciphers_iana(NULL, (int)sizeof(buf)); + (void)wolfSSL_get_ciphers_iana(buf, 0); + (void)wolfSSL_get_ciphers_iana(buf, -1); + (void)wolfSSL_get_ciphers_iana(buf, (int)sizeof(buf)); +#endif + + /* --- `ssl == NULL || dn == NULL` and the ip-address twin ----------- */ + (void)wolfSSL_check_domain_name(NULL, "example.com"); + (void)wolfSSL_check_domain_name(ssl, NULL); + (void)wolfSSL_check_domain_name(ssl, "example.com"); + (void)wolfSSL_check_ip_address(NULL, "127.0.0.1"); + (void)wolfSSL_check_ip_address(ssl, NULL); + (void)wolfSSL_check_ip_address(ssl, "127.0.0.1"); + /* an address that is not parseable, so the operand below the guard + * gets its false case too */ + (void)wolfSSL_check_ip_address(ssl, "not-an-ip"); + + /* --- `ctx != NULL && devId == INVALID_DEVID` ----------------------- */ + (void)wolfSSL_CTX_GetDevId(NULL, ssl); + (void)wolfSSL_CTX_GetDevId(NULL, NULL); + (void)wolfSSL_CTX_GetDevId(ctx, NULL); + (void)wolfSSL_CTX_GetDevId(ctx, ssl); + + /* --- `name == NULL || ...` on the suite lookup --------------------- */ + { + byte c0 = 0, c1 = 0; + + (void)wolfSSL_get_cipher_suite_from_name(NULL, &c0, &c1, NULL); + (void)wolfSSL_get_cipher_suite_from_name("TLS13-AES128-GCM-SHA256", + NULL, &c1, NULL); + (void)wolfSSL_get_cipher_suite_from_name("TLS13-AES128-GCM-SHA256", + &c0, NULL, NULL); + /* a name no build implements: the lookup's miss arm */ + (void)wolfSSL_get_cipher_suite_from_name("NO-SUCH-SUITE", + &c0, &c1, NULL); + (void)wolfSSL_get_cipher_suite_from_name("TLS13-AES128-GCM-SHA256", + &c0, &c1, NULL); + } + + /* --- the curve-name getter, which reads ssl->ecdhCurveOID ---------- */ +#if defined(HAVE_ECC) || defined(HAVE_CURVE25519) || defined(HAVE_CURVE448) + (void)wolfSSL_get_curve_name(NULL); + (void)wolfSSL_get_curve_name(ssl); + /* drive the OID arms directly: a connection negotiates one curve, so the + * others are never taken on any single ssl */ + if (ssl != NULL) { + #ifdef HAVE_CURVE25519 + ssl->ecdhCurveOID = ECC_X25519_OID; + (void)wolfSSL_get_curve_name(ssl); + #endif + #ifdef HAVE_CURVE448 + ssl->ecdhCurveOID = ECC_X448_OID; + (void)wolfSSL_get_curve_name(ssl); + #endif + ssl->ecdhCurveOID = 0; + (void)wolfSSL_get_curve_name(ssl); + } +#endif + + /* --- `(ctx == NULL) || ((file == NULL) && (path == NULL))` --------- */ +#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) + (void)wolfSSL_CTX_load_verify_locations_ex(NULL, caCertFile, NULL, 0); + /* both file and path NULL: the compound operand a caller giving either + * one never takes */ + (void)wolfSSL_CTX_load_verify_locations_ex(ctx, NULL, NULL, 0); + (void)wolfSSL_CTX_load_verify_locations_ex(ctx, caCertFile, NULL, 0); + (void)wolfSSL_CTX_load_verify_locations_ex(ctx, NULL, "certs", 0); +#endif + + /* --- `(ssl == NULL) || (der == NULL)` ------------------------------ */ +#ifndef NO_CERTS + { + static const byte tinyDer[] = { 0x30, 0x03, 0x02, 0x01, 0x00 }; + + (void)wolfSSL_use_certificate_ASN1(NULL, tinyDer, + (int)sizeof(tinyDer)); + (void)wolfSSL_use_certificate_ASN1(ssl, NULL, (int)sizeof(tinyDer)); + (void)wolfSSL_use_certificate_ASN1(ssl, tinyDer, 0); + (void)wolfSSL_use_certificate_ASN1(ssl, tinyDer, + (int)sizeof(tinyDer)); + } +#endif + + /* --- `ssl == NULL || out == NULL || label == NULL` ----------------- */ +#ifdef HAVE_KEYING_MATERIAL + { + byte km[32]; + + XMEMSET(km, 0, sizeof(km)); + (void)wolfSSL_export_keying_material(NULL, km, sizeof(km), + "lbl", 3, NULL, 0, 0); + (void)wolfSSL_export_keying_material(ssl, NULL, sizeof(km), + "lbl", 3, NULL, 0, 0); + (void)wolfSSL_export_keying_material(ssl, km, sizeof(km), + NULL, 3, NULL, 0, 0); + (void)wolfSSL_export_keying_material(ssl, km, sizeof(km), + "lbl", 3, NULL, 0, 0); + } +#endif + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_ext.h b/tests/api/test_ssl_ext.h index 3e61ba5b5b5..8d7b70d8a25 100644 --- a/tests/api/test_ssl_ext.h +++ b/tests/api/test_ssl_ext.h @@ -26,6 +26,7 @@ int test_wolfSSL_ech_config_api(void); int test_wolfSSL_api_null_burndown(void); int test_wolfSSL_session_null_burndown(void); int test_wolfSSL_api_null_operands(void); +int test_wolfSSL_public_null_operands(void); int test_wolfSSL_NoTicketTLSv12_ext(void); int test_wolfSSL_CTX_UseMaxFragment_ext(void); @@ -115,6 +116,7 @@ int test_wolfSSL_ticket_key_cb_renew_ext(void); TEST_DECL_GROUP("ssl_ext", test_wolfSSL_ech_config_api), \ TEST_DECL_GROUP("ssl_ext", test_wolfSSL_api_null_burndown), \ TEST_DECL_GROUP("ssl_ext", test_wolfSSL_session_null_burndown), \ - TEST_DECL_GROUP("ssl_ext", test_wolfSSL_api_null_operands) + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_api_null_operands), \ + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_public_null_operands) #endif /* TESTS_API_SSL_EXT_H */ From 794ebecbb72c293040d0817377218162f55bbb37 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 09:21:18 +0200 Subject: [PATCH 40/60] tests: session lifecycle guards, clearing the post-rebase ssl_sess.c regression The rebase cost ssl_sess.c two conditions: 32/120 before, 30/120 after, same denominator, deterministic across two runs with byte-identical GAPS.md. The only upstream change to that file is a one-line fopen swap inside wolfSSL_save_session_cache, whose conditions are all still covered, so the cause is elsewhere -- most likely the +52 lines upstream added to src/ssl.c, which drives these paths. Rather than accept a lower baseline, the two are recovered by covering more: the session object lifecycle guards, which are almost all "session == NULL || something about the session" and which a test that establishes a session reaches with a well-formed object every time. wolfSSL_SESSION_new / _dup / _up_ref / _free are unguarded in both ssl.h and src/ssl_sess.c -- checked before writing, since a declaration without a compiled implementation is a link error, not a compile error. Vectors cover the NULL half of each entry point, a session that exists but was never established, the up_ref / double-free refcount path, set_session with a not-set-up session, and SetServerID's three operands plus its new-session arm. ssl_sess.c 30/120 -> 32/120. Gate passes with no baseline drop. --- tests/api/test_ssl_ext.c | 79 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_ext.h | 4 +- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index e18f120ae4e..d5319a336ba 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -2004,3 +2004,82 @@ int test_wolfSSL_public_null_operands(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * Session object lifecycle argument guards. + * + * ssl_sess.c is the second-largest remaining file and its guards are almost + * all of the form `session == NULL || `. A test + * that establishes a session reaches them with a well-formed object every + * time, so the NULL half and the malformed half never occur. + * + * wolfSSL_SESSION_new / _dup / _up_ref / _free are unguarded in both ssl.h and + * src/ssl_sess.c, so they are callable in every configuration that has the + * session cache at all -- checked before writing, because a declaration + * without a compiled implementation is a link error. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_session_lifecycle_guards(void) +{ + EXPECT_DECLS; +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_SESSION_CACHE) && \ + !defined(NO_WOLFSSL_CLIENT) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + WOLFSSL_SESSION* fresh = NULL; + WOLFSSL_SESSION* dup = NULL; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* --- the NULL half of each lifecycle entry point ------------------- */ + (void)wolfSSL_SESSION_dup(NULL); + (void)wolfSSL_SESSION_up_ref(NULL); + wolfSSL_SESSION_free(NULL); + (void)wolfSSL_get_session(NULL); + (void)wolfSSL_get1_session(NULL); + (void)wolfSSL_set_session(NULL, NULL); + + /* --- a session that exists but was never established --------------- */ + fresh = wolfSSL_SESSION_new(); + if (fresh != NULL) { + /* dup of a real object: the accepting partner for the NULL above */ + dup = wolfSSL_SESSION_dup(fresh); + if (dup != NULL) + wolfSSL_SESSION_free(dup); + + /* up_ref then free twice: the refcount path, which a test that + * establishes one session and frees it once never exercises */ + (void)wolfSSL_SESSION_up_ref(fresh); + wolfSSL_SESSION_free(fresh); /* drops the extra reference */ + + /* set_session with a session that is not set up: the operand a + * successful resumption never takes */ + (void)wolfSSL_set_session(ssl, fresh); + + wolfSSL_SESSION_free(fresh); + } + + /* set_session on a valid ssl with NULL, and NULL ssl with a session */ + (void)wolfSSL_set_session(ssl, NULL); + +#ifndef NO_CLIENT_CACHE + { + byte id[16]; + + XMEMSET(id, 0x7E, sizeof(id)); + /* `ssl == NULL || id == NULL || len <= 0` -- one call per operand */ + (void)wolfSSL_SetServerID(NULL, id, (int)sizeof(id), 0); + (void)wolfSSL_SetServerID(ssl, NULL, (int)sizeof(id), 0); + (void)wolfSSL_SetServerID(ssl, id, 0, 0); + (void)wolfSSL_SetServerID(ssl, id, -1, 0); + (void)wolfSSL_SetServerID(ssl, id, (int)sizeof(id), 0); + /* and with the "new session" flag, which takes the other arm */ + (void)wolfSSL_SetServerID(ssl, id, (int)sizeof(id), 1); + } +#endif + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_ext.h b/tests/api/test_ssl_ext.h index 8d7b70d8a25..9da8d490241 100644 --- a/tests/api/test_ssl_ext.h +++ b/tests/api/test_ssl_ext.h @@ -27,6 +27,7 @@ int test_wolfSSL_api_null_burndown(void); int test_wolfSSL_session_null_burndown(void); int test_wolfSSL_api_null_operands(void); int test_wolfSSL_public_null_operands(void); +int test_wolfSSL_session_lifecycle_guards(void); int test_wolfSSL_NoTicketTLSv12_ext(void); int test_wolfSSL_CTX_UseMaxFragment_ext(void); @@ -117,6 +118,7 @@ int test_wolfSSL_ticket_key_cb_renew_ext(void); TEST_DECL_GROUP("ssl_ext", test_wolfSSL_api_null_burndown), \ TEST_DECL_GROUP("ssl_ext", test_wolfSSL_session_null_burndown), \ TEST_DECL_GROUP("ssl_ext", test_wolfSSL_api_null_operands), \ - TEST_DECL_GROUP("ssl_ext", test_wolfSSL_public_null_operands) + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_public_null_operands), \ + TEST_DECL_GROUP("ssl_ext", test_wolfSSL_session_lifecycle_guards) #endif /* TESTS_API_SSL_EXT_H */ From 011efca7db3b7fa79e12770e1aead2d472357d07 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 10:16:14 +0200 Subject: [PATCH 41/60] tests: DTLS API and X509 accessor argument guards Two more batches of public-API argument NULLs, the cheapest category left. ssl_api_dtls.c 11/53 -> 19/53. Its guards are ordinary NULL-and-zero pairs, but the accepting half of most needs a DTLS connection, which is why the file sat at 3/53 until one was supplied. Added: dtls_get0_peer's two operands, DTLSv1_get_timeout's two, set_timeout_max including the zero boundary, dtls13_use_quick_timeout with the fast-timeout flag set both ways, the dtls13_pending_work chain driven through each state it reports on (output buffered, key update owed, ack owed) because a connection only reaches those mid-flight between a blocked write and its retry, and SetCookieSecret's "buffer with zero length", which is neither the clear call (NULL, 0) nor a real secret. x509.c 12/37 -> 21/37, reusing the parsed-certificate fixture already in this file. Added: check_host's object and string operands plus the chklen case where the length includes the NUL terminator -- what a caller using strlen() never passes; check_ip_asc's three operands including an unparseable address; and load_certificate_file's NULL name, an empty file, a directory, and an unknown format. Guards were read from src/ssl_api_dtls.c and src/x509.c before writing rather than assumed from ssl.h. dtls13_pending_work is compiled only under WOLFSSL_DTLS13, SetCookieSecret only under WOLFSSL_DTLS && !NO_WOLFSSL_SERVER, several are gated on !WOLFSSL_LEANPSK, and the X509 name checks need !NO_ASN. --- tests/api/test_ssl_cert.c | 152 ++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_cert.h | 2 + 2 files changed, 154 insertions(+) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 96b6d1314ed..f51c525f930 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2792,8 +2792,48 @@ int test_wolfSSL_x509_accessor_guards(void) (void)wolfSSL_X509_get_pubkey_buffer(x509, NULL, (int*)&wSz); (void)wolfSSL_X509_get_pubkey_buffer(x509, buf, NULL); + /* --- host and IP matching, both operands of each guard --------- */ +#ifndef NO_ASN + /* `(x == NULL) || (chk == NULL)` */ + (void)wolfSSL_X509_check_host(NULL, "example.com", 11, 0, NULL); + (void)wolfSSL_X509_check_host(x509, NULL, 11, 0, NULL); + (void)wolfSSL_X509_check_host(x509, "example.com", 11, 0, NULL); + /* `chklen > 1 && chk[chklen - 1] == 0` -- a length that includes the + * terminator, which a caller using strlen() never passes */ + (void)wolfSSL_X509_check_host(x509, "example.com", 12, 0, NULL); + (void)wolfSSL_X509_check_host(x509, "e", 1, 0, NULL); + (void)wolfSSL_X509_check_host(x509, "", 0, 0, NULL); + + /* `(x == NULL) || (x->derCert == NULL) || (ipasc == NULL)` */ + (void)wolfSSL_X509_check_ip_asc(NULL, "127.0.0.1", 0); + (void)wolfSSL_X509_check_ip_asc(x509, NULL, 0); + (void)wolfSSL_X509_check_ip_asc(x509, "127.0.0.1", 0); + (void)wolfSSL_X509_check_ip_asc(x509, "not-an-ip", 0); +#endif + wolfSSL_X509_free(x509); } + + /* --- load_certificate_file: `fname == NULL`, and the size bounds ---- */ + (void)wolfSSL_X509_load_certificate_file(NULL, WOLFSSL_FILETYPE_PEM); + /* a file that exists but is empty: sz < 0 || sz > MAX is the guard the + * happy path never reaches */ + { + const char* emptyPem = "test-x509-empty.tmp"; + XFILE ef = XFOPEN(emptyPem, "wb"); + + if (ef != XBADFILE) { + XFCLOSE(ef); + (void)wolfSSL_X509_load_certificate_file(emptyPem, + WOLFSSL_FILETYPE_PEM); + (void)remove(emptyPem); + } + /* a directory where a file is expected */ + (void)wolfSSL_X509_load_certificate_file("certs", + WOLFSSL_FILETYPE_PEM); + /* an unknown format on a real certificate */ + (void)wolfSSL_X509_load_certificate_file(svrCertFile, -1); + } (void)wSz; #endif return EXPECT_RESULT(); @@ -3230,3 +3270,115 @@ int test_wolfSSL_alloc_failure_sweep(void) #endif return EXPECT_RESULT(); } + +/* --------------------------------------------------------------------------- + * DTLS API argument guards, on a DTLS object. + * + * ssl_api_dtls.c is the weakest of the newly-visible files. Its guards are the + * usual NULL-and-zero pairs, but the ACCEPTING half of most of them needs a + * DTLS connection -- the same reason the file sat at 3/53 until one was + * supplied. These add the entry points the earlier pass did not reach. + * + * Guards were read from src/ssl_api_dtls.c before writing, not assumed from + * ssl.h: wolfSSL_dtls13_pending_work is compiled only under WOLFSSL_DTLS13, + * SetCookieSecret only under WOLFSSL_DTLS && !NO_WOLFSSL_SERVER, and several + * are additionally gated on !WOLFSSL_LEANPSK. + * ------------------------------------------------------------------------- */ +int test_wolfSSL_dtls_api_more_guards(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_DTLS) && !defined(WOLFSSL_LEANPSK) && \ + !defined(WOLFCRYPT_ONLY) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_CERTS) + WOLFSSL_CTX* dctx = NULL; + WOLFSSL* dssl = NULL; + byte peer[64]; + unsigned int peerSz = (unsigned int)sizeof(peer); + const void* p0 = NULL; + unsigned int p0Sz = 0; + + XMEMSET(peer, 0, sizeof(peer)); + ExpectNotNull(dctx = wolfSSL_CTX_new(wolfDTLSv1_2_client_method())); + ExpectNotNull(dssl = wolfSSL_new(dctx)); + + /* `peer == NULL || peerSz == NULL` -- one call per operand */ + (void)wolfSSL_dtls_get0_peer(NULL, &p0, &p0Sz); + (void)wolfSSL_dtls_get0_peer(dssl, NULL, &p0Sz); + (void)wolfSSL_dtls_get0_peer(dssl, &p0, NULL); + (void)wolfSSL_dtls_get0_peer(dssl, &p0, &p0Sz); + + /* `ssl && timeleft` -- both operands */ + { + WOLFSSL_TIMEVAL tv; + + XMEMSET(&tv, 0, sizeof(tv)); + (void)wolfSSL_DTLSv1_get_timeout(NULL, &tv); + (void)wolfSSL_DTLSv1_get_timeout(dssl, NULL); + (void)wolfSSL_DTLSv1_get_timeout(dssl, &tv); + } + + /* `ssl == NULL || timeout < 0` -- and the boundary at zero */ + (void)wolfSSL_dtls_set_timeout_max(NULL, 5); + (void)wolfSSL_dtls_set_timeout_max(dssl, -1); + (void)wolfSSL_dtls_set_timeout_max(dssl, 0); + (void)wolfSSL_dtls_set_timeout_max(dssl, 5); + (void)wolfSSL_dtls_set_timeout_init(dssl, 0); + + /* the peer setters/getters with a real DTLS object */ + peerSz = (unsigned int)sizeof(peer); + (void)wolfSSL_dtls_get_peer(dssl, peer, &peerSz); + +#ifdef WOLFSSL_DTLS13 + /* `ssl != NULL && ssl->dtls13FastTimeout` -- both operands; the flag is + * never set on a connection that has not scheduled fast retransmission */ + (void)wolfSSL_dtls13_use_quick_timeout(NULL); + (void)wolfSSL_dtls13_use_quick_timeout(dssl); + if (dssl != NULL) { + dssl->dtls13FastTimeout = 1; + (void)wolfSSL_dtls13_use_quick_timeout(dssl); + dssl->dtls13FastTimeout = 0; + } + + /* `ssl == NULL || !Dtls13ScheduledWorkReady(ssl)` and the pending-work + * chain below it: output buffered, a key update owed, an ack owed. Each + * flag is set directly because a connection only reaches these states + * mid-flight, between a write that blocked and its retry. */ + (void)wolfSSL_dtls13_pending_work(NULL); + (void)wolfSSL_dtls13_pending_work(dssl); + if (dssl != NULL) { + dssl->options.handShakeDone = 1; + (void)wolfSSL_dtls13_pending_work(dssl); + dssl->dtls13DoKeyUpdate = 1; + (void)wolfSSL_dtls13_pending_work(dssl); + dssl->dtls13DoKeyUpdate = 0; + dssl->options.sendKeyUpdate = 1; + (void)wolfSSL_dtls13_pending_work(dssl); + dssl->options.sendKeyUpdate = 0; + dssl->dtls13SendingAckOrRtx = 1; + (void)wolfSSL_dtls13_pending_work(dssl); + dssl->dtls13SendingAckOrRtx = 0; + dssl->options.handShakeDone = 0; + } + (void)wolfSSL_dtls13_has_pending_msg(dssl); +#endif + +#if !defined(NO_WOLFSSL_SERVER) + { + byte secret[16]; + + XMEMSET(secret, 0xC0, sizeof(secret)); + /* `secret != NULL && secretSz == 0` -- the "clear the secret" call is + * (NULL, 0); a buffer with a zero length is the operand pair no + * caller produces */ + (void)wolfSSL_DTLS_SetCookieSecret(NULL, secret, 0); + (void)wolfSSL_DTLS_SetCookieSecret(NULL, NULL, 0); + (void)wolfSSL_DTLS_SetCookieSecret(NULL, secret, + (word32)sizeof(secret)); + } +#endif + + wolfSSL_free(dssl); + wolfSSL_CTX_free(dctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index c67056a72c2..8a74aab6fac 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -30,6 +30,7 @@ int test_wolfSSL_x509_accessor_guards(void); int test_wolfSSL_dtls_api_on_dtls_object(void); int test_wolfSSL_load_pathological_files(void); int test_wolfSSL_load_from_fifo(void); +int test_wolfSSL_dtls_api_more_guards(void); int test_wolfSSL_alloc_failure_sweep(void); int test_wolfSSL_get_verify_mode(void); @@ -120,6 +121,7 @@ int test_wolfSSL_verify_post_handshake_defers(void); TEST_DECL_GROUP("ssl_cert", test_wolfSSL_dtls_api_on_dtls_object), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_load_pathological_files), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_load_from_fifo), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_dtls_api_more_guards), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_alloc_failure_sweep) #endif /* TESTS_API_SSL_CERT_H */ From 7b59e428ac63200fb4d596a6b87a6bbae19afa16 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 14:27:09 +0200 Subject: [PATCH 42/60] tests: widen the allocation-failure workload, and record the small-stack cause The sweep only drove a CTX, one connection object and three file loads, so it reached few of the error arms it exists for. It now also exercises the extension setters, the session object lifecycle, the CertManager with its CRL and OCSP sub-objects, and the chain loader -- each of which allocates on paths whose failure branches a working configuration never takes. ssl_certman.c 48/113 -> 50/113, ssl_load.c 41/155 -> 43/155. The WOLFSSL_SMALL_STACK exclusion stays, but its comment no longer says the cause is unknown: DecodeCertInternal indexes RPKdataASN before checking the ret that CALLOC_ASNGETDATA sets, so an allocation failure dereferences NULL while parsing any certificate. A per-index sweep crashes at five indices (7, 30, 51, 68, 90), all at the same instruction, reached through load_verify_locations, use_certificate_file, use_certificate_chain_file and CertManagerVerify. Fixed upstream in PR 11378; the exclusion comes off once that merges and the sweep passes on the small-stack variant. --- tests/api/test_ssl_cert.c | 64 +++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index f51c525f930..e5e9cf0b597 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -3108,12 +3108,15 @@ int test_wolfSSL_load_from_fifo(void) * allocator installed would break every test that runs after this one in the * same binary, which costs the whole variant. * ------------------------------------------------------------------------- */ -/* Not under WOLFSSL_SMALL_STACK: that variant segfaults during the sweep - * while the default one completes it cleanly. Whether that is a small-stack - * allocation path that does not handle failure, or the harness exhausting - * something the small-stack build is more sensitive to, is not established -- - * and a crash there discards the whole variant, so it is excluded until the - * difference is understood rather than left to take the evidence down. */ +/* Not under WOLFSSL_SMALL_STACK, and the reason is now known rather than + * suspected: DecodeCertInternal indexes RPKdataASN before checking the ret + * that CALLOC_ASNGETDATA sets, so under that build an allocation failure + * dereferences NULL while parsing any certificate. A per-index sweep crashes + * at five allocation indices (7, 30, 51, 68, 90), all at the same + * instruction, reached through load_verify_locations, use_certificate_file, + * use_certificate_chain_file and CertManagerVerify. Fixed upstream in + * PR 11378; drop this exclusion once that merges and the sweep passes on the + * small-stack variant. A crash here would discard the whole variant. */ #if !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ !defined(WOLFSSL_SMALL_STACK) && \ !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) @@ -3190,10 +3193,59 @@ static void fi_workload(void) #endif #ifdef HAVE_SESSION_TICKET (void)wolfSSL_UseSessionTicket(ssl); +#endif +#ifdef HAVE_MAX_FRAGMENT + (void)wolfSSL_UseMaxFragment(ssl, WOLFSSL_MFL_2_9); +#endif +#ifdef HAVE_TRUSTED_CA + (void)wolfSSL_UseTrustedCA(ssl, WOLFSSL_TRUSTED_CA_PRE_AGREED, + NULL, 0); +#endif +#ifdef HAVE_OCSP + (void)wolfSSL_EnableOCSP(ssl, 0); #endif (void)wolfSSL_SetVersion(ssl, WOLFSSL_TLSV1_2); + (void)wolfSSL_set_cipher_list(ssl, "DEFAULT"); +#ifndef NO_SESSION_CACHE + { + WOLFSSL_SESSION* s1 = wolfSSL_get1_session(ssl); + + if (s1 != NULL) { + WOLFSSL_SESSION* s2 = wolfSSL_SESSION_dup(s1); + + if (s2 != NULL) + wolfSSL_SESSION_free(s2); + wolfSSL_SESSION_free(s1); + } + } +#endif wolfSSL_free(ssl); } + + /* A second reach: the CertManager and its CRL/OCSP sub-objects allocate + * on their own paths, and every one of those allocations is an error arm + * that a working configuration never takes. */ + { + WOLFSSL_CERT_MANAGER* cm = wolfSSL_CertManagerNew(); + + if (cm != NULL) { + (void)wolfSSL_CertManagerLoadCA(cm, caCertFile, NULL); + (void)wolfSSL_CertManagerVerify(cm, svrCertFile, + WOLFSSL_FILETYPE_PEM); +#ifdef HAVE_CRL + (void)wolfSSL_CertManagerEnableCRL(cm, WOLFSSL_CRL_CHECK); +#endif +#ifdef HAVE_OCSP + (void)wolfSSL_CertManagerEnableOCSP(cm, 0); +#endif + wolfSSL_CertManagerFree(cm); + } + } + + /* And the chain/buffer loaders, which have their own allocation and + * error-propagation chains distinct from the file loaders above. */ + (void)wolfSSL_CTX_use_certificate_chain_file(ctx, svrCertFile); + wolfSSL_CTX_free(ctx); } From 1b2072608fce548fea0cd4a78923e170dc43072e Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 14:30:53 +0200 Subject: [PATCH 43/60] tests: white-box the certificate error classification in internal.c Error propagation is the largest uncovered category in internal.c, and most of it needs the failing value produced by something upstream. These two functions are the exception: they classify an error handed to them, so the failing value is an argument and every arm is reachable by passing the code that arm names. DoCertFatalAlert maps a verification failure onto the alert the peer is sent. A handshake produces one failure at a time and most of them not at all -- an expired certificate, then a path-length-invalid one, then a revoked one, each needing its own chain -- so the arms are mutually exclusive per run and never pair. The mapping is security-relevant: it decides what a rejected peer learns about why. Swept over every code it discriminates on, two it does not, and both tls1_3 settings, since NO_PEER_CERT branches again on that. ProcessPeerCertCheckKey enforces the per-algorithm minimum key size. The minimums are configuration, fixed for the life of a connection, and the negative sentinel is never set by a working one, so both operands of each guard are constant in any real run. Swept over each key OID the switch names plus one it does not, four minimums including the sentinel, and verifyNone both ways. 75 vectors, no fixture, no fault injection, no certificate chain -- a zeroed WOLFSSL and a DecodedCert filled in by hand. internal.c 762/1732 -> 780/1732. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + .../test_internal_certerror_whitebox.c | 201 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 tests/unit-mcdc/test_internal_certerror_whitebox.c diff --git a/tests/include.am b/tests/include.am index 660fbdca9c5..064edac6874 100644 --- a/tests/include.am +++ b/tests/include.am @@ -167,6 +167,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_integer_whitebox.c \ tests/unit-mcdc/test_kdf_hash_fault_whitebox.c \ tests/unit-mcdc/test_internal_domain_whitebox.c \ + tests/unit-mcdc/test_internal_certerror_whitebox.c \ tests/unit-mcdc/test_internal_clienthello_whitebox.c \ tests/unit-mcdc/test_internal_dhskehash_whitebox.c \ tests/unit-mcdc/test_internal_eddsa_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index e006f8783de..54299c127d7 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -20,6 +20,7 @@ test_hpke_fault_whitebox test_hpke_whitebox test_integer_fault_whitebox test_integer_whitebox +test_internal_certerror_whitebox test_internal_clienthello_whitebox test_internal_dhskehash_whitebox test_internal_domain_whitebox diff --git a/tests/unit-mcdc/test_internal_certerror_whitebox.c b/tests/unit-mcdc/test_internal_certerror_whitebox.c new file mode 100644 index 00000000000..fe3898645ae --- /dev/null +++ b/tests/unit-mcdc/test_internal_certerror_whitebox.c @@ -0,0 +1,201 @@ +/* test_internal_certerror_whitebox.c -- MC/DC white-box driver for the + * certificate error-classification decisions in src/internal.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-1301, USA + */ + +/* Error propagation is the largest uncovered category in internal.c, and most + * of it needs the failing value to be produced by something upstream. These + * two functions are the exception: they CLASSIFY an error that is handed to + * them, so the failing value is an argument, and every arm is reachable by + * calling them with the code that arm names. + * + * DoCertFatalAlert maps a verification failure onto the alert a peer is sent. + * A handshake produces one failure at a time and most of them not at all -- + * a test would need an expired certificate, then a path-length-invalid one, + * then a revoked one, each with its own chain -- so the arms are mutually + * exclusive per run and never pair. Passing the codes directly covers the + * whole map, and the mapping is security-relevant: it decides what a peer + * learns about why it was rejected. + * + * ProcessPeerCertCheckKey enforces the minimum key size per algorithm. The + * minimums are configuration fixed for the life of a connection and the + * negative sentinel is never set by a working configuration, so both operands + * of each guard are constant in any real run. + * + * Neither needs fault injection, a certificate chain, or a peer -- only a + * zeroed WOLFSSL and, for the key check, a DecodedCert filled in by hand. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(NO_CERTS) + +static int g_checks; + +/* ------------------------------------------------------ DoCertFatalAlert */ + +static void wb_cert_fatal_alert(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + /* Every code the function discriminates on, plus two it does not, so the + * default arm has its own vector. */ + static const int kCodes[] = { + 0, + ASN_AFTER_DATE_E, ASN_BEFORE_DATE_E, + ASN_NO_SIGNER_E, ASN_PATHLEN_INV_E, ASN_PATHLEN_SIZE_E, +#ifdef HAVE_RPK + RPK_UNTRUSTED_E, UNSUPPORTED_CERTIFICATE, +#endif +#ifdef OPENSSL_EXTRA + CRL_CERT_REVOKED, +#endif + NO_PEER_CERT, + ASN_SIG_CONFIRM_E, BUFFER_E, MEMORY_E + }; + size_t i; + int tls13; + + /* ssl == NULL is the first operand; ret == 0 the second. */ + DoCertFatalAlert(NULL, ASN_NO_SIGNER_E); + g_checks++; + + /* NO_PEER_CERT branches again on tls1_3, so both settings are swept. */ + for (tls13 = 0; tls13 < 2; tls13++) { + for (i = 0; i < sizeof(kCodes) / sizeof(kCodes[0]); i++) { + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = tls13 ? TLSv1_3_MINOR : TLSv1_2_MINOR; + ssl->options.side = WOLFSSL_CLIENT_END; + ssl->options.tls1_3 = (byte)tls13; + /* no CBIOSend: DoCertFatalAlert records the alert reason on the + * ssl rather than writing it, so nothing is transmitted here */ + DoCertFatalAlert(ssl, kCodes[i]); + g_checks++; + } + } +} + +/* ----------------------------------------------- ProcessPeerCertCheckKey */ + +static void wb_check_key_size(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + /* One entry per key algorithm the switch names, so each arm is entered. */ + static const int kOids[] = { +#ifndef NO_RSA + RSAk, + #ifdef WC_RSA_PSS + RSAPSSk, + #endif +#endif +#ifdef HAVE_ECC + ECDSAk, +#endif +#ifdef HAVE_ED25519 + ED25519k, +#endif +#ifdef HAVE_ED448 + ED448k, +#endif + 0 /* an OID the switch does not name: the default arm */ + }; + /* The size relative to the configured minimum, and the negative sentinel + * that a working configuration never sets. */ + static const int kMins[] = { -1, 0, 1024, 4096 }; + ProcPeerCertArgs args; + DecodedCert dCert; + size_t o, m; + int verifyNone; + + for (verifyNone = 0; verifyNone < 2; verifyNone++) { + for (o = 0; o < sizeof(kOids) / sizeof(kOids[0]); o++) { + for (m = 0; m < sizeof(kMins) / sizeof(kMins[0]); m++) { + XMEMSET(ssl, 0, sizeof(*ssl)); + XMEMSET(&args, 0, sizeof(args)); + XMEMSET(&dCert, 0, sizeof(dCert)); + ssl->ctx = ctx; + ssl->options.verifyNone = (byte)verifyNone; + /* every minimum set together: the switch picks one arm, and + * the arm it picks reads its own field */ + ssl->options.minRsaKeySz = kMins[m]; + ssl->options.minEccKeySz = kMins[m]; + dCert.keyOID = kOids[o]; + dCert.pubKeySize = 2048; + args.dCert = &dCert; + (void)ProcessPeerCertCheckKey(ssl, &args); + g_checks++; + } + } + } +} + +/* ---------------------------------------------------------------- main */ + +int main(void) +{ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal certerror white-box: wolfSSL_Init failed\n"); + goto done; + } + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) { + printf("internal certerror white-box: CTX_new failed\n"); + goto done; + } + ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + if (ssl == NULL) { + printf("internal certerror white-box: out of memory\n"); + goto done; + } + + wb_cert_fatal_alert(ssl, ctx); + wb_check_key_size(ssl, ctx); + + printf("internal certerror white-box: %d vectors driven\n", g_checks); + +done: + XFREE(ssl, NULL, DYNAMIC_TYPE_SSL); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal certerror white-box: skipped (TLS/certs not built)\n"); + return 0; +} + +#endif From 7e8c3333f3745faf5085397fbe60c17b25d4dfc5 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 15:16:42 +0200 Subject: [PATCH 44/60] tests: white-box the certificate-status and send-path error decisions Three more error-classification clusters, none needing a peer. CsrDoStatusVerifyCb lets an application override the library's OCSP verdict, and the interesting arms are the disagreements: the callback forcing an error on a good status, and clearing one on a bad status. No in-tree test installs a callback that disagrees, so neither arm had been taken. A mock callback returning a chosen value against a chosen incoming result sweeps the matrix, including the invalid positive return. DoCertificateStatus compares the declared status length against the record size; a conforming peer always makes them agree, so the mismatch arms need bytes no real peer sends. Driven with crafted input, no fixture. SendData's opening guards ask whether the connection is resuming from a blocked write, which a test that writes successfully never sets up. The oversized-length guard above them returns before any IO happens. internal.c 780/1732 -> 784/1732. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + .../unit-mcdc/test_internal_status_whitebox.c | 236 ++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 tests/unit-mcdc/test_internal_status_whitebox.c diff --git a/tests/include.am b/tests/include.am index 064edac6874..0298503d3b7 100644 --- a/tests/include.am +++ b/tests/include.am @@ -175,6 +175,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_internal_peerkey_whitebox.c \ tests/unit-mcdc/test_keys_whitebox.c \ tests/unit-mcdc/test_internal_record_whitebox.c \ + tests/unit-mcdc/test_internal_status_whitebox.c \ tests/unit-mcdc/test_internal_sanity_whitebox.c \ tests/unit-mcdc/test_internal_suites_whitebox.c \ tests/unit-mcdc/test_kdf_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index 54299c127d7..b99d1c2ec35 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -29,6 +29,7 @@ test_internal_nullguard_whitebox test_internal_peerkey_whitebox test_internal_record_whitebox test_internal_sanity_whitebox +test_internal_status_whitebox test_internal_suites_whitebox test_keys_whitebox test_lms_bds_whitebox diff --git a/tests/unit-mcdc/test_internal_status_whitebox.c b/tests/unit-mcdc/test_internal_status_whitebox.c new file mode 100644 index 00000000000..ea8c1bd10e1 --- /dev/null +++ b/tests/unit-mcdc/test_internal_status_whitebox.c @@ -0,0 +1,236 @@ +/* test_internal_status_whitebox.c -- MC/DC white-box driver for the + * certificate-status and send-path error decisions in src/internal.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-1301, USA + */ + +/* Three more error-classification clusters, none of which needs a peer. + * + * CsrDoStatusVerifyCb lets an application override the library's OCSP verdict. + * Its decisions compare the library's result against the callback's, and the + * interesting combinations are precisely the disagreements: the callback + * forcing an error on a good status, and the callback clearing an error on a + * bad one. No in-tree test installs a callback that disagrees, so those arms + * have never been taken. A mock callback returning a chosen value against a + * chosen incoming result sweeps the whole matrix. + * + * DoCertificateStatus parses a CertificateStatus message off the wire. Its + * length guards compare the declared status length against the record size, + * and a conforming peer always makes them agree -- so the mismatch arms need + * bytes a real peer never sends. Crafted input, no fixture. + * + * SendData's opening guards ask whether the connection is resuming from a + * blocked write. A test that writes successfully never sets ssl->error to + * WANT_WRITE first, so that operand is constant; setting it directly pairs it. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(NO_CERTS) + +static int g_checks; + +static int wb_send_sink(WOLFSSL* ssl, char* buf, int sz, void* ctx) +{ + (void)ssl; (void)buf; (void)ctx; + return sz; /* swallow output, report it fully written */ +} + +/* ------------------------------------------------- CsrDoStatusVerifyCb */ + +#if !defined(NO_WOLFSSL_SERVER) && \ + (defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2)) + +static int g_verRet; /* what the mock callback returns */ + +static int wb_status_verify_cb(WOLFSSL* ssl, int err, byte* resp, + word32 respSz, word32 idx, void* arg) +{ + (void)ssl; (void)err; (void)resp; (void)respSz; (void)idx; (void)arg; + return g_verRet; +} + +static void wb_csr_status_verify(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + /* The library's verdict, and the callback's, swept against each other. + * (0, <0) is "callback forces an error"; (<0, 0) is "callback overrides + * the error"; >0 is the invalid-return arm. */ + static const int kLibRet[] = { 0, -1, ASN_NO_SIGNER_E }; + static const int kCbRet[] = { 0, -1, 1, 42 }; + byte input[32]; + size_t a, b; + int installed; + + XMEMSET(input, 0, sizeof(input)); + + for (installed = 0; installed < 2; installed++) { + for (a = 0; a < sizeof(kLibRet) / sizeof(kLibRet[0]); a++) { + for (b = 0; b < sizeof(kCbRet) / sizeof(kCbRet[0]); b++) { + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + /* the `callback != NULL` operand: a build that never + * installs one keeps it false forever */ + ctx->ocspStatusVerifyCb = installed ? wb_status_verify_cb + : NULL; + ctx->ocspStatusVerifyCbArg = NULL; + g_verRet = kCbRet[b]; + (void)CsrDoStatusVerifyCb(ssl, input, (word32)sizeof(input), + 0, kLibRet[a]); + g_checks++; + } + } + } + ctx->ocspStatusVerifyCb = NULL; +} +#endif + +/* ------------------------------------------------- DoCertificateStatus */ + +static void wb_do_certificate_status(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + /* A CertificateStatus body is: status_type(1) status_length(3) data. + * The guards compare `size` against those fields, so the vectors are + * (declared length, actual size) pairs that agree and disagree. */ + static const struct { byte type; word32 declared; word32 size; + const char* what; } rows[] = { + { 1, 4, 4 + 1 + 3, "consistent, type ocsp" }, + { 2, 4, 4 + 1 + 3, "consistent, type ocsp_multi" }, + { 0, 4, 4 + 1 + 3, "consistent, type none" }, + { 9, 4, 4 + 1 + 3, "consistent, unknown type" }, + { 1, 4, 3, "size below the fixed header" }, + { 1, 0, 1 + 3, "zero-length status" }, + { 1, 64, 1 + 3 + 4, "declared longer than the record" }, + { 1, 1, 1 + 3 + 4, "declared shorter than the record" }, + }; + byte input[128]; + size_t i; + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + word32 idx = 0; + + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = TLSv1_2_MINOR; + ssl->options.side = WOLFSSL_CLIENT_END; + ssl->CBIOSend = wb_send_sink; + + XMEMSET(input, 0, sizeof(input)); + input[0] = rows[i].type; + c32to24(rows[i].declared, input + 1); + + (void)DoCertificateStatus(ssl, input, &idx, rows[i].size); + g_checks++; + } +} + +/* ------------------------------------------------------------- SendData */ + +static void wb_send_data(WOLFSSL* ssl, WOLFSSL_CTX* ctx) +{ + /* The opening guard asks whether ssl->error holds a blocked-write code + * (or, under async, a pending-crypto one), i.e. whether this call resumes + * a write that could not complete. A test that writes successfully never + * leaves that state behind, so the operand is constant. The + * oversized-length guard above it returns before any IO happens at all. */ + static const int kErrors[] = { 0, WANT_WRITE, WC_PENDING_E, SOCKET_ERROR_E }; + static const char payload[] = "hello"; + size_t i; + + for (i = 0; i < sizeof(kErrors) / sizeof(kErrors[0]); i++) { + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = TLSv1_2_MINOR; + ssl->options.side = WOLFSSL_CLIENT_END; + ssl->CBIOSend = wb_send_sink; + ssl->error = kErrors[i]; + (void)SendData(ssl, payload, sizeof(payload) - 1); + g_checks++; + } + + /* `sz > INT_MAX`: a length no caller passes, and the only guard that + * returns before the connection state is even consulted. */ + XMEMSET(ssl, 0, sizeof(*ssl)); + ssl->ctx = ctx; + ssl->CBIOSend = wb_send_sink; + (void)SendData(ssl, payload, (size_t)INT_MAX + 1); + g_checks++; +} + +/* ---------------------------------------------------------------- main */ + +int main(void) +{ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal status white-box: wolfSSL_Init failed\n"); + goto done; + } + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) { + printf("internal status white-box: CTX_new failed\n"); + goto done; + } + ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + if (ssl == NULL) { + printf("internal status white-box: out of memory\n"); + goto done; + } + +#if !defined(NO_WOLFSSL_SERVER) && \ + (defined(HAVE_CERTIFICATE_STATUS_REQUEST) || \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2)) + wb_csr_status_verify(ssl, ctx); +#endif + wb_do_certificate_status(ssl, ctx); + wb_send_data(ssl, ctx); + + printf("internal status white-box: %d vectors driven\n", g_checks); + +done: + XFREE(ssl, NULL, DYNAMIC_TYPE_SSL); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal status white-box: skipped (TLS/certs not built)\n"); + return 0; +} + +#endif From 70a36048144171805d2fd152a5832c3508bd5aa7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 15:24:34 +0200 Subject: [PATCH 45/60] tests: fake the revocation back ends to drive the leaf-revocation decisions ProcessPeerCertLeafRevocation decides what a revocation answer MEANS, and its guards discriminate between specific codes: an explicit assertion that the certificate is revoked, a responder that does not know it, one that could not be reached, a certificate naming no responder, a lookup still in flight, and the CRL equivalents. The difference between them is the difference between failing a handshake and continuing it. Producing them for real needs four separate responder deployments, one per vector, so none of these arms had been taken. This translation unit already #includes internal.c, so the revocation entry points it calls but does not define are redirected to fakes with a #define ahead of the include -- the idiom mcdc_fault_hash.h already uses for wolfcrypt primitives. CheckCertOCSP_ex, CheckCertCRL, CheckCertCRL_ex and OcspNoUrlPolicy live in ocsp.c and crl.c, so this rewrites only the driver's copy and leaves the library untouched. Each fake returns the code the vector chose, which is the point: the answer is the input under test. The answers are only half of it. The guards below them also read ocspEnabled, crlEnabled, crlCheckAll, tls1_3, totalCerts and whether the decoded certificate has a CA; a first version pinned all six while sweeping only the codes and gained 2 conditions. Sweeping them one at a time from both saturated ends, crossed with the codes, gained 13. 7168 vectors. internal.c 786/1732 -> 799/1732. --- tests/include.am | 1 + tests/unit-mcdc/smoke-expected.txt | 1 + .../test_internal_revocation_whitebox.c | 257 ++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 tests/unit-mcdc/test_internal_revocation_whitebox.c diff --git a/tests/include.am b/tests/include.am index 0298503d3b7..eb0cdd442fe 100644 --- a/tests/include.am +++ b/tests/include.am @@ -174,6 +174,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_internal_nullguard_whitebox.c \ tests/unit-mcdc/test_internal_peerkey_whitebox.c \ tests/unit-mcdc/test_keys_whitebox.c \ + tests/unit-mcdc/test_internal_revocation_whitebox.c \ tests/unit-mcdc/test_internal_record_whitebox.c \ tests/unit-mcdc/test_internal_status_whitebox.c \ tests/unit-mcdc/test_internal_sanity_whitebox.c \ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index b99d1c2ec35..f3265051c25 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -28,6 +28,7 @@ test_internal_eddsa_whitebox test_internal_nullguard_whitebox test_internal_peerkey_whitebox test_internal_record_whitebox +test_internal_revocation_whitebox test_internal_sanity_whitebox test_internal_status_whitebox test_internal_suites_whitebox diff --git a/tests/unit-mcdc/test_internal_revocation_whitebox.c b/tests/unit-mcdc/test_internal_revocation_whitebox.c new file mode 100644 index 00000000000..d2d41a7a2c6 --- /dev/null +++ b/tests/unit-mcdc/test_internal_revocation_whitebox.c @@ -0,0 +1,257 @@ +/* test_internal_revocation_whitebox.c -- MC/DC white-box driver for the leaf + * revocation-check decisions in src/internal.c, using faked revocation + * back ends + * + * 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-1301, USA + */ + +/* FAKING THE REVOCATION BACK ENDS. + * + * ProcessPeerCertLeafRevocation decides what a revocation answer MEANS. Its + * guards discriminate between specific codes -- an explicit assertion that the + * certificate is revoked, a responder that does not know it, a responder that + * could not be reached, a certificate naming no responder, a lookup still in + * flight, and the CRL equivalents. The difference between them is the + * difference between failing a handshake and continuing it, which makes these + * among the most security-relevant decisions in the file. + * + * They are unreachable from any test that owns only certificates: producing + * the codes for real needs a responder that answers "unknown", then one that + * times out, then one taken offline, then a certificate with no AIA extension + * -- four separate deployments, one per vector. + * + * Since this translation unit #includes internal.c, the revocation entry + * points it CALLS but does not DEFINE can be redirected to fakes with a + * #define ahead of the include -- the idiom mcdc_fault_hash.h already uses for + * wolfcrypt primitives. CheckCertOCSP_ex, CheckCertCRL and OcspNoUrlPolicy + * live in ocsp.c and crl.c, so redirecting them rewrites only this driver's + * copy of internal.c and leaves the library untouched. + * + * The answers are only half the input: the guards below them also read + * ocspEnabled, crlEnabled, crlCheckAll, tls1_3, totalCerts and whether the + * decoded certificate has a CA. Those are swept one at a time from both + * saturated ends, crossed with the codes -- an earlier version of this driver + * pinned them and left every guard that reads them constant. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(NO_CERTS) && \ + (defined(HAVE_OCSP) || defined(HAVE_CRL)) + +/* ---- the fakes, and the code each is told to return ---------------------- */ + +static int g_ocspRet; /* what the OCSP back end answers */ +static int g_crlRet; /* what the CRL back end answers */ +static int g_noUrlRet; /* what the no-URL policy answers */ +static int g_ocspCalls; +static int g_crlCalls; + +#ifdef HAVE_OCSP +static int mcdc_CheckCertOCSP_ex(WOLFSSL_OCSP* ocsp, DecodedCert* cert, + WOLFSSL* ssl) +{ + (void)ocsp; (void)cert; (void)ssl; + g_ocspCalls++; + return g_ocspRet; +} +static int mcdc_OcspNoUrlPolicy(WOLFSSL_CERT_MANAGER* cm) +{ + (void)cm; + return g_noUrlRet; +} +#endif + +#ifdef HAVE_CRL +static int mcdc_CheckCertCRL(WOLFSSL_CRL* crl, DecodedCert* cert) +{ + (void)crl; (void)cert; + g_crlCalls++; + return g_crlRet; +} +/* The chain walk uses a different entry point, and it dereferences the Signer + * chain it is handed. Faking it as well keeps the chain vectors from walking + * a hand-built Signer, and makes the chain answer selectable like the leaf + * answer. */ +static int mcdc_CheckCertCRL_ex(WOLFSSL_CRL* crl, byte* issuerHash, + byte* serial, int serialSz, byte* serialHash, const byte* extCrlInfo, + int extCrlInfoSz, void* issuerName) +{ + (void)crl; (void)issuerHash; (void)serial; (void)serialSz; + (void)serialHash; (void)extCrlInfo; (void)extCrlInfoSz; (void)issuerName; + g_crlCalls++; + return g_crlRet; +} +#endif + +/* Redirect internal.c's calls to the fakes. These are defined in ocsp.c and + * crl.c, not here, so nothing is redefined -- only this driver's view of what + * internal.c calls. */ +#ifdef HAVE_OCSP + #define CheckCertOCSP_ex(a, b, c) mcdc_CheckCertOCSP_ex((a), (b), (c)) + #define OcspNoUrlPolicy(a) mcdc_OcspNoUrlPolicy((a)) +#endif +#ifdef HAVE_CRL + #define CheckCertCRL(a, b) mcdc_CheckCertCRL((a), (b)) + #define CheckCertCRL_ex(a, b, c, d, e, f, g, h) \ + mcdc_CheckCertCRL_ex((a), (b), (c), (d), (e), (f), (g), (h)) +#endif + +#include + +static int g_checks; + +int main(void) +{ + /* Every code the decisions discriminate on, plus success and one they do + * not name, so the default arm has a vector too. */ + static const int kCodes[] = { + 0, +#ifdef HAVE_OCSP + OCSP_CERT_REVOKED, OCSP_CERT_UNKNOWN, OCSP_LOOKUP_FAIL, OCSP_NO_URL, + #ifdef WOLFSSL_NONBLOCK_OCSP + OCSP_WANT_READ, + #endif +#endif +#ifdef HAVE_CRL + CRL_MISSING, CRL_CERT_REVOKED, +#endif + ASN_NO_SIGNER_E + }; + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + WOLFSSL_CERT_MANAGER* cm = NULL; + ProcPeerCertArgs args; + DecodedCert dCert; + Signer caSigner; + size_t a, b; + int side, statusReq, mustStaple, flagBase, which, k; + + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("internal revocation white-box: wolfSSL_Init failed\n"); + goto done; + } + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) { + printf("internal revocation white-box: CTX_new failed\n"); + goto done; + } + ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + if (ssl == NULL) { + printf("internal revocation white-box: out of memory\n"); + goto done; + } + cm = ctx->cm; + if (cm == NULL) { + printf("internal revocation white-box: no cert manager\n"); + goto done; + } + + for (side = 0; side < 2; side++) { + for (statusReq = 0; statusReq < 2; statusReq++) { + for (mustStaple = 0; mustStaple < 2; mustStaple++) { + for (flagBase = 0; flagBase < 2; flagBase++) { + /* which == -1 is the saturated baseline; 0..5 flip exactly one flag, + * which is the independence pair for the operand that reads it. */ + for (which = -1; which < 6; which++) { + for (a = 0; a < sizeof(kCodes) / sizeof(kCodes[0]); a++) { + for (b = 0; b < sizeof(kCodes) / sizeof(kCodes[0]); b++) { + int pRet = 0; + int f[6]; + + for (k = 0; k < 6; k++) + f[k] = flagBase; + if (which >= 0) + f[which] = !flagBase; + + XMEMSET(ssl, 0, sizeof(*ssl)); + XMEMSET(&args, 0, sizeof(args)); + XMEMSET(&dCert, 0, sizeof(dCert)); + XMEMSET(&caSigner, 0, sizeof(caSigner)); + ssl->ctx = ctx; + ssl->version.major = SSLv3_MAJOR; + ssl->version.minor = f[3] ? TLSv1_3_MINOR : TLSv1_2_MINOR; + ssl->options.side = side ? WOLFSSL_SERVER_END + : WOLFSSL_CLIENT_END; + ssl->options.handShakeDone = (byte)side; + ssl->options.tls1_3 = (byte)f[3]; + ssl->status_request = (byte)statusReq; + args.dCert = &dCert; + /* totalCerts == 1 is one operand of the chain check */ + args.totalCerts = f[4] ? 1 : 2; + /* a decoded cert with and without a CA of its own */ + /* a real zeroed Signer, not a cast of some other + * struct: the callee walks Signer fields. */ + dCert.ca = f[5] ? &caSigner : NULL; + +#ifdef HAVE_OCSP + cm->ocspEnabled = (byte)f[0]; + cm->ocspMustStaple = (byte)mustStaple; +#endif +#ifdef HAVE_CRL + cm->crlEnabled = (byte)f[1]; + cm->crlCheckAll = (byte)f[2]; +#endif + g_ocspRet = kCodes[a]; + g_crlRet = kCodes[b]; + g_noUrlRet = kCodes[b]; + + (void)ProcessPeerCertLeafRevocation(ssl, &args, &pRet); + g_checks++; + } + } + } + } + } + } + } + + printf("internal revocation white-box: %d vectors driven " + "(%d ocsp, %d crl back-end calls)\n", + g_checks, g_ocspCalls, g_crlCalls); + +done: + XFREE(ssl, NULL, DYNAMIC_TYPE_SSL); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal revocation white-box: skipped (needs OCSP or CRL)\n"); + return 0; +} + +#endif From 540e4bf6e00716c2a904d7d57710335744abac7a Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 15:36:20 +0200 Subject: [PATCH 46/60] tests: drive the OCSP cache lookup and mock the responder replies GetOcspStatus walks the cached status list for a matching serial and then decides whether the cached answer is still usable. A cache the parser populated always holds self-consistent entries, so "same length, different serial", "cached with no stored response body" and "cached with a date that no longer validates" are states the library only reaches after time passes or a responder misbehaves. Built by hand: it reads entry->status and writes *status but stores nothing, so stack objects are correct and the entry is never linked into ocsp->ocspList. CheckOcspRequest's remaining guards read what the responder gave back, and a real responder cannot be asked for a positive length with a NULL buffer, nor for the two negative sentinels the caller maps onto WANT_READ and HTTP_TIMEOUT. The mock returns whatever the vector chose, across: a body with and without a free hook, a NULL buffer with a positive length, a zero-length reply, a generic error, no transport installed at all, a URL that is present but empty, and no URL. Each row uses a distinct issuer hash so it misses the cache and actually reaches the transport. ocsp.c 24/47 -> 28/47. Not attempted, and worth recording: CheckOcspResponse's newStatus/newSingle/ ocspResponse NULL checks are WOLFSSL_SMALL_STACK allocations and plain stack arrays otherwise, so in the default variant the decision cannot be true at all and in the small-stack variant it needs the allocation injector, which is fenced until PR 11378 lands. --- tests/unit-mcdc/test_ocsp_whitebox.c | 167 +++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/tests/unit-mcdc/test_ocsp_whitebox.c b/tests/unit-mcdc/test_ocsp_whitebox.c index ea7d4794585..a7c85e1742f 100644 --- a/tests/unit-mcdc/test_ocsp_whitebox.c +++ b/tests/unit-mcdc/test_ocsp_whitebox.c @@ -447,6 +447,171 @@ static void wb_check_request_ioctx(WOLFSSL_CERT_MANAGER* cm) wolfSSL_CTX_free(ctx); } + +/* --------------------------------------------------------- GetOcspStatus + + * The cache lookup: walk this entry's status list for one matching the + * request's serial, then decide whether the cached answer is still usable. + * + * (*status)->serialSz == request->serialSz && !XMEMCMP(serial, ...) + * responseBuffer && *status && !(*status)->rawOcspResponse + * XVALIDATE_DATE(thisDate) && nextDate[0] != 0 && XVALIDATE_DATE(nextDate) + * + * A cache populated by the parser always holds self-consistent entries, so + * "same length, different serial", "cached but with no stored response body" + * and "cached with a date that no longer validates" are states the library + * produces only after time passes or a responder misbehaves. Built by hand + * here. + * + * Ownership: GetOcspStatus READS entry->status and writes *status; it stores + * nothing, so stack objects are correct. The entry is never linked into + * ocsp->ocspList, so teardown cannot reach this frame. */ +static void wb_get_ocsp_status(WOLFSSL_OCSP* ocsp) +{ + OcspRequest req; + OcspEntry entry; + CertStatus st; + CertStatus* found = NULL; + buffer respBuf; + byte serial[8]; + byte raw[4]; + size_t i; + + static const struct { int stSz; byte stByte; int reqSz; byte reqByte; + int haveRaw; int haveBuf; int thisFmt; + int nextSet; const char* what; } rows[] = { + /* serial match/mismatch by length and by content */ + { 8, 0xA1, 8, 0xA1, 1, 1, ASN_UTC_TIME, 1, "match, cached, dated" }, + { 8, 0xA1, 8, 0xB2, 1, 1, ASN_UTC_TIME, 1, "same length, other serial" }, + { 4, 0xA1, 8, 0xA1, 1, 1, ASN_UTC_TIME, 1, "shorter cached serial" }, + { 8, 0xA1, 4, 0xA1, 1, 1, ASN_UTC_TIME, 1, "shorter request serial" }, + /* cached entry with no stored response body: forces a refetch */ + { 8, 0xA1, 8, 0xA1, 0, 1, ASN_UTC_TIME, 1, "no raw response, buffer" }, + { 8, 0xA1, 8, 0xA1, 0, 0, ASN_UTC_TIME, 1, "no raw response, no buffer" }, + { 8, 0xA1, 8, 0xA1, 1, 0, ASN_UTC_TIME, 1, "raw response, no buffer" }, + /* date shapes: unset format, and a nextDate that was never written */ + { 8, 0xA1, 8, 0xA1, 1, 1, 0, 1, "no date format" }, + { 8, 0xA1, 8, 0xA1, 1, 1, ASN_UTC_TIME, 0, "no nextDate" }, + { 8, 0xA1, 8, 0xA1, 1, 1, ASN_GENERALIZED_TIME, 1, "generalized time" }, + }; + + XMEMSET(raw, 0x30, sizeof(raw)); + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + XMEMSET(&req, 0, sizeof(req)); + XMEMSET(&entry, 0, sizeof(entry)); + XMEMSET(&st, 0, sizeof(st)); + XMEMSET(&respBuf, 0, sizeof(respBuf)); + XMEMSET(serial, rows[i].reqByte, sizeof(serial)); + + st.serialSz = rows[i].stSz; + XMEMSET(st.serial, rows[i].stByte, sizeof(st.serial)); + st.rawOcspResponse = rows[i].haveRaw ? raw : NULL; + st.rawOcspResponseSz = rows[i].haveRaw ? (word32)sizeof(raw) : 0; + st.thisDateFormat = (byte)rows[i].thisFmt; + st.nextDateFormat = (byte)rows[i].thisFmt; + if (rows[i].nextSet) + st.nextDate[0] = 0x30; + st.next = NULL; + entry.status = &st; + entry.next = NULL; + + req.serial = serial; + req.serialSz = rows[i].reqSz; + + found = NULL; + WB_NOTE(GetOcspStatus(ocsp, &req, &entry, &found, + rows[i].haveBuf ? &respBuf : NULL, NULL)); + XFREE(respBuf.buffer, NULL, DYNAMIC_TYPE_TMP_BUFFER); + respBuf.buffer = NULL; + } + + /* an entry whose status list is empty: the loop body never runs */ + XMEMSET(&req, 0, sizeof(req)); + XMEMSET(&entry, 0, sizeof(entry)); + req.serial = serial; + req.serialSz = (int)sizeof(serial); + found = NULL; + WB_NOTE(GetOcspStatus(ocsp, &req, &entry, &found, NULL, NULL)); +} + +/* --------------------------------- CheckOcspRequest: responder replies + + * The remaining guards read what the responder callback gave back: + * + * url != NULL && url[0] != '\0' a URL that is present but empty + * requestSz > 0 && ocsp->cm->ocspIOCb no transport installed + * responseSz >= 0 && response a size with no buffer, and back + * response != NULL && ocsp->cm->ocspRespFreeCb no free hook installed + * + * A real responder cannot be asked for "a positive length and a NULL buffer", + * nor for the two negative sentinels the caller maps onto WANT_READ and + * HTTP_TIMEOUT. The mock returns whatever the vector chose, which is the + * whole point. */ +static byte g_replyBody[48]; + +static int wb_reply_io(void* ctx, const char* url, int urlSz, + unsigned char* request, int requestSz, + unsigned char** response) +{ + (void)ctx; (void)url; (void)urlSz; (void)request; (void)requestSz; + if (response != NULL) + *response = g_ioNullResponse ? NULL : g_replyBody; + return g_ioResult; +} + +static void wb_responder_replies(WOLFSSL_CERT_MANAGER* cm) +{ + OcspRequest req; + byte serial[8]; + byte url[] = "http://ocsp.example.com/"; + byte emptyUrl[] = ""; + size_t i; + + static const struct { int result; int nullResp; int io; int freeCb; + int useUrl; const char* what; } rows[] = { + { 16, 0, 1, 1, 1, "a body, both hooks" }, + { 16, 0, 1, 0, 1, "a body, no free hook" }, + { 16, 1, 1, 1, 1, "positive length, NULL buffer" }, + { 0, 0, 1, 1, 1, "zero-length reply" }, + { -1, 0, 1, 1, 1, "generic transport error" }, + { 16, 0, 0, 1, 1, "no transport installed" }, + { 16, 0, 1, 1, 2, "url present but empty" }, + { 16, 0, 1, 1, 0, "no url at all" }, + { WOLFSSL_CBIO_ERR_WANT_READ, 0, 1, 1, 1, "responder would block" }, + { WOLFSSL_CBIO_ERR_TIMEOUT, 0, 1, 1, 1, "responder timed out" }, + }; + + XMEMSET(g_replyBody, 0, sizeof(g_replyBody)); + g_replyBody[0] = 0x30; + g_replyBody[1] = 0x02; + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + XMEMSET(&req, 0, sizeof(req)); + XMEMSET(serial, (byte)(0x40 + i), sizeof(serial)); + req.serial = serial; + req.serialSz = (int)sizeof(serial); + /* a distinct issuer per row, so each row misses the cache and + * actually reaches the transport */ + XMEMSET(req.issuerHash, (byte)(0x70 + i), OCSP_DIGEST_SIZE); + if (rows[i].useUrl == 1) { + req.url = url; req.urlSz = (int)sizeof(url) - 1; + } + else if (rows[i].useUrl == 2) { + req.url = emptyUrl; req.urlSz = 0; + } + + g_ioResult = rows[i].result; + g_ioNullResponse = rows[i].nullResp; + (void)wolfSSL_CertManagerSetOCSP_Cb(cm, + rows[i].io ? wb_reply_io : NULL, + rows[i].freeCb ? wb_ocsp_respfree : NULL, NULL); + + WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL)); + } + (void)wolfSSL_CertManagerSetOCSP_Cb(cm, NULL, NULL, NULL); +} + /* ---------------------------------------------------------- main */ int main(void) @@ -473,6 +638,8 @@ int main(void) wb_check_response(cm); wb_free_ocsp_entry(cm->ocsp); wb_check_request_ioctx(cm); + wb_get_ocsp_status(cm->ocsp); + wb_responder_replies(cm); printf("ocsp white-box: %d vectors driven\n", g_checks); From 8705774eee471f6ec839f2559d622c75dbce49c9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 16:03:03 +0200 Subject: [PATCH 47/60] tests: fail one crypto operation on demand to reach the verify guards The signature-verify wrappers in internal.c all end in the same two-operand guard, and an ordinary handshake pairs neither operand. A good signature gives (F,F). A bad signature also gives (F,F) at that line, because a bad signature is not an error: the wc_*_verify_* call returns 0 and reports the verdict in eccVerifyRes. The first operand is true only when the maths itself breaks. WOLF_CRYPTO_CB is the supported way to be the thing that breaks. mcdc_fault_ cryptocb.h registers a device that answers CRYPTOCB_UNAVAILABLE to everything except the one operation a vector selects, so each wrapper can be driven three ways: the device refuses, the device succeeds with the verdict "no", the device succeeds with the verdict "yes". Dispatch happens after the argument checks but before any key material is touched, so the vectors need a key object carrying a devId and nothing else -- no certificate, no peer, no valid public point. wc_ed448_verify_msg zeroes *res before dispatching and the other two do not, so the device sets the verdict rather than the caller. VerifyRsaSign's recovered-plaintext check uses the #define idiom instead, and the reason is worth keeping. RsaPublicDecrypt routes every operation except verify through the callback, and the path that does dispatch feeds its output back through PKCS#1 unpadding, so a device returning anything but a correctly padded block makes ret negative and the guard is never reached. The middle operand also defends against a positive length arriving with a NULL buffer, which no implementation produces. Redirecting the call reaches all three. internal.c 799/1748 -> 810/1748. One vector failed and is kept for what it shows. EccMakeKey's (ret == 0 && key->dp) looked like the same shape -- let a device claim success without generating a key and dp should still be NULL. It is not: _ecc_make_key_ex calls wc_ecc_set_curve before it consults the device, and set_curve either fails, making ret non-zero, or assigns dp. (T,F) does not exist, so the operand is now an exclusion with the argument written out rather than an open condition. --- tests/unit-mcdc/mcdc_fault_cryptocb.h | 240 +++++++++ tests/unit-mcdc/smoke-expected.txt | 1 + .../test_internal_cryptocb_whitebox.c | 460 ++++++++++++++++++ 3 files changed, 701 insertions(+) create mode 100644 tests/unit-mcdc/mcdc_fault_cryptocb.h create mode 100644 tests/unit-mcdc/test_internal_cryptocb_whitebox.c diff --git a/tests/unit-mcdc/mcdc_fault_cryptocb.h b/tests/unit-mcdc/mcdc_fault_cryptocb.h new file mode 100644 index 00000000000..ef96e70d786 --- /dev/null +++ b/tests/unit-mcdc/mcdc_fault_cryptocb.h @@ -0,0 +1,240 @@ +/* mcdc_fault_cryptocb.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * mcdc_fault_cryptocb.h -- make one crypto operation fail, on demand. + * + * PURPOSE + * ------- + * The protocol engine is written defensively around every crypto call: + * + * ret = wc_ecc_verify_hash(sig, sigSz, hash, hashSz, &verify, key); + * if (ret != 0 || verify != 1) + * return VERIFY_SIGN_ERROR; + * + * A correct build cannot make the first operand true. The maths works, so the + * error arm is dead code to every ordinary test, and the operand stays + * unpaired no matter how many handshakes are run. The other injectors in this + * directory reach that arm by redirecting a symbol with a #define, which + * requires a white-box that #includes the .c under test. + * + * WOLF_CRYPTO_CB reaches the same arm from the outside, and it is the + * supported way to do it: a registered device sees every dispatched operation + * before the software implementation does, and whatever it returns is what the + * caller gets. Returning CRYPTOCB_UNAVAILABLE means "not mine", and the + * software path runs as usual. So a device that answers CRYPTOCB_UNAVAILABLE + * to everything except one selected operation, which it fails, turns any + * ordinary black-box test into a fault-injection vector -- no #define, no + * white-box, and the library is exercised exactly as a real offload device + * would exercise it. + * + * WHAT IT DOES NOT DO + * ------------------- + * Only operations that are actually dispatched through the callback can be + * failed, and dispatch happens only when the key or context carries a devId + * other than INVALID_DEVID. Handing the devId to the object under test is the + * caller's job -- wolfSSL_CTX_SetDevId() for a whole CTX, or the per-key + * wc_*_init_ex(). An operation on a default-devId key runs in software and + * never reaches this device, which is the correct behaviour and is also the + * usual reason a vector that "should" have failed did not. + * + * USE + * --- + * mcdc_cb_reset(); + * mcdc_cb_fail_pk(WC_PK_TYPE_ECDSA_VERIFY, SIG_VERIFY_E); + * wolfSSL_CTX_SetDevId(ctx, MCDC_CB_DEVID); + * ... run the handshake, which now fails at the signature check ... + * ExpectIntEQ(mcdc_cb_hits(), 1); + * mcdc_cb_reset(); + * + * mcdc_cb_after(n) lets the first n matching operations through to software + * before failing the next one, for the handshakes that verify more than one + * signature and where only the second one is the interesting guard. + */ + +#ifndef MCDC_FAULT_CRYPTOCB_H +#define MCDC_FAULT_CRYPTOCB_H + +#include +#include + +#ifdef WOLF_CRYPTO_CB + +#include +#include + +/* Any value that is not INVALID_DEVID. Kept away from the small integers the + * async and PKCS#11 tests use so a stray registration cannot collide. */ +#define MCDC_CB_DEVID 1729 + +typedef struct McdcCbState { + int algoType; /* WC_ALGO_TYPE_* to fail, or -1 for "any" */ + int pkType; /* WC_PK_TYPE_* to fail, or -1 for "any" */ + int failCode; /* what the device returns for a matching operation */ + int after; /* let this many matches through before failing */ + int setRes; /* for a verify op answered 0: what to write to *res */ + int matched; /* matching operations seen, whether failed or not */ + int failed; /* matching operations actually failed */ + int seen; /* every operation the device was offered */ + int armed; /* zero disarms without unregistering the device */ +} McdcCbState; + +static McdcCbState mcdc_cb_state; + +static WC_INLINE void mcdc_cb_reset(void) +{ + XMEMSET(&mcdc_cb_state, 0, sizeof(mcdc_cb_state)); + mcdc_cb_state.algoType = -1; + mcdc_cb_state.pkType = -1; + mcdc_cb_state.failCode = WC_HW_E; + mcdc_cb_state.setRes = -1; +} + +/* Answer a public-key operation: WC_PK_TYPE_ECDSA_VERIFY, _RSA, + * _ED25519_VERIFY, _ED448_VERIFY, _ECDH and so on. A negative code is a device + * that refuses; a code of 0 is a device that claims the operation succeeded + * without doing it, which is how the "succeeded but the answer is no" arm of a + * verify guard is reached. */ +static WC_INLINE void mcdc_cb_answer_pk(int pkType, int code) +{ + mcdc_cb_reset(); + mcdc_cb_state.algoType = WC_ALGO_TYPE_PK; + mcdc_cb_state.pkType = pkType; + mcdc_cb_state.failCode = code; + mcdc_cb_state.setRes = -1; /* leave *res as the caller left it */ + mcdc_cb_state.armed = 1; +} + +static WC_INLINE void mcdc_cb_fail_pk(int pkType, int failCode) +{ + mcdc_cb_answer_pk(pkType, failCode); +} + +/* Verify entry points differ in whether they zero *res before dispatching: + * wc_ed448_verify_msg does, wc_ecc_verify_hash and wc_ed25519_verify_msg do + * not. Setting the verdict from inside the device removes the difference, so a + * vector means the same thing whichever algorithm it names. Pass -1 to leave + * *res untouched. */ +static WC_INLINE void mcdc_cb_verdict(int res) +{ + mcdc_cb_state.setRes = res; +} + +/* Fail a whole class: WC_ALGO_TYPE_HASH, _CIPHER, _RNG, _HMAC, _KDF, _SEED. */ +static WC_INLINE void mcdc_cb_fail_algo(int algoType, int failCode) +{ + mcdc_cb_reset(); + mcdc_cb_state.algoType = algoType; + mcdc_cb_state.failCode = failCode; + mcdc_cb_state.armed = 1; +} + +/* Let the first n matching operations run in software, then fail the next. */ +static WC_INLINE void mcdc_cb_after(int n) +{ + mcdc_cb_state.after = n; +} + +static WC_INLINE void mcdc_cb_disarm(void) +{ + mcdc_cb_state.armed = 0; +} + +static WC_INLINE int mcdc_cb_hits(void) { return mcdc_cb_state.failed; } +static WC_INLINE int mcdc_cb_matched(void) { return mcdc_cb_state.matched; } +static WC_INLINE int mcdc_cb_seen(void) { return mcdc_cb_state.seen; } + +static WC_INLINE int mcdc_cb_callback(int devId, wc_CryptoInfo* info, void* ctx) +{ + McdcCbState* st = (McdcCbState*)ctx; + + (void)devId; + + if (st == NULL || info == NULL) + return CRYPTOCB_UNAVAILABLE; + + st->seen++; + + if (!st->armed) + return CRYPTOCB_UNAVAILABLE; + + if (st->algoType >= 0 && info->algo_type != st->algoType) + return CRYPTOCB_UNAVAILABLE; + + /* info->pk.type is only meaningful for WC_ALGO_TYPE_PK; reading it for a + * hash or cipher would be reading the wrong union arm. */ + if (st->pkType >= 0) { + if (info->algo_type != WC_ALGO_TYPE_PK) + return CRYPTOCB_UNAVAILABLE; + if (info->pk.type != st->pkType) + return CRYPTOCB_UNAVAILABLE; + } + + st->matched++; + + if (st->matched <= st->after) + return CRYPTOCB_UNAVAILABLE; /* software runs this one */ + + /* A device that answers a verify has to report the verdict somewhere; the + * caller reads *res, not the return value. Only touched when the vector + * asked for it, and only for the three verify types that carry a res. */ + if (st->setRes >= 0 && info->algo_type == WC_ALGO_TYPE_PK) { + int* res = NULL; + + switch (info->pk.type) { + #ifdef HAVE_ECC + case WC_PK_TYPE_ECDSA_VERIFY: res = info->pk.eccverify.res; break; + #endif + #ifdef HAVE_ED25519 + case WC_PK_TYPE_ED25519_VERIFY: + res = info->pk.ed25519verify.res; + break; + #endif + #ifdef HAVE_ED448 + case WC_PK_TYPE_ED448_VERIFY: res = info->pk.ed448verify.res; break; + #endif + default: break; + } + if (res != NULL) + *res = st->setRes; + } + + st->failed++; + return st->failCode; +} + +/* Registration is global and idempotent; the state is reset separately so a + * vector can re-arm without re-registering. */ +static WC_INLINE int mcdc_cb_install(void) +{ + mcdc_cb_reset(); + return wc_CryptoCb_RegisterDevice(MCDC_CB_DEVID, mcdc_cb_callback, + &mcdc_cb_state); +} + +static WC_INLINE void mcdc_cb_uninstall(void) +{ + wc_CryptoCb_UnRegisterDevice(MCDC_CB_DEVID); + mcdc_cb_reset(); +} + +#endif /* WOLF_CRYPTO_CB */ +#endif /* MCDC_FAULT_CRYPTOCB_H */ diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index f3265051c25..c84553b8dc1 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -22,6 +22,7 @@ test_integer_fault_whitebox test_integer_whitebox test_internal_certerror_whitebox test_internal_clienthello_whitebox +test_internal_cryptocb_whitebox test_internal_dhskehash_whitebox test_internal_domain_whitebox test_internal_eddsa_whitebox diff --git a/tests/unit-mcdc/test_internal_cryptocb_whitebox.c b/tests/unit-mcdc/test_internal_cryptocb_whitebox.c new file mode 100644 index 00000000000..604087fc519 --- /dev/null +++ b/tests/unit-mcdc/test_internal_cryptocb_whitebox.c @@ -0,0 +1,460 @@ +/* test_internal_cryptocb_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-1301, USA + */ + +/* The signature-verify wrappers in internal.c all end in the same guard: + * + * if (ret != 0 || ssl->eccVerifyRes == 0) EccVerify + * ret = (ret != 0 || ssl->eccVerifyRes == 0) ? VERIFY_SIGN_ERROR : 0; + * Ed25519Verify, Ed448Verify + * + * Two operands, and an ordinary handshake pairs neither. A good signature + * gives (F,F); a bad one gives... still (F,F) at this line, because a bad + * signature is not an error -- wc_*_verify_* returns 0 and reports the verdict + * in eccVerifyRes, which the black-box tests cannot set to 0 without a + * genuinely forged signature, and cannot make the call itself fail at all. + * The first operand is true only when the maths breaks: out of memory in the + * bignum layer, or an offload device that refuses. + * + * WOLF_CRYPTO_CB is the supported way to be that device. mcdc_fault_cryptocb.h + * registers one that answers CRYPTOCB_UNAVAILABLE to everything except the + * operation a vector selects, so each wrapper can be driven three ways: + * + * device returns an error -> (T,-) first operand, second short-circuited + * device returns 0, res 0 -> (F,T) second operand: "verify said no" + * device returns 0, res 1 -> (F,F) the good path + * + * The dispatch in wc_ecc_verify_hash / wc_ed25519_verify_msg / + * wc_ed448_verify_msg happens after the argument checks but before any key + * material is touched, so these vectors need a key object with a devId and + * nothing else -- no certificate, no peer, no valid public point. That is the + * whole reason this is cheap: the device intercepts before the maths. + * + * EccMakeKey gets the same treatment for a different guard, and it is the one + * that did not work -- recorded here because the reason is the useful part. + * Its + * + * if (ret == 0 && key->dp) + * + * looked like the same shape: let a device claim the key was generated without + * generating anything, and dp should still be NULL. It is not. _ecc_make_key_ex + * calls wc_ecc_set_curve() before it consults the device, and set_curve either + * fails -- making ret non-zero -- or assigns dp from the curve table. There is + * no path that returns 0 with dp unset, so (T,F) does not exist and the second + * operand has no independence pair in any configuration. The vectors below are + * kept because they exercise the offload dispatch and because they are the + * evidence for that claim; the operand itself is now an exclusion with the + * argument written out. + * + * VerifyRsaSign's NULL checks are here too. They are not fault injection -- + * they are a WOLFSSL_LOCAL entry point whose arguments are always non-NULL by + * construction in the protocol code, so only a direct call reaches them. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + */ + +#include + +#include "tests/unit-mcdc/mcdc_fault_cryptocb.h" + +/* VerifyRsaSign checks the RECOVERED plaintext against the expected one: + * + * if (ret > 0) { + * if (ret != (int)plainSz || !out || XMEMCMP(plain, out, plainSz) != 0) + * + * A device cannot drive this. RsaPublicDecrypt routes every operation except + * verify through the callback ("Everything except verify goes to crypto cb"), + * and the one path that does dispatch feeds its output back through PKCS#1 + * unpadding, so a device that returns anything but a correctly padded block + * makes ret negative and the guard is never reached at all. Reaching it needs + * a positive ret with a chosen length and a chosen buffer -- including a NULL + * buffer, which no real implementation ever produces alongside a positive + * length, and which is exactly what the middle operand is defending against. + * + * So this one uses the #define idiom instead: a white-box that #includes the + * .c under test can redirect any function that .c calls but does not define. + * Two levers on one guard, each where it fits. */ +/* One condition decides both the redirect and the body that defines what the + * redirect points at. They were separate once: the #define sat outside the + * feature guard and the mock inside it, so a build without WOLF_CRYPTO_CB + * redirected internal.c's calls to a function that had been compiled away. + * That fails at link, and the smoke harness files a link failure under "not + * built here" -- indistinguishable from a driver legitimately skipped for its + * configuration. Keep them on one macro. */ +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && defined(WOLF_CRYPTO_CB) && \ + !defined(HAVE_PK_CALLBACKS) && !defined(WOLFSSL_ASYNC_CRYPT) + #define MCDC_CRYPTOCB_WB +#endif + +#if defined(MCDC_CRYPTOCB_WB) && !defined(NO_RSA) + static int mcdc_rsa_verify_inline(byte* in, word32 inLen, byte** out, + RsaKey* key); + #define wc_RsaSSL_VerifyInline(a, b, c, d) \ + mcdc_rsa_verify_inline((a), (b), (c), (d)) +#endif + +#include + +#include +#include + +#ifdef MCDC_CRYPTOCB_WB + +static int g_checks; + +/* A digest length the verify entry points accept: inside + * [WC_MIN_DIGEST_SIZE_FOR_VERIFY, WC_MAX_DIGEST_SIZE], so the length check + * above the callback dispatch lets the vector through. */ +static byte g_sig[72]; +static byte g_hash[32]; + +/* The three answers a device can give, named for what they mean at the guard + * rather than for what the device does. */ +enum { + WB_DEV_ERROR, /* refuses: ret != 0 */ + WB_DEV_SAYS_NO, /* succeeds, verdict is "bad signature" */ + WB_DEV_SAYS_YES /* succeeds, verdict is "good signature" */ +}; + +static const char* wb_answer_name(int answer) +{ + switch (answer) { + case WB_DEV_ERROR: return "device error"; + case WB_DEV_SAYS_NO: return "verify says no"; + default: return "verify says yes"; + } +} + +/* Arm the device for one vector. eccVerifyRes is the out-parameter the + * wrappers read, and the device deliberately does not write it: leaving it as + * the caller set it is what separates SAYS_NO from SAYS_YES. */ +static void wb_arm(WOLFSSL* ssl, int pkType, int answer) +{ + mcdc_cb_answer_pk(pkType, answer == WB_DEV_ERROR ? WC_HW_E : 0); + /* wc_ed448_verify_msg zeroes *res before dispatching and the other two do + * not, so the device sets the verdict rather than the caller. */ + mcdc_cb_verdict(answer == WB_DEV_SAYS_YES ? 1 : 0); + ssl->eccVerifyRes = (answer == WB_DEV_SAYS_YES) ? 1 : 0; +} + +/* ------------------------------------------------------------- EccVerify */ + +#ifdef HAVE_ECC +static void wb_ecc_verify(WOLFSSL* ssl) +{ + static const int kAnswers[] = { WB_DEV_ERROR, WB_DEV_SAYS_NO, + WB_DEV_SAYS_YES }; + size_t i; + + for (i = 0; i < sizeof(kAnswers) / sizeof(kAnswers[0]); i++) { + ecc_key key; + int ret; + + if (wc_ecc_init_ex(&key, NULL, MCDC_CB_DEVID) != 0) + continue; + + wb_arm(ssl, WC_PK_TYPE_ECDSA_VERIFY, kAnswers[i]); + ret = EccVerify(ssl, g_sig, (word32)sizeof(g_sig), + g_hash, (word32)sizeof(g_hash), &key, NULL); + printf(" EccVerify %-16s -> %d (res %d, device hit %d)\n", + wb_answer_name(kAnswers[i]), ret, ssl->eccVerifyRes, + mcdc_cb_matched()); + g_checks++; + + wc_ecc_free(&key); + } + + /* The same guard once more with the device silent, so the wrapper is also + * seen taking the software path it takes in every other test. */ + mcdc_cb_disarm(); +} + +/* ------------------------------------------------------------ EccMakeKey */ + +static void wb_ecc_make_key(WOLFSSL* ssl, WC_RNG* rng) +{ + ecc_key key; + + /* _ecc_make_key_ex rejects a NULL rng before it reaches the callback + * dispatch, so these vectors need a real one on the WOLFSSL. */ + ssl->rng = rng; + + /* Device claims the key was generated but generates nothing. dp is set + * anyway, by the wc_ecc_set_curve() that ran before the dispatch -- this + * is the vector that demonstrates the operand is unpairable. */ + if (wc_ecc_init_ex(&key, NULL, MCDC_CB_DEVID) == 0) { + int ret; + + mcdc_cb_answer_pk(WC_PK_TYPE_EC_KEYGEN, 0); + ret = EccMakeKey(ssl, &key, NULL); + printf(" EccMakeKey empty success -> %d (dp %s)\n", + ret, key.dp == NULL ? "NULL" : "set"); + g_checks++; + wc_ecc_free(&key); + } + + /* And the refusing device, for the ret operand. */ + if (wc_ecc_init_ex(&key, NULL, MCDC_CB_DEVID) == 0) { + int ret; + + mcdc_cb_answer_pk(WC_PK_TYPE_EC_KEYGEN, WC_HW_E); + ret = EccMakeKey(ssl, &key, NULL); + printf(" EccMakeKey device error -> %d\n", ret); + g_checks++; + wc_ecc_free(&key); + } + + mcdc_cb_disarm(); + ssl->rng = NULL; +} +#endif /* HAVE_ECC */ + +/* --------------------------------------------------------- Ed25519Verify */ + +#if defined(HAVE_ED25519) && defined(HAVE_ED25519_VERIFY) +static void wb_ed25519_verify(WOLFSSL* ssl) +{ + static const int kAnswers[] = { WB_DEV_ERROR, WB_DEV_SAYS_NO, + WB_DEV_SAYS_YES }; + size_t i; + + for (i = 0; i < sizeof(kAnswers) / sizeof(kAnswers[0]); i++) { + ed25519_key key; + int ret; + + if (wc_ed25519_init_ex(&key, NULL, MCDC_CB_DEVID) != 0) + continue; + + wb_arm(ssl, WC_PK_TYPE_ED25519_VERIFY, kAnswers[i]); + ret = Ed25519Verify(ssl, g_sig, ED25519_SIG_SIZE, + g_hash, (word32)sizeof(g_hash), &key, NULL); + printf(" Ed25519Verify %-16s -> %d (res %d)\n", + wb_answer_name(kAnswers[i]), ret, ssl->eccVerifyRes); + g_checks++; + + wc_ed25519_free(&key); + } + + mcdc_cb_disarm(); +} +#endif + +/* ----------------------------------------------------------- Ed448Verify */ + +#if defined(HAVE_ED448) && defined(HAVE_ED448_VERIFY) +static void wb_ed448_verify(WOLFSSL* ssl) +{ + static const int kAnswers[] = { WB_DEV_ERROR, WB_DEV_SAYS_NO, + WB_DEV_SAYS_YES }; + static byte sig448[ED448_SIG_SIZE]; + size_t i; + + for (i = 0; i < sizeof(kAnswers) / sizeof(kAnswers[0]); i++) { + ed448_key key; + int ret; + + if (wc_ed448_init_ex(&key, NULL, MCDC_CB_DEVID) != 0) + continue; + + wb_arm(ssl, WC_PK_TYPE_ED448_VERIFY, kAnswers[i]); + ret = Ed448Verify(ssl, sig448, (word32)sizeof(sig448), + g_hash, (word32)sizeof(g_hash), &key, NULL); + printf(" Ed448Verify %-16s -> %d (res %d)\n", + wb_answer_name(kAnswers[i]), ret, ssl->eccVerifyRes); + g_checks++; + + wc_ed448_free(&key); + } + + mcdc_cb_disarm(); +} +#endif + +/* ---------------------------------------------------------- VerifyRsaSign */ + +#ifndef NO_RSA + +/* What the next redirected wc_RsaSSL_VerifyInline reports. */ +static int g_rsaRet; +static byte* g_rsaOut; + +static int mcdc_rsa_verify_inline(byte* in, word32 inLen, byte** out, + RsaKey* key) +{ + (void)in; (void)inLen; (void)key; + + if (out != NULL) + *out = g_rsaOut; + return g_rsaRet; +} + +/* The recovered-plaintext comparison, one vector per operand. */ +static void wb_rsa_recovered_plain(WOLFSSL* ssl) +{ + static byte plain[32]; + static byte recovered[32]; + static byte wrong[32]; + int ret; + size_t i; + + static const struct { + const char* name; + int retVal; /* what the verify reports as the length */ + int useOut; /* 0 -> NULL buffer, 1 -> recovered, 2 -> wrong */ + } kRows[] = { + /* length disagrees: first operand true, rest short-circuited */ + { "short length", (int)sizeof(plain) - 1, 1 }, + /* length agrees but no buffer came back: second operand */ + { "NULL plaintext", (int)sizeof(plain), 0 }, + /* length and buffer agree, content does not: third operand */ + { "wrong content", (int)sizeof(plain), 2 }, + /* everything agrees: the accepting path, all three false */ + { "match", (int)sizeof(plain), 1 }, + /* not a positive return at all, so the guard is skipped */ + { "verify failed", -1, 1 } + }; + + XMEMSET(plain, 0x33, sizeof(plain)); + XMEMSET(recovered, 0x33, sizeof(recovered)); + XMEMSET(wrong, 0x44, sizeof(wrong)); + + for (i = 0; i < sizeof(kRows) / sizeof(kRows[0]); i++) { + g_rsaRet = kRows[i].retVal; + g_rsaOut = (kRows[i].useOut == 0) ? NULL : + (kRows[i].useOut == 1) ? recovered : wrong; + + ret = VerifyRsaSign(ssl, g_sig, (word32)sizeof(g_sig), + plain, (word32)sizeof(plain), 0, 0, NULL, NULL); + printf(" VerifyRsaSign %-15s -> %d\n", kRows[i].name, ret); + g_checks++; + } + + g_rsaRet = 0; + g_rsaOut = NULL; +} + +static void wb_rsa_null_args(WOLFSSL* ssl) +{ + static byte plain[32]; + int ret; + + /* First operand: signature buffer NULL, plain valid. */ + ret = VerifyRsaSign(ssl, NULL, (word32)sizeof(g_sig), + plain, (word32)sizeof(plain), 0, 0, NULL, NULL); + printf(" VerifyRsaSign sig NULL -> %d\n", ret); + g_checks++; + + /* Second operand: signature valid, plain NULL. The first operand must be + * false for this one to be evaluated at all. */ + ret = VerifyRsaSign(ssl, g_sig, (word32)sizeof(g_sig), + NULL, (word32)sizeof(plain), 0, 0, NULL, NULL); + printf(" VerifyRsaSign plain NULL -> %d\n", ret); + g_checks++; + + /* Both non-NULL, but the signature is longer than the engine accepts, so + * the size guard below the NULL checks is reached with (F,F) above it. */ + ret = VerifyRsaSign(ssl, g_sig, ENCRYPT_LEN + 1, + plain, (word32)sizeof(plain), 0, 0, NULL, NULL); + printf(" VerifyRsaSign oversize -> %d\n", ret); + g_checks++; +} +#endif + +int main(void) +{ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; +#ifdef HAVE_ECC + WC_RNG rng; + int rngReady = 0; +#endif + + wolfSSL_Init(); + + XMEMSET(g_sig, 0xA5, sizeof(g_sig)); + XMEMSET(g_hash, 0x5A, sizeof(g_hash)); + + if (mcdc_cb_install() != 0) { + printf("internal cryptocb white-box: device registration refused\n"); + goto done; + } + + /* The wrappers read ssl->eccVerifyRes and, on some builds, ssl->ctx; a + * zeroed WOLFSSL with a real CTX behind it is all they need. */ + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) { + printf("internal cryptocb white-box: no CTX\n"); + goto done; + } + + ssl = (WOLFSSL*)XMALLOC(sizeof(WOLFSSL), NULL, DYNAMIC_TYPE_SSL); + if (ssl == NULL) { + printf("internal cryptocb white-box: no SSL\n"); + goto done; + } + XMEMSET(ssl, 0, sizeof(WOLFSSL)); + ssl->ctx = ctx; + +#ifdef HAVE_ECC + wb_ecc_verify(ssl); + if (wc_InitRng(&rng) == 0) { + rngReady = 1; + wb_ecc_make_key(ssl, &rng); + } +#endif +#if defined(HAVE_ED25519) && defined(HAVE_ED25519_VERIFY) + wb_ed25519_verify(ssl); +#endif +#if defined(HAVE_ED448) && defined(HAVE_ED448_VERIFY) + wb_ed448_verify(ssl); +#endif +#ifndef NO_RSA + wb_rsa_null_args(ssl); + wb_rsa_recovered_plain(ssl); +#endif + + printf("internal cryptocb white-box: %d vectors driven\n", g_checks); + +done: +#ifdef HAVE_ECC + if (rngReady) + wc_FreeRng(&rng); +#endif + mcdc_cb_uninstall(); + XFREE(ssl, NULL, DYNAMIC_TYPE_SSL); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("internal cryptocb white-box: skipped " + "(needs WOLF_CRYPTO_CB, no pk-callbacks, no async)\n"); + return 0; +} + +#endif From 1fc0f5c9dcd61e341b783f6f11eab25272fc0774 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 16:43:28 +0200 Subject: [PATCH 48/60] tests: name the renegotiation_info extension and widen two skip messages The ClientHello builder wrote the renegotiation_info extension type as a conditional whose two branches were the same constant, which is a constant expression some -Wextra builds reject and which hid what 0xFF01 is. It is TLSX_RENEGOTIATION_INFO; use that. The CRL and OCSP skip stubs are guarded on their feature macro plus certs plus not-WOLFCRYPT_ONLY. Their printed messages already said so; the #else comments still named only the feature macro, so a reader chasing a skip saw the wrong reason. No coverage change: tls_core 810/1748 and revocation 28/47 + 31/48 both re-measured identical, gates green. --- tests/unit-mcdc/test_crl_whitebox.c | 2 +- tests/unit-mcdc/test_internal_clienthello_whitebox.c | 2 +- tests/unit-mcdc/test_ocsp_whitebox.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit-mcdc/test_crl_whitebox.c b/tests/unit-mcdc/test_crl_whitebox.c index 7afc3e37e80..7fd2c7b755a 100644 --- a/tests/unit-mcdc/test_crl_whitebox.c +++ b/tests/unit-mcdc/test_crl_whitebox.c @@ -523,7 +523,7 @@ int main(void) return 0; /* always 0: a non-zero exit discards the variant */ } -#else /* !HAVE_CRL */ +#else /* not (HAVE_CRL && certs && !WOLFCRYPT_ONLY) */ int main(void) { diff --git a/tests/unit-mcdc/test_internal_clienthello_whitebox.c b/tests/unit-mcdc/test_internal_clienthello_whitebox.c index e54b70bf79d..27149f8b437 100644 --- a/tests/unit-mcdc/test_internal_clienthello_whitebox.c +++ b/tests/unit-mcdc/test_internal_clienthello_whitebox.c @@ -169,7 +169,7 @@ static void wb_build(Frame* f, const Hello* h, int dtls, const WOLFSSL* ssl) Frame ext; fr_reset(&ext); if (h->extReneg) { - fr_u16(&ext, HELLO_EXT_SIG_ALGO == 0 ? 0xFF01 : 0xFF01); + fr_u16(&ext, TLSX_RENEGOTIATION_INFO); fr_u16(&ext, 1); fr_u8(&ext, 0); /* empty renegotiated_connection */ } diff --git a/tests/unit-mcdc/test_ocsp_whitebox.c b/tests/unit-mcdc/test_ocsp_whitebox.c index a7c85e1742f..6efbdb8b83f 100644 --- a/tests/unit-mcdc/test_ocsp_whitebox.c +++ b/tests/unit-mcdc/test_ocsp_whitebox.c @@ -650,7 +650,7 @@ int main(void) return 0; /* always 0: a non-zero exit discards the variant */ } -#else /* !HAVE_OCSP */ +#else /* not (HAVE_OCSP && certs && !WOLFCRYPT_ONLY) */ int main(void) { From 02eb0800aece41c3ca32f5bd09ab706a4eefa419 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 4 Sep 2026 18:10:23 +0200 Subject: [PATCH 49/60] tests: fix the build guards the narrow CI configurations broke Five configurations failed, four of them at build time, each the same mistake in a different place: a guard that describes what the test needs rather than what the build actually provides. Also registers the four tests/unit-mcdc files that were missing from EXTRA_DIST. Verified by building all five configurations locally: all pass. Campaign unaffected -- dtls, ssl_api and tls_core re-measured identical, gates green, white-box smoke 82 passed 0 failed. --- tests/api/test_dtls.c | 27 +++++++++++++++++++++++---- tests/api/test_ssl_cert.c | 39 +++++++++++++++++++++++++++++++-------- tests/include.am | 4 ++++ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index e08726b4694..5f462df092d 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -8398,8 +8398,23 @@ int test_dtls13_wire_mangle(void) * that later need to read a protected body; it is not enabled in the campaign * option list, so it compiles out there. * ========================================================================= */ +/* The whole forgery harness drives a real client against a real server in one + * process, so it needs both endpoints compiled in -- NO_WOLFSSL_CLIENT and + * NO_WOLFSSL_SERVER each remove one of the wolfDTLSv1_*_{client,server}_method + * pairs the sweeps below are called with. The two test entry points carry the + * same condition; keep them in step. + * + * WOLFSSL_ASYNC_CRYPT is excluded because the harness drives wolfSSL_accept and + * wolfSSL_connect directly and never runs an async event loop, so it cannot + * service a WC_PENDING_E. Measured under --enable-asynccrypt --enable-all + * --enable-dtls13: the DTLS 1.3 sweep takes SIGSEGV inside wolfAsync_EventInit, + * reached from BuildTls13Message via Dtls13SendFragment during + * SendTls13Certificate. Whether a plain accept on a real socket ought to + * survive that configuration is a library question and is reported separately; + * the harness has no business asserting it either way. */ #if defined(WOLFSSL_DTLS) && !defined(NO_RSA) && !defined(NO_CERTS) && \ - !defined(NO_FILESYSTEM) + !defined(NO_FILESYSTEM) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_WOLFSSL_SERVER) && !defined(WOLFSSL_ASYNC_CRYPT) #define DF_MAX_PKT 384 #define DF_MAX_SZ 1600 @@ -9516,7 +9531,9 @@ int test_dtls12_packet_forgeries(void) { EXPECT_DECLS; #if defined(WOLFSSL_DTLS) && !defined(WOLFSSL_NO_TLS12) && !defined(NO_RSA) \ - && !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + && !defined(NO_CERTS) && !defined(NO_FILESYSTEM) \ + && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) \ + && !defined(WOLFSSL_ASYNC_CRYPT) ExpectIntEQ(df_sweep(wolfDTLSv1_2_client_method, wolfDTLSv1_2_server_method), 0); #endif @@ -9527,7 +9544,9 @@ int test_dtls13_packet_forgeries(void) { EXPECT_DECLS; #if defined(WOLFSSL_DTLS13) && defined(WOLFSSL_TLS13) && !defined(NO_RSA) \ - && !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + && !defined(NO_CERTS) && !defined(NO_FILESYSTEM) \ + && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) \ + && !defined(WOLFSSL_ASYNC_CRYPT) ExpectIntEQ(df_sweep(wolfDTLSv1_3_client_method, wolfDTLSv1_3_server_method), 0); #endif @@ -9560,7 +9579,7 @@ int test_wolfSSL_dtls_cid_arg_guards(void) { EXPECT_DECLS; #if defined(WOLFSSL_DTLS_CID) && defined(WOLFSSL_DTLS) && !defined(NO_RSA) && \ - !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_WOLFSSL_CLIENT) WOLFSSL_CTX* ctx = NULL; WOLFSSL* plain = NULL; /* CID never enabled */ WOLFSSL* enabled = NULL; /* CID enabled, never negotiated */ diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index e5e9cf0b597..9c3e772340a 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2731,10 +2731,15 @@ int test_wolfSSL_x509_accessor_guards(void) * macro they became implicit declarations, which -Werror=implicit-function- * declaration and -Werror=nested-externs turn into build failures (and the * implicit int return then trips -Werror=int-conversion on the assignment). */ +/* wolfSSL_X509_get_signature, _get_next_altname and _get_pubkey_buffer are + * declared in ssl.h unconditionally but implemented in src/x509.c under + * OPENSSL_EXTRA || KEEP_OUR_CERT || KEEP_PEER_CERT only. A build with + * SESSION_CERTS or OPENSSL_EXTRA_X509_SMALL and none of those three sees the + * declarations and no definitions, which is a link error, not a compile error + * -- invisible to anything that only reads headers. Match the implementation. */ #if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ - (defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) || \ - defined(KEEP_PEER_CERT) || defined(KEEP_OUR_CERT) || \ - defined(SESSION_CERTS)) + (defined(OPENSSL_EXTRA) || defined(KEEP_PEER_CERT) || \ + defined(KEEP_OUR_CERT)) WOLFSSL_X509* x509 = NULL; byte buf[2048]; int iSz = (int)sizeof(buf); @@ -3117,7 +3122,11 @@ int test_wolfSSL_load_from_fifo(void) * use_certificate_chain_file and CertManagerVerify. Fixed upstream in * PR 11378; drop this exclusion once that merges and the sweep passes on the * small-stack variant. A crash here would discard the whole variant. */ -#if !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ +/* wolfSSL_SetAllocators lives in wolfcrypt/src/memory.c under + * #ifdef USE_WOLFSSL_MEMORY; without it the symbol is declared and never + * defined. */ +#if defined(USE_WOLFSSL_MEMORY) && \ + !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ !defined(WOLFSSL_SMALL_STACK) && \ !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) @@ -3265,12 +3274,25 @@ static void fi_workload(void) int test_wolfSSL_alloc_failure_sweep(void) { EXPECT_DECLS; -#if !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ +/* wolfSSL_SetAllocators lives in wolfcrypt/src/memory.c under + * #ifdef USE_WOLFSSL_MEMORY; without it the symbol is declared and never + * defined. */ +#if defined(USE_WOLFSSL_MEMORY) && \ + !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ !defined(WOLFSSL_SMALL_STACK) && \ !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) int n; int injected = 0; int total; + wolfSSL_Malloc_cb prevMalloc = NULL; + wolfSSL_Free_cb prevFree = NULL; + wolfSSL_Realloc_cb prevRealloc = NULL; + + /* Keep whatever was installed before, so the teardown below can put it + * back. Re-installing our own wrappers is NOT a restore: they would stay + * live for every later test in this binary, and unit.test runs threaded + * tests after this one. */ + (void)wolfSSL_GetAllocators(&prevMalloc, &prevFree, &prevRealloc); /* Install the wrappers with injection off, and count how many * allocations the workload makes, so the sweep covers all of them and @@ -3310,10 +3332,11 @@ int test_wolfSSL_alloc_failure_sweep(void) * hit repeatedly. */ ExpectIntGT(injected, 0); - /* Restore, and prove the library still works afterwards -- a failing - * allocator left installed would break every later test in this binary. */ + /* Put the original allocators back, and prove the library still works + * afterwards -- any allocator of ours left installed would sit under every + * later test in this binary, including the threaded ones. */ fi_failAt = -1; - (void)wolfSSL_SetAllocators(fi_malloc, fi_free, fi_realloc); + (void)wolfSSL_SetAllocators(prevMalloc, prevFree, prevRealloc); { WOLFSSL_CTX* ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); ExpectNotNull(ctx); diff --git a/tests/include.am b/tests/include.am index eb0cdd442fe..73818f8e268 100644 --- a/tests/include.am +++ b/tests/include.am @@ -120,8 +120,11 @@ DISTCLEANFILES+= tests/.libs/unit.test # non-compiled files. Do not move them to tests_unit_test_SOURCES. EXTRA_DIST += \ tests/unit-mcdc/README.md \ + tests/unit-mcdc/run-whitebox-smoke.sh \ + tests/unit-mcdc/smoke-expected.txt \ tests/unit-mcdc/mcdc_der_edit.h \ tests/unit-mcdc/mcdc_fault_alloc.h \ + tests/unit-mcdc/mcdc_fault_cryptocb.h \ tests/unit-mcdc/mcdc_fault_hash.h \ tests/unit-mcdc/mcdc_fault_mp.h \ tests/unit-mcdc/mcdc_fault_mpint.h \ @@ -169,6 +172,7 @@ EXTRA_DIST += \ tests/unit-mcdc/test_internal_domain_whitebox.c \ tests/unit-mcdc/test_internal_certerror_whitebox.c \ tests/unit-mcdc/test_internal_clienthello_whitebox.c \ + tests/unit-mcdc/test_internal_cryptocb_whitebox.c \ tests/unit-mcdc/test_internal_dhskehash_whitebox.c \ tests/unit-mcdc/test_internal_eddsa_whitebox.c \ tests/unit-mcdc/test_internal_nullguard_whitebox.c \ From 3f6658a6a26268d763c5f6319ee24888e455989b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 7 Sep 2026 01:22:39 +0200 Subject: [PATCH 50/60] tests: cover the LMS and XMSS bounds checks the rebase brought in The upstream hardening commits added defensive checks on deserialized private key state, and none of them are reachable from a keygen/sign/verify cycle: the library's own output always satisfies them, so every one of the decisions is permanently false in an ordinary run. Each is driven here by handing the function the state a tampered or truncated key would produce. wc_lms_priv_state_load stack offset past the end of the stack, and an offset that is in range but not a whole number of nodes, plus both accepting partners wc_lms_treehash_update a restored offset that says the data stack is already full, so the first push has nowhere to go wc_xmss_bds_update a NULL height array, and an offset past the subtree height wc_xmss_bds_next_idx a retain index below the first retained node, and one that would run off the end of retain The retain guard needs the merge loop to reach a height at or above sub_h - bds_k, so bds_k = 3 against sub_h = 4 puts that at height 1 and the caller-supplied height/offset pair steers the loop there in two iterations. The three indices then select each operand: 4 gives (i >> h) = 2, 6 gives a retain offset inside the buffer, 18 gives one exactly at its end. wc_lms_impl.c 134/140 -> 137/140, wc_xmss_impl.c 71/79 -> 75/79. The LMS drivers belong inside WB_GAP_SIGN. Placed outside it first, they failed to compile under WOLFSSL_WC_LMS_SMALL and WOLFSSL_LMS_VERIFY_ONLY, and because a white-box that does not build is recorded as a skip, both variants were dropped whole and took four conditions elsewhere in the file with them -- the file went down by one overall while the intended three were covered. Both drivers are now syntax-checked against every combination of the small and verify-only macros. Two conditions are left and neither is this shape: wc_lms_impl.c:2454's ret operand needs a hash failure earlier in the same loop iteration, which belongs to the hash-fault driver, and wc_xmss_impl.c:3077 needs a tree-hash instance that is in use while the stack offset is zero. --- .../unit-mcdc/test_wc_lms_impl_whitebox_gap.c | 151 +++++++++++++++++- tests/unit-mcdc/test_wc_xmss_impl_whitebox.c | 141 ++++++++++++++++ 2 files changed, 291 insertions(+), 1 deletion(-) 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 6258dea18e9..269fa4c3eed 100644 --- a/tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c +++ b/tests/unit-mcdc/test_wc_lms_impl_whitebox_gap.c @@ -1031,6 +1031,150 @@ static void wb_hss_full_cycle(void) WB_NOTE("hss_full_cycle (levels=2, subtree transition) drive complete"); } +/* ------------------------------------------------------------------------ + * wc_lms_priv_state_load() bounds checks (wc_lms_impl.c:1821) + * + * if ((state->stack.offset > LMS_STACK_CACHE_LEN(height, hash_len)) || + * ((state->stack.offset % params->hash_len) != 0)) + * return BUFFER_E; + * + * Added upstream by "Add some bounds checks to LMS.". The loader carves a + * serialized private key into an LmsPrivState and reads stack.offset straight + * off the wire with ato32, so both operands describe a private key that has + * been tampered with or truncated -- a key the library itself wrote always has + * an offset that is both in range and a whole number of nodes, which is why an + * ordinary keygen/sign/reload cycle leaves the decision permanently false. + * + * The loader stores pointers into the buffer and validates two integers; it + * allocates nothing and hashes nothing, so a stack buffer of the right length + * with a hand-written offset field is the whole fixture. + * ------------------------------------------------------------------------ */ +static void wb_priv_state_load_bounds(void) +{ + LmsParams params; + LmsPrivState state; + /* height 5, rootLevels 2, cacheBits 2, hash_len 32 */ + byte priv[LMS_PRIV_STATE_LEN(5, 2, 2, WB_HLEN)]; + /* stack.offset sits after auth_path (height * hash_len) and the stack + * itself ((height + 1) * hash_len). */ + const word32 offPos = (word32)5 * WB_HLEN + (word32)6 * WB_HLEN; + const word32 cacheLen = LMS_STACK_CACHE_LEN(5, WB_HLEN); + int ret; + size_t i; + + static const struct { + const char* name; + word32 offset; /* what to write into stack.offset */ + int expect; /* 0 accept, BUFFER_E reject */ + } rows[] = { + /* (T,-) too large: first operand true, second short-circuited */ + { "offset past end of stack", 0, BUFFER_E }, /* filled below */ + /* (F,T) in range but not a whole node */ + { "offset not a node multiple", 1, BUFFER_E }, + /* (F,F) the accepting partner for both operands */ + { "offset exactly at the end", 2, 0 }, + { "offset zero", 3, 0 } + }; + + wb_make_params(¶ms, 1, 5, 2, 2); + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + word32 off; + + switch (rows[i].offset) { + case 0: off = cacheLen + WB_HLEN; break; /* > cache length */ + case 1: off = WB_HLEN + 1U; break; /* in range, unaligned */ + case 2: off = cacheLen; break; /* == cache length */ + default: off = 0; break; + } + + XMEMSET(priv, 0, sizeof(priv)); + c32toa(off, priv + offPos); + XMEMSET(&state, 0, sizeof(state)); + + ret = wc_lms_priv_state_load(¶ms, &state, priv); + printf(" [wb] priv_state_load %-27s off=%u -> %d\n", + rows[i].name, (unsigned)off, ret); + if (ret != rows[i].expect) { + printf(" [wb] FAIL: expected %d\n", rows[i].expect); + wb_fail = 1; + } + } +} + +/* ------------------------------------------------------------------------ + * wc_lms_treehash_update() data-stack-full guard (wc_lms_impl.c:2454) + * + * if ((ret == 0) && ((size_t)(spEnd - sp) < params->hash_len)) { + * ret = BUFFER_E; -- no room on the stack to push onto + * + * Added upstream by "Add some bounds checks to LMS.". sp starts at + * stack + stackCache->offset and spEnd at stack + LMS_STACK_CACHE_LEN, so the + * remaining room is exactly (cache length - offset). The traversal only ever + * pushes as many nodes as the tree height allows, so a private state the + * library produced always leaves room and the guard is dead -- it is defending + * against a restored state whose offset says the stack is already full, which + * is the state written here. + * + * Only the second operand is driven. The first is false only when an earlier + * step in the SAME loop iteration already failed, which on this path means the + * hash failing, and that belongs to the hash-fault driver rather than here. + * ------------------------------------------------------------------------ */ +static void wb_treehash_update_stack_full(void) +{ + LmsParams params; + LmsState state; + byte id[LMS_I_LEN]; + byte seed[WB_HLEN]; + byte auth_path[5 * WB_HLEN]; + byte stack_buf[(5 + 1) * WB_HLEN]; + byte root_buf[((1U << 2) - 1U) * WB_HLEN]; + byte leaf_cache[(1U << 2) * WB_HLEN]; + LmsPrivState priv; + int ret; + + wb_make_params(¶ms, 1, 5, 2, 2); + XMEMSET(id, 0xAA, sizeof(id)); + XMEMSET(seed, 0xBB, sizeof(seed)); + XMEMSET(auth_path, 0, sizeof(auth_path)); + XMEMSET(stack_buf, 0, sizeof(stack_buf)); + XMEMSET(root_buf, 0, sizeof(root_buf)); + XMEMSET(leaf_cache, 0, sizeof(leaf_cache)); + + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("wb_state_init failed for treehash_update_stack_full"); + wb_fail = 1; + return; + } + + XMEMSET(&priv, 0, sizeof(priv)); + priv.auth_path = auth_path; + priv.stack.stack = stack_buf; + priv.root = root_buf; + priv.leaf.cache = leaf_cache; + + ret = wc_lms_treehash_init(&state, &priv, id, seed, 0); + if (ret == 0) { + /* Say the stack is already full: spEnd - sp becomes 0, which is below + * every hash length, so the first push has nowhere to go. */ + priv.stack.offset = LMS_STACK_CACHE_LEN(params.height, params.hash_len); + + ret = wc_lms_treehash_update(&state, &priv, id, seed, 0, 0, 0, 0); + printf(" [wb] treehash_update full stack (offset=%u) -> %d\n", + (unsigned)priv.stack.offset, ret); + if (ret != WC_NO_ERR_TRACE(BUFFER_E)) { + printf(" [wb] FAIL: expected BUFFER_E\n"); + wb_fail = 1; + } + } + else { + WB_NOTE("treehash_init failed in stack_full test"); + wb_fail = 1; + } + + wb_state_free(&state); +} + #else /* !WB_GAP_SIGN */ static void wb_compute_y_kc_ret(void) @@ -1044,6 +1188,8 @@ static void wb_hss_sign_checks(void) {} static void wb_next_subtree_inc(void) {} static void wb_hss_verify_checks(void) {} static void wb_hss_full_cycle(void) {} +static void wb_priv_state_load_bounds(void) {} +static void wb_treehash_update_stack_full(void) {} #endif /* WB_GAP_SIGN */ @@ -1054,6 +1200,8 @@ static void wb_q_expand(void) WB_NOTE("WOLFSSL_HAVE_LMS not compiled in this variant; skipped"); } static void wb_compute_y_kc_ret(void) {} +static void wb_priv_state_load_bounds(void) {} +static void wb_treehash_update_stack_full(void) {} static void wb_treehash_init_edges(void) {} static void wb_treehash_update_leafslide(void) {} static void wb_verify_corrupt(void) {} @@ -1061,7 +1209,6 @@ static void wb_hss_sign_checks(void) {} static void wb_next_subtree_inc(void) {} static void wb_hss_verify_checks(void) {} static void wb_hss_full_cycle(void) {} - #endif /* WOLFSSL_HAVE_LMS */ int main(void) @@ -1085,6 +1232,8 @@ int main(void) wb_next_subtree_inc(); wb_hss_verify_checks(); wb_hss_full_cycle(); + wb_priv_state_load_bounds(); + wb_treehash_update_stack_full(); printf("done (%s)\n", wb_fail ? "with skips" : "ok"); /* Setup failures are surfaced as skips (printed notes + wb_fail), not diff --git a/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c b/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c index 6239fc996a0..69896aae79e 100644 --- a/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c +++ b/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c @@ -1231,7 +1231,146 @@ static void wb_full_cycle_d1(void) } } } +/******************************************** + * 3215: wc_xmss_bds_update()'s + * "if ((bds->height == NULL) || (bds->offset > params->sub_h))" + * 2921: wc_xmss_bds_next_idx()'s retain-index guard + * "if (((i >> h) < 3) || (ro * n >= XMSS_RETAIN_LEN(params->bds_k, n)))" + * + * Both were added upstream with the XMSS hardening commits. They describe a + * BDS state that the library's own key generation and signing never produce: + * a NULL height array, an offset past the subtree height, or a retain index + * that would run off the end of bds->retain. Only a restored or tampered + * private key reaches them, so an ordinary sign/verify cycle leaves both + * decisions permanently false. + * + * wc_xmss_bds_update returns before touching anything once the guard fires, so + * a BdsState with a NULL height or an out-of-range offset is safe to pass. + * + * The retain guard needs the merge loop to actually reach a height at or above + * hsk = sub_h - bds_k, which is where the "else" retain arm lives. bds_k = 3 + * against sub_h = 4 puts hsk at 1, so the second iteration of the loop lands + * in retain, and the loop is steered entirely by the caller-supplied + * height[]/offset pair: offset 2 with height {1, 0} runs h = 0 then h = 1. + * With h = 1, ro = 1 + (((i >> 1) - 3) >> 1) and XMSS_RETAIN_LEN(3, 32) = 128, + * so i selects each operand: + * + * i = 4 -> (i >> 1) = 2 -> first operand true, second short-circuited + * i = 6 -> (i >> 1) = 3, ro = 1, ro * n = 32 -> both false, the partner + * i = 18 -> (i >> 1) = 9, ro = 4, ro * n = 128 -> second operand true + * + * sp starts several nodes into state->stack because the loop walks node + * downwards once per iteration. + ********************************************/ +static void wb_bds_hardening(void) +{ + XmssParams params; + XmssState state; + BdsState bds[1]; + byte sk[2048]; + byte sk_seed[32]; + byte pk_seed[32]; + HashAddress addr; + word8 height[8]; + word8 offset; + byte* sp; + size_t i; + + /* h=4, d=1 -> sub_h = 4; bds_k = 3 -> hsk = 1. */ + wb_params_init(¶ms, WC_HASH_TYPE_SHA256, 32, 32, 4, 1, 4, 3); + + if (wb_state_init(&state, ¶ms) != 0) { + WB_NOTE("bds_hardening: state init failed; skipped"); + return; + } + XMEMSET(sk_seed, 0x33, sizeof(sk_seed)); + XMEMSET(pk_seed, 0x44, sizeof(pk_seed)); + XMEMSET(sk, 0, sizeof(sk)); + + /* ---- 3215: wc_xmss_bds_update() argument guard ---- */ + if (wc_xmss_bds_state_load(&state, sk, bds, NULL) == 0) { + word8* savedHeight = bds[0].height; + + XMEMSET(&addr, 0, sizeof(addr)); + + /* (T,-) no height array at all. */ + bds[0].next = 0; + bds[0].offset = 0; + bds[0].height = NULL; + state.ret = 0; + wc_xmss_bds_update(&state, &bds[0], sk_seed, pk_seed, addr); + printf(" [wb] bds_update height=NULL -> ret %d\n", state.ret); + if (state.ret == 0) { + WB_NOTE("FAIL: bds_update accepted a NULL height array"); + wb_fail = 1; + } + + /* (F,T) height present, offset past the subtree height. */ + bds[0].height = savedHeight; + bds[0].next = 0; + bds[0].offset = (word8)(params.sub_h + 1); + state.ret = 0; + wc_xmss_bds_update(&state, &bds[0], sk_seed, pk_seed, addr); + printf(" [wb] bds_update offset>sub_h -> ret %d\n", state.ret); + if (state.ret == 0) { + WB_NOTE("FAIL: bds_update accepted an out-of-range offset"); + wb_fail = 1; + } + bds[0].height = savedHeight; + } + else { + WB_NOTE("bds_hardening: bds_state_load failed; bds_update skipped"); + } + + /* ---- 2921: wc_xmss_bds_next_idx() retain-index guard ---- */ + { + static const struct { + word32 i; + const char* what; + int expectFail; + } rows[] = { + { 4, "(i>>h) < 3 ", 1 }, + { 6, "in range, accepted", 0 }, + { 18, "ro past retain end", 1 } + }; + + for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) { + if (wc_xmss_bds_state_load(&state, sk, bds, NULL) != 0) { + WB_NOTE("bds_hardening: bds_state_load failed; retain skipped"); + break; + } + XMEMSET(&addr, 0, sizeof(addr)); + /* offset 2 with heights {1, 0}: the loop runs h = 0 then h = 1, + * and h = 1 >= hsk lands in the retain arm. */ + XMEMSET(height, 0, sizeof(height)); + height[0] = 1; + height[1] = 0; + offset = 2; + /* Leave room below sp: the loop walks node down once per pass. */ + sp = state.stack + 4 * params.n; + state.ret = 0; + + wc_xmss_bds_next_idx(&state, &bds[0], sk_seed, pk_seed, addr, + rows[i].i, height, &offset, &sp); + + printf(" [wb] bds_next_idx i=%-2u %s -> ret %d\n", + (unsigned)rows[i].i, rows[i].what, state.ret); + if (rows[i].expectFail && (state.ret == 0)) { + WB_NOTE("FAIL: retain guard did not fire"); + wb_fail = 1; + } + if (!rows[i].expectFail && (state.ret != 0)) { + WB_NOTE("FAIL: retain guard fired on an in-range index"); + wb_fail = 1; + } + } + } + + wb_state_free(&state); +} + #else /* verify-only, or the small signing path */ +static void wb_bds_hardening(void) {} static void wb_bds_next_idx(void) { WB_NOTE("BDS helpers not compiled in; wb_bds_next_idx skipped"); @@ -1498,6 +1637,7 @@ static void wb_smallmt_bad_idx_len(void) { WB_NOTE("WOLFSSL_HAVE_XMSS not compiled in; skipped"); } +static void wb_bds_hardening(void) {} #endif /* WOLFSSL_HAVE_XMSS */ @@ -1508,6 +1648,7 @@ int main(void) wb_hash_family_pairs(); wb_wots_chain_loop(); wb_bds_next_idx(); + wb_bds_hardening(); wb_bds_auth_path(); wb_full_cycle_d2(); wb_full_cycle_d1(); From 1ee35883f6c5210c4e628fe7d7800fc698bf6fb2 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 7 Sep 2026 06:58:52 +0200 Subject: [PATCH 51/60] tests: build the CA bucket GetCAByName's second operand needs while (signers && ret == NULL) is false in its second operand only when a match was just assigned and the bucket still holds an entry after it, so the loop condition is evaluated once more with ret set. If the match is the only entry in its row, or the last one, signers goes NULL and the first operand ends the loop instead. That makes the operand a property of CA-table occupancy rather than of any test: it needs two CAs hashing to the same row and a lookup for the one that is not at the tail. Which certificates a run loads, and in what order they were added, decide whether that ever happens -- which is why this one condition was covered on one host and not on another from the same tree, the same tests and byte-identical certificates. The sweep measured 49/113 two nights running where the development host measured 50/113, and the difference was this line and nothing else. The bucket is now built rather than hoped for. GetCAByName reads only subjectNameHash and next and takes cm->caLock, so two zeroed Signers linked head to tail are a complete fixture; they are on the stack, so the row is detached before the CertManager is freed. Four vectors: the head of a two-entry row, its tail, an absent hash, and a NULL manager. ssl_certman.c is #included into ssl.c and refuses to compile alone, so the white-box includes src/ssl.c. --- tests/unit-mcdc/test_ssl_certman_whitebox.c | 146 ++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 tests/unit-mcdc/test_ssl_certman_whitebox.c diff --git a/tests/unit-mcdc/test_ssl_certman_whitebox.c b/tests/unit-mcdc/test_ssl_certman_whitebox.c new file mode 100644 index 00000000000..8aae9d191df --- /dev/null +++ b/tests/unit-mcdc/test_ssl_certman_whitebox.c @@ -0,0 +1,146 @@ +/* test_ssl_certman_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-1301, USA + */ + +/* GetCAByName() walks the CertManager's CA hash table: + * + * while (signers && ret == NULL) { + * if (XMEMCMP(hash, signers->subjectNameHash, ...) == 0) + * ret = signers; + * signers = signers->next; + * } + * + * The second operand is false only when a match was just assigned AND the + * bucket still has an entry after it, so the loop condition gets re-evaluated + * with ret set. If the match is the last entry in its bucket -- or the only + * one -- signers goes NULL and the first operand short-circuits instead. + * + * That makes the operand a property of CA-table occupancy rather than of any + * test: it needs two CAs that hash to the same row of CA_TABLE_SIZE, and the + * lookup has to ask for the one that is not at the tail. Which certificates a + * suite happens to load, and the order they were added in, decide whether that + * ever happens. It is why this single condition was covered on one machine and + * not on another from the same tree, the same tests and the same certificates + * -- the 2026-09-05 and 2026-09-06 sweeps measured 49/113 where this host + * measured 50/113, and the difference was exactly this line. + * + * Rather than leave it to whichever CAs a run happens to load, the bucket is + * built here. GetCAByName reads only subjectNameHash and next and takes + * cm->caLock, so two zeroed Signers linked head-to-tail are a complete and + * safe fixture -- nothing else in either object is ever dereferenced. They + * live on the stack, so the row is detached again before the CertManager is + * freed; leaving them linked would send the teardown walking into this frame. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_CERTS) && !defined(NO_SKID) + +static int g_checks; + +int main(void) +{ + WOLFSSL_CERT_MANAGER* cm = NULL; + Signer head; + Signer tail; + byte hashHead[SIGNER_DIGEST_SIZE]; + byte hashTail[SIGNER_DIGEST_SIZE]; + byte hashMiss[SIGNER_DIGEST_SIZE]; + Signer* got; + + wolfSSL_Init(); + + cm = wolfSSL_CertManagerNew(); + if (cm == NULL) { + printf("ssl_certman white-box: no CertManager\n"); + goto done; + } + + XMEMSET(&head, 0, sizeof(head)); + XMEMSET(&tail, 0, sizeof(tail)); + XMEMSET(hashHead, 0x11, sizeof(hashHead)); + XMEMSET(hashTail, 0x22, sizeof(hashTail)); + XMEMSET(hashMiss, 0x33, sizeof(hashMiss)); + XMEMCPY(head.subjectNameHash, hashHead, SIGNER_DIGEST_SIZE); + XMEMCPY(tail.subjectNameHash, hashTail, SIGNER_DIGEST_SIZE); + + /* One row, two entries: head -> tail. Which row does not matter; the walk + * visits every row until it finds something. */ + head.next = &tail; + tail.next = NULL; + cm->caTable[0] = &head; + + /* (T,F): the match is the head, so a signer remains after it and the loop + * condition is evaluated once more with ret already set. */ + got = GetCAByName(cm, hashHead); + printf(" GetCAByName head of a two-entry row -> %s\n", + got == &head ? "found" : "MISSED"); + g_checks++; + + /* (F,-): the match is the tail, so signers goes NULL and the first operand + * ends the loop before the second is looked at. */ + got = GetCAByName(cm, hashTail); + printf(" GetCAByName tail of a two-entry row -> %s\n", + got == &tail ? "found" : "MISSED"); + g_checks++; + + /* No match anywhere: every row is walked to its end. */ + got = GetCAByName(cm, hashMiss); + printf(" GetCAByName absent hash -> %s\n", + got == NULL ? "not found" : "UNEXPECTED"); + g_checks++; + + /* The NULL-manager guard above the walk. */ + got = GetCAByName(NULL, hashHead); + printf(" GetCAByName NULL manager -> %s\n", + got == NULL ? "NULL" : "UNEXPECTED"); + g_checks++; + + /* Detach before teardown: both Signers are on this stack frame. */ + cm->caTable[0] = NULL; + + printf("ssl_certman white-box: %d vectors driven\n", g_checks); + +done: + if (cm != NULL) + wolfSSL_CertManagerFree(cm); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("ssl_certman white-box: skipped (needs certs and SKID)\n"); + return 0; +} + +#endif From 885284edd23595e714c2fbe47b0c1bd1183e16ea Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 7 Sep 2026 13:11:12 +0200 Subject: [PATCH 52/60] tests: guard the API-availability macros the no-TLS builds need Three more configurations failed to link, all the same shape as the last round: a guard that names what the test needs rather than what the build provides. NO_TLS builds (certgen-no-tls, no-tls-cryptocb-aesgcm-setkey-free) wolfSSLv23_client_method and wolfSSLv23_server_method are implemented under !NO_TLS && !NO_WOLFSSL_{CLIENT,SERVER}, and wolfSSL_UseSNI, wolfSSL_CTX_UseSNI, wolfSSL_SNI_Get*, wolfSSL_UseSupportedCurve and wolfSSL_CTX_UseSupportedCurve all sit under !NO_TLS in ssl_api_ext.c on top of their own feature macro. Eleven blocks were missing !NO_TLS. dtls13-client-minimal (WOLFSSL_NO_TLS12) wolfDTLSv1_2_{client,server}_method are implemented under !WOLFSSL_NO_TLS12: DTLS 1.2 is built on the TLS 1.2 code. Three blocks wanted that, including the CID argument-guard test. Found the first pass by line number and missed a second block carrying the identical guard text, so this was checked mechanically instead: for every call site of each of these symbols, walk the enclosing #if chain and assert it carries the macros the implementation requires. That found the leftovers and now reports zero for this branch. Verified by building all seven affected configurations locally -- the three new ones and the four fixed last round, so neither set regressed the other. Campaign unaffected: ssl_api, dtls, revocation and tls_core re-measured, gates green, white-box smoke 82 passed 0 failed. --- tests/api/test_dtls.c | 3 ++- tests/api/test_ssl_cert.c | 24 ++++++++++++++---------- tests/api/test_ssl_ext.c | 12 ++++++------ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index 5f462df092d..65c1d480509 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -9579,7 +9579,8 @@ int test_wolfSSL_dtls_cid_arg_guards(void) { EXPECT_DECLS; #if defined(WOLFSSL_DTLS_CID) && defined(WOLFSSL_DTLS) && !defined(NO_RSA) && \ - !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_WOLFSSL_CLIENT) + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(WOLFSSL_NO_TLS12) WOLFSSL_CTX* ctx = NULL; WOLFSSL* plain = NULL; /* CID never enabled */ WOLFSSL* enabled = NULL; /* CID enabled, never negotiated */ diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 9c3e772340a..7e08948d67d 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -1528,7 +1528,7 @@ int test_wolfSSL_cert_unload(void) int test_wolfSSL_cert_api_arg_guards(void) { EXPECT_DECLS; -#if !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) +#if !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; #ifdef HAVE_RPK @@ -1637,7 +1637,7 @@ int test_wolfSSL_cert_api_arg_guards(void) int test_wolfSSL_crl_ocsp_api_arg_guards(void) { EXPECT_DECLS; -#if !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) +#if !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; @@ -2568,7 +2568,7 @@ int test_wolfSSL_crl_io_mock(void) { EXPECT_DECLS; #if defined(HAVE_CRL) && defined(HAVE_CRL_IO) && !defined(NO_CERTS) && \ - !defined(NO_WOLFSSL_CLIENT) + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; int i; @@ -2629,7 +2629,8 @@ int test_wolfSSL_ocsp_stapling_accessors(void) EXPECT_DECLS; #if defined(HAVE_OCSP) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ - !defined(NO_CERTS) && !defined(NO_FILESYSTEM) + !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ + !defined(NO_TLS) WOLFSSL_CTX* cctx = NULL; /* client */ WOLFSSL_CTX* sctx = NULL; /* server: the side operand's partner */ WOLFSSL* cssl = NULL; @@ -2847,7 +2848,8 @@ int test_wolfSSL_x509_accessor_guards(void) int test_wolfSSL_dtls_api_on_dtls_object(void) { EXPECT_DECLS; -#if defined(WOLFSSL_DTLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) +#if defined(WOLFSSL_DTLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) && \ + !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) WOLFSSL_CTX* dctx = NULL; /* the object the second operand needs */ WOLFSSL_CTX* tctx = NULL; /* a TLS one, for the operand's other half */ WOLFSSL* dssl = NULL; @@ -2943,7 +2945,8 @@ int test_wolfSSL_dtls_api_on_dtls_object(void) int test_wolfSSL_load_pathological_files(void) { EXPECT_DECLS; -#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_WOLFSSL_CLIENT) +#if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; WOLFSSL_CERT_MANAGER* cm = NULL; const char* emptyFile = "test-empty-cert.tmp"; @@ -3035,7 +3038,7 @@ int test_wolfSSL_load_from_fifo(void) { EXPECT_DECLS; #if !defined(NO_CERTS) && !defined(NO_FILESYSTEM) && \ - !defined(NO_WOLFSSL_CLIENT) && defined(__unix__) + !defined(NO_WOLFSSL_CLIENT) && defined(__unix__) && !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; const char* fifo = "test-cert-fifo.tmp"; int fd = -1; @@ -3128,7 +3131,7 @@ int test_wolfSSL_load_from_fifo(void) #if defined(USE_WOLFSSL_MEMORY) && \ !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ !defined(WOLFSSL_SMALL_STACK) && \ - !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) + !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) && !defined(NO_TLS) static int fi_failAt = -1; /* which allocation to fail; -1 = none */ static int fi_count; /* allocations seen since the last reset */ @@ -3280,7 +3283,7 @@ int test_wolfSSL_alloc_failure_sweep(void) #if defined(USE_WOLFSSL_MEMORY) && \ !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ !defined(WOLFSSL_SMALL_STACK) && \ - !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) + !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) && !defined(NO_TLS) int n; int injected = 0; int total; @@ -3364,7 +3367,8 @@ int test_wolfSSL_dtls_api_more_guards(void) EXPECT_DECLS; #if defined(WOLFSSL_DTLS) && !defined(WOLFSSL_LEANPSK) && \ !defined(WOLFCRYPT_ONLY) && !defined(NO_WOLFSSL_CLIENT) && \ - !defined(NO_CERTS) + !defined(NO_CERTS) && \ + !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) WOLFSSL_CTX* dctx = NULL; WOLFSSL* dssl = NULL; byte peer[64]; diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index d5319a336ba..7afbb11fe2a 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1524,7 +1524,7 @@ int test_wolfSSL_ech_config_api(void) int test_wolfSSL_api_null_burndown(void) { EXPECT_DECLS; -#if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) +#if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) && !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; #ifdef HAVE_ALPN @@ -1650,7 +1650,7 @@ int test_wolfSSL_api_null_burndown(void) int test_wolfSSL_session_null_burndown(void) { EXPECT_DECLS; -#if !defined(NO_SESSION_CACHE) && !defined(NO_WOLFSSL_CLIENT) +#if !defined(NO_SESSION_CACHE) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; WOLFSSL_SESSION* sess = NULL; @@ -1706,7 +1706,7 @@ int test_wolfSSL_session_null_burndown(void) int test_wolfSSL_api_null_operands(void) { EXPECT_DECLS; -#if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) +#if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CERTS) && !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; byte buf[64]; @@ -1801,7 +1801,7 @@ int test_wolfSSL_api_null_operands(void) /* --- SNI from a raw ClientHello buffer ------------------------------ */ /* Server-side only: it parses what a client sent (HAVE_SNI && * !NO_WOLFSSL_SERVER in src/ssl_api_ext.c). */ -#if defined(HAVE_SNI) && !defined(NO_WOLFSSL_SERVER) +#if defined(HAVE_SNI) && !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) { byte hello[64]; word32 outSz = (word32)sizeof(buf); @@ -1884,7 +1884,7 @@ int test_wolfSSL_api_null_operands(void) int test_wolfSSL_public_null_operands(void) { EXPECT_DECLS; -#if !defined(WOLFCRYPT_ONLY) && !defined(NO_WOLFSSL_CLIENT) +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; char buf[512]; @@ -2022,7 +2022,7 @@ int test_wolfSSL_session_lifecycle_guards(void) { EXPECT_DECLS; #if !defined(WOLFCRYPT_ONLY) && !defined(NO_SESSION_CACHE) && \ - !defined(NO_WOLFSSL_CLIENT) + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) WOLFSSL_CTX* ctx = NULL; WOLFSSL* ssl = NULL; WOLFSSL_SESSION* fresh = NULL; From a9520091741dac4c11137a9566b59c01ddf605c6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 8 Sep 2026 17:17:10 +0200 Subject: [PATCH 53/60] tests: restore the CRL mock's counters after the rebase move Rebasing onto master put upstream's new verify-mode tests in the same place as this branch's, and git aligned the two on the shared create-ctx / free-ctx boilerplate, which splits both function bodies. Resolving those hunks by copying whole functions across is right, but it moved the mock without the two file-scope counters above it, so g_crlIoCalls and g_crlIoResult became undeclared. They are back, immediately above the mock that uses them. Nothing else was lost: every file-scope static that existed before the rebase and is still referenced is still defined. --- tests/api/test_ssl_cert.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 7e08948d67d..c06d3ffd3c1 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2557,6 +2557,9 @@ int test_wolfSSL_verify_post_handshake_defers(void) return EXPECT_RESULT(); } +static int g_crlIoCalls; +static int g_crlIoResult; + static int test_crl_io_mock(WOLFSSL_CRL* crl, const char* url, int urlSz) { (void)crl; (void)url; (void)urlSz; From 03206d68add2de2ee0c3d567ff60913553d59d76 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 8 Sep 2026 17:52:09 +0200 Subject: [PATCH 54/60] tests: record the certman white-box as smoke-covered test_ssl_certman_whitebox includes src/ssl.c, and against the previous smoke build that failed to link: the harness compiles with fixed flags, and whether XFDOPEN is defined decides whether wc_fopen_owner_only is a function or a macro. It builds and passes against the post-rebase --enable-all --enable-static tree, so it is in the expected list now and a break in it will be caught locally rather than only by a full sweep. --- tests/unit-mcdc/smoke-expected.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index c84553b8dc1..9b3892db0e9 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -65,6 +65,7 @@ test_sp_c32_whitebox test_sp_c64_whitebox test_sp_cortexm_crafted_whitebox test_sp_cortexm_fault_whitebox +test_ssl_certman_whitebox test_tfm_fault_whitebox test_tfm_whitebox test_tls13_null_whitebox From 717bfb18c9438ef8e603af13e29968de1fe46891 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 8 Sep 2026 18:14:03 +0200 Subject: [PATCH 55/60] tests: compile the CRL mock only where its caller is The rebase resolution copied test_crl_io_mock across without the #if that had surrounded it, leaving it defined at file scope while its only caller keeps the guard inside its own body. Any build where that body is compiled out -- a default build has neither HAVE_CRL nor HAVE_CRL_IO -- then has a static function nothing references, which -Werror=unused-function rejects. The guard is back, and it now carries the !defined(NO_TLS) the caller's body gained after the original was written, so the two conditions are identical rather than merely similar. Reproduced before fixing: the pre-fix file fails to compile in a default build with exactly "'test_crl_io_mock' defined but not used", and builds clean after. Checked the other six functions the rebase moved in this file the same way -- comparing each one's enclosing guard chain against its pre-rebase version -- and this was the only one that lost anything. Verified across default, --enable-crl, --enable-all and a no-TLS build, all with -Werror=unused-function and -Werror=unused-variable. --- tests/api/test_ssl_cert.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index c06d3ffd3c1..02bfbc5b854 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2557,6 +2557,11 @@ int test_wolfSSL_verify_post_handshake_defers(void) return EXPECT_RESULT(); } +/* Compiled exactly when the body of test_wolfSSL_crl_io_mock() below is: the + * mock has no other caller, so a wider condition here leaves it defined and + * unused, which -Werror=unused-function rejects. Keep the two in step. */ +#if defined(HAVE_CRL) && defined(HAVE_CRL_IO) && !defined(NO_CERTS) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_TLS) static int g_crlIoCalls; static int g_crlIoResult; @@ -2566,6 +2571,7 @@ static int test_crl_io_mock(WOLFSSL_CRL* crl, const char* url, int urlSz) g_crlIoCalls++; return g_crlIoResult; } +#endif int test_wolfSSL_crl_io_mock(void) { From ef08af0b04492d22b3ab16a4ba7badcc898b58c3 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 9 Sep 2026 08:26:17 +0200 Subject: [PATCH 56/60] tests: run the allocation sweep on small-stack builds too The sweep was excluded from WOLFSSL_SMALL_STACK because DecodeCertInternal indexed RPKdataASN before checking the ret that CALLOC_ASNGETDATA sets, so failing an allocation dereferenced NULL while parsing any certificate. That was reported and fixed upstream in PR 11378, which the last rebase brought in, so the exclusion is gone. Verified rather than assumed: built --enable-all --enable-smallstack with WOLFSSL_SMALL_STACK actually defined in options.h, ran the ssl_cert group, and the sweep passes with no errors and no crash. A crash would have discarded the whole variant, which is what the exclusion was protecting against. The small-stack variant now contributes what it always should have: internal.c 810/1748 -> 825/1754 ssl_load.c 43/182 -> 48/182 ssl_certman.c 50/113 -> 53/113 keys.c 33/40 -> 34/40 --- tests/api/test_ssl_cert.c | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 02bfbc5b854..96960eebf70 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -3125,22 +3125,24 @@ int test_wolfSSL_load_from_fifo(void) * allocator installed would break every test that runs after this one in the * same binary, which costs the whole variant. * ------------------------------------------------------------------------- */ -/* Not under WOLFSSL_SMALL_STACK, and the reason is now known rather than - * suspected: DecodeCertInternal indexes RPKdataASN before checking the ret - * that CALLOC_ASNGETDATA sets, so under that build an allocation failure - * dereferences NULL while parsing any certificate. A per-index sweep crashes - * at five allocation indices (7, 30, 51, 68, 90), all at the same - * instruction, reached through load_verify_locations, use_certificate_file, - * use_certificate_chain_file and CertManagerVerify. Fixed upstream in - * PR 11378; drop this exclusion once that merges and the sweep passes on the - * small-stack variant. A crash here would discard the whole variant. */ +/* This ran everywhere except WOLFSSL_SMALL_STACK for a while, because under + * that build DecodeCertInternal indexed RPKdataASN before checking the ret + * that CALLOC_ASNGETDATA sets, so failing an allocation dereferenced NULL + * while parsing any certificate -- a per-index sweep crashed at five + * allocation indices (7, 30, 51, 68, 90), all the same instruction, reached + * through load_verify_locations, use_certificate_file, + * use_certificate_chain_file and CertManagerVerify. That was reported and + * fixed upstream in PR 11378, which is in the tree, so the small-stack + * exclusion is gone and the sweep now runs on every variant. A crash here + * still costs the whole variant, so it is worth re-checking after any change + * to the ASN.1 allocation macros. */ /* wolfSSL_SetAllocators lives in wolfcrypt/src/memory.c under * #ifdef USE_WOLFSSL_MEMORY; without it the symbol is declared and never * defined. */ #if defined(USE_WOLFSSL_MEMORY) && \ !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ - !defined(WOLFSSL_SMALL_STACK) && \ - !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) && !defined(NO_TLS) + !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) && \ + !defined(NO_TLS) static int fi_failAt = -1; /* which allocation to fail; -1 = none */ static int fi_count; /* allocations seen since the last reset */ @@ -3291,8 +3293,8 @@ int test_wolfSSL_alloc_failure_sweep(void) * defined. */ #if defined(USE_WOLFSSL_MEMORY) && \ !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ - !defined(WOLFSSL_SMALL_STACK) && \ - !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) && !defined(NO_TLS) + !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) && \ + !defined(NO_TLS) int n; int injected = 0; int total; From 08ee7410e6a94b0995f43d0abefb11840b449745 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 9 Sep 2026 08:31:53 +0200 Subject: [PATCH 57/60] tests: check API availability guards before the build does tests/api is one binary compiled in every CI configuration, so a test calling an API the build did not compile is not a test failure -- it is a link error that takes the whole binary down. It is also invisible to header inspection, because wolfSSL declares plenty of API unconditionally and implements it under a narrower condition. That combination broke CI four separate times on this branch, each time found by CI rather than locally, and each time the fix was the same: name what the build provides, not what the test needs. check-api-guards.py walks the enclosing #if chain of every call site and requires the macros the IMPLEMENTATION carries. It is a whitelist rather than a parse of ssl.h on purpose: the mapping from symbol to implementation guard cannot be derived from the declaration, which is the whole problem. Two things make it usable rather than noisy: It only looks at call sites this branch changed. Run over everything it reports 28 long-standing sites that are fine in practice because the configurations that would break them are not built; auditing those is a different job, and --all still does it. It knows which macros imply TLS. A block under WOLFSSL_TLS13 or HAVE_SNI cannot also need !defined(NO_TLS) spelled out, and comments and string literals are blanked before matching, since these files discuss the very API names being searched for. It refuses to run against a ref it cannot resolve rather than reporting success, because a shallow checkout would otherwise make every diff empty and the check would pass without looking at anything. The workflow checks out with fetch-depth: 0 for that reason, and runs the check before the smoke build -- it needs no build and costs a second. Verified both directions: clean on this branch, and it reports the exact site when !defined(NO_TLS) is removed from a guard that needs it. --- .github/workflows/whitebox-smoke.yml | 15 ++ tests/api/check-api-guards.py | 212 +++++++++++++++++++++++++++ tests/api/include.am | 3 +- 3 files changed, 229 insertions(+), 1 deletion(-) create mode 100755 tests/api/check-api-guards.py diff --git a/.github/workflows/whitebox-smoke.yml b/.github/workflows/whitebox-smoke.yml index 8465dab657f..138f43a578e 100644 --- a/.github/workflows/whitebox-smoke.yml +++ b/.github/workflows/whitebox-smoke.yml @@ -36,6 +36,10 @@ jobs: timeout-minutes: 20 steps: - uses: actions/checkout@v4 + with: + # check-api-guards.py diffs against the base branch, so it + # needs more than the default shallow checkout. + fetch-depth: 0 - name: Install build dependencies run: | @@ -51,6 +55,17 @@ jobs: CPPFLAGS=-DWOLFSSL_TEST_STATIC_BUILD make -j"$(nproc)" + - name: Check API availability guards + # tests/api is one binary built in every CI configuration, so a test + # that calls an API the build did not compile is a link error that + # takes the whole binary down -- and it is invisible to anything that + # only reads headers, because plenty of API is declared unconditionally + # and implemented under a narrower condition. This checks the call + # sites this branch changed against the conditions the implementations + # actually carry. It costs a second and needs no build, so it runs + # before the smoke build rather than after it. + run: python3 tests/api/check-api-guards.py origin/${{ github.base_ref || 'master' }} + - name: Run white-box smoke # smoke-expected.txt is generated with gcc; six TUs build under clang # and not gcc, so the compiler has to match or the run reports false diff --git a/tests/api/check-api-guards.py b/tests/api/check-api-guards.py new file mode 100755 index 00000000000..d43317c301c --- /dev/null +++ b/tests/api/check-api-guards.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# +# check-api-guards.py [--all] [base-ref] +# +# Every entry in tests/api is compiled into the one unit.test binary, in every +# configuration CI builds. A test that calls an API the build did not compile +# is not a test failure -- it is a link error that takes the whole binary down, +# and it is invisible to anything that only reads headers, because wolfSSL +# declares plenty of API unconditionally and implements it under a narrower +# condition. That combination has broken CI here four separate times: a guard +# that names what the test NEEDS rather than what the build PROVIDES. +# +# This checks the other direction. For each API below it walks every call site's +# enclosing #if chain and requires the macros the IMPLEMENTATION requires. Run +# it over the test sources; it exits non-zero if a call site is not covered. +# +# Adding an entry: find where the function is defined (not declared) and copy +# the conditions around it. "requires_off" are macros that must be excluded, +# "requires_on" macros that must be present. +# +# It is deliberately a whitelist rather than a parse of ssl.h: the mapping from +# symbol to implementation guard cannot be derived from the declaration, which +# is the entire problem it exists to catch. +# +# By default it only looks at call sites on lines this branch added or changed +# against the base ref (origin/master), which is what makes it usable as a +# pre-push check. Running it over everything reports plenty of long-standing +# call sites that are fine in practice, because the configurations that would +# break them are not built -- auditing those is a different job. --all does +# that anyway. +import re +import sys +import glob + +# symbol -> (must be excluded, must be defined), from the definition site +API = { + # src/ssl.c, under !NO_WOLFSSL_CLIENT && !NO_TLS + 'wolfSSLv23_client_method': (['NO_TLS', 'NO_WOLFSSL_CLIENT'], []), + 'wolfSSLv23_server_method': (['NO_TLS', 'NO_WOLFSSL_SERVER'], []), + # src/tls.c, additionally under WOLFSSL_DTLS && !WOLFSSL_NO_TLS12: + # DTLS 1.2 is built out of the TLS 1.2 code + 'wolfDTLSv1_2_client_method': (['NO_WOLFSSL_CLIENT', 'WOLFSSL_NO_TLS12'], + ['WOLFSSL_DTLS']), + 'wolfDTLSv1_2_server_method': (['NO_WOLFSSL_SERVER', 'WOLFSSL_NO_TLS12'], + ['WOLFSSL_DTLS']), + # src/ssl_api_ext.c: each sits under !NO_TLS as well as its own feature + 'wolfSSL_UseSNI': (['NO_TLS'], ['HAVE_SNI']), + 'wolfSSL_CTX_UseSNI': (['NO_TLS'], ['HAVE_SNI']), + 'wolfSSL_SNI_GetRequest': (['NO_TLS', 'NO_WOLFSSL_SERVER'], ['HAVE_SNI']), + 'wolfSSL_SNI_GetFromBuffer': (['NO_TLS', 'NO_WOLFSSL_SERVER'], ['HAVE_SNI']), + 'wolfSSL_UseSupportedCurve': (['NO_TLS'], ['HAVE_SUPPORTED_CURVES']), + 'wolfSSL_CTX_UseSupportedCurve': (['NO_TLS'], ['HAVE_SUPPORTED_CURVES']), + # wolfcrypt/src/memory.c, under USE_WOLFSSL_MEMORY -- which --enable-leantls + # removes by way of WOLFSSL_LEANPSK + 'wolfSSL_SetAllocators': ([], ['USE_WOLFSSL_MEMORY']), + 'wolfSSL_GetAllocators': ([], ['USE_WOLFSSL_MEMORY']), +} + + +def strip_comments(text): + """Blank out comments and string literals, keeping every newline so line + numbers still line up. Without this the scan matches the API names in the + explanatory comments these tests are full of.""" + out = [] + i, n = 0, len(text) + while i < n: + c = text[i] + if c == '/' and i + 1 < n and text[i + 1] == '*': + j = text.find('*/', i + 2) + j = n if j < 0 else j + 2 + out.append(''.join(ch if ch == '\n' else ' ' for ch in text[i:j])) + i = j + elif c == '/' and i + 1 < n and text[i + 1] == '/': + j = text.find('\n', i) + j = n if j < 0 else j + out.append(' ' * (j - i)) + i = j + elif c in '"\'': + q, j = c, i + 1 + while j < n and text[j] != q: + j += 2 if text[j] == '\\' else 1 + j = min(j + 1, n) + out.append(''.join(ch if ch == '\n' else ' ' for ch in text[i:j])) + i = j + else: + out.append(c) + i += 1 + return ''.join(out) + + +def guard_chain(lines, upto): + """The #if directives open at line `upto`, joined, continuations included.""" + stack = [] + for i, line in enumerate(lines[:upto], 1): + s = line.strip() + if re.match(r'#\s*if', s): + text, j = [], i - 1 + while True: + text.append(lines[j].strip()) + if not lines[j].rstrip().endswith('\\'): + break + j += 1 + stack.append(' '.join(text)) + elif re.match(r'#\s*endif', s): + if stack: + stack.pop() + elif re.match(r'#\s*el(se|if)', s): + if stack: + stack[-1] = s + return ' '.join(stack) + + +# Macros that are only ever defined in a build that has TLS, so a block already +# guarded by one of them cannot also need !defined(NO_TLS) spelled out. +IMPLIES_TLS = ( + 'WOLFSSL_TLS13', 'WOLFSSL_DTLS', 'WOLFSSL_DTLS13', 'HAVE_SNI', 'HAVE_ALPN', + 'HAVE_SESSION_TICKET', 'HAVE_SECURE_RENEGOTIATION', 'HAVE_MAX_FRAGMENT', + 'HAVE_SUPPORTED_CURVES', 'HAVE_EXTENDED_MASTER', 'HAVE_TRUSTED_CA', + 'HAVE_ENCRYPT_THEN_MAC', 'HAVE_SERVER_RENEGOTIATION_INFO', + 'HAVE_CERTIFICATE_STATUS_REQUEST', 'HAVE_TLS_EXTENSIONS', 'HAVE_ECH', +) + + +def require_ref(base): + """Refuse to run against a ref git cannot resolve. + + A shallow checkout has no base branch, and then every diff comes back + empty and the check passes without looking at anything -- which is worse + than not running it, because it reports success. Fail loudly instead. + """ + import subprocess + r = subprocess.run(['git', 'rev-parse', '--verify', '--quiet', base + '^{commit}'], + capture_output=True, text=True) + if r.returncode != 0: + sys.stderr.write( + f"check-api-guards: cannot resolve '{base}'.\n" + f" The diff scope needs it. In CI, check out with fetch-depth: 0;\n" + f" locally, fetch the base branch, or pass --all to scan every\n" + f" call site instead.\n") + sys.exit(2) + + +def changed_lines(path, base): + """Line numbers this branch added or changed in path.""" + import subprocess + out = subprocess.run(['git', 'diff', '-U0', f'{base}...HEAD', '--', path], + capture_output=True, text=True).stdout + hit = set() + for m in re.finditer(r'^@@ -\S+ \+(\d+)(?:,(\d+))? @@', out, re.M): + start = int(m.group(1)) + count = int(m.group(2)) if m.group(2) else 1 + hit.update(range(start, start + count)) + return hit + + +def check(path, only=None): + lines = strip_comments(open(path, errors='replace').read()).split('\n') + bad = [] + for i, line in enumerate(lines, 1): + if only is not None and i not in only: + continue + for sym, (off, on) in API.items(): + if not re.search(r'\b' + re.escape(sym) + r'\s*\(', line): + continue + chain = guard_chain(lines, i) + miss_off = [m for m in off + if f'!defined({m})' not in chain and f'ifndef {m}' not in chain] + miss_on = [m for m in on + if f'defined({m})' not in chain and f'ifdef {m}' not in chain] + # WOLFSSL_DTLS implies TLS is compiled in, so a DTLS-guarded block + # never needs !NO_TLS spelled out as well. + if any(f'defined({m})' in chain or f'ifdef {m}' in chain + for m in IMPLIES_TLS): + miss_off = [m for m in miss_off if m != 'NO_TLS'] + if miss_off or miss_on: + bad.append((i, sym, miss_off, miss_on)) + return bad + + +def main(): + args = [a for a in sys.argv[1:]] + scan_all = '--all' in args + if scan_all: + args.remove('--all') + base = args[0] if args else 'origin/master' + if not scan_all: + require_ref(base) + paths = sorted(glob.glob('tests/api/test_*.c')) + total = 0 + for path in paths: + only = None if scan_all else changed_lines(path, base) + if only is not None and not only: + continue + for line, sym, off, on in check(path, only): + need = [] + if off: + need.append('!defined(' + '), !defined('.join(off) + ')') + if on: + need.append('defined(' + '), defined('.join(on) + ')') + print(f'{path}:{line}: {sym} needs {" and ".join(need)}') + total += 1 + scope = 'every call site' if scan_all else f'call sites changed since {base}' + if total: + print(f'\n{total} call site(s) reachable in a build that does not ' + f'implement the API ({scope})') + return 1 + print(f'api guards: {scope} covered') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/api/include.am b/tests/api/include.am index efb49b40c32..4e60f7a4301 100644 --- a/tests/api/include.am +++ b/tests/api/include.am @@ -147,7 +147,8 @@ 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 +EXTRA_DIST += tests/api/check-api-guards.py \ + tests/api/api.h EXTRA_DIST += tests/api/api_decl.h EXTRA_DIST += tests/api/test_md2.h EXTRA_DIST += tests/api/test_md4.h From ea10d4104c3d2832d015b57b6ceddb2da537ba44 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 9 Sep 2026 08:45:41 +0200 Subject: [PATCH 58/60] tests: pin the session-cache ring-start conditions Both session caches walk their row backwards from the most recently used entry, and both start the walk with idx = row->nextIdx - 1; if (idx < 0 || idx >= SESSIONS_PER_ROW) nextIdx is the ring's insertion point, so idx lands in [-1, PER_ROW-1] and the first operand is true exactly when nextIdx is 0 -- an untouched row, or one that has just wrapped. Whether any test produced that state was decided by what an earlier test in the same binary had left in the cache, not by anything the test itself did. That is not hypothetical: ssl_sess.c measured 32/120 on 2026-09-05 and 33/120 on 2026-09-06 from identical wolfssl and campaign commits on the same host, and 33 is what this now reaches every run. The white-box empties the rows itself and looks up against them, then repeats each lookup with nextIdx in the middle of the ring, because both halves have to run in one binary or the operand has no independence pair -- a first attempt drove only the true side and scored zero. Two details cost a measurement each and are worth writing down: ClientCache is CLIENT_SESSION_ROWS long while SessionCache is SESSION_ROWS, so zeroing the first with the second's bound leaves the row the id hashes to untouched; and wolfSSL_GetSessionClient returns before the ring walk when the context has the cache switched off, which makes every vector a silent no-op. ssl_sess.c 32/120 -> 33/120. The second operand of each guard is excluded rather than chased: nextIdx is bounded by the ring at every write, so idx >= PER_ROW cannot occur and has no pair in any configuration. --- tests/unit-mcdc/smoke-expected.txt | 1 + tests/unit-mcdc/test_ssl_sess_whitebox.c | 215 +++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 tests/unit-mcdc/test_ssl_sess_whitebox.c diff --git a/tests/unit-mcdc/smoke-expected.txt b/tests/unit-mcdc/smoke-expected.txt index 9b3892db0e9..66bd3125e94 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -66,6 +66,7 @@ test_sp_c64_whitebox test_sp_cortexm_crafted_whitebox test_sp_cortexm_fault_whitebox test_ssl_certman_whitebox +test_ssl_sess_whitebox test_tfm_fault_whitebox test_tfm_whitebox test_tls13_null_whitebox diff --git a/tests/unit-mcdc/test_ssl_sess_whitebox.c b/tests/unit-mcdc/test_ssl_sess_whitebox.c new file mode 100644 index 00000000000..5fa5620be24 --- /dev/null +++ b/tests/unit-mcdc/test_ssl_sess_whitebox.c @@ -0,0 +1,215 @@ +/* test_ssl_sess_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-1301, USA + */ + +/* Both session caches walk their row backwards from the most recently used + * entry, and both start that walk the same way: + * + * idx = row->nextIdx - 1; + * if (idx < 0 || idx >= SESSIONS_PER_ROW) + * idx = SESSIONS_PER_ROW - 1; -- back to front, previous was end + * + * nextIdx is the ring's insertion point and lives in [0, SESSIONS_PER_ROW), so + * idx lands in [-1, SESSIONS_PER_ROW-1]. The first operand is true exactly + * when nextIdx is 0 -- an untouched row, or one that has just wrapped. The + * second cannot be true at all while the ring is maintained correctly; it + * defends against a corrupted row, and is recorded as an exclusion rather than + * chased. + * + * Both halves have to run in this one binary or neither operand pairs: a + * lookup that only ever starts on an empty ring shows idx < 0 true and never + * false, which is no independence pair and scores nothing. Each vector below + * is therefore run twice, once against nextIdx 0 and once against a nextIdx + * in the middle of the ring. + * + * Whether the first operand is covered therefore depends on what the cache + * happens to hold when some earlier test in the same binary last touched it -- + * not on anything this file does. That is not a hypothetical: ssl_sess.c + * measured 32/120 one night and 33/120 the next from identical wolfssl and + * campaign commits on the same host. Driving the lookup against a row that is + * known to be empty makes the operand a property of the test instead. + * + * The caches are file-static in ssl_sess.c, which is #included into ssl.c, so + * this white-box includes src/ssl.c and the module entry names src/ssl.c as + * its "tu" while reporting against src/ssl_sess.c. + * + * Rules, as for the sibling drivers: + * - options.h FIRST, or the smoke build compiles this with the feature + * macros undefined and it silently becomes a no-op that still exits 0. + * - main() ALWAYS returns 0; a non-zero exit discards the whole variant. + */ + +#include + +#include + +#include +#include + +#if !defined(WOLFCRYPT_ONLY) && !defined(NO_TLS) && !defined(NO_SESSION_CACHE) \ + && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_CLIENT_CACHE) \ + && !defined(NO_CERTS) + +static int g_checks; + +int main(void) +{ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte id[ID_LEN]; + WOLFSSL_SESSION* got; + + wolfSSL_Init(); + + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + if (ctx == NULL) { + printf("ssl_sess white-box: no CTX\n"); + goto done; + } + ssl = wolfSSL_new(ctx); + if (ssl == NULL) { + printf("ssl_sess white-box: no SSL\n"); + goto done; + } + + XMEMSET(id, 0x5C, sizeof(id)); + + /* wolfSSL_GetSessionClient returns before the ring walk if the context has + * the cache switched off, which would make every vector below a no-op. */ + ctx->sessionCacheOff = 0; + ssl->options.side = WOLFSSL_CLIENT_END; + + /* Every client row emptied: nextIdx 0 everywhere, so the backwards walk + * starts at idx == -1 whichever row the id hashes to. totalCount 0 keeps + * the loop below it from reading entries that were never written. */ + { + int r; + for (r = 0; r < CLIENT_SESSION_ROWS; r++) { + ClientCache[r].nextIdx = 0; + ClientCache[r].totalCount = 0; + } + } + got = wolfSSL_GetSessionClient(ssl, id, (int)sizeof(id)); + printf(" GetSessionClient on an empty ring (nextIdx 0) -> %s\n", + got == NULL ? "no session" : "session"); + g_checks++; + + /* Same shape on the server-side cache, reached through the lookup that + * TlsSessionCacheGetAndLock serves. */ + { + int r; + for (r = 0; r < SESSION_ROWS; r++) { + SessionCache[r].nextIdx = 0; + SessionCache[r].totalCount = 0; + } + } + { + const WOLFSSL_SESSION* s = NULL; + word32 lockedRow = 0; + int ret; + + ret = TlsSessionCacheGetAndLock(id, &s, &lockedRow, 1, + WOLFSSL_CLIENT_END); + if (ret == 0) + TlsSessionCacheUnlockRow(lockedRow); + printf(" TlsSessionCacheGetAndLock on an empty ring -> ret %d\n", + ret); + g_checks++; + } + + /* And once more with a row that has wrapped: nextIdx back at 0 with + * entries present, which is the other way the first operand goes true. */ + { + int r; + for (r = 0; r < CLIENT_SESSION_ROWS; r++) { + ClientCache[r].nextIdx = 0; + ClientCache[r].totalCount = CLIENT_SESSIONS_PER_ROW; + } + } + got = wolfSSL_GetSessionClient(ssl, id, (int)sizeof(id)); + printf(" GetSessionClient on a wrapped ring -> %s\n", + got == NULL ? "no match" : "match"); + g_checks++; + + /* The accepting partner for both, without which neither operand pairs: + * nextIdx in the middle of the ring, so idx is >= 0 and < the row size and + * the guard is (F,F). */ + { + int r; + for (r = 0; r < CLIENT_SESSION_ROWS; r++) { + ClientCache[r].nextIdx = 1; + ClientCache[r].totalCount = 1; + } + for (r = 0; r < SESSION_ROWS; r++) { + SessionCache[r].nextIdx = 1; + SessionCache[r].totalCount = 1; + } + } + got = wolfSSL_GetSessionClient(ssl, id, (int)sizeof(id)); + printf(" GetSessionClient mid-ring (nextIdx 1) -> %s\n", + got == NULL ? "no match" : "match"); + g_checks++; + { + const WOLFSSL_SESSION* s2 = NULL; + word32 lockedRow2 = 0; + int ret2; + + ret2 = TlsSessionCacheGetAndLock(id, &s2, &lockedRow2, 1, + WOLFSSL_CLIENT_END); + if (ret2 == 0) + TlsSessionCacheUnlockRow(lockedRow2); + printf(" TlsSessionCacheGetAndLock mid-ring -> ret %d\n", + ret2); + g_checks++; + } + + /* Leave the caches as they were found. */ + { + int r; + for (r = 0; r < CLIENT_SESSION_ROWS; r++) { + ClientCache[r].nextIdx = 0; + ClientCache[r].totalCount = 0; + } + for (r = 0; r < SESSION_ROWS; r++) { + SessionCache[r].nextIdx = 0; + SessionCache[r].totalCount = 0; + } + } + + printf("ssl_sess white-box: %d vectors driven\n", g_checks); + +done: + if (ssl != NULL) + wolfSSL_free(ssl); + if (ctx != NULL) + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else + +int main(void) +{ + printf("ssl_sess white-box: skipped (needs the session and client caches)\n"); + return 0; +} + +#endif From 2fcaeff5c76994ace996bb128fe97eb3f34c0f5d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 9 Sep 2026 11:16:11 +0200 Subject: [PATCH 59/60] tests: ship the two new white-boxes, and guard a dereference after a failed Expect Two PRB findings, unrelated to each other. tests/unit-mcdc/test_ssl_certman_whitebox.c and test_ssl_sess_whitebox.c were never added to EXTRA_DIST, so make dist left them out. Confirmed by building a tarball before and after: the two were the only files this branch adds that were missing, and both are in it now. Every other file the branch adds was already listed. test_wolfSSL_ocsp_stapling_accessors wrote cssl->ocspProducedDateFormat directly. Expect* records a failure and carries on rather than returning, so on the path where wolfSSL_new() failed that is a dereference of NULL, which is what the static analyser reported at the ExpectNotNull above it. The three direct field writes are guarded; the calls that merely pass cssl are safe either way, because the API checks it. Checked the rest of the file for the same shape rather than fixing only the reported line: of the raw dereferences of Expect-obtained pointers, the DTLS ones were already inside if (dssl != NULL) blocks and the remainder are inside Expect* macros, which short-circuit once a previous one has failed. These three were the only unguarded ones. --- tests/api/test_ssl_cert.c | 26 +++++++++++++++++--------- tests/include.am | 2 ++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 96960eebf70..f065fb632b2 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2694,15 +2694,23 @@ int test_wolfSSL_ocsp_stapling_accessors(void) /* Drive the format operands directly. A stapled response carrying a * GeneralizedTime rather than a UTCTime is legal, rare, and not * something the test responder emits -- so this is the only way the - * second half of that decision is ever taken. */ - cssl->ocspProducedDateFormat = ASN_UTC_TIME; - (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), &fmt); - (void)wolfSSL_get_ocsp_producedDate(cssl, NULL, sizeof(when), &fmt); - (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), NULL); - (void)wolfSSL_get_ocsp_producedDate(cssl, when, 1, &fmt); - cssl->ocspProducedDateFormat = ASN_GENERALIZED_TIME; - (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), &fmt); - cssl->ocspProducedDateFormat = 0; + * second half of that decision is ever taken. + * + * Guarded because Expect* records a failure and carries on rather than + * returning: if wolfSSL_new() above failed, cssl is NULL here and + * these are dereferences of it, which is what the static analyser + * reported. Every call that merely PASSES cssl is safe either way -- + * the API checks it -- so only the direct field writes need this. */ + if (cssl != NULL) { + cssl->ocspProducedDateFormat = ASN_UTC_TIME; + (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), &fmt); + (void)wolfSSL_get_ocsp_producedDate(cssl, NULL, sizeof(when), &fmt); + (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), NULL); + (void)wolfSSL_get_ocsp_producedDate(cssl, when, 1, &fmt); + cssl->ocspProducedDateFormat = ASN_GENERALIZED_TIME; + (void)wolfSSL_get_ocsp_producedDate(cssl, when, sizeof(when), &fmt); + cssl->ocspProducedDateFormat = 0; + } } #endif diff --git a/tests/include.am b/tests/include.am index 73818f8e268..038042ba62a 100644 --- a/tests/include.am +++ b/tests/include.am @@ -173,6 +173,8 @@ EXTRA_DIST += \ tests/unit-mcdc/test_internal_certerror_whitebox.c \ tests/unit-mcdc/test_internal_clienthello_whitebox.c \ tests/unit-mcdc/test_internal_cryptocb_whitebox.c \ + tests/unit-mcdc/test_ssl_certman_whitebox.c \ + tests/unit-mcdc/test_ssl_sess_whitebox.c \ tests/unit-mcdc/test_internal_dhskehash_whitebox.c \ tests/unit-mcdc/test_internal_eddsa_whitebox.c \ tests/unit-mcdc/test_internal_nullguard_whitebox.c \ From 4cfc07d672d6b3c1195d3ce4fef85a124742122d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 9 Sep 2026 14:14:46 +0200 Subject: [PATCH 60/60] tests: uppercase numeric literal suffixes, and make the allocation sweep opt-in Two Jenkins findings. --- tests/api/test_asn.c | 8 ++-- tests/api/test_dtls.c | 6 +-- tests/api/test_ssl_cert.c | 21 ++++++++-- .../unit-mcdc/test_internal_sanity_whitebox.c | 40 +++++++++---------- tests/unit-mcdc/test_wc_xmss_impl_whitebox.c | 2 +- 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 8c1efaf0bb4..d3a700adcd0 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -3863,17 +3863,17 @@ static int test_asn_findValidityTime(const byte* der, word32 derSz, byte tag, { word32 i, j; - if (der == NULL || derSz < (word32)contentSz + 2u) + if (der == NULL || derSz < (word32)contentSz + 2U) return 0; - for (i = 0; i + 2u + contentSz <= derSz; i++) { + for (i = 0; i + 2U + contentSz <= derSz; i++) { if (der[i] != tag || der[i + 1] != contentSz) continue; - for (j = 0; j < (word32)contentSz - 1u; j++) { + for (j = 0; j < (word32)contentSz - 1U; j++) { if (der[i + 2 + j] < '0' || der[i + 2 + j] > '9') break; } - if (j == (word32)contentSz - 1u && + if (j == (word32)contentSz - 1U && der[i + 2 + j] == 'Z') { return 1; } diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index 65c1d480509..69c7a531704 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -8591,7 +8591,7 @@ static void df_pol_replay(DfCtx* c, DfPkt* p) static void df_pol_seq_future(DfCtx* c, DfPkt* p) { if (p->idx != c->target) return; - df_set_seq(p, 0, 0x000FFFFFu); + df_set_seq(p, 0, 0x000FFFFFU); c->nMod++; } @@ -8645,7 +8645,7 @@ static void df_pol_frag_beyond(DfCtx* c, DfPkt* p) if (p->idx != c->target || p->type != handshake) return; if (p->len < DFH_HDR_SZ + DFHS_HDR_SZ) return; hs = p->data + DFH_HDR_SZ; - df_set_u24(hs + DFHS_FRAGOFF, 0x00FFFFu); + df_set_u24(hs + DFHS_FRAGOFF, 0x00FFFFU); c->nMod++; } @@ -8657,7 +8657,7 @@ static void df_pol_frag_over(DfCtx* c, DfPkt* p) if (p->idx != c->target || p->type != handshake) return; if (p->len < DFH_HDR_SZ + DFHS_HDR_SZ) return; hs = p->data + DFH_HDR_SZ; - df_set_u24(hs + DFHS_FRAGLEN, 0x00FFFFu); + df_set_u24(hs + DFHS_FRAGLEN, 0x00FFFFU); c->nMod++; } diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index f065fb632b2..aabd0da44f6 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -3133,7 +3133,22 @@ int test_wolfSSL_load_from_fifo(void) * allocator installed would break every test that runs after this one in the * same binary, which costs the whole variant. * ------------------------------------------------------------------------- */ -/* This ran everywhere except WOLFSSL_SMALL_STACK for a while, because under +/* OPT-IN, via WOLFSSL_MCDC_ALLOC_SWEEP, which the coverage campaign's own + * option list defines and no ordinary build does. + * + * Not timidity: this deliberately fails allocations dozens of times deep + * inside the library, and an allocation that fails part-way through can leave + * a global -- the session cache, a CertManager's tables -- in a state the next + * test in the same binary then trips over. The value of the sweep is the + * coverage it measures, and that is measured in the campaign build; running it + * in every CI configuration adds risk to unrelated tests and no evidence. A + * PRB run aborted with SIGABRT after a cascade of certificate-loading and + * memio failures in tests that come later in the binary, which is exactly that + * shape. It was not reproduced here in --enable-all or --enable-smallstack, in + * either order against ssl_hs, so this is not a diagnosis -- it is declining to + * carry a hazard whose only benefit is realised elsewhere. + * + * This ran everywhere except WOLFSSL_SMALL_STACK for a while, because under * that build DecodeCertInternal indexed RPKdataASN before checking the ret * that CALLOC_ASNGETDATA sets, so failing an allocation dereferenced NULL * while parsing any certificate -- a per-index sweep crashed at five @@ -3147,7 +3162,7 @@ int test_wolfSSL_load_from_fifo(void) /* wolfSSL_SetAllocators lives in wolfcrypt/src/memory.c under * #ifdef USE_WOLFSSL_MEMORY; without it the symbol is declared and never * defined. */ -#if defined(USE_WOLFSSL_MEMORY) && \ +#if defined(WOLFSSL_MCDC_ALLOC_SWEEP) && defined(USE_WOLFSSL_MEMORY) && \ !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) && \ !defined(NO_TLS) @@ -3299,7 +3314,7 @@ int test_wolfSSL_alloc_failure_sweep(void) /* wolfSSL_SetAllocators lives in wolfcrypt/src/memory.c under * #ifdef USE_WOLFSSL_MEMORY; without it the symbol is declared and never * defined. */ -#if defined(USE_WOLFSSL_MEMORY) && \ +#if defined(WOLFSSL_MCDC_ALLOC_SWEEP) && defined(USE_WOLFSSL_MEMORY) && \ !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) && \ !defined(NO_TLS) diff --git a/tests/unit-mcdc/test_internal_sanity_whitebox.c b/tests/unit-mcdc/test_internal_sanity_whitebox.c index 4b47cc9f7f9..05279f8d6a5 100644 --- a/tests/unit-mcdc/test_internal_sanity_whitebox.c +++ b/tests/unit-mcdc/test_internal_sanity_whitebox.c @@ -91,7 +91,7 @@ static void wb_set_msgs(WOLFSSL* ssl, word32 mask) XMEMSET(&ssl->msgsReceived, 0, sizeof(ssl->msgsReceived)); for (i = 0; i < M_COUNT; i++) { - if ((mask & (1u << i)) == 0) + if ((mask & (1U << i)) == 0) continue; switch (i) { case M_HELLO_REQUEST: ssl->msgsReceived.got_hello_request = 1; @@ -146,14 +146,14 @@ enum { static void wb_set_opts(WOLFSSL* ssl, word32 mask) { - ssl->options.resuming = (mask & (1u << O_RESUMING)) ? 1 : 0; - ssl->options.verifyPeer = (mask & (1u << O_VERIFY_PEER)) ? 1 : 0; - ssl->options.usingPSK_cipher = (mask & (1u << O_PSK_CIPHER)) ? 1 : 0; - ssl->options.usingAnon_cipher= (mask & (1u << O_ANON_CIPHER)) ? 1 : 0; - ssl->options.havePeerCert = (mask & (1u << O_HAVE_PEER_CERT)) ? 1 : 0; - ssl->options.havePeerVerify = (mask & (1u << O_HAVE_PEER_VERIFY)) ? 1 : 0; + ssl->options.resuming = (mask & (1U << O_RESUMING)) ? 1 : 0; + ssl->options.verifyPeer = (mask & (1U << O_VERIFY_PEER)) ? 1 : 0; + ssl->options.usingPSK_cipher = (mask & (1U << O_PSK_CIPHER)) ? 1 : 0; + ssl->options.usingAnon_cipher= (mask & (1U << O_ANON_CIPHER)) ? 1 : 0; + ssl->options.havePeerCert = (mask & (1U << O_HAVE_PEER_CERT)) ? 1 : 0; + ssl->options.havePeerVerify = (mask & (1U << O_HAVE_PEER_VERIFY)) ? 1 : 0; #ifdef WOLFSSL_DTLS - ssl->options.dtls = (mask & (1u << O_DTLS)) ? 1 : 0; + ssl->options.dtls = (mask & (1U << O_DTLS)) ? 1 : 0; #endif } @@ -180,8 +180,8 @@ static void wb_call(WOLFSSL* ssl, byte type, int side, word32 msgs, * exchange values that server_hello_done and certificate_request test. */ static void wb_sweep_type(WOLFSSL* ssl, byte type, int side) { - const word32 msgAll = (1u << M_COUNT) - 1u; - const word32 optAll = (1u << O_COUNT) - 1u; + const word32 msgAll = (1U << M_COUNT) - 1U; + const word32 optAll = (1U << O_COUNT) - 1U; word32 optEnds[2]; int i, e; @@ -193,8 +193,8 @@ static void wb_sweep_type(WOLFSSL* ssl, byte type, int side) wb_call(ssl, type, side, 0, optEnds[e], rsa_kea, 0); wb_call(ssl, type, side, msgAll, optEnds[e], rsa_kea, 0); for (i = 0; i < M_COUNT; i++) { - wb_call(ssl, type, side, 1u << i, optEnds[e], rsa_kea, 0); - wb_call(ssl, type, side, msgAll & ~(1u << i), optEnds[e], + wb_call(ssl, type, side, 1U << i, optEnds[e], rsa_kea, 0); + wb_call(ssl, type, side, msgAll & ~(1U << i), optEnds[e], rsa_kea, 0); } } @@ -203,8 +203,8 @@ static void wb_sweep_type(WOLFSSL* ssl, byte type, int side) for (e = 0; e < 2; e++) { word32 msgs = e ? msgAll : 0; for (i = 0; i < O_COUNT; i++) { - wb_call(ssl, type, side, msgs, 1u << i, rsa_kea, 0); - wb_call(ssl, type, side, msgs, optAll & ~(1u << i), rsa_kea, 0); + wb_call(ssl, type, side, msgs, 1U << i, rsa_kea, 0); + wb_call(ssl, type, side, msgs, optAll & ~(1U << i), rsa_kea, 0); } } @@ -213,7 +213,7 @@ static void wb_sweep_type(WOLFSSL* ssl, byte type, int side) * negotiates one kea per connection. The message state omits * server_key_exchange so the enclosing decision is entered. */ { - const word32 msgs = msgAll & ~(1u << M_SERVER_KEY_EXCH); + const word32 msgs = msgAll & ~(1U << M_SERVER_KEY_EXCH); static const byte keas[3] = { rsa_kea, psk_kea, ecc_diffie_hellman_kea }; size_t k; @@ -250,7 +250,7 @@ typedef struct { const char* what; } SanityBase; -#define B(x) (1u << (x)) +#define B(x) (1U << (x)) static const SanityBase kBases[] = { { hello_request, WOLFSSL_CLIENT_END, 0, "HelloRequest" }, @@ -302,7 +302,7 @@ static const SanityBase kBases[] = { static void wb_sweep_baselines(WOLFSSL* ssl) { - const word32 optAll = (1u << O_COUNT) - 1u; + const word32 optAll = (1U << O_COUNT) - 1U; size_t r; int i; @@ -314,14 +314,14 @@ static void wb_sweep_baselines(WOLFSSL* ssl) /* one operand true at a time, the rest still false */ for (i = 0; i < M_COUNT; i++) - wb_call(ssl, b->type, b->side, b->base ^ (1u << i), 0, rsa_kea, 0); + wb_call(ssl, b->type, b->side, b->base ^ (1U << i), 0, rsa_kea, 0); /* the option operands in the same chains -- resuming, verifyPeer, * usingPSK_cipher, usingAnon_cipher, havePeerCert, havePeerVerify, * dtls -- paired against the accepting baseline. */ for (i = 0; i < O_COUNT; i++) { - wb_call(ssl, b->type, b->side, b->base, 1u << i, rsa_kea, 0); - wb_call(ssl, b->type, b->side, b->base, optAll & ~(1u << i), + wb_call(ssl, b->type, b->side, b->base, 1U << i, rsa_kea, 0); + wb_call(ssl, b->type, b->side, b->base, optAll & ~(1U << i), rsa_kea, 0); } diff --git a/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c b/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c index 69896aae79e..4b924e120ac 100644 --- a/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c +++ b/tests/unit-mcdc/test_wc_xmss_impl_whitebox.c @@ -1353,7 +1353,7 @@ static void wb_bds_hardening(void) wc_xmss_bds_next_idx(&state, &bds[0], sk_seed, pk_seed, addr, rows[i].i, height, &offset, &sp); - printf(" [wb] bds_next_idx i=%-2u %s -> ret %d\n", + printf(" [wb] bds_next_idx i=%-2U %s -> ret %d\n", (unsigned)rows[i].i, rows[i].what, state.ret); if (rows[i].expectFail && (state.ret == 0)) { WB_NOTE("FAIL: retain guard did not fire");