# Tectra Provenance Protocol Specification

**Version:** 1.0  
**Status:** Stable  
**Date:** 2026-03-02  
**Authors:** Tectra Vision Team  

---

## Table of Contents

1. [Overview](#1-overview)
2. [Signing Pipeline](#2-signing-pipeline)
3. [Blockchain Anchoring](#3-blockchain-anchoring)
4. [Verification Pipeline](#4-verification-pipeline)
5. [Origin Type Taxonomy](#5-origin-type-taxonomy)
6. [Security Considerations](#6-security-considerations)
7. [Change Process](#7-change-process)

---

## 1. Overview

### 1.1 Purpose

The Tectra Provenance Protocol enables cryptographic proof of content origin. It binds an immutable identity to digital media at the point of creation, allowing any third party to independently verify when, by whom, and how a piece of content was produced.

### 1.2 Design Goals

| Goal | Description |
|------|-------------|
| **Multi-layer redundancy** | Four independent verification layers (watermark, SHA-256, perceptual hash, blockchain) ensure that no single point of failure can defeat provenance detection. |
| **Transformation resilience** | Provenance survives common media transformations — JPEG recompression, rescaling, moderate cropping, and format conversion — through the combination of perceptual hashing and steganographic watermarking. |
| **Open verification** | The verification side of the protocol is fully open-source (`tectra-verify`). Any party can verify content without access to signing keys or proprietary infrastructure. |
| **Standards alignment** | The protocol builds on established cryptographic primitives (SHA-256, Ed25519) and, where applicable, embeds C2PA Content Credentials for interoperability with the broader content authenticity ecosystem. |

### 1.3 Normative References

| Reference | Description |
|-----------|-------------|
| [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final) | Secure Hash Standard (SHA-256) |
| [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) | Edwards-Curve Digital Signature Algorithm (Ed25519) |
| [RFC 4648](https://www.rfc-editor.org/rfc/rfc4648) | Base encodings (hex encoding) |
| [C2PA Specification v2.x](https://c2pa.org/specifications/specifications/2.0/specs/C2PA_Specification.html) | Content Credentials |
| [IEEE 754](https://standards.ieee.org/standard/754-2019.html) | Floating-point arithmetic (timestamps) |

---

## 2. Signing Pipeline

The signing pipeline transforms raw content into a signed, watermarked, and blockchain-anchored asset. Each step is deterministic given the same inputs.

```
Raw Content
    │
    ├──▶ [2.1] Content Hashing (SHA-256 + pHash)
    │
    ├──▶ [2.2] Fingerprint Construction
    │
    ├──▶ [2.3] Digital Signature (Ed25519)
    │
    ├──▶ [2.4] Watermark Embedding (DWT+DCT)
    │
    ├──▶ [2.5] C2PA Manifest (optional)
    │
    └──▶ [3.x] Blockchain Anchoring
```

### 2.1 Content Hashing

Two hashes are computed on the **original content bytes before any watermark embedding**.

#### 2.1.1 SHA-256 (Cryptographic Hash)

- Algorithm: SHA-256 as defined in [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final).
- Input: raw content bytes (the file as read from disk or received over the network).
- Output: 256-bit digest, represented as a 64-character lowercase hexadecimal string.

```
sha256 = SHA-256(raw_bytes)          # 32 bytes
sha256_hex = hex(sha256)             # "a3f2…" (64 chars)
```

#### 2.1.2 Perceptual Hash (pHash)

- Algorithm: DCT-based perceptual hash as implemented by the `imagehash` library (`imagehash.phash()`).
- Process:
  1. Decode the image.
  2. Resize to 32×32 and convert to grayscale.
  3. Apply a 32×32 DCT.
  4. Retain the top-left 8×8 DCT coefficients (excluding DC).
  5. Compute the median of the 64 coefficients.
  6. Threshold each coefficient against the median to produce a 64-bit hash.
- Output: 64-bit value represented as a 16-character lowercase hexadecimal string.

```
phash = pHash(decode(raw_bytes))     # 8 bytes / 64 bits
phash_hex = hex(phash)               # "d4e1…" (16 chars)
```

Both hashes MUST be computed on the original, unmodified content. Computing them after watermark embedding would create a circular dependency that prevents verification of the original.

### 2.2 Fingerprint Construction

The fingerprint is a canonical string that binds the content hashes to their provenance metadata.

#### 2.2.1 Format

```
"{sha256}|{phash}|{unix_timestamp}|{user_id}|{origin_code}"
```

| Field | Type | Description |
|-------|------|-------------|
| `sha256` | string | 64-char hex SHA-256 digest from §2.1.1 |
| `phash` | string | 16-char hex perceptual hash from §2.1.2 |
| `unix_timestamp` | integer | Signing time as a Unix epoch timestamp (seconds since 1970-01-01T00:00:00Z, UTC) |
| `user_id` | string | Unique identifier of the signing user/organization |
| `origin_code` | string | Origin type code from the taxonomy defined in §5 |

#### 2.2.2 Encoding

The fingerprint string is encoded as **UTF-8 bytes** before signing. No BOM. No trailing newline.

```
fingerprint_str = f"{sha256}|{phash}|{ts}|{uid}|{origin}"
fingerprint_bytes = fingerprint_str.encode("utf-8")
```

#### 2.2.3 Example

```
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9|d4e1a2b3c5f60718|1709312400|user_abc123|AI_FULL"
```

### 2.3 Digital Signature

#### 2.3.1 Algorithm

- **Ed25519** as specified in [RFC 8032, §5.1](https://www.rfc-editor.org/rfc/rfc8032#section-5.1).
- Signing is deterministic — the same key and message always produce the same signature (no per-signature randomness required).

#### 2.3.2 Input

- The `fingerprint_bytes` produced in §2.2.2.

#### 2.3.3 Output

- A 64-byte (512-bit) Ed25519 signature.

#### 2.3.4 Key Format

| Key | Size | Encoding |
|-----|------|----------|
| Private key (seed) | 32 bytes | Hex-encoded (64 characters) |
| Public key | 32 bytes | Hex-encoded (64 characters) |

#### 2.3.5 Private Key Encryption at Rest

Private keys MUST be encrypted when stored on disk or in a database.

- Encryption scheme: **Fernet** (as defined by the `cryptography` Python library).
- Underlying primitives: AES-128-CBC for encryption + HMAC-SHA256 for authentication.
- The Fernet key is derived from a passphrase or retrieved from a secrets manager; its management is outside the scope of this protocol.
- Decryption yields the raw 32-byte Ed25519 seed, which is used to reconstruct the signing key.

#### 2.3.6 Signature Computation

```
from nacl.signing import SigningKey

seed = bytes.fromhex(private_key_hex)       # 32 bytes
signing_key = SigningKey(seed)
signature = signing_key.sign(fingerprint_bytes).signature  # 64 bytes
```

### 2.4 Watermark Embedding

The watermark provides a verification layer that is bound to the image pixel data itself, surviving transformations that alter file-level metadata and change the cryptographic hash.

#### 2.4.1 Payload Derivation

```
payload = SHA-256(signature)[:16]            # First 16 bytes (128 bits)
```

- Input: the 64-byte Ed25519 signature from §2.3.
- Process: compute SHA-256 of the raw signature bytes.
- Output: truncate to the first 16 bytes (128 bits). This is the watermark payload.

The payload is a one-way derivative of the signature. An attacker cannot reconstruct the signature from the payload, but a verifier can confirm a match by recomputing `SHA-256(signature)[:16]` and comparing.

#### 2.4.2 Embedding Method

- **Domain:** Discrete Wavelet Transform (DWT) + Discrete Cosine Transform (DCT) steganography.
- **Library:** `imwatermark` (`WatermarkEncoder` with `method="dwtDct"`).
- **Process:**
  1. Convert the image to BGR numpy array.
  2. Apply DWT to decompose the image into wavelet sub-bands.
  3. Apply DCT to mid-frequency wavelet coefficients.
  4. Embed the 128-bit payload by modulating selected DCT coefficients.
  5. Apply inverse DCT and inverse DWT to reconstruct the watermarked image.

#### 2.4.3 Imperceptibility

The embedding modifies mid-frequency coefficients that are below the threshold of human visual perception. The watermarked image MUST be visually indistinguishable from the original under normal viewing conditions.

#### 2.4.4 Robustness

The watermark is designed to survive the following transformations:

| Transformation | Survival Condition |
|----------------|-------------------|
| JPEG compression | Quality factor ≥ 60 |
| Resolution scaling | Down to 50% of original dimensions |
| Moderate cropping | Up to ~25% area removal |
| Format conversion | PNG ↔ JPEG ↔ WebP ↔ TIFF |
| Color space conversion | RGB ↔ sRGB |

Robustness degrades gracefully. Partial payload recovery is accounted for by fuzzy matching in the verification pipeline (§4.1).

### 2.5 C2PA Manifest (Optional)

When a CA-issued ES256 (ECDSA P-256) certificate is configured for the signing entity, the protocol embeds a [C2PA Content Credentials](https://c2pa.org/specifications/specifications/2.0/specs/C2PA_Specification.html) manifest into the output asset.

#### 2.5.1 Activation Condition

C2PA manifest embedding is performed **only when** a valid X.509 certificate chain is available with:
- Leaf certificate key type: EC P-256 (ES256 / `secp256r1`)
- Extended Key Usage: `id-kp-documentSigning` (OID 1.3.6.1.5.5.7.3.36) or equivalent C2PA EKU
- Valid (not expired, not revoked) at signing time

#### 2.5.2 Manifest Structure

The manifest contains two assertions:

**Assertion 1: `c2pa.actions`**

```json
{
  "label": "c2pa.actions",
  "data": {
    "actions": [
      {
        "action": "c2pa.created",
        "softwareAgent": "Tectra/1.0",
        "when": "<ISO 8601 timestamp>"
      }
    ]
  }
}
```

**Assertion 2: `tectra.provenance`**

```json
{
  "label": "tectra.provenance",
  "data": {
    "sha256": "<content SHA-256 hex>",
    "phash": "<perceptual hash hex>",
    "signerPublicKey": "<Ed25519 public key hex>",
    "organization": "<signing organization name>",
    "originCode": "<origin type code>"
  }
}
```

#### 2.5.3 Signature

The C2PA manifest is signed using ES256 with the CA-issued certificate (separate from the Ed25519 provenance signature). This provides a PKI-backed trust chain in addition to the Tectra provenance signature.

---

## 3. Blockchain Anchoring

Blockchain anchoring provides a tamper-evident, publicly auditable record of content existence. Content hashes are batched into Merkle trees and the tree roots are committed on-chain.

### 3.1 Merkle Batch Accumulation

- Content hashes (SHA-256 hex strings from §2.1.1) are queued in a **Redis list**, keyed per user.
- The queue is flushed on a **10-second interval** by a background worker.
- If the queue is empty at flush time, no action is taken.
- Each flush produces exactly one Merkle tree and one on-chain transaction (if leaves exist).

### 3.2 Merkle Tree Construction

#### 3.2.1 Leaf Nodes

Each leaf is the SHA-256 hash of a content item, represented as raw 32-byte digest.

```
leaf = bytes.fromhex(content_sha256_hex)     # 32 bytes
```

#### 3.2.2 Sorting

Before tree construction, leaves are **sorted lexicographically by their hex-encoded hash**. This ensures deterministic tree construction regardless of insertion order.

#### 3.2.3 Odd Leaf Handling

If the number of leaves is odd, the **last leaf is duplicated** to make the count even. This applies recursively at each tree level.

#### 3.2.4 Internal Nodes

Internal nodes are computed as:

```
internal_node = SHA-256(left_child || right_child)
```

Where `||` denotes byte concatenation of the raw 32-byte child digests. The result is a 32-byte digest.

#### 3.2.5 Construction Algorithm

```
function buildTree(leaves):
    sort(leaves)
    level = leaves
    while len(level) > 1:
        if len(level) is odd:
            level.append(level[-1])        # duplicate last
        next_level = []
        for i in range(0, len(level), 2):
            parent = SHA-256(level[i] || level[i+1])
            next_level.append(parent)
        level = next_level
    return level[0]                         # Merkle root
```

### 3.3 On-Chain Registration

#### 3.3.1 Blockchain

- **Network:** Polygon PoS (EVM-compatible).
- **Contract:** `ProvenanceRegistry` (deployed address published at `tectra.vision/contract`).

#### 3.3.2 Registration Function

```solidity
function registerBatch(
    bytes32 merkleRoot,
    uint32  leafCount,
    bytes32 signerPubKey,
    uint64  timestamp
) external;
```

| Parameter | Description |
|-----------|-------------|
| `merkleRoot` | Root hash of the Merkle tree from §3.2 |
| `leafCount` | Number of original leaves (before duplication) |
| `signerPubKey` | Ed25519 public key of the signing entity (first 32 bytes) |
| `timestamp` | Unix epoch timestamp of the batch |

#### 3.3.3 Events

The contract emits:

```solidity
event BatchRegistered(
    bytes32 indexed merkleRoot,
    uint32  leafCount,
    bytes32 signerPubKey,
    uint64  timestamp,
    address indexed submitter
);
```

### 3.4 Merkle Proof Format

Each content item receives a Merkle inclusion proof that allows independent verification against the on-chain root.

#### 3.4.1 Structure

A proof is an ordered JSON array of sibling nodes from the leaf to the root:

```json
[
  {"hash": "abc123...", "position": "left"},
  {"hash": "def456...", "position": "right"}
]
```

| Field | Type | Description |
|-------|------|-------------|
| `hash` | string | Hex-encoded SHA-256 hash of the sibling node |
| `position` | string | `"left"` or `"right"` — the position of the **sibling** relative to the current node |

#### 3.4.2 Verification Algorithm

```
function verifyProof(leafHash, proof, expectedRoot):
    current = bytes.fromhex(leafHash)
    for step in proof:
        sibling = bytes.fromhex(step.hash)
        if step.position == "left":
            current = SHA-256(sibling || current)
        else:
            current = SHA-256(current || sibling)
    return current.hex() == expectedRoot
```

When `position` is `"left"`, the sibling is on the left side of the concatenation (i.e., the sibling was the left child and the current node was the right child at that tree level). When `"right"`, the sibling is on the right.

---

## 4. Verification Pipeline

Verification is designed to be performed by any party using only public information. The `tectra-verify` library implements the client-side portion; the Tectra API provides database lookups and blockchain queries.

### 4.1 Watermark Check

#### 4.1.1 Extraction

1. Decode the image to a BGR numpy array.
2. Instantiate a DWT+DCT watermark decoder for 128-bit payloads (the verification library uses a 256-bit extraction window to account for implementation variance).
3. Extract the raw payload bytes.

#### 4.1.2 Exact Match

Compare the extracted payload byte-for-byte against stored watermark payloads:

```
extracted == stored_payload
```

#### 4.1.3 Fuzzy Match

If exact match fails, compute the bitwise Hamming distance:

```
distance = popcount(extracted XOR stored_payload)
```

A fuzzy match is declared if:

```
distance ≤ 6 bits (out of 128)
```

This threshold accounts for minor degradation introduced by lossy transformations while maintaining a negligible false-positive rate (probability of random collision ≈ 2⁻⁹⁵).

### 4.2 SHA-256 Check

Compare the SHA-256 digest of the submitted image against **both**:

1. The stored **original content hash** (pre-watermark).
2. The stored **watermarked content hash** (post-watermark).

A match against either constitutes a pass.

### 4.3 Perceptual Hash Check

#### 4.3.1 Prefix Bucketing

For efficient lookup, perceptual hashes are indexed by their **first 4 hexadecimal characters** (16 bits), creating **65,536 buckets**. This reduces the search space before performing full comparison.

#### 4.3.2 Full Comparison

Within the candidate bucket, compute the Hamming distance between the query pHash and each stored pHash:

```
distance = popcount(query_phash XOR stored_phash)
```

#### 4.3.3 Match Threshold

```
distance ≤ 10 bits (out of 64)
```

A distance of 0 indicates an identical image. Distances of 1–10 indicate visually similar images that likely share the same origin. Distances above 10 are rejected.

### 4.4 Blockchain Check

#### 4.4.1 Direct Lookup

Query the `ProvenanceRegistry` contract by the content's SHA-256 hash to find a batch containing that hash.

#### 4.4.2 Merkle Proof Verification

If a direct index is unavailable, the verifier uses the stored Merkle proof:

1. Retrieve the Merkle proof and batch root for the content item.
2. Execute the verification algorithm from §3.4.2.
3. Confirm the batch root matches the on-chain `merkleRoot` from the `BatchRegistered` event.

#### 4.4.3 Perceptual Hash Fallback

If SHA-256 lookup fails (e.g., the image has been re-encoded), fall back to querying by perceptual hash using the bucketed index described in §4.3.1.

### 4.5 Confidence Scoring

Verification produces a confidence score derived from five independent checks:

| # | Check | Section |
|---|-------|---------|
| 1 | Watermark extraction + match | §4.1 |
| 2 | SHA-256 exact match | §4.2 |
| 3 | Perceptual hash match | §4.3 |
| 4 | Blockchain proof verification | §4.4 |
| 5 | Digital signature validation | §2.3 |

#### 4.5.1 Score Computation

```
confidence = passed_checks / total_checks
```

Where `total_checks` is the number of checks that were **attempted** (some checks may be skipped if prerequisites are missing, e.g., no watermark library installed).

#### 4.5.2 Authenticity Threshold

```
verified = (confidence ≥ 0.4)
```

This requires **at least 2 out of 5 checks to pass**. The threshold is intentionally set below majority to accommodate scenarios where some layers are unavailable (e.g., watermark stripped, blockchain node unreachable).

#### 4.5.3 Result Structure

```json
{
  "verified": true,
  "confidence": 0.8,
  "checks": {
    "watermark": {"passed": true, "distance": 2},
    "sha256": {"passed": true, "match_type": "watermarked"},
    "phash": {"passed": true, "distance": 3},
    "blockchain": {"passed": true, "batch_root": "0xabc..."},
    "signature": {"passed": false, "reason": "no_signature_data"}
  },
  "content_id": "...",
  "origin_code": "AI_FULL",
  "signer": "..."
}
```

---

## 5. Origin Type Taxonomy

Every signed content item carries an origin code classifying how the content was produced. Signers MUST select the most specific applicable code.

| Code | Category | Description |
|------|----------|-------------|
| `CAMERA` | Camera Capture | Directly captured by a physical camera sensor with no substantive post-processing beyond standard ISP pipeline (demosaicing, white balance, tone mapping). |
| `EDIT` | Human Edit | Created or substantially modified by a human using manual editing tools (e.g., Photoshop, Lightroom) without AI-generative features. |
| `AI_FULL` | AI Generated | Entirely generated by an AI model (e.g., text-to-image diffusion, GAN synthesis) with no photographic source material. |
| `AI_EDIT` | AI-Assisted Edit | A human-directed edit that uses AI-powered tools (e.g., generative fill, AI upscaling, style transfer) applied to existing source material. |
| `COMPOSITE` | Composite | Assembled from multiple source images or media elements, regardless of whether AI tools were used in the compositing process. |
| `SCAN` | Scan / Digitization | Digitized from a physical medium — printed photograph, document, painting, film negative, etc. |
| `SCREEN` | Screen Capture | Captured from a digital display (screenshot, screen recording frame). |

### 5.1 Selection Rules

1. If AI generation produced >50% of the pixel content, use `AI_FULL`.
2. If AI tools modified an existing image, use `AI_EDIT`.
3. If multiple images are combined, use `COMPOSITE` regardless of whether AI was involved in blending.
4. When ambiguous between `EDIT` and `AI_EDIT`, prefer `AI_EDIT` (disclose more, not less).

---

## 6. Security Considerations

### 6.1 Ed25519 Key Compromise

If an Ed25519 private key is compromised, the impact is limited to content signed with that specific key. Other signers' content is unaffected. Mitigation:

- Rotate keys immediately upon suspected compromise.
- The application layer supports key revocation lists; revoked keys are flagged during verification.
- Blockchain records created by a compromised key cannot be deleted but can be annotated with a revocation notice.

### 6.2 Watermark Removal / Tampering

Adversarial watermark removal (e.g., through diffusion-based regeneration or aggressive compression) degrades but does not eliminate provenance detection. The remaining three verification layers — SHA-256, perceptual hash, and blockchain anchoring — continue to function independently.

### 6.3 SHA-256 Collision Resistance

Finding a second preimage for SHA-256 requires approximately 2¹²⁸ operations, which is computationally infeasible with current and foreseeable technology (per [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final)).

### 6.4 Perceptual Hash Collisions

Perceptual hash collisions are **expected and intentional** for visually similar content — this is the mechanism by which transformed versions of the same image are matched. However, distinct images with coincidentally similar low-frequency structure may produce false positives. Mitigation:

- Perceptual hash match alone is insufficient for verification (requires ≥2 checks).
- The 10-bit Hamming distance threshold limits false positives to images with very high visual similarity.

### 6.5 Blockchain Immutability

Once a Merkle root is committed to the Polygon network, the record cannot be altered or deleted. This is by design: it prevents retroactive falsification of provenance records. Consequences:

- Erroneous registrations persist on-chain. Corrections are handled at the application layer by publishing updated records and marking prior records as superseded.
- Revocation of a signer's key does not remove their historical blockchain entries. Verification clients MUST check revocation status in addition to blockchain proof validity.

### 6.6 Replay and Substitution Attacks

The fingerprint (§2.2) binds the content hash to a specific user, timestamp, and origin code. An attacker cannot:

- **Replay** a valid signature on different content (the SHA-256 hash would not match).
- **Substitute** a different origin code (it is covered by the signature).
- **Backdate** content (the timestamp is signed, and blockchain anchoring provides an independent lower bound on registration time).

### 6.7 Watermark Oracle Attacks

An attacker with access to the verification API could iteratively modify an image and observe whether the watermark is still detected, eventually removing it. Mitigation:

- Rate limiting on the verification API.
- The watermark is one of four layers; its removal alone does not defeat provenance.

---

## 7. Change Process

### 7.1 Versioning Scheme

This protocol follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) adapted for protocol specifications:

| Version Component | Increment When |
|-------------------|----------------|
| **Major** (e.g., 1.x → 2.0) | Breaking changes that alter fingerprint format, hash algorithms, signature scheme, proof format, or verification semantics. Existing proofs/signatures MAY NOT verify under the new version. |
| **Minor** (e.g., 1.0 → 1.1) | Backward-compatible extensions: new optional fields, additional origin codes, new optional verification checks. Existing proofs/signatures MUST still verify. |
| **Patch** (e.g., 1.0.0 → 1.0.1) | Clarifications, typo fixes, and non-normative editorial changes. No functional impact. |

### 7.2 Breaking Change Policy

- Breaking changes MUST increment the major version number.
- A migration path MUST be documented for each breaking change.
- Implementations SHOULD support verification of content signed under the previous major version for a minimum of 24 months after a new major version is published.

### 7.3 Extension Mechanism

Minor version extensions are permitted under the following constraints:

- New fields added to the fingerprint format (§2.2) MUST be appended after existing fields and MUST NOT change the semantics of existing fields.
- New verification checks (§4.5) MUST NOT reduce the confidence score of content that previously verified successfully.
- New origin codes (§5) MAY be added without a version bump, but MUST be documented in the next minor release.

### 7.4 Governance

Protocol changes are proposed via pull request to the specification repository. Changes require:

- A written rationale describing the motivation and impact.
- At least one reference implementation demonstrating the change.
- Review and approval from at least two core maintainers.

---

*End of Tectra Provenance Protocol Specification v1.0*
