TL;DR
We built an iOS camera app that creates cryptographically verifiable proof of when and where photos were taken. No blockchain required—just RFC 3161 timestamps, Merkle trees, and a novel "Completeness Invariant" that detects if anyone deletes evidence. Here's how it works and why it matters for the future of digital trust.
The Problem: Digital Trust is Broken
In 2026, we face an unprecedented crisis of digital authenticity. AI-generated images are indistinguishable from real photos. Metadata can be trivially forged. And existing solutions like C2PA (Coalition for Content Provenance and Authenticity) rely on self-attestation—creators signing their own claims with no independent verification.
C2PA Model:
Creator signs → "Trust me" → NO INDEPENDENT CHECK ❌
Our Model (CPP):
Creator signs → TSA countersigns → INDEPENDENT THIRD-PARTY ✅
This is the fundamental flaw. In any serious evidentiary context—legal proceedings, journalism, insurance claims—"trust me, I signed it" doesn't cut it.
Introducing VeriCapture: A Camera That Cannot Lie
VeriCapture is an iOS application implementing the Capture Provenance Profile (CPP)—an open specification we developed for cryptographically verifiable media capture. The core promise:
"Verify, Don't Trust"
Every photo captured with VeriCapture includes:
- ✅ Cryptographic signature (ES256 via Secure Enclave)
- ✅ RFC 3161 timestamp from independent Time-Stamping Authority
- ✅ Merkle tree inclusion for batch verification
- ✅ Completeness Invariant that detects deleted events
- ✅ Hash chain linkage connecting all captures
Let's dive into the technical implementation.
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CameraView │ │ GalleryView │ │ VerifyView │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ SERVICES LAYER │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │CryptoService│ │AnchorService│ │StorageService│ │
│ │ (Sendable) │ │(@MainActor) │ │ (Sendable) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ DATA LAYER │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ SQLite3 Database ││
│ │ Tables: events, assets, anchors, chains, tombstones ││
│ └─────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────┤
│ EXTERNAL SERVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ RFC 3161 │ │ Apple │ │ StoreKit2 │ │
│ │ TSA Server │ │Secure Enclave│ │ IAP │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
Key design decisions:
- Zero external dependencies - Only Apple frameworks for security auditability
- Swift 6.0 Strict Concurrency - Data race safety guaranteed at compile time
- Secure Enclave keys - Private keys never leave hardware
- Local-first - Works offline, syncs timestamps when connected
The Capture Event Flow
When a user taps the shutter, here's what happens:
User Taps Shutter
│
▼
┌──────────────────┐
│ CaptureViewModel │
│ takePhoto() │
└────────┬─────────┘
│
▼
┌──────────────────┐ ┌──────────────────┐
│ CameraService │────▶│ Raw Image │
│ capture() │ │ (JPEG Data) │
└──────────────────┘ └────────┬─────────┘
│
┌────────────────────────┴───────────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ SHA-256 Hash │ │ Sensor Data │
│ assetHash │ │ GPS, Accel, etc │
└────────┬─────────┘ └────────┬─────────┘
│ │
└──────────────────┬──────────────────────────┘
▼
┌──────────────────┐
│ CPPEventBuilder │
│ buildEvent() │
└────────┬─────────┘
│
▼
┌──────────────────────┐
│ Secure Enclave │
│ ES256 Signature │
└──────────────────────┘
The CPP Event Structure
Every capture generates a JSON event following RFC 8785 (JSON Canonicalization Scheme):
struct CPPEvent: Codable, Sendable {
// Event Identity
let eventId: String // UUIDv7 format
let chainId: String // Device chain identifier
let prevHash: String // Hash chain linkage
let timestamp: String // ISO 8601 with milliseconds
// Event Metadata
let eventType: CPPEventType // INGEST or EXPORT
let hashAlgo: String // "SHA256"
let signAlgo: String // "ES256"
// Payload
let payload: CPPPayload
// Security
let signature: String // Base64-encoded ES256
}
The eventHash is computed as:
func computeEventHash(_ event: CPPEvent) -> String {
// 1. Serialize to canonical JSON (RFC 8785)
let canonical = try JSONEncoder.canonical.encode(event)
// 2. SHA-256 hash
let hash = SHA256.hash(data: canonical)
// 3. Prefix with algorithm identifier
return "sha256:" + hash.hexString
}
Critical: No Exclusion Lists
Unlike C2PA, CPP has zero exclusion lists. Every field is covered by the signature. If any byte changes, verification fails. This eliminates a major attack vector where malicious actors modify "excluded" metadata.
RFC 3161 Timestamping: The Independent Witness
The killer feature of CPP is mandatory external timestamping. Here's how we implement it:
Batch Anchoring Flow
Timer Fires (every 30 minutes)
OR Manual Trigger
│
▼
┌──────────────────┐
│ AnchorService │
│ processBatch() │
└────────┬─────────┘
│
▼
┌──────────────────────────────────────────┐
│ Build Merkle Tree │
│ │
│ [MerkleRoot] │
│ / \ │
│ [H01] [H23] │
│ / \ / \ │
│ [E0] [E1] [E2] [E3] │
└────────────────────┬─────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Create ASN.1 DER RFC 3161 Request │
│ │
│ TimeStampReq ::= SEQUENCE { │
│ version INTEGER { v1(1) }, │
│ messageImprint MessageImprint, │
│ certReq BOOLEAN DEFAULT FALSE │
│ } │
└────────────────────┬─────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ HTTP POST to TSA Server │
│ │
│ POST https://rfc3161.ai.moda │
│ Content-Type: application/timestamp-query │
│ Body: [ASN.1 DER Request] │
└──────────────────────────────────────────┘
Why Merkle Trees?
Instead of timestamping every photo individually (expensive, slow), we batch events into a Merkle tree and timestamp only the root. This gives us:
- O(log n) proof size - Verify any event with ~10 hashes
- Batch efficiency - One TSA request for hundreds of events
- Privacy preservation - Reveal only the event being verified
func buildMerkleTree(eventHashes: [String]) -> MerkleTree {
// Pad to power of 2
var leaves = eventHashes
while !leaves.count.isPowerOfTwo {
leaves.append(leaves.last!) // Duplicate last leaf
}
// Build tree bottom-up (RFC 6962 compatible)
var currentLevel = leaves.map { SHA256.hash($0) }
var tree = [currentLevel]
while currentLevel.count > 1 {
var nextLevel: [Data] = []
for i in stride(from: 0, to: currentLevel.count, by: 2) {
let combined = currentLevel[i] + currentLevel[i+1]
nextLevel.append(SHA256.hash(combined))
}
tree.append(nextLevel)
currentLevel = nextLevel
}
return MerkleTree(levels: tree, root: currentLevel[0])
}
TSA Selection
We evaluated 17 public TSA providers. Our recommendation:
| TSA | URL | Why |
|---|---|---|
| rfc3161.ai.moda | https://rfc3161.ai.moda |
Auto-failover to 7+ TSAs, 99.995% uptime |
| DigiCert | http://timestamp.digicert.com |
Adobe AATL certified |
| Sectigo | https://timestamp.sectigo.com |
eIDAS compliant option |
Important: We only send the Merkle root hash to TSAs—never image data or PII.
The Completeness Invariant: Detecting Deletions
This is our novel contribution. Traditional hash chains can detect tampering, but not deletion. If someone removes event #3 from a chain, and you only have events #1, #2, #4, #5... how do you know #3 ever existed?
The Math
We compute a running XOR of all event hashes:
CI = {
expected_count: n,
hash_sum: H(E₁) ⊕ H(E₂) ⊕ ... ⊕ H(Eₙ),
first_timestamp: T₁,
last_timestamp: Tₙ
}
When a collection is "sealed," we record this Completeness Invariant. Later, to verify:
def verify_completeness(events, seal):
ci = seal.completeness_invariant
# Check count
if len(events) != ci.expected_count:
return VIOLATION # Missing or extra events!
# Recompute hash sum
computed = bytes(32)
for e in events:
computed = xor(computed, sha256(e))
# Compare
if computed != ci.hash_sum:
return VIOLATION # Hash mismatch!
return VALID
Attack Detection
| Attack | Detection Method |
|---|---|
| Delete event | Hash sum mismatch |
| Add fake event | Count + hash mismatch |
| Reorder events | Chain hash mismatch |
| Replace event | Individual hash mismatch |
The beauty of XOR is that it's order-independent for detection but order-dependent when combined with the hash chain. An attacker would need to find a collision in SHA-256 to forge a valid Completeness Invariant—computationally infeasible.
Attested Capture Mode: Binding Biometrics
For high-assurance scenarios, VeriCapture offers "Attested Capture Mode" using Face ID/Touch ID:
struct HumanAttestation: Codable, Sendable {
let attestationType: String // "FACE_ID" | "TOUCH_ID"
let attestationResult: String // "SUCCESS" | "FAILURE"
let attestationDuration: Double // Seconds
let deviceAttestation: String? // Apple DeviceCheck token
}
Critical privacy principle:
"We prove authentication was attempted. We store ZERO biometric data."
| ✅ We Record | ❌ We Do NOT Record |
|---|---|
| Auth method used | Facial geometry |
| Success/failure | Fingerprint data |
| Duration | Biometric templates |
| Device attestation | Raw sensor data |
This satisfies legal requirements (proving a human was present) without creating a biometric database.
Deletion Without Lying: Tombstones
CPP separates Media (the actual photo) from Proof (the cryptographic evidence). Users can delete photos for privacy, but the proof remains. If they want to invalidate the proof entirely, we record a Tombstone:
struct TombstoneEvent: Codable, Sendable {
let eventType: String = "CPP_TOMBSTONE"
let target: TombstoneTarget
let reason: TombstoneReason
let timestamp: String
let signature: String
}
enum TombstoneReason: String, Codable {
case userRequested = "USER_REQUESTED"
case privacyConcern = "PRIVACY_CONCERN"
case legalRequirement = "LEGAL_REQUIREMENT"
case accidentalCapture = "ACCIDENTAL_CAPTURE"
}
The Tombstone is added to the hash chain, so:
"Delete ≠ Never happened"
"Delete = Record that deletion occurred"
This is GDPR-compliant (users can remove their data) while maintaining audit integrity (the fact of deletion is recorded).
Chain Integrity Verification UI
We built a comprehensive verification dashboard:
┌─────────────────────────────────────────────────────────────┐
│ Chain Integrity │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐│
│ │ CHAIN STATISTICS ││
│ │ ││
│ │ 47 45 2 ││
│ │ Total Active Invalidated ││
│ │ ││
│ │ 🔖 Tombstones: 2 ✅ Anchored: 40 ⏱ Pending: 7 ││
│ │ ││
│ │ Date Range: 2026/01/15 → 2026/01/19 ││
│ └─────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐│
│ │ INTEGRITY VERIFICATION ││
│ │ ││
│ │ [🔒 Run Verification >] ││
│ │ ││
│ │ After verification: ││
│ │ ✅ Chain Integrity Verified ││
│ │ Checked 47 events, 2 tombstones ││
│ │ Verified at 2026/01/19 15:30:45 ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
Users can independently verify their entire proof chain at any time.
Localization: Privacy Respects Culture
VeriCapture ships with full English and Japanese localization:
// Localization.swift
struct L10n {
enum Camera {
static let title = "camera.title".localized
static let takePhoto = "camera.take_photo".localized
}
enum Verify {
static let title = "verify.title".localized
static let chainIntegrity = "verify.chain_integrity".localized
}
}
en.lproj/Localizable.strings:
"camera.title" = "Camera";
"verify.title" = "Verify";
"verify.chain_integrity" = "Chain Integrity";
ja.lproj/Localizable.strings:
"camera.title" = "カメラ";
"verify.title" = "検証";
"verify.chain_integrity" = "チェーン整合性";
UI Guidelines: Provenance ≠ Truth
A critical design principle: never mislead users about what we prove.
| ✅ Use | ❌ Avoid |
|---|---|
| "Provenance Available" | "Verified" |
| "Capture Recorded" | "Authenticated" |
| Information icon (ℹ️) | Checkmark (✓) |
Required disclosure on every proof:
"This shows capture data. It does NOT verify content truthfulness."
We prove when and where a photo was taken, and that it hasn't been tampered with. We do NOT prove that the scene depicted is "true" or "real."
Conformance Levels
CPP defines three conformance levels, aligned with the broader VAP (Verifiable AI Provenance) framework:
| Level | TSA Frequency | Completeness | Retention | Use Case |
|---|---|---|---|---|
| Bronze | Optional | Required | 6 months | Personal, PoC |
| Silver | Daily | Required | 2 years | Commercial |
| Gold | Per-capture | Required | 5+ years | Legal evidence |
VeriCapture implements Silver by default, with Gold available for premium subscribers.
C2PA Interoperability
We're not trying to replace C2PA—we complement it. CPP answers "Was this actually captured?" while C2PA answers "How was this edited?"
Export format for C2PA compatibility:
{
"c2pa.actions": [
{
"action": "c2pa.captured",
"parameters": {
"vso.cpp.verification_url": "https://verify.veritaschain.org/cpp/CPP-2026-ABC123XYZ"
}
}
]
}
The Bigger Picture: Why This Matters
VeriCapture is the first consumer implementation of CPP, but CPP itself is one profile in the VAP (Verifiable AI Provenance) framework. Other profiles include:
- VCP - VeritasChain Protocol for financial trading audit trails
- CAP - Content/Creative AI Profile for generative AI provenance
- DVP - Driving Vehicle Profile for autonomous vehicle decisions
- MAP - Medical AI Profile for healthcare AI accountability
All profiles share:
- Common cryptographic primitives (SHA-256, Ed25519/ES256)
- RFC 3161 external anchoring
- Completeness Invariant pattern
- Evidence Pack format
- Privacy-preserving verification
By shipping VeriCapture to consumers, we're:
- Validating the architecture in production
- Building developer familiarity with VAP patterns
- Creating reference implementations for enterprise adoption
- Educating the market on verifiable provenance
Try It Yourself
VeriCapture is available on the App Store (iOS 17.0+).
Technical specifications:
| Attribute | Value |
|---|---|
| Platform | iOS 17.0+ |
| Language | Swift 6.0 (Strict Concurrency) |
| Cryptography | ES256 (ECDSA P-256), SHA-256 |
| Key Storage | Apple Secure Enclave |
| TSA Protocol | RFC 3161 |
| Database | SQLite3 (local encrypted) |
| Bundle ID | org.veritaschain.vericapture |
Open specifications:
What's Next
We're working on:
- Android implementation - Bringing CPP to the other 70% of the market
- Web verification tool - Verify proofs without installing an app
- SDK release - Let other apps integrate CPP
- Gold conformance - Per-capture TSA timestamps for legal-grade evidence
Conclusion
Digital trust is broken, but it's fixable. The solution isn't "trust me"—it's "verify me."
VeriCapture demonstrates that cryptographically verifiable media capture is practical, privacy-preserving, and user-friendly. The same patterns apply to financial auditing (VCP), AI decision trails (CAP), and beyond.
The age of "Verify, Don't Trust" begins now.
Resources:
This article is part of the "Verifiable AI Provenance" series. Follow for updates on VCP, CAP, and other VAP profiles.
© 2026 VeritasChain Standards Organization. CC BY 4.0
Top comments (0)