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:
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" \