C2PA manifest extraction with raw crypto-witness output — built for ZK proof pipelines.
Extract C2PA (Content Credentials) manifests from images and video — including the raw cryptographic material most C2PA tooling hides: COSE signature bytes, X.509 chains with TBS/SPKI byte offsets, claim/assertion hash chains, and RSA limb encodings ready for ZK circuits.
The official C2PA SDKs answer "is this manifest valid?" This library additionally answers "give me the exact bytes": the Sig_structure that was signed, the signature r‖s, the signer's public key and where it sits inside the DER, the claim bytes, and the offsets binding content hash → claim → assertions. That is what you need to re-verify a C2PA manifest somewhere the SDK can't follow — a ZK circuit, an enclave, a smart contract.
Built at Apertrue to feed the Noir circuits at Apertrue/circuits, which verify C2PA signatures inside zero-knowledge proofs. The two repos together are a complete, open pipeline for proving statements about signed media without revealing it.
flowchart LR
A[media bytes] --> B[containers/<br/>JPEG · PNG · TIFF · MP4<br/>JUMBF box tree]
B --> C[cose/<br/>Sig_structure · alg detect]
B --> D[claims/<br/>claim & assertion bytes<br/>hash-chain offsets]
C --> E[x509/<br/>chain · TBS/SPKI offsets<br/>PSS validation]
E --> F[zk/<br/>noir-bignum limbs<br/>Barrett params · toZKInput]
D --> F
F --> G[ZK circuit witness]
B --> H[metadata/<br/>GPS · time · device]
Everything left of the arrowhead is untrusted parsing; the trust decision belongs to whatever verifies the extracted material (in our case, the circuit — it re-checks the signature and every hash binding, so a malicious parser can't forge anything).
npm install @apertrue/c2pa-extractorBrowser (WASM-backed SDK view + raw extraction):
import { configureC2pa, extractC2PA, toZKInput } from '@apertrue/c2pa-extractor/browser';
// Serve @contentauth/c2pa-web's WASM yourself (same-origin avoids CDN SRI issues)
configureC2pa({ wasmSrc: '/wasm/c2pa_bg.wasm' });
const result = await extractC2PA(imageBytes);
if (result.success) {
console.log(result.signer, result.claims);
const witness = toZKInput(result); // circuit-ready input, or null
}Node:
import { extractC2PA } from '@apertrue/c2pa-extractor/node';
const result = await extractC2PA(await fs.readFile('photo.jpg'));ZK layer only (no SDK dependency — pure byte work):
import { toNoirLimbs, computeRedcParam } from '@apertrue/c2pa-extractor/zk';| Function | Entry | What it does |
|---|---|---|
extractC2PA(bytes, opts?) |
browser, node | Full extraction: manifest view + RawC2PAData (signature, pubkey, cert chain, hash-chain offsets, signing time) |
toZKInput(result) |
browser, node, zk | Convert an extraction result into circuit witness data |
configureC2pa({ wasmSrc }) |
browser | Point the WASM loader at your served binary (call before first extraction) |
hasC2PAManifest(bytes) |
browser, node | Cheap presence check |
quickExtractAlgorithm(bytes) |
cose | <100ms COSE algorithm sniff — pick your circuit variant before committing to a full parse |
analyzeProvenanceFromImage(bytes) |
browser | Walk the ingredient/edit chain (depth-capped) |
computeContentHash(bytes) |
browser, node | SHA-256 of the asset |
RawC2PAData in one glance:
{
signatureType: 'ecdsa' | 'rsa',
signatureR?, signatureS?, publicKeyX?, publicKeyY?, curve?, // ECDSA (low-S normalized)
rsaData?: RSACircuitData, // modulus + signature as 18×120-bit noir-bignum limbs,
// Barrett reduction params, modulus hash
signingTime?: number,
// ...plus claim bytes, claimHash, assertion hash-chain data and byte offsets
}| Containers | JPEG (APP11 multi-segment reassembly, incl. recovery from known encoder offset bugs), PNG (caBX), MP4/MOV (uuid box), TIFF/DNG/NEF/ARW (tag 0xCD41), remote manifests (dcterms:provenance, allowlisted HTTPS hosts only) |
| Signatures | COSE_Sign1 with ECDSA P-256/P-384 (DER→raw, low-S normalized) and RSA-2048/4096 PKCS#1 v1.5 + PSS (with PSS parameter validation) |
| Signers exercised in production | Leica, Sony, Pixel, Samsung, Adobe (incl. Lightroom remote manifests), ProofMode, ChatGPT/DALL-E outputs |
| Semantics | AI-content state, capture-device classification, trust tiering, edit/ingredient history, normalized GPS/time/device metadata |
containers/ JUMBF box walker + per-format extraction (jpeg, png, tiff, manifest store)
cose/ COSE_Sign1 parsing, Sig_structure reconstruction, fast algorithm sniffing
x509/ DER walking, chain building, TBS/SPKI offsets, RSA-PSS validation, validity checks
claims/ claim/assertion parsing, hash-chain binding, vendor signer patterns
metadata/ GPS, timestamps, device info — normalized across signer formats
zk/ noir-bignum limb encoding, Barrett reduction params, witness builder → ./zk
extract/ orchestrator producing RawC2PAData
Subpath exports: . (browser barrel), ./browser, ./node, ./zk.
This library parses attacker-controlled bytes; treat its output as untrusted input to your verifier — that's the design.
- The X.509 path ships with a 75-case malformed-DER fuzz corpus (run in CI-able
npm test). - Box walkers bound their recovery scans, cap recursion depth, and reject >32-bit box lengths.
- Remote-manifest fetching is HTTPS-only against an allowlist, with timeouts (SSRF-resistant).
x509/validitychecks certificate validity windows only — it is deliberately not chain-signature verification. Signature and binding verification belong to the consumer (e.g. the circuits, which re-verify everything in-proof).
npm install
npm test # vitest: unit + DER fuzz corpus (90 tests)
npm run test:real-images # downloads CAI public example assets, runs end-to-end
npm run build # tsup: ESM + d.ts — index, browser, node, zk entries
npm run typecheckApache-2.0. Contributions welcome — especially additional container formats, signer patterns, and fuzz corpora.
