DEV Community

Cover image for Building a Camera That Cannot Lie: How VeriCapture's CPP Implementation Advances Verifiable Provenance

Building a Camera That Cannot Lie: How VeriCapture's CPP Implementation Advances Verifiable Provenance

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 ✅
Enter fullscreen mode Exit fullscreen mode

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     │            │
│  └─────────────┘ └─────────────┘ └─────────────┘            │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key design decisions:

  1. Zero external dependencies - Only Apple frameworks for security auditability
  2. Swift 6.0 Strict Concurrency - Data race safety guaranteed at compile time
  3. Secure Enclave keys - Private keys never leave hardware
  4. 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    │
              └──────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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]                │
      └──────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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ₙ
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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                     ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

en.lproj/Localizable.strings:

"camera.title" = "Camera";
"verify.title" = "Verify";
"verify.chain_integrity" = "Chain Integrity";
Enter fullscreen mode Exit fullscreen mode

ja.lproj/Localizable.strings:

"camera.title" = "カメラ";
"verify.title" = "検証";
"verify.chain_integrity" = "チェーン整合性";
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Validating the architecture in production
  2. Building developer familiarity with VAP patterns
  3. Creating reference implementations for enterprise adoption
  4. 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:

  1. Android implementation - Bringing CPP to the other 70% of the market
  2. Web verification tool - Verify proofs without installing an app
  3. SDK release - Let other apps integrate CPP
  4. 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)