259 lines
11 KiB
Bash
Executable File
259 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The production change that breaks these cases is accepting an unverified
|
|
# download, a wrong signer, or executable/malformed provenance data.
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
fixtures="$repo_dir/tests/setup/fixtures/provenance"
|
|
config="$repo_dir/setup/provenance/installers.conf"
|
|
test_tmp="$(mktemp -d)"
|
|
host_gnupg="${GNUPGHOME:-$HOME/.gnupg}"
|
|
host_rpmdb="/usr/lib/sysimage/rpm/rpmdb.sqlite"
|
|
ambient_gnupg="$test_tmp/fresh-ambient-gnupg"
|
|
|
|
cleanup() {
|
|
rm -rf -- "$test_tmp"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
fail() {
|
|
printf 'package provenance contract: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
expect_success() {
|
|
"$@" || fail "expected success: $*"
|
|
}
|
|
|
|
expect_failure() {
|
|
if "$@"; then
|
|
fail "expected failure: $*"
|
|
fi
|
|
}
|
|
|
|
assert_file_bytes() {
|
|
local path="$1" expected="$2"
|
|
[[ "$(<"$path")" == "$expected" ]] || fail "unexpected bytes in $path"
|
|
}
|
|
|
|
snapshot() {
|
|
local path="$1"
|
|
if [[ -e "$path" ]]; then
|
|
stat -c '%i:%s:%Y:%Z' "$path"
|
|
else
|
|
printf 'absent'
|
|
fi
|
|
}
|
|
|
|
snapshot_gpg_state() {
|
|
local path="$1" file relative
|
|
[[ -d "$path" ]] || {
|
|
printf 'absent\n'
|
|
return 0
|
|
}
|
|
|
|
{
|
|
printf 'directory\0'
|
|
find "$path" -mindepth 1 -print0 | LC_ALL=C sort -z \
|
|
| while IFS= read -r -d '' file; do
|
|
relative="${file#"$path"/}"
|
|
printf '%s\0' "$relative"
|
|
if [[ -L "$file" ]]; then
|
|
printf 'symlink\0%s\0' "$(readlink -- "$file")"
|
|
elif [[ -f "$file" ]]; then
|
|
printf 'file\0%s\0' "$(sha256sum -- "$file" | awk '{ print $1 }')"
|
|
elif [[ -d "$file" ]]; then
|
|
printf 'directory\0'
|
|
else
|
|
printf 'other\0%s\0' "$(stat -c '%F:%a:%s:%Y:%Z' "$file")"
|
|
fi
|
|
done
|
|
} | sha256sum | awk '{ print $1 }'
|
|
}
|
|
|
|
export GNUPGHOME="$ambient_gnupg"
|
|
mkdir -m 700 "$ambient_gnupg"
|
|
printf 'baseline-value\n' > "$ambient_gnupg/unexpected-entry"
|
|
ambient_before="$(snapshot "$ambient_gnupg")"
|
|
ambient_gpg_files_before="$(snapshot_gpg_state "$ambient_gnupg")"
|
|
printf 'tampered-value\n' > "$ambient_gnupg/unexpected-entry"
|
|
[[ "$ambient_gpg_files_before" != "$(snapshot_gpg_state "$ambient_gnupg")" ]] \
|
|
|| fail 'ambient GPG snapshot ignored unexpected file content'
|
|
printf 'baseline-value\n' > "$ambient_gnupg/unexpected-entry"
|
|
ambient_gpg_files_before="$(snapshot_gpg_state "$ambient_gnupg")"
|
|
before_gnupg="$(snapshot "$host_gnupg")"
|
|
before_gpg_files="$(snapshot_gpg_state "$host_gnupg")"
|
|
before_rpmdb="$(snapshot "$host_rpmdb")"
|
|
|
|
# This must be the only production file sourced by the contract.
|
|
# shellcheck source=../../setup/lib/artifact-provenance
|
|
source "$repo_dir/setup/lib/artifact-provenance"
|
|
|
|
fixture_fingerprint='6016FF18CAE298CE3648EE2325E01F765E1EF9FA'
|
|
tiny_sha256='291bd319ae85488101e908e37fc0fa1b0da1429ba27e10d2b391cb3f60dd44ea'
|
|
base64 --decode "$fixtures/signed-fixture.rpm.base64" > "$test_tmp/signed-fixture.rpm"
|
|
base64 --decode "$fixtures/unsigned-fixture.rpm.base64" > "$test_tmp/unsigned-fixture.rpm"
|
|
base64 --decode "$fixtures/wrong-signer-fixture.rpm.base64" > "$test_tmp/wrong-signer-fixture.rpm"
|
|
|
|
[[ "$(wc -l < "$fixtures/SHASUMS256.txt")" -eq 4 ]] || fail 'signed checksum fixture is not four lines'
|
|
[[ "$(cmp -l "$fixtures/tiny-artifact" "$fixtures/tiny-artifact-tampered" | wc -l)" -eq 1 ]] \
|
|
|| fail 'tampered artifact does not differ by exactly one byte'
|
|
|
|
expect_success key_fingerprint_matches "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
[[ "$ambient_before" == "$(snapshot "$ambient_gnupg")" ]] \
|
|
|| fail 'fingerprint inspection created ambient GPG state'
|
|
[[ "$ambient_gpg_files_before" == "$(snapshot_gpg_state "$ambient_gnupg")" ]] \
|
|
|| fail 'fingerprint inspection created an ambient GPG keybox or trust database'
|
|
expect_failure key_fingerprint_matches "$fixtures/fixture-key.asc" '0000000000000000000000000000000000000000'
|
|
cat "$fixtures/fixture-key.asc" "$fixtures/wrong-signer-key.asc" > "$test_tmp/combined-key.asc"
|
|
expect_failure key_fingerprint_matches "$test_tmp/combined-key.asc" "$fixture_fingerprint"
|
|
|
|
expect_success verify_detached_signature \
|
|
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
|
expect_failure verify_detached_signature \
|
|
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/tiny-artifact"
|
|
expect_failure verify_detached_signature \
|
|
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/tiny-artifact-tampered"
|
|
cp "$fixtures/SHASUMS256.txt.asc" "$test_tmp/bad-signature.asc"
|
|
sed -i 's/^=MJqv$/=MJqa/' "$test_tmp/bad-signature.asc"
|
|
expect_failure verify_detached_signature \
|
|
"$fixtures/fixture-key.asc" "$test_tmp/bad-signature.asc" "$fixtures/SHASUMS256.txt"
|
|
expect_failure verify_detached_signature \
|
|
"$test_tmp/combined-key.asc" "$fixtures/wrong-signer-SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
|
|
|
mkdir "$test_tmp/bin"
|
|
cat > "$test_tmp/bin/curl" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
output=''
|
|
connect_timeout=''
|
|
max_time=''
|
|
max_filesize=''
|
|
while (($#)); do
|
|
case "$1" in
|
|
--output) output="$2"; shift 2 ;;
|
|
--connect-timeout) connect_timeout="$2"; shift 2 ;;
|
|
--max-time) max_time="$2"; shift 2 ;;
|
|
--max-filesize) max_filesize="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
[[ "$connect_timeout" == 10 && "$max_time" == 600 && "$max_filesize" == "${CURL_EXPECTED_MAX_BYTES:?}" ]] || exit 65
|
|
case "${CURL_FIXTURE:?}" in
|
|
good) cp "$CURL_FIXTURE_ROOT/tiny-artifact" "$output" ;;
|
|
oversized) head -c 1025 /dev/zero > "$output" ;;
|
|
interrupted) printf 'partial' > "$output"; exit 42 ;;
|
|
*) exit 64 ;;
|
|
esac
|
|
STUB
|
|
chmod +x "$test_tmp/bin/curl"
|
|
PATH="$test_tmp/bin:$PATH"
|
|
export PATH CURL_FIXTURE_ROOT="$fixtures" CURL_EXPECTED_MAX_BYTES=1024
|
|
|
|
destination="$test_tmp/destination"
|
|
export CURL_FIXTURE=good
|
|
expect_success download_sha256 'https://fixture.invalid/good' "$tiny_sha256" 1024 "$destination"
|
|
cmp -s "$fixtures/tiny-artifact" "$destination" || fail 'verified download changed artifact bytes'
|
|
[[ ! -e "$destination.part" ]] || fail 'successful download left a part file'
|
|
|
|
export CURL_FIXTURE=good
|
|
expect_failure download_sha256 'https://fixture.invalid/uppercase-digest' \
|
|
'291BD319AE85488101E908E37FC0FA1B0DA1429BA27E10D2B391CB3F60DD44EA' 1024 \
|
|
"$test_tmp/uppercase-destination"
|
|
|
|
printf 'known-good\n' > "$destination"
|
|
export CURL_FIXTURE=good
|
|
expect_failure download_sha256 'https://fixture.invalid/bad-digest' \
|
|
'0000000000000000000000000000000000000000000000000000000000000000' 1024 "$destination"
|
|
assert_file_bytes "$destination" 'known-good'
|
|
[[ ! -e "$destination.part" ]] || fail 'bad digest left a part file'
|
|
|
|
protected="$test_tmp/protected-known-good"
|
|
printf 'known-good\n' > "$protected"
|
|
ln -s "$protected" "$destination.part"
|
|
expect_failure download_sha256 'https://fixture.invalid/symlink-part' \
|
|
'0000000000000000000000000000000000000000000000000000000000000000' 1024 "$destination"
|
|
assert_file_bytes "$protected" 'known-good'
|
|
[[ -L "$destination.part" ]] || fail 'symlinked destination part was not preserved'
|
|
rm -f -- "$destination.part"
|
|
|
|
ln "$protected" "$destination.part"
|
|
expect_failure download_sha256 'https://fixture.invalid/hard-link-part' \
|
|
'0000000000000000000000000000000000000000000000000000000000000000' 1024 "$destination"
|
|
assert_file_bytes "$protected" 'known-good'
|
|
[[ "$(stat -c %i "$protected")" == "$(stat -c %i "$destination.part")" ]] \
|
|
|| fail 'hard-linked destination part was not preserved'
|
|
rm -f -- "$destination.part"
|
|
|
|
export CURL_FIXTURE=oversized
|
|
expect_failure download_sha256 'https://fixture.invalid/oversized' "$tiny_sha256" 1024 "$destination"
|
|
assert_file_bytes "$destination" 'known-good'
|
|
[[ ! -e "$destination.part" ]] || fail 'oversized download left a part file'
|
|
|
|
export CURL_FIXTURE=interrupted
|
|
expect_failure download_sha256 'https://fixture.invalid/interrupted' "$tiny_sha256" 1024 "$destination"
|
|
assert_file_bytes "$destination" 'known-good'
|
|
[[ ! -e "$destination.part" ]] || fail 'interrupted download left a part file'
|
|
|
|
expect_success rpm_signature_matches \
|
|
"$test_tmp/signed-fixture.rpm" "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
expect_failure rpm_signature_matches \
|
|
"$test_tmp/unsigned-fixture.rpm" "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
expect_failure rpm_signature_matches \
|
|
"$test_tmp/signed-fixture.rpm" "$fixtures/fixture-key.asc" '0000000000000000000000000000000000000000'
|
|
expect_failure rpm_signature_matches \
|
|
"$test_tmp/wrong-signer-fixture.rpm" "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
expect_failure rpm_signature_matches \
|
|
"$test_tmp/wrong-signer-fixture.rpm" "$test_tmp/combined-key.asc" "$fixture_fingerprint"
|
|
expect_success load_installer_provenance "$config"
|
|
[[ "${INSTALLER_PROVENANCE[BUN_VERSION]:-}" == '1.4.0' ]] || fail 'valid provenance was not loaded'
|
|
for key_spec in \
|
|
'terra44 TERRA_FINGERPRINT' \
|
|
'claude-code CLAUDE_CODE_FINGERPRINT' \
|
|
'bun BUN_FINGERPRINT' \
|
|
'rpmfusion-free RPMFUSION_FREE_FINGERPRINT' \
|
|
'rpmfusion-nonfree RPMFUSION_NONFREE_FINGERPRINT' \
|
|
'hyprland-copr HYPRLAND_COPR_FINGERPRINT' \
|
|
'flathub FLATHUB_FINGERPRINT' \
|
|
'claude-desktop CLAUDE_DESKTOP_FINGERPRINT'; do
|
|
read -r key_file fingerprint_name <<<"$key_spec"
|
|
expect_success key_fingerprint_matches "$repo_dir/setup/provenance/keys/$key_file.asc" \
|
|
"${INSTALLER_PROVENANCE[$fingerprint_name]}"
|
|
done
|
|
|
|
[[ "$before_gnupg" == "$(snapshot "$host_gnupg")" ]] || fail 'host GPG state changed'
|
|
[[ "$before_gpg_files" == "$(snapshot_gpg_state "$host_gnupg")" ]] || fail 'host GPG state changed'
|
|
[[ "$before_rpmdb" == "$(snapshot "$host_rpmdb")" ]] || fail 'host RPM database changed'
|
|
[[ "$ambient_before" == "$(snapshot "$ambient_gnupg")" ]] || fail 'production helper created ambient GPG state'
|
|
[[ "$ambient_gpg_files_before" == "$(snapshot_gpg_state "$ambient_gnupg")" ]] \
|
|
|| fail 'production helper created or changed an ambient GPG entry'
|
|
|
|
parser_fixture="$test_tmp/installers.conf"
|
|
cp "$config" "$parser_fixture"
|
|
printf 'UNKNOWN_KEY=value\n' >> "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
cp "$config" "$parser_fixture"
|
|
printf 'BUN_VERSION=1.4.0\n' >> "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
grep -v '^NODE_VERSION=' "$config" > "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
cp "$config" "$parser_fixture"
|
|
sed -i 's/^BUN_VERSION=/ BUN_VERSION=/' "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
cp "$config" "$parser_fixture"
|
|
sed -i 's/^BUN_VERSION=.*/BUN_VERSION=$(id -u)/' "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
cp "$config" "$parser_fixture"
|
|
printf 'BUN_ARMV7_URL=https://fixture.invalid/bun\n' >> "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
printf 'package provenance contract: PASS\n'
|