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

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

Six commands. check is accepted as an alias for inspect and verify.
CommandDescription
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 mcpRun the MCP server over stdio.

Flags

Every flag. A value flag takes the next argument — write --trust file, not --trust=file.
FlagDescription
--trust <file>A trust list: a C2PA trust list JSON document or a PEM certificate bundle.
--require-trustedTreat an untrusted signer as a failure. Sets assertTrusted; it only bites when a trust list is supplied.
--jsonPrint a machine-readable JSON document to stdout.
--verbose, -vInclude per-assertion detail and certificate validity windows.
--quiet, -qSuppress output; rely on the exit code.
--registry <path>The registry file for anchor and resolve, in JSON Lines form.
-h, --helpShow the help text and exit.
--versionPrint 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
$ 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
$ 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

The exit code is the CI gate.
CodeMeaning
0Valid, or nothing to report. An untrusted signer also exits 0 unless --require-trusted is set.
1Invalid, untrusted with --require-trusted, unsigned (no manifest), or a resolve mismatch.
2Usage error: a missing argument, an unknown option or command, or --registry omitted where it is required.
3I/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

registry
$ 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"]
    }
  }
}
The three MCP tools.
ToolArgumentsReturns
provenance_verifydata (string, base64, required), name (string, optional)A one-line summary followed by the text report.
provenance_inspectdata (string, base64, required), name (string, optional)The full verbose report, including every assertion.
provenance_carddata (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.

trust
# 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.