From a0d64d72232056401c136e76052db3ea045068b9 Mon Sep 17 00:00:00 2001 From: mendezr Date: Wed, 23 Sep 2026 20:12:20 +0000 Subject: [PATCH] fix(apps): pin and sha256-verify OpenTabletDriver install fetches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes projectbluefin/common#1170. install-opentabletdriver fetched its release tarball through `curl -s` (no -f, no verification) and piped it straight into a root `sudo cp` of udev rules, then installed a user systemd unit from a moving flathub branch ref — also via `curl -s`, so an HTTP error page would have been written verbatim into $HOME/.config/systemd/user and enabled. A compromised upstream release asset or flathub packaging repo meant root-installed udev rules (RUN+= code execution) and session-level code execution. - Pin the release to v0.6.7 and verify the tarball with sha256sum before extraction; all fetches now use `curl -fsSL`. - Pin the flathub opentabletdriver.service fetch to commit 1a2a2083b8ed831df3b8b6ae3ddfe7dc21d02e01 and sha256-verify the unit before enabling it. - Make the install branch fail fast (`set -euo pipefail`) so a failed download or checksum mismatch aborts before anything is copied as root or enabled; gum's confirm/uninstall/Ctrl-C semantics are kept. - The tarball now downloads to a file and verifies before extraction, which also removes the old regex's ambiguity (it matched both the linux-x64 binary and simple tarballs and streamed both into one tar). - Add a Renovate custom regex manager so the pinned OTD_RELEASE tag stays current; the two sha256 pins are documented as manual updates in the same PR (checksum gate fails closed until then). Tests: extend tests/test_apps_just.bats — pinned-URL, sha256-gate, tampered-payload (fails closed before udev/flatpak/unit), HTTP-error (fail-closed), unit-fetched-from-pinned-commit, and sha256-before- enable ordering assertions; curl mock honours -f/-o; uninstall branch assertions unchanged. Pins are mirrored as constants in the test file and must move with the recipe. Verified the recipe end-to-end against the real upstream assets (tarball and unit both verify OK) and docs/skills and TESTING.md are updated in the same PR per AGENTS.md. Hive-Run: projectbluefin/common#1170 Hive-Plan: issue#1170/recommendation Hive-Spec: sec-check#1170 Assisted-by: openrouter/z-ai/glm-5.3-flash via Hive Signed-off-by: mendezr --- .github/renovate.json5 | 13 ++ docs/TESTING.md | 3 + .../references/renovate-and-tools.md | 10 ++ .../shared/usr/share/ublue-os/just/apps.just | 37 +++- tests/test_apps_just.bats | 159 +++++++++++++++--- 5 files changed, 188 insertions(+), 34 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index ee355b164..6eb3282af 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -29,6 +29,19 @@ ], datasourceTemplate: "github-releases", depNameTemplate: "projectbluefin/bonedigger" + }, + // OpenTabletDriver release pin in the install-opentabletdriver ujust + // recipe (projectbluefin/common#1170). Matches the OTD_RELEASE variable; + // the tarball and systemd-unit sha256s next to it are NOT managed by + // Renovate and must be updated manually in the same PR. + { + customType: "regex", + managerFilePatterns: ["/^system_files/shared/usr/share/ublue-os/just/apps\\.just$/"], + matchStrings: [ + "OTD_RELEASE\\s*=\\s*\"(?v[0-9.]+)\"" + ], + datasourceTemplate: "github-releases", + depNameTemplate: "OpenTabletDriver/OpenTabletDriver" } ], diff --git a/docs/TESTING.md b/docs/TESTING.md index dc3e3a587..01a85749d 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -100,6 +100,9 @@ Do not add exemptions for scripts with branching logic. | `tests/test_brew_preinstall.bats` | Managed Brewfile lifecycle plus user-unit ordering, resource priority, and preset delivery | | `tests/test_validate_brewfiles.bats` | Brewfile metadata validation, tap setup failures, ambiguity diagnostics, safe argument passing, and qualified wallpaper/Zed references | | `tests/test_brew_tap_trust.bats` | `apps.just`, `system.just`, `bazaar-hook` — `brew tap` + `brew trust` are separate commands; `brew tap --trust` is invalid (#814) | +| `tests/test_apps_just.bats` | `apps.just` — `install-opentabletdriver` pinned-download + sha256 gates (tampered payload, HTTP error, unit-before-enable ordering), install/uninstall branches, and `cncf` | +| `tests/test_image_repo.bats` | `usr/libexec/ublue-image-repo` — image-name/tag routing to upstream GitHub repos | +| `tests/test_shared_just.bats` | `shared.just` — `powerwash` (double confirmation) and `toggle-tpm2` recipes | ## Quality Epic diff --git a/docs/skills/ci-tooling/references/renovate-and-tools.md b/docs/skills/ci-tooling/references/renovate-and-tools.md index 4f589f07c..91dd6e37f 100644 --- a/docs/skills/ci-tooling/references/renovate-and-tools.md +++ b/docs/skills/ci-tooling/references/renovate-and-tools.md @@ -136,5 +136,15 @@ SC2207 (arrays from command output) is suppressed globally in the shellcheck ste | Binary | Source | Renovate pattern | |---|---|---| | `bonedigger` | `projectbluefin/bonedigger` GitHub releases | `BONEDIGGER_VERSION` in `system_files/bluefin/usr/share/ublue-os/just/60-bonedigger.just` | +| `opentabletdriver` | `OpenTabletDriver/OpenTabletDriver` GitHub releases | `OTD_RELEASE="v…"` in `system_files/shared/usr/share/ublue-os/just/apps.just` | When adding a new binary pinned to a specific version in a script or just file, add a corresponding regex manager entry in `renovate.json5` so the version stays current automatically. + +### Pinned release fetches with sha256 verification (projectbluefin/common#1170) + +`install-opentabletdriver` pins both fetches and verifies them with `sha256sum -c -` before anything is extracted, copied as root, or enabled: + +- the release tarball: pinned tag in `OTD_RELEASE` (Renovate-tracked above) plus a `sha256:` digest for the exact asset; +- the flathub `opentabletdriver.service` unit: pinned to a full commit SHA plus its own sha256 — never fetch a moving branch ref (`refs/heads/…`) for something that gets installed. + +**Coupling to know:** Renovate PRs update `OTD_RELEASE` only. The two hashes are not managed by Renovate — a version bump fails the recipe's checksum gate (fail-closed, never fail-open) until the hashes are updated manually in the same PR. Compute them with `sha256sum` against the new release asset and the raw file at the pinned ref. Tests in `tests/test_apps_just.bats` mirror these pins as constants and must move with them. diff --git a/system_files/shared/usr/share/ublue-os/just/apps.just b/system_files/shared/usr/share/ublue-os/just/apps.just index 9dcfc299d..37d656294 100644 --- a/system_files/shared/usr/share/ublue-os/just/apps.just +++ b/system_files/shared/usr/share/ublue-os/just/apps.just @@ -14,31 +14,52 @@ install-jetbrains-toolbox: brew install --cask ublue-os/tap/jetbrains-toolbox-linux # Install OpenTabletDriver, an open source, cross-platform, user-mode tablet driver +# +# Supply-chain pins (projectbluefin/common#1170) — update together on a bump: +# - OTD_RELEASE: the upstream release tag; Renovate keeps it current via a +# custom regex manager in .github/renovate.json5. The tarball filename is +# derived from it, so the URL stays consistent. +# - OTD_TARBALL_SHA256 / OTD_SERVICE_SHA256: sha256 of the release tarball +# (GitHub shows it on the release asset as the "sha256:" digest) and of the +# pinned systemd unit. Renovate does NOT manage these — update them +# manually in the same PR (sha256sum the new asset and raw unit file). +# - the systemd unit URL is pinned to a fixed flathub packaging commit. [group('Apps')] install-opentabletdriver: #!/usr/bin/bash - gum confirm --affirmative="Install" --negative="Uninstall" "Installer for OpenTabletDriver" - EXIT_CODE="$?" + set -euo pipefail + EXIT_CODE=0 + gum confirm --affirmative="Install" --negative="Uninstall" "Installer for OpenTabletDriver" || EXIT_CODE=$? if [ "${EXIT_CODE}" == 0 ] ; then echo "Installing OpenTabletDriver..." OTD_TMPDIR="$(mktemp -d)" - curl -s "https://api.github.com/repos/OpenTabletDriver/OpenTabletDriver/releases/latest" \ - | jq -r '.assets[] | select(.name | test("opentabletdriver.*tar.gz$")) | .browser_download_url' \ - | xargs curl -L -s \ - | tar --strip-components=1 -xvzf - -C "${OTD_TMPDIR}" + # Tag + sha256 pinned; upstream publishes no separate checksum asset. + OTD_RELEASE="v0.6.7" + OTD_TARBALL_SHA256="ab3ecfed8579864d947b2ad04c236822a109bc7c52519af7aaa36e03e99d2265" + OTD_TARBALL_URL="https://github.com/OpenTabletDriver/OpenTabletDriver/releases/download/${OTD_RELEASE}/opentabletdriver-${OTD_RELEASE#v}_linux-x64_simple.tar.gz" + curl -fsSL "${OTD_TARBALL_URL}" \ + -o "${OTD_TMPDIR}/otd.tar.gz" + echo "${OTD_TARBALL_SHA256} ${OTD_TMPDIR}/otd.tar.gz" | sha256sum -c - + tar --strip-components=1 -xzf "${OTD_TMPDIR}/otd.tar.gz" -C "${OTD_TMPDIR}" # https://opentabletdriver.net/Wiki/Documentation/RequiredPermissions - sudo cp "${OTD_TMPDIR}/etc/udev/rules.d/70-opentabletdriver.rules" /etc/udev/rules.d/71-opentabletdriver.rules + # The simple-package tarball keeps the rules file at its archive root. + sudo cp "${OTD_TMPDIR}/70-opentabletdriver.rules" /etc/udev/rules.d/71-opentabletdriver.rules echo -ne "blacklist hid_uclogic\nblacklist wacom\n" | sudo tee /etc/modprobe.d/blacklist-opentabletdriver.conf rm -rf "${OTD_TMPDIR}" flatpak --system install -y flathub net.opentabletdriver.OpenTabletDriver mkdir -p "$HOME/.config/systemd/user" - curl -s "https://raw.githubusercontent.com/flathub/net.opentabletdriver.OpenTabletDriver/refs/heads/master/scripts/opentabletdriver.service" > "$HOME/.config/systemd/user/opentabletdriver.service" + # Pinned to a fixed flathub commit; a moving branch ref would let the + # installed unit change silently at any time (projectbluefin/common#1170). + OTD_SERVICE_URL="https://raw.githubusercontent.com/flathub/net.opentabletdriver.OpenTabletDriver/1a2a2083b8ed831df3b8b6ae3ddfe7dc21d02e01/scripts/opentabletdriver.service" + OTD_SERVICE_SHA256="ef2f5c450ed1b30cd143285ee119f3cd89fc7b00e1a62ffe471dc4bdd0a1260a" + curl -fsSL "${OTD_SERVICE_URL}" -o "$HOME/.config/systemd/user/opentabletdriver.service" + echo "${OTD_SERVICE_SHA256} $HOME/.config/systemd/user/opentabletdriver.service" | sha256sum -c - systemctl --user daemon-reload systemctl enable --user --now opentabletdriver.service diff --git a/tests/test_apps_just.bats b/tests/test_apps_just.bats index 755c5b54b..2265cb964 100644 --- a/tests/test_apps_just.bats +++ b/tests/test_apps_just.bats @@ -1,12 +1,20 @@ #!/usr/bin/env bats # Tests for apps.just recipes: install-opentabletdriver and cncf. # +# install-opentabletdriver carries supply-chain pins (projectbluefin/common#1170): +# the OTD_TARBALL_* / OTD_SERVICE_* constants below mirror the URL and sha256 +# pins in the recipe and must be updated together with it on every bump. +# # Scope note: the `install-jetbrains-toolbox` and `install-asus` recipes in the # same file are not covered here yet. Their `brew tap` / `brew trust` lines are # covered by tests/test_brew_tap_trust.bats; recipe-level coverage for them is # a follow-up. APPS_JUST="${BATS_TEST_DIRNAME}/../system_files/shared/usr/share/ublue-os/just/apps.just" +OTD_TARBALL_URL="https://github.com/OpenTabletDriver/OpenTabletDriver/releases/download/v0.6.7/opentabletdriver-0.6.7_linux-x64_simple.tar.gz" +OTD_TARBALL_SHA256="ab3ecfed8579864d947b2ad04c236822a109bc7c52519af7aaa36e03e99d2265" +OTD_SERVICE_URL="https://raw.githubusercontent.com/flathub/net.opentabletdriver.OpenTabletDriver/1a2a2083b8ed831df3b8b6ae3ddfe7dc21d02e01/scripts/opentabletdriver.service" +OTD_SERVICE_SHA256="ef2f5c450ed1b30cd143285ee119f3cd89fc7b00e1a62ffe471dc4bdd0a1260a" WORKDIR="" MOCKDIR="" COMMAND_LOG="" @@ -62,26 +70,63 @@ echo "sudo $*" >> "${COMMAND_LOG}" exec "$@" MOCK - _write_mock "curl" <<'MOCK' + # The otd curl mock honours the flags the recipe passes: -f (fail mode) and +# -o (write payload to file instead of stdout). Modes set via CURL_OTD_MODE +# (default 'ok'; 'corrupt' serves a tampered tarball, 'http-error' exits 22). +_write_mock "curl" <<'MOCK' #!/usr/bin/env bash echo "curl $*" >> "${COMMAND_LOG}" +url="" +outfile="" +want_outfile=0 for arg in "$@"; do + if [[ "${want_outfile}" == 1 ]]; then + outfile="${arg}" + want_outfile=0 + continue + fi case "${arg}" in - *api.github.com/repos/OpenTabletDriver*) - cat "${MOCK_RELEASE_JSON}" - exit 0 - ;; - *opentabletdriver.service) - printf '%s\n' "MOCK-SYSTEMD-UNIT" - exit 0 - ;; + -o|--output) want_outfile=1 ;; + -*) ;; + *) url="${arg}" ;; esac done -# asset download leg: emit the prepared tarball on stdout -if [[ -n "${MOCK_OTD_TARBALL:-}" && -f "${MOCK_OTD_TARBALL}" ]]; then - cat "${MOCK_OTD_TARBALL}" +mode="${CURL_OTD_MODE:-ok}" +if [[ "${mode}" == "http-error" ]]; then + echo "curl: (22) The requested URL returned error: 500" >&2 + exit 22 fi +if [[ -n "${outfile}" ]]; then + emit() { cat > "${outfile}"; } +else + emit() { cat; } +fi +case "${url}" in + *opentabletdriver.service) + printf '%s\n' "MOCK-SYSTEMD-UNIT" | emit + ;; + *OpenTabletDriver*releases/latest*) + emit < "${MOCK_RELEASE_JSON}" + ;; + *) + # tarball download leg; 'corrupt' serves a payload that fails the + # recipe's sha256 gate + if [[ "${mode}" == "corrupt" ]]; then + printf 'tampered-payload' | emit + elif [[ -n "${MOCK_OTD_TARBALL:-}" && -f "${MOCK_OTD_TARBALL}" ]]; then + emit < "${MOCK_OTD_TARBALL}" + fi + ;; +esac exit 0 +MOCK + + # Log-and-passthrough so checksum-gate ordering is observable while the + # real verification (and its tamper behaviour) still runs. + _write_mock "sha256sum" <<'MOCK' +#!/usr/bin/env bash +echo "sha256sum $*" >> "${COMMAND_LOG}" +exec /usr/bin/sha256sum "$@" MOCK _write_mock "flatpak" <<'MOCK' @@ -120,16 +165,19 @@ MOCK } JSON - # Fixture: tarball shaped like the real release (one top-level dir that - # --strip-components=1 removes), carrying the udev rule the recipe copies. - local stage="${WORKDIR}/stage/OpenTabletDriver" - mkdir -p "${stage}/etc/udev/rules.d" - printf '%s\n' "MOCK-UDEV-RULE" > "${stage}/etc/udev/rules.d/70-opentabletdriver.rules" + # Fixture: tarball shaped like the upstream 'simple' release package (one + # top-level dir that --strip-components=1 removes, rules file at the + # archive root), carrying the udev rule the recipe copies. + local stage="${WORKDIR}/stage/opentabletdriver-Simple" + mkdir -p "${stage}" + printf '%s\n' "MOCK-UDEV-RULE" > "${stage}/70-opentabletdriver.rules" MOCK_OTD_TARBALL="${WORKDIR}/otd.tar.gz" - tar -czf "${MOCK_OTD_TARBALL}" -C "${WORKDIR}/stage" OpenTabletDriver + tar -czf "${MOCK_OTD_TARBALL}" -C "${WORKDIR}/stage" opentabletdriver-Simple + # Matches OTD_TARBALL_SHA256 so the recipe's checksum gate passes. + MOCK_OTD_TARBALL_SHA256="${OTD_TARBALL_SHA256}" # Redirect the recipe's absolute system paths into the sandbox. Anchor on a - # leading space so the "${OTD_TMPDIR}/etc/..." source path is left alone. + # leading space so the "${OTD_TMPDIR}/..." source path is left alone. FAKE_ROOT="${WORKDIR}/root" mkdir -p "${FAKE_ROOT}/etc/udev/rules.d" "${FAKE_ROOT}/etc/modprobe.d" \ "${FAKE_ROOT}/usr/share/ublue-os/homebrew" @@ -137,6 +185,17 @@ JSON -e "s| /etc/udev/rules.d| ${FAKE_ROOT}/etc/udev/rules.d|g" \ -e "s| /etc/modprobe.d| ${FAKE_ROOT}/etc/modprobe.d|g" \ "${OTD_SCRIPT}" + # The sha256 gate in the recipe uses the real release pin; substitute the + # fixture tarball's own sha256 so the sandboxed install run passes its + # checksum gate (the tampered-payload test asserts the gate still fires). + local fixture_hash + fixture_hash="$(sha256sum "${MOCK_OTD_TARBALL}" | awk '{print $1}')" + sed -i "s|${OTD_TARBALL_SHA256}|${fixture_hash}|" "${OTD_SCRIPT}" + # Same for the systemd-unit gate: the mock serves a fixture unit, so swap + # in the hash of the exact bytes the mock writes. + local fixture_service_hash + fixture_service_hash="$(printf '%s\n' "MOCK-SYSTEMD-UNIT" | sha256sum | awk '{print $1}')" + sed -i "s|${OTD_SERVICE_SHA256}|${fixture_service_hash}|" "${OTD_SCRIPT}" sed -i \ -e "s|/usr/share/ublue-os/homebrew|${FAKE_ROOT}/usr/share/ublue-os/homebrew|g" \ "${CNCF_SCRIPT}" @@ -160,6 +219,8 @@ _run_otd() { MOCK_GUM_CONFIRM_EXIT="${MOCK_GUM_CONFIRM_EXIT:-0}" \ MOCK_RELEASE_JSON="${MOCK_RELEASE_JSON}" \ MOCK_OTD_TARBALL="${MOCK_OTD_TARBALL}" \ + MOCK_OTD_TARBALL_SHA256="${MOCK_OTD_TARBALL_SHA256}" \ + CURL_OTD_MODE="${CURL_OTD_MODE:-ok}" \ bash "${OTD_SCRIPT}" } @@ -193,12 +254,37 @@ _run_cncf() { [[ "$output" != *"Uninstalling OpenTabletDriver..."* ]] } -@test "install-opentabletdriver: install selects the tar.gz asset, not the deb or rpm" { +@test "install-opentabletdriver: install fetches the pinned release tarball URL" { MOCK_GUM_CONFIRM_EXIT=0 _run_otd [ "$status" -eq 0 ] - grep -q "otd.tar.gz" "${COMMAND_LOG}" - ! grep -q "otd.deb" "${COMMAND_LOG}" - ! grep -q "otd.rpm" "${COMMAND_LOG}" + # $* in the mock collapses the recipe's quoting; the URL appears unquoted + grep -qF "curl -fsSL ${OTD_TARBALL_URL}" "${COMMAND_LOG}" +} + +@test "install-opentabletdriver: install verifies the tarball sha256 before extraction" { + MOCK_GUM_CONFIRM_EXIT=0 _run_otd + [ "$status" -eq 0 ] + local log_line + log_line="$(grep -F "sha256sum -c" "${COMMAND_LOG}" | head -1)" + [ -n "${log_line}" ] +} + +@test "install-opentabletdriver: install rejects a tampered tarball (sha256 mismatch)" { + CURL_OTD_MODE=corrupt MOCK_GUM_CONFIRM_EXIT=0 _run_otd + [ "$status" -ne 0 ] + [[ "$output" == *"WARNING: 1 computed checksum did NOT match"* ]] + # Nothing may be installed after the failed verification. + ! grep -q "sudo cp" "${COMMAND_LOG}" + ! grep -q "flatpak --system install" "${COMMAND_LOG}" + [ ! -f "${FAKE_ROOT}/etc/udev/rules.d/71-opentabletdriver.rules" ] + [ ! -f "${HOMEDIR}/.config/systemd/user/opentabletdriver.service" ] +} + +@test "install-opentabletdriver: install fetches the systemd unit from a pinned commit, not a moving branch" { + MOCK_GUM_CONFIRM_EXIT=0 _run_otd + [ "$status" -eq 0 ] + grep -qF "curl -fsSL ${OTD_SERVICE_URL}" "${COMMAND_LOG}" + ! grep -qE "refs/heads/|/master/" "${COMMAND_LOG}" } @test "install-opentabletdriver: install renames the udev rule 70- -> 71-" { @@ -209,6 +295,21 @@ _run_cncf() { grep -q "MOCK-UDEV-RULE" "${FAKE_ROOT}/etc/udev/rules.d/71-opentabletdriver.rules" } +@test "install-opentabletdriver: install writes the systemd unit only after its sha256 verifies" { + MOCK_GUM_CONFIRM_EXIT=0 _run_otd + [ "$status" -eq 0 ] + [ -f "${HOMEDIR}/.config/systemd/user/opentabletdriver.service" ] + grep -q "MOCK-SYSTEMD-UNIT" "${HOMEDIR}/.config/systemd/user/opentabletdriver.service" + # The unit sha256 gate runs against the downloaded file before enable. + grep -q "sha256sum -c" "${COMMAND_LOG}" + local last_check + last_check="$(grep -n "sha256sum -c" "${COMMAND_LOG}" | tail -1 | cut -d: -f1)" + local enable_line + enable_line="$(grep -n "systemctl enable --user --now" "${COMMAND_LOG}" | head -1 | cut -d: -f1)" + [ -n "${enable_line}" ] + [ "${last_check}" -lt "${enable_line}" ] +} + @test "install-opentabletdriver: install blacklists hid_uclogic and wacom" { MOCK_GUM_CONFIRM_EXIT=0 _run_otd [ "$status" -eq 0 ] @@ -234,15 +335,21 @@ _run_cncf() { grep -q "flatpak --system install -y flathub net.opentabletdriver.OpenTabletDriver" "${COMMAND_LOG}" } -@test "install-opentabletdriver: install writes the user service unit and enables it" { +@test "install-opentabletdriver: install enables the user service unit" { MOCK_GUM_CONFIRM_EXIT=0 _run_otd [ "$status" -eq 0 ] - [ -f "${HOMEDIR}/.config/systemd/user/opentabletdriver.service" ] - grep -q "MOCK-SYSTEMD-UNIT" "${HOMEDIR}/.config/systemd/user/opentabletdriver.service" grep -q "systemctl --user daemon-reload" "${COMMAND_LOG}" grep -q "systemctl enable --user --now opentabletdriver.service" "${COMMAND_LOG}" } +@test "install-opentabletdriver: install fails closed when curl errors (no -f would hide it)" { + CURL_OTD_MODE=http-error MOCK_GUM_CONFIRM_EXIT=0 _run_otd + [ "$status" -ne 0 ] + ! grep -q "sudo cp" "${COMMAND_LOG}" + ! grep -q "flatpak --system install" "${COMMAND_LOG}" + [ ! -f "${HOMEDIR}/.config/systemd/user/opentabletdriver.service" ] +} + # --- install-opentabletdriver: uninstall branch -------------------------- @test "install-opentabletdriver: gum confirm negative runs the uninstall branch" {