From d68e8873d9c7d227d8fdba8c2a68ec1f09279d3c Mon Sep 17 00:00:00 2001 From: DevomB Date: Tue, 6 Oct 2026 21:22:14 -0700 Subject: [PATCH 1/3] kryptik-recover verifies a slot against the signed command line before it commits it: --restore-slot took the medium's unsigned root.json at its word, so a medium whose root was altered and whose root.json was rewritten to match was written, read back as matching, and committed as a disk that boots The installer's checks of root.json move into build/service-scripts/medium-root.sh, which kryptik-install and kryptik-recover both source: every field has its form before it is used (a root_hash that is not a hash is refused without being printed), the record must name the root hash and size in the medium's signed command line, each slot kernel copied from the medium must carry that hash, and veritysetup verify runs on what was written before anything names it. The installer now checks both medium kernels before its first write (the ISO's ESP image is mounted for that) and lists veritysetup and grep among the tools it checks up front. integrity-test.sh step 4 first boots a copy of the medium with one byte changed in a free block of its root and root.json's sha256 rewritten to match, and expects the restore refused and the ESP naming no kernel for slot a; tools/tests/installer.sh covers the record and kernel checks. --- build/recipes/boot-check.sh | 1 + build/service-scripts/medium-root.sh | 66 ++++++++++++++++++++++++++++ docs/design/boot-and-updates.md | 9 ++++ docs/user-guide.md | 5 ++- tools/image/integrity-test.sh | 44 +++++++++++++++++-- tools/install/kryptik-install.sh | 53 +++++----------------- tools/tests/installer.sh | 59 ++++++++++++++++++++++--- tools/update/kryptik-recover | 15 ++++++- 8 files changed, 198 insertions(+), 54 deletions(-) create mode 100755 build/service-scripts/medium-root.sh diff --git a/build/recipes/boot-check.sh b/build/recipes/boot-check.sh index 8c19c9dd..7f0b1bef 100644 --- a/build/recipes/boot-check.sh +++ b/build/recipes/boot-check.sh @@ -91,6 +91,7 @@ s_boot_check() { chk "boot scripts" /usr/libexec/kryptik/sysinit.sh x chk "test control helper" /usr/libexec/kryptik/testctl.sh chk "keyboard helper" /usr/libexec/kryptik/keyboard.sh + chk "medium root checks" /usr/libexec/kryptik/medium-root.sh chk "keyboard layouts" /usr/share/kryptik/keyboard-layouts chk "loadkeys" /usr/bin/loadkeys x chk "boot-success" /usr/libexec/kryptik/boot-success.sh x diff --git a/build/service-scripts/medium-root.sh b/build/service-scripts/medium-root.sh new file mode 100755 index 00000000..7ba745b4 --- /dev/null +++ b/build/service-scripts/medium-root.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# The root image an install medium carries, as kryptik-install and +# kryptik-recover take it onto a disk. root.json is the medium's own record, +# not signed material: a rewritten record must be able to make a tool fail and +# nothing else, so every field is checked for form before it is used, and what +# lands on a disk is verified against the root hash the signed kernel carries. +# POSIX sh, sourced; the caller defines die. +# medium_root JSON CMDLINE the record, checked; sets ROOT_BYTES ROOT_SHA +# VERSION and the table's V_BLOCKS V_HASH_START +# V_HASH V_SALT +# kernel_names_root EFI the kernel's command line carries V_HASH +# root_verifies DEV DEV holds the root V_HASH names + +decimal_field() { # decimal_field NAME VALUE: a bounded decimal, or die + case "$2" in ''|*[!0-9]*) die "root.json: $1 is not a number" ;; esac + [ "${#2}" -le 15 ] || die "root.json: $1 is too large" +} +hex_field() { # hex_field NAME VALUE LENGTH: lowercase hex of that length, or die + case "$2" in *[!0-9a-f]*) die "root.json: $1 is not a hash" ;; esac + [ "${#2}" -eq "$3" ] || die "root.json: $1 is not a hash" +} +# The root's dm-verity table from the signed command line, as +# "data_blocks hash_start_block root_hash salt"; empty when there is none. +verity_of() { # verity_of FILE + sed -n 's/.* verity 1 [^ ]* [^ ]* 4096 4096 \([0-9][0-9]*\) \([0-9][0-9]*\) sha256 \([0-9a-f]\{64\}\) \([0-9a-f]*\) .*/\1 \2 \3 \4/p' "$1" | head -1 +} +json_field() { # json_field JSON NAME: a top-level field of root.json + sed -n "s/^ \"$2\": \"\{0,1\}\([^\",]*\)\"\{0,1\},\{0,1\}\$/\1/p" "$1" | head -1 +} + +medium_root() { # medium_root JSON CMDLINE + [ -r "$1" ] || die "no root.json on the medium" + ROOT_BYTES="$(json_field "$1" total_bytes)"; ROOT_SHA="$(json_field "$1" sha256)"; VERSION="$(json_field "$1" version)" + [ -n "$ROOT_BYTES" ] && [ -n "$ROOT_SHA" ] || die "root.json is incomplete" + decimal_field total_bytes "$ROOT_BYTES" + hex_field sha256 "$ROOT_SHA" 64 + case "$VERSION" in *[!A-Za-z0-9._-]*) die "root.json: version has characters a version cannot" ;; esac + VERITY="$(verity_of "$2")" + [ -n "$VERITY" ] || die "could not read the root's verity table from the signed command line" + read -r V_BLOCKS V_HASH_START V_HASH V_SALT <` Boot#### entry for a trial. When the trial ends, committed or not, `forget` removes @@ -175,5 +183,6 @@ Host-side: `tools/tests/boot-success.sh`, `efiboot.sh`, `build/stages/06-iso.sh`, `06-kernel-bind.sh`, `tools/install/kryptik-install.sh`, `build/service-scripts/sysinit.sh`, +`medium-root.sh`, `boot-success.sh`, `tools/update/kryptik-update`, `kryptik-recover`, `tools/efi/kryptik-efiboot.c`, `tools/release-manifest.sh`. diff --git a/docs/user-guide.md b/docs/user-guide.md index f3430915..c430c6fd 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -339,8 +339,9 @@ state partition. `--restore-slot` writes the medium's own root image and kernel into the slot, as the installer does, then commits it. Every byte comes from the -medium, and the next boot checks them: the kernel refuses a root that does -not match the hash it carries. The state partition is not touched, so users +medium, and the slot is committed only once it verifies against the root +hash the medium's signed kernel carries. A medium whose root or slot kernel +is not that release's is refused with the reason, and nothing is committed. The state partition is not touched, so users and zone volumes survive. The result is the medium's version, which may be older than what was installed: the tool names the version it writes but does not compare it, so read `--status` first. diff --git a/tools/image/integrity-test.sh b/tools/image/integrity-test.sh index 0e27628b..fe6e995a 100755 --- a/tools/image/integrity-test.sh +++ b/tools/image/integrity-test.sh @@ -12,8 +12,9 @@ # (control: the signed medium boots under the same store); then # kryptik-recover --commit-slot a puts the signed kernel back # step 3 a byte of slot a's root flipped: dm-verity stops the boot -# step 4 kryptik-recover --restore-slot a from the medium; its records on -# the ESP are whole, and the user's data survives +# step 4 kryptik-recover --restore-slot a refuses a medium whose root is +# not the one its signed kernel names, then restores slot a from the +# medium; its records on the ESP are whole, and the user's data survives # step 5 an anchor, zone, sysctl, preload library and udev rule planted in # the state's /etc layer: none takes effect # step 6 the state header saved, wiped and restored by kryptik-recover @@ -32,12 +33,12 @@ while [[ "$#" -gt 0 ]]; do --usb) USB="${2:?}"; shift 2 ;; --disk) DISK="${2:?}"; shift 2 ;; --timeout) TIMEOUT="${2:?}"; shift 2 ;; - -h|--help) sed -n '2,21p' "${BASH_SOURCE[0]}"; exit 0 ;; + -h|--help) sed -n '2,22p' "${BASH_SOURCE[0]}"; exit 0 ;; *) die "unknown argument: $1" ;; esac done [[ -f "$USB" ]] || die "--usb IMG is required" -for t in python3 sbsign sbverify sbattach openssl mcopy mdel mdir mtype sfdisk cryptsetup losetup; do have "$t" || die "required tool not found: $t"; done +for t in python3 sbsign sbverify sbattach openssl mcopy mdel mdir mtype sfdisk cryptsetup losetup debugfs; do have "$t" || die "required tool not found: $t"; done VMDIR="${KRYPTIK_WORK}/vm"; mkdir -p "$VMDIR" DISK="${DISK:-${VMDIR}/integrity.img}" [[ -e "$DISK" && ! -f "$DISK" ]] && die "refusing: ${DISK} is not a regular file" @@ -155,6 +156,41 @@ grep -q 'login:' <<<"$T3" && red "a login prompt appeared on a tampered root" || step "step 4: recovery from the medium restores slot a; state survives" CTLR="${VMDIR}/testctl-recover.img" "${SELF}/mk-testctl.sh" --out "$CTLR" --key "$TESTCTL_KEY" recover_disk=/dev/vda recover_slot=a recover_mode=restore smoke_poweroff=1 install_wait=5 > /dev/null +# First a medium altered as whoever can write to it could alter it: a byte of a +# root block nothing reads at boot, and root.json rewritten to match. Slot a +# reads back as that record says; only the signed root hash tells. +ALT="$(mktemp -d)"; ALTUSB="${VMDIR}/integrity-altered-usb.img" +cp --sparse=always "$USB" "$ALTUSB" +U_ESP=$(( $(part_start "$ALTUSB" 1) * 512 )); U_ROOT=$(( $(part_start "$ALTUSB" 2) * 512 )) +mtype -i "${ALTUSB}@@${U_ESP}" ::/kryptik/root.json > "$ALT/root.json" +A_BLOCKS="$(sed -n 's/^ "data_blocks": \([0-9]*\).*/\1/p' "$ALT/root.json")" +A_BYTES="$(sed -n 's/^ "total_bytes": \([0-9]*\).*/\1/p' "$ALT/root.json")" +# A free block from the middle of the root's ext4: never read, still verified. +A_FREE="" +if [[ -n "$A_BLOCKS" ]] && A_LOOP="$(losetup --find --show --read-only --offset "$U_ROOT" --sizelimit $(( A_BLOCKS * 4096 )) "$ALTUSB")"; then + A_FREE="$(debugfs -R "ffb 1 $(( A_BLOCKS / 2 ))" "$A_LOOP" 2>/dev/null | sed -n 's/^Free blocks found: \([0-9][0-9]*\).*/\1/p')" + losetup -d "$A_LOOP" +fi +if [[ -n "$A_FREE" && -n "$A_BYTES" ]]; then + printf '\x5a' | dd of="$ALTUSB" bs=1 seek=$(( U_ROOT + A_FREE * 4096 + 100 )) conv=notrunc status=none + A_SHA="$(dd if="$ALTUSB" bs=4M iflag=skip_bytes,count_bytes skip="$U_ROOT" count="$A_BYTES" status=none | sha256sum | cut -c1-64)" + sed -i "s/^ \"sha256\": \"[0-9a-f]*\"/ \"sha256\": \"${A_SHA}\"/" "$ALT/root.json" + mcopy -o -i "${ALTUSB}@@${U_ESP}" "$ALT/root.json" ::/kryptik/root.json + smoke integ-p4a --usb "$ALTUSB" --disk "$DISK" --testctl "$CTLR" --vars enrolled --timeout "$TIMEOUT" > /dev/null + T4A="$(boot_txt)" + if grep -q 'KRYPTIK_RECOVER: rc=1' <<<"$T4A" && grep -q 'does not verify against the root hash' <<<"$T4A"; then + green "a medium whose root is not the one its signed kernel names is refused, though its root.json was rewritten to match" + else + red "the altered medium's root was not refused"; grep 'KRYPTIK_RECOVER' <<<"$T4A" | tail -5 | sed 's/^/ /' + fi + dd if="$DISK" of="$ESPIMG" bs=1M iflag=skip_bytes,count_bytes skip="$ESP_OFF" count=$((512*1024*1024)) status=none + mtype -i "$ESPIMG" ::/EFI/kryptik/kryptik-a.efi >/dev/null 2>&1 \ + && red "the refused restore left a kernel for slot a on the ESP" \ + || green "the refused restore left the ESP naming no kernel for slot a, so --commit-slot a takes nothing" +else + red "could not alter a copy of the medium (free block '${A_FREE}', total_bytes '${A_BYTES}')" +fi +rm -rf "$ALT" "$ALTUSB" smoke integ-p4 --usb "$USB" --disk "$DISK" --testctl "$CTLR" --vars enrolled --timeout "$TIMEOUT" > /dev/null boot_txt | grep -q 'KRYPTIK_RECOVER: rc=0' && green "kryptik-recover --restore-slot a succeeded from the medium" || { red "recovery did not report success"; boot_txt | grep 'KRYPTIK_RECOVER' | tail -5 | sed 's/^/ /'; } # The records recovery wrote on the ESP, read from the host: whole, and diff --git a/tools/install/kryptik-install.sh b/tools/install/kryptik-install.sh index 1f1feea6..a41b565b 100755 --- a/tools/install/kryptik-install.sh +++ b/tools/install/kryptik-install.sh @@ -12,23 +12,6 @@ PROG="kryptik-install" say() { printf '%s: %s\n' "$PROG" "$*"; } die() { printf '%s: FAILED: %s\n' "$PROG" "$*" >&2; exit 1; } -# root.json is the medium's own record, not signed material: a rewritten -# record must be able to make the install fail and nothing else, so every -# field is checked for form before it is used, and what lands on the disk is -# verified against the root hash the signed kernel carries. -decimal_field() { # decimal_field NAME VALUE: a bounded decimal, or die - case "$2" in ''|*[!0-9]*) die "root.json: $1 is not a number" ;; esac - [ "${#2}" -le 15 ] || die "root.json: $1 is too large" -} -hex_field() { # hex_field NAME VALUE LENGTH: lowercase hex of that length, or die - case "$2" in *[!0-9a-f]*) die "root.json: $1 is not a hash" ;; esac - [ "${#2}" -eq "$3" ] || die "root.json: $1 is not a hash" -} -# The root's dm-verity table from the signed command line, as -# "data_blocks hash_start_block root_hash salt"; empty when there is none. -verity_of() { # verity_of FILE - sed -n 's/.* verity 1 [^ ]* [^ ]* 4096 4096 \([0-9][0-9]*\) \([0-9][0-9]*\) sha256 \([0-9a-f]\{64\}\) \([0-9a-f]*\) .*/\1 \2 \3 \4/p' "$1" | head -1 -} # A slot may be made larger than this image needs, never smaller. slot_size_ok() { # slot_size_ok ASKED NEEDED: both in MiB, or die case "$1" in ''|0*|*[!0-9]*) die "--slot-size takes a number of MiB" ;; esac @@ -71,8 +54,8 @@ done # --- every external tool, checked before the first write ------------------- missing="" -for tool in sfdisk partx blockdev blkid cryptsetup stty mkfs.ext4 dd sha256sum mount umount sync awk sed \ - readlink lsblk head tail wc cmp cp mv chmod mkdir stat tr loadkeys chattr; do +for tool in sfdisk partx blockdev blkid cryptsetup veritysetup stty mkfs.ext4 dd sha256sum mount umount sync awk sed \ + grep readlink lsblk head tail wc cmp cp mv chmod mkdir stat tr loadkeys chattr; do command -v "$tool" >/dev/null 2>&1 || missing="${missing} ${tool}" done [ -z "$missing" ] || die "this system is missing:${missing} @@ -91,6 +74,8 @@ part_dev() { # are this system's own. . /usr/libexec/kryptik/devices.sh . /usr/libexec/kryptik/keyboard.sh +# What the medium's root.json may be trusted for, shared with kryptik-recover. +. /usr/libexec/kryptik/medium-root.sh [ -z "$KEYBOARD" ] || kb_row "$KEYBOARD" > /dev/null || die "no keyboard layout named ${KEYBOARD}: kryptik keyboard lists them" # --- refuse anything that is not a disposable whole disk ------------------- @@ -183,6 +168,8 @@ case "$media" in mount -t iso9660 -o ro /dev/sr0 "$MNT_BASE/media" || die "could not mount the medium (/dev/sr0)" ESP_SRC="$MNT_BASE/media/esp.img" [ -f "$ESP_SRC" ] || die "no esp.img on the medium" + # For its kernels, read before anything is written. + mount -t vfat -o ro,loop "$ESP_SRC" "$MNT_BASE/esp" || die "could not mount the medium's ESP image" ROOT_JSON="$MNT_BASE/media/root.json" ROOT_SRC=/dev/sr0 # The signed command line's linear table is "0 N linear /dev/sr0 START": @@ -193,24 +180,11 @@ case "$media" in ;; *) die "unknown medium type '${media}'" ;; esac -[ -r "$ROOT_JSON" ] || die "no root.json on the medium" -jget() { sed -n "s/^ \"$1\": \"\{0,1\}\([^\",]*\)\"\{0,1\},\{0,1\}\$/\1/p" "$ROOT_JSON" | head -1; } -ROOT_BYTES="$(jget total_bytes)"; ROOT_SHA="$(jget sha256)"; VERSION="$(jget version)" -[ -n "$ROOT_BYTES" ] && [ -n "$ROOT_SHA" ] || die "root.json is incomplete" -decimal_field total_bytes "$ROOT_BYTES" -hex_field sha256 "$ROOT_SHA" 64 -case "$VERSION" in *[!A-Za-z0-9._-]*) die "root.json: version has characters a version cannot" ;; esac -# The record must name the root the signed kernel carries, and its size must -# hold that root and its hash tree without being absurd. -VERITY="$(verity_of /proc/cmdline)" -[ -n "$VERITY" ] || die "could not read the root's verity table from the signed command line" -read -r V_BLOCKS V_HASH_START V_HASH V_SALT </dev/null || blockdev --getsize64 "$ESP_SRC")" [ -b "$ESP_SRC" ] && ESP_BYTES="$(blockdev --getsize64 "$ESP_SRC")" @@ -317,10 +291,7 @@ say "reading kryptik-a back" got="$(dd if="$P2" bs=4M iflag=count_bytes count="$ROOT_BYTES" status=none | sha256sum | cut -c1-64)" [ "$got" = "$ROOT_SHA" ] || die "kryptik-a does not verify: wrote ${got}, the medium says ${ROOT_SHA}" say "kryptik-a verifies (${got})" -# The record can lie about itself; the kernel's root hash cannot. -veritysetup verify --no-superblock --hash=sha256 --data-block-size=4096 --hash-block-size=4096 \ - --data-blocks="$V_BLOCKS" --hash-offset=$(( V_HASH_START * 4096 )) --salt="$V_SALT" "$P2" "$P2" "$V_HASH" \ - || die "kryptik-a does not verify against the root hash the signed kernel carries" +root_verifies "$P2" || die "kryptik-a does not verify against the root hash the signed kernel carries" say "kryptik-a verifies against the signed kernel's root hash" say "clearing kryptik-b" dd if=/dev/zero of="$P3" bs=1M count=4 conv=fsync status=none || die "clearing kryptik-b failed" diff --git a/tools/tests/installer.sh b/tools/tests/installer.sh index a87a6f31..033916c4 100755 --- a/tools/tests/installer.sh +++ b/tools/tests/installer.sh @@ -107,11 +107,18 @@ fi echo echo "-- root.json is checked for form, and against the signed command line" -# The helpers, taken from the installer; die exits the subshell each runs in. -eval "$(sed -n '/^decimal_field()/,/^}/p; /^hex_field()/,/^}/p; /^verity_of()/,/^}/p' "$INSTALLER")" +# The checks kryptik-install and kryptik-recover share; die exits the subshell +# each runs in. +MEDIUM_ROOT="${ROOT}/build/service-scripts/medium-root.sh" +# shellcheck source=/dev/null +. "$MEDIUM_ROOT" die() { printf 'die: %s\n' "$*"; exit 1; } -if ! declare -F decimal_field >/dev/null || ! declare -F verity_of >/dev/null; then - red "could not extract the record checks from the installer" +grep -q '^\. /usr/libexec/kryptik/medium-root\.sh' "$INSTALLER" \ + && grep -q '^ *\. /usr/libexec/kryptik/medium-root\.sh' "${ROOT}/tools/update/kryptik-recover" \ + && green "the installer and kryptik-recover take the medium's root through the same checks" \ + || red "the installer or kryptik-recover does not source medium-root.sh" +if ! declare -F decimal_field >/dev/null || ! declare -F verity_of >/dev/null || ! declare -F medium_root >/dev/null; then + red "could not read the record checks from ${MEDIUM_ROOT}" else out="$( (decimal_field total_bytes '1$(reboot)') 2>&1 )"; rc=$? [[ "$rc" -ne 0 && "$out" == *"total_bytes is not a number"* ]] \ @@ -139,7 +146,49 @@ else [[ -z "$(verity_of "$tmp")" ]] \ && green "a table whose root hash is not 64 hex digits reads as none" \ || red "a malformed table was accepted" - rm -f "$tmp" + # A medium's command line naming root hash $h, and its record. + printf 'ro dm-mod.create="kroot,,0,ro,0 2097152 verity 1 PARTLABEL=kryptik-media PARTLABEL=kryptik-media 4096 4096 262144 262144 sha256 %s 0123abcd 1 panic_on_corruption" kryptik.media=usb\n' "$h" > "$tmp" + rec="$(mktemp)" + record() { # record ROOT_HASH DATA_BLOCKS: root.json as stage 06 writes it + printf '{\n "version": "1.0.0",\n "root_hash": "%s",\n "salt": "0123abcd",\n "data_bytes": 1073741824,\n "data_blocks": %s,\n "data_sectors": 2097152,\n "hash_start_block": 262144,\n "total_bytes": 1094713344,\n "sha256": "%s"\n}\n' "$1" "$2" "$h" > "$rec" + } + record "$h" 262144 + out="$( (medium_root "$rec" "$tmp" && echo "took $ROOT_BYTES $VERSION $V_BLOCKS $V_HASH_START $V_SALT") 2>&1 )"; rc=$? + [[ "$rc" -eq 0 && "$out" == "took 1094713344 1.0.0 262144 262144 0123abcd" ]] \ + && green "a record that names the signed root is taken, with the table's numbers" \ + || red "a matching record: rc=${rc} ${out}" + b="$(printf 'b%.0s' $(seq 64))" + record "$b" 262144 + out="$( (medium_root "$rec" "$tmp") 2>&1 )"; rc=$? + [[ "$rc" -ne 0 && "$out" == *"root.json names root hash ${b}; the signed kernel carries ${h}"* ]] \ + && green "a record that names another root is refused, both hashes named" \ + || red "another root hash: rc=${rc} ${out}" + record "$(printf '\033]0;owned\007')${h}" 262144 + out="$( (medium_root "$rec" "$tmp") 2>&1 )"; rc=$? + [[ "$rc" -ne 0 && "$out" == *"root_hash is not a hash"* && "$out" != *$'\033'* ]] \ + && green "a root hash carrying a terminal sequence is refused without printing it" \ + || red "a terminal sequence in root_hash: rc=${rc}" + record "$h" 262143 + out="$( (medium_root "$rec" "$tmp") 2>&1 )"; rc=$? + [[ "$rc" -ne 0 && "$out" == *"262143 data blocks; the signed kernel carries 262144"* ]] \ + && green "a record that names another size of root is refused" \ + || red "another data_blocks: rc=${rc} ${out}" + record "$h" 262144 + printf 'ro root=/dev/sda2 kryptik.media=usb\n' > "$tmp" + out="$( (medium_root "$rec" "$tmp") 2>&1 )"; rc=$? + [[ "$rc" -ne 0 && "$out" == *"could not read the root's verity table"* ]] \ + && green "a command line with no verity table is refused" \ + || red "no verity table: rc=${rc} ${out}" + # A slot's kernel: its command line, inside the binary, names the root. + V_HASH="$h" + printf 'MZ\0\0\377pe\0dm-mod.create="kroot,,0,ro,0 2097152 verity 1 PARTLABEL=kryptik-a PARTLABEL=kryptik-a 4096 4096 262144 262144 sha256 %s 0123abcd 1 panic_on_corruption"\0\001' "$h" > "$tmp" + kernel_names_root "$tmp" && green "a kernel whose command line carries the root hash is taken" \ + || red "a kernel that carries the root hash was refused" + printf 'MZ\0\0\377pe\0dm-mod.create="kroot,,0,ro,0 2097152 verity 1 PARTLABEL=kryptik-a PARTLABEL=kryptik-a 4096 4096 262144 262144 sha256 %s 0123abcd 1 panic_on_corruption"\0\001' "$b" > "$tmp" + kernel_names_root "$tmp" && red "a kernel for another root was taken" \ + || green "a kernel whose command line names another root is refused" + kernel_names_root "$tmp.absent" && red "a missing kernel was taken" || green "a missing kernel is refused" + rm -f "$tmp" "$rec" fi echo diff --git a/tools/update/kryptik-recover b/tools/update/kryptik-recover index 73bccf5c..4ca21133 100755 --- a/tools/update/kryptik-recover +++ b/tools/update/kryptik-recover @@ -100,6 +100,7 @@ commit_slot() { # commit_slot SLOT (ESP mounted rw at /run/kryptik-recover) if [ -n "$RESTORE" ]; then sd="$(slot_dev "$RESTORE")" + command -v veritysetup >/dev/null 2>&1 || die "missing tool: veritysetup; nothing was written" # The medium's own root image and kernel, found as kryptik-install finds them. mkdir -p /run/kryptik-recover-media case "$media" in @@ -117,7 +118,13 @@ if [ -n "$RESTORE" ]; then mkdir -p /run/kryptik-recover-esp && mount -t vfat -o ro,loop /run/kryptik-recover-media/esp.img /run/kryptik-recover-esp || die "cannot mount the medium's ESP image" kdir=/run/kryptik-recover-esp/EFI/kryptik ;; esac - bytes="$(sed -n 's/^ "total_bytes": \([0-9]*\).*/\1/p' "$rj")"; sha="$(sed -n 's/^ "sha256": "\([0-9a-f]*\)".*/\1/p' "$rj")"; ver="$(sed -n 's/^ "version": "\([^"]*\)".*/\1/p' "$rj")" + # root.json is unsigned: it must name the root the medium's signed kernel + # carries, as must the kernel the slot will boot. + . /usr/libexec/kryptik/medium-root.sh + medium_root "$rj" /proc/cmdline + bytes="$ROOT_BYTES"; sha="$ROOT_SHA"; ver="$VERSION" + kernel_names_root "$kdir/kryptik-$RESTORE.efi" \ + || die "the medium's kernel for slot $RESTORE does not carry the root hash its signed command line names; nothing was written" [ "$(blockdev --getsize64 "$sd")" -ge "$bytes" ] || die "slot $RESTORE is smaller than the medium's root image" # As kryptik-update does, the ESP stops naming the slot before it is # rewritten, and is unmounted meanwhile: a restore cut short leaves nothing @@ -131,7 +138,11 @@ if [ -n "$RESTORE" ]; then blockdev --flushbufs "$sd" # so the read-back is of the disk, not of the page cache got="$(dd if="$sd" bs=4M iflag=count_bytes count="$bytes" status=none | sha256sum | cut -c1-64)" [ "$got" = "$sha" ] || die "slot $RESTORE reads back as $got, expected $sha" - say "slot $RESTORE verifies" + # What reads back as root.json says is only what root.json says; the slot + # stays unnamed on the ESP unless it is the root the signed kernel names. + root_verifies "$sd" >/dev/null \ + || die "slot $RESTORE does not verify against the root hash this medium's signed kernel carries; nothing was committed" + say "slot $RESTORE verifies against the root hash the signed kernel carries" mount -t vfat -o rw "$ESP" /run/kryptik-recover || die "cannot mount the target ESP" cp "$kdir/kryptik-$RESTORE.efi" "/run/kryptik-recover/EFI/kryptik/kryptik-$RESTORE.efi.new" sync -f "/run/kryptik-recover/EFI/kryptik/kryptik-$RESTORE.efi.new" From fc5bf32630b77d2cf3b6c65177d4f782eee0d3a5 Mon Sep 17 00:00:00 2001 From: DevomB Date: Tue, 6 Oct 2026 21:37:40 -0700 Subject: [PATCH 2/3] The services fixture takes a helper the installer or the update tools source: medium-root.sh is sourced by kryptik-install and kryptik-recover, which installer-run runs, and the rule knew only helpers that service scripts source --- tools/tests/services.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/tests/services.sh b/tools/tests/services.sh index d9e9a0bf..391fb328 100755 --- a/tools/tests/services.sh +++ b/tools/tests/services.sh @@ -191,7 +191,8 @@ for s in "${SCRIPTS}"/*.sh; do case "$(head -1 "$s")" in "#!/bin/bash"*|"#!/usr/bin/bash"*) interp="bash" ;; esac check "${n}: valid ${interp}" "$("$interp" -n "$s" 2>/dev/null && echo ok)" check "${n}: executable" "$([[ -x "$s" ]] && echo ok)" - # Each script must be run by a service, or sourced by a script that is. + # Each script must be run by a service, or sourced by a script that is, + # or by the installer or the update tools. if grep -rqF "/usr/libexec/kryptik/${n}" "$SRC"/*/up "$SRC"/*/run 2>/dev/null; then green "${n}: referenced by a service" elif grep -lqE "^\s*\. +/usr/libexec/kryptik/${n}" "${SCRIPTS}"/*.sh 2>/dev/null \ @@ -199,6 +200,8 @@ for s in "${SCRIPTS}"/*.sh; do | xargs -r -n1 basename | while read -r u; do grep -rqF "/usr/libexec/kryptik/${u}" "$SRC"/*/up "$SRC"/*/run 2>/dev/null && exit 0; done; then green "${n}: sourced by a script a service runs" + elif grep -lqE "^\s*\. +/usr/libexec/kryptik/${n}" "$ROOT"/tools/install/*.sh "$ROOT"/tools/update/kryptik-* 2>/dev/null; then + green "${n}: sourced by the installer or the update tools" else red "${n}: installed by the stage but no service runs it" fi From 1b832977bfabd3d7f77c9e23982c3c5a943fcd71 Mon Sep 17 00:00:00 2001 From: DevomB Date: Wed, 7 Oct 2026 01:20:02 -0700 Subject: [PATCH 3/3] The installer fixture tells shellcheck that V_HASH is read by the sourced kernel_names_root: CI's shellcheck took it for unused, which the fixture step's earlier failure had hidden --- tools/tests/installer.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/tests/installer.sh b/tools/tests/installer.sh index 033916c4..2aeb1643 100755 --- a/tools/tests/installer.sh +++ b/tools/tests/installer.sh @@ -180,6 +180,7 @@ else && green "a command line with no verity table is refused" \ || red "no verity table: rc=${rc} ${out}" # A slot's kernel: its command line, inside the binary, names the root. + # shellcheck disable=SC2034 # read by the sourced kernel_names_root V_HASH="$h" printf 'MZ\0\0\377pe\0dm-mod.create="kroot,,0,ro,0 2097152 verity 1 PARTLABEL=kryptik-a PARTLABEL=kryptik-a 4096 4096 262144 262144 sha256 %s 0123abcd 1 panic_on_corruption"\0\001' "$h" > "$tmp" kernel_names_root "$tmp" && green "a kernel whose command line carries the root hash is taken" \