Skip to content

claude: feat(verify): flip run_verify.sh to one wrangle-attest verify per subject (#484 spike, v0.5.0)#781

Draft
TomHennen wants to merge 6 commits into
mainfrom
feat/484-verify-shell-flip
Draft

claude: feat(verify): flip run_verify.sh to one wrangle-attest verify per subject (#484 spike, v0.5.0)#781
TomHennen wants to merge 6 commits into
mainfrom
feat/484-verify-shell-flip

Conversation

@TomHennen

Copy link
Copy Markdown
Owner

Stage 2 of the #484 exec-ampel spike (stacked on #780): flip actions/verify/run_verify.sh from four toolbox calls per subject (ampel verify → bnd statement, each retried, plus shell hashing/append) to one wrangle-attest verify run per subject. Net −150 lines on the flip commit; ~90 lines of shell verdict/sign plumbing become engine code that is unit-tested in Go.

DRAFT, targeting v0.5.0

#484 is deferred out of v0.4.0 — nothing here needs to land now. Additionally, the engine binary ships in the digest-pinned attest-toolbox image. Until # merges, the image republishes, and the catalog auto-bump pins the new digest, the dispatch e2e here runs an image whose engine lacks the verify subcommand and fails by design (fail-closed, exit 2 — the same shape as #761's ladder). Do not mark ready until #780 merges, the catalog bump lands, and this branch is rebased + pins re-converged.

Merge as a MERGE COMMIT, not a squash — the branch carries pin-convergence cycle commits.

What moved, what stayed

Moved into the engine (wrangle_subject_arg, wrangle_ampel_verify_args, wrangle_ampel, wrangle_verify_emit_vsa, wrangle_bnd_sign_args, wrangle_sign_vsawrangle_engine_verify): subject hashing, ampel argv, verdict handling, VSA sign, bundle append.

Stayed shell, deliberately:

The in-shell wrangle_retry_once no longer wraps the verify run: the engine retries the ampel exec itself and carabiner-dev/signer retries the Sigstore hops. Known small delta: a transient docker run/image-pull failure of this one dispatch is no longer retried in-shell (an in-shell re-run after a torn bundle append could double-append a VSA line, which is worse).

The topology change — read this before LGTM

Today ampel runs in a container without the signing token, and bnd statement signs in a separate run with it; a bats test asserted that separation. One-call-per-subject necessarily puts SIGSTORE_ID_TOKEN in the same container as the ampel child. Mitigations: the engine execs ampel with the token (and ACTIONS_ID_TOKEN_REQUEST_*, AMPEL_*) stripped from its env — asserted by Go test — and the mint-anything request vars still never enter any container (bats-asserted). Residual exposure, stated plainly: same-UID processes share /proc, so a compromised ampel could still read the engine's environ. Weighing it: ampel is our own digest-pinned build and already is the verdict oracle — a compromised ampel can already emit a PASSED VSA that we then sign — so the marginal gain for an attacker is signing arbitrary statements as the workflow identity during the token's short validity. Real but bounded. If that widening is unacceptable, the fallback that keeps this spike's verdict-protocol gains is a two-run split (wrangle-attest verify --no-sign token-less + the existing --statement --append with the token, i.e. #761's shape) at the cost of the one-call collapse.

Fail-closed accounting (old abort path → new home)

Today aborts on Now
unhashable/missing file subject engine --artifact self-digest fails closed (Go test)
malformed arg vector guard engine flag validation, pre-exec (Go table test)
ampel nonzero exit engine exit 1/2 → set -e abort (bats + Go)
bnd sign failure / empty output engine sign fail-closed + empty-line guard (Go; shared with --statement)
missing/empty attest bundle unchanged shell check + engine --bundle precheck (bats + Go)
store/referrer push failure unchanged shell (bats)
not checked today: ampel exit 0 with an absent / unparseable / subject-mismatched / wrong-predicate / empty-verdict / FAILED VSA new: fails closed (Go verdict-matrix tests)
a SOFTFAIL (enforce: OFF advisory group) unchanged — still a PASSED VSA. ampel maps SOFTFAIL→PASSED and exits 0; wrangle's own wrangle-release-pinned tenet relies on this. The dual-check does not and must not catch it.

Every abort is now also classified: engine exit 1 = policy verdict FAILED, 2 = anything else — ampel itself cannot distinguish these on exit code (it exits 1 for both); the engine tells them apart by the emitted VSA, which ampel writes on FAIL but not on error.

Tests

79 → 70 bats (the deleted ones are subject-hash/bnd-argv tests whose logic moved to Go, covered by 14 new engine test functions); the full pinned-container suite (./test.sh quick) is 1398 ok / 0 not ok, counted with grep -c, not read off a tail. New real-binary integration bats drives the real engine into the real ampel over the exec seam. The token-threading assertions (name-only, no value on argv, no request vars in-container, grant/mint fail-closed, VSA-gate fail-closed) all carry over against wrangle_engine_verify.

An independent adversarial review (fresh context, briefed to find a failed-verify-read-as-pass path) ran before this PR was surfaced; findings: no fail-open found. Two of its three findings are fixed here (a restored host-side non-empty guard on the signed VSA — jq -c exits 0 on empty input, so the old guard was NOT replaced as I had claimed; and an overclaiming retry comment). The third is surfaced below.

Relationship to #761: this supersedes its run_verify.sh change (the sign flip is subsumed by the engine call); the workflow-comment sweeps overlap and will conflict textually — sequencing is the owner's call.

Part of #484 / #485 / #420.

🤖 Generated with Claude Code

review and others added 6 commits July 12, 2026 19:21
… 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]>
…ject (#484)

wrangle_subject_arg, wrangle_ampel_verify_args, wrangle_ampel,
wrangle_verify_emit_vsa, wrangle_bnd_sign_args, and wrangle_sign_vsa collapse
into wrangle_engine_verify: one VSA-gated toolbox run per subject in which the
engine execs the in-image ampel, dual-checks the verdict (exit code AND the
emitted VSA must agree), signs the VSA, writes the signed line, and appends it
to the per-artifact bundle. bnd push github, cosign attach, and the release
attach flow stay shell per the #485 boundary; wrangle_resolve_policy stays
because the toolbox mount decision needs the resolved path host-side.

Topology note, stated explicitly: the signing token now rides the same
container as the ampel run (previously ampel ran token-less and bnd signed in
a separate run). The engine strips SIGSTORE_ID_TOKEN / ACTIONS_ID_TOKEN_REQUEST_* /
AMPEL_* from ampel's child env (asserted in go test); the request-URL vars
still never enter any container.

The in-shell wrangle_retry_once no longer wraps the verify run: the engine
retries the ampel exec itself and carabiner-dev/signer retries the Sigstore
hops; an in-shell re-run after a torn bundle append could double-append.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
jq -c exits 0 on empty input, so the old host-side non-empty guard on the
signed VSA was not actually replaced by the jq flatten. Restore it: an engine
that exits 0 without landing the line here (a mount mismatch) would otherwise
push an empty statement to the store and the registry.

Also drop the overclaiming retry comment — the in-shell retry is gone because
a re-run after a completed bundle append would double-append, not because the
signer's retry behavior is settled.

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. So the dual-check 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.

What the dual-check does catch is unchanged: an absent, unparseable,
wrong-subject, wrong-predicate-type, empty-verdict, or FAILED-with-exit-0 VSA.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@TomHennen

Copy link
Copy Markdown
Owner Author

claude: Status note — this shows CONFLICTING against main, as expected and not a surprise to be discovered later: v0.4.0's release commits landed on main and both sides re-converged self-ref pins, so the conflicts are in the uses: pin lines of the workflow files, not in any logic.

Not fixing it now on purpose: this PR targets v0.5.0 and must be rebased + pins re-converged anyway after #780 merges and the attest-toolbox image republishes with the new engine. Rebasing it today would just have to be redone. It stays DRAFT until then — it is not ready and is not claiming to be.

The verdict write-up for the spike is on #484.

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.

1 participant