From d8b10d516ed3e6a4b21510542430b24319f30cb8 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Wed, 11 Nov 2026 04:30:09 +0100 Subject: [PATCH 1/2] fix(release): verify reused Desktop archive signatures Verify the immutable published ZIP before either open or merged cask handoff. Keep the CLI cask independent, and exercise real macOS resource seals in required CI. Part of #8305 --- .../scripts/cask-pr-handoff-workflow.test.sh | 34 ++++++++ .../verify-published-desktop-archive.sh | 44 +++++++++++ .../verify-published-desktop-archive.test.sh | 79 +++++++++++++++++++ .github/workflows/cd.yaml | 7 ++ .github/workflows/ci.yaml | 23 ++++++ 5 files changed, 187 insertions(+) create mode 100755 .github/scripts/verify-published-desktop-archive.sh create mode 100755 .github/scripts/verify-published-desktop-archive.test.sh diff --git a/.github/scripts/cask-pr-handoff-workflow.test.sh b/.github/scripts/cask-pr-handoff-workflow.test.sh index 8773e9f53b..663c3ee98b 100755 --- a/.github/scripts/cask-pr-handoff-workflow.test.sh +++ b/.github/scripts/cask-pr-handoff-workflow.test.sh @@ -70,6 +70,40 @@ assert_not_contains() { fi } +# Run the real loop up to PR discovery. An invalid published Desktop archive must +# never reach either the open-PR handoff or the already-merged recovery path, while +# the independently verified CLI cask remains deliverable. +archive_gate="${tmp_dir}/archive-gate.sh" +awk ' + /^ for name in ksail ksail-desktop; do/ { in_loop = 1 } + in_loop && /^ matches=/ { exit } + in_loop { line = $0; sub(/^ /, "", line); print line } + END { print " printf '\''delivered:%s\\n'\'' \"$name\""; print "done" } +' "${homebrew_block}" >"${archive_gate}" +gate_root="${tmp_dir}/archive-gate" +mkdir -p "${gate_root}/.github/scripts" +cat >"${gate_root}/.github/scripts/verify-published-desktop-archive.sh" <<'EOF' +#!/usr/bin/env bash +printf 'BLOCKED: published Desktop bundle has an invalid resource seal\n' >&2 +exit 1 +EOF +chmod +x "${gate_root}/.github/scripts/verify-published-desktop-archive.sh" +( + cd "${gate_root}" + TAG=v7.212.40 TAP=devantler-tech/homebrew-tap handoff_failed=0 + GITHUB_STEP_SUMMARY="${tmp_dir}/archive-gate-summary" + # shellcheck source=/dev/null + source "${archive_gate}" + [[ "${handoff_failed}" -eq 1 ]] || fail 'invalid Desktop archive must make the handoff fail' +) >"${tmp_dir}/archive-gate.log" 2>&1 || { + cat "${tmp_dir}/archive-gate.log" >&2 + fail 'invalid published Desktop archive reached cask handoff' +} +grep -Fxq 'delivered:ksail' "${tmp_dir}/archive-gate.log" || fail 'Desktop rejection blocked the CLI cask' +if grep -Fxq 'delivered:ksail-desktop' "${tmp_dir}/archive-gate.log"; then + fail 'invalid published Desktop archive reached open or merged PR recovery' +fi + race_root="${tmp_dir}/merged-race" mkdir -p "${race_root}/.github/scripts" "${race_root}/evidence" cat >"${race_root}/.github/scripts/find-merged-cask-pr-handoff.sh" <<'EOF' diff --git a/.github/scripts/verify-published-desktop-archive.sh b/.github/scripts/verify-published-desktop-archive.sh new file mode 100755 index 0000000000..605595a5c5 --- /dev/null +++ b/.github/scripts/verify-published-desktop-archive.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Verify the immutable Desktop download before delivering or recovering its cask. +set -euo pipefail + +if [[ $# != 1 || ! "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then + printf 'Usage: verify-published-desktop-archive.sh TAG\n' >&2 + exit 2 +fi + +tag="$1" +asset="KSail_${tag#v}_darwin_arm64.zip" +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export GH_TOKEN="${KSAIL_CASK_READ_TOKEN:-${GH_TOKEN:-}}" + +"${script_dir}/cask-read-api.sh" "repos/devantler-tech/ksail/releases/tags/${tag}" >"${work}/release.json" +if ! digest="$(jq -er --arg tag "$tag" --arg asset "$asset" ' + select(.tag_name == $tag and .draft == false and .immutable == true) + | select(.published_at | type == "string" and length > 0) + | [.assets[] | select(.name == $asset)] + | select(length == 1) | .[0] + | select(.size | type == "number" and . > 0 and floor == .) + | .digest | select(type == "string" and test("^sha256:[0-9a-f]{64}$")) +' "${work}/release.json")"; then + printf 'BLOCKED: immutable published Desktop asset evidence is incomplete\n' >&2 + exit 1 +fi + +# The destination is fixed by the product and validated tag, never by response text. +archive="${work}/desktop.zip" +curl --fail --location --silent --show-error --max-time 180 \ + --proto '=https' --proto-redir '=https' \ + --output "$archive" \ + "https://github.com/devantler-tech/ksail/releases/download/${tag}/${asset}" +actual="$(shasum -a 256 "$archive" | awk '{print $1}')" +if [[ "sha256:${actual}" != "$digest" ]]; then + printf 'BLOCKED: downloaded Desktop archive does not match its published digest\n' >&2 + exit 1 +fi + +bash "${repo_root}/desktop/scripts/verify-macos-archive.sh" "$archive" +printf 'PASS: published Desktop archive %s has a valid resource seal (%s)\n' "$tag" "$actual" diff --git a/.github/scripts/verify-published-desktop-archive.test.sh b/.github/scripts/verify-published-desktop-archive.test.sh new file mode 100755 index 0000000000..1224bc1669 --- /dev/null +++ b/.github/scripts/verify-published-desktop-archive.test.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Exercise publication recovery with real macOS resource seals and ZIP extraction. +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +[[ "$(uname -s)" == Darwin ]] || exit 2 +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/KSail.app/Contents/MacOS" "$work/KSail.app/Contents/Resources" "$work/bin" +cp /usr/bin/true "$work/KSail.app/Contents/MacOS/KSail" +cat >"$work/KSail.app/Contents/Info.plist" <<'EOF' + + +CFBundleExecutableKSail +CFBundleIdentifiertech.devantler.ksail.signature-fixture +CFBundlePackageTypeAPPL + +EOF +printf 'Original license\n' >"$work/KSail.app/Contents/Resources/LICENSE" +codesign --force --sign - --timestamp=none "$work/KSail.app" 2>"$work/sign.log" +ditto -c -k --keepParent "$work/KSail.app" "$work/valid.zip" +printf 'Changed after signing\n' >"$work/KSail.app/Contents/Resources/LICENSE" +ditto -c -k --keepParent "$work/KSail.app" "$work/invalid.zip" + +# Replace transport only; keep the real digest, extraction and signature checks. +cat >"$work/bin/gh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +[[ "$*" == 'api repos/devantler-tech/ksail/releases/tags/v7.212.40' ]] || exit 2 +cat "$RELEASE_FIXTURE" +EOF +cat >"$work/bin/curl" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +output="" url="" +while (($#)); do + case "$1" in + --output) output="$2"; shift 2 ;; + https://*) url="$1"; shift ;; + *) shift ;; + esac +done +[[ "$url" == https://github.com/devantler-tech/ksail/releases/download/v7.212.40/KSail_7.212.40_darwin_arm64.zip && -n "$output" ]] || exit 2 +cp "$ARCHIVE_FIXTURE" "$output" +EOF +chmod +x "$work/bin/gh" "$work/bin/curl" +export PATH="$work/bin:$PATH" +export RELEASE_FIXTURE="$work/release.json" ARCHIVE_FIXTURE="$work/valid.zip" + +# Describe the actual fixture publication without using production parsing code. +publication() { + local digest + digest="$(shasum -a 256 "$1" | awk '{print $1}')" + jq -n --arg digest "sha256:$digest" '{tag_name:"v7.212.40",draft:false, + immutable:true,published_at:"2026-11-11T00:00:00Z", + assets:[{name:"KSail_7.212.40_darwin_arm64.zip",size:1,digest:$digest}]}' >"$RELEASE_FIXTURE" +} + +publication "$ARCHIVE_FIXTURE" +bash "${script_dir}/verify-published-desktop-archive.sh" v7.212.40 >"$work/valid.log" 2>&1 +grep -q '^PASS: published Desktop archive' "$work/valid.log" +printf 'PASS: valid immutable publication\n' + +ARCHIVE_FIXTURE="$work/invalid.zip" +if bash "${script_dir}/verify-published-desktop-archive.sh" v7.212.40 >"$work/digest.log" 2>&1; then + printf 'FAIL: download with a different checksum was accepted\n' >&2 + exit 1 +fi +grep -q 'does not match its published digest' "$work/digest.log" +printf 'PASS: changed download rejected\n' + +# Even a checksum-matching publication must fail when its signed resources changed. +publication "$ARCHIVE_FIXTURE" +if bash "${script_dir}/verify-published-desktop-archive.sh" v7.212.40 >"$work/seal.log" 2>&1; then + printf 'FAIL: checksum-matching invalid resource seal was accepted\n' >&2 + exit 1 +fi +grep -q 'sealed resource is missing or invalid' "$work/seal.log" +printf 'PASS: checksum-matching invalid resource seal rejected\n' diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 8e618c9378..041306a8cd 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -1065,6 +1065,13 @@ jobs: for name in ksail ksail-desktop; do branch="goreleaser/${name}" + # Check the actual immutable download before either open-PR delivery or + # merged-PR recovery. A checksum match alone cannot prove a valid seal. + if [[ "$name" == ksail-desktop ]] && ! .github/scripts/verify-published-desktop-archive.sh "$TAG"; then + echo "::error::Published Desktop archive failed signature verification; leaving its cask untouched" + handoff_failed=1 + continue + fi # `|| true` keeps the job fail-soft: a transient API failure (5xx, rate limit, # network blip) must not abort the step under `set -e` after the release is already public — # invalid evidence below then warns and skips, same as the no-PR-found case. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cb07761fb6..4ea4a3b4f6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -452,6 +452,9 @@ jobs: - 'pkg/svc/installer/cni/calico/Dockerfile' - '.github/workflows/ci.yaml' cask-pr-handoff: + - '.github/scripts/verify-published-desktop-archive.sh' + - '.github/scripts/verify-published-desktop-archive.test.sh' + - 'desktop/scripts/verify-macos-archive.sh' - '.github/scripts/cask-read-api.sh' - '.github/scripts/cask-read-api.test.sh' - 'server.json' @@ -725,6 +728,22 @@ jobs: .github/actions/ksail-test-workload/remote-resource-guard.test.sh .github/actions/ksail-test-workload/install-with-recovery.test.sh + desktop-recovery-signature: + name: 🔐 Verify Published Desktop Signature + runs-on: macos-latest + timeout-minutes: 5 + needs: [changes] + if: needs.changes.outputs.cask-pr-handoff == 'true' + permissions: + contents: read + steps: + - name: 📄 Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Verify real resource seals in publication recovery + run: bash .github/scripts/verify-published-desktop-archive.test.sh + cask-pr-handoff: name: 🔐 Validate Cask PR Handoff runs-on: ubuntu-latest @@ -763,6 +782,8 @@ jobs: .github/scripts/upload-release-assets.test.sh \ .github/scripts/validate-cask-pr-handoff.sh \ .github/scripts/validate-cask-pr-handoff.test.sh \ + .github/scripts/verify-published-desktop-archive.sh \ + .github/scripts/verify-published-desktop-archive.test.sh \ .github/scripts/wait-for-github-api-budget.sh \ .github/scripts/wait-for-github-api-budget.test.sh \ .github/scripts/cask-pr-handoff-workflow.test.sh @@ -3032,6 +3053,7 @@ jobs: test-cask-rendering, release-graph-audit, cask-pr-handoff, + desktop-recovery-signature, native-home-paths, windows-notice-staging, eks-smoke-scripts, @@ -3079,6 +3101,7 @@ jobs: ${{ needs.test-cask-rendering.result }} ${{ needs.release-graph-audit.result }} ${{ needs.cask-pr-handoff.result }} + ${{ needs.desktop-recovery-signature.result }} ${{ needs.native-home-paths.result }} ${{ needs.windows-notice-staging.result }} ${{ needs.eks-smoke-scripts.result }} From fb90c896613ca2010e2c4d68be1a9ebb149fba97 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Wed, 11 Nov 2026 04:54:30 +0100 Subject: [PATCH 2/2] test(release): refresh the audited handoff configuration Re-establish the shipped dependency graph verdict after the post-publication Desktop signature gate. The full Go suite passes with pinned action commits required. Part of #8305 --- pkg/client/kubescape/release_graph_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/client/kubescape/release_graph_test.go b/pkg/client/kubescape/release_graph_test.go index 5a1f9f4a9b..c0c2d5106b 100644 --- a/pkg/client/kubescape/release_graph_test.go +++ b/pkg/client/kubescape/release_graph_test.go @@ -422,7 +422,7 @@ func auditedReleaseConfigDigests() map[string]string { ".github/actions/release-admission/action.yaml": "a8ac217c7e2195fee64b364748a6724b99aa0aa4a9988127084f1d6b45953f0c", "scripts/stage-webui.sh": "56f99977b9e5026eedd693e59da4e0cd82d0167e94d0ce4b9c7bb5ce2cd8ed3c", ".github/actions/free-disk-space/free-disk-space.sh": "2dd12fcf3779137ca1cb5f21194947f6a10d3f94f8fb438a1a29ec9021fbdc30", - ".github/workflows/cd.yaml#without-uses": "04ab2010cd4425e47ccadbff1dbadf421e496120b3961dd72496bc0451bb4f1a", + ".github/workflows/cd.yaml#without-uses": "7235813459ca01e063df01e523341533ed62c8f7c4c29428a08efbcff2f20206", } }