The Hash of an Empty String

On October 2, David Buchanan published How to Hack Time, With C2PA. He took a photo from a phone that signs its pictures with C2PA content credentials, edited it to show a winning lottery ticket, and ended up with a file that a hosted verifier accepted as carrying a real timestamp from Google’s time-stamping authority. That timestamp was dated before the draw. The trick uses the data-hash exclusion range. This is the byte span a signer marks as “not covered by the hash,” because that span holds the manifest itself. In his proof of concept the exclusion begins at byte 0 and runs for 3,995,383 bytes, which is the entire file. The hash recorded in the manifest is 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=. That is the SHA-256 of the empty string. The signature is genuine and the timestamp is genuine, and they cover nothing. Buchanan’s framing was that no tool flags this. I wanted to check that claim myself, not just repeat it.

The spec was the first place to look. C2PA 2.4, §15.12 already says that a validator shall ensure the exclusion range containing the manifest store “consists of only the C2PA Manifest Store and any appropriate padding,” and that it shall otherwise reject with assertion.dataHash.mismatch. The same text appears in 2.3, and 2.2 has a close relative of it. So the standard was never silent on this. Next I looked at the reference implementation. A walk back through c2pa-rs turned up PR #2653, “Reject data hash exclusions that don’t match the manifest location.” It was merged on September 16 and released in v0.91.0 on September 21, about 11 days before the blog post. The patch is roughly nine lines. When a manifest is embedded, the validator re-parses the asset to find where the manifest actually sits and requires that the declared exclusion be exactly that span. A whole-file exclusion can’t satisfy that check. The PR was motivated by a different report, a detached-manifest attack in #2643, and it closes the whole-file case as a side effect. It belongs to a year-long run of exclusion hardening in the same repo: integer-overflow fixes in January and July (#1715, #2263, #2359) and a non-contiguous-segment fix in September (#2610).

Then I ran his actual file. I downloaded the proof-of-concept JPEG and passed it through c2pa-python 0.38.0, which wraps c2pa-rs 0.91.0 and targets spec 2.4.0. The result is Invalid. The successes list still shows timeStamp.validated against the Google authority and claimSignature.validated, so the cryptography is fine. The failures list also contains assertion.dataHash.mismatch: data hash exclusion does not match the manifest location in the asset. The current reference engine catches the exploit, and it catches it for the reason the spec gives.

So the trick works only against verifiers that haven’t upgraded yet. What the post really measured is deployment lag: the hosted verifier that accepted the file was running an engine older than the fix. The weakness had been known for more than a year (Buchanan credits an earlier critique from June 2025 that listed “large exclusion range” as a flaw). The spec required rejection, and the fix was already published but had not yet reached every deployed verifier. That changes what a provenance check tells you. “This file validates” is incomplete until you also say which validator version checked it. As with TLS, the chain is only as strong as the oldest unpatched verifier in it. The useful lesson isn’t that C2PA is broken. A credential’s validity also depends on how up to date the software checking it is, and that part of the system has no signature on it.