Offline · no upload · CI-ready
Verify Content Credentials, offline.
attest reads the C2PA manifest embedded in a signed image, validates the claim signature and the hard binding, and reports the signer chain, the recorded actions, and every issue it finds. It runs entirely on your machine: no upload, no account, no network call.
bunx github:srivtx/attest#main inspect photo.jpg
attest is not on npm; bunx github:srivtx/attest#main runs the latest main with Bun. Install the attest binary with the one-line script, or add it as a dev dependency.
$ attest inspect fixtures/C2PA-signed.jpgVALID jpeg 132518 bytessha256 a2d14755db55de67a47c04090340d8266e892367be4104a45626d7a6fa6e9ffdmanifest contentauth:urn:uuid:b2b1f7fa-b119-4de1-9c0d-c97fbea3f2c3 title C.jpg generator make_test_images format image/jpeg instance xmp:iid:22704d84-c37f-4733-a207-56c4c2e67b1a signature valid (PS256) trust unknown — no trust list was supplied chain . C=US, ST=CA, L=Somewhere, O=C2PA Test Signing Cert, CN=C2PA Signer . C=US, ST=CA, L=Somewhere, O=C2PA Test Intermediate Root CA, CN=Intermediate CA [CA] actions c2pa.created by Make Test Images 0.33.1 hard binding matched (sha256) assertions 40 error(s), 0 warning(s)
Valid vs tampered
One changed byte separates a signed file from a forged one. The signature still validates, because the manifest is untouched — but the hard binding no longer matches the pixels.
$ attest verify fixtures/C2PA-signed.jpgvalid: C.jpg — signature valid, hard binding matchedexit 0$ attest verify tampered.jpginvalid: C.jpg — signature valid, hard binding mismatchexit 1
The exit code is the gate: 0 means the file verified, 1 means it did not. In CI, that is the whole check.
Try it in your browser
Choose a signed image and attest verifies it right here. The file is read with File.arrayBuffer() and never leaves this machine: there is no upload and no network request. You can watch the network tab.
Runs entirely in your browser. Nothing is uploaded, and no request is made.
No file chosen.
An unsigned file is not a failure of the tool: it means the file carries no Content Credentials at all, so its origin is unknown.
What C2PA is
C2PA — the Coalition for Content Provenance and Authenticity — is an open standard for attaching cryptographically signed provenance to media: who made it, with what tool, and what happened to it since. A signed file carries a manifest holding a signed claim plus assertions, such as the creating action, a thumbnail, and a hash of the asset itself. Content Credentials is the user-facing name for that provenance.
The manifest is written as JUMBF boxes, embedded in the file's own bytes: an APP11 segment in a JPEG, a caBX chunk in a PNG, or a C2PA chunk in a WebP. It can also stand alone as a raw JUMBF file.
What it checks
attest re-derives each check from the bytes in front of it. Nothing is taken on trust from the file's own metadata.
- The claim signature
- The COSE Sign1 signature over the claim, verified against the signing certificate. PS256, ES256, RS256 family algorithms and the other COSE algorithms are handled.
- The signer chain
- Every certificate in the
x5chain: its subject and issuer, its validity window, and the issuer signature of each link. - Trust
- Whether any certificate in the chain is on the trust list you supply, as a SHA-256 fingerprint or an exact subject name.
- Assertion hashes
- Every assertion the claim declares must be present, and its SHA-256 must match the hash recorded in the claim.
- The hard binding
- The asset bytes are re-hashed with the manifest region excluded and compared to the
c2pa.hash.dataassertion. This is what detects editing after signing. - Actions and ingredients
- The recorded actions, their software agents and timestamps, the digital source type, and any declared ingredients.
Not checked. This build does not evaluate c2pa.hash.boxes (the BMFF fragment bindings) and reports them as unsupported. It does not perform OCSP or CRL revocation checks, and it does not fetch a remote manifest — a remote_manifest URL is reported, never requested. It is not a conformance implementation, and it makes no claim about whether the picture is truthful.
Why offline matters
Verifying provenance is sensitive. The file you are checking is often the very thing you are not ready to publish, and sending it to a third-party service tells that service what you are looking at. attest reads the file and does the cryptography on the machine you are already on.
Offline also means it works where a network does not: an air-gapped newsroom machine, a locked-down build runner, a laptop on a plane. There is no API key to rotate, no rate limit, and no service that can disappear or change its answer underneath you.
The tool is deterministic: the same bytes produce the same report and the same exit code, today or in a year. That is what makes it usable as a gate. The only network call anywhere in this project is the one you type to install it.
In CI
Point attest at your image directory and let the exit code fail the build when a file is invalid or signed by a signer you do not trust.
$ attest verify public/hero.jpg --require-trusted --trust ci/trust-list.json# exit 0 when the signature, the hard binding, and the trust list all agree# exit 1 when the file changed, the chain is broken, or the signer is not trusted
There is a complete GitHub Actions workflow, including anchoring a card in a registry commit, on the CI page.
Commands
One binary, six commands. Everything the browser bundle does, the CLI does with the same engine.
inspect- Verify and print the full report: chain, actions, ingredients, assertions, and every issue.
verify- The same check with a one-line summary, for scripts and CI. The exit code is the answer.
card- Print a portable provenance card — the asset hash, state, signer, and actions — as JSON.
anchor- Append a card to a JSON Lines registry, so the state of a file is recorded in a commit.
resolve- Look up a card by SHA-256, or re-verify a file against the card already in the registry.
mcp- Run the Model Context Protocol server over stdio, exposing three provenance tools to an agent.
Every command, flag, exit code, and the browser API are in Usage.
Documentation
The landing page is the pitch. The details live on four short pages.
- Rules
- Every issue code the engine emits, the four validation states, and what the hard binding is.
- Usage
- The CLI commands and flags, exit codes, the MCP server, the JavaScript API, trust lists, and the registry.
- CI
- A copy-paste GitHub Actions workflow, anchoring cards, and the exit codes as a build gate.
- FAQ
- Scope, honest limits, the file types it reads, and what provenance can and cannot prove.