Guide
CI and code scanning
attest is built to run in a pipeline: no network, a documented exit code, and the same deterministic result every time. The exit code is the gate.
Add it to a workflow
Install Bun, then verify the images. The single-file step is the gate; the loop extends it to a whole directory and still fails the job if any file does not verify.
name: Content Credentials
on: [push, pull_request]
jobs:
attest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.x
- name: Verify the hero image
run: |
bunx github:srivtx/attest#main verify public/hero.jpg \
--require-trusted --trust ci/trust-list.json
- name: Verify every image
shell: bash
run: |
fail=0
shopt -s globstar nullglob
for file in public/**/*.jpg public/**/*.png public/**/*.webp; do
echo "attest verify $file"
bunx github:srivtx/attest#main verify "$file" \
--require-trusted --trust ci/trust-list.json || fail=1
done
exit "$fail"
Pass --trust with --require-trusted: without a trust list the signer status is unknown, which does not fail. With the list, a file whose signer you did not vouch for exits 1 and stops the job.
What a failure looks like
$ attest verify tampered.jpg --require-trusted --trust ci/trust-list.jsoninvalid: C.jpg — signature valid, hard binding mismatchexit 1$ attest verify photo.jpg --require-trusted --trust ci/trust-list.jsonuntrusted: C.jpg — signature valid, hard binding matchedexit 1
The first file changed after it was signed. The second is intact, but its signer is not on the list, so the job treats it as untrusted.
Exit codes in CI terms
| Code | CI meaning |
|---|---|
0 | Pass. The file verified, or there was nothing to report. An untrusted signer also passes without --require-trusted. |
1 | Fail. The file is invalid, unsigned, untrusted under --require-trusted, or did not match the registry card. |
2 | Fail. Usage error: a bad flag, an unknown command, or a missing --registry. A misconfigured gate fails loudly instead of passing. |
3 | Fail. I/O error: the image or the trust list could not be read. |
Anchor a card in a registry commit
Verification answers for the file in front of you. Anchoring records that answer in the repository, so a later change to the same path is detectable. On a push to the default branch, append a card for each image and commit the registry; the history of the registry is then the history of the assets' provenance.
- name: Anchor the cards
if: github.event_name == 'push'
shell: bash
run: |
mkdir -p provenance
touch provenance/registry.jsonl
shopt -s globstar nullglob
for file in public/**/*.jpg public/**/*.png public/**/*.webp; do
bunx github:srivtx/attest#main anchor "$file" --registry provenance/registry.jsonl
done
- name: Commit the registry
if: github.event_name == 'push'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add provenance/registry.jsonl
git diff --staged --quiet || git commit -m "chore: anchor content credentials"
git push
Each card records the asset SHA-256, the state, the active manifest, and the hard binding. Because the card id is derived without the issue timestamp, re-anchoring an unchanged file produces the same id.
To catch a swap after the fact, resolve the current file against the committed registry. A file that was replaced, re-encoded, or re-signed fails the comparison and exits 1.
- name: Re-verify against the registry
run: |
bunx github:srivtx/attest#main resolve public/hero.jpg --registry provenance/registry.jsonl
That step compares the live file to the card on disk: the SHA-256 and size, the active manifest id, the hard binding hash, and the signature state. Any difference is printed as a reason and the step fails.