Skip to content

Commit 0f3255e

Browse files
committed
fix(ci): verify TestGrid bundle provenance
Signed-off-by: Sujan Rao <sujan@nvidia.com>
1 parent 8c87f9b commit 0f3255e

3 files changed

Lines changed: 47 additions & 36 deletions

File tree

.github/workflows/evidence-ingest.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,6 @@ jobs:
299299
if gh workflow run testgrid-publish.yml \
300300
--repo "${REPO}" --ref main \
301301
-f bundle_ref="${BUNDLE_REF}" \
302-
-f source_class=uat \
303302
-f environment=prod; then
304303
echo "dispatched TestGrid Publish on main"
305304
exit 0

.github/workflows/testgrid-publish.yml

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@
2222
# until the prod WIF pool/SA exist (see aicr-testgrid Terraform apply).
2323
#
2424
# Pipeline:
25-
# 1. Accept the verified, digest-pinned OCI bundle ref from evidence ingest
26-
# 2. Validate the bundle ref format
25+
# 1. Accept and validate the digest-pinned OCI bundle ref
26+
# 2. Authenticate to GHCR and verify the signature, issuer, and UAT identity
2727
# 3. Authenticate to GCP via WIF (publish SA, groups/ prefix write-only)
28-
# 4. Log in to GHCR to pull the evidence OCI bundle
29-
# 5. Build testgrid-publish from source
30-
# 6. Pull the OCI bundle and write started/junit/finished to GCS
28+
# 4. Build testgrid-publish from source
29+
# 5. Pull the same immutable bundle and write started/junit/finished to GCS
3130
#
3231
# Auth:
3332
# WIF pool: aicr-testgrid[-<env>]-github (aicr-testgrid Terraform)
@@ -57,20 +56,13 @@ on:
5756
# Evidence ingest explicitly dispatches this workflow with the verified,
5857
# digest-pinned bundle ref. workflow_dispatch is an allowed GITHUB_TOKEN
5958
# recursion exception, unlike workflow_run chains from bot-dispatched UAT.
60-
# Manual dispatch uses the same input for testing and backfills.
59+
# Manual dispatch supports NVIDIA UAT backfills through the same verification.
6160
workflow_dispatch:
6261
inputs:
6362
bundle_ref:
64-
description: "OCI bundle ref (ghcr.io/<org>/...@sha256:...)"
63+
description: "NVIDIA UAT OCI bundle ref (ghcr.io/nvidia/...@sha256:...)"
6564
type: string
6665
required: true
67-
source_class:
68-
description: "Source class"
69-
type: choice
70-
default: "uat"
71-
options:
72-
- uat
73-
- community
7466
environment:
7567
description: "Target environment"
7668
type: choice
@@ -103,7 +95,11 @@ jobs:
10395
id-token: write # required for WIF token exchange
10496

10597
env:
106-
TG_SOURCE_CLASS: ${{ inputs.source_class || 'uat' }}
98+
# Production is stricter than staging ingest: only main/release UAT signers.
99+
FIRST_PARTY_ISSUER: 'https://token.actions.githubusercontent.com'
100+
FIRST_PARTY_IDENTITY: '^https://github\.com/NVIDIA/aicr/\.github/workflows/uat-(aws|gcp|azure|kind)\.yaml@refs/heads/(main|release/.+)$'
101+
TRUSTED_REGISTRIES: 'ghcr.io/nvidia'
102+
TG_SOURCE_CLASS: uat
107103

108104
steps:
109105
- name: Checkout
@@ -161,10 +157,8 @@ jobs:
161157
BUNDLE_REF: ${{ inputs.bundle_ref }}
162158
run: |
163159
set -euo pipefail
164-
# Validate format: ghcr.io/<org>/...@sha256:<64 hex chars>
165-
# Namespace is intentionally NOT restricted to nvidia/ — community
166-
# bundles (source_class=community) are hosted under contributor
167-
# namespaces, e.g. ghcr.io/yuanchen8911/aicr-evidence.
160+
# This production path accepts only NVIDIA UAT bundle repositories;
161+
# the provenance verification below independently pins the signer.
168162
#
169163
# Uses bash's [[ =~ ]] (whole-string match, POSIX ERE) rather than
170164
# `echo | grep -qE`, which is line-based: a value containing an
@@ -173,22 +167,38 @@ jobs:
173167
# reaches `echo >> GITHUB_OUTPUT` below and injects a second,
174168
# attacker-controlled output key. [[ =~ ]] anchors ^ and $ to the
175169
# start/end of the whole variable, so an embedded newline fails.
176-
if [[ ! "${BUNDLE_REF}" =~ ^ghcr\.io/[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9._/-]+@sha256:[0-9a-f]{64}$ ]]; then
177-
echo "::error::BUNDLE_REF '${BUNDLE_REF}' does not match expected format ghcr.io/<org>/...@sha256:<64hex>"
170+
if [[ ! "${BUNDLE_REF}" =~ ^ghcr\.io/nvidia/[A-Za-z0-9._/-]+@sha256:[0-9a-f]{64}$ ]]; then
171+
echo "::error::BUNDLE_REF '${BUNDLE_REF}' does not match expected format ghcr.io/nvidia/...@sha256:<64hex>"
178172
exit 1
179173
fi
180174
echo "bundle_ref=${BUNDLE_REF}" >> "${GITHUB_OUTPUT}"
181175
echo "Resolved bundle: ${BUNDLE_REF}"
182176
183-
# Authenticate to GCP via WIF using the testgrid publish SA.
184-
# This SA has objectCreator on gs://aicr-testgrid[-<env>]/groups/ only.
185-
#
186-
# No `if: steps.bundle.outputs.skip != 'true'` guard here (or on the
187-
# steps below): unlike the old workflow_run-triggered pointer lookup,
188-
# which had a legitimate "no evidence produced" skip path, "Resolve
189-
# bundle ref" above validates a required workflow_dispatch input and
190-
# exits 1 on failure — a failed resolve already halts the job before
191-
# these steps run, so there is no skip state left to guard against.
177+
# Authenticate to GHCR and verify provenance before exchanging GCP
178+
# credentials. The verifier rejects unsigned bundles and pins both the
179+
# Fulcio issuer and the exact NVIDIA UAT workflow identity family.
180+
- name: Login to GHCR
181+
uses: ./.github/actions/ghcr-login
182+
183+
- name: Build evidence verifier
184+
run: |
185+
set -euo pipefail
186+
GOFLAGS="-mod=vendor" go build \
187+
-o /tmp/evidence-project ./tools/evidence-project
188+
189+
- name: Verify evidence provenance
190+
env:
191+
BUNDLE_REF: ${{ steps.bundle.outputs.bundle_ref }}
192+
run: |
193+
set -euo pipefail
194+
/tmp/evidence-project \
195+
-in "${BUNDLE_REF}" \
196+
-out /tmp/testgrid-verified-evidence \
197+
--expected-issuer "${FIRST_PARTY_ISSUER}" \
198+
--expected-identity-regexp "${FIRST_PARTY_IDENTITY}" \
199+
--trusted-registry "${TRUSTED_REGISTRIES}"
200+
201+
# Exchange GCP credentials only after provenance verification succeeds.
192202
- name: Authenticate to GCP
193203
id: gcp-auth
194204
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
@@ -199,11 +209,6 @@ jobs:
199209
- name: Set up gcloud
200210
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
201211

202-
# Authenticate to GHCR so testgrid-publish can pull the evidence OCI
203-
# bundle. The packages: read permission above gates the token scope.
204-
- name: Login to GHCR
205-
uses: ./.github/actions/ghcr-login
206-
207212
# Always build from source — ensures the binary matches the current
208213
# commit and avoids integrity risks from downloading a release artifact
209214
# into a privileged WIF context. goreleaser packages testgrid-publish

docs/contributor/evidence-ingest.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ evidence:
8787
(`bundle_ref` set) on `main` or a `release/*` ref; a feature-branch UAT
8888
run or a push-triggered community/partner ingest never reaches TestGrid.
8989

90+
TestGrid treats the dispatch as a hint, not as proof of provenance. Before it
91+
exchanges GCP credentials, it independently pulls the immutable bundle and
92+
re-verifies the signature against the GitHub Actions issuer and the NVIDIA UAT
93+
workflow identity, restricted to `main` and `release/*`. Manual UAT backfills
94+
take the same path; unsigned, feature-branch, non-NVIDIA-registry, or
95+
unexpected-signer bundles fail closed.
96+
9097
Both dispatches run `needs: publish`, so they inherit the
9198
`produced == 'true'` gate — a no-op ingest (allowlist-only change, deleted
9299
pointer) never fires either.

0 commit comments

Comments
 (0)