Extraction & Trust

C2PA Extraction

How Apertrue reads and parses C2PA manifests from images.

When you upload an image, the first thing Apertrue does is parse the C2PA manifest embedded in the file. This happens entirely in your browser. The raw bytes are read, the cryptographic structures are extracted, and the result is a set of values ready to feed into the zero-knowledge proof system. No server ever sees this data.

This section explains what a C2PA manifest actually contains at the binary level, how Apertrue parses it, and how the extracted data connects to the proof pipeline.

What lives inside the image file

A C2PA manifest is embedded in the image file itself using a container format called JUMBF (ISO 19566-5). JUMBF is a nested box structure — similar in concept to how MP4 files contain boxes for video, audio, and metadata. Each box has a type, a length, and either raw data or nested child boxes.

Inside a C2PA-signed image, the JUMBF structure looks roughly like this:

JUMBF manifest structure
Manifest Store (jumb)
├── Manifest (jumb) — identified by a C2PA UUID
│   ├── Claim (cbor) — the signed assertion set
│   │   ├── Content hash binding
│   │   ├── List of assertion references (hash + URI)
│   │   └── Signature reference
│   ├── Assertions (jumb)
│   │   ├── stds.exif — EXIF metadata (device, GPS, timestamp)
│   │   ├── c2pa.actions.v2 — edit history
│   │   ├── c2pa.ingredient — source content references
│   │   └── c2pa.ai_generated — AI declaration (if applicable)
│   └── Signature (uuid) — COSE_Sign1 envelope
│       ├── Protected header (algorithm ID)
│       ├── Unprotected header (X.509 certificate chain)
│       ├── Payload (claim digest — detached)
│       └── Signature bytes (ECDSA or RSA)
└── (optional) Additional manifests for ingredient images

The critical path for verification is: Signature → Claim → Content Hash. The signature covers the claim. The claim contains a hash of the image content. If either link is broken — if the claim has been modified, or the image pixels don't match the committed hash — verification fails.

How Apertrue parses this

Extraction runs as a multi-stage pipeline. Each stage takes the output of the previous stage and produces a more structured representation:

Extraction pipeline: Image bytes flow through format detection (JPEG/PNG/MP4), JUMBF parser (find manifest store), COSE parser (extract signature envelope), X.509 parser (extract certificate chain), and raw data builder (assemble circuit inputs). Each stage produces a specific data type.

Stage 1: Format detection and JUMBF extraction

The parser first identifies the image format by reading magic bytes: FFD8 for JPEG, 89504E47 for PNG, ftyp for MP4/MOV. Each format embeds JUMBF differently:

FormatEmbedding method
JPEGAPP11 markers — JUMBF split across multiple segments, reassembled by sequence number
PNGcaBX/caLi chunks — JUMBF bytes stored directly, no reassembly needed
MP4/MOVuuid atom in the container — JUMBF bytes alongside video tracks
TIFF/DNGXMP or proprietary markers — used by camera RAW formats

The parser walks the JUMBF box tree, locating the manifest store by its C2PA UUID. Inside, it finds the claim box (CBOR-encoded), the assertion boxes, and the signature box.

Stage 2: COSE signature parsing

The signature box contains a COSE_Sign1 structure (RFC 8152). This is a CBOR-encoded envelope that holds four things:

  1. Protected header — CBOR map containing the algorithm identifier. For ECDSA P-256 this is algorithm -7 (ES256). For RSA-PSS with SHA-256 it's -37 (PS256).
  2. Unprotected header — contains the X.509 certificate chain under label 33 (x5chain). This is how the signer proves their identity.
  3. Payload — the claim bytes that were signed. In C2PA this is typically detached (the payload is the claim box stored separately in the JUMBF tree).
  4. Signature — the actual cryptographic signature bytes. For ECDSA this is 64 bytes (R || S). For RSA-2048 this is 256 bytes.

The parser also reconstructs the Sig_structure — the exact byte sequence that was signed. This is critical because the ZK circuit needs to verify the signature against the same message the signer produced:

Sig_structure (RFC 8152 §4.4)
Sig_structure = CBOR_encode([
  "Signature1",        // context string
  protected_header,    // raw bytes from COSE
  external_aad,        // empty (no external data)
  claim_bytes          // the CBOR claim box
])

message_hash = SHA-256(Sig_structure)

This message_hash is what the ZK circuit actually verifies — it checks that the signature is valid over this hash using the signer's public key. The circuit never sees the full Sig_structure, only the 32-byte hash.

Stage 3: Certificate chain extraction

The x5chain in the COSE header contains one or more DER-encoded X.509 certificates. Apertrue parses each certificate to extract:

  • Public key — the signer's key from the leaf certificate. For P-256 this is two 32-byte coordinates (X, Y). For RSA-2048 this is a 256-byte modulus and exponent (typically 65537).
  • Certificate signature — the intermediate CA's signature over the leaf certificate's TBS (to-be-signed) region. This proves the leaf was issued by a trusted authority.
  • Issuer and subject names — used to identify the signer (e.g., "Leica Camera AG") and match against the trust list.
  • Validity period — notBefore and notAfter timestamps. The circuit can optionally verify the certificate was valid at signing time.
  • SPKI offsets — byte positions within the TBS that locate the Subject Public Key Info. The circuit uses these to extract the key from the raw certificate bytes without parsing ASN.1.
Note
The X.509 parser handles the full ASN.1/DER encoding — tag-length-value structures, OID decoding, BIT STRING unwrapping, and distinguished name parsing. This is one of the most complex parts of the extraction pipeline at around 1,500 lines of code.

Supported signature algorithms

Different cameras and software use different cryptographic algorithms. Apertrue's ZK circuits support the algorithms that cover the vast majority of C2PA-signed content in the wild:

AlgorithmUsed byCircuit support
ECDSA P-256 (ES256)Google Pixel, Samsung Galaxy, ProofMode, TruepicFull support — ProofA ECDSA circuit
RSA-2048 PSS (PS256)Adobe products, many software signersFull support — ProofA RSA circuit
RSA-4096 PSSSome enterprise signersFull support — ProofA RSA circuit (8-limb mode)
ECDSA P-384 (ES384)Leica camerasFull support — ProofA ECDSA P-384 circuit
ECDSA P-521 (ES512)RareNot supported

Each algorithm requires a different Noir circuit because the cryptographic operations (field arithmetic, hash functions, signature verification equations) differ. The extraction pipeline detects the algorithm from the COSE header and routes to the appropriate circuit.

ECDSA signature normalisation

For ECDSA signatures, the parser enforces low-S normalisation. An ECDSA signature (R, S) has an equivalent signature (R, N - S) where N is the curve order. Noir's standard library requires S to be in the lower half of the curve order. If the extracted S is greater than N/2, the parser replaces it with N - S before passing it to the circuit. This prevents signature malleability — the same signed message always produces the same normalised signature.

RSA limb conversion

RSA-2048 values are too large for a single field element (BN254 is ~254 bits, RSA-2048 is 2048 bits). The extractor converts each RSA value — modulus, signature, and Barrett reduction parameter — into 18 limbs of 120 bits each. This matches the format expected by the noir_rsa library (by ZKPassport) used in the circuit.

Content hash binding

The extraction pipeline computes two independent hashes that together bind the image content to the cryptographic signature:

Image content hash

The image bytes are stripped of all metadata — EXIF, C2PA manifest, XMP, APP markers — leaving only the raw pixel data. The SHA-256 hash of these stripped bytes is the content hash. This is what the claim commits to. If anyone modifies a single pixel after signing, this hash changes and the claim commitment breaks.

Claim hash

The raw CBOR claim box is hashed separately. This claim contains references to each assertion (by hash), the content hash binding, and a pointer to the signature. The claim hash becomes the payload in the Sig_structure — it's what the signer actually signed. The chain is: Signature → Claim hash → Content hash → Image pixels.

Hash chain binding: bottom layer shows image pixels producing SHA-256 content hash, middle layer shows claim containing content hash reference and assertion hash references, top layer shows COSE signature covering the claim hash. Signature verifies claim hash, claim hash commits to content hash, content hash binds to pixels.
Key Insight
This hash chain is what makes C2PA tamper-evident. The ZK circuit verifies the signature over the claim hash, and the claim hash commits to the content hash. If you modify the image after signing, the content hash changes, the claim no longer matches, and the signature verification fails inside the circuit. No one can forge this chain without the signer's private key.

AI content detection

The extractor classifies every image into one of three states:

StateMeaningDetection method
NONEPure camera capture, no AI involvementNo AI claims, no generative actions, hardware origin device
GENERATEDEntirely AI-generatedc2pa.ai_generated claim, or known AI generator in signer chain (DALL-E, Midjourney, Firefly)
EDITEDCamera origin with AI modificationsGenerative actions found (generative_fill, etc.) but ingredient chain traces to real camera

Detection works by searching recursively through ingredient manifests. If an image was opened in Photoshop and had AI fill applied, the extractor traces the ingredient chain back to find the original camera capture, classifying the result as EDITED. The provenance chain records both the origin device and the AI actions applied.

What the extraction produces

After all stages complete, the extraction pipeline produces a RawC2PAData object that contains everything the ZK proof system needs:

  • Signature data — ECDSA (R, S) or RSA (18 limbs each for signature, modulus, and Barrett parameter)
  • Public key — the signer's key from the leaf certificate
  • Message hash — SHA-256 of the reconstructed Sig_structure
  • Certificate chain data — intermediate CA's signature over the leaf, TBS bytes with SPKI offsets, issuer/subject names
  • Content hash — SHA-256 of the stripped image bytes
  • Metadata — signing time, GPS coordinates, device info, AI state

This object is then passed to the circuit input builder, which transforms it into the exact field format the Noir circuits expect — computing Merkle proofs against the trust list, preparing nullifiers, and structuring the data for the split-proof architecture described in the next sections.

Extraction is the trust boundary
Everything after extraction — the ZK proofs, the aggregation, the blockchain verification — operates on the data the extractor produces. If extraction is wrong, everything downstream is wrong. This is why the extractor has its own test suite covering every supported algorithm, every file format, edge cases like multi-segment JPEG manifests, remote manifests, and malformed certificates. It's also why extraction runs entirely client-side: the user can verify (or audit) exactly what data enters the proof system.