DEV Community

Cover image for Deep Dive into VeraSnap: Building a Cryptographic Evidence Camera with Hash Chains, Hardware Signatures, and the VAP Framework

Deep Dive into VeraSnap: Building a Cryptographic Evidence Camera with Hash Chains, Hardware Signatures, and the VAP Framework

How we built a production iOS app that mathematically proves when and where photos were taken—and detects if any are deleted


Introduction: Why Does the World Need a "Camera That Cannot Lie"?

In an era of deepfakes, AI-generated imagery, and sophisticated photo manipulation, a fundamental question emerges: How do you prove a photo is authentic?

Most existing solutions—including the widely-adopted C2PA (Coalition for Content Provenance and Authenticity)—use a self-attestation model: the creator signs their own content and essentially says "trust me." But in legal, insurance, construction, and forensic contexts, self-attestation isn't enough. You need independent, third-party verification.

This is where VeraSnap comes in.

VeraSnap is an iOS application that implements the Capture Provenance Profile (CPP), an open specification under the Verifiable AI Provenance (VAP) framework. Unlike C2PA's edit-history approach, CPP focuses on a more fundamental question: Was this media actually captured at this moment, by this device, with this person present?

In this article, I'll take you deep into the technical architecture of VeraSnap:

  1. Hash Chains — How we link events into tamper-evident sequences
  2. Case-Based Chain Separation — Why each project gets its own cryptographic chain
  3. Merkle Trees — Batch anchoring for efficient timestamp verification
  4. Hardware Signatures — Leveraging Apple's Secure Enclave for unforgeable keys
  5. Completeness Invariant — Mathematically detecting deleted evidence
  6. The VAP/CPP/VCP Ecosystem — How a camera app connects to financial auditing protocols

Let's dive in.


Part 1: Hash Chains — The Foundation of Immutable History

The Core Concept

A hash chain is the simplest form of an immutable ledger. Each event in the chain contains a cryptographic reference to the previous event, creating an unbreakable sequence.

Event₁: PrevHash = "GENESIS" (or 64 zeros)
Event₂: PrevHash = EventHash(Event₁)
Event₃: PrevHash = EventHash(Event₂)
Event₄: PrevHash = EventHash(Event₃)
Enter fullscreen mode Exit fullscreen mode

If anyone attempts to modify Event₂, its hash changes. This breaks the chain because Event₃.PrevHash no longer matches the modified EventHash(Event₂).

Computing EventHash

Every CPP event is hashed using SHA-256 over its canonical JSON representation (RFC 8785 JSON Canonicalization Scheme):

func calculateEventHash(_ event: CPPEvent) -> String {
    // 1. Serialize to canonical JSON (sorted keys, no whitespace)
    let encoder = JSONEncoder()
    encoder.outputFormatting = .sortedKeys
    encoder.dateEncodingStrategy = .iso8601

    let canonicalJSON = try encoder.encode(event)

    // 2. SHA-256 hash
    let hash = SHA256.hash(data: canonicalJSON)

    // 3. Format with prefix
    return "sha256:" + hash.hexString
}
Enter fullscreen mode Exit fullscreen mode

The sha256: prefix is critical—it allows future algorithm upgrades without breaking verification logic.

Why Canonicalization Matters

JSON objects are unordered by spec. Without canonicalization, the same logical data could produce different byte sequences:

// Same data, different serializations:
{"name":"Alice","age":30}
{"age":30,"name":"Alice"}
{"name": "Alice", "age": 30}
Enter fullscreen mode Exit fullscreen mode

RFC 8785 (JCS) eliminates this ambiguity by mandating:

  • Sorted keys
  • No insignificant whitespace
  • Specific number formatting (no trailing zeros)
  • Unicode normalization

This ensures any compliant implementation produces identical hashes for identical logical data.

Deletion Detection

The hash chain's killer feature is automatic deletion detection:

Original chain:    E₁ → E₂ → E₃ → E₄
After deleting E₃: E₁ → E₂ → [gap] → E₄

Verification:
  E₄.PrevHash = "sha256:abc123..."
  EventHash(E₂) = "sha256:def456..."

  abc123 ≠ def456 → CHAIN VIOLATION DETECTED
Enter fullscreen mode Exit fullscreen mode

This is fundamentally different from C2PA, which only detects tampering within a single file—not missing files from a collection.


Part 2: Case-Based Chain Separation — One Chain Per Project

The Problem with Single-Device Chains

Early versions of our protocol used a single chain per device. This seemed logical: one device, one continuous history. But real-world usage revealed a critical flaw:

Single Chain (problematic):
[Genesis] → [Construction Site A] → [Insurance Claim B] → 
[Site A again] → [Personal Photo] → [Site A final]
Enter fullscreen mode Exit fullscreen mode

Problems:

  1. Context mixing — Unrelated captures interleaved
  2. Export leakage — Exporting Site A evidence includes unrelated events
  3. Tombstone blast radius — Invalidating one event affects unrelated proofs
  4. Audit complexity — Auditors must filter irrelevant events

The Case-per-Chain Architecture

CPP v1.0 doesn't restrict ChainID to one-per-device. VeraSnap exploits this by creating a dedicated chain for each case (project):

Case: "Construction Site A"
└── Chain-xxx: [Genesis] → [Photo1] → [Photo2] → [Photo3]

Case: "Insurance Claim B"  
└── Chain-yyy: [Genesis] → [Photo1] → [Damage Photo] → [Repair Photo]

Case: "Personal"
└── Chain-zzz: [Genesis] → [Sunset] → [Cat Photo]
Enter fullscreen mode Exit fullscreen mode

Data Model

struct Case: Identifiable, Codable, Hashable {
    // Identity
    let caseId: String           // UUIDv7
    let chainId: String          // UUIDv7 - Dedicated chain for this case

    // User metadata
    var name: String             // "Site A 2026/01"
    var description: String?
    var icon: String             // SF Symbol name
    var colorHex: String         // UI theme color

    // Timestamps
    let createdAt: Date
    var updatedAt: Date

    // State
    var isArchived: Bool
    var eventCount: Int          // Cached for performance
    var lastCaptureAt: Date?
}
Enter fullscreen mode Exit fullscreen mode

Genesis Event Per Case

When a new case is created, we generate a fresh chain with its own genesis event:

func createCase(name: String, icon: String, color: CaseColor) async throws -> Case {
    let caseId = UUIDv7.generate()
    let chainId = UUIDv7.generate()

    // Create genesis event for this chain
    let genesis = CPPEvent(
        eventId: UUIDv7.generate(),
        chainId: chainId,
        eventType: .GENESIS,
        prevHash: String(repeating: "0", count: 64),  // 64 zeros
        timestamp: Date(),
        payload: GenesisPayload(
            protocolVersion: "CPP/1.3",
            deviceInfo: DeviceInfo.current,
            chainPurpose: "EVIDENCE_CAPTURE"
        )
    )

    // Sign and store
    let eventHash = calculateEventHash(genesis)
    let signature = try cryptoService.sign(eventHash)

    try storageService.saveEvent(genesis, signature: signature)

    return Case(
        caseId: caseId,
        chainId: chainId,
        name: name,
        icon: icon,
        colorHex: color.rawValue
    )
}
Enter fullscreen mode Exit fullscreen mode

Benefits for Auditors

This architecture transforms audit workflows:

Aspect Single Chain Case-per-Chain
Export scope Entire device history Only relevant case
Privacy Unrelated data exposed Clean isolation
Tombstone impact Cross-contamination Contained to case
Verification speed O(all events) O(case events)

Part 3: Merkle Trees — Efficient Batch Anchoring

The Timestamp Challenge

RFC 3161 Time Stamping Authorities (TSAs) provide independent proof of existence at a specific time. But calling a TSA for every capture is:

  1. Expensive — Many TSAs charge per request
  2. Slow — Network round-trips add latency
  3. Privacy-invasive — TSA sees every hash you timestamp

The Merkle Solution

Instead of timestamping each event individually, we build a Merkle tree over a batch of events and timestamp only the root hash:

               ┌──────────────┐
               │  MerkleRoot  │  ← TSA timestamps this
               └──────┬───────┘
              ┌───────┴───────┐
              │               │
         ┌────┴────┐    ┌────┴────┐
         │   H01   │    │   H23   │
         └────┬────┘    └────┬────┘
        ┌─────┴─────┐   ┌─────┴─────┐
        │     │     │   │     │     │
      ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐
      │L0 │ │L1 │ │L2 │ │L3 │
      └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘
        │     │     │     │
       E0    E1    E2    E3

    Where:
      Eₙ = EventHash (input)
      Lₙ = LeafHash = SHA256(Eₙ)
      H01 = SHA256(L0 || L1)
      H23 = SHA256(L2 || L3)
      Root = SHA256(H01 || H23)
Enter fullscreen mode Exit fullscreen mode

CPP v1.3 Merkle Specification

The latest CPP specification defines exact algorithms for deterministic tree construction:

Leaf Hash Computation:

def compute_leaf_hash(event_hash: str) -> str:
    """
    LeafHash = SHA256(EventHash_bytes)

    Note: EventHash is HASHED AGAIN to create LeafHash.
    This prevents second preimage attacks.
    """
    hex_str = event_hash.replace("sha256:", "")
    event_hash_bytes = bytes.fromhex(hex_str)  # 32 bytes
    leaf_hash_bytes = sha256(event_hash_bytes)
    return "sha256:" + leaf_hash_bytes.hex()
Enter fullscreen mode Exit fullscreen mode

Why hash the hash? This follows RFC 6962 (Certificate Transparency) conventions. Without the additional hash, an attacker could potentially craft a malicious event that equals an internal node hash, creating ambiguity.

Tree Construction:

def build_merkle_tree(event_hashes: list[str]) -> str:
    # 1. Compute leaf hashes
    leaves = [compute_leaf_hash(eh) for eh in event_hashes]

    # 2. Pad to power of 2 (duplicate last leaf)
    while len(leaves) & (len(leaves) - 1):  # Not power of 2
        leaves.append(leaves[-1])

    # 3. Build tree bottom-up
    level = leaves
    while len(level) > 1:
        next_level = []
        for i in range(0, len(level), 2):
            parent = sha256(level[i] + level[i+1])  # Concatenate bytes
            next_level.append(parent)
        level = next_level

    return level[0]  # Root
Enter fullscreen mode Exit fullscreen mode

Padding Rule: If the number of leaves isn't a power of 2, duplicate the last leaf until it is. The original TreeSize is preserved in metadata.

Proof Generation and Verification

A Merkle proof allows verifying that a specific event was included in the timestamped batch:

def generate_merkle_proof(leaf_index: int, tree_levels: list) -> list[str]:
    """
    Generate sibling hashes from bottom to top
    """
    proof = []
    index = leaf_index

    for level in tree_levels[:-1]:  # Exclude root level
        sibling_index = index ^ 1  # XOR flips last bit: even↔odd
        proof.append(level[sibling_index])
        index //= 2

    return proof

def verify_merkle_proof(
    event_hash: str,
    leaf_index: int,
    proof: list[str],
    expected_root: str
) -> bool:
    """
    Recompute root from event hash and proof
    """
    current = compute_leaf_hash(event_hash)
    index = leaf_index

    for sibling in proof:
        if index % 2 == 0:  # Current is LEFT child
            current = sha256(current + sibling)
        else:              # Current is RIGHT child
            current = sha256(sibling + current)
        index //= 2

    return current == expected_root
Enter fullscreen mode Exit fullscreen mode

TSA Integration

The Merkle root is sent to an RFC 3161 TSA:

func anchorToTSA(merkleRoot: String) async throws -> TSAResponse {
    // 1. Prepare timestamp request
    let digest = Data(hexString: merkleRoot.dropFirst(7))!  // Remove "sha256:"
    let tsaRequest = try createTSARequest(
        digest: digest,
        algorithm: .sha256,
        nonce: generateNonce()
    )

    // 2. Send to TSA
    let response = try await URLSession.shared.data(
        for: URLRequest(url: tsaURL, method: .POST, body: tsaRequest)
    )

    // 3. Parse and validate response
    let tsaResponse = try parseTSAResponse(response.0)
    guard tsaResponse.messageImprint == digest else {
        throw AnchorError.imprintMismatch
    }

    return tsaResponse
}
Enter fullscreen mode Exit fullscreen mode

Key Fields in TSA Response:

  • messageImprint — The hash that was timestamped (must match our root)
  • genTime — When the timestamp was created
  • serialNumber — Unique identifier
  • TSAToken — The signed timestamp token (DER encoded)

This token can be verified offline, years later, without contacting the TSA again.


Part 4: Hardware Signatures — Apple Secure Enclave

Why Hardware Security Matters

Software-only keys are vulnerable:

  • Process memory dumps
  • File system access
  • Backup extraction
  • Malware with root access

Secure Enclave is a dedicated security coprocessor in Apple devices that:

  • Generates and stores keys in hardware
  • Performs cryptographic operations internally
  • Never exports private keys — they literally cannot leave the chip

Key Generation

func generateSecureEnclaveKey() throws -> SecKey {
    // Access control: key usable only when device unlocked
    let access = SecAccessControlCreateWithFlags(
        kCFAllocatorDefault,
        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        [.privateKeyUsage],  // Require user presence (biometric/passcode)
        nil
    )!

    let attributes: [String: Any] = [
        kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeySizeInBits: 256,
        kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,  // ← The magic
        kSecPrivateKeyAttrs: [
            kSecAttrIsPermanent: true,
            kSecAttrApplicationTag: "org.veritaschain.verasnap.signing.key",
            kSecAttrAccessControl: access
        ]
    ]

    var error: Unmanaged<CFError>?
    guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
        throw CryptoError.keyGenerationFailed(error?.takeRetainedValue())
    }

    return privateKey
}
Enter fullscreen mode Exit fullscreen mode

ES256 Signatures

CPP v1.1+ uses ECDSA P-256 (ES256) for iOS implementations:

func sign(eventHash: String) throws -> String {
    // 1. Convert hash string to bytes
    let hashHex = eventHash.replacingOccurrences(of: "sha256:", with: "")
    let hashData = Data(hexString: hashHex)!

    // 2. Sign in Secure Enclave (private key never leaves)
    var error: Unmanaged<CFError>?
    guard let signature = SecKeyCreateSignature(
        privateKey,
        .ecdsaSignatureDigestX962SHA256,  // Pre-hashed input
        hashData as CFData,
        &error
    ) else {
        throw CryptoError.signingFailed(error?.takeRetainedValue())
    }

    // 3. Format as ES256 signature string
    return "es256:" + (signature as Data).base64EncodedString()
}
Enter fullscreen mode Exit fullscreen mode

Public Key Export

While the private key never leaves the Enclave, we can export the public key for verification:

func getPublicKeyJWK() throws -> [String: Any] {
    guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
        throw CryptoError.publicKeyExtractionFailed
    }

    let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil)! as Data

    // Parse X9.63 format: 04 || x (32 bytes) || y (32 bytes)
    let x = publicKeyData[1..<33]
    let y = publicKeyData[33..<65]

    return [
        "kty": "EC",
        "crv": "P-256",
        "x": x.base64URLEncodedString(),
        "y": y.base64URLEncodedString()
    ]
}
Enter fullscreen mode Exit fullscreen mode

Security Properties

Property Guarantee
Key extraction Impossible (hardware enforced)
Key backup NOT included in iCloud/iTunes
Device binding Key is ThisDeviceOnly
User presence Biometric/passcode required
Algorithm ECDSA P-256 (FIPS 186-5)

Part 5: Completeness Invariant — Detecting Deleted Evidence

The Omission Attack Problem

Hash chains detect modification and insertion. But what about selective deletion?

Scenario: A construction company captures 100 photos of a job site. Three show safety violations. They delete those three and export the remaining 97 with a clean chain.

A hash chain alone won't catch this—the 97 remaining events still link correctly.

The XOR Hash Sum Solution

The Completeness Invariant creates a cryptographic commitment to the entire collection at the time of sealing:

def compute_completeness_invariant(events: list) -> dict:
    """
    Create a commitment that can detect ANY missing event
    """
    # XOR all event hashes together
    hash_sum = bytes(32)  # Start with 32 zero bytes
    for event in events:
        event_hash_bytes = bytes.fromhex(event.hash.replace("sha256:", ""))
        hash_sum = xor_bytes(hash_sum, sha256(event_hash_bytes))

    return {
        "expected_count": len(events),
        "hash_sum": hash_sum.hex(),
        "first_timestamp": events[0].timestamp,
        "last_timestamp": events[-1].timestamp
    }
Enter fullscreen mode Exit fullscreen mode

Why XOR Works

XOR has a critical property: self-inverse.

A ⊕ B ⊕ C = X
A ⊕ B = Y

If C is deleted:
Computed: A ⊕ B = Y
Expected: X
Y ≠ X → MISSING EVENT DETECTED
Enter fullscreen mode Exit fullscreen mode

Critically, the verifier doesn't need to know which event is missing—just that something is missing.

Verification

def verify_completeness(events: list, commitment: dict) -> bool:
    # Check count
    if len(events) != commitment["expected_count"]:
        return False  # Wrong number of events

    # Recompute hash sum
    computed = bytes(32)
    for event in events:
        computed = xor_bytes(computed, sha256(event.hash_bytes))

    # Compare
    if computed.hex() != commitment["hash_sum"]:
        return False  # Hash mismatch → deletion detected

    return True
Enter fullscreen mode Exit fullscreen mode

Attack Resistance

Attack Detection
Delete one event Count mismatch OR hash mismatch
Delete multiple events Count mismatch OR hash mismatch
Replace event with fake Hash mismatch
Reorder events Chain hash mismatch (PrevHash)

Tombstones: Legitimate Deletion

Sometimes users legitimately need to delete events (GDPR right to erasure, for example). CPP handles this through Tombstone events:

struct TombstoneEvent: CPPEvent {
    let eventId: String
    let chainId: String
    let eventType: EventType = .TOMBSTONE
    let prevHash: String
    let timestamp: Date
    let payload: TombstonePayload
}

struct TombstonePayload: Codable {
    let deletedEventId: String
    let deletedEventHash: String
    let reason: TombstoneReason
    let deletedAt: Date
    let isUserInitiated: Bool
}

enum TombstoneReason: String, Codable {
    case USER_REQUEST        // User chose to delete
    case GDPR_ERASURE        // Right to be forgotten
    case LEGAL_HOLD_RELEASE  // End of retention requirement
    case DUPLICATE           // Accidental duplicate
    case CORRUPTED           // Unrecoverable file
}
Enter fullscreen mode Exit fullscreen mode

A tombstoned event is marked as invalidated but its proof remains in the chain. The audit trail shows:

  • That something was captured
  • That it was later invalidated
  • When and why it was invalidated
  • Who authorized the invalidation

This satisfies both forensic requirements (nothing truly disappears) and privacy regulations (content is purged).


Part 6: The VAP/CPP/VCP Ecosystem

Three-Layer Hierarchy

VeraSnap isn't just a standalone app—it's part of a broader framework for cryptographically verifiable provenance across multiple domains:

┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│     VAP (Verifiable AI Provenance Framework) v1.2                   │
│     ═══════════════════════════════════════════                     │
│     Cross-domain framework defining requirements for                │
│     "cryptographically verifiable decision provenance"              │
│                                                                     │
│     Common Infrastructure:                                          │
│     ├─ Conformance Levels (Bronze/Silver/Gold)                      │
│     ├─ External Anchoring Specification (RFC 3161 / SCITT)          │
│     ├─ Completeness Invariant Pattern                               │
│     ├─ Evidence Pack Format                                         │
│     ├─ Privacy-Preserving Verification Protocol                     │
│     └─ Retention Framework                                          │
│                                                                     │
│                          │                                          │
│                          │ publishes profiles                       │
│                          ▼                                          │
│                                                                     │
│     ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐    │
│     │   VCP   │ │   CPP   │ │   CAP   │ │   DVP   │ │   MAP   │    │
│     │ Finance │ │ Capture │ │ Content │ │Automotive│ │ Medical │    │
│     │  v1.1   │ │  v1.3   │ │  v1.0   │ │ (draft) │ │ (draft) │    │
│     └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘    │
│                                                                     │
│     Domain-specific profile implementations                         │
└─────────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Shared Cryptographic Foundation

VCP (VeritasChain Protocol for financial auditing) and CPP share identical cryptographic primitives:

Purpose Algorithm Status
Event Hash SHA-256 REQUIRED
Digital Signature Ed25519 (VCP) / ES256 (CPP) REQUIRED
Merkle Tree SHA-256 (RFC 6962) REQUIRED
Completeness XOR of SHA-256 REQUIRED
JSON Canonicalization RFC 8785 (JCS) REQUIRED
External Anchoring RFC 3161 TSA REQUIRED (Silver+)
Post-Quantum ML-DSA-65 RESERVED

This means code developed for VeraSnap—Merkle tree builders, TSA clients, JSON canonicalizers—can be directly reused in VCP implementations for algorithmic trading audit trails.

Why a Camera App Matters for Financial Protocols

At first glance, a consumer camera app seems unrelated to MiFID II compliance. But there's a strategic connection:

  1. Battle-Tested Components: VeraSnap processes real-world captures from thousands of users. Every edge case discovered improves the shared infrastructure.

  2. Concept Popularization: When executives experience "my photos have cryptographic proofs," they understand "my trading decisions need cryptographic proofs."

  3. Regulatory Reference: When auditors ask "how does this work in practice?", there's a production system they can examine.

  4. Open Protocol Alternative: Unlike C2PA's vendor-controlled trust lists, the VAP ecosystem is open. Financial institutions avoid lock-in.

C2PA Comparison

Aspect C2PA CPP
Primary focus Edit history tracking Capture moment proof
Deletion detection None Completeness Invariant
External timestamp Optional Required (Silver+)
Trust model Centralized trust lists Open TSA ecosystem
Privacy Limited redaction support Privacy-by-design
Biometric binding Not supported ACE (Attested Capture)

CPP complements rather than competes with C2PA. A photo can have both:

  • CPP proof: "This was captured on Jan 29, 2026 at 3:15 PM by device XYZ"
  • C2PA manifest: "After capture, it was cropped and color-corrected in Photoshop"

Implementation Details

Proof Export Format

VeraSnap exports proofs in a portable JSON format:

{
  "proof_id": "019467a1-0001-7000-0000-000000000001",
  "proof_type": "CPP_INGEST_PROOF",
  "proof_version": "1.3",
  "event": {
    "eventId": "019467a1-0001-7000-0000-000000000002",
    "chainId": "019467a1-0000-7000-0000-000000000001",
    "eventType": "INGEST",
    "prevHash": "sha256:abc123...",
    "timestamp": "2026-01-29T15:15:00.000Z",
    "payload": {
      "assetType": "IMAGE",
      "assetHash": "sha256:def456...",
      "captureDevice": "iPhone 15 Pro",
      "humanAttestation": {
        "method": "FACE_ID",
        "result": "SUCCESS",
        "durationMs": 234
      }
    }
  },
  "signature": {
    "algorithm": "ES256",
    "value": "es256:MEUC...",
    "publicKey": {
      "kty": "EC",
      "crv": "P-256",
      "x": "base64url...",
      "y": "base64url..."
    }
  },
  "timestamp_proof": {
    "type": "RFC3161",
    "merkle_root": "sha256:789abc...",
    "merkle_proof": ["sha256:111...", "sha256:222..."],
    "merkle_index": 0,
    "tree_size": 4,
    "anchor_digest": "789abc...",
    "tsa_response": "base64:MIIx...",
    "tsa_timestamp": "2026-01-29T15:30:00.000Z",
    "tsa_service": "https://rfc3161.ai.moda"
  }
}
Enter fullscreen mode Exit fullscreen mode

Third-Party Verification

Anyone can verify a VeraSnap proof without trusting the app:

def verify_verasnap_proof(proof: dict) -> VerificationResult:
    # 1. Verify event hash
    computed_event_hash = sha256(canonicalize(proof["event"]))

    # 2. Verify signature
    public_key = load_ec_public_key(proof["signature"]["publicKey"])
    if not verify_es256(computed_event_hash, proof["signature"]["value"], public_key):
        return VerificationResult.SIGNATURE_INVALID

    # 3. Verify Merkle inclusion
    if not verify_merkle_proof(
        computed_event_hash,
        proof["timestamp_proof"]["merkle_index"],
        proof["timestamp_proof"]["merkle_proof"],
        proof["timestamp_proof"]["merkle_root"]
    ):
        return VerificationResult.MERKLE_PROOF_INVALID

    # 4. Verify TSA response
    tsa_response = base64_decode(proof["timestamp_proof"]["tsa_response"])
    if not verify_rfc3161_response(
        tsa_response,
        proof["timestamp_proof"]["anchor_digest"]
    ):
        return VerificationResult.TSA_INVALID

    return VerificationResult.VALID
Enter fullscreen mode Exit fullscreen mode

Conformance Levels

CPP defines three conformance levels allowing progressive adoption:

Level TSA Biometric Use Case
Bronze Optional Optional Personal documentation
Silver Per-batch (30 min) Optional Business records, insurance
Gold Per-capture REQUIRED Legal evidence, forensics

VeraSnap Free: Silver conformance (batch TSA anchoring)

VeraSnap Pro: Gold conformance (per-capture TSA + biometric attestation)


Conclusion

VeraSnap demonstrates that production-grade cryptographic evidence capture is achievable on consumer smartphones. The key innovations:

  1. Hash chains create tamper-evident event sequences
  2. Case-based chains provide clean audit boundaries
  3. Merkle trees enable efficient batch timestamping
  4. Secure Enclave provides hardware-backed, unforgeable signatures
  5. Completeness Invariant mathematically detects missing evidence
  6. The VAP framework connects camera evidence to broader provenance standards

The "Verify, Don't Trust" philosophy underpinning CPP represents a fundamental shift from self-attestation to independent verification. As deepfakes and AI-generated content proliferate, this shift becomes increasingly critical.


Resources


This article is published under CC BY 4.0. Technical specifications are maintained by the VeritasChain Standards Organization (VSO).


Tags: #ios #cryptography #security #swift #blockchain #authentication #privacy

Top comments (0)