kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 209cfaa3f8102565c021bddf4fc346126551535e
parent 044e5f07360dc16c6ec987ec54eec100c1fd7d1a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 16 Jul 2026 10:10:16 -0700

pkg: authenticate release updates and detached signatures

Diffstat:
MMakefile | 9+++++++++
Mdriver/cmd/pkg.c | 111++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mdriver/cmd/update.c | 217++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mdriver/release_key.c | 37++++++++++++++++---------------------
Minclude/kit/package.h | 27+++++++++++++++++++++++++++
Mmk/dist.mk | 5++++-
Mmk/flags.mk | 28++++++++++++++++++++++++++++
Ascripts/gen_release_config.sh | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/release.sh | 23+++++++++++++++++++++--
Msrc/api/package.c | 41+++++++++++++++++++++++++++++++++++++++++
Mtest/dist/run.sh | 151+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtest/pkg/run.sh | 36++++++++++++++++++++++++++++++++++++
Mtest/release/run.sh | 2++
13 files changed, 676 insertions(+), 74 deletions(-)

diff --git a/Makefile b/Makefile @@ -167,6 +167,15 @@ $(BUILD_DIR)/driver/version.o $(BUILD_DIR)/custom-driver/version.o: $(VERSION_CO $(BUILD_DIR)/driver/version.o $(BUILD_DIR)/custom-driver/version.o: \ DRIVER_CFLAGS += $(KIT_VERSION_DEFS) +# Generated update-channel URL + trust-anchor set. Keep it out of the public +# headers and attach its include path only to the one driver translation unit. +$(RELEASE_CONFIG_HEADER): FORCE scripts/gen_release_config.sh $(KIT_RELEASE_PUBKEYS_EFFECTIVE) + @sh scripts/gen_release_config.sh "$@" "$(KIT_UPDATE_INDEX_URL)" $(KIT_RELEASE_PUBKEYS_EFFECTIVE) + +$(BUILD_DIR)/driver/release_key.o $(BUILD_DIR)/custom-driver/release_key.o: $(RELEASE_CONFIG_HEADER) +$(BUILD_DIR)/driver/release_key.o $(BUILD_DIR)/custom-driver/release_key.o: \ + DRIVER_CFLAGS += -I$(BUILD_DIR)/generated + $(BUILD_DIR)/driver/env/%.o: driver/env/%.c Makefile $(BUILD_CONFIG) @mkdir -p $(dir $@) $(CC) $(DRIVER_ENV_CFLAGS) $(DRIVER_ENV_OS_CFLAGS) $(DEPFLAGS) -c $< -o $@ diff --git a/driver/cmd/pkg.c b/driver/cmd/pkg.c @@ -33,6 +33,7 @@ void driver_help_pkg(void) { " [--external DIR] FILE -C DIR\n" " kit pkg inspect [--manifest | --encoding] FILE\n" " kit pkg sign -s SECKEY [-o OUT] [--comment C] FILE\n" + " kit pkg verify-signature [-p PUBKEY] [-x SIG] FILE\n" " kit pkg trust {path | list | add PUBKEY [label] | remove KEYID}\n" "\n" "DESCRIPTION\n" @@ -44,6 +45,7 @@ void driver_help_pkg(void) { " -o OUT output path for keygen/create/sign\n" " -s SECKEY secret signing key\n" " -p PUBKEY explicit verification key\n" + " -x SIG detached signature (default FILE.minisig)\n" " --tofu trust on first use\n" " --format kpkg|tar.gz select or force the package format\n" " --external DIR external CAS-shaped object store\n" @@ -89,9 +91,9 @@ void driver_help_pkg(void) { "\n" "DETACHED SIGNATURES\n" " sign writes a stock-minisign-compatible detached signature to OUT, or\n" - " FILE.minisig when -o is omitted. This release has no advertised Kit\n" - " command for verifying detached signatures: `pkg verify` verifies\n" - " packages, not arbitrary FILE/FILE.minisig pairs.\n" + " FILE.minisig when -o is omitted. verify-signature authenticates an\n" + " arbitrary FILE against -p PUBKEY or the trusted-key store. It never\n" + " uses TOFU because a detached signature contains no public key.\n" "\n" "PATHS\n" " Bare -- is not accepted; spell a leading-dash file as ./-name.\n" @@ -701,6 +703,107 @@ static int pkg_trust(DriverEnv* env, const KitContext* ctx, int argc, } /* ---------------------------------------------------------------------- */ +/* verify-signature: detached minisign verification */ +/* ---------------------------------------------------------------------- */ + +static int pkg_verify_signature(DriverEnv* env, const KitContext* ctx, + int argc, char** argv) { + const char *file = NULL, *pubkey = NULL, *sig_path = NULL; + char sig_buf[PKG_PATH_BUF], tpath[PKG_PATH_BUF]; + KitFileData datafd, sigfd, pubfd, trustfd; + KitPkgDetachedVerifyOptions opts; + KitPkgDetachedVerifyResult result; + int data_loaded = 0, sig_loaded = 0, pub_loaded = 0, trust_loaded = 0; + int i, rc = 1; + (void)env; + + for (i = 0; i < argc; ++i) { + if (driver_streq(argv[i], "-p") && i + 1 < argc) { + pubkey = argv[++i]; + } else if (driver_streq(argv[i], "-x") && i + 1 < argc) { + sig_path = argv[++i]; + } else if (argv[i][0] != '-') { + if (file) { + driver_errf(PKG_TOOL, "verify-signature: unexpected argument: %s", + argv[i]); + return 2; + } + file = argv[i]; + } else { + driver_errf(PKG_TOOL, "verify-signature: unknown option: %s", argv[i]); + return 2; + } + } + if (!file) { + driver_errf(PKG_TOOL, "verify-signature: FILE is required"); + return 2; + } + if (!sig_path) { + int n = snprintf(sig_buf, sizeof sig_buf, "%s.minisig", file); + if (n < 0 || (size_t)n >= sizeof sig_buf) { + driver_errf(PKG_TOOL, "verify-signature: signature path is too long"); + return 1; + } + sig_path = sig_buf; + } + + memset(&opts, 0, sizeof opts); + if (!pkg_read(ctx, file, &datafd)) { + driver_errf(PKG_TOOL, "verify-signature: cannot read file: %s", file); + goto done; + } + data_loaded = 1; + opts.data = datafd.data; + opts.data_len = datafd.size; + if (!pkg_read(ctx, sig_path, &sigfd)) { + driver_errf(PKG_TOOL, "verify-signature: cannot read signature: %s", + sig_path); + goto done; + } + sig_loaded = 1; + opts.signature = sigfd.data; + opts.signature_len = sigfd.size; + + if (pubkey) { + if (!pkg_read(ctx, pubkey, &pubfd)) { + driver_errf(PKG_TOOL, "verify-signature: cannot read public key: %s", + pubkey); + goto done; + } + pub_loaded = 1; + opts.pubkey_bytes = pubfd.data; + opts.pubkey_len = pubfd.size; + } else { + char legacy[PKG_PATH_BUF]; + if (pkg_trust_path(tpath, sizeof tpath) == 0 && + pkg_read(ctx, tpath, &trustfd)) { + trust_loaded = 1; + } else if (pkg_trust_legacy_path(legacy, sizeof legacy) == 0 && + pkg_read(ctx, legacy, &trustfd)) { + trust_loaded = 1; + } + if (trust_loaded) { + opts.trusted_keys = trustfd.data; + opts.trusted_keys_len = trustfd.size; + } + } + + if (kit_pkg_verify_detached(ctx, &opts, &result) == KIT_OK) { + char idhex[PKG_KEYID_HEX]; + kit_hex_encode(idhex, result.keyid, KIT_PKG_KEYID_LEN); + driver_printf("ok: %s signer %s [%s]\n", file, idhex, result.trusted); + rc = 0; + } + +done: + if (trust_loaded) pkg_release(ctx, &trustfd); + if (pub_loaded) pkg_release(ctx, &pubfd); + if (sig_loaded) pkg_release(ctx, &sigfd); + if (data_loaded) pkg_release(ctx, &datafd); + return rc; +} + +/* ---------------------------------------------------------------------- */ /* sign: detached minisign signature over a file */ /* ---------------------------------------------------------------------- */ @@ -792,6 +895,8 @@ int driver_pkg(int argc, char** argv) { rc = pkg_inspect(&env, &ctx, argc - 2, argv + 2); else if (driver_streq(sub, "sign")) rc = pkg_sign(&env, &ctx, argc - 2, argv + 2); + else if (driver_streq(sub, "verify-signature")) + rc = pkg_verify_signature(&env, &ctx, argc - 2, argv + 2); else if (driver_streq(sub, "trust")) rc = pkg_trust(&env, &ctx, argc - 2, argv + 2); else { diff --git a/driver/cmd/update.c b/driver/cmd/update.c @@ -22,9 +22,10 @@ * * Offline-first: `kit update <file.kpkg>` verifies a local package against the * embedded release key set (driver/release_key.c) — or an explicit --key — and - * installs it with no network. The networked forms resolve a signed channel - * index (kit-release) through a fetch hint URL and download the .kpkg, but the - * trust always comes from the package signature, never the transport. */ + * installs it with no network. Networked forms authenticate the detached + * signature on the channel index before parsing it, then require the package + * to carry the same signer identity and the advertised version/package id. + * URLs and mirrors are untrusted transport hints. */ #define UPDATE_TOOL "update" #define UP_PATH 4096u @@ -139,6 +140,47 @@ static void up_release(const KitContext* ctx, KitFileData* fd) { ctx->file_io->release(ctx->file_io->user, fd); } +/* Verify detached bytes against --key when present, otherwise the compiled + * release-key set. The matching signer is returned so the caller can require + * the package selected by a channel index to use that same identity. */ +static int up_verify_detached(const KitContext* ctx, const uint8_t* data, + size_t data_len, const uint8_t* signature, + size_t signature_len, const uint8_t* key_bytes, + size_t key_len, + uint8_t signer[KIT_PKG_KEYID_LEN]) { + KitPkgDetachedVerifyOptions opts; + KitPkgDetachedVerifyResult result; + const KitReleaseKey* keys; + unsigned nkeys = 0, i; + memset(&opts, 0, sizeof opts); + opts.data = data; + opts.data_len = data_len; + opts.signature = signature; + opts.signature_len = signature_len; + if (key_bytes) { + opts.pubkey_bytes = key_bytes; + opts.pubkey_len = key_len; + if (kit_pkg_verify_detached(ctx, &opts, &result) != KIT_OK) return 1; + memcpy(signer, result.keyid, KIT_PKG_KEYID_LEN); + return 0; + } + keys = driver_release_keys(&nkeys); + if (!keys || nkeys == 0) { + driver_errf(UPDATE_TOOL, + "no compiled release keys; pass --key <pubkey>"); + return 1; + } + for (i = 0; i < nkeys; ++i) { + opts.pubkey_bytes = (const uint8_t*)keys[i].pubkey; + opts.pubkey_len = strlen(keys[i].pubkey); + if (kit_pkg_verify_detached(ctx, &opts, &result) == KIT_OK) { + memcpy(signer, result.keyid, KIT_PKG_KEYID_LEN); + return 0; + } + } + return 1; +} + /* Pull the 64-hex package id out of a verified trusted comment ("...pkgid=<hex> * ..."). Returns 0 + fills out[65] on success. */ static int up_extract_pkgid(const char* trusted, char* out) { @@ -175,7 +217,8 @@ static int up_extract_pkgid(const char* trusted, char* out) { static int up_verify(DriverEnv* env, const KitContext* ctx, const uint8_t* data, size_t len, const uint8_t* key_bytes, size_t key_len, const char* unpack_dir, char* name, size_t namecap, - char* version, size_t vercap, char* pkgid_hex) { + char* version, size_t vercap, char* pkgid_hex, + uint8_t signer[KIT_PKG_KEYID_LEN]) { KitCasHost host = driver_cas_host(env); KitPkgVerifyOptions opts; KitPkgVerifyResult res; @@ -193,6 +236,7 @@ static int up_verify(DriverEnv* env, const KitContext* ctx, const uint8_t* data, if (kit_pkg_verify(ctx, &host, &opts, &res) != KIT_OK) return 1; snprintf(name, namecap, "%s", res.name); snprintf(version, vercap, "%s", res.version); + memcpy(signer, res.keyid, KIT_PKG_KEYID_LEN); return up_extract_pkgid(res.trusted, pkgid_hex); } @@ -208,6 +252,7 @@ static int up_verify(DriverEnv* env, const KitContext* ctx, const uint8_t* data, if (kit_pkg_verify(ctx, &host, &opts, &res) == KIT_OK) { snprintf(name, namecap, "%s", res.name); snprintf(version, vercap, "%s", res.version); + memcpy(signer, res.keyid, KIT_PKG_KEYID_LEN); return up_extract_pkgid(res.trusted, pkgid_hex); } } @@ -240,7 +285,8 @@ static int up_refresh_links(DriverEnv* env, const UpPaths* p) { unsigned done = 0, fail = 0; driver_remove_tree(p->bin); if (driver_mkdir_p(env, p->bin) != 0) return 1; - snprintf(target_exe, sizeof target_exe, "%s/bin/kit", p->current); + snprintf(target_exe, sizeof target_exe, "%s/bin/kit%s", p->current, + driver_host_target().os == KIT_OS_WINDOWS ? ".exe" : ""); memset(&opts, 0, sizeof opts); opts.target_exe = target_exe; opts.tool_tag = UPDATE_TOOL; @@ -288,14 +334,16 @@ static int up_install_bytes(DriverEnv* env, const KitContext* ctx, const UpPaths* p, const uint8_t* data, size_t len, const uint8_t* key_bytes, size_t key_len, int dry_run, const char* expect_version, - const char* expect_pkgid_hex) { + const char* expect_pkgid_hex, + const uint8_t* expect_signer) { char name[KIT_PKG_NAME_MAX], version[KIT_PKG_VERSION_MAX], pkgid[65]; char tmp[UP_PATH], verdir[UP_PATH]; + uint8_t signer[KIT_PKG_KEYID_LEN]; /* Discovery pass: verify only (no materialize), to learn name/version/id and * confirm a key matches before touching the filesystem. */ if (up_verify(env, ctx, data, len, key_bytes, key_len, NULL, name, - sizeof name, version, sizeof version, pkgid) != 0) { + sizeof name, version, sizeof version, pkgid, signer) != 0) { driver_errf(UPDATE_TOOL, "signature verification failed (no trusted key matched)"); return 1; @@ -314,6 +362,12 @@ static int up_install_bytes(DriverEnv* env, const KitContext* ctx, "package id does not match the channel index entry"); return 1; } + if (expect_signer && + memcmp(expect_signer, signer, KIT_PKG_KEYID_LEN) != 0) { + driver_errf(UPDATE_TOOL, + "package signer does not match authenticated channel signer"); + return 1; + } if (dry_run) { driver_printf("would install kit %s and make it current\n", version); return 0; @@ -332,7 +386,7 @@ static int up_install_bytes(DriverEnv* env, const KitContext* ctx, return 1; } if (up_verify(env, ctx, data, len, key_bytes, key_len, tmp, name, sizeof name, - version, sizeof version, pkgid) != 0) { + version, sizeof version, pkgid, signer) != 0) { driver_errf(UPDATE_TOOL, "verification failed while unpacking"); driver_remove_tree(tmp); return 1; @@ -368,7 +422,7 @@ static int up_install_file(DriverEnv* env, const KitContext* ctx, return 1; } rc = up_install_bytes(env, ctx, p, fd.data, fd.size, key_bytes, key_len, - dry_run, NULL, NULL); + dry_run, NULL, NULL, NULL); up_release(ctx, &fd); return rc; } @@ -447,51 +501,89 @@ static int up_rollback(DriverEnv* env, const UpPaths* p, int dry_run) { /* ---------------------------------------------------------------------- */ static const char* up_index_url(const char* index_opt) { + const char* env; + const char* compiled; if (index_opt) return index_opt; - return driver_getenv("KIT_UPDATE_INDEX_URL"); + env = driver_getenv("KIT_UPDATE_INDEX_URL"); + if (env && *env) return env; + compiled = driver_release_index_url(); + return (compiled && *compiled) ? compiled : NULL; } /* Fetch + parse the channel index and locate this host's entry. Returns 0 on - * success (idx filled, *host points into idx). The index is an untrusted fetch - * hint: trust comes from the per-package signature plus the kpkg-id match the - * caller performs after download. */ + * success (idx filled, *host points into idx). Both INDEX and INDEX.minisig are + * fetched through curl; the detached signature is authenticated before any + * index bytes are parsed. */ static int up_load_index(DriverEnv* env, const KitContext* ctx, - const UpPaths* p, const char* index_url, - KitReleaseIndex* idx, const KitReleaseHost** host) { - char dest[UP_PATH]; - KitFileData fd; + const char* download_dir, const char* index_url, + const uint8_t* key_bytes, size_t key_len, + KitReleaseIndex* idx, const KitReleaseHost** host, + uint8_t signer[KIT_PKG_KEYID_LEN]) { + char dest[UP_PATH], sig_dest[UP_PATH], sig_url[UP_PATH]; + KitFileData fd, sigfd; const char* triple = kit_host_triple(); unsigned i; - snprintf(dest, sizeof dest, "%s/channel.index", p->downloads); - if (up_ensure_dirs(env, p) != 0) return 1; + int fd_loaded = 0, sig_loaded = 0, rc = 1; + int n = snprintf(sig_url, sizeof sig_url, "%s.minisig", index_url); + if (n < 0 || (size_t)n >= sizeof sig_url) { + driver_errf(UPDATE_TOOL, "channel signature URL is too long"); + return 1; + } + snprintf(dest, sizeof dest, "%s/channel.index", download_dir); + snprintf(sig_dest, sizeof sig_dest, "%s/channel.index.minisig", download_dir); + if (driver_mkdir_p(env, download_dir) != 0) { + driver_errf(UPDATE_TOOL, "could not create update download directory"); + return 1; + } if (driver_fetch_url(index_url, dest) != 0) { driver_errf(UPDATE_TOOL, "could not fetch channel index from %s", index_url); return 1; } + if (driver_fetch_url(sig_url, sig_dest) != 0) { + driver_errf(UPDATE_TOOL, "could not fetch channel signature from %s", + sig_url); + return 1; + } if (!up_read(ctx, dest, &fd)) { driver_errf(UPDATE_TOOL, "could not read fetched channel index"); return 1; } + fd_loaded = 1; + if (!up_read(ctx, sig_dest, &sigfd)) { + driver_errf(UPDATE_TOOL, "could not read fetched channel signature"); + goto done; + } + sig_loaded = 1; + if (up_verify_detached(ctx, fd.data, fd.size, sigfd.data, sigfd.size, + key_bytes, key_len, signer) != 0) { + driver_errf(UPDATE_TOOL, "channel index authentication failed"); + goto done; + } if (kit_release_index_parse(ctx, fd.data, fd.size, idx) != KIT_OK) { - up_release(ctx, &fd); - return 1; /* parse already emitted detail via ctx->diag */ + goto done; /* parse already emitted detail via ctx->diag */ } - up_release(ctx, &fd); *host = NULL; for (i = 0; i < idx->n_hosts; ++i) if (strcmp(idx->hosts[i].target, triple) == 0) { *host = &idx->hosts[i]; - return 0; + rc = 0; + goto done; } driver_errf(UPDATE_TOOL, "channel has no release for this host (%s)", triple); - return 1; +done: + if (sig_loaded) up_release(ctx, &sigfd); + if (fd_loaded) up_release(ctx, &fd); + return rc; } /* Download the host's .kpkg (trying each mirror), verify, and install. */ static int up_fetch_install(DriverEnv* env, const KitContext* ctx, - const UpPaths* p, const KitReleaseIndex* idx, + const UpPaths* p, const char* download_dir, + const KitReleaseIndex* idx, const KitReleaseHost* host, const uint8_t* key_bytes, - size_t key_len, int dry_run) { + size_t key_len, + const uint8_t signer[KIT_PKG_KEYID_LEN], + int dry_run) { char dest[UP_PATH], expect_id[65]; KitFileData fd; unsigned i; @@ -501,12 +593,7 @@ static int up_fetch_install(DriverEnv* env, const KitContext* ctx, return 1; } kit_hex_encode(expect_id, host->kpkg_id, KIT_CAS_HASH_LEN); - if (dry_run) { - driver_printf("would fetch and install kit %s for %s\n", idx->version, - host->target); - return 0; - } - snprintf(dest, sizeof dest, "%s/kit-%s-%s.kpkg", p->downloads, idx->version, + snprintf(dest, sizeof dest, "%s/kit-%s-%s.kpkg", download_dir, idx->version, host->target); for (i = 0; i < host->n_urls; ++i) { if (driver_fetch_url(host->urls[i], dest) == 0) { @@ -523,8 +610,8 @@ static int up_fetch_install(DriverEnv* env, const KitContext* ctx, driver_errf(UPDATE_TOOL, "cannot read downloaded package: %s", dest); return 1; } - rc = up_install_bytes(env, ctx, p, fd.data, fd.size, key_bytes, key_len, 0, - idx->version, expect_id); + rc = up_install_bytes(env, ctx, p, fd.data, fd.size, key_bytes, key_len, + dry_run, idx->version, expect_id, signer); up_release(ctx, &fd); return rc; } @@ -545,8 +632,9 @@ void driver_help_update(void) { " (default $XDG_DATA_HOME/kit, i.e. ~/.local/share/kit), verifying its\n" " signature against the built-in release key set (or --key) before any\n" " change. A local <file.kpkg> installs fully offline. Network operations\n" - " use curl and require --index URL or KIT_UPDATE_INDEX_URL; this release\n" - " has no built-in channel-index URL.\n" + " use curl and authenticate INDEX.minisig before parsing INDEX. Channel\n" + " selection is --index, then KIT_UPDATE_INDEX_URL, then the compiled\n" + " stable URL (development builds may have no compiled URL).\n" "\n" " Put $KIT_HOME/bin on PATH once; `current` is flipped atomically and\n" " the bin links refreshed on each install. All versions are retained,\n" @@ -561,7 +649,7 @@ void driver_help_update(void) { " --rollback flip to the previous installed version\n" " --prune remove all non-current installed versions\n" " --key <pubkey> verify against an explicit minisign public key\n" - " --index <url> channel index URL (else $KIT_UPDATE_INDEX_URL)\n" + " --index <url> channel index URL (overrides env/compiled URL)\n" " --allow-downgrade permit a channel-driven downgrade\n" " --dry-run print the plan; change nothing\n" " -h, --help show this help\n" @@ -571,7 +659,8 @@ void driver_help_update(void) { " XDG_DATA_HOME parent of default KIT_HOME\n" " KIT_UPDATE_INDEX_URL channel index when --index is absent\n" " KIT_HOME contains versions/, current, bin/, config/, and downloaded\n" - " update state. --index overrides KIT_UPDATE_INDEX_URL. Put only\n" + " update state. --index overrides KIT_UPDATE_INDEX_URL, which overrides\n" + " the compiled stable URL. Put only\n" " $KIT_HOME/bin on PATH after a successful install. Updates retain old\n" " versions until --prune; --rollback and --version VER switch locally\n" " when possible. Here --version requires VER; use `kit --version` for\n" @@ -593,9 +682,9 @@ void driver_help_update(void) { " go to stderr. --dry-run and --check do not modify the installation.\n" "\n" "EXIT CODES\n" - " 0 success 1 verify/install/I/O error 2 bad usage\n" - " Known limitation: `--check` with no configured index diagnoses the\n" - " missing index on stderr but currently returns 0.\n"); + " 0 successful action or authenticated check\n" + " 1 missing channel, fetch/auth/validation/install/I/O failure\n" + " 2 bad usage\n"); } int driver_update(int argc, char** argv) { @@ -606,6 +695,8 @@ int driver_update(int argc, char** argv) { const char* index_opt = NULL; int do_check = 0, do_list = 0, do_prune = 0, do_rollback = 0; int dry_run = 0, allow_downgrade = 0, opts_done = 0, i, rc = 2; + char transient_dir[UP_PATH]; + int transient_fetch_active = 0, transient_home_existed = 1; KitFileData keyfd; int key_loaded = 0; const uint8_t* key_bytes = NULL; @@ -704,9 +795,11 @@ int driver_update(int argc, char** argv) { /* Networked paths need a channel index URL. */ { const char* idx_url = up_index_url(index_opt); + const char* download_dir = paths.downloads; KitReleaseIndex* idx; const KitReleaseHost* host = NULL; char cur[KIT_PKG_VERSION_MAX]; + uint8_t channel_signer[KIT_PKG_KEYID_LEN]; int have_cur, cmp; if (!idx_url) { @@ -719,20 +812,34 @@ int driver_update(int argc, char** argv) { driver_errf(UPDATE_TOOL, "no channel index configured; pass a local <file.kpkg>, " "or set --index <url> / $KIT_UPDATE_INDEX_URL"); - rc = (do_check) ? 0 : 1; + rc = 1; if (do_check) { /* still report what is installed locally */ up_list(&env, &paths); } goto done; } + /* --check and --dry-run may fetch/authenticate bytes, but must leave no + * managed cache or installation state behind. Use a reserved transient + * directory and remove the newly-created KIT_HOME as well when it did not + * exist on entry. */ + if (do_check || dry_run) { + transient_home_existed = driver_path_lexists(paths.home); + snprintf(transient_dir, sizeof transient_dir, "%s/.update-readonly-tmp", + paths.home); + driver_remove_tree(transient_dir); + download_dir = transient_dir; + transient_fetch_active = 1; + } + idx = (KitReleaseIndex*)driver_alloc_zeroed(&env, sizeof *idx); if (!idx) { driver_errf(UPDATE_TOOL, "out of memory"); rc = 1; goto done; } - if (up_load_index(&env, &ctx, &paths, idx_url, idx, &host) != 0) { + if (up_load_index(&env, &ctx, download_dir, idx_url, key_bytes, key_len, + idx, &host, channel_signer) != 0) { driver_free(&env, idx, sizeof *idx); rc = 1; goto done; @@ -784,13 +891,33 @@ int driver_update(int argc, char** argv) { } } - rc = up_fetch_install(&env, &ctx, &paths, idx, host, key_bytes, key_len, - dry_run); + /* A binary launched outside the managed layout still supplies a downgrade + * floor. Do not silently replace a newer running Kit with an older channel + * when there is no current symlink to compare against. An explicit local + * package remains a deliberate offline choice and bypasses this policy. */ + if (!have_cur && !allow_downgrade && + kit_calver_compare(idx->version, kit_version_string(), &cmp) == KIT_OK && + cmp < 0) { + driver_errf(UPDATE_TOOL, + "channel offers kit %s, older than running kit %s; pass " + "--allow-downgrade to move back", + idx->version, kit_version_string()); + driver_free(&env, idx, sizeof *idx); + rc = 1; + goto done; + } + + rc = up_fetch_install(&env, &ctx, &paths, download_dir, idx, host, + key_bytes, key_len, channel_signer, dry_run); driver_free(&env, idx, sizeof *idx); goto done; } done: + if (transient_fetch_active) { + driver_remove_tree(transient_dir); + if (!transient_home_existed) driver_remove_tree(paths.home); + } if (key_loaded) up_release(&ctx, &keyfd); driver_env_fini(&env); return rc; diff --git a/driver/release_key.c b/driver/release_key.c @@ -1,26 +1,21 @@ -/* Embedded release trust anchor(s) for `kit update`. See doc/plan/SELFDIST.md. - * - * `kit update` verifies a downloaded release against this built-in set of - * minisign public keys; any one key verifying suffices (overlap-window - * rotation). At real release time the production release public key is added - * here, with its secret held offline and never in the tree. - * - * The default entry below is the in-tree NON-RELEASE test key - * (test/dist/keys/nonrelease.pub, key id c1709dd2922282f6) — the same key the - * hermetic end-to-end test and the `make dist` fallback signer use, so a - * stock-built kit can verify a `make dist` artifact against a built-in anchor. - * It is NOT a release key and grants no trust over real distributions. */ +/* Embedded release trust anchors and stable channel URL for `kit update`. + * Official builds generate this configuration from KIT_RELEASE_PUBKEYS and + * KIT_UPDATE_INDEX_URL; the build rejects an absent production configuration + * and the repository's non-release test anchor. Development builds retain the + * hermetic test anchor but intentionally have no default network channel. */ #include "driver.h" - -static const KitReleaseKey kit_release_keys[] = { - {"kit NON-RELEASE test key (c1709dd2922282f6)", - "untrusted comment: kit public key c1709dd2922282f6\n" - "RWTBcJ3SkiKC9rw+zEgXOtsk4rCI4x+PSf9VtBaP9XJFczB5RLtTOjLJ\n"}, -}; +#include <kit_release_config.h> const KitReleaseKey* driver_release_keys(unsigned* count) { - if (count) - *count = (unsigned)(sizeof kit_release_keys / sizeof kit_release_keys[0]); - return kit_release_keys; + if (count) *count = KIT_GENERATED_RELEASE_KEY_COUNT; +#if KIT_GENERATED_RELEASE_KEY_COUNT > 0 + return kit_generated_release_keys; +#else + return NULL; +#endif +} + +const char* driver_release_index_url(void) { + return kit_generated_update_index_url; } diff --git a/include/kit/package.h b/include/kit/package.h @@ -76,6 +76,33 @@ KIT_API KitStatus kit_pkg_sign_detached(const KitContext* ctx, size_t seckey_len, const char* comment, KitWriter* out); +typedef struct KitPkgDetachedVerifyOptions { + const uint8_t* data; + size_t data_len; + const uint8_t* signature; + size_t signature_len; + /* An explicit Minisign public-key file, or NULL to resolve the signature's + * key id through trusted_keys. Detached signatures never imply TOFU: unlike + * a package container, they carry no candidate public key to authenticate. */ + const uint8_t* pubkey_bytes; + size_t pubkey_len; + const uint8_t* trusted_keys; + size_t trusted_keys_len; +} KitPkgDetachedVerifyOptions; + +typedef struct KitPkgDetachedVerifyResult { + uint8_t keyid[KIT_PKG_KEYID_LEN]; + char trusted[KIT_PKG_TRUSTED_COMMENT_MAX]; +} KitPkgDetachedVerifyResult; + +/* Verify a stock-Minisign-compatible detached signature. With no explicit + * public key, the signature key id is resolved from the caller-supplied + * trusted-key store. On success, returns the authenticated signer id and + * trusted comment. */ +KIT_API KitStatus kit_pkg_verify_detached( + const KitContext* ctx, const KitPkgDetachedVerifyOptions* opts, + KitPkgDetachedVerifyResult* result); + /* Trusted-keys store helpers (the store is plain text managed by the caller). * lookup finds keyid's public key; format_entry renders one NUL-terminated, * newline-included store line. */ diff --git a/mk/dist.mk b/mk/dist.mk @@ -21,7 +21,10 @@ DIST_KPKG = $(DIST_DIR)/$(DIST_BASE).kpkg DIST_TARGZ = $(DIST_DIR)/$(DIST_BASE).tar.gz dist: - $(MAKE) RELEASE=1 bin + $(MAKE) RELEASE=1 \ + KIT_UPDATE_INDEX_URL="$(if $(KIT_UPDATE_INDEX_URL),$(KIT_UPDATE_INDEX_URL),https://example.invalid/kit-development.index)" \ + KIT_RELEASE_PUBKEYS="$(if $(KIT_RELEASE_PUBKEYS),$(KIT_RELEASE_PUBKEYS),test/dist/keys/nonrelease.pub)" \ + KIT_RELEASE_ALLOW_TEST_KEY="$(if $(KIT_RELEASE_PUBKEYS),$(KIT_RELEASE_ALLOW_TEST_KEY),1)" bin @if [ -z "$(KIT_SIGN_KEY)" ]; then \ printf '\n*** WARNING: KIT_SIGN_KEY is unset — signing with the in-tree\n'; \ printf '*** NON-RELEASE test key (test/dist/keys/nonrelease.key). The\n'; \ diff --git a/mk/flags.mk b/mk/flags.mk @@ -147,3 +147,31 @@ LIB_CFLAGS = $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Isrc DRIVER_CFLAGS = $(FREESTANDING_CFLAGS) -Iinclude -Ilang -Idriver -Idriver/lib DRIVER_ENV_CFLAGS = $(HOST_CFLAGS) -Iinclude -Ilang -Idriver -Idriver/lib TEST_HOST_CFLAGS = $(HOST_CFLAGS) -Iinclude -Ilang + +# `kit update` release trust is generated per build. Development binaries keep +# the repository's explicit NON-RELEASE key solely for hermetic selfdist tests +# and have no default channel URL. Official RELEASE=1 binaries must be given a +# stable URL and one or more public-key files, and may never embed that test +# anchor. KIT_RELEASE_ALLOW_TEST_KEY=1 is reserved for release harnesses. +KIT_UPDATE_INDEX_URL ?= +KIT_RELEASE_PUBKEYS ?= +KIT_RELEASE_ALLOW_TEST_KEY ?= 0 +ifeq ($(RELEASE),1) +ifeq ($(strip $(KIT_UPDATE_INDEX_URL)),) +$(error RELEASE=1 requires KIT_UPDATE_INDEX_URL (compiled stable channel URL)) +endif +ifeq ($(strip $(KIT_RELEASE_PUBKEYS)),) +$(error RELEASE=1 requires KIT_RELEASE_PUBKEYS (production Minisign public key file(s))) +endif +ifneq ($(KIT_RELEASE_ALLOW_TEST_KEY),1) +KIT_RELEASE_TEST_ANCHOR := $(strip $(shell grep -l 'c1709dd2922282f6' $(KIT_RELEASE_PUBKEYS) 2>/dev/null)) +ifneq ($(KIT_RELEASE_TEST_ANCHOR),) +$(error RELEASE=1 refuses repository NON-RELEASE test anchor: $(KIT_RELEASE_TEST_ANCHOR)) +endif +endif +KIT_RELEASE_PUBKEYS_EFFECTIVE := $(KIT_RELEASE_PUBKEYS) +else +KIT_RELEASE_PUBKEYS_EFFECTIVE := $(if $(strip $(KIT_RELEASE_PUBKEYS)),$(KIT_RELEASE_PUBKEYS),test/dist/keys/nonrelease.pub) +endif + +RELEASE_CONFIG_HEADER = $(BUILD_DIR)/generated/kit_release_config.h diff --git a/scripts/gen_release_config.sh b/scripts/gen_release_config.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# Generate the driver-only compiled update-channel configuration. The output is +# a C header included after driver.h, so it may declare KitReleaseKey values. + +set -eu + +[ "$#" -ge 2 ] || { + printf 'usage: gen_release_config.sh OUT INDEX_URL [PUBKEY ...]\n' >&2 + exit 2 +} + +out=$1 +index_url=$2 +shift 2 +tmp="$out.$$.tmp" + +escape_line() { + LC_ALL=C awk '{ + gsub(/\\/, "\\\\") + gsub(/\"/, "\\\"") + printf " \"%s\\n\"\n", $0 + }' +} + +escape_one_line() { + printf '%s\n' "$1" | LC_ALL=C awk '{ + gsub(/\\/, "\\\\") + gsub(/\"/, "\\\"") + printf "\"%s\"", $0 + }' +} + +mkdir -p "$(dirname "$out")" +{ + printf '#ifndef KIT_GENERATED_RELEASE_CONFIG_H\n' + printf '#define KIT_GENERATED_RELEASE_CONFIG_H\n\n' + printf 'static const char kit_generated_update_index_url[] = ' + escape_one_line "$index_url" + printf ';\n\n' + printf '#define KIT_GENERATED_RELEASE_KEY_COUNT %su\n' "$#" + if [ "$#" -gt 0 ]; then + printf 'static const KitReleaseKey kit_generated_release_keys[] = {\n' + n=0 + for key in "$@"; do + n=$((n + 1)) + [ -f "$key" ] || { + printf 'release public key is not a file: %s\n' "$key" >&2 + exit 1 + } + printf ' {"embedded release key %s",\n' "$n" + escape_line < "$key" + printf ' },\n' + done + printf '};\n' + fi + printf '\n#endif\n' +} > "$tmp" + +if [ -f "$out" ] && cmp -s "$tmp" "$out"; then + rm -f "$tmp" +else + mv "$tmp" "$out" +fi diff --git a/scripts/release.sh b/scripts/release.sh @@ -37,6 +37,11 @@ # Environment knobs # ----------------- # KIT_SIGN_KEY (required) path to the real release minisign secret key. +# KIT_RELEASE_PUBKEYS (required) one or more production Minisign public-key +# files to embed in every released `kit update` binary. +# KIT_UPDATE_INDEX_URL +# (required) production stable channel-index URL compiled +# into every released binary. # KIT_RELEASE_ALLOW_TEST_KEY # (optional, default 0) set to 1 only for hermetic tests # that intentionally sign with the in-tree non-release key. @@ -80,6 +85,14 @@ canon_path() { "KIT_SIGN_KEY is unset — set it to the real release minisign secret key. (Unlike 'make dist', release.sh does NOT fall back to the in-tree test key.)" [ -f "$KIT_SIGN_KEY" ] || die "KIT_SIGN_KEY=$KIT_SIGN_KEY is not a file" +[ -n "${KIT_RELEASE_PUBKEYS:-}" ] || die \ + "KIT_RELEASE_PUBKEYS is unset — provide production Minisign public-key file(s)" +for release_pubkey in $KIT_RELEASE_PUBKEYS; do + [ -f "$release_pubkey" ] || die \ + "KIT_RELEASE_PUBKEYS entry is not a file: $release_pubkey" +done +[ -n "${KIT_UPDATE_INDEX_URL:-}" ] || die \ + "KIT_UPDATE_INDEX_URL is unset — provide the production stable channel URL" TEST_SIGN_KEY="$ROOT/test/dist/keys/nonrelease.key" if [ "${KIT_RELEASE_ALLOW_TEST_KEY:-0}" != 1 ] && [ "$(canon_path "$KIT_SIGN_KEY")" = "$(canon_path "$TEST_SIGN_KEY")" ]; then @@ -114,7 +127,10 @@ INDEX_FILE="$OUT_DIR/$CHANNEL.index" # host-runnable kit. Build the release native binary first and use it as the # packaging tool. (Mirrors mk/dist.mk, which packages with build/release/kit.) log "building native bootstrap kit (make RELEASE=1 bin)" -make RELEASE=1 bin +make RELEASE=1 \ + KIT_UPDATE_INDEX_URL="$KIT_UPDATE_INDEX_URL" \ + KIT_RELEASE_PUBKEYS="$KIT_RELEASE_PUBKEYS" \ + KIT_RELEASE_ALLOW_TEST_KEY="${KIT_RELEASE_ALLOW_TEST_KEY:-0}" bin KIT="${KIT:-$ROOT/build/release/kit}" [ -x "$KIT" ] || die "native kit not found at $KIT after 'make RELEASE=1 bin'" @@ -197,7 +213,10 @@ build_target() { # (kit.exe for windows), so capture it immediately into the per-target # staging tree before any later run can clobber it. log "cross-building kit binary" - bash "$ROOT/scripts/kit_cross.sh" "$token" --cc=kit + KIT_UPDATE_INDEX_URL="$KIT_UPDATE_INDEX_URL" \ + KIT_RELEASE_PUBKEYS="$KIT_RELEASE_PUBKEYS" \ + KIT_RELEASE_ALLOW_TEST_KEY="${KIT_RELEASE_ALLOW_TEST_KEY:-0}" \ + bash "$ROOT/scripts/kit_cross.sh" "$token" --cc=kit local cross_dir="$ROOT/build/kit-cross/kit/$token" local cross_bin bin_name if [ "$os" = windows ]; then diff --git a/src/api/package.c b/src/api/package.c @@ -1746,6 +1746,47 @@ KitStatus kit_pkg_sign_detached(const KitContext* ctx, const uint8_t* msg, return kit_writer_status(out) == KIT_OK ? KIT_OK : KIT_IO; } +KitStatus kit_pkg_verify_detached( + const KitContext* ctx, const KitPkgDetachedVerifyOptions* opts, + KitPkgDetachedVerifyResult* result) { + uint8_t pk[DIST_ED25519_PK_LEN]; + uint8_t keyid_check[DIST_KEYID_LEN]; + if (!ctx || !opts || !result || (!opts->data && opts->data_len) || + !opts->signature) + return KIT_INVALID; + memset(result, 0, sizeof *result); + if (dist_minisig_sig_keyid(opts->signature, opts->signature_len, + result->keyid) != DIST_OK) { + kit_ctx_diagf(ctx, "malformed detached signature"); + return KIT_MALFORMED; + } + if (opts->pubkey_bytes) { + if (dist_minisig_parse_pubkey(opts->pubkey_bytes, opts->pubkey_len, pk, + keyid_check) != DIST_OK) { + kit_ctx_diagf(ctx, "malformed public key"); + return KIT_MALFORMED; + } + if (memcmp(keyid_check, result->keyid, DIST_KEYID_LEN) != 0) { + kit_ctx_diagf(ctx, "public key id does not match detached signature"); + return KIT_ERR; + } + } else if (!opts->trusted_keys || + dist_trust_lookup(opts->trusted_keys, opts->trusted_keys_len, + result->keyid, pk) != DIST_OK) { + char hex[2 * DIST_KEYID_LEN + 1]; + dist_hex_encode(hex, result->keyid, DIST_KEYID_LEN); + kit_ctx_diagf(ctx, "untrusted detached signer (key id %s)", hex); + return KIT_ERR; + } + if (dist_minisig_verify(opts->signature, opts->signature_len, opts->data, + opts->data_len, pk, result->trusted, + sizeof result->trusted) != DIST_OK) { + kit_ctx_diagf(ctx, "detached signature verification FAILED"); + return KIT_ERR; + } + return KIT_OK; +} + KitStatus kit_pkg_keygen(const KitContext* ctx, KitPkgRandomFn rng, void* rng_user, KitWriter* pub_out, KitWriter* sec_out, uint8_t out_keyid[KIT_PKG_KEYID_LEN]) { diff --git a/test/dist/run.sh b/test/dist/run.sh @@ -39,6 +39,17 @@ KIT_KIT_DIR="$repo_root/test/lib" . "$repo_root/test/lib/kit_sh_kit.sh" kit_report_init +# Official build configuration is a release gate: URL + production anchors are +# mandatory, and the checked-in non-release anchor is rejected by key id. +run_fail "release-config-missing-fails" make -n -C "$repo_root" RELEASE=1 bin +run_fail "release-config-test-anchor-fails" make -n -C "$repo_root" \ + RELEASE=1 KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ + KIT_RELEASE_PUBKEYS="$PUBKEY" bin +run_ok "release-config-keygen" "$KIT" pkg keygen -o "$work/release-config" +run_ok "release-config-production-shape" make -n -C "$repo_root" RELEASE=1 \ + KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ + KIT_RELEASE_PUBKEYS="$work/release-config.pub" bin + # ---- local helpers --------------------------------------------------------- have_cmd() { command -v "$1" >/dev/null 2>&1; } @@ -100,6 +111,53 @@ host_triple() { "$KIT" --version 2>/dev/null | sed -n 's/^kit [^ ]* (.*, \(.*\))/\1/p' | sed -n '1p' } +# An unauthenticated/nonexistent check is an operational failure and a clean +# read-only invocation must not create KIT_HOME. +kit_home kh-check-no-config +unset KIT_UPDATE_INDEX_URL +run_fail "check-no-config-fails" "$KIT" update --check +contains "check-no-config-diagnostic" "$work/check-no-config-fails.err" \ + "no channel index configured" +if [ -e "$KIT_HOME" ]; then + echo "--check without configuration created KIT_HOME" > \ + "$work/check-no-config-no-state.diag" + not_ok "check-no-config-no-state" "$work/check-no-config-no-state.diag" +else + ok "check-no-config-no-state" +fi +kit_home kh-check-fetch-failure +run_fail "check-fetch-failure" "$KIT" update --check \ + --index "file://$work/does-not-exist.index" +if [ -e "$KIT_HOME" ]; then + echo "failed-fetch --check created KIT_HOME" > \ + "$work/check-fetch-failure-no-state.diag" + not_ok "check-fetch-failure-no-state" \ + "$work/check-fetch-failure-no-state.diag" +else + ok "check-fetch-failure-no-state" +fi + +# Remote update transport is intentionally curl-only. With curl absent, Kit +# must fail instead of silently changing downloader/TLS behavior to wget. +mkdir -p "$work/fetch-path" +printf '%s\n' '#!/bin/sh' 'printf called > "$WGET_MARKER"' 'exit 0' > \ + "$work/fetch-path/wget" +chmod +x "$work/fetch-path/wget" +kit_home kh-check-no-curl +saved_path=$PATH +WGET_MARKER="$work/wget-called" PATH="$work/fetch-path" \ + run_fail "check-curl-only" "$KIT" update --check \ + --index https://updates.invalid/stable.index +PATH=$saved_path +export PATH +if [ -e "$work/wget-called" ]; then + echo "update transport invoked wget when curl was unavailable" > \ + "$work/check-curl-only.diag" + not_ok "check-no-wget-fallback" "$work/check-curl-only.diag" +else + ok "check-no-wget-fallback" +fi + # =========================================================================== # # (a) offline install of 2026.6.0 # =========================================================================== # @@ -217,8 +275,8 @@ fi # =========================================================================== # # (j) networked + monotonic via file:// channel index # =========================================================================== # -# A signed package's trust comes from its own signature; the index is only a -# fetch hint. We hand-write a `kit-release 1` index (format per +# Both the channel index and selected package are signed by the same identity; +# URLs remain fetch hints. We hand-write a `kit-release 1` index (format per # test/api/release_index_test.c) pointing at the local 2026.6.0 kpkg over a # file:// URL. If the fetch transport is unavailable here, the whole block is # skipped rather than failed. @@ -243,6 +301,71 @@ else printf 'kpkg = %s\n' "$id60" printf 'url = file://%s\n' "$kpkg_60" } > "$idx" + run_ok "j-sign-index" "$KIT" pkg sign -s "$SECKEY" \ + --comment "selfdist stable index" "$idx" + + sed 's/version = 2026\.6\.0/version = 2025.1.0/' "$idx" > \ + "$work/channel-older.index" + run_ok "j-sign-older-index" "$KIT" pkg sign -s "$SECKEY" \ + "$work/channel-older.index" + kit_home kh-j-running-floor + run_fail "j-running-version-is-floor" "$KIT" update \ + --index "file://$work/channel-older.index" + contains "j-running-version-floor-message" \ + "$work/j-running-version-is-floor.err" "older than running kit" + + sed 's/version = 2026\.6\.0/version = 2026.6.1/' "$idx" > \ + "$work/channel-version-mismatch.index" + run_ok "j-sign-version-mismatch-index" "$KIT" pkg sign -s "$SECKEY" \ + "$work/channel-version-mismatch.index" + kit_home kh-j-version-mismatch + run_fail "j-package-version-mismatch-fails" "$KIT" update \ + --index "file://$work/channel-version-mismatch.index" + contains "j-package-version-mismatch-message" \ + "$work/j-package-version-mismatch-fails.err" \ + "does not match expected" + + zero_id=0000000000000000000000000000000000000000000000000000000000000000 + sed "s/kpkg = $id60/kpkg = $zero_id/" "$idx" > \ + "$work/channel-id-mismatch.index" + run_ok "j-sign-id-mismatch-index" "$KIT" pkg sign -s "$SECKEY" \ + "$work/channel-id-mismatch.index" + kit_home kh-j-id-mismatch + run_fail "j-package-id-mismatch-fails" "$KIT" update \ + --index "file://$work/channel-id-mismatch.index" + contains "j-package-id-mismatch-message" \ + "$work/j-package-id-mismatch-fails.err" \ + "package id does not match" + + cp "$idx" "$work/channel-other-signer.index" + run_ok "j-sign-index-other-identity" "$KIT" pkg sign \ + -s "$work/other.key" "$work/channel-other-signer.index" + kit_home kh-j-other-signer + run_fail "j-key-applies-to-index-and-package" "$KIT" update \ + --key "$work/other.pub" \ + --index "file://$work/channel-other-signer.index" + + cp "$idx" "$work/channel-tampered.index" + cp "$idx.minisig" "$work/channel-tampered.index.minisig" + printf '# changed after signing\n' >> "$work/channel-tampered.index" + kit_home kh-j-bad-signature + run_fail "j-tampered-index-fails" "$KIT" update --check \ + --index "file://$work/channel-tampered.index" + if [ -e "$KIT_HOME" ]; then + echo "bad-index --check created KIT_HOME" > \ + "$work/j-tampered-index-no-state.diag" + not_ok "j-tampered-index-no-state" \ + "$work/j-tampered-index-no-state.diag" + else + ok "j-tampered-index-no-state" + fi + + printf 'not a release index\n' > "$work/channel-malformed.index" + run_ok "j-sign-malformed-index" "$KIT" pkg sign -s "$SECKEY" \ + "$work/channel-malformed.index" + kit_home kh-j-malformed + run_fail "j-authenticated-malformed-index-fails" "$KIT" update --check \ + --index "file://$work/channel-malformed.index" # Probe the file:// transport against a fresh root before asserting: # if it cannot fetch+install here, skip the block gracefully. @@ -261,11 +384,35 @@ else else ok "j-check-nochange" fi + if [ -e "$work/kh-j-check" ]; then + echo "--check left persistent KIT_HOME state" > \ + "$work/j-check-no-persistent-state.diag" + not_ok "j-check-no-persistent-state" \ + "$work/j-check-no-persistent-state.diag" + else + ok "j-check-no-persistent-state" + fi + + kit_home kh-j-dry-run + run_ok "j-dry-run-authenticates-package" "$KIT" update \ + --index "file://$idx" --dry-run + contains "j-dry-run-plan" "$work/j-dry-run-authenticates-package.out" \ + "would install kit 2026.6.0" + if [ -e "$work/kh-j-dry-run" ]; then + echo "--dry-run left persistent KIT_HOME state" > \ + "$work/j-dry-run-no-state.diag" + not_ok "j-dry-run-no-state" "$work/j-dry-run-no-state.diag" + else + ok "j-dry-run-no-state" + fi # Fresh root: networked fetch + install 2026.6.0. kit_home kh-j-fetch run_ok "j-fetch-install" "$KIT" update --index "file://$idx" current_is "j-fetch-current-60" "$work/kh-j-fetch" 2026.6.0 + run_ok "j-check-no-update" "$KIT" update --index "file://$idx" --check + contains "j-check-no-update-current" "$work/j-check-no-update.out" \ + "installed current: 2026.6.0" # Monotonic guard: with current = 2026.6.1, an index advertising the # older 2026.6.0 is refused, but --allow-downgrade succeeds. diff --git a/test/pkg/run.sh b/test/pkg/run.sh @@ -310,6 +310,40 @@ else not_ok "pkg-keygen-keyid" "$work/pkg-keygen-keyid.diag" fi +printf 'detached verification payload\n' > "$work/detached.txt" +run_ok "pkg-detached-sign" "$KIT" pkg sign -s "$work/key.key" \ + --comment "authenticated detached comment" "$work/detached.txt" +run_ok "pkg-detached-verify-explicit-default-sig" "$KIT" pkg verify-signature \ + -p "$work/key.pub" "$work/detached.txt" +contains "pkg-detached-trusted-comment" \ + "$work/pkg-detached-verify-explicit-default-sig.out" \ + "authenticated detached comment" +cp "$work/detached.txt" "$work/detached-changed.txt" +printf tamper >> "$work/detached-changed.txt" +run_fail "pkg-detached-changed-file-fails" "$KIT" pkg verify-signature \ + -p "$work/key.pub" -x "$work/detached.txt.minisig" \ + "$work/detached-changed.txt" +run_ok "pkg-detached-wrong-keygen" "$KIT" pkg keygen -o "$work/detached-wrong" +run_fail "pkg-detached-wrong-key-fails" "$KIT" pkg verify-signature \ + -p "$work/detached-wrong.pub" "$work/detached.txt" +printf 'not a signature\n' > "$work/malformed.minisig" +run_fail "pkg-detached-malformed-fails" "$KIT" pkg verify-signature \ + -p "$work/key.pub" -x "$work/malformed.minisig" "$work/detached.txt" + +if have_cmd minisign; then + run_ok "pkg-detached-kit-to-stock" minisign -Vm "$work/detached.txt" \ + -p "$work/key.pub" -x "$work/detached.txt.minisig" + run_ok "pkg-detached-stock-sign" minisign -S -W -m "$work/detached.txt" \ + -s "$work/key.key" -x "$work/stock.minisig" \ + -t "stock authenticated comment" + run_ok "pkg-detached-stock-to-kit" "$KIT" pkg verify-signature \ + -p "$work/key.pub" -x "$work/stock.minisig" "$work/detached.txt" + contains "pkg-detached-stock-comment" \ + "$work/pkg-detached-stock-to-kit.out" "stock authenticated comment" +else + skip_test "pkg-detached-minisign-interop" "minisign not installed" +fi + run_ok "pkg-cas-add-tree" "$KIT" cas add-tree --cas "$work/cas" --root "$work/in" tree_id=$(first_hex_id "$work/pkg-cas-add-tree.out") if [ -n "$tree_id" ]; then @@ -489,6 +523,8 @@ run_ok "pkg-trust-list-added" "$KIT" pkg trust list contains "pkg-trust-list-added-key" "$work/pkg-trust-list-added.out" "$keyid" contains "pkg-trust-list-added-label" "$work/pkg-trust-list-added.out" "matrix-label" run_ok "pkg-verify-trusted-store" "$KIT" pkg verify "$work/pkg/matrix.tar.gz" +run_ok "pkg-detached-verify-trusted-store" "$KIT" pkg verify-signature \ + "$work/detached.txt" run_ok "pkg-trust-remove" "$KIT" pkg trust remove "$keyid" run_ok "pkg-trust-list-removed" "$KIT" pkg trust list not_contains "pkg-trust-list-removed-key" "$work/pkg-trust-list-removed.out" "$keyid" diff --git a/test/release/run.sh b/test/release/run.sh @@ -76,6 +76,8 @@ runs_version() { if env KIT= \ KIT_SIGN_KEY="$SECKEY" \ KIT_RELEASE_ALLOW_TEST_KEY=1 \ + KIT_RELEASE_PUBKEYS="$PUBKEY" \ + KIT_UPDATE_INDEX_URL="https://example.invalid/stable.index" \ KIT_RELEASE_ALLOW_DIRTY=1 \ KIT_RELEASE_TARGETS="$TARGET" \ KIT_RELEASE_OUT_DIR="$OUT_DIR" \