diff --git a/.github/scripts/install-nak.sh b/.github/scripts/install-nak.sh new file mode 100755 index 0000000..f04e897 --- /dev/null +++ b/.github/scripts/install-nak.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# ── Install nak, checksum-verified ────────────────────────────────────────── +# nak signs the release announcement events, and the jobs that call this script +# hand it the publishing nsec on argv. An unverified download therefore runs +# with the signing key in reach, so the binary is staged, checked against a +# pinned SHA-256, and only then installed. +# +# Called from .github/workflows/package-openwrt.yml by both the .ipk (`build`) +# and .apk (`build-apk`) jobs, which is why it lives here rather than under +# packaging/openwrt-ipk/ — that directory is the .ipk payload tree. +# +# Exit 0 = installed and verified. Any non-zero exit means nothing was +# installed. +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +NAK_VERSION="0.16.2" +INSTALL_PATH="/usr/local/bin/nak" + +ARCH=$(uname -m) +# Each arch carries the expected SHA-256 of its upstream release asset. +# +# Unlike the zig hashes in the same workflow, which come from ziglang.org's own +# https://ziglang.org/download/index.json, these are NOT upstream-attested: +# fiatjaf/nak publishes no checksum document, sidecar or SHA256SUMS alongside +# its release assets, so the only way to obtain a hash is to download the asset +# and compute it. These were derived that way on 2026-08-11 from +# https://github.com/fiatjaf/nak/releases/download/v0.16.2/nak-v0.16.2-linux- +# What the pin buys is therefore continuity, not authenticity: it detects the +# asset changing under a fixed tag, a corrupted or truncated transfer, and a +# substituted download, but it cannot attest that the bytes captured on that +# date were the bytes upstream intended. Bumping NAK_VERSION means re-deriving +# every hash below, and adding an arch means adding its hash here too. +case "$ARCH" in + x86_64|amd64) + NAK_ARCH="amd64" + NAK_SHA256="495243c070c4533ce96e98b6f34b7e97fd4be2da3353488b400233ed7ed0d4da" + ;; + aarch64|arm64) + NAK_ARCH="arm64" + NAK_SHA256="1fb8868c60ebf77dd86f90d6374ebf8557412baa37026d2844af932776085b88" + ;; + *) + echo "Unsupported architecture: $ARCH" + exit 1 + ;; +esac + +if [ -z "${NAK_SHA256:-}" ]; then + echo "No SHA-256 pinned for nak ${NAK_VERSION} on ${NAK_ARCH}." + echo "Add one to the case above, derived by downloading the asset:" + echo " curl -fsSL | sha256sum" + exit 1 +fi + +NAME="nak-v${NAK_VERSION}-linux-${NAK_ARCH}" +URL="https://github.com/fiatjaf/nak/releases/download/v${NAK_VERSION}/${NAME}" +# Stage outside the checkout so a failed attempt cannot leave a stray binary in +# the working tree, and so nothing lands at $INSTALL_PATH before it verifies. +NAK_TMP="$(mktemp -d)" +trap 'rm -rf "$NAK_TMP"' EXIT +TMP="${NAK_TMP}/${NAME}" + +# Download to a file and check it before installing. curl's own --retry does +# not cover a short read (exit 18), and a checksum mismatch needs a fresh +# download anyway, so the retry is an explicit bounded loop — the same failure +# mode that forced one on the zig step in this workflow. +verified="" +previous="" +for attempt in 1 2 3; do + rm -f "$TMP" + if curl -fsSL -o "$TMP" "$URL" && [ -s "$TMP" ]; then + actual="$(sha256sum < "$TMP" | cut -d' ' -f1)" + if [ "$actual" = "$NAK_SHA256" ]; then + echo "nak binary matches its pinned SHA-256 (${actual})" + verified=yes + break + fi + echo "nak binary failed its checksum on attempt ${attempt}:" + echo " expected ${NAK_SHA256}" + echo " actual ${actual}" + echo " size $(wc -c < "$TMP") bytes" + if [ "$actual" = "$previous" ]; then + echo "Two attempts fetched byte-identical content, so retrying is not" + echo "going to help: the pin is stale, upstream re-published, or the" + echo "source is serving the same bad file every time." + break + fi + previous="$actual" + else + echo "nak binary download failed on attempt ${attempt}" + fi + if [ "$attempt" -lt 3 ]; then + sleep $((attempt * 10)) + fi +done + +if [ -z "$verified" ]; then + echo "nak ${NAK_VERSION} (${NAK_ARCH}) did not download with its pinned" + echo "SHA-256 ${NAK_SHA256} in 3 attempts. Nothing installed at ${INSTALL_PATH}." + exit 1 +fi + +install -m 0755 "$TMP" "$INSTALL_PATH" +echo "Installed nak ${NAK_VERSION} (${NAK_ARCH}) at ${INSTALL_PATH}" diff --git a/.github/workflows/aur-publish-git.yml b/.github/workflows/aur-publish-git.yml index 8d9c47f..28f7ab4 100644 --- a/.github/workflows/aur-publish-git.yml +++ b/.github/workflows/aur-publish-git.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Patch PKGBUILD-git b2sums for local assets run: | @@ -42,7 +42,7 @@ jobs: awk '/^b2sums=\(/,/\)$/' packaging/aur/PKGBUILD-git - name: Publish to AUR - uses: KSXGitHub/github-actions-deploy-aur@v4.1.2 + uses: KSXGitHub/github-actions-deploy-aur@abe8ac26b51011c88be58c8809fd2ac674068ea5 # v4.1.2 with: pkgname: fips-git pkgbuild: packaging/aur/PKGBUILD-git diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index 286f0d9..50eadd1 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -41,7 +41,7 @@ jobs: set -euo pipefail pacman -Sy --noconfirm --needed base-devel namcap git curl - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Resolve package version id: ver @@ -176,7 +176,7 @@ jobs: echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" echo "pkgrel=${PKGREL}" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: ref: ${{ steps.tag.outputs.tag }} @@ -188,7 +188,7 @@ jobs: run: bash packaging/aur/patch-pkgbuild.sh - name: Publish to AUR - uses: KSXGitHub/github-actions-deploy-aur@v4.1.2 + uses: KSXGitHub/github-actions-deploy-aur@abe8ac26b51011c88be58c8809fd2ac674068ea5 # v4.1.2 with: pkgname: fips pkgbuild: packaging/aur/PKGBUILD diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9083806..a271b7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: name: CI parity runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install Python deps run: pip3 install --quiet pyyaml - name: Check local and GitHub runners cover the same work @@ -67,6 +67,8 @@ jobs: run: python3 testing/check-trailing-log.py - name: Check nothing resolves the shared mutable test image run: bash testing/check-image-scoping.sh + - name: Check every action is pinned to a commit SHA + run: bash testing/check-action-pins.sh # Hermetic: synthetic ping functions, no containers, ~45s. Lives beside # the other two so both runners gate on it identically — putting it in # only one would create exactly the drift check-ci-parity.sh exists to @@ -79,8 +81,8 @@ jobs: name: Format check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: components: rustfmt cache: false @@ -91,16 +93,16 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: components: clippy cache: false rustflags: '' - name: Cache Cargo registry + build - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry @@ -125,7 +127,7 @@ jobs: - os: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Set SOURCE_DATE_EPOCH from git (Unix) if: runner.os != 'Windows' @@ -147,13 +149,13 @@ jobs: run: sudo nft -c -f packaging/common/fips.nft - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: cache: false rustflags: '' - name: Cache Cargo registry + build - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry @@ -182,7 +184,7 @@ jobs: # Upload the Linux binary so integration jobs can use it without rebuilding - name: Upload Linux binary if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: fips-linux path: | @@ -203,7 +205,7 @@ jobs: runs-on: ubuntu-latest needs: [build] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Set SOURCE_DATE_EPOCH from git run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV" @@ -212,13 +214,13 @@ jobs: run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: cache: false rustflags: '' - name: Cache Cargo registry + build - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry @@ -235,7 +237,7 @@ jobs: run: cargo nextest run --all --profile ci - name: Publish test report (Checks tab) - uses: dorny/test-reporter@v2 + uses: dorny/test-reporter@df6247429542221bc30d46a036ee47af1102c451 # v2 if: always() with: name: Unit Tests @@ -244,7 +246,7 @@ jobs: fail-on-error: false - name: Publish test report (run summary) - uses: mikepenz/action-junit-report@v4 + uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() with: report_paths: target/nextest/ci/junit.xml @@ -259,19 +261,19 @@ jobs: runs-on: macos-latest needs: [build] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Set SOURCE_DATE_EPOCH from git run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV" - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: cache: false rustflags: '' - name: Cache Cargo registry + build - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry @@ -294,16 +296,16 @@ jobs: name: Unit tests (Windows) runs-on: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: cache: false rustflags: '' - name: Cache Cargo registry + build - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry @@ -332,7 +334,7 @@ jobs: name: PowerShell lint (Windows packaging) runs-on: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Run PSScriptAnalyzer shell: pwsh @@ -471,11 +473,11 @@ jobs: type: dns-resolver steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 # Fetch the pre-built Linux binary from job 1 - name: Download Linux binary - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: fips-linux path: _bin @@ -646,7 +648,7 @@ jobs: - name: Upload sim results on failure (chaos) if: matrix.type == 'chaos' && failure() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: sim-results-${{ matrix.scenario }} path: testing/chaos/sim-results/ diff --git a/.github/workflows/package-linux.yml b/.github/workflows/package-linux.yml index d7eefa6..b0d5327 100644 --- a/.github/workflows/package-linux.yml +++ b/.github/workflows/package-linux.yml @@ -19,7 +19,7 @@ jobs: outputs: linux_package_version: ${{ steps.linux_version.outputs.linux_package_version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -61,7 +61,7 @@ jobs: deb_arch: arm64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -72,14 +72,14 @@ jobs: run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libdbus-1-dev llvm - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: cache: false rustflags: '' - name: Cache Cargo registry + build if: ${{ env.ACT != 'true' }} - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry @@ -140,7 +140,7 @@ jobs: - name: Upload artifact (GitHub only) if: ${{ env.ACT != 'true' }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: fips_${{ needs.determine-versioning.outputs.linux_package_version }}_${{ matrix.artifact_arch }}_linux path: | @@ -164,7 +164,7 @@ jobs: steps: - name: Download Linux artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: dist merge-multiple: true diff --git a/.github/workflows/package-macos.yml b/.github/workflows/package-macos.yml index e9b212e..a0ad927 100644 --- a/.github/workflows/package-macos.yml +++ b/.github/workflows/package-macos.yml @@ -19,7 +19,7 @@ jobs: outputs: macos_package_version: ${{ steps.macos_version.outputs.macos_package_version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -61,7 +61,7 @@ jobs: target: x86_64-apple-darwin steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -69,14 +69,14 @@ jobs: run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV" - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: target: ${{ matrix.target }} cache: false rustflags: '' - name: Cache Cargo registry + build - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry @@ -209,7 +209,7 @@ jobs: ( cd "$(dirname "$PKG")" && shasum -a 256 "$(basename "$PKG")" | tee "$(basename "$PKG").sha256" ) - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: fips_${{ needs.determine-versioning.outputs.macos_package_version }}_${{ matrix.arch }}_macos path: | @@ -229,7 +229,7 @@ jobs: steps: - name: Download macOS artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: dist merge-multiple: true @@ -283,7 +283,7 @@ jobs: steps: - name: Download macOS artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: dist merge-multiple: true diff --git a/.github/workflows/package-openwrt.yml b/.github/workflows/package-openwrt.yml index 1552a44..c120ada 100644 --- a/.github/workflows/package-openwrt.yml +++ b/.github/workflows/package-openwrt.yml @@ -27,7 +27,7 @@ jobs: apk_version: ${{ steps.version.outputs.apk_version }} release_channel: ${{ steps.channel.outputs.release_channel }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -104,13 +104,13 @@ jobs: # x86 routers / VMs steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 - name: Install Rust toolchain (stable) if: matrix.rust_channel == 'stable' - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: target: ${{ matrix.rust_target }} cache: false @@ -124,7 +124,7 @@ jobs: - name: Cache Cargo registry + build if: ${{ env.ACT != 'true' }} - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry @@ -247,7 +247,7 @@ jobs: ls -lh out/ - name: Upload binaries artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: fips-bins-${{ matrix.openwrt_arch }} path: out/ @@ -270,7 +270,7 @@ jobs: openwrt_arch: x86_64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -283,24 +283,17 @@ jobs: echo "PACKAGE_FILENAME=$PACKAGE_FILENAME" >> $GITHUB_ENV - name: Download prebuilt binaries - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: fips-bins-${{ matrix.openwrt_arch }} path: bins + # nak receives the signing nsec on argv further down, so the download is + # staged and checked against a pinned SHA-256 before it is installed. - name: Install nak shell: bash run: | - NAK_VERSION="0.16.2" - ARCH=$(uname -m) - case "$ARCH" in - x86_64|amd64) NAK_ARCH="amd64" ;; - aarch64|arm64) NAK_ARCH="arm64" ;; - *) echo "Unsupported architecture: $ARCH"; exit 1 ;; - esac - curl -fsSL "https://github.com/fiatjaf/nak/releases/download/v${NAK_VERSION}/nak-v${NAK_VERSION}-linux-${NAK_ARCH}" \ - -o /usr/local/bin/nak - chmod +x /usr/local/bin/nak + bash .github/scripts/install-nak.sh nak --version - name: Install jq @@ -346,6 +339,13 @@ jobs: fi shellcheck --version + # Its own step, and its own shell dialect. The shipped-scripts lint below + # runs --shell=sh with the OpenWrt rc.common exclude set, which misfires + # on a bash script; install-nak.sh is also not shipped in the package. + - name: Lint install-nak.sh + shell: bash + run: shellcheck --shell=bash .github/scripts/install-nak.sh + - name: Lint shipped shell scripts shell: bash run: | @@ -529,7 +529,7 @@ jobs: - name: Upload artifact (GitHub only) if: ${{ env.ACT != 'true' }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ env.PACKAGE_FILENAME }} path: dist/${{ env.PACKAGE_FILENAME }} @@ -684,7 +684,7 @@ jobs: APK_TOOLS_COMMIT: "b5a31c0d865342ad80be10d68f1bb3d3ad9b0866" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -697,7 +697,7 @@ jobs: echo "PACKAGE_FILENAME=$PACKAGE_FILENAME" >> $GITHUB_ENV - name: Download prebuilt binaries - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: fips-bins-${{ matrix.openwrt_arch }} path: bins @@ -821,25 +821,18 @@ jobs: - name: Upload artifact (GitHub only) if: ${{ env.ACT != 'true' }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ env.PACKAGE_FILENAME }} path: dist/${{ env.PACKAGE_FILENAME }} retention-days: 30 + # nak receives the signing nsec on argv further down, so the download is + # staged and checked against a pinned SHA-256 before it is installed. - name: Install nak shell: bash run: | - NAK_VERSION="0.16.2" - ARCH=$(uname -m) - case "$ARCH" in - x86_64|amd64) NAK_ARCH="amd64" ;; - aarch64|arm64) NAK_ARCH="arm64" ;; - *) echo "Unsupported architecture: $ARCH"; exit 1 ;; - esac - curl -fsSL "https://github.com/fiatjaf/nak/releases/download/v${NAK_VERSION}/nak-v${NAK_VERSION}-linux-${NAK_ARCH}" \ - -o /usr/local/bin/nak - chmod +x /usr/local/bin/nak + bash .github/scripts/install-nak.sh nak --version - name: Install jq @@ -983,7 +976,7 @@ jobs: steps: - name: Download package artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: # Only the .ipk/.apk packages (named fips__.*), not the # fips-bins-* raw-binary artifacts shared between the build jobs. @@ -1000,7 +993,7 @@ jobs: > checksums-openwrt.txt - name: Create release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: files: | dist/*.ipk diff --git a/.github/workflows/package-windows.yml b/.github/workflows/package-windows.yml index ca13320..286d0ab 100644 --- a/.github/workflows/package-windows.yml +++ b/.github/workflows/package-windows.yml @@ -19,7 +19,7 @@ jobs: outputs: package_version: ${{ steps.version.outputs.package_version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -52,7 +52,7 @@ jobs: needs: determine-versioning steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -63,13 +63,13 @@ jobs: echo "SOURCE_DATE_EPOCH=$epoch" >> $env:GITHUB_ENV - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 with: cache: false rustflags: '' - name: Cache Cargo registry + build - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: | ~/.cargo/registry @@ -146,7 +146,7 @@ jobs: } - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: fips_${{ needs.determine-versioning.outputs.package_version }}_x86_64_windows path: deploy/fips-*-windows-*.zip @@ -170,7 +170,7 @@ jobs: steps: - name: Download Windows artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: dist merge-multiple: true diff --git a/testing/check-action-pins.sh b/testing/check-action-pins.sh new file mode 100755 index 0000000..63a9e2c --- /dev/null +++ b/testing/check-action-pins.sh @@ -0,0 +1,145 @@ +#!/bin/bash +# ── GitHub Action pinning guard ───────────────────────────────────────────── +# Every third-party action this repository invokes must be referenced by a +# 40-character commit SHA, with its human-readable tag in a trailing comment. +# +# A tag is a mutable pointer. Whoever controls an action's repository can move +# `v6` to different code at any time, and several of the jobs here are worth +# moving it for: aur-publish.yml and aur-publish-git.yml hand an action +# AUR_SSH_PRIVATE_KEY, and the OpenWrt release jobs run with HIVE_CI_NSEC in +# the environment. A SHA is content-addressed and cannot be repointed. The +# trailing comment is required rather than optional so the pin stays legible: +# a bare 40-hex string tells a reader nothing about which release it is, and a +# pin nobody can read is a pin nobody updates. +# +# What counts as a violation: any `uses:` reference that is not +# * `owner/repo@<40 hex> # ` — the required form, comment mandatory; or +# * a local action, `./path` or `docker://...`; or +# * one of the individually justified references listed below. +# +# WHAT THIS GUARD DOES NOT COVER, so a green run is not read as "the workflows +# fetch nothing unverified": +# * the actions that the pinned actions themselves invoke. Pinning +# KSXGitHub/github-actions-deploy-aur removes the retag vector; it does not +# constrain what that action does with the SSH key it is given by design +# (aur-publish.yml, aur-publish-git.yml). +# * `pip3 install --quiet pyyaml` in ci.yml's ci-parity job, which holds +# `checks: write`. Unpinned entirely, version and hash both. +# * `cargo install cargo-zigbuild --version 0.19.8 --locked` in +# package-openwrt.yml. Version-pinned, not hash-pinned. +# * anything a workflow downloads at run time. The zig tarball and the nak +# binary are SHA-256 checked in their own steps; nothing here enforces that. +# +# Exit 0 = clean. Exit 1 = an unpinned reference. Exit 2 = the guard could not +# run; never treated as a pass. +# ───────────────────────────────────────────────────────────────────────────── +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$SCRIPT_DIR/.." + +# The one accepted form for a third-party action. The comment is mandatory +# rather than optional: an optional comment would let the checker accept a pin +# it cannot describe, and a pin nobody can read is a pin nobody updates. +PINNED_RE='^[^@]+@[0-9a-f]{40} +#.*$' + +# Individually justified unpinned references. Each entry is the exact ref text. +# +# Both of these actions read the tool they install from the ref name itself +# (`github.action_ref`), so replacing the ref with a SHA hands them a 40-hex +# string where a toolchain or tool name belongs and the step fails outright. +# They are not pinnable without also moving the selection into `with:`, which +# changes which toolchain resolves, and that is a separate decision from +# pinning. Note what stays exposed: both remain repointable by their upstream +# owners. +ALLOWED_REFS=( + 'dtolnay/rust-toolchain@nightly' + 'taiki-e/install-action@nextest' +) + +if ! command -v git >/dev/null 2>&1; then + echo "check-action-pins: git not available, cannot sweep" >&2 + exit 2 +fi +if [[ ! -d "$REPO_ROOT/.github" ]]; then + echo "check-action-pins: $REPO_ROOT/.github missing, refusing to pass" >&2 + exit 2 +fi + +# Tracked files only. Workflows plus any composite/local action definition: +# a future .yaml extension and a future .github/actions/ tree both have to be +# swept, or the guard silently narrows as the repository grows. +if ! tracked="$(git -C "$REPO_ROOT" ls-files -- '.github/workflows/*.yml' '.github/workflows/*.yaml' '.github/actions/*.yml' '.github/actions/*.yaml')"; then + echo "check-action-pins: git ls-files failed, refusing to pass" >&2 + exit 2 +fi +if [[ -z "$tracked" ]]; then + echo "check-action-pins: no tracked workflow or action files, refusing to pass" >&2 + exit 2 +fi +mapfile -t files < <(printf '%s\n' "$tracked") +if [[ ${#files[@]} -eq 0 ]]; then + echo "check-action-pins: empty file list, refusing to pass" >&2 + exit 2 +fi + +# True when this ref is one of the justified references above. +allowed_ref() { + local ref="$1" entry + for entry in "${ALLOWED_REFS[@]}"; do + [[ "$ref" == "$entry" ]] && return 0 + done + return 1 +} + +violations=0 +checked=0 + +for f in "${files[@]}"; do + [[ -f "$REPO_ROOT/$f" ]] || continue + + while IFS= read -r hit; do + n="${hit%%:*}" + text="${hit#*:}" + # A commented-out step is describing a reference, not resolving it. + [[ "$text" =~ ^[[:space:]]*# ]] && continue + + # Everything after `uses:`, with surrounding whitespace and any quoting + # removed. The trailing comment is part of the ref text on purpose: + # the accepted form requires it. + ref="${text#*uses:}" + ref="${ref#"${ref%%[![:space:]]*}"}" + ref="${ref%"${ref##*[![:space:]]}"}" + + checked=$((checked + 1)) + + # A local action or a container image is not a mutable upstream tag. + [[ "$ref" == ./* ]] && continue + [[ "$ref" == docker://* ]] && continue + [[ "$ref" =~ $PINNED_RE ]] && continue + allowed_ref "$ref" && continue + + echo "$f:$n: $ref" + violations=$((violations + 1)) + done < <(grep -nE '^[[:space:]]*(- )?uses:' "$REPO_ROOT/$f" 2>/dev/null) +done + +if [[ $checked -eq 0 ]]; then + echo "check-action-pins: no uses: references found at all, refusing to pass" >&2 + exit 2 +fi + +if [[ $violations -gt 0 ]]; then + echo "" + echo "check-action-pins: $violations action reference(s) are not pinned to a commit SHA." + echo "Required form: uses: owner/repo@<40-hex-commit-sha> # " + echo "Resolve one with:" + echo " git ls-remote https://github.com/owner/repo 'refs/tags/^{}' refs/tags/" + echo "and use the peeled (^{}) SHA when the tag is annotated." + echo "A tag is a mutable pointer its owner can repoint; several of these jobs" + echo "hold a signing key or an SSH deploy key while the action runs." + exit 1 +fi + +echo "check-action-pins: all $checked action reference(s) pinned or justified" +exit 0 diff --git a/testing/ci-local.sh b/testing/ci-local.sh index 6858e5f..f3df3ab 100755 --- a/testing/ci-local.sh +++ b/testing/ci-local.sh @@ -1318,6 +1318,16 @@ run_image_scoping() { record "image-scoping" $rc } +# Every third-party action must be referenced by commit SHA. A tag is a mutable +# pointer, and the jobs holding the AUR deploy key and the release signing key +# are exactly the ones worth repointing it for. Static, and it costs nothing. +run_action_pins() { + local rc=0 + info "[action-pins] Checking that every action is pinned to a commit SHA" + "$SCRIPT_DIR/check-action-pins.sh" || rc=$? + record "action-pins" $rc +} + # Every daemon log string a test matches on must still be emitted by src/. # A stale one does not fail — it stops observing, and an expect-zero assertion # built on it then passes for the wrong reason. @@ -1372,6 +1382,7 @@ main() { run_log_strings run_trailing_log run_image_scoping + run_action_pins run_wait_converge if [[ "$TEST_ONLY" == true ]]; then