diff --git a/src/cli/cli.c b/src/cli/cli.c index 142d8a880..884849e0b 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -2968,15 +2968,16 @@ static bool prompt_yn(const char *question) { /* Minimum line length in checksums.txt: CBM_SZ_64 hex + 2 spaces + 1 char filename */ #define CHECKSUM_LINE_MIN (SHA256_HEX_LEN + 2) -/* Compute SHA-CBM_SZ_256 of a file using platform tools (sha256sum/shasum). - * Writes CBM_SZ_64-char hex digest + NUL to out. Returns 0 on success. */ -static int sha256_file(const char *path, char *out, size_t out_size) { +/* Compute the SHA-256 of a file using platform tools (sha256sum/shasum). + * Writes a 64-char hex digest + NUL to out. Returns 0 on success. Not static: + * exercised directly by the self-update checksum regression test. */ +int cbm_cli_sha256_file(const char *path, char *out, size_t out_size) { if (out_size < SHA256_BUF_SIZE) { return CLI_ERR; } char cmd[CLI_BUF_1K]; #ifdef __APPLE__ - snprintf(cmd, sizeof(cmd), "shasum -a CBM_SZ_256 '%s' 2>/dev/null", path); + snprintf(cmd, sizeof(cmd), "shasum -a 256 '%s' 2>/dev/null", path); #else snprintf(cmd, sizeof(cmd), "sha256sum '%s' 2>/dev/null", path); #endif @@ -3107,8 +3108,8 @@ static int verify_download_checksum(const char *archive_path, const char *archiv } char actual[SHA256_BUF_SIZE] = {0}; - if (sha256_file(archive_path, actual, sizeof(actual)) != 0) { - (void)fprintf(stderr, "warning: sha256sum/shasum not available — skipping verification\n"); + if (cbm_cli_sha256_file(archive_path, actual, sizeof(actual)) != 0) { + (void)fprintf(stderr, "error: could not compute checksum (sha256 tool unavailable)\n"); return CLI_ERR; } @@ -4250,8 +4251,12 @@ static int download_verify_install(const char *url, const char *ext, const char const char *portable = (strcmp(os, "linux") == 0) ? "-portable" : ""; snprintf(archive_name, sizeof(archive_name), "codebase-memory-mcp-%s%s-%s%s.%s", want_ui ? "ui-" : "", os, arch, portable, ext); + /* Fail closed: install only a positively-verified download. A mismatch, + * a missing checksum entry, or an unavailable hash tool (crc != 0) all + * abort rather than install an unverified binary. */ int crc = verify_download_checksum(tmp_archive, archive_name); - if (crc == CLI_TRUE) { + if (crc != 0) { + (void)fprintf(stderr, "error: refusing to install an unverified download\n"); cbm_unlink(tmp_archive); return CLI_TRUE; } diff --git a/tests/test_cli.c b/tests/test_cli.c index 1aa4ea716..6f2b19c4a 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -3010,11 +3010,38 @@ TEST(cli_print_tool_help_issue680) { PASS(); } +/* The self-update path verifies a downloaded archive against a published + * checksum. That check is only meaningful if the digest is actually computed — + * a broken hash command (it once invoked `shasum -a CBM_SZ_256`, an invalid + * algorithm, from a bad macro rename inside the shell string) makes every + * digest fail, and the caller then falls through and installs unverified. + * Guard the digest itself against a known vector. */ +extern int cbm_cli_sha256_file(const char *path, char *out, size_t out_size); + +TEST(cli_sha256_file_matches_known_vector) { + char path[512]; + snprintf(path, sizeof(path), "%s/cbm_sha_XXXXXX", cbm_tmpdir()); + int fd = cbm_mkstemp(path); + ASSERT_TRUE(fd >= 0); + FILE *fp = fdopen(fd, "wb"); + ASSERT_NOT_NULL(fp); + fwrite("abc", 1, 3, fp); /* sha256("abc") is a NIST test vector */ + fclose(fp); + + char digest[128] = {0}; + int rc = cbm_cli_sha256_file(path, digest, sizeof(digest)); + remove(path); + ASSERT_EQ(rc, 0); + ASSERT_STR_EQ(digest, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + PASS(); +} + /* ═══════════════════════════════════════════════════════════════════ * Suite definition * ═══════════════════════════════════════════════════════════════════ */ SUITE(cli) { + RUN_TEST(cli_sha256_file_matches_known_vector); /* Version (2 tests — selfupdate_test.go) */ RUN_TEST(cli_compare_versions); RUN_TEST(cli_version_get_set);