Architecture3 min

Build vs. Buy for Provenance: Why We Own the C2PA Engine

Build vs. Buy for Provenance: Why We Own the C2PA Engine

The obvious choice was not to build another C2PA implementation. Mature tooling exists, maintained by people who know the standard inside out. We built our own engine anyway, and this post explains the reasoning, what it bought us, and what it costs. If you are evaluating build-vs-buy for trust infrastructure, this is the decision record we wish we had read first.

Frame the decision around requirements, not language preference

Architecture decisions that start with technology preference are usually wrong, so let us start with what the product must do. Five requirements kept colliding with the boundaries of wrapping an existing runtime:

1. The engine must run in the browser and across mobile devices. Evidence is sealed at capture time on the device. The signing pipeline, the JUMBF serialization, the asset hashing—all of it must execute right where the media is created. The official tooling leaves a glaring gap here: c2pa-rs is Rust, and c2pa-js (@contentauth/c2pa-web) is strictly a read-only reader in client runtimes that cannot construct or sign manifests. Even if signing were compiled into WebAssembly, WASM has a cold-start penalty, lacks direct access to native security hardware, and is a non-starter in embedded mobile runtimes like iOS JavaScriptCore, where WASM execution is unsupported.

2. It must stream large assets. Insurance video does not fit in a mobile tab's memory. The engine must hash, sign, and rewrite gigabyte-scale files without materializing them. This requires control over how asset bytes are read and buffered, down to the segment list and the chunk size. A wrapped runtime exposes an API surface, not a memory model.

3. It must carry custom assertion types. GPS traces, sensor snapshots, license plate results, and camera telemetry are not in the standard C2PA vocabulary. Our TrustLabelAssertion embeds domain data inside the signed manifest. Extending the assertion framework requires controlling how assertions are registered, serialized, and round-tripped.

4. It must preserve provenance chains when re-signing. Evidence passes through multiple processing stages. Each stage must produce a new manifest that references the previous one as a parent ingredient, not overwrite it. The V2 → V3 ingredient semantics (activeManifest + validationResults together) and the c2pa.created vs c2pa.opened action distinction are product-level decisions, not library defaults.

5. It must integrate with our own certificate issuance and timestamp services through narrow interfaces. The Signer interface has three methods. The TimestampProvider interface has one. Nothing else crosses the boundary. A wrapped runtime that bundles its own signing and timestamping becomes a negotiation about which CA and which TSA, which is a product decision hiding inside a dependency.

Each requirement alone might be negotiable. Together they describe an engine, not an integration.

What ownership demands

Owning the engine means owning everything below the abstraction boundary.

Asset containers. JPEG APP11 segment walking (0xEB markers, JUMBF header parsing, multi-segment reassembly), BMFF box surgery (ftyp/moov/mdat layout, stco/co64/iloc offset patching), PNG chunk handling, MP3 frame scanning. Each format has its own parsing code, its own hash exclusion logic, and its own gotchas. JPEG has a 64 KB segment size limit. BMFF has offset-sensitive data that must be patched when the manifest shifts mdat. PNG requires CRC recalculation per chunk.

COSE signing and RFC 3161 timestamps. The signing pipeline builds COSE Sign1 structures, manages certificate chains, and fetches RFC 3161 timestamps from our TSA. The timestamp provider interface is one method: give me bytes, give me a countersignature. Everything behind that interface is replaceable.

Validation. Validation returns structured, spec-aligned status codes (ClaimSignature.validated, AssertionHashedURI.mismatch, ClaimSignature.trustedTime.mismatch), not a boolean. Our product builds UI on those codes. Our ingredient assertions carry validation results. A boolean pass/fail would lose the information that makes the inspect view useful.

This is not free. Every spec ambiguity becomes your problem, and there are more of them than you expect.

What ownership enables

The payoff shows up in places a wrapper cannot reach.

One codebase, four runtimes, dedicated mobile targets. The same TypeScript library runs in the browser (WebCrypto + @noble/hashes) and in AWS Lambda (Node.js crypto). But crucially, esbuild compiles it directly into dedicated native mobile targets: target: 'ios16' for sdk-ios (running inside Apple's native JavaScriptCore without WASM) and an es2022 IIFE bundle for sdk-android. The JavaScript engine orchestrates manifest assembly and claim hashing, while cryptographic signing bridges cleanly to hardware roots of trust (Apple Secure Enclave via Keychain, Android Keystore). A WASM-compiled Rust library would require complex FFI bindings per platform, a separate build pipeline per architecture, and cannot run inside JavaScriptCore.

The TrustLabelAssertion. Our custom assertion carrying sensor and capture metadata exists only because we control the assertion framework. CustomJsonAssertion<T> is a base class with 40 lines of code that handles JUMBF serialization, round-trip validation, and label namespacing. Writing a new assertion type is a one-liner subclass. A wrapped runtime would require upstream support or an interop layer for custom assertion types.

Provenance-preserving re-signing. When an existing manifest is detected, the protect pipeline reads it, builds an ingredient referencing the active manifest, and creates a new manifest with a c2pa.opened action. The old manifest becomes a verifiable parent, not a discarded artifact. This builds on owned manifest semantics: the decision about whether to use c2pa.created or c2pa.opened, whether to include validationResults in the ingredient, and how to handle V2 vs V3 differences are all product decisions expressed in library code.

Streaming pipeline for large video. The BlobDataReader with lazy segments, 64 MB streaming chunks, and segment-list splicing exists because we control how asset bytes flow through the system. The hashWithExclusions function with 1 MB chunks and offset marker handling exists because we control the hashing pipeline. A wrapped runtime would need to expose these internals or impose its own memory model.

The cost side, honestly

Three bills arrive on a regular schedule.

Spec ambiguities. The entries field in training-and-data-mining assertions lives at two different levels in files produced by different implementations. The digitalSourceType uses http:// or https:// depending on the producer. Action assertions have V1 and V2 variants with different field names (softwareAgent vs softwareAgentIndex). For each ambiguity, we maintain a tolerant reader and an exact writer. The readers accumulate; they never shrink.

Standards tracking. The C2PA spec has three claim versions (V1, V2, V3), the CAWG (Creator Assertions Working Group) adds community assertions, and the IPTC vocabularies evolve independently. Tracking means reading drafts, attending to breaking changes, and updating the library before customers encounter files our code cannot read. We link upstream spec issues in code comments so the context survives developer turnover.

Interop testing. Your files have to validate in tools you do not control. We test against the C2PA reference validator, Adobe's Content Credentials, and the C2PA tool CLI. Files that validate in our library but fail in theirs are our bugs, not theirs. Files that fail in our library but validate in theirs are usually spec ambiguities that require another tolerant reader.

If your product does not need the five capabilities listed above, pay none of these bills. Wrap the reference tooling and ship.

The lesson generalizes

Differentiation in infrastructure often comes from owning exactly the abstraction layer your product must extend. The value here is not TypeScript instead of Rust. It is control over the trust boundary: the point where bytes become evidence.

The decision also stays re-evaluable. Ownership earns its keep only while requirements keep exceeding what wrapped tooling offers. The day the reference implementations cover browser execution, streaming, custom assertions, provenance chaining, and narrow signing interfaces cleanly, the calculus changes. That day is not today.

Frequently asked questions

Why not wrap c2pa-js or c2pa-rs? Wrapping gives you someone else's constraints. The official c2pa-js library is strictly a verification reader in client environments, with no client-side signing capability. Meanwhile, c2pa-rs compiles to WebAssembly, which cannot run in iOS's native JavaScriptCore runtime and has no direct bridge to the Secure Enclave. We needed capture-time signing, streaming large assets, and custom assertions—capabilities we explored when building our TypeScript engine from scratch.

Does owning the engine mean you ignore the ecosystem? The opposite. Interop is a hard requirement, so we track the spec closely, cite upstream issues in code comments, and keep readers tolerant and writers exact. Our test suite includes files produced by other implementations.

How do you keep up with spec changes? Version-parallel code paths (V1/V2/V3 claim handling, CAWG/standard assertion labels) and a conformance mindset: bytes on the wire are the contract, not any single implementation.

Would you make the same call again? Given the same five requirements, yes. With a narrower product, no. If we only needed to verify C2PA signatures on a server, we would wrap the Rust implementation and be done.

What about contributing upstream? We report spec ambiguities and file conformance issues when we find them. While our enterprise evidence pipelines integrate proprietary hardware semantics, our foundational C2PA engine is open source as c2pa-ts on GitHub. Interop findings flow back through the spec process. Trust infrastructure benefits from a healthy ecosystem more than it benefits from lock-in.

Latest articles

How to find us

Want to learn more?