From fc6cae075072a049ab73a9a60e8b9bf225c3097f 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] 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 | 6 +- Makefile | 62 +++++++-- bin/keel-archive-check | 142 +++++++++++++++++++++ changelog | 31 +++++ conf.d/main | 10 +- tests/archive-check.bats | 264 +++++++++++++++++++++++++++++++++++++++ tests/coverage.sh | 7 +- 7 files changed, 506 insertions(+), 16 deletions(-) create mode 100755 bin/keel-archive-check create mode 100644 tests/archive-check.bats diff --git a/COVERAGE.md b/COVERAGE.md index 409da5b..6100e5c 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -11,12 +11,13 @@ LXC as the acceptance test of a recipe, docs/org-plan.md section 1). | overlay/usr/lib/inithooks/lib/postgresql.sh | tests/postgresql.bats (11 tests) | 100 percent (41/41) under kcov | every function and every branch | | overlay/usr/lib/inithooks/firstboot.d/36pgsqlverify | tests/hook.bats (10 tests) | 100 percent (15/15) under kcov | every path, including the two failures that matter | | tests/lib/boot-test-lib.sh | tests/boot-test.bats (44 tests) | 100 percent (154/154) under kcov | argument parsing, address discovery, deadlines, the container marks, the database, module, Webmin and diff verdicts | +| bin/keel-archive-check | tests/archive-check.bats (25 tests) | 100 percent (52/52) under kcov | the build time check: the archive copy in the build tree is the live archive, the source entry names the keyring through signed-by, nothing says trusted=yes, and the signature on the copied InRelease verifies against the staging key (tracker#7) | | 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 | | overlay ... firstboot.d/35pgsqlpass | common | not this repository | the hook that sets the password belongs to the `common` fork and is used, not rewritten | -Total over the three measured shell files: **100 percent (210/210)**, -65 bats tests. `tests/coverage.sh` fails below `COVERAGE_THRESHOLD`, which +Total over the four measured shell files: **100 percent (262/262)**, +90 bats tests. `tests/coverage.sh` fails below `COVERAGE_THRESHOLD`, which the workflow sets to 100, the measured number. It is only ever raised (decision 0006). @@ -25,6 +26,7 @@ the workflow sets to 100, the measured number. It is only ever raised 100.00 41/41 postgresql.sh 100.00 15/15 36pgsqlverify 100.00 154/154 boot-test-lib.sh + 100.00 52/52 keel-archive-check ## What the hook tests cover diff --git a/Makefile b/Makefile index 364fb9e..b1878cb 100644 --- a/Makefile +++ b/Makefile @@ -23,21 +23,65 @@ WEBMIN_FW_TCP_INCOMING = 22 5432 12320 12321 include $(FAB_PATH)/common/mk/turnkey.mk # The project's own packages (inithooks, confconsole, keel) 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/main removes both -# from the image and leaves the future apt.keellinux.org entry in place, -# disabled. Same block as keel-nodebb, which is where the pattern is -# maintained. +# 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/main removes the copy of the archive, the source +# entry and the keyring from the image and leaves the future apt.keellinux.org +# entry in place, disabled. +# +# 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 +# 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, 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/; - 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"; + 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; + fab-chroot $O/bootstrap "apt-get update --error-on=any"; endef bootstrap/post += $(_keel_bootstrap/post) + +# bootstrap is a stamped target, so a second build of the same product reuses +# the copy the first one made and a check in bootstrap/post does not run at +# 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. 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; +endef +root.patched/pre += $(_keel_root.patched/pre) diff --git a/bin/keel-archive-check b/bin/keel-archive-check new file mode 100755 index 0000000..4de10e6 --- /dev/null +++ b/bin/keel-archive-check @@ -0,0 +1,142 @@ +#!/bin/bash +# 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: +# +# 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] +# +# SOURCE_REPO the live archive on the build host, e.g. /srv/keel-apt/repo +# TREE the build tree that carries the copy under srv/keel-apt/repo +# DIST the distribution, e.g. trixie-staging +# ARCH the Debian architecture, e.g. amd64 +# LABEL name of the build step, for the messages (default: TREE) +# +# 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 +} + +[ $# -ge 4 ] && [ $# -le 5 ] || usage +source_repo=$1 +tree=$2 +dist=$3 +arch=$4 +label=${5:-$2} + +for value in "$source_repo" "$tree" "$dist" "$arch"; do + [ -n "$value" ] || usage +done + +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" + +if ! cmp -s "$source_index" "$copy_index"; then + echo "FATAL [archive-check $label]: $copy_index is not $source_index" >&2 + diff -u "$copy_index" "$source_index" >&2 || true + echo "The build would install the packages the archive offered when this" >&2 + echo "bootstrap was made, not the ones it offers now. Remove the build" >&2 + echo "directory (or at least build/stamps/bootstrap) and build again." >&2 + 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 5ca7b24..37499ca 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,34 @@ +turnkey-postgresql-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/main removes the keyring with the source entry and the copy of the + archive: 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, whatever a + recipe does. + + -- Keel Linux maintainers Sun, 27 Sep 2026 19:30:00 +0000 + turnkey-postgresql-19.0 (2) turnkey; urgency=low * Listen on both loopback addresses, named literally: diff --git a/conf.d/main b/conf.d/main index 5908c2a..17fc47f 100755 --- a/conf.d/main +++ b/conf.d/main @@ -113,9 +113,15 @@ keel 0.2.1 EOF dpkg-query -W -f '${Package} ${Version} ${Status}\n' inithooks confconsole keel -# The build time package source is not for appliances: remove it, 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 /etc/apt/sources.list.d/keel-staging.list +rm -f /etc/apt/keyrings/keel-staging-keyring.asc rm -rf /srv/keel-apt rm -rf /var/lib/apt/lists/* grep -q '^Enabled: no' /etc/apt/sources.list.d/keel.sources diff --git a/tests/archive-check.bats b/tests/archive-check.bats new file mode 100644 index 0000000..ace9f31 --- /dev/null +++ b/tests/archive-check.bats @@ -0,0 +1,264 @@ +#!/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, 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/.." + CHECK="$ROOT/bin/keel-archive-check" + scratch="$BATS_TEST_TMPDIR/archive" + 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" + 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 "$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() { + cat > "$1" < "$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" { + check + [ "$status" -eq 0 ] + [[ "$output" == *"[archive-check $TREE]"* ]] +} + +@test "a copy of an older archive fails and shows what changed" { + write_index "$COPY/$INDEX" 2.3.6+keel1 + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"is not $SOURCE/$INDEX"* ]] + [[ "$output" == *"-Version: 2.3.6+keel1"* ]] + [[ "$output" == *"+Version: 2.3.6+keel4"* ]] + [[ "$output" == *"build/stamps/bootstrap"* ]] +} + +@test "a build tree with no copy at all fails" { + rm -f "$COPY/$INDEX" + check bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"the build tree has no archive index"* ]] +} + +@test "an archive with no index for this distribution fails" { + run "$CHECK" "$SOURCE" "$TREE" trixie-nowhere "$ARCH" bootstrap + [ "$status" -eq 1 ] + [[ "$output" == *"the archive has no index"* ]] +} + +@test "too few arguments is a usage error" { + 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" { + run "$CHECK" "$SOURCE" "$TREE" "$DIST" "$ARCH" label extra + [ "$status" -eq 1 ] + [[ "$output" == *"usage: keel-archive-check"* ]] +} + +@test "an empty argument is a usage error rather than a wrong path" { + run "$CHECK" "$SOURCE" "$TREE" "" "$ARCH" bootstrap + [ "$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/coverage.sh b/tests/coverage.sh index 3beedf1..6b6325c 100755 --- a/tests/coverage.sh +++ b/tests/coverage.sh @@ -3,8 +3,9 @@ # bats suite (decision 0004). The measured files are the first boot library # (lib/postgresql.sh), the first boot hook this layer adds # (firstboot.d/36pgsqlverify) and the logic of the boot test -# (tests/lib/boot-test-lib.sh); the 95 percent bar of decision 0003 applies -# to all three and all three are at 100. Exits 1 below the threshold, 2 when +# (tests/lib/boot-test-lib.sh) and the build time archive check the recipe +# runs (bin/keel-archive-check); the 95 percent bar of decision 0003 applies +# to all four and all four are at 100. Exits 1 below the threshold, 2 when # a tool is missing. tests/boot-test.sh is the thin main that runs keel and # LXC as root and is exercised by the container run in test-appliance.yml, # not measured here; conf.d/main is a build time script and is exercised by @@ -27,7 +28,7 @@ done report="${COVERAGE_DIR:-$(mktemp -d)}" # The include pattern is the whitelist, so no exclude pattern is needed; an # exclude of /tests/ would drop tests/lib/boot-test-lib.sh with it. -kcov --include-pattern=/lib/postgresql.sh,/firstboot.d/36pgsqlverify,/tests/lib/boot-test-lib.sh \ +kcov --include-pattern=/lib/postgresql.sh,/firstboot.d/36pgsqlverify,/tests/lib/boot-test-lib.sh,/bin/keel-archive-check \ "$report" bats "$here" json="$(find "$report" -mindepth 2 -maxdepth 2 -name coverage.json -not -path "*/kcov-merged/*" | head -1)"