Under the hood
Every signed image passes through a multi-layer pipeline that combines cryptographic signatures, invisible watermarking, content hashing, and blockchain anchoring. This page explains each layer in detail.
On this page
Signing PipelineInvisible Watermarking (DWT+DCT)Dual Hashing (SHA-256 + pHash)Video Signing (Segment-Chained Hashing)Ed25519 Cryptographic SigningBlockchain Anchoring (Polygon + Merkle)C2PA Content CredentialsVerification PipelineSigning Pipeline
When you call POST /api/v1/sign, the backend runs these steps in sequence:
Upload image
│
├─ Validate (PNG/JPEG/WebP/MP4/WebM, max 50MB image / 500MB video)
├─ Fetch Ed25519 signing key from DB (verify ownership)
│
├─ compute_sha256(bytes) → exact fingerprint
├─ compute_perceptual_hash(bytes) → fuzzy fingerprint
│
├─ fingerprint_data = sha256|phash|timestamp|user_id
├─ decrypt_private_key(encrypted_key) ← Fernet decryption
├─ signature = Ed25519.sign(private_key, fingerprint_data)
│
├─ watermark_payload = SHA256(signature)[:16 bytes]
├─ embed_watermark(image, payload) ← DWT+DCT steganography
│
├─ [optional] add_c2pa_manifest(image, org, sha256, pubkey)
│
├─ Save ContentRecord to PostgreSQL
├─ Push to Redis batch queue
├─ Dispatch webhook (if configured) → POST to subscriber URL
│
└─ Return { record_id, sha256, certificate, watermarked_file }Invisible Watermarking (DWT+DCT)
The watermark is a 128-bit (16-byte) payload derived from the Ed25519 signature. It is embedded using a two-stage transform:
Stage 1 - Discrete Wavelet Transform (DWT)
The image is decomposed into frequency sub-bands. Low-frequency components carry most visual information; the watermark is embedded in mid-frequency coefficients that survive compression.
Stage 2 - Discrete Cosine Transform (DCT)
Within the DWT sub-bands, DCT is applied per block. The payload bits modulate specific DCT coefficients. This dual-domain embedding makes the watermark robust against JPEG compression, cropping, and re-encoding.
Robustness
The watermark survives JPEG compression down to ~60% quality, social media re-encoding (Instagram, Twitter), screenshots, and moderate cropping. Verification uses Hamming distance ≤ 6 bits for fuzzy matching.
Dual Hashing
SHA-256 (Exact Match)
A SHA-256 hash of the raw image bytes is computed and stored. This provides exact-match verification - any single bit change produces a completely different hash. Both the original file hash and the watermarked file hash are stored, so the original can still be verified after watermarking.
Perceptual Hash - pHash (Fuzzy Match)
A perceptual hash (using the imagehash library) is also computed. pHash encodes the visual structure of an image into a 64-bit integer. Two images with Hamming distance ≤ threshold are considered the same image. This catches re-compressed, cropped, or slightly edited versions.
Verification pre-filters candidates using a 4-character hash prefix before full Hamming comparison, making lookup fast even at scale.
Video Signing (Segment-Chained Hashing)
Videos use a specialized segment-chained hashing architecture that divides the timeline into segments, computes a chained hash across segments, and stores one record per segment rather than per frame. This is both faster to process and more resilient to partial corruption.
How It Works
Upload video (MP4/WebM, up to 5GB depending on plan)
│
├─ Stream to MinIO/S3, compute SHA-256 on the fly
├─ Create parent ContentRecord (status: "processing")
├─ Dispatch background Celery task
│
└─ Background task:
│
├─ Extract keyframes (seek-based, adaptive FPS)
│ └─ ≤5min: 1fps │ ≤30min: 0.5fps │ ≤2hr: 0.2fps │ >2hr: 0.1fps
│
├─ Parallel pHash computation (ProcessPoolExecutor)
│ └─ Each frame → 64-bit perceptual hash
│
├─ Group frames into 5-second segments
│
├─ Build segment chain:
│ segment_hash[0] = SHA256(ph0 || ph1 || ... || 0x00)
│ segment_hash[1] = SHA256(ph3 || ph4 || ... || segment_hash[0])
│ segment_hash[N] = SHA256(phK || ... || segment_hash[N-1])
│
├─ Store one ContentRecord per segment (not per frame)
│ └─ segment_hash, aggregate_phash, chain JSON
│
├─ Store final_chain_hash on parent record
└─ Push to Merkle batch queue → Polygon blockchainSegment Chain Design
Each segment's hash is computed from the perceptual hashes of its constituent frames concatenated with the previous segment's hash. This creates a forward chain where each segment depends on all prior segments:
Chaining formula
segment_hash[i] = SHA256(frame_phash_0 || frame_phash_1 || ... || segment_hash[i-1])The first segment chains from a zero seed (0x00 * 32). Each subsequent segment incorporates the hash of the previous segment, forming an ordered chain.
Tamper Detection + Corruption Tolerance
If a single segment is tampered with, the chain hash breaks for that segment and all subsequent ones - making tampering detectable. But each segment is also individually verifiable via its aggregate pHash, so a few corrupted segments (common with video re-encoding) don't invalidate the entire video.
Why Segments, Not Frames
Per-frame (old)
5-min video = ~300 DB rows, 300+ Merkle entries, 10 separate DB queries per verification
Per-segment (new)
5-min video = ~60 DB rows, ~60 Merkle entries, 1 bulk DB query per verification
Video Verification
When verifying a video, the system re-extracts keyframes, groups them into the same segments, and recomputes the segment chain. It then compares against stored segments using:
Exact video SHA-256 match (video completely untouched)
Exact segment hash match for each segment (segment pixels unchanged)
Segment aggregate pHash similarity (segment re-encoded but visually identical)
Blockchain check via parent record's Merkle proof (single check)
The confidence score is verified_segments / total_segments. A video is considered authentic if ≥50% of segments verify, or if the overall check confidence is ≥50%. The response includes a chain_intact flag indicating whether all segments matched exactly (no re-encoding or tampering).
Ed25519 Cryptographic Signing
Each organization registers Ed25519 keypairs (via PyNaCl). The private key is encrypted at rest using Fernet symmetric encryption - only the organization's Fernet key can decrypt it. This means even a database breach cannot expose private signing keys.
# What is signed:
fingerprint = f"{sha256}|{phash}|{timestamp}|{user_id}"
signature = nacl.signing.SigningKey(private_key).sign(fingerprint)
# Stored:
signature_hex = signature.hex()
public_key_hex = signing_key.verify_key.encode().hex()The signature is verifiable by anyone with the signer's public key - no Tectra servers required.
Blockchain Anchoring (Polygon + Merkle Trees)
Batch Accumulation
Content records are not anchored individually (which would be expensive). Instead, records are batched:
Every ~10 seconds, Celery task runs:
1. Collect all pending hashes from Redis queue
2. Build Merkle tree from {record_id: sha256} leaves
3. Compute merkle_root = tree.root
4. Call ProvenanceRegistry.registerBatch(
merkleRoot, leafCount, signerPubKey, timestamp
) on Polygon
5. Store tx_hash, merkle_root, and per-record proofs in PostgreSQLMerkle Proof Verification
Each content record stores its Merkle proof path. During verification, the proof is re-verified client-side against the on-chain Merkle root. This means verification is fully trustless - you only need the Polygon contract.
Smart Contract
The ProvenanceRegistry.sol contract is deployed on Polygon. It stores a mapping from Merkle root to batch metadata (timestamp, signer pub key, leaf count). See the Blockchain docs for the full ABI.
C2PA Content Credentials
Tectra is a C2PA member. Optionally, Tectra embeds a C2PA Content Credentials manifest directly into the image file. This is the industry standard for content provenance, supported natively by Adobe Photoshop, Lightroom, and major media platforms.
The manifest includes: a c2pa.actions claim (content created), and a custom tectra.provenance assertion containing the SHA-256, pHash, signer public key, and organization name.
C2PA requires a valid CA-issued ES256 certificate. Self-signed certs will embed but may show a warning in validators. See setup instructions.
Verification Pipeline
When POST /api/v1/verify is called, four independent checks run:
Watermark Check
Extract the DWT+DCT watermark payload. Try exact match, then fuzzy Hamming ≤ 6 bits against all stored payloads.
SHA-256 Check
Hash the uploaded file. Check against both original and watermarked hashes in the database.
Perceptual Hash Check
Compute pHash. Pre-filter by hash prefix, then Hamming distance comparison. Catches re-compressed copies.
Blockchain Check
Look up content hash on-chain. If found in a batch, re-verify the Merkle proof against the anchored root.
The confidence score is passed_checks / total_checks. A score ≥ 40% returns authentic: true. See the Verification Guide for details.
Verification clients
You can verify images through the REST API, the Python or TypeScript SDKs, the tectra-verify open-source package (no account needed), or the Tectra browser extension for one-click verification on any webpage.
Next: Use Cases
See how different industries integrate Tectra.