Skip to main content
Glama

attest

Offline C2PA Content Credentials verifier and provenance tool.

by svx · MIT Licensed

CI release license


attest reads the JUMBF manifest store out of a JPEG, PNG, WebP, or raw JUMBF file, decodes the CBOR claim, verifies the COSE_Sign1 signature against the x5chain, verifies every assertion hash, checks the c2pa.hash.data hard binding over the asset bytes, walks the certificate chain, evaluates a trust list, and reports a validation state with stable issue codes. It runs entirely in-process, in Bun or the browser, with no upload and no account. It can also emit a portable provenance card and anchor cards in a JSON Lines registry.

The normative description of what is and is not checked is spec/SPEC.md.

Why

A Content Credential is only useful if you can check it yourself. The common alternatives are a vendor service that uploads your file, an account you have to create, or a hard dependency on a closed platform. attest is none of those:

  • Offline. It never opens a socket. The manifest, the certificate chain, and the signatures are all verified from the bytes you hand it.

  • No upload, no account. Your media never leaves the machine.

  • Verifiable in CI. The CLI returns stable exit codes, so a pipeline can fail a build when a signed asset is tampered with or a signer is not trusted.

  • Transparency you can point at. From 2 August 2026, EU AI Act Article 50 requires providers and deployers of certain AI systems to mark and disclose AI-generated or AI-manipulated content. A C2PA manifest is one of the machine-readable disclosures that obligation can rest on; attest verifies that a manifest is present, signed, and internally consistent without sending the asset anywhere.

Verification tells you who signed these exact bytes and whether they have been altered since. It does not tell you the content is true; see What is not verified.

Related MCP server: c2pa-mcp

Quick start

Requires Bun (>= 1.1.0).

bun install
bun src/cli.ts inspect fixtures/C2PA-signed.jpg --verbose
VALID  jpeg  132518 bytes
sha256  a2d14755db55de67a47c04090340d8266e892367be4104a45626d7a6fa6e9ffd

manifest  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 ok

0 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).

The default verify prints one line:

bun src/cli.ts verify fixtures/C2PA-signed.jpg
valid: C.jpg — signature valid, hard binding matched

Change one byte of the scan data and the hard binding fails. (This reproduces the /tmp/opencode/tampered.jpg used below from the committed fixture.)

cp fixtures/C2PA-signed.jpg /tmp/opencode/tampered.jpg
printf '\xd2' | dd of=/tmp/opencode/tampered.jpg bs=1 seek=127518 conv=notrunc count=1 2>/dev/null
bun src/cli.ts verify /tmp/opencode/tampered.jpg
invalid: C.jpg — signature valid, hard binding mismatch

The exit code is 1 and the claim signature still validates: the bytes changed, the signature covers the claim, and the hard binding is what detects the change.

CLI

attest inspect <file> [options]        verify and print a full report
attest verify <file> [options]         verify; one summary line unless --verbose
attest card <file>                     print a portable provenance card as JSON
attest anchor <file> --registry <p>    append a provenance card to a registry
attest resolve <hash|file> --registry <p>
                                       look up a card, and re-verify a file against it
attest mcp                             run the MCP server over stdio

inspect and verify are the same verification; inspect prints the full report and verify prints the one-line summary unless --verbose is passed. check is accepted as an alias of inspect. A target of - reads the media bytes from stdin.

Flag

Meaning

--trust <file>

Trust list: a C2PA-style trust list JSON or PEM certificates

--require-trusted

Treat an untrusted signer as a failure (exit 1)

--json

Print machine-readable JSON instead of text

--verbose, -v

Include per-assertion detail and certificate validity windows

--quiet, -q

Suppress output; rely on the exit code

--registry <path>

Registry file for anchor and resolve (JSON Lines)

-h, --help

Show the usage text

--version

Print the version and exit 0

Exit codes:

Code

Meaning

0

Valid, or nothing to report. An untrusted signer is 0 unless --require-trusted is set

1

Invalid, untrusted under --require-trusted, unsigned, or a card was not found / did not match

2

Usage error: unknown flag, unknown command, missing target, or unreadable format

3

I/O error: a file could not be read, or a trust list could not be loaded

MCP

Any MCP-capable agent can verify media over stdio. The MCP server takes the file bytes as base64 and makes no network calls.

{
  "mcpServers": {
    "attest": {
      "command": "bunx",
      "args": ["github:srivtx/attest#main", "mcp"]
    }
  }
}

The server exposes three tools:

Tool

Arguments

Returns

provenance_verify

data (base64), optional name

The summary line, the validation state, signer chain, hard binding result, actions, and issues

provenance_inspect

data (base64), optional name

The full verbose report, every assertion with its declared/present/hash state

provenance_card

data (base64)

The portable provenance card and the validation state as JSON

It implements initialize, notifications/initialized, ping, tools/list, and tools/call. The tool names are stable.

JavaScript API

src/index.ts is the public surface, and it is browser-safe: no node: imports, only WebCrypto and standard web APIs. scripts/build-site.mjs bundles it into site/assets/studio.js as the global Attest.

import { verifyBytes, toCard, parseTrustList, evaluateTrust } from "attest";

const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
const asset = await verifyBytes(bytes);

console.log(asset.state);          // "valid" | "invalid" | "untrusted" | "unsigned"
console.log(asset.file.hash);      // sha256 of the file
for (const manifest of asset.manifests) {
  console.log(manifest.signature.valid, manifest.hardBinding.status);
}

const card = toCard(asset);

Exported groups:

Group

Exports

Bytes

Reader, concat, toHex, fromHex, base64, fromBase64, utf8, encodeUtf8, bytesEqual, startsWith, indexOfBytes

CBOR

decodeCbor, encodeCbor, CborValue, and the mapGet* accessors

DER

DER_TAG, DER_CLASS, parseDer, readDerNode, derOid, oidName, DerNode

X.509

parseCertificate, importPublicKey, verifyCertificateSignature, algorithmFromOid, certificatesFromPem, pemToDer, Certificate, SignatureAlgorithm

COSE

parseCoseSign1, verifyCoseSign1, coseSigStructure, COSE_ALGORITHMS, coseAlgorithmLabel, CoseSign1

JUMBF

locateManifest, parseJumbfBoxes, walk, findBox, findBoxes, formatUuid, JumbfBox, ManifestLocation

C2PA

parseManifestStore, assertionLabelFromUri, findAssertionByUri, Manifest, ManifestStore, Claim, Assertion

Verify

verifyBytes, hashWithExclusions, sha256, AssetVerification, ManifestVerification, Issue, VerifyOptions, ChainLink, Ingredient, ActionRecord

Trust

parseTrustList, evaluateTrust, TrustList, TrustEntry, TrustEvaluation

Report

formatReport, formatJson, summarize, TextReportOptions

Card

toCard, cardId, serializeCard, parseRegistry, resolveByHash, matchCard, CARD_VERSION, ProvenanceCard, CardMatch

verifyBytes(bytes, options) takes trustList, assertTrusted, and an optional now date for testing expiry.

Trust list formats

A trust list is supplied with --trust. Two formats are accepted.

JSON. Either a top-level array of entries, or an object with one of the keys trusted_list, trusted, allowed, entries, or certificates holding an array. An entry may carry any of:

{
  "trusted_list": [
    {
      "cert_sha256": "68e2ccccf0ee0529528e931abd0f68ec8077238da4e041d344d900498a2b953c",
      "subject": "C=US, ST=CA, L=Somewhere, O=C2PA Test Signing Cert, OU=FOR_TESTING_ONLY, CN=C2PA Signer"
    }
  ]
}

The fingerprint keys accepted are cert_sha256, certSha256, sha256, and fingerprint; the subject keys are subject and subject_dn; the issuer keys are issuer and issuer_dn. Fingerprints are normalized by stripping : and lowercasing. The issuer field is parsed but not used when matching.

PEM. One or more -----BEGIN CERTIFICATE----- blocks. Each is parsed and recorded by its SHA-256 fingerprint and its subject and issuer distinguished names.

Evaluation walks the chain as reported in the manifest. The first link whose fingerprint equals cert_sha256 or whose subject equals subject marks the chain trusted (with that subject as the anchor). If no link matches, the chain is untrusted. If no list was supplied, trust is unknown, and the asset stays valid; only --require-trusted turns an untrusted evaluation into the asset state untrusted and exit 1.

Provenance card and registry

attest card emits a portable JSON record of a verification:

{
  "version": "attest/card/1",
  "asset": { "sha256": "…", "size": 132518, "container": "jpeg" },
  "state": "valid",
  "manifests": [
    {
      "label": "…",
      "title": "C.jpg",
      "generator": "make_test_images",
      "format": "image/jpeg",
      "instanceId": "…",
      "signature": { "valid": true, "algorithm": "PS256" },
      "trust": { "status": "unknown", "reason": "no trust list was supplied" },
      "hardBinding": { "status": "matched", "algorithm": "sha256", "assertedHash": "…", "computedHash": "…" },
      "actions": [ { "action": "c2pa.created", "softwareAgent": "Make Test Images 0.33.1" } ],
      "ingredients": [],
      "chain": [ { "subject": "…", "fingerprint": "…" } ]
    }
  ],
  "issuedAt": "2026-09-20T15:48:29.306Z"
}

The card id is the SHA-256 of the canonical JSON of version, asset, state, and manifestsissuedAt is deliberately excluded — so the same verification always yields the same id.

attest anchor <file> --registry <path> verifies the file and appends the card as one line of JSON to the registry. The registry is JSON Lines; a line that does not parse or is not attest/card/1 is ignored on read.

attest resolve <hash|file> --registry <path> looks a card up by asset SHA-256 (an optional sha256: prefix is accepted) or verifies a file and looks it up by its hash. For a file it also re-checks that the card still matches: the file hash, the file size, the active manifest id, the hard binding asserted hash, and the signature state must all be unchanged. resolve exits 0 on a match and 1 if the card is missing or the file changed.

What is verified

  • Container and manifest location. The manifest store is found in a JPEG (APP11 segments), PNG (caBX chunk), WebP (C2PA RIFF chunk), or a raw JUMBF file, and the exact byte ranges that hold it are recorded.

  • JUMBF structure. Box headers, superboxes, description boxes (type UUID, toggle byte, NUL-terminated label), the manifest store, each manifest, the claim box, the signature box, and the assertion set.

  • The CBOR claim. The claim map is decoded, including the fields listed in the spec, and the declared assertion list is read as hashed URIs.

  • The claim signature. The COSE_Sign1 Signature1 Sig_structure is built over the protected header and the claim bytes, and the signature is verified with WebCrypto against the leaf certificate's public key. The algorithms ES256, ES384, ES512, PS256, PS384, PS512, RS256, and EdDSA (Ed25519) are supported.

  • Every assertion hash. For each hashed URI the claim declares, the assertion is located and SHA-256 over its box payload is compared with the declared hash.

  • The hard binding. c2pa.hash.data is re-computed over the asset bytes with the declared exclusions removed and compared with the asserted hash.

  • The certificate chain. Each certificate's signature is checked against the next certificate in the x5chain, and every certificate's validity window is checked against the current time.

  • The trust list. The chain is evaluated against a supplied trust list.

  • A stable result. A validation state and a set of issue codes with severities, plus a portable provenance card and registry lookup.

What is not verified

This list is deliberately blunt. attest is a focused verifier, not a conformance authority.

  • No BMFF or box-hash bindings. A c2pa.hash.boxes assertion is reported as hardBinding.unsupported; it is not evaluated.

  • No OCSP or CRL revocation. A certificate that was revoked but not expired still verifies. There is no revocation check of any kind.

  • No remote manifests. The claim's remote_manifest URL is read and shown, but it is never fetched. Only the store embedded in the file is inspected.

  • No platform trust store. The OS or browser trust store is not consulted. Without --trust, trust is unknown; a signature can be cryptographically valid and still be from an untrusted issuer.

  • Provenance is not truth. A valid signature proves that the holder of the signing key signed this claim over these bytes. It does not prove the content is accurate, unedited in meaning, not misleading, or AI-generated or not AI-generated. It does not prove the signer is who they say they are beyond what the certificate and trust list establish.

  • No assertion semantics. Assertions are checked for presence and hash integrity, not for meaning: schema.org payloads, action vocabularies, thumbnails, and ingredient metadata are not validated.

  • No ingredient or manifest-chain recursion. Ingredients are listed, but their manifests and thumbnails are not resolved or verified.

  • No CAWG identity assertion validation. An identity assertion is treated as opaque bytes.

  • Only SHA-256. All digests — assertion hashes and the hard binding — are SHA-256. The claim's alg and the hard binding's alg are read and displayed, but no other hash algorithm is implemented.

  • No path building. The x5chain is read in order; issuer and subject are not matched, name constraints and policies are not processed, and key usage is not enforced. A chain that does not terminate in a self-signed root produces an info-level signingCredential.intermediate and leaves trust to the list.

  • The claim's signature hashed URI is not checked. Only entries in the claim's assertions array are hash-verified.

  • No signing. attest cannot create or sign a manifest.

  • Float CBOR and indefinite-length text strings. The CBOR decoder does not decode floating-point items and enforces byte-string chunks inside an indefinite-length text string (where the standard uses text-string chunks).

  • --version prints only the version string. There is no build metadata or commit hash.

Validation states

The asset state is one of:

State

Meaning

Exit

valid

A manifest store was found and verified, with no error-severity issue

0

invalid

At least one error-severity issue (a failed signature, a hash mismatch, a mismatched hard binding, or a missing hard binding)

1

untrusted

Verification found no error, but the signer is not on the trust list and --require-trusted is set

1

unsigned

No C2PA manifest store was found

1

The per-manifest state is valid, invalid, or untrusted. The asset state is invalid if any manifest is invalid, valid if all are valid, untrusted if the remainder is untrusted, and unsigned if there are no manifests. A broken hard binding is an error; a missing issuer is not.

Issue severities are error (invalidates the manifest), warning (reported but does not invalidate), and info. The complete code set is in spec/SPEC.md.

File formats

Container

Where the manifest is

Notes

JPEG

APP11 segments (0xFF 0xEB)

Multiple JP segments are concatenated until the declared box length is reached

PNG

caBX chunk

The first caBX chunk is used

WebP

C2PA RIFF chunk

The first C2PA chunk is used

Raw JUMBF

The whole file

Used when the file is not a recognized image

Not supported: BMFF/ISO base media (HEIF, AVIF, MP4), TIFF, SVG, PDF, or any other container not listed above.

Install

attest is not published to npm. Requires Bun (>= 1.1.0). Install from GitHub:

# Install the `attest` binary globally
bun add -g github:srivtx/attest

# Or run once, without installing
bunx github:srivtx/attest#main --help

# Add to a project
bun add -d github:srivtx/attest

Then:

attest verify signed.jpg --trust trust.pem

Contributing

See CONTRIBUTING.md for the development gate and the code conventions, and AGENTS.md for the layout and the hard rules. Change behavior and spec/SPEC.md together.

License

MIT — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables reading and verifying C2PA Content Credentials from images and videos through both embedded manifests and invisible watermarks. Supports direct filesystem access, URL checking, and provides structured provenance data including creator identity, AI generation info, and content authenticity.
    2
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Verifies C2PA Content Credentials for local files or URLs and returns an LLM-ready verdict on trust, AI generation, and provenance.
    4
    11 npm
    1
    Apache 2.0
  • A
    license
    A
    quality
    B
    maintenance
    An MCP server that reads a file's embedded C2PA Content Credential to report whether it declares AI generation or a real-world capture, and checks whether that declaration's signature still verifies against the file's current bytes.
    2
    21 PyPI
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables offline verification that a supplied DSH workflow adhered to declared duty-separation constraints, producing a redacted, content-addressed JSON verdict.
    MIT