Hash Chain Verification
Trustless binding from signature to metadata via cryptographic hash chains.
The previous section described how Apertrue extracts the C2PA manifest from an image. But extraction alone isn't proof of anything — a malicious client could extract data from one image and claim it belongs to another. The hash chain is what makes this impossible.
A C2PA manifest contains a chain of cryptographic hashes that bind every piece of metadata to the original signature. If any link in this chain is broken — a single byte of GPS data changed, one edit action removed, the image pixels modified — the verification fails. This section explains how that chain works and how Apertrue verifies it inside a zero-knowledge circuit.
The chain
The hash chain has four layers, each binding to the one above it:
- COSE signature → message hash. The signature (ECDSA or RSA) covers the SHA-256 hash of the Sig_structure. This hash is what the ZK circuit verifies — if the signature is valid, the message hash is authentic.
- Message hash → claim bytes. The Sig_structure contains the raw claim bytes as its payload. The message hash commits to these exact bytes. Changing a single byte of the claim would change the message hash and break the signature.
- Claim → assertion hashes. The claim is a CBOR object that contains an array of assertion references. Each reference is a pair: a JUMBF path (identifying which assertion) and a SHA-256 hash (of that assertion's content). The claim doesn't contain the assertion data itself — just its hash.
- Assertion hashes → raw data + image pixels. Each assertion (EXIF metadata, edit history, AI declarations, content hash) is stored as a separate JUMBF box. Its SHA-256 hash must match the reference in the claim. The content binding assertion (
c2pa.hash.data) contains the SHA-256 of the stripped image bytes — tying the pixels themselves to the signature.
Claim structure
The claim is a CBOR-encoded object at the heart of the hash chain. Its key fields are:
Each entry in the assertions array is a commitment: the signer attests that at signing time, the assertion identified by that URL had exactly that SHA-256 hash. The ZK circuit verifies this by hashing the assertion bytes and comparing against the value embedded in the claim.
How offsets work
The circuit needs to find each assertion hash inside the raw claim bytes. Rather than parsing CBOR inside the circuit (which would cost thousands of additional constraints), the client-side extractor locates the byte offset where each hash appears in the claim and passes that offset as a circuit input. The circuit then reads 32 bytes from that position and compares them against the computed assertion hash.
This is safe because the circuit independently verifies that the claim bytes themselves hash to the value committed by the COSE signature. If the client provides a wrong offset, the extracted bytes won't match the assertion hash, and the circuit rejects the proof.
Content binding
The content binding assertion (c2pa.hash.data) is special. It ties the image pixels to the signature chain. Inside this assertion:
- Hash algorithm — always SHA-256 in practice.
- Content hash — the SHA-256 of the image bytes with exclusion ranges applied.
- Exclusion ranges — byte ranges to skip when computing the hash, typically the location where the C2PA manifest itself is stored (since the manifest can't hash itself).
In JPEG files, the manifest is appended in APP11 markers — those byte ranges are excluded. In PNG files, the manifest lives in ancillary chunks that are excluded. The remaining bytes — the actual pixel data and compression structures — are what gets hashed.
The ZK circuit takes this content hash as a public input. It extracts the content hash from the claim bytes (at a client-provided offset) and asserts it matches the public input. Anyone verifying the proof can see the content hash and independently compute it from the image — confirming the proof covers that specific image.
What the circuit verifies
The hash chain verification runs inside ProofA (the certificate and claim verification circuit). It performs five checks:
- Claim hash. Compute SHA-256 of the raw claim bytes and verify it matches the expected claim hash (which is bound to the COSE signature through the Sig_structure).
- EXIF assertion hash. Read 32 bytes from the claim at the provided offset. Compute SHA-256 of the EXIF assertion bytes. Assert they match. This proves the EXIF data (device info, timestamps, GPS) is exactly what the signer committed to.
- Actions assertion hash. Same process for the edit history assertion. This proves the declared edit actions (AI modifications, software edits) are authentic.
- Content hash binding. Extract the content hash from the claim bytes and verify it matches the public content hash input. This proves the image pixels are bound to the signature.
- Link commitment. Compute a Pedersen hash of the intermediate certificate leaf, the claim hash (as a field element), and a random blinding factor. This value must match across ProofA and ProofB, binding them to the same claim and certificate chain.
The link commitment
The hash chain culminates in a link commitment — a Pedersen hash binding the certificate chain, claim content, and a random blinding factor into a single value. This commitment is output as a public value by both ProofA and ProofB. The aggregation circuit checks that they match, preventing an attacker from mixing claims between the two split proofs. The full link commitment mechanism is detailed inSplit-Proof Architecture.
Client-side vs. in-circuit verification
Not everything is verified inside the ZK circuit. The design splits work between client-side extraction and in-circuit verification based on what needs to be trustless:
| Check | Where | Why |
|---|---|---|
| CBOR claim parsing | Client | Extracting offsets and data for circuit inputs — the circuit doesn't parse CBOR |
| Claim hash verification | Circuit | Core binding — must be trustless |
| Assertion hash verification | Circuit | Core binding — must be trustless |
| Content hash binding | Circuit | Core binding — must be trustless |
| Certificate signature | Circuit | Core binding — must be trustless |
| COSE signature verification | Circuit | Core binding — must be trustless |
| Trust list membership | Circuit | Core binding — must be trustless |
| GPS value extraction from EXIF | Client | Parsing CBOR rationals in-circuit costs ~1000 constraints; hash chain prevents tampering |
| Timestamp extraction | Client | Same — hash chain guarantees the assertion bytes are authentic |
| Time/location range proofs | Circuit | Privacy enforcement — must be in-circuit |
Why this matters
Without the hash chain, Apertrue would have to trust the client to provide honest extraction results. The client could claim an image was taken in London when it was taken in Moscow, or strip edit history to hide AI modifications. With the hash chain verified inside the circuit:
- The client cannot substitute assertion data — the hashes won't match the signed claim.
- The client cannot swap claims between proofs — the link commitment binds them.
- The client cannot modify image pixels — the content hash binding fails.
- The verifier doesn't need to trust anything except the math.
This is what "trustless" means in practice. The hash chain converts a C2PA manifest — which is designed to be verified by trusting the signer — into a structure that can be verified by anyone, about anything the signer committed to, without revealing the underlying data. The next section explains how different signers are classified into trust tiers.