From d3acd642487a3971350ff18e3c22fcdb360a97fd Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Fri, 29 May 2026 09:51:06 -0600 Subject: [PATCH] F-4836 fixed ocsp port truncation --- src/ocsp/clu_ocsp.c | 37 +++++++++-- src/tools/clu_funcs.c | 42 +++++++++++++ tests/ocsp/ocsp-test.py | 129 ++++++++++++++++++++++++++++++++++++++ wolfclu/clu_header_main.h | 10 +++ 4 files changed, 214 insertions(+), 4 deletions(-) diff --git a/src/ocsp/clu_ocsp.c b/src/ocsp/clu_ocsp.c index 0f2bd725..6b49e3d5 100644 --- a/src/ocsp/clu_ocsp.c +++ b/src/ocsp/clu_ocsp.c @@ -28,6 +28,8 @@ #include +#include /* for INT_MAX */ + enum { WOLFCLU_OCSP_HELP = 2100, WOLFCLU_OCSP_IGNORE_ERR, @@ -932,10 +934,23 @@ int wolfCLU_OcspSetup(int argc, char** argv) clientConfig.noNonce = 1; break; - case WOLFCLU_OCSP_PORT: + case WOLFCLU_OCSP_PORT: { + long pv; + if (optarg == NULL) { + wolfCLU_LogError("Option -port requires an argument: " + "must be 1-65535"); + return WOLFCLU_FATAL_ERROR; + } + if (wolfCLU_parseDecimalBounded(optarg, 1, 65535, &pv) + != WOLFCLU_SUCCESS) { + wolfCLU_LogError("Invalid port \"%s\": must be 1-65535", + optarg); + return WOLFCLU_FATAL_ERROR; + } isResponderMode = 1; - responderConfig.port = (word16)XATOI(optarg); + responderConfig.port = (word16)pv; break; + } case WOLFCLU_OCSP_IGNORE_ERR: wolfCLU_LogError("Option -ignore_err is not yet supported"); @@ -989,9 +1004,23 @@ int wolfCLU_OcspSetup(int argc, char** argv) wolfCLU_LogError("Option -nmin is not yet supported"); return WOLFCLU_FATAL_ERROR; - case WOLFCLU_OCSP_NREQUEST: - responderConfig.nrequest = XATOI(optarg); + case WOLFCLU_OCSP_NREQUEST: { + long nr; + if (optarg == NULL) { + wolfCLU_LogError("Option -nrequest requires an argument: " + "must be 0-%d", INT_MAX); + return WOLFCLU_FATAL_ERROR; + } + /* 0 means unlimited; reject negative/non-numeric/overflow */ + if (wolfCLU_parseDecimalBounded(optarg, 0, INT_MAX, &nr) + != WOLFCLU_SUCCESS) { + wolfCLU_LogError("Invalid -nrequest \"%s\": must be 0-%d", + optarg, INT_MAX); + return WOLFCLU_FATAL_ERROR; + } + responderConfig.nrequest = (int)nr; break; + } case WOLFCLU_OCSP_REQIN: wolfCLU_LogError("Option -reqin is not yet supported"); diff --git a/src/tools/clu_funcs.c b/src/tools/clu_funcs.c index d0521567..66c168c0 100644 --- a/src/tools/clu_funcs.c +++ b/src/tools/clu_funcs.c @@ -1159,6 +1159,48 @@ int wolfCLU_version(void) return WOLFCLU_SUCCESS; } +/* parse digits-only string into [minVal, maxVal] without overflow; rejects + * sign, whitespace, empty and trailing text. only non-negative digit strings + * are accepted, so the parsed value is always >= 0 and a negative minVal can + * never reject a valid input. returns WOLFCLU_SUCCESS (sets *out) or + * WOLFCLU_FATAL_ERROR. */ +int wolfCLU_parseDecimalBounded(const char* str, long minVal, long maxVal, + long* out) +{ + const char* p; + long val = 0; + int over = 0; + + if (str == NULL || out == NULL) { + return WOLFCLU_FATAL_ERROR; + } + + /* check the bound before multiplying so val never overflows; once over, + * stop accumulating but keep scanning to reject non-digits */ + for (p = str; *p != '\0'; p++) { + int digit; + if (*p < '0' || *p > '9') { + return WOLFCLU_FATAL_ERROR; + } + digit = *p - '0'; + if (!over) { + if (maxVal < digit || val > (maxVal - digit) / 10) { + over = 1; + } + else { + val = (val * 10) + digit; + } + } + } + + if (over || p == str || val < minVal || val > maxVal) { + return WOLFCLU_FATAL_ERROR; + } + + *out = val; + return WOLFCLU_SUCCESS; +} + /* return 0 for not found and index found at otherwise */ int wolfCLU_checkForArg(const char* searchTerm, int length, int argc, char** argv) diff --git a/tests/ocsp/ocsp-test.py b/tests/ocsp/ocsp-test.py index 9ad887b1..b9a05ccc 100644 --- a/tests/ocsp/ocsp-test.py +++ b/tests/ocsp/ocsp-test.py @@ -340,6 +340,135 @@ class TestOpensslClientOpensslResponder(_OCSPInteropBase): RESPONDER_BIN = "openssl" +class TestPortValidation(unittest.TestCase): + """Boundary tests for the -port range check in wolfCLU_OcspSetup. + + Validation happens during argument parsing, so no live responder is + needed -- an out-of-range or non-numeric port is rejected immediately. + The exact upper boundary (65535) is asserted directly here; a valid port + with no -CA fails the required-option check after parsing, so the bind + loop is never reached. + """ + + @classmethod + def setUpClass(cls): + if not _ocsp_supported(WOLFSSL_BIN): + raise unittest.SkipTest(f"OCSP not supported by {WOLFSSL_BIN}") + + def _run_port(self, port): + r = subprocess.run( + [WOLFSSL_BIN, "ocsp", "-port", str(port)], + capture_output=True, text=True, + stdin=subprocess.DEVNULL, timeout=10) + return r.returncode, r.stdout + r.stderr + + def _assert_rejected(self, port): + rc, out = self._run_port(port) + self.assertNotEqual(rc, 0, f"port {port!r} should be rejected: {out}") + self.assertRegex(out, re.compile("invalid port|1-65535", + re.IGNORECASE), + f"expected range diagnostic for {port!r}: {out}") + + def test_port_zero_rejected(self): + self._assert_rejected(0) + + def test_port_negative_rejected(self): + self._assert_rejected(-1) + + def test_port_above_max_rejected(self): + self._assert_rejected(65536) + + def test_port_truncation_value_rejected(self): + # 65537 would have truncated to 1 under the old word16 cast. + self._assert_rejected(65537) + + def test_port_non_numeric_rejected(self): + self._assert_rejected("abc") + + def test_port_empty_rejected(self): + self._assert_rejected("") + + def test_port_trailing_text_rejected(self): + self._assert_rejected("80x") + + def test_port_int_overflow_rejected(self): + # would wrap to a valid port under the old XATOI cast + self._assert_rejected(4294967297) # 2**32 + 1 + + def test_port_huge_input_rejected(self): + # wider than any integer type; parser must not overflow while scanning + self._assert_rejected("9" * 40) + + def test_port_max_accepted(self): + # exact upper boundary must pass the parser's overflow check; a regression + # rejecting 65535 (e.g. > vs >= slip in the bound math) is caught here. + # -CA is absent, so validation fails after parsing, never binding. + rc, out = self._run_port(65535) + self.assertNotRegex(out, re.compile("invalid port|1-65535", + re.IGNORECASE), + f"port 65535 should be accepted by parser: {out}") + + def test_port_missing_argument_rejected(self): + # trailing -port leaves optarg NULL; must emit a clean missing-argument + # diagnostic, not crash formatting NULL with %s. + r = subprocess.run( + [WOLFSSL_BIN, "ocsp", "-port"], + capture_output=True, text=True, + stdin=subprocess.DEVNULL, timeout=10) + self.assertNotEqual(r.returncode, 0, + "trailing -port should be rejected") + self.assertRegex(r.stdout + r.stderr, + re.compile("requires an argument|1-65535", + re.IGNORECASE), + "expected missing-argument diagnostic for -port") + + +class TestNrequestValidation(unittest.TestCase): + """Boundary tests for the -nrequest range check in wolfCLU_OcspSetup. + + Validation happens during parsing. -nrequest alone never enters responder + mode (that needs -port), so a valid count exits without blocking, letting + us assert the accept path too. + """ + + @classmethod + def setUpClass(cls): + if not _ocsp_supported(WOLFSSL_BIN): + raise unittest.SkipTest(f"OCSP not supported by {WOLFSSL_BIN}") + + def _run(self, nrequest): + r = subprocess.run( + [WOLFSSL_BIN, "ocsp", "-nrequest", str(nrequest)], + capture_output=True, text=True, + stdin=subprocess.DEVNULL, timeout=10) + return r.stdout + r.stderr + + def _assert_rejected(self, nrequest): + out = self._run(nrequest) + self.assertRegex(out, re.compile("invalid -nrequest", re.IGNORECASE), + f"expected diagnostic for {nrequest!r}: {out}") + + def test_nrequest_negative_rejected(self): + # old XATOI accepted -1 as a degenerate count + self._assert_rejected(-1) + + def test_nrequest_non_numeric_rejected(self): + # old XATOI returned 0, silently treated as unlimited + self._assert_rejected("abc") + + def test_nrequest_overflow_rejected(self): + self._assert_rejected(4294967297) # 2**32 + 1 + + def test_nrequest_valid_accepted(self): + # 0-means-unlimited boundary, a small count, and the exact upper bound + # (INT_MAX) must all pass parsing; INT_MAX locks in the overflow check. + for nrequest in (0, 5, 2147483647): + out = self._run(nrequest) + self.assertNotRegex( + out, re.compile("invalid -nrequest", re.IGNORECASE), + f"-nrequest {nrequest!r} should be accepted by parser: {out}") + + def load_tests(loader, tests, pattern): """Exclude the abstract _OCSPInteropBase from test discovery.""" suite = unittest.TestSuite() diff --git a/wolfclu/clu_header_main.h b/wolfclu/clu_header_main.h index 7f239b97..ca0f2625 100644 --- a/wolfclu/clu_header_main.h +++ b/wolfclu/clu_header_main.h @@ -461,6 +461,16 @@ int wolfCLU_GetOpt(int argc, char** argv, const char *options, const struct opti */ int wolfCLU_getline(char **line, size_t *len, FILE *fp); +/** + * @brief Parse a digits-only string into [minVal, maxVal] without overflow. + * Rejects sign, whitespace, empty and trailing text. Because only + * non-negative digit strings are accepted, the parsed value is always + * >= 0, so a negative minVal can never reject a valid input. + * @return WOLFCLU_SUCCESS (sets *out), or WOLFCLU_FATAL_ERROR + */ +int wolfCLU_parseDecimalBounded(const char* str, long minVal, long maxVal, + long* out); + /* * generic function to check for a specific input argument. Return the * argv[i] where argument was found. Useful for getting following value after