/* * signature.c - signify-based package signing and verification * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause * * .signature file format: * ← no "untrusted comment:" line stored * * SHA256 (MAKEPKG) = ... * SHA256 (.footprint) = ... * SHA256 (source.tar.gz) = ... * * The "untrusted comment:" line is stripped on write and reconstructed * in memory on verify — signify needs it internally but we don't store it. * * The recipe line always reads "MAKEPKG" and the footprint line * ".footprint", whichever file was hashed: for a locked port those are * MAKEPKG.lock and .footprint.lock, and the signature itself lives in * .signature.lock (all three chosen once, in mkpkg.c). * * Verification, in order: * 1. the signify() key in the recipe must match a key in * TRUSTED_KEYS_DIR/.pub — the trust anchor is local; * 2. signify -V with THAT trusted file authenticates the list; * 3. bind_sig_checksums() proves the recipe and footprint on disk * are the files the list describes. */ #include "mkpkg.h" #include #include #include #include #include #include #include #include #include #include static void error(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fputs("mkpkg: error: ", stderr); vfprintf(stderr, fmt, ap); fputc('\n', stderr); va_end(ap); } /* ── Temp file helpers ───────────────────────────────────────────────── */ static int make_tmpfd(char *path, size_t pathsz) { const char *tmp = getenv("TMPDIR"); if (!tmp || !tmp[0]) tmp = "/dev/shm"; snprintf(path, pathsz, "%s/mkpkg.XXXXXX", tmp); return mkstemp(path); } 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; } fclose(fp); if (rename(tmp, path) != 0) { unlink(tmp); return -1; } return 0; } /* ── Run signify ─────────────────────────────────────────────────────── */ static int run_signify(char *const argv[]) { pid_t pid = fork(); if (pid < 0) { warn("fork"); return -1; } if (pid == 0) { /* silence signify output — mkpkg owns the terminal */ int devnull = open("/dev/null", O_WRONLY); if (devnull >= 0) { dup2(devnull, STDOUT_FILENO); dup2(devnull, STDERR_FILENO); close(devnull); } execvp("signify", argv); _exit(1); } int status; waitpid(pid, &status, 0); return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? 0 : -1; } /* ── split_signature ─────────────────────────────────────────────────── */ /* * Split the combined .signature file into two temp files: * tmp_sig: the signify base64 line (with dummy comment prepended) * tmp_checksums: the SHA256 checksum lines * * The split happens at the first blank line. */ static int split_signature(const char *sigfile, char *tmp_sig, size_t sig_sz, char *tmp_cks, size_t cks_sz) { FILE *in, *fsig, *fcks; char line[1024]; int in_checksums = 0; int fd; in = fopen(sigfile, "r"); if (!in) { warn("%s", sigfile); return -1; } fd = make_tmpfd(tmp_sig, sig_sz); if (fd < 0) { warn("mkstemp"); fclose(in); return -1; } fsig = fdopen(fd, "w"); if (!fsig) { close(fd); unlink(tmp_sig); fclose(in); return -1; } fd = make_tmpfd(tmp_cks, cks_sz); if (fd < 0) { warn("mkstemp"); fclose(fsig); unlink(tmp_sig); fclose(in); return -1; } fcks = fdopen(fd, "w"); if (!fcks) { close(fd); unlink(tmp_cks); fclose(fsig); unlink(tmp_sig); fclose(in); return -1; } /* signify requires "untrusted comment: ...\n\n" format */ fprintf(fsig, "untrusted comment: mkpkg\n"); while (fgets(line, sizeof(line), in)) { if (!in_checksums) { if (line[0] == '\n' || line[0] == '\r') { in_checksums = 1; continue; } /* skip any stored comment line */ if (strncmp(line, "untrusted comment:", 18) == 0) continue; fputs(line, fsig); } else { fputs(line, fcks); } } fclose(in); fclose(fsig); fclose(fcks); return 0; } /* ── verify_signature ────────────────────────────────────────────────── */ int verify_signature(const char *sigfile, pkgfile_t *pkgfile, config_t *config) { char pubkey[PATH_MAX_LEN]; char tmp_sig[PATH_MAX_LEN]; char tmp_cks[PATH_MAX_LEN]; int ret; if (!pkgfile->has_signify || !pkgfile->signify_pubkey) { error("no signify() public key found in %s", config->pkgfile); return E_SIGNATURE; } if (signify_trusted_pubkey(pkgfile->signify_pubkey, pubkey, sizeof(pubkey)) != 0) { error("signify() key in %s is not trusted " "(no matching key in %s/*.pub)", config->pkgfile, TRUSTED_KEYS_DIR); return E_SIGNATURE; } if (split_signature(sigfile, tmp_sig, sizeof(tmp_sig), tmp_cks, sizeof(tmp_cks)) != 0) { error("cannot split signature file: %s", sigfile); return E_SIGNATURE; } { char *argv[] = { "signify", "-V", "-p", pubkey, "-m", tmp_cks, "-x", tmp_sig, NULL }; ret = run_signify(argv); } unlink(tmp_sig); if (ret != 0) { unlink(tmp_cks); error("signature verification failed"); return E_SIGNATURE; } /* the list is authentic — now prove the files are the ones it lists */ ret = bind_sig_checksums(tmp_cks, config); unlink(tmp_cks); return ret; } /* ── find_secret_key ─────────────────────────────────────────────────── */ static int find_secret_key(config_t *config, char *out, size_t outsz) { if (config->secret_key[0]) { snprintf(out, outsz, "%s", config->secret_key); } else { const char *env = getenv("MKPKG_SECRET_KEY"); if (env && env[0]) snprintf(out, outsz, "%s", env); else snprintf(out, outsz, TRUSTED_KEYS_DIR "/rawnix.sec"); } return access(out, R_OK) == 0 ? 0 : -1; } /* ── create_signature ────────────────────────────────────────────────── */ int create_signature(pkgfile_t *pkgfile, config_t *config) { char secret_key[PATH_MAX_LEN]; char tmp_cks[PATH_MAX_LEN]; char tmp_raw_sig[PATH_MAX_LEN]; char sig_path[PATH_MAX_LEN]; FILE *in, *out; char line[1024]; int fd; if (find_secret_key(config, secret_key, sizeof(secret_key)) != 0) return -2; /* no key configured -- caller decides whether to warn */ /* build checksums file */ fd = make_tmpfd(tmp_cks, sizeof(tmp_cks)); if (fd < 0) err(E_SIGNATURE, "mkstemp"); close(fd); if (make_sig_checksums(pkgfile, config, tmp_cks) != 0) { unlink(tmp_cks); return E_SIGNATURE; } /* sign with signify → tmp_raw_sig */ snprintf(tmp_raw_sig, sizeof(tmp_raw_sig), "%s.sig", tmp_cks); { char *argv[] = { "signify", "-S", "-s", secret_key, "-m", tmp_cks, "-x", tmp_raw_sig, NULL }; if (run_signify(argv) != 0) { unlink(tmp_cks); errx(E_SIGNATURE, "signing failed"); } } /* combine: base64 (no comment) + blank line + checksums */ snprintf(sig_path, sizeof(sig_path), "%s/%s", config->root_dir, config->signature_file); out = fopen(sig_path, "w"); if (!out) err(E_SIGNATURE, "cannot create: %s", sig_path); /* write signify output, stripping the untrusted comment line */ in = fopen(tmp_raw_sig, "r"); if (in) { while (fgets(line, sizeof(line), in)) { if (strncmp(line, "untrusted comment:", 18) == 0) continue; fputs(line, out); } fclose(in); } fputc('\n', out); /* append checksums */ in = fopen(tmp_cks, "r"); if (in) { while (fgets(line, sizeof(line), in)) fputs(line, out); fclose(in); } fclose(out); unlink(tmp_cks); unlink(tmp_raw_sig); info("signature created: %s/%s", config->root_dir, config->signature_file); /* * Self-check. A signature made with a key that is not the one in * signify(), or not in TRUSTED_KEYS_DIR, can never verify — say so * now rather than at the next build. (A locked port signed by its * user needs `mkpkg -up` first, and the user's .pub installed.) */ if (verify_signature(sig_path, pkgfile, config) != 0) warning("%s does not verify: the signing key must match " "signify() in %s and be in %s", config->signature_file, config->pkgfile, TRUSTED_KEYS_DIR); return 0; } /* ── show_pubkey ─────────────────────────────────────────────────────── */ int show_pubkey(pkgfile_t *pkgfile) { if (!pkgfile->has_signify || !pkgfile->signify_pubkey) { warnx("no signify() function found in MAKEPKG"); return E_GENERAL; } printf("%s\n", pkgfile->signify_pubkey); return 0; } /* ── update_pubkey ───────────────────────────────────────────────────── */ static int find_pubkey(const char *seckey, char *out, size_t outsz) { /* try .sec → .pub in same directory */ size_t len = strlen(seckey); if (len > 4 && strcmp(seckey + len - 4, ".sec") == 0) { snprintf(out, outsz, "%.*s.pub", (int)(len - 4), seckey); if (access(out, R_OK) == 0) return 0; } /* try /etc/ports/keys/.pub */ const char *base = strrchr(seckey, '/'); base = base ? base + 1 : seckey; char alt[PATH_MAX_LEN]; snprintf(alt, sizeof(alt), "/etc/ports/keys/%s", base); { size_t al = strlen(alt); if (al > 4 && strcmp(alt + al - 4, ".sec") == 0) { snprintf(alt + al - 4, sizeof(alt) - (al - 4), ".pub"); } else { snprintf(alt + al, sizeof(alt) - al, ".pub"); } } if (access(alt, R_OK) == 0) { snprintf(out, outsz, "%s", alt); return 0; } return -1; } int update_pubkey(pkgfile_t *pkgfile, config_t *config) { char secret_key[PATH_MAX_LEN]; char pub_path[PATH_MAX_LEN]; char pkgfile_path[PATH_MAX_LEN]; char pubkey_block[2048]; char line[512]; FILE *fp; int line_count = 0; char *content, *result; size_t content_len, block_len, result_len; (void)pkgfile; if (find_secret_key(config, secret_key, sizeof(secret_key)) != 0) errx(E_SIGNATURE, "secret key not found — set MKPKG_SECRET_KEY in /etc/mkpkg.conf"); if (find_pubkey(secret_key, pub_path, sizeof(pub_path)) != 0) errx(E_SIGNATURE, "cannot find public key for: %s", secret_key); info("reading public key from: %s", pub_path); fp = fopen(pub_path, "r"); if (!fp) err(E_SIGNATURE, "cannot open: %s", pub_path); pubkey_block[0] = '\0'; while (fgets(line, sizeof(line), fp) && line_count < 2) { /* strip newline */ size_t l = strlen(line); while (l > 0 && (line[l-1] == '\n' || line[l-1] == '\r')) line[--l] = '\0'; if (!l) continue; if (pubkey_block[0]) strncat(pubkey_block, "\n ", sizeof(pubkey_block) - strlen(pubkey_block) - 1); else strncat(pubkey_block, " ", sizeof(pubkey_block) - strlen(pubkey_block) - 1); strncat(pubkey_block, line, sizeof(pubkey_block) - strlen(pubkey_block) - 1); line_count++; } fclose(fp); if (line_count < 2) errx(E_SIGNATURE, "invalid public key file (expected 2 lines): %s", pub_path); /* build signify() block */ char sig_block[2560]; snprintf(sig_block, sizeof(sig_block), "signify() {\n%s\n}\n", pubkey_block); block_len = strlen(sig_block); /* read MAKEPKG */ snprintf(pkgfile_path, sizeof(pkgfile_path), "%s/%s", config->root_dir, config->pkgfile); fp = fopen(pkgfile_path, "r"); if (!fp) err(E_GENERAL, "cannot open: %s", pkgfile_path); content_len = 0; size_t cap = 16384; content = xmalloc(cap); { size_t n; while ((n = fread(content + content_len, 1, cap - content_len - 1, fp)) > 0) { content_len += n; if (content_len + 1 >= cap) { cap *= 2; content = xrealloc(content, cap); } } content[content_len] = '\0'; } fclose(fp); /* find existing signify() block to replace, or insert before # vim: */ char *sig_start = strstr(content, "signify()"); if (sig_start) { /* find closing brace */ char *p = sig_start; int brace = 0, found_open = 0; while (*p) { if (*p == '{') { brace++; found_open = 1; } if (*p == '}') { brace--; if (found_open && brace == 0) { p++; if (*p == '\n') p++; break; } } p++; } size_t before = (size_t)(sig_start - content); size_t after = (size_t)(content + content_len - p); result_len = before + block_len + after; result = xmalloc(result_len + 1); memcpy(result, content, before); memcpy(result + before, sig_block, block_len); memcpy(result + before + block_len, p, after); result[result_len] = '\0'; } else { char *vim = strstr(content, "# vim:"); if (vim) { size_t before = (size_t)(vim - content); size_t after = content_len - before; result_len = before + block_len + 1 + after; result = xmalloc(result_len + 1); memcpy(result, content, before); memcpy(result + before, sig_block, block_len); result[before + block_len] = '\n'; memcpy(result + before + block_len + 1, vim, after); result[result_len] = '\0'; } else { result_len = content_len + 1 + block_len; result = xmalloc(result_len + 1); memcpy(result, content, content_len); result[content_len] = '\n'; memcpy(result + content_len + 1, sig_block, block_len); result[result_len] = '\0'; } } free(content); /* write atomically — avoids truncating MAKEPKG on interrupt */ if (write_file_atomic(pkgfile_path, result, result_len) != 0) { free(result); err(E_GENERAL, "cannot write: %s", pkgfile_path); } free(result); info("signify() updated in %s", config->pkgfile); return 0; }