Adding C2PA Provenance to AI Images and Reading It Back — Writing “AI-Generated” with c2patool and Having AI Explain the Validation

One way to mark an AI-made image as “AI-generated” in a form machines can read is a C2PA provenance record (C2PA stands for the Coalition for Content Provenance and Authenticity). The specification is public, and the tools that write the records are free. But when you read a record back, what you get is JSON full of states and codes such as Valid and signingCredential.untrusted. What has been confirmed and what has not is not obvious at a glance.
This article contains affiliate links. As an Amazon Associate, we earn from qualifying purchases.
So we took one of this site’s article images (a PNG generated with ComfyUI) and attached a C2PA provenance record to it with c2patool 0.28.4 and the test key bundled with c2patool, then read it back. We also read a record that was already attached to an article image from January 2026, signed with a Google LLC certificate. Then we had claude -p (Claude Sonnet 5.5) and gemma4:12b, running on a local GPU, read the JSON that c2patool produced, and scored their answers against an answer key built from the JSON. Every condition was run once. The signed images have not been published, and the images in this article carry no C2PA records.
The results first. In this run, the image signed with the test key came out Valid, and at the same time the output said the signing certificate is not on a trust list (signingCredential.untrusted). The file grew by 59,226 bytes (6.8%), and signing, including the validation that runs right after it, took 0.09 seconds.
The January 2026 image, on the other hand, was Invalid, because an expired certificate (signingCredential.expired) was added to the failures. Both AI models answered “is it recorded as AI-generated?” and “who signed it?” correctly in all 6 runs. The difference was in how they listed “what has not been verified”: only Claude pointed out that whether the content of the record is true is not something validation checks.
Putting machine-readable marks on AI-made images also ties into the transparency rules of the EU AI Act. The deadlines and the framework are covered in Where AI-Generated Labels and Marks Disappear in Images and 3D Data (published 2026-11-09) and Update the Law Before You Memorise It: Japan AI Act, Copyright, and the EU AI Act (published 2026-10-04). This article is not a legal explainer. It checks what you get when a creator attaches a C2PA record on their own machine and reads it back.
- C2PA provenance is read as a signed “manifest” embedded in the file
- “AI-generated” is the digitalSourceType on c2pa.created, and AI disclosure is a separate assertion
- Validation states have different names in the specification and in c2patool — Well-Formed, Valid, Trusted and trust lists
- Test conditions — this site’s ComfyUI image, signed with c2patool’s test key
- Results — Valid, but the signer is not on a trust list, and the file grew by 59,226 bytes
- The Google LLC signature on a January 2026 article image was Invalid
- We had claude -p and gemma4:12b read c2patool’s JSON and scored them against an answer key built from the JSON
- How to read it — passing validation does not prove “AI-generated”
- What Valid guarantees is that the record is bound to this file and has not been tampered with
- The signer and the software that created the claim are separate fields
- The field named issuer held the organization name of the signing certificate
- untrusted means not on the validator’s trust list, not fake
- Time-stamps and revocation checks
- When you have AI read it, put what is missing from the JSON into the questions
- What these measurements cannot tell you
- Summary — read C2PA provenance records by separating the state, the codes, the signer and the declaration
- Sources
C2PA provenance is read as a signed “manifest” embedded in the file
A C2PA record is embedded in the image file as a “manifest.” At the core of the manifest is a “claim” about the file. The claim holds a list of individual statements called “assertions” (how the file was made, which model was used and so on) plus the name of the software that created the claim (claim_generator_info), and the whole claim is signed with an X.509 certificate. c2patool’s default signing algorithm is ECDSA combined with SHA-256 (ES256).
Manifest, assertion and claim are the specification’s own terms, and this article uses those three words throughout. The specification is titled “Content Credentials : C2PA Technical Specification,” and C2PA records are also called Content Credentials. The current version is 2.5 (October 2026). The specification is published under CC BY 4.0, so whenever this article quotes its English text, it cites the source.
Assertions can live in two places. According to the specification, everything in created_assertions is attributed to the signer: “All created_assertions are attributed to the signer as the Trust Model is rooted in the trust of the signer.” gathered_assertions, by contrast, are part of the claim, but the specification explains that they do not originate from the software that created the claim and are not attributed to the signer either.
In c2patool’s JSON output, this distinction shows up in each assertion’s created key. "created": true means the assertion is on the side attributed to the signer (created); if the key is absent, it is on the side that is not (gathered). The default in c2pa-rs (the Rust SDK that c2patool uses) is false, so an assertion you add to the manifest JSON becomes gathered unless you specify otherwise. This difference matters later, both in the measurements and in scoring the AI.
When it reads a file, c2patool writes the validation results into the same JSON. This article reads the following keys.
active_manifest: the name of the currently active manifest; it tells you which entry inmanifeststo readmanifests[…].assertions: the list of assertions; each element haslabel,dataandcreatedmanifests[…].signature_info: signature information such asalg,issuerandcommon_namemanifests[…].claim_generator_info: the name and version of the software that created the claimvalidation_state: the validation state (one of Invalid, Valid or Trusted)validation_status: an array that lists only failure codesvalidation_results.activeManifest: three arrays, for success, informational and failure codes
Provenance is one of several production concerns once you ship products built on generative models. For the wider picture, from evaluation to guardrails, this book is a solid reference.
“AI-generated” is the digitalSourceType on c2pa.created, and AI disclosure is a separate assertion
Where in a C2PA record do you write “generated with AI”? The answer is the digitalSourceType value attached to the c2pa.created action at the start of the c2pa.actions assertion.
Section 16.4.1.2 of specification 2.5 requires that for a newly created asset, the first action in the first c2pa.actions list be c2pa.created, with a digitalSourceType recorded on it: “For all assets, a corresponding digitalSourceType field, with an appropriate value, shall be recorded with the c2pa.created action, to indicate the nature of the asset at its inception.”
Note that c2pa.created by itself does not mean AI generation. The specification describes c2pa.created as follows.
c2pa.created The asset was first created or generated. This action applies equally to assets created by humans, captured by a camera, generated by AI, produced by other means, or any combination thereof.
(Source: C2PA Technical Specification 2.5, CC BY 4.0)
In other words, a picture drawn by a person and a photo taken with a camera both start with the same c2pa.created action. What marks AI generation is the digitalSourceType value attached to it. The value is chosen from the IPTC vocabulary (the NewsCodes Digital Source Type) or from the C2PA-specific values defined in the specification. IPTC terms are recorded as http-scheme URIs starting with cv.iptc.org/newscodes/digitalsourcetype/, but c2patool accepts the short names. This article deals with the following three IPTC terms.
| Value (short name) | IPTC name (en-GB) | IPTC definition (en-GB) |
|---|---|---|
| trainedAlgorithmicMedia | Created using Generative AI | Digital media created algorithmically using an Artificial Intelligence model trained on captured content |
| compositeWithTrainedAlgorithmicMedia | Edited using Generative AI | Augmentation, correction or enhancement using a Generative AI model, such as with inpainting or outpainting operations |
| algorithmicMedia | Pure algorithmic media | Media created purely by an algorithm not based on any sampled training data, e.g. an image created by software using a mathematical formula |
(Source: IPTC NewsCodes “Digital Source Type,” CC BY 4.0)
Put simply, trainedAlgorithmicMedia is “made with generative AI”: media produced by an AI model trained on captured content such as photos. compositeWithTrainedAlgorithmicMedia is “edited with generative AI”: corrections or additions made by generative AI, such as inpainting or outpainting. algorithmicMedia is made purely by an algorithm that is not based on training data. Because no trained model is involved, the scoring in this article does not count it as AI-generated.
The C2PA-specific values are two, starting with c2pa.org/digitalsourcetype/. empty stands for media that is essentially empty (a blank canvas, a zero-length video and so on). trainedAlgorithmicData stands for data that results from algorithmically using a model built from captured content and data. The specification explains that it differs from trainedAlgorithmicMedia in that the result is a data format such as CSV or pickle rather than media such as images or video.
In c2patool, signing with --create trainedAlgorithmicMedia automatically adds a c2pa.created action as a new creation. Conversely, with neither --create nor --update, c2patool treats the operation as an “edit”: it makes the original file a parent ingredient and adds a c2pa.opened action. In that case the first action is c2pa.opened. To record a newly AI-generated image, use --create.
AI disclosure (c2pa.ai-disclosure) records which model was used
The confusing one is c2pa.ai-disclosure. According to the specification’s version history, it is an assertion for machine-readable AI transparency information that was added in 2.4 (April 2026); in 2.5 it lives in section 16.8.2. This article calls it “AI disclosure.” The only required field is modelType, whose value is chosen from Table 8 of the specification (Model type values). modelName (a human-readable model name), modelIdentifier, contentProfile, scientificDomain and metadata (additional information about the assertion) are optional.
In contentProfile, the degree of human involvement (humanOversightLevel) can be given at three levels: fully_autonomous, prompt_guided and human_validated. Section 16.8.2.3 of the specification has a table interpreting its combinations with digitalSourceType.
| digitalSourceType | humanOversightLevel | Interpretation in the specification |
|---|---|---|
| trainedAlgorithmicMedia | fully_autonomous | Pure AI generation, no human review |
| trainedAlgorithmicMedia | prompt_guided | AI generation guided by human prompts |
| compositeWithTrainedAlgorithmicMedia | human_validated | AI edits reviewed and approved by a human editor before publication |
| digitalCreation (made by a person with non-generative tools) | not applicable | No trained model invoked; AI Model Disclosure assertion is not attached |
The specification says AI disclosure does not replace the existing signals: “The c2pa.ai-disclosure assertion is designed to complement existing C2PA signals rather than replace them.” What indicates AI generation is still the digitalSourceType on c2pa.created; AI disclosure adds “with which model” on top of that. In this run, the AI disclosure contained only modelType and modelName, with no humanOversightLevel. For modelType, Table 8 has no value for the GGUF format, so we used c2pa.types.model, which stands for a model that does not fit any of the other types.
So can validation check the content of an AI disclosure? ai-disclosure is not in c2pa-rs 0.91.7’s list of known assertion labels, and the specification asks validators not to check inputs against the schemas: “Validators should not validate inputs against these schemas.” There is therefore no basis for assuming that c2patool’s validation checks whether modelType is a Table 8 value. In this run, c2patool wrote the given values as they were and returned the same values on read-back.
Validation states have different names in the specification and in c2patool — Well-Formed, Valid, Trusted and trust lists
The first thing to look at when reading a C2PA record is the validation state, and this is where the specification and c2patool use different words. The specification defines three nested states: “A C2PA Manifest may be one of the following: Well-Formed Valid Trusted Any Trusted manifest is also Valid, and any Valid manifest is also Well-Formed.”
Valid has five conditions (13.3.5): the manifest is Well-Formed; the manifest has not been changed since it was signed; the claim signature gets claimSignature.validated; the signature validity-period check gets claimSignature.insideValidity; and the signer’s certificate has not been rejected as revoked (signingCredential.ocsp.revoked). The condition for Trusted (13.3.6) is being Valid plus the signing certificate getting signingCredential.trusted.
The validation_state in c2pa-rs, which c2patool uses, has three values — Invalid, Valid and Trusted — and none of them corresponds to Well-Formed. Anything that is Well-Formed in shape but does not meet the Valid conditions becomes Invalid. On top of that, c2pa-rs treats a manifest as Valid if the only failure code is “signing certificate not on a trust list” (signingCredential.untrusted). Annex D of the specification, however, classifies signingCredential.untrusted as a failure code. As a result, a Valid JSON has one failure code listed in its validation_status. Besides signingCredential.untrusted, the failures c2pa-rs tolerates when judging Valid also include failures of CAWG (Creator Assertions Working Group) identity assertions (codes starting with cawg.x509. or cawg.identity.). None of the three JSON outputs in this article had CAWG codes.
| State in the specification | validation_state in c2pa-rs | Main conditions |
|---|---|---|
| Well-Formed (but not Valid) | Invalid | Does not meet the Valid conditions |
| Valid | Valid | Well-Formed, unchanged since signing, claimSignature.validated, claimSignature.insideValidity, not rejected as revoked (signingCredential.ocsp.revoked). Valid if the only failures are signingCredential.untrusted (and CAWG codes) |
| Trusted | Trusted | signingCredential.trusted |
validation_status holds only failure codes; the success and informational codes are in the success and informational arrays of validation_results.activeManifest. So if you look only at validation_status, you will miss warnings that come out as informational codes.
The specification is also clear about what Valid means. If a manifest is Valid, its claim can be attributed to the software that created the claim, as identified by claim_generator_info: “If a C2PA Manifest is Valid, then the manifest’s claim can be attributed to the claim generator which is identified by the claim_generator_info field of the claim.” The signer (the certificate) and the software that created the claim are separate fields. This distinction also becomes a criterion later, when scoring the AI’s answers.
Trust lists differ from tool to tool
Whether a manifest becomes Trusted depends on which trust list the validator has. Section 13.4 of the specification requires validators to include the C2PA Trust List among their trust anchors, and says they should let users configure additional trust anchors. According to the C2PA FAQ, the C2PA Trust List is a list of certificate authorities (CAs) allowed to issue signing certificates to generator products, and only products that have been reviewed and approved through the conformance program can get certificates from those CAs.
c2patool’s defaults are as follows. The c2pa-rs settings check against a trust list by default (verify_trust: true), but the default trust anchors are empty (as far as we could tell from reading the source). If you specify a trust list with the trust subcommand, c2patool’s documentation says it fetches the list from the network on every run. In this run, we passed neither a trust list nor a settings file.
There are browser-based tools too. According to the open-source documentation (opensource.contentauthenticity.org) of the Content Authenticity Initiative (CAI), a cross-industry group led by Adobe, the Content Credentials inspection site (Verify, verify.contentauthenticity.org) checks against an interim trust list (ITL), and the ITL was frozen on January 1, 2026. The same documentation says Verify shows the warning “The Content Credential issuer couldn’t be recognized. This file may not come from where it claims to.” for files signed with certificates it does not know, and shows the owner’s name and the signing time for certificates it does know.
c2patool’s documentation also says Adobe’s Inspect uses the official C2PA Trust List. Because trust lists differ between tools, the same file can be displayed differently. The images in this article were not uploaded to Verify or Inspect.
Test conditions — this site’s ComfyUI image, signed with c2patool’s test key
The tool was the Windows build of c2patool 0.28.4. We downloaded the asset v0.28.4-x86_64-pc-windows-msvc.zip from the official GitHub release (published October 26, 2026 (UTC), marked Latest) and checked it against the SHA-256 listed on the release (00e5bcfef95fcf913cc3d67825d7d50ea1eb1180c7fdbef746c9ce3b65e272f1). The version string is “c2patool 0.28.4,” and internally it uses c2pa-rs 0.91.7 (crate name c2pa). The license is MIT OR Apache-2.0, at your choice.
The PC was Windows 11 Pro with an Intel Core Ultra 7 265F, 63.7 GB of RAM and an RTX 5070 Ti 16 GB.
The input was the cover image of the 2026-11-02 article (a PNG generated with ComfyUI, 872,766 bytes). The original file was left untouched, and the signed file was written out separately. The signed file has not been uploaded anywhere.
For signing we used the test key and certificate bundled with c2patool (es256_private.key and es256_certs.pem in the sample folder, ES256). c2patool uses this certificate as a built-in default when no signing settings are given, and its documentation says “This is only suitable for development.” The certificate’s organization is “C2PA Test Signing Cert,” its common name is “C2PA Signer,” its organizational unit is “FOR TESTING_ONLY,” and it is valid until August 2030. We did not buy a real signing certificate.
Files signed with this certificate show “signer not on a trust list” (signingCredential.untrusted) on validation. c2patool’s documentation also says its sample images are signed with a test certificate and therefore show signingCredential.untrusted. Please read the results below with that in mind.
There were three conditions.
| Condition | Input | Signing | AI disclosure (c2pa.ai-disclosure) |
|---|---|---|---|
| Image with AI disclosure as created | Cover image of the 2026-11-02 article | --create trainedAlgorithmicMedia, test key | modelType and modelName, with "created": true |
| Image with AI disclosure as gathered | Same | Same | Same content, without created |
| January 2026 image | Cover image of the 2026-01-13 article (587,246 bytes) | Not signed (read only) | — |
Neither signing specified a time-stamp authority (ta_url). c2patool’s sample manifest JSON (test.json) specifies a DigiCert time-stamp, and if you use it as is, every signing connects to an external service. This time we avoided external connections, so no third party vouches for the signing time. In the specification, a time-stamp is optional supplementary evidence: “a time-stamp in a C2PA Manifest is optional supplementary evidence.”
The signing and read-back commands were as follows. Read-back used the default JSON without -d (detailed report), and that JSON is what we gave the AI.
c2patool <input PNG> -m <condition>_manifest.json --create trainedAlgorithmicMedia -o <output PNG> -f
c2patool <output PNG>For the image with AI disclosure as created, the manifest JSON was as follows (the key and certificate paths are replaced with placeholders). The gathered image was identical except that the final "created": true was missing.
{
"alg": "es256",
"private_key": "<c2patool folder>/sample/es256_private.key",
"sign_cert": "<c2patool folder>/sample/es256_certs.pem",
"claim_generator_info": [{"name": "swiftwand-provenance-test", "version": "0.1"}],
"title": "cover.png",
"assertions": [
{
"label": "c2pa.ai-disclosure",
"data": {
"modelType": "c2pa.types.model",
"modelName": "Z-Image-Turbo (GGUF Q6_K) via ComfyUI"
},
"created": true
}
]
}c2pa.actions is not written in the manifest JSON, because --create trainedAlgorithmicMedia adds c2pa.created and the digitalSourceType. The claim_generator_info name (swiftwand-provenance-test) is a name we made up for this experiment.
Results — Valid, but the signer is not on a trust list, and the file grew by 59,226 bytes
| Condition | Size after signing (bytes) | Signing time (s) | validation_state | validation_status (failure) | informational | issuer | common_name |
|---|---|---|---|---|---|---|---|
| Image with AI disclosure as created | 931,992 | 0.09 | Valid | signingCredential.untrusted | signingCredential.ocsp.skipped | C2PA Test Signing Cert | C2PA Signer |
| Image with AI disclosure as gathered | 931,992 | 0.093 | Valid | signingCredential.untrusted | signingCredential.ocsp.skipped | C2PA Test Signing Cert | C2PA Signer |
| January 2026 image | — (not signed) | — | Invalid | signingCredential.untrusted, signingCredential.expired | timeStamp.untrusted | Google LLC | Google Media Processing Services |
Both images signed with the test key were Valid. For the image with AI disclosure as created, success listed claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match (4 entries) and assertion.dataHash.match, and failure had just one entry, signingCredential.untrusted. In other words, the signature could be verified, it was within its validity period, and the hash of the image data matched the record.
On top of that, the signing certificate is not on a trust list. c2pa-rs tolerates this failure when judging Valid, so the state is Valid, not Trusted. Put another way, the signature and tamper checks passed (Valid), but because the signer is not on a trust list, it is not Trusted. This is not an “invalid signature” or a “failed validation.”
Here are the main parts of the read-back JSON (URLs, IDs, serial numbers and the like are omitted).
"assertions": [
{
"label": "c2pa.ai-disclosure",
"data": {
"modelName": "Z-Image-Turbo (GGUF Q6_K) via ComfyUI",
"modelType": "c2pa.types.model"
},
"created": true
},
{
"label": "c2pa.actions.v2",
"data": {
"actions": [
{
"action": "c2pa.created",
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
}
]
},
"created": true
}
],
"signature_info": {
"alg": "Es256",
"issuer": "C2PA Test Signing Cert",
"common_name": "C2PA Signer"
},
"validation_status": [
{"code": "signingCredential.untrusted", "explanation": "signing certificate untrusted"}
],
"validation_state": "Valid"The action added by --create was in the assertion labeled c2pa.actions.v2. The first action is c2pa.created, and its digitalSourceType is the full URI of trainedAlgorithmicMedia. In both conditions this assertion has "created": true, so it sits on the side attributed to the signer.
The AI disclosure, on the other hand, had "created": true in the image where we wrote created; in the image where we did not, the created key was absent altogether and the assertion was treated as gathered. The validation state was Valid in both and was not affected by this difference. Note also that validation_results.specVersion was 2.4.0 in all three JSON outputs, a different version number from C2PA specification 2.5.
The file grew by 6.8%, and signing took 0.09 seconds
The file size went from 872,766 bytes to 931,992 bytes. The difference is 931,992 − 872,766 = 59,226 bytes, which is 59,226 ÷ 872,766 ≈ 6.8% of the original size. Whether the AI disclosure was created or not made no difference to the size.
The read-back JSON had a thumbnail (c2pa.thumbnail.claim) in image/jpeg format, and the added bytes include this thumbnail. By default, c2pa-rs creates a thumbnail with a long edge of 1024, and for a PNG without alpha it makes a JPEG thumbnail. So the signature and the assertions are not the only reasons a file with a record gets bigger.
Signing took 0.09 and 0.093 seconds of wall-clock time. By default c2patool validates right after signing (you can skip it with --no_signing_verify), so this time includes validation. And since no time-stamp was attached, it includes no network round trip. Both values are for one image and one run each, not for other images or other PCs.
The Google LLC signature on a January 2026 article image was Invalid
The third condition is the cover image of the 2026-01-13 article. It dates from before this site started making article images with ComfyUI, and the local file carried a C2PA record signed with a Google LLC certificate. The software that created the claim is Google C2PA Core Generator Library, the first action is c2pa.created, and the digitalSourceType is trainedAlgorithmicMedia. There was no thumbnail assertion.
The validation state was Invalid. failure had two entries: signingCredential.untrusted and signingCredential.expired (the certificate has expired; this code is not in Annex D of the specification but is defined by c2pa-rs). As noted earlier, the failures c2pa-rs tolerates when judging Valid are signingCredential.untrusted and the CAWG codes, and signingCredential.expired is not among them. So this file, with the expiry added, is Invalid. Meanwhile, success had claimSignature.validated, claimSignature.insideValidity and assertion.dataHash.match: the signature itself could be verified, and the hash of the image data matched the record.
For the time-stamp, success had timeStamp.validated and informational had timeStamp.untrusted. This means the time-stamp content matched, but its issuer is not on the trust list used in this run. Unlike the two test-key images, this signature carries a time-stamp.
Expired and Invalid mean that c2patool judged it that way in this run, not that the image is fake. Also, because we read it without passing any trust list, the Google LLC certificate was judged untrusted as well. A tool with a different trust list, such as Verify or Inspect, may display it differently. How the expiry judgment relates to the signing time was not checked from the fields in this JSON.
At which stage on the way to publication the records on this site’s article images survive, and where they disappear, is counted in Where AI-Generated Labels and Marks Disappear in Images and 3D Data (published 2026-11-09). This article only covers what we got from reading the records in local files.
We had claude -p and gemma4:12b read c2patool’s JSON and scored them against an answer key built from the JSON
What we gave the models and what we asked
We gave the AI the full default JSON output from c2patool and the questions below, and the system prompt for both models was a single sentence: “You are a careful analyst of C2PA manifests.” What c2patool writes to standard error (such as the warning when the built-in key is used) is not part of the JSON and was not given to the AI. In this run, standard error was empty for both signing and read-back. The prompt was written in Japanese; below is an English translation, and in the reply format at the end, the parts in < > have been rewritten for this article.
The following is the JSON output from reading an image file with a C2PA reading tool (c2patool).
Answer only from what is written in this JSON. Do not fill in anything that is not in the JSON by guessing.
Q1. Is this image recorded as generated with AI? Choose one of these four:
"Recorded as generated with AI" / "Recorded as edited with AI" / "No record of AI generation" / "Cannot determine"
Q2. Who signed this record? Give the organization name and the common name of the signing certificate.
Q3. Within what this JSON shows, list as bullet points what has not been verified (what has not been confirmed).
Return only JSON in the following form:
{"q1": "<answer to Q1>", "q1_reason": "<reason>", "q2_org": "<organization name>", "q2_common_name": "<common name>", "q3": ["<item>", "<item>"]}claude -p is the way to run Claude Code non-interactively. The model was specified with --model sonnet, and the model ID recorded in the output was claude-sonnet-5-5. We used a minimal configuration with no built-in tools (--tools "") and no MCP (an empty config plus --strict-mcp-config), and called it from an empty folder outside the home directory. Claude Sonnet 5.5 costs $2 for input and $10 for output (per million tokens, as of November 2026).
The local model was gemma4:12b running on Ollama 0.40.13. Gemma 4 12B is an Apache 2.0 model released by Google DeepMind on 2026-06-03. The build we used (digest 6114515d63c1) has 11.9B parameters with Q4_K_M quantization and an 8.0 GB download (the download size is not the VRAM usage). Thinking was turned off (think: false), the context length (num_ctx) was 32768, and the temperature was 0.
An example of having the same two models read power logs is in Letting AI Read Per-Appliance Power Logs — How Raw Logs, Hourly Tables and Code Execution Change the Accuracy of the Answers (published 2026-11-03).
How we built the answer key
The answer key was determined from the keys in c2patool’s JSON. It is not ground truth about whether the image really was generated with AI. This image was generated with ComfyUI, but there is no way to confirm that from the JSON.
- Q1: if the first action in
c2pa.actions(orc2pa.actions.v2) of the manifest thatactive_manifestpoints to is c2pa.created and the digitalSourceType is trainedAlgorithmicMedia, the answer is “recorded as generated with AI.” All three JSON outputs met this - Q2:
issuerandcommon_nameinsignature_info. For the two test-key images, C2PA Test Signing Cert and C2PA Signer; for the January 2026 image, Google LLC and Google Media Processing Services - Q3: whichever of items (a) to (e) below hold for that JSON
| Item | Condition (JSON keys) | Expected statement | Images where it held |
|---|---|---|---|
| (a) Signer’s identity | validation_status has signingCredential.untrusted | The signing certificate is not on a trust list, so who the signer is has not been confirmed | All three |
| (b) Signing time | No time-stamp: success has no timeStamp.validated. With a time-stamp: informational has timeStamp.untrusted | No time-stamp: no third party (a time-stamp issuer) vouches for the signing time. With a time-stamp: the time-stamp issuer is not on a trust list | All three |
| (c) Revocation check | informational has signingCredential.ocsp.skipped | Whether the certificate has been revoked was not checked online | The two test-key images |
| (d) Attribution of AI disclosure | c2pa.ai-disclosure has no "created": true | The AI disclosure statement is not attributed to the signer (gathered) | Only the image with AI disclosure as gathered |
| (e) Truth of the content | Always holds (determined from the specification, not from JSON keys) | Whether the statement “generated with AI” is true is not something validation checks | All three |
Because (e) is not determined from JSON keys, we kept it separate from (a) to (d) and gave scores both with and without (e). In addition, before the runs we decided to count the following answers as “errors.”
- Saying “the signature is invalid” or “the signature is broken” when success has claimSignature.validated
- Saying “it has been tampered with” when success has assertion.dataHash.match
- Saying “Trusted (a trusted signer)” or “Invalid” when validation_state is Valid
- Giving the claim_generator_info software as the signer (Q2)
- Presenting the test certificate as the formal signature of a real organization
- Saying “validation passed, so AI generation has been proven” (Q1)
Scoring results
| Image | Model | Q1 | Q2 | Correct Q3 items | Items listed (excluding (e)) | Q3 (without (e)) | Q3 (with (e)) | Errors 1–6 | Cost (USD) | Time (s) |
|---|---|---|---|---|---|---|---|---|---|---|
| Image with AI disclosure as created | claude -p | Correct | Correct | a, b, c | a, b, c | 3/3 | 4/4 | 0 | 0.021666 | 13.1 |
| Image with AI disclosure as created | gemma4:12b | Correct | Correct | a, b, c | a, c | 2/3 | 2/4 | 0 | — | 34.5 |
| Image with AI disclosure as gathered | claude -p | Correct | Correct | a, b, c, d | a, c | 2/4 | 3/5 | 0 | 0.017916 | 8.8 |
| Image with AI disclosure as gathered | gemma4:12b | Correct | Correct | a, b, c, d | a, c | 2/4 | 2/5 | 0 | — | 2.2 |
| January 2026 image | claude -p | Correct | Correct | a, b | a, b | 2/2 | 3/3 | 0 | 0.020338 | 11.8 |
| January 2026 image | gemma4:12b | Correct | Correct | a, b | a, b | 2/2 | 2/3 | 0 | — | 2.1 |
The cost is the total_cost_usd returned by claude -p, rounded to 6 decimal places (the recorded values were 0.021665999999999998 and 0.020338000000000002; we dropped the floating-point display residue). It is a client-side estimate and can differ from the actual bill. gemma4:12b ran on a local GPU, so its API cost is 0, and electricity is not included. The 34.5 seconds for gemma4:12b’s first run was the first call of this whole run and likely includes loading the model (the loading time itself was not recorded).
The difference was in how they listed what has not been verified
In all 6 runs, Q1 and Q2 were correct, and no answer fell under errors 1 to 6. Both models got whether the image is recorded as AI-generated from the digitalSourceType value, and the signing certificate’s organization and name from signature_info. No run restated Valid as “a trusted signer,” and none gave the software that created the claim (swiftwand-provenance-test) as the signer.
The difference was in Q3. In this run, both models listed the items that are written as codes in the JSON: signingCredential.untrusted ((a)), signingCredential.ocsp.skipped ((c)) and, for the January 2026 image, timeStamp.untrusted ((b)). Items that have to be read from what the JSON does not contain, by contrast, were dropped. Only Claude, on the image with AI disclosure as created, mentioned that the two test-key images have no time-stamp ((b)). Neither model mentioned that the gathered AI disclosure is not attributed to the signer ((d)).
(e), which is determined from the specification, was listed by Claude for all three images and by gemma4:12b for none. On the image with AI disclosure as created, Claude also wrote, unprompted, that “Valid does not mean the signer can be trusted.” Both models listed signingCredential.expired for the January 2026 image. That is outside (a) to (e) and is not a mistake.
Claude’s answers did have one point of concern outside the answer key. On the image with AI disclosure as gathered, it described issuer as “the name of the organization that issued the certificate,” and also wrote that the signer’s own organization name is not in the JSON. In the c2pa-rs code, issuer holds the organization name of the signing certificate (covered in the next section). Q2 was scored on the organization name and common name values given, so it counts as correct, and since this does not fall under the definitions of errors 1 to 6 it was not counted as an error. Still, it is something readers should watch for.
If you want to repeat this kind of comparison with a local model, a hands-on guide to running and using LLMs is a good place to start.
How to read it — passing validation does not prove “AI-generated”
What Valid guarantees is that the record is bound to this file and has not been tampered with
Reading a Valid JSON as “AI generation has been proven” is a mistake. The specification’s guidance says that validation does not judge whether the content of the record is good or bad, only the following.
merely whether the assertions included within can be validated as associated with the underlying asset, correctly formed, and free from tampering.
(Source: C2PA Technical Specification 2.5, CC BY 4.0)
So what validation confirms is that the assertions are bound to this file, are correctly formed and have not been tampered with. The digitalSourceType value meaning “generated with AI” is something the signer specified with --create or in the manifest JSON: it is the creator’s declaration. Here, too, the value was written by this site’s experiment script. All that Valid tells you is that this declaration has not been rewritten since it was signed.
The signer and the software that created the claim are separate fields
The JSON has two fields that answer “who.” One is signature_info, which describes the signing certificate. The other is claim_generator_info, which holds the name and version of the software that created the claim. In this JSON, the former was C2PA Test Signing Cert and C2PA Signer, and the latter was swiftwand-provenance-test 0.1.
The specification says that if a manifest is Valid, the claim is attributed to the software that created the claim, and that created assertions are attributed to the signer. But the name of the software that created the claim was, once again, just the name we wrote into the manifest JSON for this experiment. Not mixing up the signer field and the software-name field is the first thing to watch when reading.
The field named issuer held the organization name of the signing certificate
Another confusing one is signature_info.issuer. The CAI documentation (the Reader schema in the SDK object reference) describes issuer as “Human-readable issuing authority for this signature.” The c2pa-rs code, however, puts the organization name from the subject of the signing certificate there.
And in fact, in this run the field named issuer held the signing certificate’s organization name, “C2PA Test Signing Cert.” The organization name of the intermediate CA that issued the test certificate is “C2PA Test Intermediate Root CA,” and it does not appear in the JSON. If the code holds, the “Google LLC” of the January 2026 image is also the signing certificate’s organization name.
Claude’s description of issuer as “the name of the organization that issued the certificate” is a reading that follows the documentation’s wording. But at least in c2patool 0.28.4’s output in this run, reading this field as “the CA that issued the certificate” does not match what is actually there.
untrusted means not on the validator’s trust list, not fake
Annex D of the specification describes signingCredential.untrusted as “The signing credential is not listed on any of the validator’s applicable trust lists.” It means the credential is not on the validating side’s trust lists, not that the signature is fake. Since we passed no trust list this time, both the test certificate and the Google LLC certificate got this judgment.
A manifest becomes Trusted when the signing certificate chains to a CA on the validator’s trust list. According to the FAQ, the products that can get certificates from C2PA Trust List CAs are those approved through the conformance program. This article did not try signing with a real certificate and did not check what is shown when a manifest is Trusted.
Time-stamps and revocation checks
The two test-key images have no time-stamp, so no third party vouches for their signing time. And signingCredential.ocsp.skipped in informational shows that whether the certificate had been revoked was not checked online (OCSP). The specification requires validators to emit this informational code when they decide not to perform an online OCSP check. By default, c2pa-rs does not fetch revocation information (ocsp_fetch: false).
The absence of a time-stamp does not show up as a code anywhere in the JSON. The skipped revocation check only shows up as an informational code, not in validation_status. Both are things you will not notice if you only look at failure codes.
When you have AI read it, put what is missing from the JSON into the questions
What the two models dropped in this run were the items that have to be read from what the JSON does not say. So when having AI explain C2PA JSON, one approach is to write “Is there a time-stamp?”, “Does the AI disclosure assertion have created?” and “Which field of which certificate does issuer come from?” into the questions and check them one by one. This question format, however, was not tested in this article.
Also, the AI’s explanation is only an aid to reading the JSON. Check the validation state and the list of success, informational and failure codes in c2patool’s output itself. In this run too, questions that take values straight out of the JSON, like Q1 and Q2, were answered correctly in all 6 runs, but Q3 had missed items.
What these measurements cannot tell you
Here are the limits to keep in mind when reading the results.
- One run per condition: two images (one of them signed under two conditions), and two AI models that each read every JSON once. Variation in the answers was not measured. All numbers are “this is how it went in this run” values and do not show the models’ general ability
- The answer key was built from the JSON: the scoring key came from c2patool’s JSON keys and the specification, not from ground truth about whether the images really were AI-generated
- Test key only: no real certificate was used, and the Trusted case was not checked. No trust list was passed. Nothing was uploaded to Verify or Inspect; what they display is taken from their documentation
- Size and time: values for one PNG and one run each, not for other images, formats or PCs. Attaching a time-stamp (ta_url) and fetching revocation information were not measured
- Processing after signing: how validation changes when a signed file is downscaled or re-saved is not covered in this article
- Versions: the specVersion in c2patool 0.28.4’s output was 2.4.0. These results do not show that “c2patool supports specification 2.5”
- c2pa-python: c2pa-python (MIT OR Apache-2.0), for working from Python, was not used. The latest release on PyPI, 0.38.2 (2026-10-26), ships only two macOS wheels, and the latest release with a Windows wheel is 0.38.0 (2026-09-29). Its classifiers do list Windows
- What the models know: how much the two models know about the C2PA specification from their training was not checked
- Law: this article is not legal advice. Whether this signing satisfies any legal obligation is not covered
Summary — read C2PA provenance records by separating the state, the codes, the signer and the declaration
- In C2PA, “generated with AI” is indicated by the digitalSourceType (trainedAlgorithmicMedia) attached to c2pa.created at the start of
c2pa.actions. c2pa.ai-disclosure (added in 2.4, with modelType required) is a separate assertion that records which model was used - Signing this site’s ComfyUI image with c2patool 0.28.4 and the test key gave Valid in this run, with the signer not on a trust list (signingCredential.untrusted). The file grew by 59,226 bytes (6.8%, including a thumbnail), and signing took 0.09 seconds including the validation that follows it
- The Google LLC record on the January 2026 image was Invalid because an expiry (signingCredential.expired) was added. The signature and the data hash matched, so this does not mean the image is fake
- The specification’s Well-Formed, Valid and Trusted do not line up with c2patool’s Invalid, Valid and Trusted. c2patool (c2pa-rs) treats a manifest as Valid if the only failure is untrusted
claude -pand gemma4:12b, given the JSON, answered Q1 and Q2 correctly in all 6 runs, with 0 errors of types 1 to 6. On “what has not been verified,” only Claude mentioned the truth of the content ((e)), and neither mentioned that the AI disclosure is not attributed to the signer ((d))- What Valid guarantees stops at the record being bound to the file and not tampered with; the AI-generation record is the creator’s declaration. In this output, the issuer field held the signing certificate’s organization name
- This article is not legal advice
Sources
- C2PA Specifications 2.5 (list of documents)
- Content Credentials : C2PA Technical Specification 2.5
- IPTC NewsCodes “Digital Source Type”
- contentauth/c2patool (GitHub)
- c2patool v0.28.4 release
- c2patool docs/usage.md (v0.28.4)
- c2patool docs/manifest.md (v0.28.4)
- c2patool docs/signing.md (v0.28.4)
- c2pa-rs sdk/src/validation_results.rs (c2pa-v0.91.7)
- c2pa-rs sdk/src/settings/mod.rs (c2pa-v0.91.7)
- c2pa-rs sdk/src/settings/builder.rs (c2pa-v0.91.7)
- c2pa-rs sdk/src/cose_validator.rs (c2pa-v0.91.7)
- CAI open-source SDK documentation: Introduction (opensource.contentauthenticity.org)
- SDK object reference: Reader schema (opensource.contentauthenticity.org)
- Trust lists (opensource.contentauthenticity.org)
- C2PA FAQ
- Creator Assertions Working Group (CAWG)
- Content Credentials inspection site (Verify)
- c2pa-python on PyPI
- Anthropic: Models overview
- Anthropic: Pricing
- Claude Code CLI reference
- Claude Code: running non-interactively (headless)
- Ollama model page: gemma4:12b
- Ollama documentation: Thinking
- Google: Introducing Gemma 4 12B
- Google: Apache License 2.0 | Gemma





