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.

Two layers Capture & signing Proof trailer Algorithms Payload encoding Bitstream signatures Verification Verified originals Trust model

Design principle: two integrity layers

Signatures and watermarks fail in opposite places, so the design uses both:

LayerProvesSurvives re-encoding?
Cryptographic signature over content hashesExact integrity, device identity, capture timeNo — 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 comparisonYes — ids recovered from real WhatsApp and YouTube round-trips
Your capture signed + watermarked
shared as exact file (email, drive, download)
Social platform re-encodes the pixels, strips all metadata
Verified exact copy → signature social copy → watermark

Capture & signing pipeline

Camera photo / video file written to gallery
SHA-256 32-byte fingerprint of the exact bytes
🔒 Secure element ECDSA P-256 — the private key never leaves the chip
Proof sealed in file signature + key + attestation + timestamp
  1. 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.
  2. Capture: photo or video from the app, saved to the gallery. Each capture gets a fresh random 128-bit capture id.
  3. 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.
  4. 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.
  5. 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.
  6. 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 free box, 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:

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

capture id — 128 bit (random) BCH parity — 124 bit

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.

payload — 24 bit (device id + counter) CRC-8

= 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:

  1. 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).
  2. 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.