/* * checksum.c - SHA256 source verification and update * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause */ #include "mkpkg.h" #include #include #include #include #include #include #include #include /* ── SHA256 ───────────────────────────────────────────────────────────── */ /* * Compute hex SHA256 of a file into result[65]. * Returns 0 on success, -1 on failure. * * 1 MB read buffer: with the old 8 KB buffer a 4 GB rocm-class * package meant ~500k fread calls; large sequential reads let the * kernel readahead do its job. Heap-allocated — 1 MB does not * belong on the stack. */ #define SHA_BUF_SIZE (1024 * 1024) static int sha256_file(const char *path, char *result, size_t result_size) { FILE *fp; EVP_MD_CTX *ctx; unsigned char *buf; unsigned char digest[EVP_MAX_MD_SIZE]; unsigned int digest_len; size_t n; unsigned int i; if (result_size < 65) return -1; fp = fopen(path, "rb"); if (!fp) return -1; buf = malloc(SHA_BUF_SIZE); if (!buf) { fclose(fp); return -1; } ctx = EVP_MD_CTX_new(); if (!ctx) { free(buf); fclose(fp); return -1; } if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto fail; while ((n = fread(buf, 1, SHA_BUF_SIZE, fp)) > 0) { if (EVP_DigestUpdate(ctx, buf, n) != 1) goto fail; } if (ferror(fp)) goto fail; if (EVP_DigestFinal_ex(ctx, digest, &digest_len) != 1) goto fail; EVP_MD_CTX_free(ctx); free(buf); fclose(fp); for (i = 0; i < digest_len; i++) snprintf(result + i * 2, result_size - i * 2, "%02x", digest[i]); result[digest_len * 2] = '\0'; return 0; fail: EVP_MD_CTX_free(ctx); free(buf); fclose(fp); return -1; } /* ── Streaming SHA256 + build-time hash cache ─────────────────────────── */ /* * Used by mkpkg to hash the package WHILE the compressor writes it * (see arc_create_pkg's sink), instead of re-reading a multi-GB * tarball from disk just to sign data that flowed through the * machine seconds earlier. After a big rocm-class build the page * cache is usually evicted, so that re-read was frequently a real * full disk pass. */ void *sha256_stream_new(void) { EVP_MD_CTX *ctx = EVP_MD_CTX_new(); if (!ctx) return NULL; if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) { EVP_MD_CTX_free(ctx); return NULL; } return ctx; } void sha256_stream_update(void *vctx, const void *buf, size_t len) { if (vctx) EVP_DigestUpdate((EVP_MD_CTX *)vctx, buf, len); } /* Finalise into result[65] (hex) and free the context. */ int sha256_stream_final(void *vctx, char *result, size_t result_size) { EVP_MD_CTX *ctx = vctx; unsigned char digest[EVP_MAX_MD_SIZE]; unsigned int digest_len; unsigned int i; if (!ctx || result_size < 65) { if (ctx) EVP_MD_CTX_free(ctx); return -1; } if (EVP_DigestFinal_ex(ctx, digest, &digest_len) != 1) { EVP_MD_CTX_free(ctx); return -1; } EVP_MD_CTX_free(ctx); for (i = 0; i < digest_len; i++) snprintf(result + i * 2, result_size - i * 2, "%02x", digest[i]); result[digest_len * 2] = '\0'; return 0; } /* * One-slot cache: the hash of the package mkpkg just wrote. * make_sig_checksums() consults it instead of re-hashing. The stat * snapshot (size + mtime) guards against the file changing between * build and sign — if it doesn't match exactly, fall back to a * fresh sha256_file() read. Single-threaded by design, like the * rest of mkpkg. */ static struct { char path[PATH_MAX_LEN]; char sha[65]; off_t size; time_t mtime; int valid; } g_sha_cache; void sha256_cache_store(const char *path, const char *sha) { struct stat st; g_sha_cache.valid = 0; if (!path || !sha || stat(path, &st) != 0) return; snprintf(g_sha_cache.path, sizeof(g_sha_cache.path), "%s", path); snprintf(g_sha_cache.sha, sizeof(g_sha_cache.sha), "%s", sha); g_sha_cache.size = st.st_size; g_sha_cache.mtime = st.st_mtime; g_sha_cache.valid = 1; } static int sha256_cached(const char *path, char *result, size_t result_size) { struct stat st; if (!g_sha_cache.valid || result_size < 65) return -1; if (strcmp(path, g_sha_cache.path) != 0) return -1; if (stat(path, &st) != 0 || st.st_size != g_sha_cache.size || st.st_mtime != g_sha_cache.mtime) return -1; snprintf(result, result_size, "%s", g_sha_cache.sha); return 0; } /* ── Local filename resolution ────────────────────────────────────────── */ static char *local_path(const source_t *src, const config_t *config) { char path[PATH_MAX_LEN]; const char *url = src->url; if (src->rename && strcmp(src->rename, "SKIP") != 0) { snprintf(path, sizeof(path), "%s/%s", config->source_dir, src->rename); } else if (strchr(url, '/')) { const char *base = strrchr(url, '/'); snprintf(path, sizeof(path), "%s/%s", config->source_dir, base ? base + 1 : url); } else { snprintf(path, sizeof(path), "%s/%s", config->root_dir, url); } return xstrdup(path); } static const char *path_basename(const char *p) { const char *s = strrchr(p, '/'); return s ? s + 1 : p; } /* ── verify_checksums ─────────────────────────────────────────────────── */ int verify_checksums(pkgfile_t *pkgfile, config_t *config) { int errors = 0; int i; if (pkgfile->sha256sum_count == 0) return 0; for (i = 0; i < pkgfile->source_count; i++) { source_t *src = &pkgfile->sources[i]; char *fp = local_path(src, config); const char *base = path_basename(fp); char computed[65]; if (i >= pkgfile->sha256sum_count) { free(fp); continue; } if (strcmp(pkgfile->sha256sums[i], "SKIP") == 0) { free(fp); continue; } if (sha256_file(fp, computed, sizeof(computed)) != 0) { fprintf(stderr, "mkpkg: error: failed to compute SHA256: %s\n", base); errors++; free(fp); continue; } if (strcasecmp(computed, pkgfile->sha256sums[i]) != 0) { fprintf(stderr, "mkpkg: error: SHA256 mismatch: %s\n", base); warnx(" expected: %s", pkgfile->sha256sums[i]); warnx(" computed: %s", computed); errors++; } else { info(" %s (SHA256) ok", base); } free(fp); } return errors > 0 ? E_CHECKSUM : 0; } /* ── generate_checksums ───────────────────────────────────────────────── */ int generate_checksums(pkgfile_t *pkgfile, config_t *config) { int i; printf("\nsha256sums=(\n"); for (i = 0; i < pkgfile->source_count; i++) { char *fp = local_path(&pkgfile->sources[i], config); char computed[65]; if (access(fp, F_OK) != 0 || sha256_file(fp, computed, sizeof(computed)) != 0) printf(" \"SKIP\"\n"); else printf(" \"%s\"\n", computed); free(fp); } printf(")\n"); return 0; } /* ── update_checksums ─────────────────────────────────────────────────── */ /* * Build the new sha256sums=(...) block into a dynamic buffer. * Returns heap-allocated string — caller must free(). */ static char *build_checksums_block(pkgfile_t *pkgfile, config_t *config) { size_t cap = 4096; size_t len = 0; char *buf = xmalloc(cap); int i; #define BPRINTF(fmt, ...) \ do { \ int _n; \ while (1) { \ _n = snprintf(buf + len, cap - len, fmt, ##__VA_ARGS__); \ if (_n < 0) { free(buf); return NULL; } \ if ((size_t)_n < cap - len) { len += (size_t)_n; break; } \ cap *= 2; \ buf = xrealloc(buf, cap); \ } \ } while (0) BPRINTF("sha256sums=(\n"); for (i = 0; i < pkgfile->source_count; i++) { char *fp = local_path(&pkgfile->sources[i], config); char computed[65]; if (access(fp, F_OK) != 0 || sha256_file(fp, computed, sizeof(computed)) != 0) BPRINTF(" \"SKIP\"\n"); else BPRINTF(" \"%s\"\n", computed); free(fp); } BPRINTF(")\n"); #undef BPRINTF return buf; } /* * Read an entire file into a heap buffer. * Returns pointer + sets *size. Caller must free(). */ static char *read_file(const char *path, size_t *size) { FILE *fp; char *buf; size_t cap = 16384, len = 0; size_t n; fp = fopen(path, "r"); if (!fp) return NULL; buf = xmalloc(cap); while ((n = fread(buf + len, 1, cap - len - 1, fp)) > 0) { len += n; if (len + 1 >= cap) { cap *= 2; buf = xrealloc(buf, cap); } } buf[len] = '\0'; fclose(fp); *size = len; return buf; } /* * Write buf to path atomically via a tmp file in the same directory. */ static int write_file_atomic(const char *path, const char *buf, size_t len) { char tmp[PATH_MAX_LEN]; FILE *fp; snprintf(tmp, sizeof(tmp), "%s.tmp", path); fp = fopen(tmp, "w"); if (!fp) return -1; if (fwrite(buf, 1, len, fp) != len || fflush(fp) != 0) { fclose(fp); unlink(tmp); return -1; } fsync(fileno(fp)); /* make data durable before the rename */ fclose(fp); if (rename(tmp, path) != 0) { unlink(tmp); return -1; } return 0; } int update_checksums(pkgfile_t *pkgfile, config_t *config) { char pkgfile_path[PATH_MAX_LEN]; char *content, *newblock, *result; size_t content_len, newblock_len, result_len; char *start, *end; snprintf(pkgfile_path, sizeof(pkgfile_path), "%s/%s", config->root_dir, config->pkgfile); content = read_file(pkgfile_path, &content_len); if (!content) err(E_GENERAL, "cannot read: %s", pkgfile_path); newblock = build_checksums_block(pkgfile, config); if (!newblock) { free(content); return E_GENERAL; } newblock_len = strlen(newblock); start = strstr(content, "sha256sums=("); if (!start) { /* insert before build() or append at end */ char *build_pos = strstr(content, "build()"); size_t before_len = build_pos ? (size_t)(build_pos - content) : content_len; size_t after_len = content_len - before_len; result_len = before_len + newblock_len + 1 + after_len; result = xmalloc(result_len + 1); memcpy(result, content, before_len); memcpy(result + before_len, newblock, newblock_len); result[before_len + newblock_len] = '\n'; memcpy(result + before_len + newblock_len + 1, content + before_len, after_len); result[result_len] = '\0'; } else { /* find the closing ) of sha256sums=(...) */ int paren = 0; int entered = 0; end = start; while (*end) { if (*end == '(') { paren++; entered = 1; } if (*end == ')') { paren--; } end++; if (entered && paren == 0) break; } /* skip trailing newline */ if (*end == '\n') end++; size_t before_len = (size_t)(start - content); size_t after_len = (size_t)(content + content_len - end); result_len = before_len + newblock_len + after_len; result = xmalloc(result_len + 1); memcpy(result, content, before_len); memcpy(result + before_len, newblock, newblock_len); memcpy(result + before_len + newblock_len, end, after_len); result[result_len] = '\0'; } free(content); free(newblock); if (write_file_atomic(pkgfile_path, result, result_len) != 0) { free(result); err(E_GENERAL, "cannot write: %s", pkgfile_path); } free(result); info("sha256sums updated in %s", config->pkgfile); return 0; } /* ── make_sig_checksums ───────────────────────────────────────────────── */ int make_sig_checksums(pkgfile_t *pkgfile, config_t *config, const char *outfile) { FILE *fp; char path[PATH_MAX_LEN]; char sha[65]; struct stat st; int i; fp = fopen(outfile, "w"); if (!fp) err(E_SIGNATURE, "cannot create: %s", outfile); /* * The recipe and footprint lines name the ROLE, not the file: * always "MAKEPKG" and ".footprint", even when a locked port hashes * MAKEPKG.lock and .footprint.lock. A lock snapshot of upstream's * .signature therefore still verifies the unedited lock recipe, and * bind_sig_checksums() maps the names back through config. */ /* MAKEPKG */ snprintf(path, sizeof(path), "%s/%s", config->root_dir, config->pkgfile); if (sha256_file(path, sha, sizeof(sha)) == 0) fprintf(fp, "SHA256 (MAKEPKG) = %s\n", sha); /* .footprint if present */ snprintf(path, sizeof(path), "%s/%s", config->root_dir, config->footprint_file); if (stat(path, &st) == 0 && sha256_file(path, sha, sizeof(sha)) == 0) fprintf(fp, "SHA256 (.footprint) = %s\n", sha); /* source files */ for (i = 0; i < pkgfile->source_count; i++) { source_t *src = &pkgfile->sources[i]; char *lp; if (src->rename && strcmp(src->rename, "SKIP") == 0) continue; lp = local_path(src, config); if (sha256_file(lp, sha, sizeof(sha)) == 0) fprintf(fp, "SHA256 (%s) = %s\n", path_basename(lp), sha); free(lp); } /* * Binary package, if a built tarball exists alongside MAKEPKG. * * Filename is fully deterministic from MAKEPKG + the configured * compression mode — no glob, so a stale tarball with a different * version on disk doesn't pollute the manifest. The line is * consumed by the bulk tool on the client side to verify the * downloaded binary against the (signify-signed) sum recorded * here. If the binary doesn't exist (e.g. running -us right * after -x without a rebuild), the line is just omitted; the * existing -us behaviour where it always builds first means * this is rarely the case. */ { const char *ext = config->compression[0] ? config->compression : "gz"; char binfile[PATH_MAX_LEN]; snprintf(binfile, sizeof(binfile), "%s#%s-%s.pkg.tar.%s", pkgfile->name, pkgfile->version, pkgfile->release, ext); snprintf(path, sizeof(path), "%s/%s", config->package_dir, binfile); /* prefer the hash computed while the package was written; * re-read only if the cache is missing or the file changed */ if (stat(path, &st) == 0 && (sha256_cached(path, sha, sizeof(sha)) == 0 || sha256_file(path, sha, sizeof(sha)) == 0)) fprintf(fp, "SHA256 (%s) = %s\n", binfile, sha); } fclose(fp); return 0; } /* ── bind_sig_checksums ───────────────────────────────────────────────── */ /* * The inverse of make_sig_checksums(), run on the checksum list AFTER * signify has authenticated it. `signify -V` proves the list is * signed; it says nothing about the files on disk. Without this, a * MAKEPKG with a modified build(), sources and sha256sums=() passed * verification against the maintainer's untouched .signature. * * MAKEPKG exactly one line; must match config->pkgfile * (MAKEPKG.lock for a locked port) * .footprint if listed, config->footprint_file must exist and match * * Sources need no check here: MAKEPKG is bound, so its sha256sums=() * bind them, and verify_checksums() checks those after download — * verification runs before download, so the files may not exist yet. * The binary line is bulk's: it verifies the fetched package against it. */ int bind_sig_checksums(const char *cks_path, config_t *config) { FILE *fp; char line[LINE_MAX_LEN]; int recipe_lines = 0, errors = 0; fp = fopen(cks_path, "r"); if (!fp) { warn("%s", cks_path); return E_SIGNATURE; } while (fgets(line, sizeof(line), fp)) { char name[256], want[65], got[65], path[PATH_MAX_LEN]; const char *file; if (sscanf(line, "SHA256 (%255[^)]) = %64s", name, want) != 2) continue; if (strcmp(name, "MAKEPKG") == 0) { file = config->pkgfile; recipe_lines++; } else if (strcmp(name, ".footprint") == 0) { file = config->footprint_file; } else { continue; } snprintf(path, sizeof(path), "%s/%s", config->root_dir, file); if (sha256_file(path, got, sizeof(got)) != 0) { fprintf(stderr, "mkpkg: error: %s is covered by the " "signature but cannot be read\n", file); errors++; } else if (strcasecmp(got, want) != 0) { fprintf(stderr, "mkpkg: error: %s does not match its " "signed checksum\n", file); errors++; } } fclose(fp); if (recipe_lines != 1) { fprintf(stderr, "mkpkg: error: signature %s the recipe " "(expected one MAKEPKG line)\n", recipe_lines ? "lists more than one" : "does not cover"); errors++; } return errors ? E_SIGNATURE : 0; }