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.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/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 diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index d6f9924e1ee..8c1efaf0bb4 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -3852,6 +3852,133 @@ 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); + + /* 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); + 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_dtls.c b/tests/api/test_dtls.c index f91ea99eb1c..65c1d480509 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -8154,3 +8154,1517 @@ 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(); +} + +/* =========================================================================== + * 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. + * ========================================================================= */ +/* 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_WOLFSSL_CLIENT) && \ + !defined(NO_WOLFSSL_SERVER) && !defined(WOLFSSL_ASYNC_CRYPT) + +#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. */ +/* 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)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++; +} + + +/* ======================= 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_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; + 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; + +#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 +/* 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. */ + 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); + 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_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[] = { + 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, + /* 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; + + /* 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); + + /* 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); +} + +#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) \ + && !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 + 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) \ + && !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 + return EXPECT_RESULT(); +} + +/* --------------------------------------------------------------------------- + * Connection ID argument guards. + * + * 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) + * 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 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 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) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_DTLS_CID) && defined(WOLFSSL_DTLS) && !defined(NO_RSA) && \ + !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 */ + 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_use(NULL)); + (void)(wolfSSL_dtls_cid_is_enabled(NULL)); + (void)(wolfSSL_dtls_cid_set(NULL, cid, (word32)sizeof(cid))); + (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 */ + (void)(wolfSSL_dtls_cid_set(enabled, NULL, (word32)sizeof(cid))); + (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 68904fb6123..8ef6eaf6766 100644 --- a/tests/api/test_dtls.h +++ b/tests/api/test_dtls.h @@ -22,6 +22,12 @@ #ifndef TESTS_API_DTLS_H #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); + int test_dtls12_basic_connection_id(void); int test_wolfSSL_dtls_cid_parse(void); int test_wolfSSL_dtls_cid_args(void); @@ -131,6 +137,11 @@ 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_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),\ TEST_DECL_GROUP("dtls", test_dtls13_new_connection_id), \ TEST_DECL_GROUP("dtls", test_dtls13_new_connection_id_not_negotiated), \ diff --git a/tests/api/test_ocsp.c b/tests/api/test_ocsp.c index ccac1491fb9..5eccaa7d88f 100644 --- a/tests/api/test_ocsp.c +++ b/tests/api/test_ocsp.c @@ -2374,6 +2374,100 @@ 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. */ + /* 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] = '/'; + 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; @@ -2467,4 +2561,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_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 */ diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 44073b5f611..96960eebf70 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. @@ -1499,6 +1506,224 @@ 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) && !defined(NO_TLS) + 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)); + + /* --- 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 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)); + (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); +#endif /* HAVE_RPK */ + + /* --- 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) && !defined(NO_TLS) + 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 @@ -2331,3 +2556,919 @@ int test_wolfSSL_verify_post_handshake_defers(void) #endif 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; + +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; +} +#endif + +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_TLS) + 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) && \ + !defined(NO_TLS) + 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(); +} + +/* --------------------------------------------------------------------------- + * 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; +/* 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). */ +/* 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(KEEP_PEER_CERT) || \ + defined(KEEP_OUR_CERT)) + 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); + + /* --- 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(); +} + +int test_wolfSSL_dtls_api_on_dtls_object(void) +{ + EXPECT_DECLS; +#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; + 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); + /* 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)); + (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)); +#endif + + /* MTU: `ctx == NULL || newMtu > MAX_RECORD_SIZE`, both operands */ +/* 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); + (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(); +} + +/* --------------------------------------------------------------------------- + * 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) && \ + !defined(NO_TLS) + 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); + } + { + /* 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); +#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__) && !defined(NO_TLS) + 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(); +} + +/* --------------------------------------------------------------------------- + * 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. + * ------------------------------------------------------------------------- */ +/* 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(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 */ +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 + { + 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); +#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); +} + +#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; +/* 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(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_FILESYSTEM) && \ + !defined(NO_TLS) + 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 + * 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); + + /* 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(prevMalloc, prevFree, prevRealloc); + { + WOLFSSL_CTX* ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + ExpectNotNull(ctx); + wolfSSL_CTX_free(ctx); + } +#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) && \ + !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) + 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 151895bbab6..8a74aab6fac 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -22,6 +22,17 @@ #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_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_dtls_api_more_guards(void); +int test_wolfSSL_alloc_failure_sweep(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 +112,16 @@ 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), \ + 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_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 */ diff --git a/tests/api/test_ssl_ext.c b/tests/api/test_ssl_ext.c index 132485c2be9..7afbb11fe2a 100644 --- a/tests/api/test_ssl_ext.c +++ b/tests/api/test_ssl_ext.c @@ -1362,3 +1362,724 @@ 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(); +} + +/* --------------------------------------------------------------------------- + * 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) && !defined(NO_TLS) + 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)); + 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); + /* 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); + (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); + (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); + /* 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 + + /* --- 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) && !defined(NO_TLS) + 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) { + /* 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); + } + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#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) && !defined(NO_TLS) + 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); + /* 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 -------------------------- */ +#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 ------------------------------ */ + /* 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) && !defined(NO_TLS) + { + 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(); +} + +/* --------------------------------------------------------------------------- + * 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) && !defined(NO_TLS) + 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(); +} + +/* --------------------------------------------------------------------------- + * 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) && !defined(NO_TLS) + 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 ee3da3a2108..9da8d490241 100644 --- a/tests/api/test_ssl_ext.h +++ b/tests/api/test_ssl_ext.h @@ -22,6 +22,13 @@ #ifndef TESTS_API_SSL_EXT_H #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_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); int test_wolfSSL_CTX_num_tickets_ext(void); @@ -106,6 +113,12 @@ 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), \ + 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_session_lifecycle_guards) #endif /* TESTS_API_SSL_EXT_H */ 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 c19499a3f64..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 \ @@ -140,10 +143,13 @@ 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 \ 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 \ tests/unit-mcdc/test_eccsi_fault_whitebox.c \ @@ -163,6 +169,20 @@ 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_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 \ + 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 \ + 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 \ @@ -175,6 +195,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 \ @@ -236,6 +257,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/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 de097482f0a..66bd3125e94 100644 --- a/tests/unit-mcdc/smoke-expected.txt +++ b/tests/unit-mcdc/smoke-expected.txt @@ -4,7 +4,10 @@ test_blake2s_whitebox test_chacha20_poly1305_whitebox 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 test_ed25519_whitebox @@ -17,6 +20,20 @@ test_hpke_fault_whitebox test_hpke_whitebox 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 +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 +test_keys_whitebox test_lms_bds_whitebox test_lms_fault_whitebox test_lms_hash_fault_whitebox @@ -26,6 +43,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 @@ -47,6 +65,8 @@ test_sp_c32_whitebox 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 @@ -59,5 +79,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_crl_whitebox.c b/tests/unit-mcdc/test_crl_whitebox.c new file mode 100644 index 00000000000..7fd2c7b755a --- /dev/null +++ b/tests/unit-mcdc/test_crl_whitebox.c @@ -0,0 +1,534 @@ +/* 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)); +} + + +/* --------------------------------------------- CheckCertCRLCm :668, :686 */ +/* `if (cm != NULL && cm->cbMissingCRL)` and + * `if (cm != NULL && cm->crlCb && cm->crlCb(ret, crl, cm, cm->crlCbCtx))` + * + * 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)); + + /* 1. cm NULL: operand 0 false for both decisions. */ + WB_NOTE(CheckCertCRLCm(cm->crl, issuerHash, serial, (int)sizeof(serial), + NULL, (const byte*)url, (int)XSTRLEN(url), NULL, + NULL)); + + /* 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, (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; +} + + +/* ------------------------------- 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)); + } +} + + +/* ------------------------------------------------------- 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); +} + + +/* ------------------------------------------------------------- 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) +{ + 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); + wb_missing_crl_callbacks(cm); + wb_buffer_load_store(cm); + + wb_compare_crlnumber(); + 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); + +done: + if (cm != NULL) + wolfSSL_CertManagerFree(cm); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else /* not (HAVE_CRL && certs && !WOLFCRYPT_ONLY) */ + +int main(void) +{ + printf("crl white-box: skipped (needs HAVE_CRL, certs, and not WOLFCRYPT_ONLY)\n"); + return 0; +} + +#endif 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_dtls_whitebox.c b/tests/unit-mcdc/test_dtls_whitebox.c new file mode 100644 index 00000000000..1ba380f6c88 --- /dev/null +++ b/tests/unit-mcdc/test_dtls_whitebox.c @@ -0,0 +1,362 @@ +/* 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. + */ + +/* 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 + +#include + +#include +#include + +#if defined(WOLFSSL_DTLS) && !defined(WOLFCRYPT_ONLY) + +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 */ +/* 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 + * 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)); +} +#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) +{ + 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)); +} +#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 + * 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)); + } +} +#endif /* WOLFSSL_DTLS13 && !NO_WOLFSSL_SERVER */ + +/* ---------------------------------------------------------------- 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(); +#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); + +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 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 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..27149f8b437 --- /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, TLSX_RENEGOTIATION_INFO); + 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 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 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_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 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 new file mode 100644 index 00000000000..3ce3c55e638 --- /dev/null +++ b/tests/unit-mcdc/test_internal_nullguard_whitebox.c @@ -0,0 +1,585 @@ +/* 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 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; + + /* 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++; + +#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 */ + +/* ------------------------------------------------------------ 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..a8a0f727dee --- /dev/null +++ b/tests/unit-mcdc/test_internal_peerkey_whitebox.c @@ -0,0 +1,595 @@ +/* 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 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; + + 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 + + /* 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) */ + +/* --------------------------------------------------------------- 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 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 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 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..4b47cc9f7f9 --- /dev/null +++ b/tests/unit-mcdc/test_internal_sanity_whitebox.c @@ -0,0 +1,524 @@ +/* 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); + } + } + } +} + + +/* ------------------------------------------------- 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) +{ + /* 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); + wb_msgtype(ssl, ctx); + + 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 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 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 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 new file mode 100644 index 00000000000..6efbdb8b83f --- /dev/null +++ b/tests/unit-mcdc/test_ocsp_whitebox.c @@ -0,0 +1,661 @@ +/* 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. */ +/* 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; + 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)); + /* 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. */ + 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)); + wb_free_prepended(ocsp, &seeded); + + /* 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)); + + /* 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; +} + + +/* ---------------------------------------------- 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; +} + + +/* --------------------------------------------------- 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)); +} + + +/* --------------------------------------------------------- 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); +} + + +/* --------------------------------------------------------- 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) +{ + 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); + wb_check_responder(); + wb_check_request(cm); + 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); + +done: + if (cm != NULL) + wolfSSL_CertManagerFree(cm); + wolfSSL_Cleanup(); + return 0; /* always 0: a non-zero exit discards the variant */ +} + +#else /* not (HAVE_OCSP && certs && !WOLFCRYPT_ONLY) */ + +int main(void) +{ + printf("ocsp white-box: skipped (needs HAVE_OCSP, certs, and not WOLFCRYPT_ONLY)\n"); + return 0; +} + +#endif 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 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 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(); diff --git a/tests/unit-mcdc/test_wolfio_whitebox.c b/tests/unit-mcdc/test_wolfio_whitebox.c new file mode 100644 index 00000000000..bfa2ca9891a --- /dev/null +++ b/tests/unit-mcdc/test_wolfio_whitebox.c @@ -0,0 +1,322 @@ +/* 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)); +} + + +/* --------------------------------------------------------- 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) +{ + if (wolfSSL_Init() != WOLFSSL_SUCCESS) { + printf("wolfio white-box: wolfSSL_Init failed\n"); + goto done; + } + + wb_http_response(); + wb_http_request(); + wb_sockets(); + wb_decode_url(); + wb_url_crlf(); + + 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