System architecture
How the capture pipeline, the proof-in-file and the verifier work. For what all of this does — and deliberately does not — prove, jump to the trust model.
Design principle: two integrity layers
Signatures and watermarks fail in opposite places, so the design uses both:
| Layer | Proves | Survives re-encoding? |
|---|---|---|
| Cryptographic signature over content hashes | Exact integrity, device identity, capture time | No — any transcode invalidates it, by design |
| Robust invisible watermark (VideoSeal) | Pointer to the capture (random 128-bit capture id) — the key that finds the verified original for comparison | Yes — ids recovered from real WhatsApp and YouTube round-trips |
Capture & signing pipeline
- Device setup — no account: an ECDSA P-256 key pair is generated inside StrongBox (dedicated secure element; TEE fallback on devices without it — the level is recorded in every proof). Android Key Attestation produces an X.509 chain binding the key to hardware; the chain travels inside every proof. The app picks a random device UUID and a display name — the identity a future cloud registration will bind to this key.
- Capture: photo or video from the app, saved to the gallery. Each capture gets a fresh random 128-bit capture id.
- Watermarking — on the device: photos embed the full capture id protected by BCH error correction; videos embed a random 24-bit mark id in a repetition format sized to the harsher video channel (both via the on-device VideoSeal pipeline, ONNX). Fully offline; media never leaves the phone.
- Content Credentials: a C2PA manifest (mirroring the proof's claims) is embedded after watermarking — currently signed with development credentials, until a CA-issued certificate replaces them.
- Signing: the SHA-256 of the final bytes — watermark and
manifest included — is signed in the secure element
(
SHA256withECDSA), and the same hash is sent (never the media) to a public RFC 3161 timestamp authority when online. The private key never exists in application memory. - Proof-in-file: the proof (capture id, device id, timestamps,
RFC 3161 token, public key, attestation chain, final GOP signature for video,
signature) is appended to the file itself as a trailer that is also a valid
MP4
freebox, so players and JPEG decoders ignore it. There is no server and nothing to sync — the file carries its own proof wherever it goes.
Proof trailer & signature convention
trailer = [4B box size]["free"][proof JSON][4B json length]["TCPROOF1"] canonical = file bytes without the trailer H = SHA-256(canonical) // 32 bytes sig = ECDSA-P256( SHA-256(H) ) // Android SHA256withECDSA over H
The verifier (a static page — WebCrypto, no backend) strips the trailer,
recomputes H and checks sig against the public key
carried in the proof. It then validates the full Android Key
Attestation chain: the leaf certificate must certify exactly the
signing key, every certificate is signature-verified against its issuer, and
the chain must terminate on one of Google's published hardware attestation
roots (pinned by hash in the verifier). The proof is a claim, the signature is
the evidence, and it is re-checked from scratch on every verification — by
your browser, not by a service you must trust.
The algorithms
Every layer is built on published, widely deployed primitives — nothing bespoke guards anything important:
- Neural watermark — VideoSeal (Meta AI): an encoder/decoder network pair trained adversarially against simulated compression, resizing and cropping. The encoder learns where in the image a 256-bit message can hide below visual threshold yet survive platform processing; the decoder returns per-bit confidence ("soft bits") that can be averaged across video frames, so evidence accumulates over time.
- Error correction — BCH(255,131): the algebraic code family that keeps QR codes and deep-space telemetry readable. It guarantees exact recovery of the capture id with up to 18 corrupted bits — and beyond that it fails clean: an id is either exactly right or explicitly absent, never silently wrong (0 miscorrections in 31,000 adversarial trials).
- Signature — ECDSA P-256 on a key generated inside the phone's secure element and physically unable to leave it, with Android Key Attestation binding the key to hardware in every proof.
Watermark payload encoding
The watermark carries a 256-bit message decoded from the pixels. Robustness comes from structure, not secrecy. Photos and video use two layouts, each sized to its channel; the verifier tries the BCH layout first (its check has a ~2−124 false-accept rate, so a hit is authoritative), then the repetition layout:
Photos — capture id, BCH-protected
BCH(255,131, t=18): exact id recovery up to 18 wrong bits — beyond that it fails clean, never silently wrong
The id carries no meaning by itself — it is a random UUID drawn at capture. Its job is to be a lookup key: given any surviving copy, the verifier recovers the id and resolves it to the record of the original capture (the verified originals on file today, a cloud registry in the full design). 128 random bits make guessing or colliding ids a non-issue.
Video — mark id, repetition-protected
Measured on real WhatsApp video round-trips, the channel flips ~20–25% of the decoded bits systematically — beyond what any code carrying 128 bits in 256 can survive (the information-theoretic ceiling at that error rate is ~48 bits). What does survive is positional redundancy: video therefore embeds a random 24-bit mark id in the repetition format below, and the proof (and the originals record) binds that id to the full capture id.
= one 32-bit block
× 8 repetitions = 256 bits
copies of each bit sit 32 positions apart — an error burst hits at most one copy per bit; decoding soft-combines all 8 copies and validates the CRC
Extraction averages the decoded signal across sampled frames, soft-combines the eight copies of each bit, and accepts the result only if the CRC validates and signal agreement exceeds a threshold. Measured end-to-end on real round-trips: the mark id came back from WhatsApp (89% signal) and from a YouTube Short re-encoded to AV1 (91%).
Bitstream signatures for video
On top of the whole-file seal, every video carries a signature per GOP (group of pictures, ~1s) directly in the H.264 bitstream as SEI NAL units — the transport our device tests validated end-to-end (60/60 SEI preserved through the hardware encoder and MP4 muxer). Each GOP's VCL data is hashed and the hash signed in the secure element, exactly like the trailer seal; the signature rides in the next GOP's keyframe, and the final GOP's record travels in the proof trailer.
Unlike chained designs, the signatures are deliberately independent per GOP: a losslessly trimmed clip (a stream-copy cut, no re-encode) keeps every intact interior segment cryptographically verifiable — with the whole-file seal long gone. The verifier reports how many segments verify and the time span they cover; any tampered segment fails alone, at ~1s granularity.
Verification — fully client-side
The verifier is a static web page: nothing is uploaded, ever. Given any file it resolves in order — each level is weaker than the previous and the UI says so explicitly:
- Proof trailer present → strip, hash the canonical bytes, verify the signature with WebCrypto against the key in the proof, and validate the attestation chain up to Google's pinned hardware roots — including the leaf's attestation extension (verified boot, app identity), the chain's validity windows and Google's live revocation list (fetched best-effort; unavailability is declared, never fatal), plus the RFC 3161 token and, for video, the per-GOP bitstream signatures → Verified (original). Works offline; survives any byte-exact transfer (email, Drive, file transfer).
- No trailer (platforms strip it — but they also change the bytes, so the signature was lost either way) → watermark extraction in the browser (fp16 ONNX detector, ~67 MB, WASM). Photos are scanned directly; videos are scanned by sampling 24 frames and averaging the decoded signal. The recovered capture id is looked up in the verified originals on file: a hit shows the copy side by side with the verified original; a miss yields Origin traced only.
A command-line verifier (/verify.mjs) runs the same checks for
scripting and batch workflows.
Verified originals: comparing the copy with the capture
The mark alone identifies origin, never integrity: it survives whatever preserves most of the pixels — platform re-encoding, moderate crops, screenshots — which by the same token includes edits that keep most of the original. Instead of trying to judge integrity from the surviving signal, the design resolves the capture id to the original file, cryptographically verified at the source, and puts it next to the copy: any depicted difference is an edit, and a human sees it immediately. In the full design the app registers the device and uploads each sealed original to a cloud registry at capture time; today, with no server, a static samples file holds a handful of demo captures so the flow can be tried end to end. Caveat: an attacker with the open embedder can stamp any content with any id — a mark-only match is an origin claim, and the comparison is evidence for human judgement. Cryptographic proof remains exclusive to the signed trailer.
The binding constraint on recovery is surviving content area, not
the transform chain. Measured on the production photo pipeline (simulation in
spikes/sim_screenshot.py and spikes/spike_strength_sweep.py):
a 4080px capture re-encoded by an Instagram-class pipeline (1080px, quality 60),
screenshotted and manually cropped still decodes cleanly at the production embed
strength (×1.2, PSNR ≈45 dB); below roughly half of the original area, decoding
fails at any embed strength — the verifier then reports No proof found,
deliberately indistinguishable from "never captured with TrustCam", because
claiming anything from an unreadable signal would be guessing.
Trust model and standards alignment
The trust boundary is the app, not the sensor. Android hands camera frames to the app in userspace (CameraX), and the secure element signs whatever hash the app presents — it cannot see where the bytes came from. Key attestation narrows this: the chain proves the signing key lives in genuine hardware on a real device, and the attestation extension records bootloader lock, verified-boot state, and the identity of the app that created the key. The verifier validates the chain up to Google's pinned hardware roots and reads that extension: it reports verified-boot state and bootloader lock, and matches the key-creating app against the official package name and APK signing certificate, pinned like the roots. Even with these checks, the chain from sensor to signature is not end to end: an attacker who compromises the device or the app while still passing them could have the secure element sign an injected image. Closing that gap requires a hardware-protected camera pipeline, which no Android platform currently exposes to third-party apps. A TrustCam proof therefore asserts capture by a genuine device through an app-level pipeline — not a sensor-level guarantee. This is the same weak point that broke the first C2PA cameras: what gets signed, not the cryptography.
The design maps onto C2PA concepts (hard binding = content hash + signature; soft binding = watermark) and every capture now dual-emits: a C2PA / Content Credentials manifest is embedded alongside the trailer, readable by any Content Credentials validator. Manifests are currently signed with development credentials — external validators show the chain as untrusted until a CA-issued certificate replaces them; the trailer remains the cryptographic binding TrustCam itself verifies, and the device keys/attestation flow matches the requirements of C2PA Conformance Level 2 (hardware-backed keys). Capture time is anchored by an RFC 3161 timestamp token fetched at capture: the app sends the canonical SHA-256 — never the media — to a public timestamp authority and stores the returned token in the proof; the verifier checks it client-side against a pinned TSA chain and reports "existed no later than". Offline captures simply omit the token and the time stays device-claimed, as the verifier states explicitly.