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:
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:
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:
| Format | Embedding method |
|---|---|
| JPEG | APP11 markers — JUMBF split across multiple segments, reassembled by sequence number |
| PNG | caBX/caLi chunks — JUMBF bytes stored directly, no reassembly needed |
| MP4/MOV | uuid atom in the container — JUMBF bytes alongside video tracks |
| TIFF/DNG | XMP 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:
- 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). - Unprotected header — contains the X.509 certificate chain under label
33(x5chain). This is how the signer proves their identity. - 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).
- 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:
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.
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:
| Algorithm | Used by | Circuit support |
|---|---|---|
| ECDSA P-256 (ES256) | Google Pixel, Samsung Galaxy, ProofMode, Truepic | Full support — ProofA ECDSA circuit |
| RSA-2048 PSS (PS256) | Adobe products, many software signers | Full support — ProofA RSA circuit |
| RSA-4096 PSS | Some enterprise signers | Full support — ProofA RSA circuit (8-limb mode) |
| ECDSA P-384 (ES384) | Leica cameras | Full support — ProofA ECDSA P-384 circuit |
| ECDSA P-521 (ES512) | Rare | Not 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.
AI content detection
The extractor classifies every image into one of three states:
| State | Meaning | Detection method |
|---|---|---|
| NONE | Pure camera capture, no AI involvement | No AI claims, no generative actions, hardware origin device |
| GENERATED | Entirely AI-generated | c2pa.ai_generated claim, or known AI generator in signer chain (DALL-E, Midjourney, Firefly) |
| EDITED | Camera origin with AI modifications | Generative 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.