commit 623643c4f49b8639938e03cdb045fe85d049ec91
parent 618ab868d81712aeb501c84a0e9128bf38983d33
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 16 Jul 2026 10:41:20 -0700
dist: require explicit release trust
Diffstat:
8 files changed, 119 insertions(+), 49 deletions(-)
diff --git a/doc/RELEASE.md b/doc/RELEASE.md
@@ -40,7 +40,9 @@ embeds the same bytes and the self-host byte-identity gate is preserved.
## Building release artifacts
-Two layers, so the everyday target needs no secret key.
+Both artifact paths require explicit release signing and update-channel trust
+configuration. Hermetic tests opt into the checked-in non-release key; no
+release target selects it implicitly.
### `make dist` — native host
@@ -55,13 +57,14 @@ build/dist/
kit-2026.6.0-aarch64-macos.tar.gz.minisig detached sig over the tarball (bootstrap)
```
-The staging tree is `bin/kit`, `lib/libkit.a`, `include/`, `support/rt/{include,lib}/`,
-`VERSION`, and README/license. Signing uses `KIT_SIGN_KEY=<path>` if set,
-otherwise the in-tree **NON-RELEASE** test key (`test/dist/keys/nonrelease.key`)
-with a loud warning — so the full `make dist` → verify → `kit update` pipeline
-runs hermetically in CI. The `.kpkg` is the canonical update unit; the `.tar.gz`
-carries its signed manifest internally, and the extra detached `.minisig` lets a
-user with only stock `minisign` verify before unpacking.
+The staging tree is `bin/kit`, `lib/libkit.a`, `include/`,
+`support/rt/{include,lib}/`, `VERSION`, and README/license. The target requires
+`KIT_SIGN_KEY=<path>`, `KIT_RELEASE_PUBKEYS=<path...>`, and
+`KIT_UPDATE_INDEX_URL=<stable-url>`. It rejects the in-tree **NON-RELEASE** test
+key unless a hermetic harness explicitly sets `KIT_RELEASE_ALLOW_TEST_KEY=1`.
+The `.kpkg` is the canonical update unit; the `.tar.gz` carries its signed
+manifest internally, and the extra detached `.minisig` lets a user with only
+stock `minisign` verify before unpacking.
### `scripts/release.sh` — the multi-target matrix
@@ -183,8 +186,9 @@ kit update --dry-run # print the plan; change nothing
Networked updates resolve the channel index from `--index <url>` or
`$KIT_UPDATE_INDEX_URL` and download through a fetch hook in the driver
-(`driver_fetch_url`, which shells to `curl`/`wget`); libkit itself stays
-network-free, and since packages are signed the transport is untrusted.
+(`driver_fetch_url`, which invokes `curl` directly with no downloader
+fallback); libkit itself stays network-free, and since packages are signed the
+transport is untrusted.
**Version policy**: channel-driven updates are **monotonic** by default — `kit
update` refuses a version older than the running one. Deliberate moves bypass
diff --git a/doc/RELEASE_AUDIT_2026_6_0.md b/doc/RELEASE_AUDIT_2026_6_0.md
@@ -488,17 +488,17 @@ Implementation checkpoint (updated 2026-07-16):
| KIT-P0-002 | In progress | native SDK precedence implementation present; final macOS matrix pending |
| KIT-P1-013 | In progress | Windows direct/PATH/move/repair VM scenario passes; POSIX aggregate pending |
| KIT-P1-011 | **Complete** | `test-driver-pkg`: 194 pass, 0 fail |
-| KIT-P2-007 | **Complete** | `test-selfdist`: 95 pass, 0 fail; release configuration/test-anchor build gates pass |
-| KIT-P1-012 | **Complete** | authenticated updater result cases included in `test-selfdist`: 95 pass, 0 fail |
+| KIT-P2-007 | **Complete** | `test-selfdist`: 101 pass, 0 fail; release/dist configuration and test-anchor gates pass |
+| KIT-P1-012 | **Complete** | authenticated updater result cases included in `test-selfdist`: 101 pass, 0 fail |
| KIT-P1-001 | In progress | public registry and `kit targets` implemented; final aggregate pending |
| KIT-P0-004 | In progress | public compatibility boundary implemented; final format matrix pending |
| KIT-P1-002 | In progress | owned Android define test passes; NDK matrix pending |
| KIT-P1-003 | **Complete** | `test-smoke-rv32`: 20 pass, 0 fail, including concurrent empty-cache/read-only/QEMU cases |
| KIT-P1-004 | In progress | rv32 flag/ABI cases pass; final rv64 oracle pending |
-| KIT-P0-003 | **Complete** | portable-C O0/O1/O2 native and cross driver cases pass |
+| KIT-P0-003 | **Complete** | `test-driver-cc`: 162 pass, 0 fail, including 12 portable-C native/cross O0/O1/O2 cases |
| KIT-P1-014 | **Complete** | reserved-`typeof` focused suite: 6 pass, 0 fail, including uthash probe |
| KIT-P2-001 | **Complete** | `test-driver-stack-protector`: 133 pass, 0 fail; cJSON build/test passes |
-| KIT-P1-015 | **Complete** | 21 multi-source dependency assertions pass in the driver suite |
+| KIT-P1-015 | **Complete** | 21 multi-source dependency assertions pass within `test-driver-cc`: 162 pass, 0 fail |
| KIT-P2-003 | **Complete** | `test-driver-diagnostics`: 15 pass, 0 fail |
| KIT-P1-007 | **Complete** | `test-driver-strip`: object API 26/0, relocatable/archive 5/0, linked ELF/Mach-O/PE matrix 91/0 |
| KIT-P1-017 | **Complete** | relocatable/linked ELF, Mach-O, COFF, and Wasm image cases included in `test-driver-tools`: 251 pass, 0 fail |
diff --git a/driver/cmd/update.c b/driver/cmd/update.c
@@ -748,6 +748,17 @@ int driver_update(int argc, char** argv) {
}
}
+ /* --check is specifically an authenticated channel-index query. Do not
+ * let an accompanying local action bypass that query (or, for an install,
+ * mutate state despite the read-only spelling). */
+ if (do_check &&
+ (file || want_version || do_list || do_prune || do_rollback)) {
+ driver_errf(UPDATE_TOOL,
+ "--check cannot be combined with a package, --version, "
+ "--list, --prune, or --rollback");
+ return 2;
+ }
+
driver_env_init(&env);
ctx = driver_env_to_context(&env);
if (up_resolve_paths(&paths) != 0) {
diff --git a/driver/env.h b/driver/env.h
@@ -279,9 +279,9 @@ int driver_kit_home(char* buf, size_t cap);
/* Fetch `url` to the local file `dest`, overwriting it. The transport is
* UNTRUSTED — the caller verifies a signature/content-id over the bytes. file://
- * URLs are copied internally; other schemes use curl/wget (POSIX) or curl.exe
- * (Windows), with the URL passed as a distinct argv element (no shell). Returns
- * 0 on success, nonzero on any failure. */
+ * URLs are copied internally; other schemes use curl on POSIX or curl.exe on
+ * Windows, with the URL passed as a distinct argv element (no shell and no
+ * downloader fallback). Returns 0 on success, nonzero on any failure. */
int driver_fetch_url(const char* url, const char* dest);
/* Set a linked binary output's final mode according to the active umask.
diff --git a/mk/dist.mk b/mk/dist.mk
@@ -4,33 +4,40 @@
# release (doc/plan/SELFDIST.md). `make dist` builds RELEASE=1, stages a
# self-contained kit/ tree, and emits — for the native host triple — a signed
# fat .kpkg, a signed portable .tar.gz, and a detached .tar.gz.minisig (the
-# stock-minisign bootstrap path). It signs with $(KIT_SIGN_KEY) when set, else
-# the in-tree NON-RELEASE test key (with a loud warning), so the full
-# make-dist -> verify -> update pipeline runs hermetically. The multi-target
-# matrix + channel index live in scripts/release.sh.
+# stock-minisign bootstrap path). Release trust is mandatory: callers provide
+# KIT_SIGN_KEY, KIT_RELEASE_PUBKEYS, and KIT_UPDATE_INDEX_URL. Hermetic tests
+# that intentionally use the in-tree non-release key must opt in explicitly
+# with KIT_RELEASE_ALLOW_TEST_KEY=1. The multi-target matrix + channel index
+# live in scripts/release.sh.
.PHONY: dist
+ifneq ($(filter dist,$(MAKECMDGOALS)),)
+ifeq ($(strip $(KIT_SIGN_KEY)),)
+$(error dist requires KIT_SIGN_KEY (production Minisign secret key file))
+endif
+ifneq ($(KIT_RELEASE_ALLOW_TEST_KEY),1)
+ifeq ($(realpath $(KIT_SIGN_KEY)),$(realpath test/dist/keys/nonrelease.key))
+$(error dist refuses the in-tree NON-RELEASE test signing key)
+endif
+endif
+endif
+
DIST_DIR = build/dist
DIST_STAGING = $(DIST_DIR)/kit
DIST_REL_BIN = build/release/kit
DIST_REL_LIB = build/release/libkit.a
-DIST_KEY = $(if $(KIT_SIGN_KEY),$(KIT_SIGN_KEY),test/dist/keys/nonrelease.key)
+DIST_KEY = $(KIT_SIGN_KEY)
DIST_BASE = kit-$(KIT_VERSION)-$(KIT_HOST_TRIPLE)
DIST_KPKG = $(DIST_DIR)/$(DIST_BASE).kpkg
DIST_TARGZ = $(DIST_DIR)/$(DIST_BASE).tar.gz
dist:
- $(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'; \
- printf '*** artifacts verify but are NOT a real release. Set KIT_SIGN_KEY\n'; \
- printf '*** to sign for distribution.\n\n'; \
+ @if [ ! -f "$(KIT_SIGN_KEY)" ]; then \
+ printf '%s\n' 'dist: KIT_SIGN_KEY is not a file: $(KIT_SIGN_KEY)' >&2; \
+ exit 2; \
fi
+ $(MAKE) RELEASE=1 bin
@rm -rf $(DIST_STAGING)
@mkdir -p $(DIST_STAGING)/bin $(DIST_STAGING)/lib $(DIST_STAGING)/support/rt
cp $(DIST_REL_BIN) $(DIST_STAGING)/bin/kit
@@ -43,12 +50,12 @@ dist:
@for f in LICENSE LICENSE.txt LICENSE.md NOTICE NOTICE.txt; do \
[ -f "$$f" ] && cp "$$f" "$(DIST_STAGING)/$$f"; done; true
$(DIST_REL_BIN) pkg create --name kit --version $(KIT_VERSION) \
- --format kpkg --native-shape fat -s $(DIST_KEY) \
+ --format kpkg --native-shape fat -s "$(DIST_KEY)" \
--root $(DIST_STAGING) -o $(DIST_KPKG)
$(DIST_REL_BIN) pkg create --name kit --version $(KIT_VERSION) \
- --format tar.gz -s $(DIST_KEY) \
+ --format tar.gz -s "$(DIST_KEY)" \
--root $(DIST_STAGING) -o $(DIST_TARGZ)
- $(DIST_REL_BIN) pkg sign -s $(DIST_KEY) \
+ $(DIST_REL_BIN) pkg sign -s "$(DIST_KEY)" \
-o $(DIST_TARGZ).minisig --comment "kit $(KIT_VERSION) $(KIT_HOST_TRIPLE)" \
$(DIST_TARGZ)
@echo "dist: $(abspath $(DIST_KPKG))"
diff --git a/scripts/release.sh b/scripts/release.sh
@@ -1,10 +1,10 @@
#!/usr/bin/env bash
# scripts/release.sh — the multi-target kit release driver.
#
-# This is the matrix counterpart to `make dist`: where `make dist` stages and
-# signs the toolchain for the NATIVE host triple only (and falls back to the
-# in-tree NON-RELEASE test key), release.sh does the same staging + packaging
-# for EVERY hosted (runnable) triple in scripts/hosted.sh's support set, signs
+# This is the matrix counterpart to `make dist`: where `make dist` stages,
+# packages, and signs the toolchain for the NATIVE host triple only, release.sh
+# does the same for EVERY hosted (runnable) triple in scripts/hosted.sh's
+# support set, signs
# each artifact with the REAL release key, and finally emits + signs one
# `kit-release 1` channel index covering all of them. See doc/plan/SELFDIST.md
# ("Release artifacts", "Channel index format", and the scripts/release.sh work
@@ -25,8 +25,7 @@
#
# Prerequisites
# -------------
-# * A REAL signing key in $KIT_SIGN_KEY (see below). Unlike `make dist`, this
-# script never falls back to the in-tree test key — a release is signed with
+# * A REAL signing key in $KIT_SIGN_KEY (see below). A release is signed with
# the real release secret or not at all.
# * Cross-build sysroots for every foreign-libc target, provisioned once with
# `make provision TARGET=<selector>` (doc/plan/SYSROOTS.md). kit_cross.sh
@@ -82,8 +81,7 @@ canon_path() {
# ---- preconditions ---------------------------------------------------------
# A real signing key is mandatory; releasing with the test key is never valid.
[ -n "${KIT_SIGN_KEY:-}" ] || die \
- "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.)"
+ "KIT_SIGN_KEY is unset — set it to the real release minisign secret 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)"
diff --git a/test/dist/keys/README.md b/test/dist/keys/README.md
@@ -4,11 +4,14 @@
minisign (Ed25519) keypair, key id `c1709dd2922282f6`, used only by:
- the hermetic self-distribution end-to-end test (`test/dist/run.sh`), and
-- the `make dist` fallback signer when `KIT_SIGN_KEY` is unset.
+- release-artifact harnesses that explicitly set the key paths and
+ `KIT_RELEASE_ALLOW_TEST_KEY=1`.
-Its public key is embedded in `driver/release_key.c` as a built-in `kit update`
-trust anchor **so the test and `make dist` artifacts verify against a built-in
-key**. It is published in the open here on purpose; it confers **no** trust over
-real kit distributions. The production release keypair is generated offline, its
-secret never enters the tree, and its public key replaces/augments the embedded
-set at release time (see `doc/plan/SELFDIST.md`).
+Development binaries embed its public key as a built-in `kit update` trust
+anchor so the hermetic tests can verify without external state. It is published
+in the open here on purpose; it confers **no** trust over real kit
+distributions. `make dist` and `scripts/release.sh` require explicit release
+configuration and reject this key unless a test harness opts in. The production
+release keypair is generated offline, its secret never enters the tree, and its
+public key replaces/augments the embedded set at release time (see
+`doc/plan/SELFDIST.md`).
diff --git a/test/dist/run.sh b/test/dist/run.sh
@@ -50,6 +50,22 @@ 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
+# The native distribution target is an official RELEASE=1 artifact path. It
+# must never invent a channel, trust anchor, or signing key. Tests may use the
+# checked-in non-release pair only through the same explicit opt-in as the
+# multi-target release harness.
+run_fail "release-dist-sign-key-required" make -n -C "$repo_root" dist
+run_fail "release-dist-config-required" make -C "$repo_root" dist \
+ KIT_SIGN_KEY="$work/release-config.key"
+run_fail "release-dist-test-key-rejected" make -C "$repo_root" dist \
+ KIT_SIGN_KEY="$SECKEY" \
+ KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \
+ KIT_RELEASE_PUBKEYS="$PUBKEY"
+run_ok "release-dist-explicit-test-shape" make -n -C "$repo_root" dist \
+ KIT_SIGN_KEY="$SECKEY" KIT_RELEASE_ALLOW_TEST_KEY=1 \
+ KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \
+ KIT_RELEASE_PUBKEYS="$PUBKEY"
+
# ---- local helpers ---------------------------------------------------------
have_cmd() { command -v "$1" >/dev/null 2>&1; }
@@ -58,6 +74,24 @@ have_cmd() { command -v "$1" >/dev/null 2>&1; }
kh=
kit_home() { kh="$work/$1"; export KIT_HOME="$kh"; }
+# run_usage NAME CMD... : require the driver's reserved usage status (2).
+run_usage() {
+ name=$1
+ shift
+ "$@" > "$work/$name.out" 2> "$work/$name.err"
+ got=$?
+ if [ "$got" -eq 2 ]; then
+ ok "$name"
+ else
+ {
+ printf 'expected usage status 2, got %s\n' "$got"
+ sed 's/^/stdout: /' "$work/$name.out"
+ sed 's/^/stderr: /' "$work/$name.err"
+ } > "$work/$name.diag"
+ not_ok "$name" "$work/$name.diag"
+ fi
+}
+
# stage_kpkg VER : build a signed fat kit kpkg containing bin/kit (= $KIT), a
# VERSION file, and a small lib/ payload. Records a pass/fail through run_ok and
# leaves the kpkg path in $LAST_KPKG (NOT echoed — calling this in a command
@@ -165,6 +199,19 @@ kit_home kh
stage_kpkg 2026.6.0
kpkg_60="$LAST_KPKG"
+# --check is a channel-index query, never an alias for a local mutating action.
+kit_home kh-check-local-package
+run_usage "check-local-package-usage" "$KIT" update --check "$kpkg_60"
+if [ -e "$KIT_HOME" ]; then
+ echo "local-package --check created KIT_HOME" > \
+ "$work/check-local-package-no-state.diag"
+ not_ok "check-local-package-no-state" \
+ "$work/check-local-package-no-state.diag"
+else
+ ok "check-local-package-no-state"
+fi
+kit_home kh
+
run_ok "a-install-60" "$KIT" update "$kpkg_60"
contains "a-install-60-msg" "$work/a-install-60.out" "installed kit 2026.6.0"
current_is "a-current-60" "$kh" 2026.6.0