From 8ed95c48dc7bff0a50383a87f903d8f795f40aa0 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Wed, 19 Aug 2026 00:08:43 +0200 Subject: [PATCH 1/3] :lock: fix(release): pin cosign signer identity and issuer in install.sh (SEC-03) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hack/install.sh` ran `cosign verify-blob --bundle "$bundle" "$ARCHIVE"` with no signer pin. Keyless verification without `--certificate-identity-regexp` either errors out (cosign v2 requires the flags) or accepts ANY Fulcio certificate — so a mirror-swapped archive shipped with its own validly signed bundle verified clean and `--require-signature` promised a guarantee it did not deliver, while SECURITY.md's published copy-paste instructions pinned both flags. install.sh now carries the pair byte-identically to SECURITY.md: --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp '^https://github.com/PlatformRelay/assent/' New gate `hack/release/install_cosign_pin_test.sh`, offline (stubbed `cosign` first on PATH, fake .sigstore.json bundles, no network, no real Fulcio): * REQ-AUD2-S03-01 — both flags are inside the folded cosign invocation (comment lines excluded, so prose cannot satisfy the assertion). * REQ-AUD2-S03-02 — a bundle whose certificate identity is github.com/evil-mirror/assent fails `install.sh --require-signature` and writes no binary to --dest; likewise a foreign OIDC issuer. A matching identity still installs (positive control), and the stub log proves both flags reach cosign's argv rather than merely existing in the file. * REQ-AUD2-S03-03 — non-vacuity in both halves: deleting either flag from a temp copy reddens the static assertion, AND the un-pinned copy installs the foreign-signed archive the fixed script rejects, so the pin is load-bearing. * REQ-AUD2-S03-04 — drift gate: issuer and identity regexp are extracted from both hack/install.sh and SECURITY.md (each must yield exactly one distinct value) and compared; rewriting either file alone reddens. * REQ-AUD2-S03-05 — wired as its own `task check` stage `release-install-cosign-pin-test`, with a self-pin plus the AUD-S18 CHECK_STAGES entry so the release exit gate grades it. Out of scope, noted for follow-up: hack/release/verify-artifacts.sh:124 runs the same unpinned `cosign verify-blob --bundle` on the maintainer/CI verification path. --- Taskfile.yml | 10 + hack/audit/exitgate_test.sh | 1 + hack/install.sh | 10 +- hack/release/install_cosign_pin_test.sh | 375 ++++++++++++++++++++++++ 4 files changed, 395 insertions(+), 1 deletion(-) create mode 100755 hack/release/install_cosign_pin_test.sh diff --git a/Taskfile.yml b/Taskfile.yml index f077315a..03dd4657 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -102,6 +102,11 @@ tasks: # which is skipped on pull_request — without this a regression merges green and only # reds main afterwards. Fast and offline (stubbed `gh`); needs jq. - task: release-verify-tag-gate-test + # AUD2-S03 (SEC-03): hack/install.sh's cosign signer pin. Offline (stubbed + # `cosign`), and the only gate that reads hack/install.sh's verify path — + # without this line an unpinned `cosign verify-blob` ships silently again, + # and SECURITY.md's published issuer/identity pair can drift away from it. + - task: release-install-cosign-pin-test # D-124: the AUD-S06 docs truth-lag gates shipped green and invoked by nothing, # which is the same manual-gate defect they exist to close. Sequential (not a # `deps:` entry) — go-task runs deps in parallel and readme_smoke_test.sh builds @@ -257,6 +262,11 @@ tasks: cmds: - bash hack/release/verify_tag_gate_test.sh + release-install-cosign-pin-test: + desc: "AUD2-S03 gate: hack/install.sh pins the cosign signer identity + OIDC issuer, no drift from SECURITY.md (REQ-AUD2-S03-01..05)" + cmds: + - bash hack/release/install_cosign_pin_test.sh + audit-exitgate-test: desc: "AUD-S18 exit gate: 2026-08-06 audit conditions closed + gates green at the new bar (REQ-AUD-S18-01/02). NOT in `check` — it runs `task check` itself." cmds: diff --git a/hack/audit/exitgate_test.sh b/hack/audit/exitgate_test.sh index a97519b4..3bdde4b7 100644 --- a/hack/audit/exitgate_test.sh +++ b/hack/audit/exitgate_test.sh @@ -139,6 +139,7 @@ CHECK_STAGES=( changelog-verify release-changelog-gate-test release-verify-tag-gate-test + release-install-cosign-pin-test docs-gates lint-depguard-test lint-workflow-pins-test diff --git a/hack/install.sh b/hack/install.sh index 57073015..2ea3e636 100755 --- a/hack/install.sh +++ b/hack/install.sh @@ -111,7 +111,15 @@ done if [[ -n "$bundle" ]]; then command -v cosign >/dev/null 2>&1 || die "cosign required to verify ${bundle} but not found on PATH" - cosign verify-blob --bundle "$bundle" "$ARCHIVE" >/dev/null \ + # SEC-03 / REQ-AUD2-S03-01: pin the signer. Keyless verify-blob without an + # identity pin accepts ANY Fulcio certificate, so a mirror-swapped archive + # shipped with its own validly signed bundle would verify clean. The issuer and + # identity regexp below are byte-identical to the pair SECURITY.md publishes; + # hack/release/install_cosign_pin_test.sh reddens if the two ever drift apart. + cosign verify-blob \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp '^https://github.com/PlatformRelay/assent/' \ + --bundle "$bundle" "$ARCHIVE" >/dev/null \ || die "cosign verification failed for ${ARCHIVE}" elif [[ "$REQUIRE_SIGNATURE" -eq 1 ]]; then die "--require-signature set but no .sigstore.json bundle found beside ${ARCHIVE}" diff --git a/hack/release/install_cosign_pin_test.sh b/hack/release/install_cosign_pin_test.sh new file mode 100755 index 00000000..f7b0c826 --- /dev/null +++ b/hack/release/install_cosign_pin_test.sh @@ -0,0 +1,375 @@ +#!/usr/bin/env bash +# REQ-AUD2-S03-01..05 — SEC-03: hack/install.sh pins the cosign signer identity +# and OIDC issuer, and that pin is a real, load-bearing guarantee. +# +# Finding closed here (agent-context/PROJECT-AUDIT-2026-08-18.md, SEC-03): +# `cosign verify-blob --bundle "$bundle" "$ARCHIVE"` carried NO +# --certificate-identity-regexp / --certificate-oidc-issuer, while SECURITY.md +# publishes both in its manual copy-paste instructions. Keyless verification +# without an identity pin either errors out (cosign v2 requires the flags) or +# accepts ANY Fulcio identity — so a mirror-swapped archive shipped with its +# own validly signed bundle verifies clean and `--require-signature` promises +# something it does not deliver. +# +# ONE PUBLISHED TRUTH. The issuer/identity pair is published in SECURITY.md; +# hack/install.sh copies it byte-identically. Section 3 below is a drift gate: +# if either file is edited alone, this script reddens (REQ-AUD2-S03-04). +# +# ANTI-VACUITY DISCIPLINE (this repo has a documented history of gates that +# cannot fail — D-124, AUD-S18). Every assertion here is a FUNCTION over a file +# or a fixture, run twice: once against the real tree (must be GREEN) and once +# against a mutant carrying the very defect it exists to catch (must be RED). +# Every extraction is positive-controlled (non-empty, exactly one distinct +# value) so a pattern that silently stopped matching fails loudly instead of +# passing vacuously. +# +# OFFLINE. No network, no real Fulcio, no real cosign: section 4 puts a stub +# `cosign` first on PATH that parses the flags it was handed and matches them +# against a certificate identity carved into a fake .sigstore.json bundle. The +# stub models the PERMISSIVE pre-fix behaviour (no --certificate-identity-regexp +# => accept any identity), which is the dangerous branch of the finding: it lets +# section 5c show that deleting the flag from install.sh installs a +# foreign-signed archive, i.e. that the pin is what closes the hole. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +INSTALL="$ROOT/hack/install.sh" +SECURITY="$ROOT/SECURITY.md" +TASKFILE="$ROOT/Taskfile.yml" +AUDIT_GATE="$ROOT/hack/audit/exitgate_test.sh" +STAGE="release-install-cosign-pin-test" + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +# --------------------------------------------------------------- helpers -- + +# cosign_invocation — the `cosign verify-blob …` command with its +# backslash continuations folded onto one line. Comment lines are skipped so a +# flag mentioned only in prose can never satisfy an assertion. +cosign_invocation() { + awk ' + /^[[:space:]]*#/ { next } + !inv && /cosign verify-blob/ { inv = 1 } + inv { + line = $0 + sub(/^[[:space:]]+/, "", line) + sub(/[[:space:]]*\\$/, "", line) + printf "%s ", line + if ($0 !~ /\\[[:space:]]*$/) { printf "\n"; inv = 0 } + } + ' "$1" +} + +# extract_issuer — every distinct --certificate-oidc-issuer value. +extract_issuer() { + sed -nE "s/.*--certificate-oidc-issuer[[:space:]]+['\"]?([^'\"[:space:]]+)['\"]?.*/\1/p" "$1" | sort -u +} + +# extract_identity — every distinct --certificate-identity-regexp value. +# The value is single-quoted in both files (a shell regexp must be); an unquoted +# value extracts as nothing and trips the positive controls rather than passing. +extract_identity() { + sed -nE "s/.*--certificate-identity-regexp[[:space:]]+'([^']*)'.*/\1/p" "$1" | sort -u +} + +# one_value