From ed5e2a159074f295f80924bd0ade729783dc80cc Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 25 Jun 2026 20:45:24 -0700 Subject: [PATCH] Verify linuxdeploy AppImage digest before use in desktop release (#6673) * Verify linuxdeploy AppImage digest before use in desktop release The desktop release workflow downloaded linuxdeploy-x86_64.AppImage from a GitHub release and ran chmod +x with no integrity check. Pinning the versioned release path is reproducibility, not integrity: a release asset can be replaced (or its delivery path compromised) after upload. The next step builds the AppImage with the Tauri signing private key and a contents:write GITHUB_TOKEN in scope, so a substituted linuxdeploy that ran during packaging could exfiltrate signing material or tamper with published release artifacts. Pin the immutable SHA-256 of the asset and verify it with sha256sum -c before chmod +x, so a mismatch fails the job closed before the binary is ever executable. Extend the existing in-workflow guard to require both the pinned digest and the verification step, so a future edit cannot silently drop the check. * Scope linuxdeploy guard to real step content, not its own text The self-check searched every workflow line, so the digest assertion was satisfied by the guard's own expectedLinuxdeployDigest line and the verification assertion by a comment. Deleting the LINUXDEPLOY_SHA256 env pin or the actual sha256sum -c command would still have passed. Match the digest against the LINUXDEPLOY_SHA256 env line specifically and require sha256sum -c on a non-comment line, so dropping either the pin or the verification now fails the guard. * Scope linuxdeploy guard to the Pin step block and check ordering The previous predicate still scanned the whole workflow, so the literal sha256sum -c in the guard's own code satisfied the verification check; a deleted or post-chmod verification command would still pass. Extract the 'Pin linuxdeploy for AppImage' step block and assert within it: the LINUXDEPLOY_SHA256 env pins the expected digest, a non-comment line runs sha256sum -c, and that verification precedes chmod +x. --- .github/workflows/release-desktop.yml | 63 +++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 6f1a7f14b1..188e078f90 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -424,6 +424,47 @@ jobs: if (!linuxdeployLines.some((line) => line.includes('1-alpha-20250213-2/linuxdeploy-x86_64.AppImage'))) { throw new Error('Desktop Linux release must pin linuxdeploy 1-alpha-20250213-2'); } + // A pinned version/path is reproducibility, not integrity: the asset + // can be replaced after upload. Require the immutable SHA-256 digest + // to be pinned AND verified before chmod +x. Scope every check to the + // real "Pin linuxdeploy for AppImage" step so this guard cannot + // satisfy itself; a file-wide scan would match the guard's own code. + const expectedLinuxdeployDigest = '4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a'; + const isComment = (line) => { + const trimmed = line.trim(); + return trimmed.startsWith('#') || trimmed.startsWith('//'); + }; + const stepStart = lines.findIndex((line) => /^\s*- name: Pin linuxdeploy for AppImage\s*$/.test(line)); + if (stepStart === -1) { + throw new Error('Desktop Linux release must keep the "Pin linuxdeploy for AppImage" step'); + } + const stepIndent = lines[stepStart].search(/\S/); + let stepEnd = lines.length; + for (let i = stepStart + 1; i < lines.length; i += 1) { + const line = lines[i]; + if (line.trim() === '') continue; + const indent = line.search(/\S/); + // The next sibling step ('- ...') at the same indent, or any dedent + // below the step, ends this step's block. + if (indent < stepIndent || (indent === stepIndent && /^\s*-\s/.test(line))) { + stepEnd = i; + break; + } + } + const stepLines = lines.slice(stepStart, stepEnd); + const digestEnvRe = /^\s*LINUXDEPLOY_SHA256:\s*["']([0-9a-f]{64})["']\s*$/; + const digestEnvLine = stepLines.find((line) => digestEnvRe.test(line)); + if (!digestEnvLine || digestEnvLine.match(digestEnvRe)[1] !== expectedLinuxdeployDigest) { + throw new Error('Desktop Linux release must pin the linuxdeploy SHA-256 digest in the LINUXDEPLOY_SHA256 env'); + } + const sha256Idx = stepLines.findIndex((line) => !isComment(line) && line.includes('sha256sum -c')); + if (sha256Idx === -1) { + throw new Error('Desktop Linux release must verify the linuxdeploy digest with sha256sum -c before use'); + } + const chmodIdx = stepLines.findIndex((line) => !isComment(line) && /chmod\s+\+x/.test(line)); + if (chmodIdx !== -1 && sha256Idx > chmodIdx) { + throw new Error('Desktop Linux release must verify the linuxdeploy digest before chmod +x'); + } const releaseBodies = []; for (let i = 0; i < lines.length; i += 1) { const match = lines[i].match(/^(\s*)releaseBody:\s*\|\s*$/); @@ -587,14 +628,28 @@ jobs: - name: Pin linuxdeploy for AppImage if: matrix.platform == 'ubuntu-22.04' shell: bash + env: + # Pinning the versioned release path is reproducibility, not + # integrity: a GitHub release asset can be replaced (or its delivery + # path compromised) after upload. The SHA-256 below is the immutable + # digest of this exact asset and is the integrity gate. If linuxdeploy + # publishes a new build under this tag, this run fails closed and the + # digest must be re-pinned deliberately. + LINUXDEPLOY_URL: "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage" + LINUXDEPLOY_SHA256: "4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a" run: | set -euo pipefail tools_dir="$RUNNER_TEMP/tauri-tools-cache/tauri" mkdir -p "$tools_dir" - curl -fsSL \ - "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage" \ - -o "$tools_dir/linuxdeploy-x86_64.AppImage" - chmod +x "$tools_dir/linuxdeploy-x86_64.AppImage" + dest="$tools_dir/linuxdeploy-x86_64.AppImage" + curl -fsSL "$LINUXDEPLOY_URL" -o "$dest" + # Verify the digest BEFORE the binary is ever marked executable. The + # next step builds the AppImage with the Tauri signing key and a + # contents:write GITHUB_TOKEN in scope, so a substituted linuxdeploy + # that ran here could exfiltrate signing material or tamper with + # published release artifacts. Fail closed on any mismatch. + echo "${LINUXDEPLOY_SHA256} ${dest}" | sha256sum -c - + chmod +x "$dest" # ── Linux: build + sign + upload ── - name: Build Linux app