diff --git a/.github/workflows/audit-signature-policy.yml b/.github/workflows/audit-signature-policy.yml index 0f2667e..ca46608 100644 --- a/.github/workflows/audit-signature-policy.yml +++ b/.github/workflows/audit-signature-policy.yml @@ -47,7 +47,7 @@ jobs: - name: Verify expected policy workflow identity env: - AUDIT_RECORD_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/audit-signature-policy.yml@refs/heads/main + AUDIT_RECORD_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/audit-signature-policy.yml@${{ github.ref }} run: | set -euo pipefail ./scripts/verify-audit-record.sh \ @@ -70,7 +70,7 @@ jobs: - name: Reject wrong OIDC issuer env: - AUDIT_RECORD_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/audit-signature-policy.yml@refs/heads/main + AUDIT_RECORD_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/audit-signature-policy.yml@${{ github.ref }} AUDIT_RECORD_OIDC_ISSUER: https://issuer.invalid.example run: | set -euo pipefail @@ -84,7 +84,7 @@ jobs: - name: Reject tampered signed bytes even after rechecksumming env: - AUDIT_RECORD_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/audit-signature-policy.yml@refs/heads/main + AUDIT_RECORD_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/audit-signature-policy.yml@${{ github.ref }} run: | set -euo pipefail cp /tmp/release-audit.json /tmp/release-audit.original.json diff --git a/.github/workflows/transparency-live-consumer.yml b/.github/workflows/transparency-live-consumer.yml new file mode 100644 index 0000000..013832d --- /dev/null +++ b/.github/workflows/transparency-live-consumer.yml @@ -0,0 +1,108 @@ +name: transparency-live-consumer + +on: + pull_request: + workflow_dispatch: + schedule: + - cron: '11 7 * * *' + workflow_run: + workflows: ["release-transparency"] + types: [completed] + +permissions: + contents: read + actions: read + +jobs: + policy: + if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5, Node 24 + + - name: Qualify M1.27 consumer script + shell: bash + run: | + set -euo pipefail + chmod +x scripts/verify-live-transparency.sh + sh -n scripts/verify-live-transparency.sh + if scripts/verify-live-transparency.sh >/dev/null 2>&1; then + echo 'missing arguments unexpectedly accepted' >&2 + exit 1 + fi + + - name: Install Cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 + + - name: Resolve signed transparency producer run + if: github.event_name != 'pull_request' + id: producer + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} + shell: bash + run: | + set -euo pipefail + if [[ "$EVENT_NAME" == workflow_run ]]; then + run_id="$WORKFLOW_RUN_ID" + else + run_id="$(gh run list \ + --repo "$GITHUB_REPOSITORY" \ + --workflow release-transparency.yml \ + --branch main \ + --status success \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId')" + fi + [[ "$run_id" =~ ^[0-9]+$ ]] || { echo 'unable to resolve successful release-transparency run' >&2; exit 1; } + echo "run_id=$run_id" >> "$GITHUB_OUTPUT" + + - name: Download archived M1.26 signed snapshot + if: github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + PRODUCER_RUN_ID: ${{ steps.producer.outputs.run_id }} + shell: bash + run: | + set -euo pipefail + mkdir -p /tmp/transparency-snapshot + gh run download "$PRODUCER_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name "release-transparency-$PRODUCER_RUN_ID" \ + --dir /tmp/transparency-snapshot + test -s /tmp/transparency-snapshot/release-index.json + test -s /tmp/transparency-snapshot/release-index.json.sha256 + test -s /tmp/transparency-snapshot/release-index.bundle.json + + - name: Verify M1.27 signed snapshot against current live releases + if: github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + TRANSPARENCY_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/release-transparency.yml@refs/heads/main + shell: bash + run: | + set -euo pipefail + scripts/verify-live-transparency.sh \ + /tmp/transparency-snapshot/release-index.json \ + /tmp/transparency-snapshot/release-index.json.sha256 \ + /tmp/transparency-snapshot/release-index.bundle.json + + - name: Record M1.27 qualification + shell: bash + run: | + { + echo '### M1.27 independent live transparency consumer' + echo + if [[ "$GITHUB_EVENT_NAME" == pull_request ]]; then + echo '- canonical consumer syntax and fail-closed argument handling qualified' + echo '- production snapshot verification is intentionally not performed from pull-request code' + else + echo '- archived M1.26 snapshot downloaded from a successful release-transparency producer run' + echo '- checksum, closed M1.25 schema and exact GitHub OIDC signature identity verified' + echo '- current GitHub Release assets independently reconstructed into a fresh transparency index' + echo '- signed snapshot and fresh live reconstruction required to be byte-identical' + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/transparency-signature-policy.yml b/.github/workflows/transparency-signature-policy.yml index f3f1241..1feb142 100644 --- a/.github/workflows/transparency-signature-policy.yml +++ b/.github/workflows/transparency-signature-policy.yml @@ -53,7 +53,7 @@ jobs: - name: Verify expected policy workflow identity env: - TRANSPARENCY_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/transparency-signature-policy.yml@refs/heads/main + TRANSPARENCY_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/transparency-signature-policy.yml@${{ github.ref }} run: | set -euo pipefail scripts/verify-transparency-snapshot.sh \ @@ -78,7 +78,7 @@ jobs: - name: Reject wrong OIDC issuer env: - TRANSPARENCY_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/transparency-signature-policy.yml@refs/heads/main + TRANSPARENCY_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/transparency-signature-policy.yml@${{ github.ref }} TRANSPARENCY_OIDC_ISSUER: https://issuer.invalid.example run: | set -euo pipefail @@ -93,7 +93,7 @@ jobs: - name: Reject tampered signed bytes after rechecksumming env: - TRANSPARENCY_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/transparency-signature-policy.yml@refs/heads/main + TRANSPARENCY_IDENTITY: https://github.com/Ploos-AS/gamja/.github/workflows/transparency-signature-policy.yml@${{ github.ref }} run: | set -euo pipefail cp /tmp/release-index.json /tmp/release-index.original.json diff --git a/TRANSPARENCY-CONSUMER.md b/TRANSPARENCY-CONSUMER.md new file mode 100644 index 0000000..bd7cc7b --- /dev/null +++ b/TRANSPARENCY-CONSUMER.md @@ -0,0 +1,52 @@ +# M1.27 live transparency consumer + +M1.27 adds an independent consumer path for the signed M1.26 release-transparency snapshot. + +M1.26 proves that a deterministic `release-index.json` snapshot is internally valid and was signed by the expected `release-transparency.yml@refs/heads/main` GitHub Actions OIDC identity. M1.27 additionally proves that the signed snapshot still represents the current authoritative GitHub Release asset state. + +## Canonical verification + +Download these three files from a successful `release-transparency` Actions artifact: + +- `release-index.json` +- `release-index.json.sha256` +- `release-index.bundle.json` + +Then run: + +```sh +GH_TOKEN=... ./scripts/verify-live-transparency.sh \ + release-index.json \ + release-index.json.sha256 \ + release-index.bundle.json +``` + +The verifier first delegates to the M1.26 canonical snapshot verifier. That checks canonical filenames, checksum syntax and bytes, the closed M1.25 index contract, and the Cosign/Sigstore bundle against: + +```text +https://github.com/Ploos-AS/gamja/.github/workflows/release-transparency.yml@refs/heads/main +``` + +with issuer: + +```text +https://token.actions.githubusercontent.com +``` + +It then independently calls `collect-release-index.sh` against current GitHub Releases, validates the newly reconstructed index, and requires the reconstructed JSON to be byte-identical to the signed snapshot. + +A mismatch fails closed and reports both SHA-256 values. This can indicate that release transparency state changed after the snapshot, that an indexed durable asset changed, or that the supplied snapshot does not describe the current live release state. + +## Workflow separation + +`.github/workflows/transparency-live-consumer.yml` is separate from the producer workflow. After a successful `release-transparency` run it downloads the producer's archived M1.26 artifact, verifies its signature, independently reconstructs current live release history, and requires byte equality. + +The consumer also runs on a daily schedule and manual dispatch. Scheduled/manual runs use the newest successful `release-transparency` run on `main` as the signed observation to compare against current releases. + +Pull requests qualify shell syntax and fail-closed argument handling but deliberately do not consume a production signed artifact from unmerged PR code. + +## Trust model + +M1.27 does not add a new signed object and does not make Actions artifacts authoritative. GitHub Release evidence/audit assets remain the durable source of truth. The signed M1.26 snapshot remains a reconstructable historical observation. M1.27 simply verifies that one authenticated observation is exactly reproducible from the current authoritative release state. + +This intentionally creates a useful freshness property: an older valid signed snapshot can remain cryptographically authentic while failing M1.27 if the release history has legitimately advanced. In that case a newer successful `release-transparency` snapshot is required for current-live equivalence. diff --git a/scripts/verify-live-transparency.sh b/scripts/verify-live-transparency.sh new file mode 100644 index 0000000..24cb974 --- /dev/null +++ b/scripts/verify-live-transparency.sh @@ -0,0 +1,36 @@ +#!/bin/sh +set -eu + +index=${1:-} +checksum=${2:-} +bundle=${3:-} +if [ -z "$index" ] || [ -z "$checksum" ] || [ -z "$bundle" ] || [ "${4:-}" ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +[ -n "${GH_TOKEN:-}" ] || { echo "GH_TOKEN is required for live release reconstruction" >&2; exit 2; } +command -v cmp >/dev/null 2>&1 || { echo "cmp is required" >&2; exit 2; } +command -v sha256sum >/dev/null 2>&1 || { echo "sha256sum is required" >&2; exit 2; } + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + +"$script_dir/verify-transparency-snapshot.sh" "$index" "$checksum" "$bundle" >/dev/null + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT HUP INT TERM +live="$tmp/release-index.json" + +"$script_dir/collect-release-index.sh" "$live" >/dev/null +"$script_dir/verify-release-index.sh" "$live" >/dev/null + +if ! cmp -s "$index" "$live"; then + signed_sha=$(sha256sum "$index" | awk '{print $1}') + live_sha=$(sha256sum "$live" | awk '{print $1}') + echo "signed transparency snapshot does not match current live release reconstruction" >&2 + echo "signed sha256: $signed_sha" >&2 + echo "live sha256: $live_sha" >&2 + exit 1 +fi + +printf 'M1.27 live transparency verified: sha256=%s\n' "$(sha256sum "$index" | awk '{print $1}')"