From 6d02625f7f0f9ca4b187df9853299d78a0bae939 Mon Sep 17 00:00:00 2001 From: Omarchybot Date: Sun, 23 Aug 2026 12:59:32 +0200 Subject: [PATCH] Publish a checksum beside every released ISO (#119) The release script computed the ISO's sha256 and echoed it to the releaser's terminal, where nobody downloading the ISO could ever see it, and the uploader shipped the ISO and its signature but nothing to check the download against. So "verify your download" was not advice anyone could act on. That matters more for this ISO than for most. The offline pacman mirror lives inside the airootfs and is stored uncompressed, and the installer bind-mounts it onto the target's package cache, so pacstrap reads packages straight off the medium. A flipped block there meets no decompressor and no squashfs block checksum on the way; pacman's integrity check is the first thing in the chain to notice, minutes into the install, as "invalid or corrupted package" against a package name that says nothing about the download. The sidecar names the ISO alone rather than the build path, so it verifies from whatever directory it was downloaded into. A release that cannot write it stops, because the sidecar left over from the previous release reads as a checksum for this one. The uploader now tallies every copy instead of letting the last one decide the exit status, and warns when there is no checksum to publish. Co-authored-by: Codex XHigh --- README.md | 10 +- bin/omarchy-iso-release | 10 ++ bin/omarchy-iso-upload | 22 +++- test/unit/iso-release-checksum-test.sh | 175 +++++++++++++++++++++++++ 4 files changed, 213 insertions(+), 4 deletions(-) create mode 100755 test/unit/iso-release-checksum-test.sh diff --git a/README.md b/README.md index 70cc2c6..1ec23ea 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ The Omarchy ISO is the only supported way to install Omarchy. It ships the Omarc See the ISO link on [omarchy.org](https://omarchy.org). +Every published ISO has a `.sha256` beside it at the same URL. Download both into the same directory and check the ISO before writing it to a USB stick: + +```bash +sha256sum -c omarchy-3.0.iso.sha256 +``` + +Corruption anywhere in the ISO is worth catching before the write, and corruption in the bundled package mirror is worth catching most: the mirror lives inside the ISO and the installer reads it straight off the medium, so those bytes surface minutes into the install as a pacman "invalid or corrupted package" error rather than as anything that names the download. Corruption elsewhere is louder and earlier — it stops the medium booting or mounting. There is a `.sig` beside the ISO too for anyone who wants to verify it against the Omarchy signing key. + ## Creating the ISO Run `./bin/omarchy-iso-make`; output goes into `./release`. By default the ISO uses the Omarchy packages and tracks the `quattro` branch, from the stable mirror. Pass `--edge` to use `omarchy-dev` and `omarchy-settings-dev` from the edge mirror. @@ -130,7 +138,7 @@ Run `./bin/omarchy-iso-sign [release/omarchy.iso]`. The signing key is retrieved ## Uploading the ISO -Run `./bin/omarchy-iso-upload [release/omarchy.iso]`. This requires rclone configuration (`rclone config`). +Run `./bin/omarchy-iso-upload [release/omarchy.iso]`. This requires rclone configuration (`rclone config`). The `.sig` and `.sha256` sidecars go up with the ISO when they exist beside it. ## Full release of the ISO diff --git a/bin/omarchy-iso-release b/bin/omarchy-iso-release index cf5eb99..b0423fa 100755 --- a/bin/omarchy-iso-release +++ b/bin/omarchy-iso-release @@ -56,7 +56,17 @@ cp "$latest_iso" "$release_iso" echo -e "\e[32mSigning $ISO_REF ISO\e[0m" omarchy-iso-sign "$release_iso" +# The checksum sidecar names the ISO alone, never the build path: it is +# downloaded next to the ISO and has to verify with `sha256sum -c` from +# whatever directory the person who downloaded it happens to be in. A release +# that cannot write it stops here, because the sidecar left over from the last +# release reads as a checksum for this one. iso_sha=$(sha256sum "$release_iso") +iso_sha=${iso_sha%% *} +if [[ -z $iso_sha ]] || ! printf '%s %s\n' "$iso_sha" "${release_iso##*/}" >"$release_iso.sha256"; then + echo -e "\e[31mCould not write the checksum for $release_iso\e[0m" >&2 + exit 1 +fi echo -e "\e[32mSHA256 for $ISO_REF ISO: $iso_sha\e[0m" omarchy-iso-upload "$release_iso" diff --git a/bin/omarchy-iso-upload b/bin/omarchy-iso-upload index a154858..3c55ec0 100755 --- a/bin/omarchy-iso-upload +++ b/bin/omarchy-iso-upload @@ -10,8 +10,24 @@ if [[ ! -f ~/.config/rclone/rclone.conf ]]; then exit 1 fi -rclone copy $1 Omarchy:omarchy/ -P +# Every copy reports, and one failure fails the upload. Without the tally the +# last copy decides the exit status, so a signature that never made it is +# hidden by a checksum that did. +status=0 -if [[ -f $1.sig ]]; then - rclone copy $1.sig Omarchy:omarchy/ -P +rclone copy "$1" Omarchy:omarchy/ -P || status=1 + +# The signature and the checksum are only worth anything to someone who can +# reach them beside the ISO, so they go up in the same run rather than a step +# a release can finish without. +for sidecar in "$1.sig" "$1.sha256"; do + if [[ -f $sidecar ]]; then + rclone copy "$sidecar" Omarchy:omarchy/ -P || status=1 + fi +done + +if [[ ! -f $1.sha256 ]]; then + echo "Warning: no $1.sha256 to publish, so nobody can verify this download" >&2 fi + +exit $status diff --git a/test/unit/iso-release-checksum-test.sh b/test/unit/iso-release-checksum-test.sh new file mode 100755 index 0000000..56e50f1 --- /dev/null +++ b/test/unit/iso-release-checksum-test.sh @@ -0,0 +1,175 @@ +#!/bin/bash +# +# Unit tests for the checksum sidecar omarchy-iso-release writes and +# omarchy-iso-upload ships. Both scripts derive everything from their own +# location and their argument, so each case runs against a throwaway sandbox +# with the real script copied in and rclone/sign/upload stubbed out. + +set -euo pipefail + +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) + +pass() { + printf 'ok - %s\n' "$1" +} + +fail() { + local description="$1" + local detail="${2:-}" + + [[ -n $detail ]] && printf '%s\n' "$detail" >&2 + printf 'not ok - %s\n' "$description" >&2 + exit 1 +} + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +# omarchy-iso-release resolves its release directory from the script's own +# path, so relocating the script is what points it at the sandbox. +sandbox="$work/repo" +mkdir -p "$sandbox/bin" "$sandbox/release" "$work/stubs" +cp "$ROOT/bin/omarchy-iso-release" "$sandbox/bin/" +printf 'not really an iso\n' >"$sandbox/release/omarchy-2099.01.01-x86_64-quattro.iso" + +cat >"$work/stubs/omarchy-iso-sign" <<'STUB' +#!/bin/bash +printf 'signature\n' >"$1.sig" +STUB + +cat >"$work/stubs/omarchy-iso-upload" <<'STUB' +#!/bin/bash +printf 'upload %s\n' "$*" >>"$TEST_LOG" +STUB + +# Log one argument per field rather than "$*": the point of several cases below +# is that a path survives as a single argument, which "$*" cannot show. +cat >"$work/stubs/rclone" <<'STUB' +#!/bin/bash +printf 'rclone' +printf '|%s' "$@" +printf '\n' +[[ -n ${RCLONE_FAIL_ON:-} && $* == *"$RCLONE_FAIL_ON"* ]] && exit 1 +exit 0 +STUB + +chmod +x "$work/stubs"/* + +export TEST_LOG="$work/log" +: >"$TEST_LOG" + +run_upload() { + PATH="$work/stubs:$PATH" "$ROOT/bin/omarchy-iso-upload" "$@" >"$work/rclone-log" 2>"$work/upload-err" +} + +PATH="$work/stubs:$PATH" "$sandbox/bin/omarchy-iso-release" --no-make 9.9.9 >/dev/null + +release_iso="$sandbox/release/omarchy-9.9.9.iso" +checksum_file="$release_iso.sha256" + +[[ -f $checksum_file ]] || + fail "release writes a checksum beside the ISO" "missing: $checksum_file" +pass "release writes a checksum beside the ISO" + +# The name in the file has to be the ISO alone. A build path in there verifies +# only on the release machine, which is the one machine that never needs it. +expected="$(sha256sum "$release_iso" | cut -d " " -f 1) omarchy-9.9.9.iso" +actual="$(cat "$checksum_file")" +[[ $actual == "$expected" ]] || + fail "the checksum names the ISO alone" "expected: $expected"$'\n'"actual: $actual" +pass "the checksum names the ISO alone" + +(cd "$sandbox/release" && sha256sum -c --status omarchy-9.9.9.iso.sha256) || + fail "sha256sum -c verifies the ISO from its own directory" +pass "sha256sum -c verifies the ISO from its own directory" + +# The whole point of the sidecar is catching bytes that changed after release, +# so prove it fails on bytes that changed after release. +printf 'corrupted\n' >>"$release_iso" +if (cd "$sandbox/release" && sha256sum -c --status omarchy-9.9.9.iso.sha256 2>/dev/null); then + fail "sha256sum -c rejects a corrupted ISO" +fi +pass "sha256sum -c rejects a corrupted ISO" + +# An unreadable ISO must stop the release rather than publish an empty digest +# beside a stale sidecar from the previous one. +stale="$sandbox/release/omarchy-8.8.8.iso.sha256" +printf 'deadbeef omarchy-8.8.8.iso\n' >"$stale" +rm "$sandbox/release/omarchy-2099.01.01-x86_64-quattro.iso" +printf 'not really an iso\n' >"$sandbox/release/omarchy-2099.01.02-x86_64-quattro.iso" +cp "$sandbox/release/omarchy-2099.01.02-x86_64-quattro.iso" "$sandbox/release/omarchy-8.8.8.iso" +chmod 000 "$sandbox/release/omarchy-8.8.8.iso" + +set +e +PATH="$work/stubs:$PATH" "$sandbox/bin/omarchy-iso-release" --no-make 8.8.8 >/dev/null 2>&1 +release_status=$? +set -e +chmod 644 "$sandbox/release/omarchy-8.8.8.iso" + +(( release_status != 0 )) || + fail "release stops when the ISO cannot be checksummed" "exit status was 0" +[[ "$(cat "$stale")" == "deadbeef omarchy-8.8.8.iso" ]] || + fail "a failed checksum leaves no half-written sidecar" "$(cat "$stale")" +pass "release stops when the ISO cannot be checksummed" + +# Uploading: both sidecars go up with the ISO, and a missing one is skipped +# rather than handed to rclone as a path that does not exist. +export HOME="$work/home" +mkdir -p "$HOME/.config/rclone" +: >"$HOME/.config/rclone/rclone.conf" + +# A space in the path is the case that tells a quoted argument from an unquoted +# one, so every upload case runs from a directory that has one. +mkdir -p "$work/release builds" +upload_iso="$work/release builds/omarchy-9.9.9.iso" +printf 'not really an iso\n' >"$upload_iso" +printf 'signature\n' >"$upload_iso.sig" +printf 'checksum\n' >"$upload_iso.sha256" + +run_upload "$upload_iso" + +for suffix in "" ".sig" ".sha256"; do + grep -qxF "rclone|copy|$upload_iso$suffix|Omarchy:omarchy/|-P" "$work/rclone-log" || + fail "upload ships the ISO, its signature and its checksum, path spaces intact" \ + "$(cat "$work/rclone-log")" +done +pass "upload ships the ISO, its signature and its checksum, path spaces intact" + +rm "$upload_iso.sig" +run_upload "$upload_iso" + +if grep -qF ".sig" "$work/rclone-log"; then + fail "upload skips a sidecar that is not there" "$(cat "$work/rclone-log")" +fi +grep -qF "$upload_iso.sha256" "$work/rclone-log" || + fail "upload still ships the checksum without a signature" "$(cat "$work/rclone-log")" +pass "upload skips a sidecar that is not there" + +# A failed copy has to fail the upload even when a later copy succeeds, or a +# release reports success having published an ISO nobody can download. +printf 'signature\n' >"$upload_iso.sig" +set +e +RCLONE_FAIL_ON=".sig" run_upload "$upload_iso" +upload_status=$? +set -e +(( upload_status != 0 )) || + fail "a failed sidecar copy fails the upload" "$(cat "$work/rclone-log")" +grep -qF "$upload_iso.sha256" "$work/rclone-log" || + fail "a failed sidecar copy still attempts the rest" "$(cat "$work/rclone-log")" +pass "a failed sidecar copy fails the upload without skipping the rest" + +set +e +RCLONE_FAIL_ON="omarchy-9.9.9.iso" run_upload "$upload_iso" +upload_status=$? +set -e +(( upload_status != 0 )) || + fail "a failed ISO copy fails the upload" "$(cat "$work/rclone-log")" +pass "a failed ISO copy fails the upload" + +# Publishing an ISO nobody can verify is the thing this change exists to stop, +# so the uploader says so out loud rather than exiting quietly. +rm "$upload_iso.sha256" +run_upload "$upload_iso" +grep -qF "nobody can verify" "$work/upload-err" || + fail "upload warns when there is no checksum to publish" "$(cat "$work/upload-err")" +pass "upload warns when there is no checksum to publish"