feat(slsa): verify GitHub native build-provenance attestations (#56) - #62
Merged
Merged
Conversation
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
enabled auto-merge (squash)
July 2, 2026 04:05
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-provenanceartifacts, 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 viaGET /repos/{owner}/{repo}/attestations/{digest}through the SSRF-guardedVerified.Http(GITHUB_TOKEN when present; the inline bundle is used where present —bundle_urlblobs are snappy-compressed, so verification backends fetch bundles themselves); DSSE statement extraction; the GitHub Actions builder-identity matcher.POST /verify/github-attestation(svalinn-styletokio::processwrapper aroundgh attestation verify, camelCase wire format), falling back to the localghCLI 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-gatedcheck_builder, and binds the artifact via statement subject digest vs package checksum (GitHub provenance binds via subject, not materials).:github_attestationcheck inside the 3s advisory budget. Missing attestations → skipped; verifier unreachable → info; only a cryptographically rejected attestation is an error.oci://subjects (defaultghcr.io,CHECKY_MONKEY_OCI_REGISTRIESoverride), absolute/traversal-freeartifactPathonly,kill_on_dropon the gh child, harmoniseddetails."statement"shape across both backends.mix.lockre-pin ofverisim_client(old pinned SHA vanished from the remote —deps.getwas broken for fresh clones) + theVql→Vclmodule rename it entailed; SafeExec's documented-but-broken:allowlistoption (was forwarded toSystem.cmd, which rejects unknown opts).Verification
https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunkextracts and matches, and SLSA verification grants builder trust only withbundle_verified: true→ level 2, materials bound via subject digest. Negative path (unattested artifact) degrades to{:unverified, …}without granting anything.:latestimage 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 attestedcli/clirelease artifact, overridable viaOPSM_ATTESTED_REPO/OPSM_ATTESTED_DIGESTonce an estate artifact is attested.snapquery 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