From be410a6be95ce0ead27c15cdcc31e3a569ea84fa Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 8 Jul 2026 00:05:52 +0200 Subject: [PATCH] cli: compute self-update checksum in-process instead of shelling out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cbm_cli_sha256_file shelled out to sha256sum/shasum, which is unportable: the tool may be absent, and under cmd.exe the single-quoted path is not dequoted so the hash of a downloaded release could never be computed on Windows. Add a small in-process SHA-256 (FIPS 180-4, src/foundation/sha256.c) and use it directly — no external tool, no per-OS branching, no popen. The checksum test now covers three NIST vectors (empty, "abc", and a 56-byte input that forces padding into a second block). Signed-off-by: Martin Vogel --- Makefile.cbm | 3 +- src/cli/cli.c | 52 ++++++++-------- src/foundation/sha256.c | 129 ++++++++++++++++++++++++++++++++++++++++ src/foundation/sha256.h | 30 ++++++++++ tests/test_cli.c | 31 ++++++++-- 5 files changed, 214 insertions(+), 31 deletions(-) create mode 100644 src/foundation/sha256.c create mode 100644 src/foundation/sha256.h diff --git a/Makefile.cbm b/Makefile.cbm index bc2f55ce1..0156f3544 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -112,7 +112,8 @@ FOUNDATION_SRCS = \ src/foundation/profile.c \ src/foundation/dump_verify.c \ src/foundation/limits.c \ - src/foundation/subprocess.c + src/foundation/subprocess.c \ + src/foundation/sha256.c # Existing extraction C code (compiled from current location) EXTRACTION_SRCS = \ diff --git a/src/cli/cli.c b/src/cli/cli.c index 884849e0b..a33c4b333 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -8,6 +8,7 @@ #include "foundation/compat.h" #include "foundation/platform.h" #include "foundation/constants.h" +#include "foundation/sha256.h" #include "mcp/mcp.h" // cbm_mcp_tool_input_schema — CLI flag parser + per-tool --help /* CLI buffer size constants. */ @@ -2960,44 +2961,47 @@ static bool prompt_yn(const char *question) { return (buf[0] == 'y' || buf[0] == 'Y') ? true : false; } -/* ── SHA-CBM_SZ_256 checksum verification ─────────────────────────────── */ +/* ── SHA-256 checksum verification ─────────────────────────────── */ -/* SHA-CBM_SZ_256 hex digest: CBM_SZ_64 hex chars + NUL */ +/* SHA-256 hex digest: 64 hex chars + NUL */ #define SHA256_HEX_LEN CBM_SZ_64 #define SHA256_BUF_SIZE (SHA256_HEX_LEN + CLI_SKIP_ONE) -/* Minimum line length in checksums.txt: CBM_SZ_64 hex + 2 spaces + 1 char filename */ +/* Minimum line length in checksums.txt: 64 hex + 2 spaces + 1 char filename */ #define CHECKSUM_LINE_MIN (SHA256_HEX_LEN + 2) -/* 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: +/* Compute the SHA-256 of a file in-process (no external hashing tool — those + * differ per OS, may be absent, and mis-quote paths under cmd.exe). 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 256 '%s' 2>/dev/null", path); -#else - snprintf(cmd, sizeof(cmd), "sha256sum '%s' 2>/dev/null", path); -#endif - FILE *fp = cbm_popen(cmd, "r"); + FILE *fp = cbm_fopen(path, "rb"); if (!fp) { return CLI_ERR; } - char line[CLI_BUF_256]; - if (fgets(line, sizeof(line), fp)) { - /* Output format: */ - char *space = strchr(line, ' '); - if (space && space - line == SHA256_HEX_LEN) { - memcpy(out, line, SHA256_HEX_LEN); - out[SHA256_HEX_LEN] = '\0'; - cbm_pclose(fp); - return 0; - } + cbm_sha256_ctx ctx; + cbm_sha256_init(&ctx); + unsigned char buf[CLI_BUF_1K]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) { + cbm_sha256_update(&ctx, buf, n); } - cbm_pclose(fp); - return CLI_ERR; + int read_err = ferror(fp); + fclose(fp); + if (read_err) { + return CLI_ERR; + } + uint8_t digest[CBM_SHA256_DIGEST_LEN]; + cbm_sha256_final(&ctx, digest); + static const char hex[] = "0123456789abcdef"; + for (int i = 0; i < CBM_SHA256_DIGEST_LEN; i++) { + out[i * 2] = hex[digest[i] >> 4]; + out[i * 2 + 1] = hex[digest[i] & 0x0f]; + } + out[SHA256_HEX_LEN] = '\0'; + return 0; } /* ── Download helper (shell-free curl via exec) ───────────────── */ diff --git a/src/foundation/sha256.c b/src/foundation/sha256.c new file mode 100644 index 000000000..e8bc6bca7 --- /dev/null +++ b/src/foundation/sha256.c @@ -0,0 +1,129 @@ +/* SHA-256 per FIPS 180-4. Straightforward reference implementation; validated + * against the NIST test vectors in tests/test_cli.c. */ + +#include "foundation/sha256.h" + +#include + +static const uint32_t K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2}; + +#define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define CH(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EP0(x) (ROTR(x, 2) ^ ROTR(x, 13) ^ ROTR(x, 22)) +#define EP1(x) (ROTR(x, 6) ^ ROTR(x, 11) ^ ROTR(x, 25)) +#define SIG0(x) (ROTR(x, 7) ^ ROTR(x, 18) ^ ((x) >> 3)) +#define SIG1(x) (ROTR(x, 17) ^ ROTR(x, 19) ^ ((x) >> 10)) + +static void sha256_transform(cbm_sha256_ctx *c, const uint8_t *data) { + uint32_t m[64]; + for (int i = 0, j = 0; i < 16; i++, j += 4) { + m[i] = ((uint32_t)data[j] << 24) | ((uint32_t)data[j + 1] << 16) | + ((uint32_t)data[j + 2] << 8) | (uint32_t)data[j + 3]; + } + for (int i = 16; i < 64; i++) { + m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16]; + } + + uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3]; + uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7]; + + for (int i = 0; i < 64; i++) { + uint32_t t1 = h + EP1(e) + CH(e, f, g) + K[i] + m[i]; + uint32_t t2 = EP0(a) + MAJ(a, b, cc); + h = g; + g = f; + f = e; + e = d + t1; + d = cc; + cc = b; + b = a; + a = t1 + t2; + } + + c->state[0] += a; + c->state[1] += b; + c->state[2] += cc; + c->state[3] += d; + c->state[4] += e; + c->state[5] += f; + c->state[6] += g; + c->state[7] += h; +} + +void cbm_sha256_init(cbm_sha256_ctx *c) { + c->bitlen = 0; + c->buflen = 0; + c->state[0] = 0x6a09e667; + c->state[1] = 0xbb67ae85; + c->state[2] = 0x3c6ef372; + c->state[3] = 0xa54ff53a; + c->state[4] = 0x510e527f; + c->state[5] = 0x9b05688c; + c->state[6] = 0x1f83d9ab; + c->state[7] = 0x5be0cd19; +} + +void cbm_sha256_update(cbm_sha256_ctx *c, const void *data, size_t len) { + const uint8_t *p = (const uint8_t *)data; + for (size_t i = 0; i < len; i++) { + c->buf[c->buflen++] = p[i]; + if (c->buflen == 64) { + sha256_transform(c, c->buf); + c->bitlen += 512; + c->buflen = 0; + } + } +} + +void cbm_sha256_final(cbm_sha256_ctx *c, uint8_t out[CBM_SHA256_DIGEST_LEN]) { + c->bitlen += (uint64_t)c->buflen * 8; + + size_t i = c->buflen; + c->buf[i++] = 0x80; /* append the '1' bit + zero padding */ + if (i > 56) { + while (i < 64) { + c->buf[i++] = 0; + } + sha256_transform(c, c->buf); + i = 0; + } + while (i < 56) { + c->buf[i++] = 0; + } + /* append the 64-bit big-endian message length */ + for (int j = 0; j < 8; j++) { + c->buf[56 + j] = (uint8_t)(c->bitlen >> (56 - 8 * j)); + } + sha256_transform(c, c->buf); + + for (int j = 0; j < 8; j++) { + out[j * 4] = (uint8_t)(c->state[j] >> 24); + out[j * 4 + 1] = (uint8_t)(c->state[j] >> 16); + out[j * 4 + 2] = (uint8_t)(c->state[j] >> 8); + out[j * 4 + 3] = (uint8_t)(c->state[j]); + } +} + +void cbm_sha256_hex(const void *data, size_t len, char out[CBM_SHA256_HEX_LEN + 1]) { + uint8_t digest[CBM_SHA256_DIGEST_LEN]; + cbm_sha256_ctx c; + cbm_sha256_init(&c); + cbm_sha256_update(&c, data, len); + cbm_sha256_final(&c, digest); + + static const char hex[] = "0123456789abcdef"; + for (int i = 0; i < CBM_SHA256_DIGEST_LEN; i++) { + out[i * 2] = hex[digest[i] >> 4]; + out[i * 2 + 1] = hex[digest[i] & 0x0f]; + } + out[CBM_SHA256_HEX_LEN] = '\0'; +} diff --git a/src/foundation/sha256.h b/src/foundation/sha256.h new file mode 100644 index 000000000..bdfcb1b73 --- /dev/null +++ b/src/foundation/sha256.h @@ -0,0 +1,30 @@ +#ifndef CBM_SHA256_H +#define CBM_SHA256_H + +/* In-process SHA-256 (FIPS 180-4). Used to verify the integrity of a + * downloaded release before installing it, without shelling out to a + * platform hashing tool (shasum / sha256sum / certutil) — those differ per + * OS, may be absent, and mis-quote paths under cmd.exe. */ + +#include +#include + +#define CBM_SHA256_DIGEST_LEN 32 /* raw digest bytes */ +#define CBM_SHA256_HEX_LEN 64 /* lowercase hex chars (no NUL) */ + +typedef struct { + uint32_t state[8]; + uint64_t bitlen; + uint8_t buf[64]; + size_t buflen; +} cbm_sha256_ctx; + +void cbm_sha256_init(cbm_sha256_ctx *c); +void cbm_sha256_update(cbm_sha256_ctx *c, const void *data, size_t len); +void cbm_sha256_final(cbm_sha256_ctx *c, uint8_t out[CBM_SHA256_DIGEST_LEN]); + +/* One-shot hash of a buffer to lowercase hex. `out` must hold + * CBM_SHA256_HEX_LEN + 1 bytes (hex chars + NUL). */ +void cbm_sha256_hex(const void *data, size_t len, char out[CBM_SHA256_HEX_LEN + 1]); + +#endif /* CBM_SHA256_H */ diff --git a/tests/test_cli.c b/tests/test_cli.c index 6f2b19c4a..ea663fe33 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -3018,21 +3018,40 @@ TEST(cli_print_tool_help_issue680) { * 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) { +/* Hash `content` (len bytes) via a temp file and compare to expected hex. + * Returns 1 on match, 0 otherwise. */ +static int sha256_vector_ok(const void *content, size_t len, const char *expected) { char path[512]; snprintf(path, sizeof(path), "%s/cbm_sha_XXXXXX", cbm_tmpdir()); int fd = cbm_mkstemp(path); - ASSERT_TRUE(fd >= 0); + if (fd < 0) { + return 0; + } FILE *fp = fdopen(fd, "wb"); - ASSERT_NOT_NULL(fp); - fwrite("abc", 1, 3, fp); /* sha256("abc") is a NIST test vector */ + if (!fp) { + return 0; + } + if (len > 0) { + fwrite(content, 1, len, fp); + } 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"); + return rc == 0 && strcmp(digest, expected) == 0; +} + +/* NIST FIPS 180-4 SHA-256 test vectors: empty input, a single block ("abc"), + * and a 56-byte input that forces the length padding into a second block. */ +TEST(cli_sha256_file_matches_known_vector) { + ASSERT_TRUE(sha256_vector_ok( + "", 0, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")); + ASSERT_TRUE(sha256_vector_ok( + "abc", 3, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")); + ASSERT_TRUE(sha256_vector_ok( + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1")); PASS(); }