Evidence Ingest (GP2)
Evidence Ingest (GP2)
The evidence-ingest pipeline turns a published, signed recipe-evidence
bundle into the source-keyed tree that the corroboration dashboard
generator consumes. It is the bridge between the per-run attestation
bundles produced by aicr validate --emit-attestation --push (see
ADR-007) and the aggregated,
consensus view. The tree it writes is rendered and served by
dashboard-publish (GP5).
The GPn labels name stages of the evidence-corroboration pipeline (epic #1400):
GP1 signer-allowlist management (recipes/evidence/allowlist.yaml),
GP2 ingest/verify (this doc), GP3 dashboard infrastructure
(infra/evidence-dashboard), GP4 consensus/corroboration
(pkg/corroborate), and GP5 dashboard publish.
Its defining property is verify-before-count: a bundle’s signature, issuer, identity, and source registry are all checked in a step that holds no bucket-write credentials, before any of its results are recorded. Unverified evidence never reaches the tree, and contributors never hold credentials to the publish bucket.
Pipeline
The Go pieces:
pkg/evidence/project— the synthesis library. Pure and offline: given an already-verified bundle plus its verified signer claims, it derives the coordinate and signer id-hash and writes the run directory. It performs no verification and no network I/O of its own.tools/evidence-project— the CLI that wires it together: resolve the OCI ref, enforce the trusted-registry allowlist, materialize once, verify on the unpacked directory with non-empty pins (pkg/evidence/verifier), classify, then synthesize..github/workflows/evidence-ingest.yaml+.github/scripts/evidence-ingest.sh— the CI driver with two triggers (below) and a fork-safe split between the credential-free verify job and the credentialed publish job.
Triggers
- Push to
recipes/evidence/**— community and partner pointers added or refreshed in-tree. Each changed pointer is verified with the signature pinned to the signer the pointer claims; the verifier also cross-checks the certificate against that claim, so a pointer cannot lie about who signed. workflow_call/workflow_dispatchwithbundle_ref— a first-party UAT run ingests its bundle directly, by ref, with no repo commit. The signature is pinned to the NVIDIA/aicr Actions identity.uat-aws.yaml,uat-gcp.yaml,uat-azure.yaml, and — for real-siliconservice: kindrecipes on the self-hosted GPU runners —uat-kind.yaml(DC5 #1278) call this workflow as a dependentingest-evidencejob on a successful run, passing the digest-pinned ref read from the conformance step’sevidence/pointer.yaml. The nvkind lane is a sibling of the cloud lanes: dispatched by the shareduat-run.yamlfrom thekind-h100reservation row and scoped to H100 x1, single-GPU (whatever the runner physically has).
Dashboard refresh
A successful ingest lands new evidence in the bucket, so the last job,
trigger-dashboard, dispatches
evidence-dashboard-publish.yaml to
re-render the site. It runs needs: publish, so it inherits the
produced == 'true' gate — a no-op ingest (allowlist-only change, deleted
pointer) never fires it.
The push-to-main ingest path already re-triggers the dashboard through
that workflow’s own push: trigger; the dispatch exists for the
first-party UAT path, which reaches this workflow as a nested
workflow_call (uat-run → uat-{aws,gcp,azure} → evidence-ingest,
already at GitHub’s four-level reuse limit). A nested call emits no
top-level run event and cannot nest a fifth reusable workflow, so a
workflow_dispatch is the only way to start a fresh top-level dashboard
run from that depth. It needs actions: write, threaded down from the
uat-run run-* jobs through each cloud pipeline’s ingest-evidence
job.
The dashboard’s concurrency: { group: pages, cancel-in-progress: false }
serializes to one running build and one queued build, the newest dispatch
superseding the queued one — so a multi-cell nightly batch collapses its
flurry of ingests into a single coalesced rebuild rather than a backlog.
meta.json
Each run directory carries one meta.json (schema
aicr-corroboration-meta/v1). It is the authoritative coordinate
source — the directory layout is organizational only. There is no clock
and no randomness, so output is byte-identical across runs from
identical input. Fields fall into five provenance classes:
- content-verified — from the verified predicate or the verified bundle’s recipe bytes.
- certificate-verified — from the verified Fulcio certificate.
- workflow-derived — produced by the verification workflow itself (the Rekor lookup).
- allowlist-derived — the
Allowlist.Classifyverdict over the verified signer. - caller-supplied — operational metadata the ingest caller passes in; recorded as-is, not covered by the signature.
ctrf/<phase>.json reports are copied for each phase the run produced
(deployment, performance, conformance); a phase the run did not
produce is simply absent — never stubbed.
idHash (producer ↔ consumer contract)
idHash is the stable per-signer dedup key the consensus model counts
distinct values of. It is
defined once in project.SignerIDHash. The same verified signer hashes
to the same value across every recipe and run; two different signers do
not collide. This is the GP2-producer/GP4-consumer contract — do not
change the algorithm without migrating both the bucket tree and the
consumer.
Classification
project.Allowlist.Classify derives a signer’s trust tier from the
verified (issuer, identity), never a raw pointer string:
- With an allowlist loaded, the shared loader (
pkg/evidence/allowlist, the same one the GP4 consumerpkg/corroborateuses) decides: a slug or pattern match wins,allowlisted=true. - When no allowlist file is loaded (no
--allowlistflag), a built-in heuristic admits AICR’s own UAT identity (GitHub Actions OIDC +NVIDIA/aicr) asfirst-partyso it is not mislabeled. - Otherwise
community,allowlisted=false— the fail-closed default. Reported, but never counted toward consensus.
The canonical recipes/evidence/allowlist.yaml keys first-party CI signers
by an anchored identityPattern and external contributors/partners by a
one-way source slug — it deliberately does not store a cleartext
identity field (the loader rejects one, to keep personal emails out of the
repo):
identityPattern is a ^…$-anchored regex (full-string match); the loader
rejects an over-broad pattern (any wildcard left of the OIDC subject’s @ —
only the ref may vary) and overlapping entries, so the allowlist cannot
itself be used to manufacture consensus.
Both the GP2 ingest loader (pkg/evidence/project) and the GP4 consumer
(pkg/corroborate) delegate to the shared canonical loader in
pkg/evidence/allowlist (#1505), so producer and consumer parse the
identical identityPattern/source schema and classify a verified signer
identically. The shared loader rejects a cleartext identity: field, so
personal emails cannot be reintroduced by accident.
Forward limitations
- Today’s
pkg/evidence/verifierpopulates a verified signer only from an in-artifactattestation.intoto.jsonl(DSSE + Fulcio cert). The “statement-only bundle whose signer is carried in the pointer and verified against Rekor by digest” shape is not yet implemented, so such community bundles cannot be ingested — the producer fails closed rather than record an unverified signer. - Discovery walks the per-source nested layout
recipes/evidence/<recipe>/<src>/<digest>.yaml(the<recipe>/<src>/*.yamlglob inpkg/evidence/verifier/discover.go); a flatrecipes/evidence/<recipe>.yamlis rejected as an unexpected root file. - The publish job writes to
gs://aicr-testgrid-staging/resultsusing the shared eidosx WIF service account fromuat-gcp.yaml. GP3 will replace it with a dedicatedobjectCreator-only identity scoped to that prefix.