Skip to content

feat(slsa): verify GitHub native build-provenance attestations (#56) - #62

Merged
hyperpolymath merged 3 commits into
mainfrom
feat/github-attestation-verification
Jul 2, 2026
Merged

feat(slsa): verify GitHub native build-provenance attestations (#56)#62
hyperpolymath merged 3 commits into
mainfrom
feat/github-attestation-verification

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Closes #56 — the consumer-side counterpart to the estate's 2026-06-25 producer attestation rollout.

What

OPSM can now fetch and cryptographically verify the Sigstore bundles GitHub stores for actions/attest-build-provenance artifacts, and treats the GitHub Actions builder identity as trusted only after the bundle verifies — the builder string alone never confers trust.

  • Opsm.Slsa.GithubAttestation (new): attestation lookup via GET /repos/{owner}/{repo}/attestations/{digest} through the SSRF-guarded Verified.Http (GITHUB_TOKEN when present; the inline bundle is used where present — bundle_url blobs are snappy-compressed, so verification backends fetch bundles themselves); DSSE statement extraction; the GitHub Actions builder-identity matcher.
  • Verification backends: checky-monkey's new POST /verify/github-attestation (svalinn-style tokio::process wrapper around gh attestation verify, camelCase wire format), falling back to the local gh CLI via SafeExec. Fail-open: unreachable verifiers degrade to "unverified", and only a clear verification failure counts as a definitive rejection (operational gh errors — auth, network — are never misreported as rejections).
  • Provenance.verify_github_attestation/2: maps the verified statement into the existing SLSA verify flow with a verification-gated check_builder, and binds the artifact via statement subject digest vs package checksum (GitHub provenance binds via subject, not materials).
  • Trust pipeline: new parallel :github_attestation check inside the 3s advisory budget. Missing attestations → skipped; verifier unreachable → info; only a cryptographically rejected attestation is an error.
  • Hardening from the adversarial review: SSRF registry allowlist for oci:// subjects (default ghcr.io, CHECKY_MONKEY_OCI_REGISTRIES override), absolute/traversal-free artifactPath only, kill_on_drop on the gh child, harmonised details."statement" shape across both backends.
  • Repo fixes en route: mix.lock re-pin of verisim_client (old pinned SHA vanished from the remote — deps.get was broken for fresh clones) + the VqlVcl module rename it entailed; SafeExec's documented-but-broken :allowlist option (was forwarded to System.cmd, which rejects unknown opts).

Verification

  • Live acceptance (issue's criterion, run through the OPSM code path, not raw gh): fetch finds the attestation record, the gh backend cryptographically verifies the bundle, the builder identity https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk extracts and matches, and SLSA verification grants builder trust only with bundle_verified: true → level 2, materials bound via subject digest. Negative path (unattested artifact) degrades to {:unverified, …} without granting anything.
  • Estate-artifact caveat: no estate :latest image carries a bundle yet (nothing has been rebuilt since the producer rollout; probed echidna/svalinn/palimpsest-license/boj-server/checky-monkey). The live test therefore defaults to the immutable attested cli/cli release artifact, overridable via OPSM_ATTESTED_REPO/OPSM_ATTESTED_DIGEST once an estate artifact is attested.
  • Tests: 21 new Elixir unit tests (builder matcher, DSSE extraction, gh-output decoding, trust gating, subject binding, pipeline integration — all offline) + tagged live contract tests; 4 Rust tests (arg validation, statement extraction, registry guard). Full suite: 952 tests, 1 pre-existing environmental failure (snap query hangs in WSL, unrelated). Changed files compile warning-clean; the diff was reviewed by a 4-lens adversarial workflow and all confirmed findings are fixed in the second commit.

🤖 Generated with Claude Code

hyperpolymath and others added 2 commits July 2, 2026 04:36
Consumer-side counterpart to the estate's 2026-06-25 producer rollout of
actions/attest-build-provenance. New Opsm.Slsa.GithubAttestation module:

- fetch_attestations/3: GET /repos/{owner}/{repo}/attestations/{digest}
  via Verified.Http (SSRF-guarded) with GITHUB_TOKEN when present.
  Returns attestation records; the inline bundle is used when present
  (bundle_url blobs are snappy-compressed, so verification backends
  fetch bundles themselves).
- verify_subject/4: cryptographic bundle verification via checky-monkey's
  new POST /verify/github-attestation endpoint (wraps gh attestation
  verify), falling back to the local gh CLI via SafeExec. Fail-open:
  unreachable verifiers degrade to :unverified, never block.
- Provenance.verify_github_attestation/2 + verification-gated builder
  trust: the GitHub Actions builder identity
  (https://github.com/<o>/<r>/.github/workflows/<wf>@<ref>) is trusted
  ONLY when the Sigstore bundle verified - the builder string alone
  never confers trust (check_builder/3 gate).
- Trust pipeline: new parallel :github_attestation check. Missing
  attestations skip; unreachable verifier is info; only a
  cryptographically REJECTED attestation is an error.
- checky-monkey (Rust): sync /verify/github-attestation endpoint,
  svalinn-style tokio::process wrapper around gh, camelCase wire format,
  operational-vs-rejection error split, owner/repo arg validation.
- SafeExec: fix latent bug - :allowlist opt was forwarded to System.cmd
  which rejects unknown options; nobody had passed the documented opt.
- Opsm.Http.post_json_get_json/3: POST that returns the parsed body
  (post_json/3 discards 2xx bodies), sharing get_json's validation.
- mix.lock: re-pin verisim_client to live verisimdb HEAD (old pinned
  SHA vanished from the remote; deps.get was broken for fresh clones).

Verified end-to-end against a live attested artifact (gh 2.92.0 release,
cli/cli): fetch finds the record, gh backend verifies the bundle,
builder id extracts + matches, SLSA verify grants builder trust only
with bundle_verified: true. Estate images at :latest carry no bundles
yet (nothing rebuilt since the producer rollout) - live test defaults
to the immutable cli/cli artifact until an estate artifact is attested.

Tests: 18 new unit tests (builder matcher, DSSE extraction, gh output
decoding, trust gating, pipeline integration) + tagged live contract
tests; 3 new Rust tests. Full suite: 950 tests, 1 pre-existing
environmental failure (snap query hangs in WSL, unrelated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes from the multi-lens review of the feature diff:

- Local gh fallback: capture stderr (gh writes errors there; without it
  every operational failure classified as empty-message rejection) and
  carve the JSON payload out of the merged stream on success (gh also
  writes "Loaded digest..." diagnostics to stderr). Conservative failure
  classification: only clear verification failures are definitive
  rejections; everything else degrades fail-open to unavailable. Same
  classifier mirrored in the Rust service.
- Subject-digest binding: GitHub-native provenance binds the artifact
  via statement.subject, not materials; verify_github_attestation/2 now
  compares subject sha256 digests to the package checksum and that
  comparison governs the materials check (mismatch warns and fails the
  binding even with a verified bundle).
- Trust pipeline: pass timeout: 2_500 so the attestation lookup fits the
  3s advisory yield_many budget instead of always timing out.
- verisimdb.ex: VeriSimClient.Vql -> .Vcl (module renamed upstream; the
  re-locked dep otherwise adds a compile warning).
- Rust service: SSRF guard - oci:// subjects restricted to allowlisted
  registries (default ghcr.io, CHECKY_MONKEY_OCI_REGISTRIES override);
  artifactPath must be absolute and traversal-free; kill_on_drop reaps
  the gh child if the 120s timeout fires; details."statement" mirrors
  the Elixir backend shape so callers read one key from either verifier.

Re-verified live against the attested cli/cli artifact after all fixes:
fetch + gh verify + builder gate + subject binding all pass (SLSA L2,
builder trusted, materials bound via checksum).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hyperpolymath
hyperpolymath merged commit ece8fa9 into main Jul 2, 2026
21 of 24 checks passed
@hyperpolymath
hyperpolymath deleted the feat/github-attestation-verification branch July 2, 2026 04:08
hyperpolymath added a commit that referenced this pull request Jul 2, 2026
…signatures (#65)

Follow-up to #62, from the adversarial review's **confirmed** high
finding (empirically reproduced by the verifier agent against cli/cli's
attested artifact with gh 2.92.0):

Genuine cryptographic rejections — certificate-identity mismatch,
subject-digest mismatch, signer-repo mismatch — all exit 1 with stderr
exactly `Error: verifying with issuer "sigstore.dev"`, which the shipped
`verif`+`fail` heuristic classified as *operational*. Result: a
rejected/forged artifact degraded to an advisory note instead of a hard
`{:error}` — the `verified: false` branch was effectively dead code on
current gh.

**Fix**: both classifiers (Elixir local-gh fallback and checky-monkey's
Rust endpoint) now match the empirically observed rejection signatures
(`verifying with issuer`, `no attestations found`) ahead of the generic
heuristic, with comments pinning them to the gh version for future
upgrades. Trust is still only ever *granted* on gh exit 0. Additionally,
"attestations exist but no verifier could check them" now surfaces as a
pipeline **warning** rather than info — the package claims provenance
nothing could confirm; still non-blocking per the advisory doctrine.

5 new signature tests (rejections definitive; operational/auth/empty
degrade fail-open; blocked CLI is unavailable). 23/23 Elixir + 4/4 Rust
tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trust pipeline: verify GitHub native build-provenance attestations (estate producer↔OPSM consumer link)

1 participant