claude: feat(attest): wrangle-attest verify — exec ampel, dual-check the verdict (#484 spike, v0.5.0)#780
Draft
TomHennen wants to merge 2 commits into
Draft
claude: feat(attest): wrangle-attest verify — exec ampel, dual-check the verdict (#484 spike, v0.5.0)#780TomHennen wants to merge 2 commits into
TomHennen wants to merge 2 commits into
Conversation
… sign the VSA wrangle-attest verify evaluates the policy over one subject by exec-ing the sibling ampel binary from the attest-toolbox image, then signs the emitted VSA verbatim (the --statement signer path) and appends the signed line to the per-artifact bundle. The verdict protocol is deliberately stricter than an exit-code check: ampel v1.3.1 overloads exit 1 (FAILED verdict and every tool error) and exit 0 also covers SOFTFAIL and the empty-chain soft-pass, so the engine requires exit 0 AND an emitted VSA that parses, binds exactly the requested single-sha256 subject, and says verificationResult PASSED before anything is signed. The ampel child env is scrubbed of SIGSTORE_ID_TOKEN, ACTIONS_ID_TOKEN_REQUEST_*, and AMPEL_* (ampel resolves policy context from AMPEL_<KEY> env vars). Part of #484. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Source-verified (pkg/attest/vsa.go resultStringToSLSAResult): ampel maps both StatusPASS and StatusSOFTFAIL to verificationResult PASSED, and exits nonzero only on StatusFAIL. The dual-check therefore does NOT distinguish a SOFTFAIL from a PASS — and must not: wrangle's own wrangle-release-pinned tenet is an enforce: OFF advisory group that deliberately SOFTFAILs a branch build into a PASSED VSA. The SPEC and the test case claimed otherwise; both corrected. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
TomHennen
temporarily deployed
to
integration-test
July 12, 2026 20:02 — with
GitHub Actions
Inactive
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.
Spike of #484 with a deliberately different design than the issue proposes: exec the
ampelCLI as an in-container sibling instead of importing ampel's Go packages. Stage 1 of 2 — engine only, no shell or image change.DRAFT, targeting v0.5.0. #484 is deferred out of v0.4.0; nothing here needs to land now. This PR exists so v0.5.0 starts from evidence rather than from scratch. The verdict write-up is on #484.
Why exec, not import
#484 was deferred over three blockers, all caused by the library plan: ampel's HTML report has no public API,
pkg/attestchurned VSA semantics in a patch release, and pulling the CEL evaluator + plugins into the signing engine needed a security design pass. Exec-ing the CLI dodges all three: the CLI is ampel's stable contract, the HTML report is just ampel's stdout passed through, and the engine consumes the emitted VSA as opaque bytes (it parses only_type/predicateType/subject/predicate.verificationResult— the churny VSA internals never enter our code).ampelandwrangle-attestalready ship in the sameattest-toolboximage, so the exec needs no new dispatch.The verdict protocol (the load-bearing part)
Verified by reading ampel v1.3.1 source directly (module cache), not from docs:
ampel verifyexits 1 for BOTH a FAILED verdict and every tool error.cmd/ampel/main.godoesos.Exit(1)on any returned error;internal/cmd/verify.go:542,579doesos.Exit(1)only whenresults.GetStatus() == papi.StatusFAIL && opts.SetExitCode. The exit code alone therefore cannot distinguish "policy failed" from "ampel broke".--attest-results --attest-format=vsa --results-path=Fcallsattester.AttestToFile(...)(verify.go:527) before the exit-code check, so F is written on PASS and on FAIL, but not on a tool error (which returns early). It is an in-toto VSA statement withpredicate.verificationResult.resultsobject.--exit-codeis a bool, default true (verify.go:174).So the engine dual-checks: signing requires ampel exit 0 AND an emitted VSA that parses as in-toto, binds exactly the requested single-sha256 subject, and says
verificationResult: "PASSED". There is no text-scraping anywhere — the verdict is read from a JSON field, not from the report.One honest limitation (corrected mid-spike)
pkg/attest/vsa.goresultStringToSLSAResultmaps bothStatusPASSandStatusSOFTFAILto"PASSED", and ampel exits nonzero only onStatusFAIL. So a SOFTFAIL is indistinguishable from a PASS by exit code and by VSA. This is not a hole this design opens — it is intended ampel + wrangle semantics: wrangle's ownwrangle-release-pinnedtenet is anenforce: OFFadvisory group whose policy comment already says "a branch/SHA build SOFTFAILs here and still emits a PASSED VSA". Identical in today's shell. I initially claimed the dual-check caught SOFTFAIL; that was wrong, and the SPEC and the test case are corrected in this PR rather than left overstating the guarantee.What the dual-check does catch (none of which the current shell catches): an absent VSA, an unparseable VSA, a wrong-subject VSA, a wrong-predicateType VSA, an empty verdict, and a FAILED verdict that somehow exited 0.
The engine also strips
SIGSTORE_ID_TOKEN,ACTIONS_ID_TOKEN_REQUEST_*, andAMPEL_*from ampel's child env. TheAMPEL_*scrub is load-bearing in principle —--context-envdefaults true (verify.go:154) andpkg/context/env.goreads policy context fromAMPEL_<KEY>vars, so a stray var could steer a verdict — though the container boundary already blocks host env today, making it defense-in-depth.Upstream reuse (CLAUDE.md rule 4)
pkg/verifier: the import path is exactly what Fold ampel policy verification + VSA emission into wrangle-attest (in-process) #484 deferred (unstable post-1.0 Go API, no report API); the CLI contract above is verified at v1.3.1, our pinned build.carabiner-dev/signersigns the VSA bytes verbatim through the existing--statementpath (sharedsignAndDeliver); it retries Fulcio/Rekor/TSA itself, so the engine adds no signing retry.carabiner-dev/hasherself-digests file subjects via the existingsubjects.go.Empirically verified
Ran the real engine + real ampel v1.3.1 against the checked-in consumer fixtures (
test/consumer/fixtures/npm-vsa.intoto.jsonl+ blob) and the shippedwrangle-vsa-consumer-v1.hjsonpolicy: ampel exec'd as a sibling, the HTML report (🟢 PASS) passed through stdout,validateVSAaccepted real ampel's VSA shape (subject digest + verdict), and signing failed closed at the OIDC step exactly as designed (no ambient token locally).Tests
Hermetic Go tests with a scripted fake ampel (the exit-code × VSA verdict matrix must be deterministic offline; the real CLI contract is pinned by the stacked PR's real-binary bats test and the dispatch e2e) + the existing fake-signer seam: the fail-open guard (ampel exit 0 + FAILED VSA → exit 2, nothing signed), SOFTFAIL/empty-verdict/subject-mismatch/multi-digest/wrong-predicateType rejection, warn mode, retry semantics, env scrub, and the exact ampel argv.
Part of #484. Stacked flip: #. Relationship to #761: that PR flips only the VSA signing (
bnd statement→--statement); this ladder supersedes it if the exec-ampel design is accepted — sequencing is the owner's call.🤖 Generated with Claude Code