Reference
Usage
The same engine runs from the command line, from TypeScript, from an MCP-capable agent, and in the browser. Every surface reads bytes and returns the same verification result.
Install
# one-line install (drops an `attest` binary on PATH)$ curl -fsSL https://raw.githubusercontent.com/srivtx/attest/main/install.sh | sh# run once, without installing$ bunx github:srivtx/attest#main --help# pin a branch, tag, or commit$ curl -fsSL https://raw.githubusercontent.com/srivtx/attest/main/install.sh | sh -s v0.1.0# add as a project dev dependency$ bun add -d github:srivtx/attest
attest is not published to npm, and it requires Bun. The install script runs bun add -g github:srivtx/attest#<ref>; bunx github:srivtx/attest#main runs the latest main without installing anything.
Commands
| Command | Description |
|---|---|
attest inspect <file> [options] | Verify and print the full report: signature, chain, actions, ingredients, hard binding, and every issue. |
attest verify <file> [options] | Verify and print a one summary line, unless --verbose. The exit code is the answer. |
attest card <file> | Print a portable provenance card as JSON. |
attest anchor <file> --registry <path> | Append a provenance card to a registry file. |
attest resolve <hash|file> --registry <path> | Look up a card by SHA-256, or re-verify a file against the card already in the registry. |
attest mcp | Run the MCP server over stdio. |
Flags
| Flag | Description |
|---|---|
--trust <file> | A trust list: a C2PA trust list JSON document or a PEM certificate bundle. |
--require-trusted | Treat an untrusted signer as a failure. Sets assertTrusted; it only bites when a trust list is supplied. |
--json | Print a machine-readable JSON document to stdout. |
--verbose, -v | Include per-assertion detail and certificate validity windows. |
--quiet, -q | Suppress output; rely on the exit code. |
--registry <path> | The registry file for anchor and resolve, in JSON Lines form. |
-h, --help | Show the help text and exit. |
--version | Print the version and exit. |
A full inspect
Text output is line-oriented: a state header, the asset hash, then one block per manifest ending with the issue list. --verbose adds the chain windows, the computed and asserted hard-binding hashes, and every assertion with its declared, present, and hash result.
$ attest inspect fixtures/C2PA-signed.jpg --verboseVALID 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, OU=FOR TESTING_ONLY, CN=C2PA Signer 2022-06-10T18:46:28.000Z to 2030-08-26T18:46:28.000Z 68e2ccccf0ee0529 . C=US, ST=CA, L=Somewhere, O=C2PA Test Intermediate Root CA, OU=FOR TESTING_ONLY, CN=Intermediate CA [CA] 2022-06-10T18:46:26.000Z to 2030-08-27T18:46:26.000Z 28f3833389548b0e actions c2pa.created by Make Test Images 0.33.1 hard binding matched (sha256) computed 81664d10e30740d8942df03fb4ee470f66b03b060c037b677d43c0a48afd72c9 asserted 81664d10e30740d8942df03fb4ee470f66b03b060c037b677d43c0a48afd72c9 assertions 4 c2pa.thumbnail.claim.jpeg declared, present, hash ok stds.schema-org.CreativeWork declared, present, hash ok c2pa.actions declared, present, hash ok c2pa.hash.data declared, present, hash ok0 error(s), 0 warning(s) INFO claimSignature.validated Claim signature validated (PS256). INFO signingCredential.intermediate The chain does not carry the issuer of C=US, ST=CA, L=Somewhere, O=C2PA Test Intermediate Root CA, OU=FOR TESTING_ONLY, CN=Intermediate CA; trust has to come from a trust list. INFO hardBinding.matched Asset bytes match the hard binding (sha256).
Verify, valid and tampered
verify without --verbose prints a single line. Run against the signed fixture and against a tampered copy, the difference is the hard binding.
$ 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
Exit codes
| Code | Meaning |
|---|---|
0 | Valid, or nothing to report. An untrusted signer also exits 0 unless --require-trusted is set. |
1 | Invalid, untrusted with --require-trusted, unsigned (no manifest), or a resolve mismatch. |
2 | Usage error: a missing argument, an unknown option or command, or --registry omitted where it is required. |
3 | I/O error: an input file could not be read, or a trust list could not be parsed. |
A file that cannot be read is never reported as clean: it exits non-zero, so a gate cannot pass on input the tool did not understand.
Provenance cards and the registry
card prints a portable, self-describing record of one verification. anchor appends that card to a registry; resolve looks one up and re-verifies a file against it.
The card format
The version string is attest/card/1. A card records the asset SHA-256, size, and container, the state, the manifests, and when it was issued.
{
"version": "attest/card/1",
"asset": {
"sha256": "a2d14755db55de67a47c04090340d8266e892367be4104a45626d7a6fa6e9ffd",
"size": 132518,
"container": "jpeg"
},
"state": "valid",
"manifests": [
{
"label": "contentauth:urn:uuid:b2b1f7fa-b119-4de1-9c0d-c97fbea3f2c3",
"title": "C.jpg",
"generator": "make_test_images",
"format": "image/jpeg",
"instanceId": "xmp:iid:22704d84-c37f-4733-a207-56c4c2e67b1a",
"signature": { "valid": true, "algorithm": "PS256" },
"trust": { "status": "unknown", "reason": "no trust list was supplied" },
"hardBinding": {
"status": "matched",
"algorithm": "sha256",
"excludedBytes": 45884,
"assertedHash": "81664d10e30740d8942df03fb4ee470f66b03b060c037b677d43c0a48afd72c9",
"computedHash": "81664d10e30740d8942df03fb4ee470f66b03b060c037b677d43c0a48afd72c9"
},
"actions": [
{
"action": "c2pa.created",
"softwareAgent": "Make Test Images 0.33.1",
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
}
],
"ingredients": [],
"chain": [
{ "subject": "C=US, ST=CA, L=Somewhere, O=C2PA Test Signing Cert, OU=FOR TESTING_ONLY, CN=C2PA Signer", "fingerprint": "68e2ccccf0ee0529528e931abd0f68ec8077238da4e041d344d900498a2b953c" },
{ "subject": "C=US, ST=CA, L=Somewhere, O=C2PA Test Intermediate Root CA, OU=FOR TESTING_ONLY, CN=Intermediate CA", "fingerprint": "28f3833389548b0eeb00b88823d019bd406b6a16d6c3fc0f66d59525b80df87e" }
]
}
],
"issuedAt": "2026-09-20T15:47:26.771Z"
}
An anchor also prints a card id: the SHA-256 of the canonical card with issuedAt left out, so the same verification always yields the same id.
Anchor and resolve
$ attest anchor fixtures/C2PA-signed.jpg --registry registry.jsonlanchored 972d8dfa1cdab3d79c510908dcfa6fb125fb8866d2f1740e0278fced118b6733 valid: C.jpg — signature valid, hard binding matched registry.jsonl$ attest resolve fixtures/C2PA-signed.jpg --registry registry.jsonlanchored and unchanged sha256 a2d14755db55de67a47c04090340d8266e892367be4104a45626d7a6fa6e9ffd issued 2026-09-20T15:47:26.771Z$ attest resolve a2d14755db55de67a47c04090340d8266e892367be4104a45626d7a6fa6e9ffd --registry registry.jsonla2d14755db55de67a47c04090340d8266e892367be4104a45626d7a6fa6e9ffd state valid issued 2026-09-20T15:47:26.771Z manifests 1$ attest resolve tampered.jpg --registry registry.jsonlnot anchored sha256 f8ea3283d09a3cc8adbca73b47d87b88e0a75767245949143e2633d6e3e5c35a
The registry is JSON Lines: one card per line. Blank lines and lines that are not attest/card/1 are skipped, so a registry can sit next to other data and survive hand edits.
MCP server
attest mcp speaks the Model Context Protocol over stdio, so an agent can verify provenance without a shell. Each tool takes the file bytes as base64 in data. There is no network access from the server.
{
"mcpServers": {
"attest": {
"command": "bunx",
"args": ["github:srivtx/attest#main", "mcp"]
}
}
}
| Tool | Arguments | Returns |
|---|---|---|
provenance_verify | data (string, base64, required), name (string, optional) | A one-line summary followed by the text report. |
provenance_inspect | data (string, base64, required), name (string, optional) | The full verbose report, including every assertion. |
provenance_card | data (string, base64, required) | The attest/card/1 card and the state, as JSON. |
JavaScript API
The engine is exported from src/index.ts and is browser-safe: no node: imports, so the same code runs in Bun and in a page. The built browser bundle exposes the same names on the global Attest.
import { verifyBytes, formatReport, formatJson, summarize, toCard } from "attest";
const bytes = new Uint8Array(await file.arrayBuffer());
const asset = await verifyBytes(bytes);
console.log(summarize(asset)); // one line, e.g. the verify output
console.log(formatReport(asset)); // the text report
console.log(formatJson(asset, 2)); // the stable JSON document
const card = toCard(asset); // an attest/card/1 object
Pass a trust list and ask for it to be enforced by handing verifyBytes the options directly.
import { verifyBytes, parseTrustList } from "attest";
const trustList = await parseTrustList(pemText, "roots.pem");
const asset = await verifyBytes(bytes, { trustList, assertTrusted: true });
In a page, the bundle is a classic script that defines Attest; the landing page verifier calls Attest.verifyBytes, Attest.formatReport, Attest.summarize, and Attest.formatJson. Also exported: hashWithExclusions, sha256, locateManifest, parseManifestStore, parseTrustList, evaluateTrust, cardId, serializeCard, parseRegistry, resolveByHash, and matchCard.
Trust lists
A trust list is a list of signers you have decided to accept. attest matches a chain link when its SHA-256 fingerprint equals the entry's cert_sha256, or when its subject name equals the entry's subject exactly. Any certificate in the chain may match.
Two formats are accepted. A JSON document may be a top-level array or an object with a trusted_list (also trusted, allowed, entries, or certificates) array of entries. Each entry may carry cert_sha256 (aliases certSha256, sha256, fingerprint; colons are stripped) and/or subject (alias subject_dn); an issuer field is read but is not used for matching.
{
"trusted_list": [
{ "subject": "C=US, ST=CA, L=Somewhere, O=C2PA Test Signing Cert, OU=FOR TESTING_ONLY, CN=C2PA Signer" },
{ "cert_sha256": "68e2ccccf0ee0529528e931abd0f68ec8077238da4e041d344d900498a2b953c" }
]
}
The other format is a PEM bundle: every certificate in the file becomes an entry keyed by the SHA-256 of its DER, together with its subject and issuer.
# a JSON trust list$ attest verify photo.jpg --trust trust-list.json --require-trusted# a bundle of PEM certificates$ attest verify photo.jpg --trust roots.pem --require-trusted# untrusted, with a list that does not name the signeruntrusted: C.jpg — signature valid, hard binding matchedexit 1
With no trust list at all, the trust status is unknown, not untrusted. That means --require-trusted on its own does not fail a file whose signer you never vouched for; supply --trust to make the check meaningful.