Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9970703
The update channel's rules: which statement of what is current zone 0…
DevomB Sep 20, 2026
143b18f
kryptik-update checks a manifest and a pointer for the update channel…
DevomB Sep 20, 2026
bb1938f
What zone 0 keeps for the update channel, and what each of the three …
DevomB Sep 20, 2026
a2c38e5
The update channel's verbs on the broker, and `kryptik update` for th…
DevomB Sep 20, 2026
174995f
The net zone fetches for the update channel: a pipe with a Range head…
DevomB Sep 20, 2026
4001a01
Merge remote-tracking branch 'origin/main' into update-channel
DevomB Sep 20, 2026
603d35e
The update channel's design says what was built, and the roadmap says…
DevomB Sep 20, 2026
b529c30
The launch service's log line for an update verb names the verb and n…
DevomB Sep 20, 2026
43816d6
kryptik-update makes its own snapshot directory and never takes one f…
DevomB Sep 20, 2026
80a241f
Merge remote-tracking branch 'origin/main' into update-channel
DevomB Sep 20, 2026
54aeda1
A pointer can be verified on a real image: the trust anchor enrols a …
DevomB Sep 20, 2026
da6113b
The release tool writes and signs the statement of what is current, a…
DevomB Sep 20, 2026
e0b2a5f
The update suite fetches a release over the network: the channel, end…
DevomB Sep 20, 2026
0e422ca
The update suite's network step, after review: a proven restart, a wa…
DevomB Sep 20, 2026
709d7db
Merge remote-tracking branch 'origin/main' into update-channel
DevomB Sep 20, 2026
bcea985
The broker's fuzz seeds gain the update channel's three verbs
DevomB Sep 20, 2026
7dbd0a5
The suites' release host holds a requested name to its root before it…
DevomB Sep 20, 2026
6ac2a4f
Merge remote-tracking branch 'origin/main' into update-channel
DevomB Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/distro.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,8 @@ jobs:
# Never the private halves of the keys: the acceptance job needs the
# certificate, its DER form and the variable stores, nothing that
# signs. An artifact of a public repository is public.
sudo tar --zstd -cf work-after-media.tar.zst -C work --exclude='images/kryptik-root.img' --exclude='images/esp-*.img' --exclude='keys/sb/kryptik-sb.key' --exclude='keys/release/kryptik-release' .
if sudo tar --zstd -tf work-after-media.tar.zst | grep -E 'kryptik-sb\.key$|keys/release/kryptik-release$'; then echo "a private key is in the artifact"; exit 1; fi
sudo tar --zstd -cf work-after-media.tar.zst -C work --exclude='images/kryptik-root.img' --exclude='images/esp-*.img' --exclude='keys/sb/kryptik-sb.key' --exclude='keys/release/kryptik-release' --exclude='keys/release/kryptik-latest' .
if sudo tar --zstd -tf work-after-media.tar.zst | grep -E 'kryptik-sb\.key$|keys/release/kryptik-(release|latest)$'; then echo "a private key is in the artifact"; exit 1; fi
sudo chown "$(id -u):$(id -g)" work-after-media.tar.zst
ls -la work-after-media.tar.zst; df -h /mnt | tail -1

Expand Down
15 changes: 14 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ CHROOT_RUN := $(SUDO) env $(CHROOT_ENV) "$(CHROOTD)"
vm-disk vm-disk-boot vm-restart vm-measure cli-test update-tree-test identity-test serve-test \
test-harness test-hardening test-artifacts audit-artifacts test-boot-success \
audit-artifacts-strict manifest verify-manifest test-manifest \
test-s6-init smoke-userspace test-services test-netzone-time test-libc-unwind \
test-s6-init smoke-userspace test-services test-netzone-time test-update-verify test-update-fetch test-libc-unwind \
sign-image verify-image test-image-signing test-installer test-mkdisk-guards \
install-test \
image image-boot \
Expand Down Expand Up @@ -185,6 +185,8 @@ help:
@echo " make smoke-userspace RUN the built userland in the chroot (needs root)"
@echo " make test-services validate the s6-rc service tree"
@echo " make test-netzone-time the net zone's time measurement, under every shell here"
@echo " make test-update-verify what kryptik-update believes: a payload, a manifest, a pointer"
@echo " make test-update-fetch the net zone's update fetcher, against a local server and broker"
@echo " make identity-test zone files, compositor colour table and zoneid audit agree"
@echo " make test-libc-unwind prove the target libc can unwind (needs root)"
@echo " make sign-image sign the disk image with a developer key"
Expand Down Expand Up @@ -600,6 +602,17 @@ test-services:
test-netzone-time:
@"$(TOOLS)"/test-netzone-time.sh

# What kryptik-update believes, with its own functions and a real ssh-keygen:
# a payload whose manifest is swapped under it, and the two checks the update
# channel runs on a manifest and on a statement of what is current.
test-update-verify:
@"$(TOOLS)"/test-update-manifest-snapshot.sh

# The net zone's half of the update channel: a faithful pipe, from the offsets
# zone 0 names, in pieces zone 0 takes, that stops when zone 0 says no.
test-update-fetch:
@"$(TOOLS)"/test-update-fetch.sh

# boot-success.sh's decision table (commit, refuse, fall back), driven on
# the host with stand-ins for the services, the ESP and the firmware.
test-boot-success:
Expand Down
53 changes: 50 additions & 3 deletions build/stages/04-base-system.sh
Original file line number Diff line number Diff line change
Expand Up @@ -928,9 +928,23 @@ s_release_trust() {
chmod 0600 "$keydir/kryptik-release"
echo "generated a new developer release signing key"
fi
# A second key, for one thing: signing the update channel's statement of
# what is current (docs/design/update-channel.md). It is honoured in the
# kryptik-latest namespace and nowhere else, and the release key is
# honoured in kryptik-release and nowhere else, so the key that has to be
# at hand on a schedule can never sign a release, and the key that signs
# releases never has to be. An owner who wants one key for both lists
# the release key on the second line instead; nothing else changes.
if [[ ! -f "$keydir/kryptik-latest" ]]; then
ssh-keygen -q -t ed25519 -N "" -C "kryptik-latest (developer)" -f "$keydir/kryptik-latest"
chmod 0600 "$keydir/kryptik-latest"
echo "generated a new developer key for statements of what is current"
fi
install -d -m 0755 /usr/share/kryptik/trust
printf 'kryptik-release namespaces="kryptik-release" %s\n' "$(cut -d' ' -f1,2 "$keydir/kryptik-release.pub")" \
> /usr/share/kryptik/trust/release-signers
{
printf 'kryptik-release namespaces="kryptik-release" %s\n' "$(cut -d' ' -f1,2 "$keydir/kryptik-release.pub")"
printf 'kryptik-latest namespaces="kryptik-latest" %s\n' "$(cut -d' ' -f1,2 "$keydir/kryptik-latest.pub")"
} > /usr/share/kryptik/trust/release-signers
chmod 0644 /usr/share/kryptik/trust/release-signers
# Developer tier: the updater accepts development-role manifests. A
# production image changes this file (and its key), deliberately.
Expand All @@ -957,6 +971,25 @@ s_release_trust() {
echo "FAIL: a foreign key verified against the anchor"; rm -rf "$t"; return 1
fi
echo "ok: a foreign key is refused"
# The two keys, each in its own namespace and refused in the other's:
# what makes the statement key safe to keep where a timer can reach it.
local who ns other
for who in kryptik-release kryptik-latest; do
ns="$who"; [[ "$who" == kryptik-release ]] && other=kryptik-latest || other=kryptik-release
rm -f "$t/$who.sig"
printf 'probe of %s\n' "$who" > "$t/$who"
ssh-keygen -Y sign -f "$keydir/$who" -n "$ns" "$t/$who" < /dev/null >/dev/null 2>&1 \
&& ssh-keygen -Y verify -f /usr/share/kryptik/trust/release-signers -I "$who" -n "$ns" -s "$t/$who.sig" < "$t/$who" >/dev/null 2>&1 \
|| { echo "FAIL: the $who key does not verify in its own namespace"; rm -rf "$t"; return 1; }
rm -f "$t/$who.sig"
ssh-keygen -Y sign -f "$keydir/$who" -n "$other" "$t/$who" < /dev/null >/dev/null 2>&1
# A refusal only counts when there was a signature to refuse.
[[ -s "$t/$who.sig" ]] || { echo "FAIL: could not sign the probe of $who in $other"; rm -rf "$t"; return 1; }
if ssh-keygen -Y verify -f /usr/share/kryptik/trust/release-signers -I "$who" -n "$other" -s "$t/$who.sig" < "$t/$who" >/dev/null 2>&1; then
echo "FAIL: the $who key verified in the $other namespace"; rm -rf "$t"; return 1
fi
done
echo "ok: each key verifies in its own namespace and is refused in the other's"
rm -rf "$t"
}

Expand All @@ -972,6 +1005,8 @@ s_netzone() {
# The SNTP query the net zone measures the clock with (docs/design/time.md).
install -D -m 0755 "${KRYPTIK_ROOT}/tools/net/sntp-offset.py" /usr/libexec/kryptik/sntp-offset.py
python3 -m py_compile /usr/libexec/kryptik/sntp-offset.py || { echo "sntp-offset.py does not compile under the target python"; return 1; }
install -D -m 0755 "${KRYPTIK_ROOT}/tools/net/update-fetch.py" /usr/libexec/kryptik/update-fetch.py
python3 -m py_compile /usr/libexec/kryptik/update-fetch.py || { echo "update-fetch.py does not compile under the target python"; return 1; }
rm -rf /usr/libexec/kryptik/__pycache__
for t in dhcpcd nft dnsmasq ip; do
command -v "$t" >/dev/null 2>&1 && echo " ok $t" || { echo " MISSING $t"; return 1; }
Expand Down Expand Up @@ -2512,7 +2547,7 @@ PACKAGES=(
# kryptik-update, which refuses to start without kryptik-efiboot.
"efiboot" "s_efiboot $(sha256_of "${KRYPTIK_ROOT}/tools/efi/kryptik-efiboot.c" 2>/dev/null || echo none)"
"updater" "s_updater $(sha256_of "${KRYPTIK_ROOT}/tools/update/kryptik-update" 2>/dev/null || echo none) $(sha256_of "${KRYPTIK_ROOT}/tools/update/kryptik-recover" 2>/dev/null || echo none)"
"netzone" "s_netzone $(sha256_of "${KRYPTIK_ROOT}/tools/net/netzone-init.sh" 2>/dev/null || echo none)-$(sha256_of "${KRYPTIK_ROOT}/tools/net/sntp-offset.py" 2>/dev/null || echo none)"
"netzone" "s_netzone $(sha256_of "${KRYPTIK_ROOT}/tools/net/netzone-init.sh" 2>/dev/null || echo none)-$(sha256_of "${KRYPTIK_ROOT}/tools/net/sntp-offset.py" 2>/dev/null || echo none)-$(sha256_of "${KRYPTIK_ROOT}/tools/net/update-fetch.py" 2>/dev/null || echo none)"
"installer" "s_installer $(sha256_of "${KRYPTIK_ROOT}/tools/install/kryptik-install.sh" 2>/dev/null || echo none)"
# The path and the binary's content hash are arguments so that both are
# part of this step's fingerprint; see s_kryptikd.
Expand Down Expand Up @@ -2565,6 +2600,18 @@ require_inside_chroot "stage 04" "system"
# temporary tools and nothing built with them can claim to be unchanged.
stage_depends_on "tt-" verify

# The signing keys live under ${KRYPTIK_WORK}/keys, outside the sysroot and
# outside any cache of it, on purpose. A work tree restored from such a cache
# has release-trust stamped as built and no keys; the anchor in the restored
# sysroot then names keys that no longer exist, and stage 06 would sign with
# ones the image does not trust, or find none. So, as for the kernel tree:
# no keys, no stamp. The step then makes both and writes the anchor again.
if [[ -f "${STAMPS}/${STAMP_PREFIX}release-trust" ]] && \
[[ ! -f "${KRYPTIK_WORK}/keys/release/kryptik-release" || ! -f "${KRYPTIK_WORK}/keys/release/kryptik-latest" ]]; then
warn "release-trust is stamped as built but a signing key under ${KRYPTIK_WORK}/keys/release is gone; the step runs again."
rm -f "${STAMPS}/${STAMP_PREFIX}release-trust"
fi

unwired=0
for ((i = 0; i < ${#PACKAGES[@]}; i += 2)); do
name="${PACKAGES[i]}"
Expand Down
22 changes: 22 additions & 0 deletions build/stages/06-iso.sh
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,28 @@ s_payload() {
"${KRYPTIK_ROOT}/tools/release-manifest.sh" verify --signers "$signers" --principal kryptik-release \
--root "$out" --exact --strict "$out/manifest"
ls -la "$out"

# What a release host serves beside the payload (docs/design/update-channel.md):
# the signed statement that this release is current. Outside the payload
# directory, because `apply` refuses a payload that holds anything its
# manifest does not list. `base` is relative, so the same two files serve
# from wherever the channel is. `not-a-pointer` is the same statement
# signed by the release key in the manifest's namespace, which the image
# must refuse; the update suite serves it to prove that on the real chain.
[[ -f "$keydir/kryptik-latest" ]] || { echo "no statement key at ${keydir}; stage 04 (release-trust) makes it"; return 1; }
local chan="${IMG}/channel-${KRYPTIK_VERSION}"
rm -rf "$chan"; mkdir -p "$chan"
"${KRYPTIK_ROOT}/tools/release-manifest.sh" pointer --key "$keydir/kryptik-latest" \
--manifest "$out/manifest" --base "${KRYPTIK_VERSION}/" --out "$chan/latest"
cp "$chan/latest" "$chan/not-a-pointer"
ssh-keygen -Y sign -f "$keydir/kryptik-release" -n kryptik-release "$chan/not-a-pointer" < /dev/null >/dev/null 2>&1 \
|| { echo "could not sign the control statement"; return 1; }
ssh-keygen -Y verify -f "$signers" -I kryptik-latest -n kryptik-latest -s "$chan/latest.sig" < "$chan/latest" >/dev/null \
|| { echo "FAIL: the image's anchor does not verify the statement this build just signed"; return 1; }
if ssh-keygen -Y verify -f "$signers" -I kryptik-release -n kryptik-latest -s "$chan/not-a-pointer.sig" < "$chan/not-a-pointer" >/dev/null 2>&1; then
echo "FAIL: the image's anchor accepts a statement signed by the release key"; return 1
fi
ls -la "$chan"
}

# The release record under ${KRYPTIK_OUT}: the small things (hashes, root
Expand Down
6 changes: 6 additions & 0 deletions compartments/kryptikd/fuzz-corpus/broker-requests
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ time-offset +3599.999999 16
time-offset 1e9 4
time-offset nan 4
steal
update-poll
update-latest 300 120
update-latest 8193 1
update-put manifest 0 64
update-put kryptik-root.img 1048576 1048576
update-put ../kryptik-root.img 0 5
14 changes: 14 additions & 0 deletions compartments/kryptikd/probes/boundary-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,20 @@ MATCH="does not hold the network" check "the clock's verb is refused from a zone
"
MATCH="is not an offset in seconds" check "a time claim outside the grammar is refused at parse time" 0 /usr/bin/python3 -c "$BRK" "time-offset 1e9 4
"
# The update channel (docs/design/update-channel.md): a release is brought by
# the zone that holds the network and by no other. From this zone all three
# verbs are refused by who is asking, the two that carry bytes before a byte
# of them is read, and a request outside the grammar before that.
MATCH="does not hold the network" check "a statement of what is current is refused from a zone that does not hold the network" 0 /usr/bin/python3 -c "$BRK" "update-latest 5 3
helloabc"
MATCH="does not hold the network" check "asking whether a release is wanted is refused from a zone that does not hold the network" 0 /usr/bin/python3 -c "$BRK" "update-poll
"
MATCH="does not hold the network" check "a piece of a release is refused from a zone that does not hold the network" 0 /usr/bin/python3 -c "$BRK" "update-put kryptik-root.img 0 5
hello"
MATCH="must be a single path component" check "a piece of a release named with a path is refused at parse time" 0 /usr/bin/python3 -c "$BRK" "update-put ../kryptik-root.img 0 5
hello"
MATCH="is not 1 to 1048576 bytes" check "a piece of a release larger than one piece is refused at parse time" 0 /usr/bin/python3 -c "$BRK" "update-put kryptik-root.img 0 1048577
"
MATCH="^ok text/plain 5 hello$" check "clipboard-set then clipboard-get round-trips" 0 /bin/sh -c "python3 -c '$BRK' 'clipboard-set text/plain 5
hello' >/dev/null && python3 -c '$BRK' 'clipboard-get
'"
Expand Down
Loading
Loading