Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/audit-signature-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
Expand All @@ -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
Expand Down
108 changes: 108 additions & 0 deletions .github/workflows/transparency-live-consumer.yml
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 3 additions & 3 deletions .github/workflows/transparency-signature-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
Expand All @@ -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
Expand Down
52 changes: 52 additions & 0 deletions TRANSPARENCY-CONSUMER.md
Original file line number Diff line number Diff line change
@@ -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.
36 changes: 36 additions & 0 deletions scripts/verify-live-transparency.sh
Original file line number Diff line number Diff line change
@@ -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 <release-index.json> <release-index.json.sha256> <release-index.bundle.json>" >&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}')"
Loading