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:
- Hash Chains — How we link events into tamper-evident sequences
- Case-Based Chain Separation — Why each project gets its own cryptographic chain
- Merkle Trees — Batch anchoring for efficient timestamp verification
- Hardware Signatures — Leveraging Apple's Secure Enclave for unforgeable keys
- Completeness Invariant — Mathematically detecting deleted evidence
- 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₃)
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
}
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}
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
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]
Problems:
- Context mixing — Unrelated captures interleaved
- Export leakage — Exporting Site A evidence includes unrelated events
- Tombstone blast radius — Invalidating one event affects unrelated proofs
- 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]
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?
}
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
)
}
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:
- Expensive — Many TSAs charge per request
- Slow — Network round-trips add latency
- 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)
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()
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
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
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
}
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
}
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()
}
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()
]
}
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
}
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
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
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
}
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 │
└─────────────────────────────────────────────────────────────────────┘
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:
Battle-Tested Components: VeraSnap processes real-world captures from thousands of users. Every edge case discovered improves the shared infrastructure.
Concept Popularization: When executives experience "my photos have cryptographic proofs," they understand "my trading decisions need cryptographic proofs."
Regulatory Reference: When auditors ask "how does this work in practice?", there's a production system they can examine.
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"
}
}
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
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:
- Hash chains create tamper-evident event sequences
- Case-based chains provide clean audit boundaries
- Merkle trees enable efficient batch timestamping
- Secure Enclave provides hardware-backed, unforgeable signatures
- Completeness Invariant mathematically detects missing evidence
- 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
- VeraSnap: App Store
- CPP Specification: github.com/veritaschain/cpp-spec
- VCP Specification: github.com/veritaschain/vcp-spec
- VAP Framework: github.com/veritaschain/vap-spec
- Website: veritaschain.org
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)