attest
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@attestCheck this image's Content Credentials and tell me if it's authentic"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
attest
Offline C2PA Content Credentials verifier and provenance tool.
by svx · MIT Licensed
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;
attestverifies 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 --verboseVALID 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.jpgvalid: C.jpg — signature valid, hard binding matchedChange 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.jpginvalid: C.jpg — signature valid, hard binding mismatchThe 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 stdioinspect 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 list: a C2PA-style trust list JSON or PEM certificates |
| Treat an untrusted signer as a failure (exit |
| Print machine-readable JSON instead of text |
| Include per-assertion detail and certificate validity windows |
| Suppress output; rely on the exit code |
| Registry file for |
| Show the usage text |
| Print the version and exit |
Exit codes:
Code | Meaning |
| Valid, or nothing to report. An untrusted signer is |
| Invalid, untrusted under |
| Usage error: unknown flag, unknown command, missing target, or unreadable format |
| 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 |
|
| The summary line, the validation state, signer chain, hard binding result, actions, and issues |
|
| The full verbose report, every assertion with its declared/present/hash state |
|
| 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 |
|
CBOR |
|
DER |
|
X.509 |
|
COSE |
|
JUMBF |
|
C2PA |
|
Verify |
|
Trust |
|
Report |
|
Card |
|
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 manifests — issuedAt 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 (
caBXchunk), WebP (C2PARIFF 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
Signature1Sig_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 algorithmsES256,ES384,ES512,PS256,PS384,PS512,RS256, andEdDSA(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.datais 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.boxesassertion is reported ashardBinding.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_manifestURL 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 isunknown; 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
algand the hard binding'salgare read and displayed, but no other hash algorithm is implemented.No path building. The
x5chainis 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-levelsigningCredential.intermediateand leaves trust to the list.The claim's
signaturehashed URI is not checked. Only entries in the claim'sassertionsarray are hash-verified.No signing.
attestcannot 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).
--versionprints only the version string. There is no build metadata or commit hash.
Validation states
The asset state is one of:
State | Meaning | Exit |
| A manifest store was found and verified, with no error-severity issue |
|
| At least one error-severity issue (a failed signature, a hash mismatch, a mismatched hard binding, or a missing hard binding) |
|
| Verification found no error, but the signer is not on the trust list and |
|
| No C2PA manifest store was found |
|
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 ( | Multiple JP segments are concatenated until the declared box length is reached |
PNG |
| The first |
WebP |
| The first |
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/attestThen:
attest verify signed.jpg --trust trust.pemContributing
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.
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP for C2PA intake verifier MCP, structured receipts, audit logs, and reviewer-ready evidenc
Read-only verifier for 25 ProofRelay MCP tools and non-confidential evidence bundles.
Remote MCP for C2PA disclosure policy MCP, structured receipts, audit logs, and reviewer-ready evide
Free C2PA 2.4 triage and reproducible official trust snapshot. No media, keys, or payment.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables 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.2MIT
- AlicenseAqualityBmaintenanceVerifies C2PA Content Credentials for local files or URLs and returns an LLM-ready verdict on trust, AI generation, and provenance.411 npm1Apache 2.0
- AlicenseAqualityBmaintenanceAn 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.221 PyPIMIT
- AlicenseNot gradedqualityCmaintenanceEnables offline verification that a supplied DSH workflow adhered to declared duty-separation constraints, producing a redacted, content-addressed JSON verdict.MIT