Ship the binary you built and tested
A green test job and a signed release do not, on their own, say the tests ran against the code you shipped. Between the build and the release, a file can be replaced, and between the build and the test, the source can change. Each step looks fine in isolation.
CI/lock records what each step read (materials) and wrote (products) by
SHA-256. A policy step with artifactsFrom requires its materials to match
what an earlier step used or produced, and cilock verify <file> hashes the
file you are about to ship and requires that digest in the signed evidence. In
this session the prompt asks Claude to set that up for a small Go command and
to show it refusing a release with a swapped binary.
TestifySec wrote the prompt and sent it into the session. Claude Code and
cilock ran unedited, and the recording was not changed afterwards.
What happens, step by step
-
Two keys. Claude generates two ed25519 keys with
openssl:ci.keysigns the evidence andpolicy.keysigns the policy. Whoever holds onlyci.keycannot rewrite the rules; in the recording Claude holds both, so that separation exists only oncepolicy.keygoes to someone else. -
Record the build and the tests. Claude wraps
go build -o bin/greet ./cmd/greetas stepbuildandgo test -count=1 ./cmd/greetas steptest, each withcilock run -k ci.key --material-manifest, with no platform.--material-manifestkeeps the per-file material digests thatartifactsFromcompares. In the recording Claude spells "no platform" as an empty--platform-url;--offlineis the documented flag and does the same thing:cilock run --step build -k ci.key --offline --material-manifest -a environment \ -o "$EVIDENCE/build.att.json" -- go build -o bin/greet ./cmd/greet cilock run --step test -k ci.key --offline --material-manifest -a environment \ -o "$EVIDENCE/test.att.json" -- go test -count=1 ./cmd/greet -
Write the policy. Each step must be signed by the CI key, run exactly the pinned command and exit
0. Theteststep addsartifactsFrom: ["build"].cilock policy validatewarns that the first Rego deny rules pass a predicate with the fields missing; Claude rewrites them withobject.getdefaults so a missing field is denied, then signs the policy withpolicy.key. -
Evidence moves out of the checkout. The first verify fails: the attestation files were written into
attestations/inside the repository, so the test step saw inputs the build never produced. Claude moves the evidence outside the working tree rather than adding anallowedUntrackedexception, which would open a gap in the chain. -
The real release passes. Verifying the file computes its subject (
sha256:ae1de0ce…) and returnsVerification succeeded:cilock verify bin/greet -p policy/policy.signed.json -k policy/policy.pub \ -a "$EVIDENCE/build.att.json" -a "$EVIDENCE/test.att.json" --offline -
A swapped binary is refused. Claude builds a different
bin/greet(sha256:aa876a6b…) that still printshello, ada, puts it in place, and verifies again:supplied artifact digest(s) … not present in any subject of step "build", exit1. -
A source edit between build and test is refused. Claude appends a comment to
main.go, re-records the test step and verifies the real binary: the build step passes, the test step fails withmismatched digests for cmd/greet/main.go, exit1.
What each check covers
| Change | Caught by |
|---|---|
| Released file differs from the build's product | cilock verify <file>: the computed digest must be a subject of the signed evidence |
| Tests ran on different source than the build | artifactsFrom: ["build"] on the test step |
Someone wraps true instead of the real command | the pinned-command Rego rule on each step |
| Evidence signed by an unknown key | the step's functionary names the CI key |
The chain is only as strong as the key behind it. Anyone holding ci.key can
sign a fake build, which Claude points out at the end of the session. In CI,
sign keyless with the job's OIDC identity instead; see
Signing and identity. The recording ran
offline, so the signatures carry no timestamp. Add --timestamp-servers, or
use the platform signer, when you need one.
Run it yourself
The recording used cilock built from main at commit 5b614726. Write the
.att.json files outside the checkout (for example $RUNNER_TEMP on GitHub
Actions) or the next step records them as materials. See
Policy verification for how
artifactsFrom and subjects are evaluated, and Release
gates for a gate that adds Rego over the evidence.
Reference generated from the product documentation. Match commands and support details to your installed release.