ChronoVerify

What a C2PA signature covers, and how one ended up covering nothing

A note from ChronoVerify, built and maintained by an ex intelligence analyst, 20+ years.

A C2PA Content Credential is tied to its image by a hash of the file's bytes, minus any byte ranges the signer marks as excluded. On October 2, 2026, David Buchanan showed that a signer can exclude the entire file and get a valid signature over nothing at all. The C2PA specification already tells validators to reject that. ChronoVerify did not reject it until October 14, 2026, the day we reproduced it against our own validator and shipped a check.

How a credential is tied to the picture

A C2PA manifest makes statements about an image: who signed it, which tool made it, what edits were made. Those statements only mean something if they are locked to one specific file. The lock is called the hard binding. For JPEG, PNG and WebP it is an assertion named c2pa.hash.data, which holds a SHA-256 hash of the file's bytes. The hash sits inside the manifest, and the manifest is signed.

A validator recomputes that hash over the file it was handed. Change one byte of the picture and the two hashes differ, and the credential fails with the status code assertion.dataHash.mismatch.

The manifest is stored inside the file it describes, so the hash cannot include the manifest: the signer would be hashing a file that contains its own hash. The spec gets around this with exclusions, a list of byte ranges (a start offset and a length) that the hash skips. In a normal JPEG the manifest sits in a run of APP11 segments near the top of the file, and the single exclusion covers exactly that run.

The whole-file exclusion

The hash format does not stop a signer from writing an exclusion that starts at byte 0 and runs to the end of the file. Buchanan did exactly that in How to Hack Time, With C2PA. With every byte excluded, the hash is computed over an empty input. SHA-256 of nothing is a fixed value that starts e3b0c442, the same for every file. The signature is real, and so is any trusted timestamp on it. Neither one says anything about the picture.

His point was about time. A timestamped credential that binds nothing can be attached to a photo taken later, which makes the photo look older than it is. That matters to anyone who uses Content Credentials to show when a photo existed, which is the question ChronoVerify was built to answer.

Someone has to sign such a credential first. A forger cannot turn an ordinary signed camera file into one, because changing the exclusion changes the signed data. But once a credential like this exists, it is portable. It can be copied onto any file of the same length, and padding can bring a smaller file up to that length.

What the C2PA specification says

The specification already covers this case. Section 15.12.1.1 of C2PA 2.2 tells a validator to check that the excluded range holding the manifest "consists only of the C2PA Manifest Store and appropriate padding", and to reject anything else with assertion.dataHash.mismatch. Other excluded ranges may hold the image's own metadata, such as EXIF or IPTC. Section 9.2.5 allows that and adds that excluded metadata is not attributed to the signer. The picture data is never on the list of things a signer may exclude.

The open-source library much of the C2PA ecosystem builds on did not enforce that rule until September. The c2pa-rs project merged a fix on September 16, 2026 that rejects data hash exclusions that do not match where the manifest sits, and released it in c2pa-rs 0.91.0 on September 21. In our own tests, the Python bindings built on 0.91 reject a whole-file exclusion.

What we found in ChronoVerify

ChronoVerify validates C2PA with c2pa-python 0.36, which runs c2pa-rs 0.89, a release from before that fix. After Buchanan's post we tested our own validator. We signed a JPEG with the official C2PA test certificate and wrote a single exclusion covering the whole file. Then we checked it against a trust list that includes the test certificate. It read Trusted.

Next we took that credential, put it on a completely different picture, padded the new file to the same length and checked it again. It read Trusted again, with ChronoVerify's top verdict, provenance_confirmed.

In production, an attacker would first need a signer whose certificate chains to the C2PA Trust List to make the original credential. We have seen no evidence that such a credential exists in the wild. The result was still wrong, so we fixed it.

The check we shipped

Since October 14, 2026, ChronoVerify reads the exclusion ranges out of every c2pa.hash.data binding and compares them with the file's real layout. An exclusion may cover the manifest and the file's metadata:

An exclusion that reaches into the picture data reads Invalid with assertion.dataHash.mismatch, the code the spec names for this case. AVIF and other ISO BMFF files bind with a different assertion that names boxes rather than byte ranges, so this particular trick does not apply to them.

Before shipping, we ran the check against 209 sample files under four trust lists, including the public C2PA and Microsoft interoperability samples and the conformance samples behind our C2PA listing. The two attack files were the only ones whose results changed.

How to check a file yourself

Run the image through the free verifier. A credential whose binding skips the picture comes back Invalid, with assertion.dataHash.mismatch in the status codes.

To read the exclusions directly, dump the full manifest with c2patool or a C2PA SDK (in c2pa-python, Reader.detailed_json()) and find the c2pa.hash.data assertion. In a healthy JPEG there is usually one exclusion. It starts where the APP11 segments start and runs their combined length. An exclusion that starts at 0 and ends at the file size binds nothing.

What a valid signature tells you

A Valid or Trusted signature shows that the manifest was signed by that key and has not changed since. Whether the manifest covers the picture is a separate question, and the validator has to answer it by checking the binding.

Check what a photo's Content Credentials actually cover. Free, no account.

Open the verifier

Common questions

What is an exclusion range in C2PA?

An exclusion range is a byte range, given as a start offset and a length, that a C2PA data hash skips. It exists because the manifest is stored inside the file it describes, so the hash cannot include the manifest itself. In a normal JPEG the only exclusion is the run of APP11 segments that holds the manifest. The C2PA 2.2 specification also allows asset metadata such as EXIF to be excluded, and says excluded metadata is not attributed to the signer.

Can a C2PA signature be valid but not cover the image?

Yes, if the validator does not check the exclusions. A signer can write an exclusion that covers the whole file, so the hash is taken over zero bytes and the signature covers nothing about the picture. David Buchanan described this on October 2, 2026. The C2PA 2.2 specification tells validators to reject it with assertion.dataHash.mismatch, and c2pa-rs rejects it starting with version 0.91.0, released September 21, 2026.

Does ChronoVerify catch whole-file exclusions?

Yes, since October 14, 2026. ChronoVerify compares every c2pa.hash.data exclusion with the file's real layout. Any exclusion that reaches into the picture data reads Invalid with assertion.dataHash.mismatch. Before that date, ChronoVerify ran c2pa-rs 0.89 and a whole-file exclusion read as Trusted when the signer was on the trust list.

Is this a flaw in the C2PA standard?

The rule is in the standard. Section 15.12.1.1 of C2PA 2.2 requires validators to make sure the excluded range holding the manifest contains only the manifest and padding, and to reject anything else. The gap was in implementations that did not enforce that check yet. A credential that binds nothing also still needs a signer willing to create it.

Sources: David Buchanan, How to Hack Time, With C2PA (October 2, 2026); C2PA Specification 2.2, sections 9.2.5, 15.12.1.1 and 18.5; c2pa-rs pull request 2653 (merged September 16, 2026); c2pa-rs 0.91.0 release; ChronoVerify's conformance record.