Skip to main content

How It Works

A visual overview of the SignedShot capture and verification flow.

The Big Picture

┌─────────────────────────────────────────────────────────────────────────────┐
│ CAPTURE SIDE │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Device │ │ Start │ │ Capture │ │ Sign │ │
│ │ Register │ ──── │ Session │ ──── │ Media │ ──── │ & Save │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ Get device Get nonce Take photo Sign with SE │
│ token (once) + capture_id or video Get JWT │
│ Save sidecar │
└─────────────────────────────────────────────────────────────────────────────┘

│ media + sidecar

┌─────────────────────────────────────────────────────────────────────────────┐
│ VERIFICATION SIDE │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Parse │ │ Verify │ │ Verify │ │ Cross │ │
│ │ Sidecar │ ──── │ JWT │ ──── │ Hash │ ──── │ Validate │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ Read JSON Check sig Compare hash Match IDs │
│ structure via JWKS + verify sig JWT ↔ media │
└─────────────────────────────────────────────────────────────────────────────┘

Capture Flow

Step 1: Register Device (Once)

┌─────────────┐                              ┌─────────────┐
│ │ POST /devices │ │
│ iOS App │ ─────────────────────────► │ Server │
│ │ X-Publisher-ID │ │
│ │ X-Attestation-Token │ │
│ │ │ │
│ │ ◄───────────────────────── │ │
│ │ device_token │ │
└─────────────┘ └─────────────┘
  • App sends publisher ID and attestation token
  • Server verifies device via Firebase App Check
  • Server returns device_token (store securely, use for all future requests)
  • This only happens once per device

Step 2: Start Capture Session

┌─────────────┐                              ┌─────────────┐
│ │ POST /capture/session │ │
│ iOS App │ ─────────────────────────► │ Server │
│ │ Authorization: Bearer │ │
│ │ │ │
│ │ ◄───────────────────────── │ │
│ │ capture_id, nonce │ │
└─────────────┘ └─────────────┘
  • App requests a new capture session
  • Server returns capture_id (unique ID for this capture) and nonce (one-time token)
  • Session expires after a short window

Step 3: Capture Media

┌─────────────────────────────────────────────────────────┐
│ │
│ 📷 CAPTURE │
│ │
│ User takes photo or video using the app's camera │
│ │
│ The raw bytes are immediately available for signing │
│ │
└─────────────────────────────────────────────────────────┘
  • User captures media through the app
  • Media bytes are available before any disk write

Step 4: Sign with Secure Enclave

┌─────────────────────────────────────────────────────────┐
│ SECURE ENCLAVE │
│ ┌───────────────────────────────────────────────────┐ │
│ │ │ │
│ │ media bytes ──► SHA-256 ──► hash │ │
│ │ │ │
│ │ hash + capture_id + timestamp ──► ECDSA sign │ │
│ │ │ │
│ │ Result: signature + public_key │ │
│ │ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ Private key NEVER leaves the Secure Enclave │
└─────────────────────────────────────────────────────────┘
  • Compute SHA-256 hash of media bytes
  • Sign {hash}:{capture_id}:{timestamp} with Secure Enclave
  • Private key never leaves secure hardware

Step 5: Exchange Nonce for JWT

┌─────────────┐                              ┌─────────────┐
│ │ POST /capture/trust │ │
│ iOS App │ ─────────────────────────► │ Server │
│ │ { nonce } │ │
│ │ │ │
│ │ ◄───────────────────────── │ │
│ │ trust_token (JWT) │ │
└─────────────┘ └─────────────┘
  • App sends the nonce received in Step 2
  • Server validates nonce (one-time use) and issues signed JWT
  • JWT contains: publisher_id, device_id, capture_id, attestation method

Step 6: Save Media + Sidecar

┌─────────────────────────────────────────────────────────┐
│ │
│ photo.jpg photo.sidecar.json │
│ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ │ │ { │ │
│ │ [image │ │ "version": "1.0", │ │
│ │ bytes] │ │ "capture_trust": { │ │
│ │ │ │ "jwt": "eyJ..." │ │
│ │ │ │ }, │ │
│ │ │ │ "media_integrity": { │ │
│ │ │ │ "content_hash":..., │ │
│ └─────────────┘ │ "signature": ... │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────┘ │
│ │
│ Both files travel together │
└─────────────────────────────────────────────────────────┘
  • Save original media file (unchanged)
  • Save sidecar JSON with both trust layers
  • Files can be stored, shared, or uploaded together

Verification Flow

Anyone Can Verify

┌─────────────────────────────────────────────────────────┐
│ │
│ INPUT: media file + sidecar.json │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. Parse sidecar JSON │ │
│ │ ↓ │ │
│ │ 2. Verify JWT signature (fetch JWKS) │ │
│ │ ↓ │ │
│ │ 3. Compute SHA-256 of media │ │
│ │ ↓ │ │
│ │ 4. Compare with content_hash │ │
│ │ ↓ │ │
│ │ 5. Verify ECDSA signature │ │
│ │ ↓ │ │
│ │ 6. Confirm capture_id matches │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ OUTPUT: ✓ VALID or ✗ INVALID + reason │
│ │
└─────────────────────────────────────────────────────────┘

What Each Layer Proves

┌─────────────────────────────────────────────────────────┐
│ │
│ CAPTURE TRUST (JWT) MEDIA INTEGRITY │
│ ───────────────── ──────────────── │
│ │
│ ✓ Verified device ✓ Content unchanged │
│ ✓ Authorized app ✓ Signed by device │
│ ✓ Valid session ✓ Timestamp bound │
│ ✓ Attestation method ✓ Capture ID linked │
│ │
│ ╲ ╱ │
│ ╲ ╱ │
│ ╲ ╱ │
│ ▼ ▼ │
│ ┌─────────────────────────────┐ │
│ │ │ │
│ │ "This exact content was │ │
│ │ captured on a verified │ │
│ │ device and hasn't been │ │
│ │ modified since." │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘

Quick Reference

StepActionResult
1Register devicedevice_token (one-time)
2Start sessioncapture_id + nonce
3Capture mediaRaw bytes
4Sign in Secure Enclavesignature + public_key
5Exchange noncetrust_token (JWT)
6Save filesmedia + sidecar.json
7Verify✓ or ✗

Next Steps