Circuit Reference
Technical specification for all 19 Noir circuits.
Apertrue uses 19 Noir circuits. This section is a reference catalogue — each circuit's purpose, public inputs, outputs, and approximate constraint count. All circuits compile to BN254 and produce UltraHonk proofs (500 fields) unless otherwise noted.
Circuit map
| Circuit | Category | Approx. constraints | Purpose |
|---|---|---|---|
| ProofA (generic) | Split proof | 80k–220k | Certificate chain verification + graduated privacy disclosure |
| ProofA ECDSA P-256 | Split proof | ~120k | P-256 only (ChatGPT, DALL-E, ProofMode) |
| ProofA ECDSA P-384 | Split proof | ~150k | P-384 only (Google Pixel, Truepic) |
| ProofA RSA-2048 | Split proof | ~180k | RSA-2048 only (standard RSA C2PA) |
| ProofA RSA-4096 | Split proof | ~220k | RSA-4096 only (Adobe Photoshop, Lightroom) |
| ProofA Skip | Split proof | ~80k | 2-cert chains (leaf signed by root, no intermediate) |
| ProofB (generic) | Split proof | 100k–160k | COSE signature verification + claim binding |
| ProofB ES256 | Split proof | ~100k | ECDSA COSE signatures only |
| ProofB PS256 | Split proof | ~160k | RSA-PSS COSE signatures only |
| ImageAggregator | Aggregation | ~100k | Combines ProofA + ProofB into single image proof |
| TreeAggregator | Aggregation | ~50k | Binary tree — combines 2 proofs into 1 (recursive) |
| Selective Disclosure | Privacy | ~50k | Prove claims about verified image without revealing it |
| JWT Identity | Identity | ~80k | Verify org membership from JWT without revealing email |
| Anonymous Credential | Identity | ~40k | Semaphore-style Merkle membership with nullifier |
| Credential Registration | Identity | ~10k | Prove knowledge of secret behind commitment |
| WebAuthn Account | Aztec contract | N/A | Passkey + session key dual-path authorisation |
| ApertrueVerifier | Aztec contract | N/A | On-chain proof verification and note storage |
Split proofs
Every C2PA image generates two proofs that are later combined. ProofA handles certificate chain verification and metadata extraction. ProofB handles COSE signature verification and claim binding. Splitting the workload across two circuits keeps each circuit's constraint count manageable for browser proving.
ProofA — certificate chain + privacy disclosure
ProofA verifies that a C2PA manifest was signed by a certificate chaining to a trusted intermediate CA, extracts metadata (location, time, device, actions), and applies graduated privacy disclosure. The generic circuit handles all five algorithm variants via a selector. The specialised variants (P-256, P-384, RSA-2048, RSA-4096, Skip) are faster because they omit unused code paths.
| Public input | Type | Purpose |
|---|---|---|
| trust_list_root | Field | Oracle Merkle root of trusted intermediate CAs |
| content_hash | Field | SHA-256 of stripped image (metadata removed) |
| nullifier | Field | Replay prevention — hash of content + leaf key |
| proof_timestamp | Field | Unix seconds when proof was generated |
| cert_not_before | Field | Certificate validity start |
| cert_not_after | Field | Certificate validity end |
| time_min | Field | Graduated time disclosure — lower bound (Unix seconds) |
| time_max | Field | Graduated time disclosure — upper bound (Unix seconds) |
| center_lat_scaled | Field | Privacy circle centre latitude (scaled 10^7) |
| center_lon_scaled | Field | Privacy circle centre longitude (scaled 10^7) |
| location_flags | u8 | Bit 0: lat negative (South), Bit 1: lon negative (West) |
| radius_squared_scaled | Field | Privacy radius squared (scaled units) |
| tbs_hash_commitment | Field | Poseidon2 commitment of TBS certificate hash |
| claim_hash_commitment | Field | Poseidon2 commitment of claim hash |
| content_hash_offset | u32 | Byte offset of content hash in claim |
| Public output | Type | Purpose |
|---|---|---|
| link_commit | Field | Device identity binding — links ProofA to ProofB |
| location_commitment | Field | Poseidon2([lat, lon, lat_sign, lon_sign, salt]) |
| time_commitment | Field | Poseidon2([timestamp, salt]) |
| actions_assertion_hash | [u8; 32] | SHA-256 of C2PA action history |
The circuit proves that the exact coordinates fall within the disclosed privacy circle (centre + radius), the exact timestamp falls within the disclosed time range (min/max), and the certificate chains to a trusted intermediate in the Oracle's Merkle tree.
ProofB — COSE signature + claim binding
ProofB verifies the COSE signature over the C2PA Sig_structure and confirms that the claim bytes are embedded within it. The generic circuit handles both ES256 (ECDSA) and PS256 (RSA-PSS) via a selector. ProofB ES256 and ProofB PS256 are specialised variants.
| Public input | Type | Purpose |
|---|---|---|
| trust_list_root | Field | Oracle Merkle root (must match ProofA) |
| content_hash | Field | SHA-256 of stripped image (must match ProofA) |
| nullifier | Field | Replay prevention (must match ProofA) |
| proof_timestamp | Field | Unix seconds (must match ProofA) |
| cert_not_before | Field | Certificate validity start (from ProofA) |
| cert_not_after | Field | Certificate validity end (from ProofA) |
| claim_hash_commitment | Field | Poseidon2 commitment of claim hash (must match ProofA) |
| content_hash_offset | u32 | Byte offset of content hash in claim |
| Public output | Type | Purpose |
|---|---|---|
| link_commit | Field | Device identity binding (must match ProofA) |
Six public inputs are shared between ProofA and ProofB and must match exactly: trust_list_root, content_hash, nullifier, proof_timestamp, claim_hash_commitment, and link_commit. The ImageAggregator enforces this — if any value differs, aggregation fails.
Algorithm variants
The browser selects the circuit variant based on the C2PA manifest's signature algorithm. Specialised circuits are 30–60% faster than the generic circuit because they compile only the code paths for one algorithm:
| Variant | Algorithm | Common devices/tools |
|---|---|---|
| ECDSA P-256 | SHA-256 + secp256r1 | ChatGPT/DALL-E, ProofMode, Truepic (older) |
| ECDSA P-384 | SHA-384 + secp384r1 | Google Pixel 8/9, Truepic (newer) |
| RSA-2048 | SHA-256 + PKCS#1 v1.5 | Standard RSA C2PA implementations |
| RSA-4096 | SHA-256 + PKCS#1 v1.5 | Adobe Photoshop, Lightroom, Firefly |
| Skip (2-cert) | None (trust via Merkle) | 2-certificate chains where leaf is signed directly by root |
Aggregation circuits
ImageAggregator — single image
The ImageAggregator recursively verifies a ProofA and ProofB inside a new circuit, producing a single proof that represents a fully verified image. It checks that all shared public inputs match between the two split proofs and computes an image commitment from the verified data.
| Public input | Type | Purpose |
|---|---|---|
| trust_list_root | Field | Oracle Merkle root |
| content_hash | Field | SHA-256 of stripped image |
| nullifier | Field | Replay prevention |
| proof_timestamp | Field | Unix seconds |
| Public output | Type | Purpose |
|---|---|---|
| location_commitment | Field | From ProofA (passed through) |
| time_commitment | Field | From ProofA (passed through) |
| edit_time_commitment | Field | From ProofA (passed through, 0 if no edit) |
| link_commit | Field | Device identity binding (verified matching) |
| actions_hash_field | Field | Poseidon2 reduction of 32-byte actions hash |
| image_commitment | Field | Poseidon2 of all 9 verified values |
The image commitment is a single field element that uniquely identifies a verified image with all its metadata. This commitment is the input to the tree aggregation layer.
TreeAggregator — batch combination
The TreeAggregator combines two proofs into one. It is reusable at every level of the binary tree — it can combine two ImageAggregator proofs, two TreeAggregator proofs, or one of each. The final TreeAggregator proof represents the entire batch.
| Public input | Type | Purpose |
|---|---|---|
| pad_0 through pad_8 | Field (all zero) | Padding to match ImageAggregator public input layout |
| Public output | Type | Purpose |
|---|---|---|
| combined_commitment | Field | Poseidon2([left_commitment, right_commitment]) |
The padding fields exist so that the TreeAggregator's public input layout matches the ImageAggregator's — both circuits have the same number of public inputs. This simplifies recursive verification because the verifier doesn't need to distinguish between circuit types.
Privacy circuits
Selective Disclosure
The Selective Disclosure circuit lets a user prove specific claims about a verified image (location range, time range, device, actions) without revealing the image itself. The prover chooses which fields to disclose — hidden fields are set to zero.
| Public input | Type | Purpose |
|---|---|---|
| root_commitment | Field | Batch Merkle root (verified on-chain) |
| nullifier | Field | Batch nullifier |
| contract_address | Field | Aztec contract address |
| disclosed_content_hash | Field | Image identity (0 if hidden) |
| disclosed_trust_list_root | Field | Oracle root at proof time (0 if hidden) |
| disclosed_proof_timestamp | Field | When proof was generated (0 if hidden) |
| disclosed_lat_min / lat_max | Field | Latitude bounding box (0 if hidden) |
| disclosed_lon_min / lon_max | Field | Longitude bounding box (0 if hidden) |
| disclosed_location_flags | Field | Hemisphere indicators (0 if hidden) |
| disclosed_time_min / time_max | Field | Time range (0 if hidden) |
| disclosed_link_commit | Field | Device identity (0 if hidden) |
| disclosed_actions_hash | Field | C2PA action history (0 if hidden) |
The circuit verifies a Merkle path from the image commitment to the batch root commitment (8 levels, supporting up to 256 images per batch). For each disclosed field, it proves that the disclosed value is consistent with the original committed data — the prover cannot fabricate disclosed values.
Identity circuits
JWT Identity
The JWT Identity circuit verifies an OIDC JWT from any provider (Google, Microsoft, Okta) and proves the user's email domain without revealing the full email address. Built on ZKEmail's noir-jwt.
| Public input | Type | Purpose |
|---|---|---|
| domain | BoundedVec<u8, 64> | Organisation domain to verify (e.g., "reuters.com") |
Private inputs include the JWT payload (partial SHA mode for efficiency), the RSA-2048 public key, and the signature. The circuit verifies the RSA signature, parses the JWT to find the email claim, confirms email_verified == true, and checks that the domain after the @ matches the public input.
Anonymous Credential
A Semaphore-style credential circuit. Proves membership in a Merkle tree of identity commitments with a scoped nullifier for rate limiting. Uses Baby Jubjub elliptic curve arithmetic from noir-edwards and Poseidon hashing from zk-kit.noir.
| Public input | Type | Purpose |
|---|---|---|
| hashed_scope | Field | Scoped nullifier domain (e.g., app identifier) |
| hashed_message | Field | Message to sign (prevents proof malleability) |
| Public output | Type | Purpose |
|---|---|---|
| root | Field | Merkle root of identity commitment tree |
| nullifier | Field | Poseidon([scope, secret]) — deterministic per scope |
The Merkle tree supports up to 20 levels (approximately 1 million identities). The nullifier is deterministic per secret per scope — using the same credential twice in the same scope produces the same nullifier, enabling rate limiting without revealing identity.
Credential Registration
A lightweight circuit that proves knowledge of the Baby Jubjub secret key behind a Poseidon commitment. Used during credential registration to verify the user controls the secret before adding the commitment to the tree.
| Public input | Type | Purpose |
|---|---|---|
| commitment | Field | Poseidon2([pubkey.x, pubkey.y]) |
| Public output | Type | Purpose |
|---|---|---|
| computed | Field | Recomputed commitment (must match input) |
Aztec contracts
Two Aztec contracts handle on-chain operations. These are Noir programs compiled for the Aztec Virtual Machine rather than standalone ZK circuits.
WebAuthn Account
The user's Aztec account contract. Supports dual-path authorisation via a discriminator at witness[0]:
| Path | Discriminator | Auth method | Use case |
|---|---|---|---|
| WebAuthn | 0 | ECDSA P-256 (passkey) | Initial setup, high-value operations |
| Session key | 1 | Schnorr (Grumpkin) | Proof submissions during active session |
The WebAuthn path verifies a P-256 ECDSA signature over the transaction hash, with the authenticator data and client data JSON as witness inputs. The session key path verifies a Schnorr signature from an ephemeral Grumpkin key pair authorised by the user's passkey. Session keys have a configurable expiry and scope, stored as encrypted notes in the contract's private state.
ApertrueVerifier
The verification contract. Receives the final TreeAggregator proof and creates private notes representing verified media. Also stores identity proofs (JWT and passport).
| Function | Input | Creates |
|---|---|---|
| verify_and_store | TreeAggregator proof + VK | VerifiedMediaNote (root_commitment, image_count, trust_list_root, epoch_week) |
| store_identity | JWT identity proof hash + domain hash | IdentityNote |
| store_passport_identity | Passport proof hash + nullifier hash | PassportIdentityNote |
Shared cryptographic primitives
Across all circuits, the following primitives are used:
| Primitive | Used in | Purpose |
|---|---|---|
| Poseidon2 | All circuits | Field-native hashing — commitments, Merkle trees, nullifiers |
| SHA-256 | ProofA, ProofB, ImageAgg | Content hashing, certificate TBS hashing, claim hashing |
| SHA-384 | ProofA P-384 | TBS certificate hashing for P-384 certificates |
| Pedersen | ProofA, ProofB | Trust list intermediate leaf hashing (Oracle compatibility) |
| ECDSA P-256 | ProofA, ProofB, WebAuthn Account | C2PA certificate verification, passkey authorisation |
| ECDSA P-384 | ProofA P-384 | Google Pixel / Truepic certificate verification |
| RSA-2048 | ProofA RSA, ProofB PS256, JWT Identity | C2PA certificate verification, JWT signature verification |
| RSA-4096 | ProofA RSA-4096 | Adobe certificate verification |
| Schnorr (Grumpkin) | WebAuthn Account | Session key authorisation |
| Baby Jubjub | Anon Credential, Registration | Credential key derivation |
Constraint budget
Constraint count determines proving time. In the browser (Barretenberg WASM), rough benchmarks on mid-range hardware:
| Constraints | Browser proving time | Memory |
|---|---|---|
| ~50k | 3–5 seconds | ~300 MB |
| ~100k | 6–10 seconds | ~400 MB |
| ~150k | 10–15 seconds | ~400 MB |
| ~220k | 15–25 seconds | ~500 MB |
The split proof architecture exists specifically because of these constraints. A single monolithic circuit covering all of ProofA + ProofB would exceed 300k constraints and require 30+ seconds of proving time with ~600 MB of memory — impractical for browser use. Splitting into two circuits and proving them in parallel on separate Web Workers halves the wall-clock time.
Sensor line circuits
The sensor fingerprint line has its own circuit family, built and proven on the phone rather than in the browser. Measured on an iPhone 13 Pro Max:
| Circuit | Size | On-device | What it proves |
|---|---|---|---|
| Threshold membership | ~170k gates | 4.4 s prove | A fresh capture matches the enrolled keyed template within the accept threshold, in zero knowledge |
| zk_attest | ~322k gates | 8.5 s prove · 2.0 s verify | The device's Apple-issued App Attest credential is genuine, without contacting Apple or revealing it |
| Delegation | ~415k gates | once, at enrolment | Locally minted per-capture assertions chain back to that single attestation, keeping captures unlinkable |
The circuit family is public at Apertrue/sensor-circuits, and the protocol behind it is specified in Prove the Camera, Not the Cloud.
The final section covers the security model — threat analysis, trust assumptions, and the guarantees the system provides.