Build a C2PA Content Credentials Inspector

Content Credentials are signed provenance metadata attached to images. This tutorial shows how to read a C2PA manifest programmatically, validate its signature, and extract the assertions that tell you whether AI was involved, using open tooling and a few lines of code.

Program reading a C2PA manifest store, extracting assertions and validating the signature

What you are actually reading

A C2PA credential is a manifest store embedded in an image's metadata. Inside are one or more manifests, each holding assertions (statements about the asset), a claim that binds them, and a signature. Your inspector's job is to parse that store, check the signature validates against the current bytes, and surface the assertions in a readable form. For background on the format, see our watermarking series and the C2PA specification.

The four stages of a C2PA inspectorRead fileParse manifestValidate signatureShow assertions
The four stages of a C2PA inspector

Option 1: the command line, for a quick look

The fastest way to inspect a file is the open source c2patool. It prints the full manifest as JSON, which is perfect for scripting.

bash
# install once
cargo install c2patool

# print the manifest store as JSON
c2patool image.jpg

# extract just the active manifest
c2patool image.jpg --detailed

If the output is empty or reports no manifest, the file either never carried a credential or has had it stripped by a previous re-save, which, as we cover in the robustness discussion, is trivially easy.

Option 2: read it in code

For a real inspector you want programmatic access. The C2PA project ships language bindings; the Python package exposes the manifest store directly. The sketch below reads a file, pulls the active manifest, and prints the assertions that matter for AI provenance.

python
class="syn-keyword">from c2pa class="syn-keyword">import Reader

class="syn-keyword">def inspect(path):
    with Reader.from_file(path) as reader:
        store = reader.json()   # full manifest store as JSON
        class="syn-keyword">import json
        data = json.loads(store)
        active = data.get("active_manifest")
        manifests = data.get("manifests", {})
        m = manifests.get(active, {})
        print("signed by:", m.get("signature_info", {}).get("issuer"))
        class="syn-keyword">for a class="syn-keyword">in m.get("assertions", []):
            label = a.get("label", "")
            # the assertion that flags generative AI involvement
            class="syn-keyword">if "actions" class="syn-keyword">in label or "ai" class="syn-keyword">in label.lower():
                print("assertion:", label, a.get("data"))

inspect("image.jpg")

The important field is not a single "is AI" boolean. It is the set of actions and generation assertions, together with the signer. A credential signed by a recognised provider that declares a generative action is strong evidence; an unsigned or self signed manifest is much weaker.

Validating the signature

Reading assertions is only half the job. A manifest is only trustworthy if its signature validates and its certificate chains to a trust list you accept. The bindings matter here: a hard binding hashes the file bytes, so any pixel change invalidates it, while a soft binding uses a perceptual hash to survive minor edits. Your inspector should report validation status explicitly rather than treating the presence of a manifest as proof.

Two ways a credential is bound to contentHard binding• SHA-256 over bytes• Breaks on any edit• Strong tamper evidenceSoft binding• Perceptual hash• Survives minor edits• Weaker guarantee
Two ways a credential is bound to content

Handling the common cases

A production inspector needs to handle four outcomes cleanly, because users will hit all of them:

  • Valid credential. Signature checks out, assertions readable. Show the provenance.
  • Invalid credential. A manifest is present but the signature fails. Flag it, do not trust the assertions.
  • No credential. The file carries none. Say so plainly, and do not imply the media is therefore human made.
  • Partial or unknown signer. A manifest that does not chain to a trusted certificate. Surface it as unverified.

Trust lists, the part people skip

A signature that validates only proves the bytes have not changed since signing. It does not, by itself, tell you the signer is who they claim to be. That assurance comes from the certificate chaining to a trust list you accept. C2PA validation involves checking the signer's certificate against a set of trusted anchors, and a production inspector should surface three distinct states: validly signed by a trusted signer, validly signed by an unknown or untrusted signer, and invalid signature. Collapsing these into a single "verified" badge is the most common way inspectors mislead their users, because an attacker can self sign a manifest full of false assertions, and it will happily validate against its own certificate.

The validation questions an inspector must answer in orderSignature valid?Cert chains to trust list?Assertions readableReport state
The validation questions an inspector must answer in order

A minimal reporting layer

Once you have the parsed data, the value is in how you present it. A useful inspector reduces a complex manifest to a few honest lines: who signed it, whether that signer is trusted, when the content was created or edited, and whether any assertion declares generative AI. The snippet below turns the parsed manifest into that summary.

python
class="syn-keyword">def summarize(manifest, trusted):
    sig = manifest.get("signature_info", {})
    issuer = sig.get("issuer", "unknown")
    trusted_signer = issuer class="syn-keyword">in trusted
    ai = any("ai" class="syn-keyword">in a.get("label", "").lower()
             or a.get("label") == "c2pa.actions"
             class="syn-keyword">for a class="syn-keyword">in manifest.get("assertions", []))
    class="syn-keyword">return {
        "signer": issuer,
        "signer_trusted": trusted_signer,
        "declares_ai": ai,
        "verdict": ("trusted" class="syn-keyword">if trusted_signer class="syn-keyword">else "unverified signer"),
    }

Notice the verdict never says "human made". The honest outputs are trusted, unverified signer, invalid, and no credential, and a good inspector refuses to say more than the data supports.

Testing your inspector

Build a small corpus to test against, because the failure modes only show up on real files. Include an image with a valid credential from a known tool, the same image re-saved so the credential is stripped, an image with a deliberately corrupted manifest, and one signed by an untrusted certificate. If your inspector reports all four correctly, valid, absent, invalid, and untrusted, it is doing its job. If it shows a reassuring badge for the untrusted or corrupted cases, it is worse than no inspector at all, because it manufactures false confidence.

Where an inspector fits, and where it does not

An inspector reads provenance; it does not create trust that was never signed in. That is the honest limit. If you are building tooling around content authenticity, pair inspection with the reality that credentials are removable, and design your product to treat absence as inconclusive rather than exonerating. If your goal is the opposite, cleaning metadata from files you own, our image endpoint strips C2PA along with EXIF and XMP without touching pixels, and we describe exactly what that does and does not achieve.

Summary

Reading a Content Credential is a few lines of code: parse the manifest store, validate the signature, and read the assertions with the signer in mind. The subtlety is all in interpretation, present and valid means something, absent means very little, and a good inspector makes that distinction loud. Build it that way and you will avoid the most common mistake in this space, treating provenance metadata as a lie detector rather than a signed, strippable record.

Building it into a product

An inspector is most useful when it is not a standalone script but a step in a pipeline. A publishing platform might inspect uploads and surface a provenance badge only when a credential is present and trusted. A marketplace might flag listings whose images carry a generative AI assertion. A newsroom tool might record the provenance of every asset it ingests. In each case the design principle is the same: read and report, never fabricate trust, and make the four states, trusted, unverified signer, invalid, and absent, visible rather than collapsing them. Wire it in that way and the inspector becomes a quiet, honest source of truth rather than a rubber stamp.

Frequently asked questions

Do I need a network connection to inspect a credential? Reading and validating a manifest is local, but checking the signer against a trust list, and any cloud lookup for durable credentials, may require network access.

Can an inspector prove an image is real? No. It can confirm a valid, trusted credential when one is present. Absence of a credential proves nothing about whether the media is human made.

What tools can I use? The open source c2patool for the command line, and the C2PA language bindings for programmatic access, plus the official Content Credentials verifier for a quick web check.

Keep reading: Zero-width characters in code · Attacks on text watermarks · Image API reference