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) andnonce(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
| Step | Action | Result |
|---|---|---|
| 1 | Register device | device_token (one-time) |
| 2 | Start session | capture_id + nonce |
| 3 | Capture media | Raw bytes |
| 4 | Sign in Secure Enclave | signature + public_key |
| 5 | Exchange nonce | trust_token (JWT) |
| 6 | Save files | media + sidecar.json |
| 7 | Verify | ✓ or ✗ |
Next Steps
- Two-Layer Trust — Deep dive into the trust model
- Quick Start — Verify media in 5 minutes
- iOS Integration — Capture signed media