The Oracle & Trust List
Intermediate CA promotion, Merkle trees, and multi-sig oracle bundles.
The previous section explained how Apertrue classifies signers into trust tiers. But how does the system know which signers to trust? The answer is the trust list — a curated set of approved certificate authorities whose signatures the ZK circuit will accept.
The trust list is maintained by the Oracle, a signed bundle that maps trusted intermediate certificate authorities to a Merkle tree root. The ZK circuit verifies that a signing certificate chains to an intermediate in this tree — without revealing which intermediate, preserving device privacy.
Why intermediate promotion
The C2PA ecosystem has millions of device leaf keys. Every Sony camera, every Google Pixel phone, every Leica body has a unique per-device signing key. Enumerating all of them in a trust list would be impractical — the list would need to be updated every time a new device ships from the factory.
Instead, Apertrue promotes intermediate certificate authorities. In the X.509 certificate hierarchy, device leaf keys are signed by intermediate CAs, which are signed by root CAs. By promoting ~50-100 intermediate CAs, the trust list implicitly covers every device whose leaf certificate chains to one of those intermediates — millions of devices, captured in a tree with 256 slots.
Promoted intermediates
The current Oracle bundle includes intermediates covering the major C2PA-signing ecosystems:
| Intermediate CA | Key type | Covers |
|---|---|---|
| Truepic WebClaimSigningCA | RSA-2048 | ChatGPT, OpenAI image generation, Truepic-powered apps |
| Adobe Product Services G4 | RSA-4096 | Adobe Photoshop, Firefly, Lightroom, Content Credentials |
| Adobe Product Services G3 | RSA-4096 | Legacy Adobe Content Authenticity tools |
| Leica C2PA SL3-S CA | ECDSA P-256 | Leica camera hardware signing |
| Google C2PA Mobile A 1P ICA G3 L1 | ECDSA P-384 | Google Pixel cameras |
| Samsung Electronics Co. Ltd | ECDSA P-384 | Samsung Galaxy phones (S25 Ultra and newer) |
| GlobalSign GCC R6 SMIME CA 2023 | RSA-4096 | CapCut video editor (Bytedance) |
| ProofMode Root CA | ECDSA P-256 | ProofMode mobile app (self-signed root, issuer DN hashed) |
Adding support for a new manufacturer — say Samsung or Nikon when they ship C2PA-enabled devices — requires promoting a single intermediate CA. No client update, no circuit change, just a new Oracle bundle published to the CDN.
The Merkle tree
The trust list is stored as a fixed-depth Merkle tree. Each leaf is a hash of an intermediate CA's key material. The tree root is the single value that the ZK circuit checks against.
- Depth: 8 levels, giving 28 = 256 leaf slots. Comfortably holds the current 9 promoted intermediates with room for the full C2PA ecosystem to grow.
- Hash function: Pedersen hash via Barretenberg's BN254 implementation. Chosen because it's extremely cheap in ZK constraints — roughly 1,000x cheaper than SHA-256 inside a Noir circuit.
- Padding: Empty slots are filled with a zero value (64 hex zeros). The tree is always full (256 leaves), making the circuit's fixed-size input requirement simple.
- Internal nodes: Each parent is
pedersen_hash([left_child, right_child]).
Leaf hash computation
Different key types require different leaf hash schemes. The hash computed in TypeScript (during proof generation) must exactly match what the Noir circuit computes — any mismatch means the Merkle proof fails.
| Key type | Leaf hash scheme | Details |
|---|---|---|
| ECDSA P-256 | pedersen([x, y]) | 32-byte coordinates fit directly in BN254 field |
| ECDSA P-384 | pedersen([x mod p, y mod p]) | 48-byte coordinates reduced mod BN254 scalar field prime |
| RSA-2048 | pedersen(limbs[0..8]) | Modulus split into 18 limbs of 120 bits; first 8 limbs hashed |
| RSA-4096 | pedersen(limbs[0..8]) | Modulus split into 35 limbs of 120 bits; first 8 limbs hashed |
| ProofMode (2-cert) | pedersen(DN chunks) | Issuer DN packed into 31-byte field chunks and hashed |
Merkle proof
To prove an intermediate is in the trust list, the prover provides a Merkle proof — the sibling hashes along the path from the leaf to the root:
The circuit receives merkle_path and merkle_indices as private inputs (hidden from the verifier) and trust_list_root as a public input (visible to everyone). It recomputes the root from the leaf and proof, then asserts it matches the public root:
The Oracle bundle
The Oracle bundle is the signed artefact that defines the trust list. It contains the list of promoted intermediates, the Merkle tree root, and cryptographic signatures from keyholders:
Ed25519 multi-signature
The Oracle bundle requires 2-of-3 keyholder signatures to be valid. Each keyholder signs a canonical representation of the bundle — the intermediates and metadata serialised with sorted JSON keys, then SHA-256 hashed — using Ed25519.
This threshold scheme prevents a single compromised key from poisoning the trust list. An attacker would need to compromise two separate keyholders to promote a rogue intermediate CA.
Bundle validation
Both the client (browser worker) and the backend validate the Oracle bundle before using it:
- Age check: Bundles older than 7 days are rejected. This ensures revocations propagate within a week.
- Signature threshold: At least 2 valid Ed25519 signatures from known keyholders.
- Root format: Trust list root must be exactly 64 hex characters (one BN254 field element).
- Version monotonicity: Proofs must use a bundle no more than 1 version behind the current published version.
Client-side flow
When the ZK worker needs to generate a proof, it fetches the Oracle bundle and computes the Merkle proof locally:
- Fetch bundle. The oracle client tries the primary CDN, then falls back to a secondary endpoint. In development, it uses an embedded bundle to avoid network delays.
- Verify signatures. Ed25519 signature verification runs in the worker using
@noble/curves(not Web Crypto, which has restrictions in worker contexts). - Compute intermediate leaf hash. Using the certificate data extracted from the C2PA manifest, the worker computes the leaf hash matching the algorithm-specific scheme (ECDSA, RSA, or ProofMode).
- Build Merkle proof. The worker reconstructs the Merkle tree from the bundle's intermediate hashes and computes the proof path for the target leaf. It validates the computed root matches the bundle's claimed root as a tamper check.
- Pass to circuit. The leaf hash, Merkle path, Merkle indices, and trust list root are passed to the Noir circuit as inputs.
Backend validation
The Rust backend maintains its own OracleService that loads and validates the Oracle bundle independently. When a proof is submitted for verification, the backend checks that the proof's public trust_list_root exactly matches the root from the backend's loaded bundle.
This is a critical check: the client cannot use a fabricated trust list with a rogue intermediate. The trust list root is a public output of the ZK proof, visible to everyone, and the backend only accepts roots from Oracle bundles it has verified.
Self-signing attack prevention
There's a subtle attack the circuit must prevent: an attacker creates their own intermediate CA, generates a valid Merkle proof against a fabricated tree, and claims trust list membership. The circuit prevents this with a two-step verification:
- Certificate signature verification. The circuit verifies that the leaf certificate's signature was produced by the intermediate CA's public key. This binds the leaf cert to a specific intermediate.
- Leaf hash verification. The circuit recomputes the intermediate leaf hash from the same public key material used in step 1. It asserts this matches the leaf used in the Merkle proof.
- Merkle root verification. The circuit verifies the Merkle proof against the public trust list root.
The attacker cannot substitute a different key for the Merkle proof without also breaking the certificate signature verification. And they cannot fabricate a Merkle root because the backend validates it against the official Oracle bundle.
Revocation
If an intermediate CA is compromised, it needs to be revoked. The Oracle uses an epoch-based revocation system — weekly snapshots of the revoked intermediate set. Each epoch contains the list of revoked SPKI hashes and the Merkle root of still-active intermediates.
Clients fetch the current epoch alongside the bundle. If unreachable, the client proceeds and the backend performs server-side revocation checks. The 7-day bundle age limit ensures revocations propagate within a week at most.
Adding a new trusted signer
When a new manufacturer ships C2PA-enabled devices, the process to add them is:
- Extract the intermediate certificate from a sample image signed by the new device.
- Validate its Extended Key Usage (EKU) includes C2PA claim signing or an accepted legacy OID.
- Compute the SPKI hash using the appropriate scheme for the key type.
- Add the intermediate to the Oracle bundle and recompute the Merkle root.
- Collect 2-of-3 keyholder signatures on the new bundle.
- Publish the signed bundle to the CDN.
No client update is needed. No circuit recompilation. The next proof generated by any user will fetch the new bundle and can generate Merkle proofs for devices under the new intermediate. This is the operational advantage of intermediate promotion over leaf enumeration.
The next section explains how the ZK proof itself is split into two parallel proofs — ProofA for the certificate chain and hash chain (which includes the trust list check), and ProofB for the COSE signature — and why this split matters for performance.