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 · CI
$ 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

Every non-zero code fails the step.
CodeCI meaning
0Pass. The file verified, or there was nothing to report. An untrusted signer also passes without --require-trusted.
1Fail. The file is invalid, unsigned, untrusted under --require-trusted, or did not match the registry card.
2Fail. Usage error: a bad flag, an unknown command, or a missing --registry. A misconfigured gate fails loudly instead of passing.
3Fail. 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.