Pin every GitHub Action to a commit SHA, and verify the nak download

Not one action reference in this repository was pinned. Every uses: line named
a mutable tag, and one named a branch. That includes the jobs holding the AUR
deploy key, the jobs with release write scope, and the packaging jobs that run
with a signing key in the environment, so whoever controls one of those action
repositories could repoint a tag into a job holding our credentials.

Sixty-two of the sixty-six references are now full commit SHAs with the
original tag kept as a trailing comment, so a reader can still tell which
release a pin is. Each SHA was resolved from the upstream peeled tag. Four
references are left unpinned and justified in one place rather than silently:
two actions select the tool they install from the ref name itself, so a bare
SHA hands them a hex string where a toolchain name belongs and the step fails.
Pinning those means moving the selection into with:, which changes what
resolves, and that is a separate decision from pinning.

A guard enforces the form on every sweep, wired into the parity job and the
local runner beside the existing checkers. It accepts only owner/repo@40-hex
with a mandatory trailing comment, treats an unreadable tree as exit 2 rather
than as a pass, and its header names what it does not cover: the actions that
pinned actions themselves invoke, the pip and cargo installs that are version
pinned at best, and anything fetched at run time.

The sharper hole was not the tags. The OpenWrt packaging workflow fetched a
helper binary straight from a release URL with no verification, in two jobs
that hold a signing key, which is code execution from a third-party host into a
credentialed job and needs nobody to retag anything. That download now goes
through a shared script with per-architecture pinned SHA-256 constants,
modelled on the zig block already in that workflow. Upstream publishes no
checksum document, so the provenance comment records the asset URL and the date
the hashes were taken by downloading rather than pretending they were verified
against a published sum.
This commit is contained in:
Johnathan Corgan
2026-08-11 15:44:07 +00:00
parent 9f82c4726e
commit d399f8e07d
10 changed files with 338 additions and 82 deletions
+105
View File
@@ -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-<arch>
# 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 <asset-url> | 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}"
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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
+27 -25
View File
@@ -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/
+6 -6
View File
@@ -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
+7 -7
View File
@@ -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
+26 -33
View File
@@ -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_<ver>_<arch>.*), 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
+6 -6
View File
@@ -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
+145
View File
@@ -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> # <tag>` — 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> # <tag>"
echo "Resolve one with:"
echo " git ls-remote https://github.com/owner/repo 'refs/tags/<tag>^{}' refs/tags/<tag>"
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
+11
View File
@@ -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