In this article
October 19, 2026
October 19, 2026

C2PA exclusion range flaw: Why signature verification must check coverage

C2PA’s exclusion range flaw let a signature verify while covering zero bytes. The same bug hits SAML and JWT. A checklist for verifying what a signature covers.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

Signature coverage is the set of bytes a signature actually protects. A signature can be cryptographically valid while covering little or nothing of the content an application trusts, and verifiers that only ask "does this signature validate?" will accept it. Checking coverage means confirming that the bytes you're about to rely on are the bytes that were signed.

On October 2, 2026, David Buchanan published a photograph of a lottery ticket whose C2PA metadata says it was created hours before the August 28, 2026 EuroMillions draw, showing that draw's winning numbers. The claim signature is valid. The timestamp from a Time Stamp Authority is valid. The signature covers zero bytes of the file.

Nothing in that chain is broken cryptography. The manifest excludes the entire file from the hash, so the thing being signed, and then timestamped, is the empty string. The C2PA verification tools Buchanan could find don't flag anything unusual, because they answer "does this signature validate?" rather than "what does this signature cover?" Most verification code never asks the second question either.

Diagram of the C2PA zero-coverage chain. The whole 3,995,383-byte image file is excluded from the hash, so the hash is the SHA-256 of an empty input. The claim signature over that hash is valid, and the RFC 3161 timestamp over the claim signature is valid. Every check passes while none of the image is signed.
The exclusion range spans the whole file, so the hash fed into the claim signature, and then into the timestamp, is computed over nothing.

What does the manifest actually say?

Dumping the proof-of-concept file with c2patool -d shows the c2pa.hash.data assertion that does the work:

  
"c2pa.hash.data": {
  "exclusions": [
    {
      "start": 0,
      "length": 3995383
    }
  ],
  "name": "jumbf manifest",
  "alg": "sha256",
  "hash": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
  "pad": []
},
  

C2PA allows exclusions: byte ranges inside the file that are left out of the hash. Here there's one, starting at offset 0 with a length of 3,995,383, which is the length of the entire file. The hash that remains is the SHA-256 of an empty input, which you can reproduce in one line:

  
$ openssl sha256 -binary /dev/null | base64
47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
  

In Buchanan's words:

"We can exclude the entire file, to produce an entirely valid signature over an empty string."

That lets the file be changed after the fact without invalidating the signature or the Time Stamp Authority's proof. The claim signature covers that hash, and the timestamp covers the claim signature. Both signatures are real, and neither touches a single pixel of the photo. Buchanan checked the file with the Content Authenticity Initiative's hosted verifier and its c2patool.

The photo proves nothing about timing, and Buchanan says so: he photographed a real ticket and edited the numbers in after they were announced. The demonstration is about the verifier, not the lottery.

It's also not a new finding. Neal Krawetz listed "large exclusion range" in his June 2025 list of C2PA flaws, noting that "the manifest typically excludes a very large byte range from the signatures." Buchanan's contribution is showing that a signer can choose a huge exclusion range on purpose. Sixteen months separate the warning from the working exploit, and as of this writing no spec change or vendor fix has been announced.

It isn't confined to a lab build, either. Buchanan's earlier work argued that the claim signature is "approximately worthless" in flagship implementations including the Google Pixel Camera app. For the Pixel 10, Google decided that "devices can in fact be trusted to tell their own time," which Buchanan hasn't yet evaluated. That changes who supplies the clock. It doesn't change the coverage question.

Is the RFC 3161 timestamp wrong?

No. RFC 3161 defines a service that "supports assertions of proof that a datum existed before a particular time." The Time Stamp Authority signs only a hash, and must not examine what it's timestamping beyond checking the hash's length. Buchanan didn't attack the authority; he assumed it behaves as advertised.

The RFC also puts the matching obligation on the other side: the requester "SHALL verify that what was time-stamped corresponds to what was requested to be time-stamped." A timestamp proves that some bytes existed at some time. Deciding that those bytes are the content in front of you is a separate step, and it belongs to the verifier.

Where have we seen this bug before?

The shape is familiar to anyone who has worked on federated auth.

SAML. Signature wrapping lets an attacker include a legitimately signed assertion alongside a second, unsigned one; the library verifies the signed assertion, the application reads the first assertion it finds, and the attacker signs in as someone else. Reference confusion is the same failure one level down: the signature points at an element ID, XML permits several elements with the same ID or none, and libraries pick the first match. SAML's signature problem: it's not you, it's XML walks through the variants.

JWT. The none algorithm did the same job. Tim McLean's 2015 survey of JWT libraries found some that treated tokens signed with none as valid tokens with verified signatures, so anyone could mint a token with any payload. The underlying mistake was reading alg from the attacker's own header, which also produced RSA-to-HMAC confusion, where a server expecting RS256 uses its public key as an HMAC secret. Our JWT best practices guide covers how to avoid both.

Us. In late September 2026, researcher Alex Tan reported four SAML authentication bypasses in WorkOS, where signature verification and identity extraction could read different content from the same XML document. A genuine signature passed verification while the application read attacker-controlled identity fields. The fixes were coverage fixes: verify against the original received bytes, compare the verified content with the content actually read, reject on any mismatch, and read identity only from the verifier's signed content. The full timeline is in Our response to four SAML vulnerabilities.

Different formats, different decades, one bug: verification answers a narrower question than the application asks, and nobody checks that the two questions match.

Case What the signature covered What the application trusted The fix
C2PA exclusion range (2026) An empty input; the whole file was excluded The image and its timestamp Check coverage against per-format exclusion rules
SAML signature wrapping One assertion A different, unsigned assertion Read claims only from the signed element
JWT alg: none and algorithm confusion Nothing, or an HMAC keyed with the public key The token's claims Pin the algorithm in configuration
WorkOS SAML bypasses (2026) The original XML, as one parser saw it Identity fields from a different parse Verify the original bytes and read only the verifier's signed content
Two bars comparing signature coverage with what the application reads. In the broken case, the signature covers one small segment while the application reads a different segment outside it, so the signature validates but protects nothing the application uses. In the correct case, the signed bytes and the bytes the application reads are the same.
Top: a signature that validates while what the application reads sits outside its coverage. Bottom: coverage and consumption bound to the same bytes, the only version worth anything.

What should a signature verification checklist include?

Run this against any verifier in your codebase: tokens, webhooks, documents, media manifests.

  • Pin the algorithm. Decide it from configuration, never from the token or message header.
  • Bind the key to an expected issuer. Require the issuer explicitly alongside the signature, so a valid signature from the wrong signer isn't good enough.
  • Compute coverage and check it. Reject exclusions or unsigned regions above a strict, format-specific limit.
  • Read claims only from the signed bytes. Never from a second parse, a re-serialization, or a query over the whole document.
  • Check the hash target isn't empty or trivially small. The digest of nothing is a constant, and so is the digest of a few bytes of padding.
  • Treat a timestamp as proof about the signed bytes, not about the content.
  • Add negative tests: empty coverage, a stripped signature, alg: none, a signature moved to a wrapper element, and content excluded by signature transforms.

The coverage check is the one most verifiers skip. Here's the shape it takes, with the allowed-range table left for you to fill in per format:

  
import hashlib

EMPTY_SHA256 = hashlib.sha256(b"").digest()

class CoverageError(Exception):
    pass

def merge_overlaps(exclusions):
    """Turn [(start, length), ...] into sorted, non-overlapping (start, end) ranges."""
    ranges = sorted((s, s + n) for s, n in exclusions)
    merged = []
    for start, end in ranges:
        if merged and start <= merged[-1][1]:
            merged[-1] = (merged[-1][0], max(merged[-1][1], end))
        else:
            merged.append((start, end))
    return merged

def check_coverage(file_len, exclusions, digest, allowed_ranges, min_ratio=0.99):
    if digest == EMPTY_SHA256:
        raise CoverageError("hash is the digest of an empty input")

    merged = merge_overlaps(exclusions)  # collapse first, or the arithmetic lies
    excluded = sum(end - start for start, end in merged)
    covered = file_len - excluded

    if covered < min_ratio * file_len:
        raise CoverageError(f"only {covered} of {file_len} bytes are signed")

    for start, end in merged:
        if not any(lo <= start and end <= hi for lo, hi in allowed_ranges):
            raise CoverageError(f"exclusion {start}:{end} is outside allowed ranges")

    return covered
  

allowed_ranges is the part that takes real work: for each format you accept, list the byte ranges that legitimately can't be signed, and refuse everything else. Every exclusion your verifier tolerates is a region an attacker can rewrite later.

Why can't C2PA just remove exclusions?

The obvious fix fails on contact with file formats. In PNG, each chunk has a CRC32 checksum that has to be corrected after the signature is embedded, which creates a circular dependency unless the CRC is excluded. Exclusions exist because some formats need them.

The harder problem is the small exclusion. Detecting that the whole file was excluded is trivial; judging whether a few excluded bytes are harmless or enough to change the image is not, and Buchanan points to MD5 collision proofs of concept for JPEG to show how far a few bytes can go. A verifier that waves through anything under some byte threshold has invented a threat model out of a round number. Buchanan's recommendation is to "carefully and explicitly specify which parts of a file are allowed to be excluded, for each supported file format," and have verifiers enforce it. Nobody has published that table yet.

A reasonable objection: C2PA never promised to defend against a malicious signer, and a signer who lies in the manifest is outside the threat model. That's a real difference from the auth cases. SAML and JWT defend against a third party tampering with a message, while provenance metadata is supposed to survive a source who is lying on purpose, a strictly harder job. It also makes the exclusion field strange, because it hands the signer a dial that silently narrows what their own signature means. And the objection collapses at the verification screen, where a green check reaches someone deciding whether a photo is real. If the honest answer is "this is genuine only if you already trust whoever signed it," the check is decorative, and today's tools show it without comment.

Where to start in your own code

Pick a verification path you own: the JWT middleware, the webhook handler, the SAML service provider. How to verify WorkOS access tokens in your own API covers the checks worth making explicit on the JWT side, and our SAML security best practices cover each assertion check. Then find the line where a boolean becomes a decision, and prove that the bytes behind that boolean are the same bytes the next function reads. The lottery ticket is a stunt; the gap it walks through is in most verification code ever written.

Read more