From 6cd4e732a1b7b75bec53bb3f46449862c0174e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20M=C3=A9ndez?= Date: Sun, 27 Sep 2026 19:35:09 +0000 Subject: [PATCH 1/2] fix: the build verifies the staging archive instead of reading it unverified The staging distribution was given its own signing key so that a nightly could sign itself and so recipes could stop reading it through [trusted=yes], which switches verification off. Signing landed; verification did not. A layer build printed W: OpenPGP signature verification failed: file:/srv/keel-apt/repo trixie-staging InRelease: Missing key 8CFD1A4841448B2227341CEB202CACBD0E97090A and carried on, so it installed the project own packages unverified, which is where it was before, only now it said so (tracker#7). The public half of the staging key is installed into the build tree as /etc/apt/keyrings/keel-staging-keyring.asc, from /srv/keel-apt/keys where bin/publish of keel-linux/apt leaves it; the source entry names it through signed-by; and apt-get update runs with --error-on=any. Measured against the real archive: with signed-by named and no key, apt exits 100 and says the repository is not signed, where before it warned and exited 0. A warning nobody fails on is how this shipped. bin/keel-archive-check makes the same checks itself rather than trusting apt to have complained, because the tree that is about to be configured is checked at a step where no apt-get update runs: the copied index is the live one, the entry names the keyring and this distribution, no apt source in the tree says trusted=yes, and the signature on the copied InRelease verifies against the named key with gpgv. The keyring leaves the image with the source entry and the copy of the archive, and common/removelists-final/turnkey takes all three out as well. --- COVERAGE.md | 18 ++-- Makefile | 56 +++++++--- bin/keel-archive-check | 108 ++++++++++++++++--- changelog | 33 ++++++ conf.d/zz-project-packages | 11 +- conf.d/zzz-keel-archive | 8 +- tests/archive-check.bats | 204 ++++++++++++++++++++++++++++++++++-- tests/boot-test.bats | 36 +++++++ tests/boot-test.sh | 8 +- tests/keel-archive.bats | 15 ++- tests/lib/boot-test-lib.sh | 26 +++++ tests/project-packages.bats | 17 ++- 12 files changed, 485 insertions(+), 55 deletions(-) diff --git a/COVERAGE.md b/COVERAGE.md index a725d8c..4f968aa 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -10,26 +10,28 @@ acceptance test of a recipe, docs/org-plan.md section 1). | --- | --- | --- | --- | | `overlay/usr/lib/inithooks/lib/wordpress.sh` | `tests/wordpress.bats` (40 tests) | 99.00 percent (99/100) under kcov | every function and every branch | | `overlay/usr/lib/inithooks/firstboot.d/40wordpress` | `tests/hook.bats` (29 tests) | 97.73 percent (43/44) under kcov | the hook itself, run for real | -| `tests/lib/boot-test-lib.sh` | `tests/boot-test.bats` (66 tests) | 98.88 percent (264/267) under kcov | parsing, addresses, deadlines, the container marks, every verdict | -| `conf.d/zzz-keel-archive` | `tests/keel-archive.bats` (12 tests) | 100 percent (24/24) under kcov | every way it enables and every way it refuses | -| `conf.d/zz-project-packages` | `tests/project-packages.bats` (13 tests) | 100 percent (29/29) under kcov | shared with keel-nodebb, where the pattern is maintained | -| `bin/keel-archive-check` | `tests/archive-check.bats` (8 tests) | 100 percent (26/26) under kcov | same | +| `tests/lib/boot-test-lib.sh` | `tests/boot-test.bats` (73 tests) | 98.95 percent (282/285) under kcov | parsing, addresses, deadlines, the container marks, every verdict, and the image carrying none of the build time archive files | +| `conf.d/zzz-keel-archive` | `tests/keel-archive.bats` (13 tests) | 100 percent (26/26) under kcov | every way it enables and every way it refuses, including a staging keyring left in the image | +| `conf.d/zz-project-packages` | `tests/project-packages.bats` (14 tests) | 100 percent (31/31) under kcov | shared with keel-nodebb, where the pattern is maintained | +| `bin/keel-archive-check` | `tests/archive-check.bats` (25 tests) | 100 percent (52/52) under kcov | the build time check of tracker#7: the copy is the live archive, the entry names the keyring through signed-by, nothing says trusted=yes, and the copied InRelease verifies against the staging key | | `overlay/usr/lib/inithooks/bin/wordpress.py` | none | 0 | dialog wrapper, only reached with a terminal attached | | `overlay/usr/lib/inithooks/lib/*.php` | the boot test | integration only | two PHP files `wp eval-file` runs; `conf.d/main` has PHP lint them | | `conf.d/main` | the build | integration only | build time script, 0004 pragmatic limits | | `tests/boot-test.sh` | itself | integration only | the thin main of the acceptance test: keel and LXC as root | -Total over the six measured shell files: **98.98 percent (485/490)**, 168 bats +Total over the six measured shell files: **99.07 percent (533/538)**, 194 bats tests, none failing. `tests/coverage.sh` fails below `COVERAGE_THRESHOLD`, which the workflow sets to **97**, the lowest measured file. It is only ever raised (decision 0006). $ COVERAGE_THRESHOLD=97 tests/coverage.sh kcov line coverage (threshold 97 percent): - 99.00 99/100 wordpress.sh - 100.00 24/24 zzz-keel-archive + 100.00 31/31 zz-project-packages + 100.00 26/26 zzz-keel-archive 97.73 43/44 40wordpress - 98.88 264/267 boot-test-lib.sh + 98.95 282/285 boot-test-lib.sh + 99.00 99/100 wordpress.sh + 100.00 52/52 keel-archive-check 100.00 29/29 zz-project-packages 100.00 26/26 keel-archive-check diff --git a/Makefile b/Makefile index 7d63c44..c2c73c4 100644 --- a/Makefile +++ b/Makefile @@ -37,28 +37,54 @@ COMMON_OVERLAYS += $(CURDIR)/overlay include $(FAB_PATH)/common/mk/turnkey.mk -# The project's own packages (inithooks, confconsole, keel and, new here, -# keel-archive-keyring) come from the build host's APT repository during the -# build only. The repository is copied into the bootstrap and listed as a -# [trusted=yes] file source, because the staging distribution is unsigned; -# conf.d/zz-project-packages checks what was installed against that copy and -# removes both from the image, and conf.d/zzz-keel-archive then enables the -# signed repository the appliance uses at run time. Same block as -# keel-nodebb, which is where the pattern is maintained. +# The project's own packages (inithooks, confconsole, keel and keel-archive- +# keyring) come from the build host's APT repository during the build only. +# The repository is copied into the bootstrap and the build verifies it there, +# the way an appliance verifies the release archive (tracker#7): the public +# half of the staging key is installed as a keyring, the source entry names it +# through signed-by, nothing in the tree says trusted=yes, and apt runs with +# --error-on=any, so a signature that cannot be checked fails the build +# instead of warning about it and carrying on. conf.d/zz-project-packages +# checks what was installed against that copy and removes the copy, the source +# entry and the keyring from the image, and conf.d/zzz-keel-archive then +# enables the signed repository the appliance uses at run time. +# +# None of the three build time files is for an installed appliance, because +# the staging key signs whatever the build host produced. The removelist at +# common/removelists-final/turnkey takes all three out of the image as well, +# whatever a recipe does. Same block as keel-nodebb, which is where the pattern is maintained. KEEL_APT_REPO ?= /srv/keel-apt/repo KEEL_APT_DIST ?= trixie-staging -KEEL_ARCHIVE_CHECK = $(CURDIR)/bin/keel-archive-check $(KEEL_APT_REPO) +# Beside the repository rather than inside it: bin/publish of keel-linux/apt +# installs the public half of whichever key it signed a distribution with +# here, so the key a build verifies with cannot drift from the key the archive +# was signed with. +KEEL_APT_KEYRING ?= /srv/keel-apt/keys/keel-staging-keyring.asc +# Where that key goes in the build tree, and which key has to be in it: the +# staging signing subkey (handbook decision 0011). A keyring is only a promise +# until the key inside it is named, so bin/keel-archive-check fails the build +# when the keyring it finds holds some other key. +KEEL_APT_KEYRING_PATH ?= /etc/apt/keyrings/keel-staging-keyring.asc +KEEL_APT_KEY ?= 8CFD1A4841448B2227341CEB202CACBD0E97090A +KEEL_STAGING_LIST ?= /etc/apt/sources.list.d/keel-staging.list +KEEL_ARCHIVE_CHECK = KEEL_ARCHIVE_KEY=$(KEEL_APT_KEY) \ + KEEL_ARCHIVE_KEYRING=$(KEEL_APT_KEYRING_PATH) \ + KEEL_ARCHIVE_LIST=$(KEEL_STAGING_LIST) \ + $(CURDIR)/bin/keel-archive-check $(KEEL_APT_REPO) # The copy is made fresh and then proved: bin/keel-archive-check compares the -# copied package index with the live one and stops the build when they differ. +# copied package index with the live one, verifies the signature on the copied +# InRelease against the keyring, refuses any trusted=yes, and stops the build +# when one of them is wrong. define _keel_bootstrap/post - mkdir -p $O/bootstrap/srv/keel-apt/repo; + mkdir -p $O/bootstrap/srv/keel-apt/repo $O/bootstrap$(dir $(KEEL_APT_KEYRING_PATH)); rm -rf $O/bootstrap/srv/keel-apt/repo/dists $O/bootstrap/srv/keel-apt/repo/pool; cp -a $(KEEL_APT_REPO)/dists $(KEEL_APT_REPO)/pool $O/bootstrap/srv/keel-apt/repo/; + install -m 644 $(KEEL_APT_KEYRING) $O/bootstrap$(KEEL_APT_KEYRING_PATH); + echo "deb [signed-by=$(KEEL_APT_KEYRING_PATH)] file:///srv/keel-apt/repo $(KEEL_APT_DIST) main" > $O/bootstrap$(KEEL_STAGING_LIST); $(KEEL_ARCHIVE_CHECK) $O/bootstrap $(KEEL_APT_DIST) $(FAB_ARCH) bootstrap; - echo "deb [trusted=yes] file:///srv/keel-apt/repo $(KEEL_APT_DIST) main" > $O/bootstrap/etc/apt/sources.list.d/keel-staging.list; - fab-chroot $O/bootstrap "apt-get update"; + fab-chroot $O/bootstrap "apt-get update --error-on=any"; endef bootstrap/post += $(_keel_bootstrap/post) @@ -67,7 +93,9 @@ bootstrap/post += $(_keel_bootstrap/post) # all. On 2026-09-26 "make clean" failed on a busy deck, the stamps survived, # and the rebuild installed the packages the archive had held that morning # without a word. So the tree that is about to be configured is checked on -# every build, whether or not this build made the bootstrap. +# every build, whether or not this build made the bootstrap. That check +# verifies the signature with gpgv too, which is what proves this tree at a +# step where no apt-get update runs. define _keel_root.patched/pre $(KEEL_ARCHIVE_CHECK) $O/root.patched $(KEEL_APT_DIST) $(FAB_ARCH) root.patched; diff --git a/bin/keel-archive-check b/bin/keel-archive-check index ce86d34..4de10e6 100755 --- a/bin/keel-archive-check +++ b/bin/keel-archive-check @@ -1,13 +1,30 @@ #!/bin/bash -# The build reads the project's APT archive from a copy inside the build tree. -# This checks that the copy is the archive as it is right now, byte for byte, -# and stops the build when it is not. +# The build reads the project's own packages from a copy of the project's APT +# archive inside the build tree. This checks two things about that copy and +# stops the build when either one is wrong: # -# Why it exists: fab stamps the bootstrap target, so a second build of the same -# product reuses the bootstrap the first one made, copy of the archive and all. -# On 2026-09-26 "make clean" failed on a busy deck, the stamps survived, and -# the rebuild installed the packages the archive had held that morning. The -# build said nothing, because every step of it succeeded. +# 1. the copy is the archive as it is right now, byte for byte; +# 2. the build's apt can verify the copy, and is not being told not to. +# +# Why (1) is checked: fab stamps the bootstrap target, so a second build of +# the same product reuses the bootstrap the first one made, copy of the +# archive and all. On 2026-09-26 "make clean" failed on a busy deck, the +# stamps survived, and the rebuild installed the packages the archive had held +# that morning. The build said nothing, because every step of it succeeded. +# +# Why (2) is checked (tracker#7): the staging distribution was given its own +# signing key and the build was never given the public half, while the source +# entry said [trusted=yes]. So apt printed +# +# W: OpenPGP signature verification failed: file:/srv/keel-apt/repo +# trixie-staging InRelease: Missing key 8CFD...090A +# +# and installed the project's packages unverified, which is a warning nobody +# fails on. Now the keyring, the key in it, the signed-by option and the +# absence of any trusted=yes are all checked here, and the copied InRelease is +# verified with gpgv against that keyring before a package is installed from +# it. gpgv rather than apt's word for it, so the tree that is about to be +# configured is proved even at a step where no apt-get update runs. # # keel-archive-check SOURCE_REPO TREE DIST ARCH [LABEL] # @@ -17,11 +34,24 @@ # ARCH the Debian architecture, e.g. amd64 # LABEL name of the build step, for the messages (default: TREE) # -# Exit 0 when the copy matches, 1 otherwise. +# Read from the environment, so the Makefile block stays one line per step: +# +# KEEL_ARCHIVE_KEY fingerprint of the key that must sign the archive, +# with no spaces. Required: a keyring is only a +# promise until the key inside it is named. +# KEEL_ARCHIVE_KEYRING where in TREE the keyring is +# KEEL_ARCHIVE_LIST where in TREE the source entry is +# +# Exit 0 when everything holds, 1 otherwise. set -eu +KEEL_ARCHIVE_KEY="${KEEL_ARCHIVE_KEY:-}" +KEEL_ARCHIVE_KEYRING="${KEEL_ARCHIVE_KEYRING:-/etc/apt/keyrings/keel-staging-keyring.asc}" +KEEL_ARCHIVE_LIST="${KEEL_ARCHIVE_LIST:-/etc/apt/sources.list.d/keel-staging.list}" + usage() { echo "usage: $(basename "$0") SOURCE_REPO TREE DIST ARCH [LABEL]" >&2 + echo "environment: KEEL_ARCHIVE_KEY (required), KEEL_ARCHIVE_KEYRING, KEEL_ARCHIVE_LIST" >&2 exit 1 } @@ -36,15 +66,23 @@ for value in "$source_repo" "$tree" "$dist" "$arch"; do [ -n "$value" ] || usage done -index=dists/$dist/main/binary-$arch/Packages -source_index=$source_repo/$index -copy_index=$tree/srv/keel-apt/repo/$index - fatal() { echo "FATAL [archive-check $label]: $*" >&2 exit 1 } +[ -n "$KEEL_ARCHIVE_KEY" ] \ + || fatal "KEEL_ARCHIVE_KEY names no key: the archive would be verified against whatever the keyring happens to hold" + +index=dists/$dist/main/binary-$arch/Packages +source_index=$source_repo/$index +copy=$tree/srv/keel-apt/repo +copy_index=$copy/$index +keyring=$tree/$KEEL_ARCHIVE_KEYRING +list=$tree/$KEEL_ARCHIVE_LIST +inrelease=$copy/dists/$dist/InRelease + +# 1. The copy is the archive as it is right now. [ -f "$source_index" ] || fatal "the archive has no index at $source_index" [ -f "$copy_index" ] || fatal "the build tree has no archive index at $copy_index" @@ -57,4 +95,48 @@ if ! cmp -s "$source_index" "$copy_index"; then exit 1 fi +# 2. Nothing in the tree switches verification off. A single trusted=yes +# anywhere turns every failure below into a warning, which is the defect this +# check exists for, so it is refused wherever it is written, in either of the +# two formats apt reads a source in. +verification_off='trusted *= *yes|^ *Trusted: *yes' +apt_sources="$tree/etc/apt/sources.list $tree/etc/apt/sources.list.d" +# shellcheck disable=SC2086 # the two paths are one word each, on purpose +untrusted=$(grep -rlE "$verification_off" $apt_sources 2>/dev/null || true) +[ -z "$untrusted" ] \ + || fatal "verification is switched off in: $(echo "$untrusted" | tr '\n' ' ')" + +# 3. The source entry names this distribution and the keyring it is verified +# with. apt takes signed-by from the entry, so the entry is what decides +# whether anything is verified at all. +[ -f "$list" ] || fatal "the build tree has no source entry at $list" +entry=$(grep -E "^[[:space:]]*deb[[:space:]]" "$list" | head -1) +[ -n "$entry" ] || fatal "$list has no deb line" +[[ "$entry" == *"signed-by=$KEEL_ARCHIVE_KEYRING"* ]] \ + || fatal "$list does not name signed-by=$KEEL_ARCHIVE_KEYRING: $entry" +# Padded with spaces on both sides so that trixie does not pass for the entry +# of trixie-staging. +[[ " $entry " == *" $dist "* ]] \ + || fatal "$list does not name the distribution $dist: $entry" + +# 4. The keyring is there and holds the key that must have signed the archive. +# A keyring that is present but holds the wrong key reads as a configured +# build and fails only at apt, in a line that scrolls past. +[ -s "$keyring" ] || fatal "$keyring is missing or empty: the build cannot verify the archive" +gpg --batch --show-keys --with-colons "$keyring" 2>/dev/null \ + | awk -F: -v want="$KEEL_ARCHIVE_KEY" '$1 == "fpr" && $10 == want { found = 1 } END { exit !found }' \ + || fatal "$keyring does not hold key $KEEL_ARCHIVE_KEY" + +# 5. The signature on the copied index is good under that keyring. gpgv wants +# a binary keyring, and dearmouring a public key needs no agent and no +# network. +[ -s "$inrelease" ] || fatal "$inrelease is missing or empty: this copy of the archive is not signed" +binary_keyring=$(mktemp) +trap 'rm -f "$binary_keyring"' EXIT +if ! gpg --batch --dearmor < "$keyring" > "$binary_keyring" 2>/dev/null \ + || ! gpgv --keyring "$binary_keyring" "$inrelease" >/dev/null 2>&1; then + fatal "the signature on $inrelease does not verify against $keyring" +fi + echo "[archive-check $label] $copy_index is the archive at $source_index" +echo "[archive-check $label] $inrelease verifies against $KEEL_ARCHIVE_KEY, nothing is trusted unverified" diff --git a/changelog b/changelog index fee3310..83ca154 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,36 @@ +turnkey-wordpress-19.0 (3) turnkey; urgency=low + + * The build verifies the project's own APT archive instead of reading it + unverified. The staging distribution has been signed since 2026-09-27, but + the build environment had no copy of the public key and the source entry + said [trusted=yes], which switches verification off: apt printed + "W: OpenPGP signature verification failed ... Missing key + 8CFD1A4841448B2227341CEB202CACBD0E97090A" and installed inithooks, + confconsole and keel anyway. The public half of the staging key is now + installed into the build tree as + /etc/apt/keyrings/keel-staging-keyring.asc, the source entry names it + through signed-by, and apt-get update runs with --error-on=any, so a + signature that cannot be checked fails the build instead of warning + (tracker#7). + + * bin/keel-archive-check does the same checks itself rather than trusting + apt to have complained: the copied package index is the live one, the + source entry names the keyring and this distribution, no apt source in + the tree says trusted=yes, and the signature on the copied InRelease + verifies against the named key with gpgv. It runs where the copy is made + and again on the tree that is about to be configured, which is a step + where no apt-get update runs at all. + + * conf.d/zz-project-packages removes the keyring with the source entry and + the copy of the archive, conf.d/zzz-keel-archive refuses to enable the + appliance's own source while either is still in the image, and the boot + test reads the finished rootfs and fails if any of the three is there. The + staging key signs whatever the build host produced, so an image that kept + it would carry trust in a nightly. The removelist + common/removelists-final/turnkey takes all three out as well. + + -- Keel Linux maintainers Sun, 27 Sep 2026 19:30:00 +0000 + turnkey-wordpress-19.0 (2) turnkey; urgency=low * The appliance is a Keel layer on the published mariadb layer: Apache, PHP diff --git a/conf.d/zz-project-packages b/conf.d/zz-project-packages index e7028a2..2e65dfd 100755 --- a/conf.d/zz-project-packages +++ b/conf.d/zz-project-packages @@ -26,6 +26,7 @@ KEEL_APT_ROOT="${KEEL_APT_ROOT:-/srv/keel-apt}" KEEL_APT_REPO="$KEEL_APT_ROOT/repo" KEEL_STAGING_LIST="${KEEL_STAGING_LIST:-/etc/apt/sources.list.d/keel-staging.list}" +KEEL_STAGING_KEYRING="${KEEL_STAGING_KEYRING:-/etc/apt/keyrings/keel-staging-keyring.asc}" KEEL_SOURCES="${KEEL_SOURCES:-/etc/apt/sources.list.d/keel.sources}" KEEL_APT_LISTS="${KEEL_APT_LISTS:-/var/lib/apt/lists}" KEEL_PROJECT_PACKAGES="${KEEL_PROJECT_PACKAGES:-inithooks confconsole keel}" @@ -72,9 +73,15 @@ for package in $KEEL_PROJECT_PACKAGES; do echo "$package $installed, the candidate of the project archive" done -# the build time package source is not for appliances: remove it, and keep the -# documented, disabled entry for the future signed repository (overlay) +# the build time package source is not for appliances: remove the source entry, +# the copy of the archive it names and the keyring the build verified that +# archive with, and keep the documented, disabled entry for the future signed +# repository (overlay). The staging key signs whatever the build host produced, +# so an image that kept it would carry trust in a nightly (tracker#7). +# common/removelists-final/turnkey removes the same three paths at the end of +# the build, whatever a recipe does. rm -f "$KEEL_STAGING_LIST" +rm -f "$KEEL_STAGING_KEYRING" rm -rf "$KEEL_APT_ROOT" rm -rf "${KEEL_APT_LISTS:?}"/* grep -q '^Enabled: no' "$KEEL_SOURCES" diff --git a/conf.d/zzz-keel-archive b/conf.d/zzz-keel-archive index 01d0f34..24697a4 100755 --- a/conf.d/zzz-keel-archive +++ b/conf.d/zzz-keel-archive @@ -23,6 +23,7 @@ KEEL_KEYRING="${KEEL_KEYRING:-/usr/share/keyrings/keel-archive-keyring.gpg}" KEEL_ARCHIVE_URI="${KEEL_ARCHIVE_URI:-https://archive.keellinux.org}" KEEL_ARCHIVE_SUITE="${KEEL_ARCHIVE_SUITE:-trixie}" KEEL_STAGING_LIST="${KEEL_STAGING_LIST:-/etc/apt/sources.list.d/keel-staging.list}" +KEEL_STAGING_KEYRING="${KEEL_STAGING_KEYRING:-/etc/apt/keyrings/keel-staging-keyring.asc}" fatal() { echo "fatal [zzz-keel-archive]: $*" >&2 @@ -37,9 +38,14 @@ field() { [ -f "$KEEL_SOURCES" ] || fatal "$KEEL_SOURCES is not in the image" [ -s "$KEEL_KEYRING" ] || fatal "$KEEL_KEYRING is missing or empty: the plan asks for keel-archive-keyring" -# The build time source must be gone before this one is enabled. +# The build time source must be gone before this one is enabled, and so must +# the keyring the build verified it with: the staging key signs whatever the +# build host produced, so an image that kept it would carry trust in a nightly +# (tracker#7). [ ! -e "$KEEL_STAGING_LIST" ] \ || fatal "$KEEL_STAGING_LIST is still in the image: conf.d/zz-project-packages did not run" +[ ! -e "$KEEL_STAGING_KEYRING" ] \ + || fatal "$KEEL_STAGING_KEYRING is still in the image: the staging key must not reach an appliance" # Never the unsigned staging distribution: it exists only inside a build. suite=$(field Suites "$KEEL_SOURCES") diff --git a/tests/archive-check.bats b/tests/archive-check.bats index 4526e5d..ace9f31 100644 --- a/tests/archive-check.bats +++ b/tests/archive-check.bats @@ -1,7 +1,38 @@ #!/usr/bin/env bats # bin/keel-archive-check: the copy of the project archive inside a build tree -# has to be the archive as it is right now. Everything here runs against -# scratch directories; no root, no network, no fab. +# has to be the archive as it is right now, and the build's apt has to be able +# to verify it. Everything here runs against scratch directories and a key +# generated for the test; no root, no network, no fab. + +setup_file() { + # One key for the whole file: generating an ed25519 key is a second, and + # every test needs the same signature to verify against. + export GNUPGHOME="$BATS_FILE_TMPDIR/gnupg" + mkdir -m 700 -p "$GNUPGHOME" + gpg --batch --quiet --passphrase '' --quick-generate-key \ + 'Keel Test Staging ' ed25519 sign never + gpg --batch --quiet --passphrase '' --quick-generate-key \ + 'Keel Test Other ' ed25519 sign never + + export GOOD_KEY OTHER_KEY + GOOD_KEY=$(key_fingerprint staging@example.invalid) + OTHER_KEY=$(key_fingerprint other@example.invalid) + + export GOOD_KEYRING="$BATS_FILE_TMPDIR/good.asc" + export OTHER_KEYRING="$BATS_FILE_TMPDIR/other.asc" + gpg --batch --quiet --armor --export "$GOOD_KEY" > "$GOOD_KEYRING" + gpg --batch --quiet --armor --export "$OTHER_KEY" > "$OTHER_KEYRING" + + export SIGNED_INRELEASE="$BATS_FILE_TMPDIR/InRelease" + printf 'Suite: staging\nCodename: trixie-staging\n' \ + | gpg --batch --quiet --local-user "$GOOD_KEY" --clearsign \ + > "$SIGNED_INRELEASE" +} + +key_fingerprint() { + gpg --batch --with-colons --list-keys "$1" \ + | awk -F: '$1 == "fpr" { print $10; exit }' +} setup() { ROOT="$BATS_TEST_DIRNAME/.." @@ -10,13 +41,22 @@ setup() { DIST=trixie-staging ARCH=amd64 INDEX="dists/$DIST/main/binary-$ARCH/Packages" + KEYRING_PATH=/etc/apt/keyrings/keel-staging-keyring.asc + LIST_PATH=/etc/apt/sources.list.d/keel-staging.list SOURCE="$scratch/srv/keel-apt/repo" TREE="$scratch/build/root.patched" - mkdir -p "$SOURCE/$(dirname "$INDEX")" "$TREE/srv/keel-apt/repo/$(dirname "$INDEX")" + COPY="$TREE/srv/keel-apt/repo" + mkdir -p "$SOURCE/$(dirname "$INDEX")" "$COPY/$(dirname "$INDEX")" \ + "$COPY/dists/$DIST" "$TREE/etc/apt/keyrings" "$TREE/etc/apt/sources.list.d" write_index "$SOURCE/$INDEX" 2.3.6+keel4 - write_index "$TREE/srv/keel-apt/repo/$INDEX" 2.3.6+keel4 + write_index "$COPY/$INDEX" 2.3.6+keel4 + cp "$SIGNED_INRELEASE" "$COPY/dists/$DIST/InRelease" + cp "$GOOD_KEYRING" "$TREE$KEYRING_PATH" + write_list "$DIST" "$KEYRING_PATH" + + export KEEL_ARCHIVE_KEY="$GOOD_KEY" } write_index() { @@ -27,22 +67,32 @@ Architecture: all INDEX } -@test "a copy that is the archive passes and says so" { - run "$CHECK" "$SOURCE" "$TREE" "$DIST" "$ARCH" root.patched +write_list() { + echo "deb [signed-by=$2] file:///srv/keel-apt/repo $1 main" \ + > "$TREE$LIST_PATH" +} + +check() { + run "$CHECK" "$SOURCE" "$TREE" "$DIST" "$ARCH" "$@" +} + +@test "a copy that is the archive, signed by the named key, passes and says so" { + check root.patched [ "$status" -eq 0 ] [[ "$output" == *"[archive-check root.patched]"* ]] [[ "$output" == *"is the archive at $SOURCE/$INDEX"* ]] + [[ "$output" == *"verifies against $GOOD_KEY"* ]] } @test "the label defaults to the tree when it is not given" { - run "$CHECK" "$SOURCE" "$TREE" "$DIST" "$ARCH" + check [ "$status" -eq 0 ] [[ "$output" == *"[archive-check $TREE]"* ]] } @test "a copy of an older archive fails and shows what changed" { - write_index "$TREE/srv/keel-apt/repo/$INDEX" 2.3.6+keel1 - run "$CHECK" "$SOURCE" "$TREE" "$DIST" "$ARCH" bootstrap + write_index "$COPY/$INDEX" 2.3.6+keel1 + check bootstrap [ "$status" -eq 1 ] [[ "$output" == *"is not $SOURCE/$INDEX"* ]] [[ "$output" == *"-Version: 2.3.6+keel1"* ]] @@ -51,8 +101,8 @@ INDEX } @test "a build tree with no copy at all fails" { - rm -f "$TREE/srv/keel-apt/repo/$INDEX" - run "$CHECK" "$SOURCE" "$TREE" "$DIST" "$ARCH" bootstrap + rm -f "$COPY/$INDEX" + check bootstrap [ "$status" -eq 1 ] [[ "$output" == *"the build tree has no archive index"* ]] } @@ -67,6 +117,7 @@ INDEX run "$CHECK" "$SOURCE" "$TREE" "$DIST" [ "$status" -eq 1 ] [[ "$output" == *"usage: keel-archive-check"* ]] + [[ "$output" == *"KEEL_ARCHIVE_KEY (required)"* ]] } @test "too many arguments is a usage error" { @@ -80,3 +131,134 @@ INDEX [ "$status" -eq 1 ] [[ "$output" == *"usage: keel-archive-check"* ]] } + +@test "no key named is a failure, because a keyring alone promises nothing" { + KEEL_ARCHIVE_KEY="" check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"KEEL_ARCHIVE_KEY names no key"* ]] +} + +@test "trusted=yes in the entry itself fails: that is the defect" { + echo "deb [trusted=yes] file:///srv/keel-apt/repo $DIST main" > "$TREE$LIST_PATH" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"verification is switched off in"* ]] + [[ "$output" == *"keel-staging.list"* ]] +} + +@test "trusted=yes anywhere else in the tree fails too" { + printf 'deb [ trusted = yes ] http://example.invalid trixie main\n' \ + > "$TREE/etc/apt/sources.list.d/other.list" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"verification is switched off in"* ]] + [[ "$output" == *"other.list"* ]] +} + +@test "Trusted: yes in a deb822 source fails as well" { + printf 'Types: deb\nURIs: http://example.invalid\nSuites: trixie\nTrusted: yes\n' \ + > "$TREE/etc/apt/sources.list.d/other.sources" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"verification is switched off in"* ]] +} + +@test "a tree with no source entry at all fails" { + rm -f "$TREE$LIST_PATH" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"has no source entry at"* ]] +} + +@test "a source file with no deb line fails" { + printf '# nothing but a comment\n' > "$TREE$LIST_PATH" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"has no deb line"* ]] +} + +@test "an entry that does not name the keyring fails" { + echo "deb file:///srv/keel-apt/repo $DIST main" > "$TREE$LIST_PATH" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"does not name signed-by=$KEYRING_PATH"* ]] +} + +@test "an entry that names another keyring fails" { + write_list "$DIST" /usr/share/keyrings/somebody-else.asc + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"does not name signed-by=$KEYRING_PATH"* ]] +} + +@test "an entry for another distribution fails" { + write_list trixie "$KEYRING_PATH" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"does not name the distribution $DIST"* ]] +} + +@test "a missing keyring fails" { + rm -f "$TREE$KEYRING_PATH" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"is missing or empty: the build cannot verify"* ]] +} + +@test "an empty keyring fails" { + : > "$TREE$KEYRING_PATH" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"is missing or empty: the build cannot verify"* ]] +} + +@test "a keyring holding another key fails, present though it is" { + cp "$OTHER_KEYRING" "$TREE$KEYRING_PATH" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"does not hold key $GOOD_KEY"* ]] +} + +@test "a keyring that is not a public key file fails" { + printf 'this is not a key\n' > "$TREE$KEYRING_PATH" + KEEL_ARCHIVE_KEY="$GOOD_KEY" check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"does not hold key $GOOD_KEY"* ]] +} + +@test "an unsigned copy of the archive fails" { + rm -f "$COPY/dists/$DIST/InRelease" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"is missing or empty: this copy of the archive is not signed"* ]] +} + +@test "a signature made by another key fails" { + printf 'Suite: staging\n' \ + | gpg --batch --quiet --local-user "$OTHER_KEY" --clearsign \ + > "$COPY/dists/$DIST/InRelease" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"does not verify against"* ]] +} + +@test "a signature over content that was changed afterwards fails" { + sed -i 's/^Suite: staging$/Suite: tampered/' "$COPY/dists/$DIST/InRelease" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"does not verify against"* ]] +} + +@test "the keyring path and the source path are overridable together" { + mkdir -p "$TREE/usr/share/keyrings" + mv "$TREE$KEYRING_PATH" "$TREE/usr/share/keyrings/elsewhere.asc" + echo "deb [signed-by=/usr/share/keyrings/elsewhere.asc] file:///srv/keel-apt/repo $DIST main" \ + > "$TREE/etc/apt/sources.list.d/elsewhere.list" + rm -f "$TREE$LIST_PATH" + run env KEEL_ARCHIVE_KEY="$GOOD_KEY" \ + KEEL_ARCHIVE_KEYRING=/usr/share/keyrings/elsewhere.asc \ + KEEL_ARCHIVE_LIST=/etc/apt/sources.list.d/elsewhere.list \ + "$CHECK" "$SOURCE" "$TREE" "$DIST" "$ARCH" bootstrap + [ "$status" -eq 0 ] + [[ "$output" == *"verifies against $GOOD_KEY"* ]] +} diff --git a/tests/boot-test.bats b/tests/boot-test.bats index 9dc9ed4..8631e19 100644 --- a/tests/boot-test.bats +++ b/tests/boot-test.bats @@ -491,6 +491,42 @@ EOF [[ "$output" == *"is enabled and verified with"* ]] } +@test "build_leftovers_verdict passes on an image that kept none of them" { + run bt_build_leftovers_verdict "$S/rootfs" + [ "$status" -eq 0 ] + [[ "$output" == *"no build time package source, archive copy or staging keyring"* ]] +} + +@test "build_leftovers_verdict names the archive copy the image kept" { + mkdir -p "$S/rootfs/srv/keel-apt/repo" + run bt_build_leftovers_verdict "$S/rootfs" + [ "$status" -eq 1 ] + [[ "$output" == *"still carries the build time /srv/keel-apt"* ]] +} + +@test "build_leftovers_verdict names the source entry the image kept" { + install -D /dev/null "$S/rootfs/etc/apt/sources.list.d/keel-staging.list" + run bt_build_leftovers_verdict "$S/rootfs" + [ "$status" -eq 1 ] + [[ "$output" == *"keel-staging.list"* ]] +} + +@test "build_leftovers_verdict names the staging keyring the image kept" { + install -D /dev/null "$S/rootfs/etc/apt/keyrings/keel-staging-keyring.asc" + run bt_build_leftovers_verdict "$S/rootfs" + [ "$status" -eq 1 ] + [[ "$output" == *"keel-staging-keyring.asc"* ]] +} + +@test "build_leftovers_verdict reports every one of them, not only the first" { + mkdir -p "$S/rootfs/srv/keel-apt" + install -D /dev/null "$S/rootfs/etc/apt/sources.list.d/keel-staging.list" + install -D /dev/null "$S/rootfs/etc/apt/keyrings/keel-staging-keyring.asc" + run bt_build_leftovers_verdict "$S/rootfs" + [ "$status" -eq 1 ] + [ "$(grep -c 'still carries the build time' <<< "$output")" -eq 3 ] +} + @test "sources_verdict refuses a disabled source, a staging suite, another archive and another keyring" { _sources https://archive.keellinux.org trixie no run bt_sources_verdict "$S/keel.sources" diff --git a/tests/boot-test.sh b/tests/boot-test.sh index dcfd985..5157e77 100755 --- a/tests/boot-test.sh +++ b/tests/boot-test.sh @@ -17,12 +17,15 @@ # 5. the administrator logs in over HTTP with the declared app_password and # is given a session cookie, a wrong password is refused, and the # dashboard comes back for the session -# 6. apt-get update against archive.keellinux.org verifies its signature, +# 6. the image carries none of the build time files the recipe installed the +# project's own packages from: the copy of the staging archive, its +# source entry and the keyring that verified it +# 7. apt-get update against archive.keellinux.org verifies its signature, # apt takes a project package from that archive at the appliance's own pin # priority, a project package the image has not got is fetched from it # against the digest of the signed index, and a project package the image # has is downloaded again and put through dpkg -# 7. keel diff reports no drift between the spec and the machine +# 8. keel diff reports no drift between the spec and the machine # # Called by the reusable workflow test-appliance.yml after keel pull and keel # verify; runnable by hand as root on any host with LXC, see tests/README.md. @@ -219,6 +222,7 @@ rm -f "$passfile" "$container_dir/bad_password" # the half of the deliverable that is not about WordPress, and it is proved # on the booted machine because a source file that says the right thing and # an apt that cannot verify the archive look identical from the build. +bt_build_leftovers_verdict "$BT_ROOTFS" bt_sources_verdict "$BT_ROOTFS/$BT_SOURCES" if [ "$BT_SKIP_UPDATE" -eq 1 ]; then log "--skip-update: the two APT proofs were not run" diff --git a/tests/keel-archive.bats b/tests/keel-archive.bats index a4fc5b7..edfb911 100644 --- a/tests/keel-archive.bats +++ b/tests/keel-archive.bats @@ -10,9 +10,10 @@ setup() { export KEEL_SOURCES="$S/keel.sources" export KEEL_KEYRING="$S/keel-archive-keyring.gpg" export KEEL_STAGING_LIST="$S/keel-staging.list" + export KEEL_STAGING_KEYRING="$S/keel-staging-keyring.asc" _keyring _sources - rm -f "$KEEL_STAGING_LIST" + rm -f "$KEEL_STAGING_LIST" "$KEEL_STAGING_KEYRING" # gpg is stubbed: a keyring is a file, and what matters to this script is # how many public keys the reader says are in it. STUBS="$S/bin" @@ -102,7 +103,7 @@ _keyring() { } @test "a build time source still in the image is fatal" { - printf 'deb [trusted=yes] file:///srv/keel-apt/repo trixie-staging main\n' \ + printf 'deb [signed-by=/etc/apt/keyrings/keel-staging-keyring.asc] file:///srv/keel-apt/repo trixie-staging main\n' \ > "$KEEL_STAGING_LIST" run bash "$SCRIPT" [ "$status" -ne 0 ] @@ -110,6 +111,16 @@ _keyring() { grep -qx "Enabled: no" "$KEEL_SOURCES" } +@test "the build time staging keyring still in the image is fatal" { + # It verified the archive during the build and it signs whatever the build + # host produced, so it is not an appliance's business (tracker#7). + printf 'the staging public key\n' > "$KEEL_STAGING_KEYRING" + run bash "$SCRIPT" + [ "$status" -ne 0 ] + [[ "$output" == *"must not reach an appliance"* ]] + grep -qx "Enabled: no" "$KEEL_SOURCES" +} + @test "a staging suite is refused by name" { export KEEL_ARCHIVE_SUITE=trixie-staging _sources "Suites=trixie-staging" diff --git a/tests/lib/boot-test-lib.sh b/tests/lib/boot-test-lib.sh index 0256ad4..ff944f0 100644 --- a/tests/lib/boot-test-lib.sh +++ b/tests/lib/boot-test-lib.sh @@ -46,6 +46,14 @@ BT_ARCHIVE_HOST="${BT_ARCHIVE_HOST%%/*}" BT_ARCHIVE_SUITE="trixie" BT_ARCHIVE_KEYRING="/usr/share/keyrings/keel-archive-keyring.gpg" BT_SOURCES="etc/apt/sources.list.d/keel.sources" +# What the build used to reach the project's own packages and what must not be +# in the finished image: the copy of the staging archive, the source entry that +# named it and the keyring the build verified it with (tracker#7). The staging +# key signs whatever the build host produced, so an image that kept it would +# carry trust in a nightly. Relative to the rootfs, like BT_SOURCES. +BT_BUILD_LEFTOVERS="srv/keel-apt +etc/apt/sources.list.d/keel-staging.list +etc/apt/keyrings/keel-staging-keyring.asc" # What the two update proofs use, beyond apt-get update itself. # # A project package the image already carries, to show that apt would take its @@ -519,6 +527,24 @@ bt_dashboard_verdict() { echo "boot-test: /wp-admin/ answered 200 with the dashboard for the logged in admin" } +bt_build_leftovers_verdict() { + # bt_build_leftovers_verdict ROOTFS: none of the build time files the + # recipe used to install the project's own packages is in the image. The + # recipe's conf script removes them and common/removelists-final/turnkey + # removes them again; this is the only place that reads the finished image + # and says so, which is what "not in the image" has to mean. + local rootfs=$1 path found=0 + while read -r path; do + [ -n "$path" ] || continue + if [ -e "$rootfs/$path" ]; then + echo "boot-test: the image still carries the build time /$path" >&2 + found=1 + fi + done <<< "$BT_BUILD_LEFTOVERS" + [ "$found" -eq 0 ] || return 1 + echo "boot-test: no build time package source, archive copy or staging keyring in the image" +} + bt_sources_verdict() { # bt_sources_verdict FILE: the appliance's own APT source, as it ships: # enabled, the signed distribution, our keyring, and never a staging one. diff --git a/tests/project-packages.bats b/tests/project-packages.bats index 60711b0..b09ca7c 100644 --- a/tests/project-packages.bats +++ b/tests/project-packages.bats @@ -13,16 +13,20 @@ setup() { export KEEL_APT_ROOT="$scratch/srv/keel-apt" export KEEL_STAGING_LIST="$scratch/apt/sources.list.d/keel-staging.list" + export KEEL_STAGING_KEYRING="$scratch/apt/keyrings/keel-staging-keyring.asc" export KEEL_SOURCES="$scratch/apt/sources.list.d/keel.sources" export KEEL_APT_LISTS="$scratch/apt/lists" export FIXTURES="$scratch/fixtures" INDEX="$KEEL_APT_ROOT/repo/dists/$DIST/main/binary-$ARCH/Packages" mkdir -p "$(dirname "$INDEX")" "$(dirname "$KEEL_STAGING_LIST")" \ + "$(dirname "$KEEL_STAGING_KEYRING")" \ "$KEEL_APT_LISTS" "$FIXTURES" "$scratch/bin" touch "$KEEL_APT_LISTS/keel_Packages" - echo "deb [trusted=yes] file://$KEEL_APT_ROOT/repo $DIST main" > "$KEEL_STAGING_LIST" + echo "deb [signed-by=$KEEL_STAGING_KEYRING] file://$KEEL_APT_ROOT/repo $DIST main" \ + > "$KEEL_STAGING_LIST" + printf 'not a key, and this script never reads one\n' > "$KEEL_STAGING_KEYRING" printf 'Types: deb\nURIs: https://apt.keellinux.org\nEnabled: no\n' > "$KEEL_SOURCES" offer inithooks 2.3.6+keel4 @@ -92,6 +96,14 @@ installed() { printf 'version=%s\nstatus=%s\n' "$2" "$3" > "$FIXTURES/installed. [ -f "$KEEL_SOURCES" ] } +@test "the keyring that verified the staging archive is gone too" { + # The staging key signs whatever the build host produced, so it must not + # reach an installed appliance (tracker#7). + run "$SCRIPT" + [ "$status" -eq 0 ] + [ ! -e "$KEEL_STAGING_KEYRING" ] +} + @test "the recipe names no version: a new publication is simply the new candidate" { rm "$INDEX" offer inithooks 2.3.7+keel9 @@ -170,7 +182,8 @@ installed() { printf 'version=%s\nstatus=%s\n' "$2" "$3" > "$FIXTURES/installed. } @test "a source list that names a distribution the archive has not got fails" { - echo "deb [trusted=yes] file://$KEEL_APT_ROOT/repo trixie-nowhere main" > "$KEEL_STAGING_LIST" + echo "deb [signed-by=$KEEL_STAGING_KEYRING] file://$KEEL_APT_ROOT/repo trixie-nowhere main" \ + > "$KEEL_STAGING_LIST" run "$SCRIPT" [ "$status" -eq 1 ] [[ "$output" == *"no package index at"* ]] From bc3d203a2397be9c444057311be2c7aa3633eda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20M=C3=A9ndez?= Date: Sun, 27 Sep 2026 20:06:04 +0000 Subject: [PATCH 2/2] fix: the trusted=yes that is refused is one on the project archive The check refused a trusted=yes on any apt source in the build tree. The captured pool of decision 0012 sets Trusted: yes on purpose, for a file: index generated on this machine from files keel-pool verify checks against the same digests apt does, so the wider rule would have failed every pinned build. What is refused is now a source that names the project archive and switches verification off, which is the defect of tracker#7, in whichever file and in either of apt's two formats. What the pool does is the pool's business. Measured with the same suite: bin/keel-archive-check 100 percent (54/54) over 27 bats tests, three of them new: the pool's Trusted: yes passes, a trusted=yes on the project archive in another file fails, and the archive the rule applies to is overridable. --- COVERAGE.md | 6 +++--- bin/keel-archive-check | 24 ++++++++++++++++++------ changelog | 6 ++++++ tests/archive-check.bats | 33 ++++++++++++++++++++++++++------- 4 files changed, 53 insertions(+), 16 deletions(-) diff --git a/COVERAGE.md b/COVERAGE.md index 4f968aa..8ca22a6 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -13,13 +13,13 @@ acceptance test of a recipe, docs/org-plan.md section 1). | `tests/lib/boot-test-lib.sh` | `tests/boot-test.bats` (73 tests) | 98.95 percent (282/285) under kcov | parsing, addresses, deadlines, the container marks, every verdict, and the image carrying none of the build time archive files | | `conf.d/zzz-keel-archive` | `tests/keel-archive.bats` (13 tests) | 100 percent (26/26) under kcov | every way it enables and every way it refuses, including a staging keyring left in the image | | `conf.d/zz-project-packages` | `tests/project-packages.bats` (14 tests) | 100 percent (31/31) under kcov | shared with keel-nodebb, where the pattern is maintained | -| `bin/keel-archive-check` | `tests/archive-check.bats` (25 tests) | 100 percent (52/52) under kcov | the build time check of tracker#7: the copy is the live archive, the entry names the keyring through signed-by, nothing says trusted=yes, and the copied InRelease verifies against the staging key | +| `bin/keel-archive-check` | `tests/archive-check.bats` (27 tests) | 100 percent (54/54) under kcov | the build time check of tracker#7: the copy is the live archive, the entry names the keyring through signed-by, nothing says trusted=yes, and the copied InRelease verifies against the staging key | | `overlay/usr/lib/inithooks/bin/wordpress.py` | none | 0 | dialog wrapper, only reached with a terminal attached | | `overlay/usr/lib/inithooks/lib/*.php` | the boot test | integration only | two PHP files `wp eval-file` runs; `conf.d/main` has PHP lint them | | `conf.d/main` | the build | integration only | build time script, 0004 pragmatic limits | | `tests/boot-test.sh` | itself | integration only | the thin main of the acceptance test: keel and LXC as root | -Total over the six measured shell files: **99.07 percent (533/538)**, 194 bats +Total over the six measured shell files: **99.07 percent (535/540)**, 196 bats tests, none failing. `tests/coverage.sh` fails below `COVERAGE_THRESHOLD`, which the workflow sets to **97**, the lowest measured file. It is only ever raised (decision 0006). @@ -31,7 +31,7 @@ sets to **97**, the lowest measured file. It is only ever raised (decision 97.73 43/44 40wordpress 98.95 282/285 boot-test-lib.sh 99.00 99/100 wordpress.sh - 100.00 52/52 keel-archive-check + 100.00 54/54 keel-archive-check 100.00 29/29 zz-project-packages 100.00 26/26 keel-archive-check diff --git a/bin/keel-archive-check b/bin/keel-archive-check index 4de10e6..a61607d 100755 --- a/bin/keel-archive-check +++ b/bin/keel-archive-check @@ -41,6 +41,8 @@ # promise until the key inside it is named. # KEEL_ARCHIVE_KEYRING where in TREE the keyring is # KEEL_ARCHIVE_LIST where in TREE the source entry is +# KEEL_ARCHIVE_PATH the path an apt source names this archive by, which is +# the archive a trusted=yes is refused for # # Exit 0 when everything holds, 1 otherwise. set -eu @@ -74,6 +76,9 @@ fatal() { [ -n "$KEEL_ARCHIVE_KEY" ] \ || fatal "KEEL_ARCHIVE_KEY names no key: the archive would be verified against whatever the keyring happens to hold" +# The path an apt source names this archive by, which is what makes a +# trusted=yes elsewhere in the tree somebody else's business. +ARCHIVE_PATH="${KEEL_ARCHIVE_PATH:-/srv/keel-apt/repo}" index=dists/$dist/main/binary-$arch/Packages source_index=$source_repo/$index copy=$tree/srv/keel-apt/repo @@ -95,16 +100,23 @@ if ! cmp -s "$source_index" "$copy_index"; then exit 1 fi -# 2. Nothing in the tree switches verification off. A single trusted=yes -# anywhere turns every failure below into a warning, which is the defect this -# check exists for, so it is refused wherever it is written, in either of the -# two formats apt reads a source in. +# 2. Nothing in the tree switches verification off for this archive. A single +# trusted=yes on it turns every failure below into a warning, which is the +# defect this check exists for, so it is refused in either of the two formats +# apt reads a source in and in whichever file it is written. +# +# Scoped to the sources that name this archive, not to every source in the +# tree: the captured pool of decision 0012 sets Trusted: yes on purpose, for a +# file: index generated on this machine from files keel-pool verify checks +# against the same digests apt does. Refusing that would fail every pinned +# build. What the pool does is the pool's business; this archive is verified. verification_off='trusted *= *yes|^ *Trusted: *yes' apt_sources="$tree/etc/apt/sources.list $tree/etc/apt/sources.list.d" # shellcheck disable=SC2086 # the two paths are one word each, on purpose -untrusted=$(grep -rlE "$verification_off" $apt_sources 2>/dev/null || true) +distrusting=$(grep -rlE "$verification_off" $apt_sources 2>/dev/null || true) +untrusted=$(echo "$distrusting" | xargs -r grep -lF "$ARCHIVE_PATH" || true) [ -z "$untrusted" ] \ - || fatal "verification is switched off in: $(echo "$untrusted" | tr '\n' ' ')" + || fatal "verification is switched off for $ARCHIVE_PATH in: $(echo "$untrusted" | tr '\n' ' ')" # 3. The source entry names this distribution and the keyring it is verified # with. apt takes signed-by from the entry, so the entry is what decides diff --git a/changelog b/changelog index 83ca154..d722e4f 100644 --- a/changelog +++ b/changelog @@ -21,6 +21,12 @@ turnkey-wordpress-19.0 (3) turnkey; urgency=low and again on the tree that is about to be configured, which is a step where no apt-get update runs at all. + * The trusted=yes that bin/keel-archive-check refuses is one on the project + archive, not one on every apt source in the build tree. The captured pool + of decision 0012 sets Trusted: yes on purpose, for a file: index generated + on this machine from files keel-pool verify checks against the same digests + apt does, so the wider rule would have failed every pinned build. + * conf.d/zz-project-packages removes the keyring with the source entry and the copy of the archive, conf.d/zzz-keel-archive refuses to enable the appliance's own source while either is still in the image, and the boot diff --git a/tests/archive-check.bats b/tests/archive-check.bats index ace9f31..1ba2380 100644 --- a/tests/archive-check.bats +++ b/tests/archive-check.bats @@ -142,25 +142,44 @@ check() { echo "deb [trusted=yes] file:///srv/keel-apt/repo $DIST main" > "$TREE$LIST_PATH" check bootstrap [ "$status" -eq 1 ] - [[ "$output" == *"verification is switched off in"* ]] + [[ "$output" == *"verification is switched off for /srv/keel-apt/repo"* ]] [[ "$output" == *"keel-staging.list"* ]] } -@test "trusted=yes anywhere else in the tree fails too" { - printf 'deb [ trusted = yes ] http://example.invalid trixie main\n' \ +@test "trusted=yes on this archive in another file fails too" { + printf 'deb [ trusted = yes ] file:///srv/keel-apt/repo trixie-staging main\n' \ > "$TREE/etc/apt/sources.list.d/other.list" check bootstrap [ "$status" -eq 1 ] - [[ "$output" == *"verification is switched off in"* ]] + [[ "$output" == *"verification is switched off for /srv/keel-apt/repo"* ]] [[ "$output" == *"other.list"* ]] } -@test "Trusted: yes in a deb822 source fails as well" { - printf 'Types: deb\nURIs: http://example.invalid\nSuites: trixie\nTrusted: yes\n' \ +@test "Trusted: yes on this archive in a deb822 source fails as well" { + printf 'Types: deb\nURIs: file:///srv/keel-apt/repo\nSuites: trixie-staging\nTrusted: yes\n' \ > "$TREE/etc/apt/sources.list.d/other.sources" check bootstrap [ "$status" -eq 1 ] - [[ "$output" == *"verification is switched off in"* ]] + [[ "$output" == *"verification is switched off for"* ]] +} + +@test "the captured pool may say Trusted: yes, because it is not this archive" { + # Decision 0012: a file: index generated on this machine, whose digests + # keel-pool verify checks. Refusing it would fail every pinned build. + printf 'Types: deb\nURIs: file:/keel-pool\nSuites: 2026-09-27\nTrusted: yes\n' \ + > "$TREE/etc/apt/sources.list.d/keel-pool.sources" + check bootstrap + [ "$status" -eq 0 ] + [[ "$output" == *"nothing is trusted unverified"* ]] +} + +@test "the archive a trusted=yes is refused for is overridable" { + printf 'deb [trusted=yes] file:///elsewhere/repo trixie main\n' \ + > "$TREE/etc/apt/sources.list.d/other.list" + run env KEEL_ARCHIVE_KEY="$GOOD_KEY" KEEL_ARCHIVE_PATH=/elsewhere/repo \ + "$CHECK" "$SOURCE" "$TREE" "$DIST" "$ARCH" bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"switched off for /elsewhere/repo"* ]] } @test "a tree with no source entry at all fails" {