AID-H-002.007: External Media & Document Provenance Verification

Version 1.20261015 · Actionable sub-technique

Verify provenance metadata for external images, video, PDFs, and documents before they are ingested into multimodal RAG, OCR, evidence-processing, OSINT, or agent workflows. This sub-technique focuses on inbound provenance verification, not outbound watermarking.

Coverage includes:Critical processing order: when this control is paired with multimodal sanitization, provenance extraction and verification must occur before any strip-all-metadata or media-rebuild step. Verify and preserve the C2PA / Content Credentials verdict first, then allow downstream sanitization to strip non-provenance metadata or rebuild media bytes as required.

Pillars: data, app

Lifecycle: building, operation

Scope boundary

Owns inbound cryptographic provenance verification for externally produced media and documents before OCR, chunking, embedding, or sanitization, and propagates the verified, unsigned, invalid, or stripped state into downstream metadata.

AID-H-002.003: AID-H-002.007 verifies source attestation before destructive normalization; AID-H-002.003 strips unsafe metadata, decodes, rebuilds, and admits the artifact. The provenance verdict must be preserved before sanitization changes the original bytes or metadata.

AID-D-001.002: AID-H-002.007 verifies an external publisher or capture-device attestation chain; AID-D-001.002 uses signal-level forensics to detect synthetic or manipulated media. Cryptographic provenance and forensic authenticity are independent verdicts and either may be absent or contradictory.

AID-M-002.002: AID-H-002.007 verifies third-party media provenance at inbound use; AID-M-002.002 verifies whole-artifact integrity against organization-governed digests and signatures. External publisher trust and internal artifact-lifecycle trust use different authorities and populations.

AID-H-020.001: AID-H-002.007 establishes the source-media provenance verdict before OCR and chunking; AID-H-020.001 protects the integrity of the resulting chunks from ingestion through retrieval. Source provenance precedes chunk integrity and neither substitutes for the other.

Implementation guidance

AID-H-002.007-G001

Verify C2PA or equivalent provenance metadata at ingestion time and classify each asset as verified, unsigned, invalid, or stripped before it enters OCR, chunking, or embedding pipelines.
Concept:

Verify C2PA / Content Credentials before metadata stripping, media rebuild, OCR, chunking, or transcoding. The verifier must use the supported c2pa-python object lifecycle, a pinned trust-anchor configuration, and the manifest validation state rather than guessing fields in the raw JSON.

Step 1: Verify at the file edge with the supported c2pa-python API
# File: provenance/verify_asset.py
from __future__ import annotations

import json
from dataclasses import dataclass
from enum import Enum
from hashlib import sha256
from pathlib import Path

from c2pa import C2paError, Context, Reader, Settings

TRUST_ANCHORS_PATH = Path("/etc/c2pa/trust_anchors.pem")


class ProvenanceStatus(str, Enum):
    VERIFIED = "verified"
    UNSIGNED = "unsigned"
    INVALID = "invalid"
    STRIPPED = "stripped"


@dataclass(frozen=True)
class ProvenanceVerdict:
    asset_sha256: str
    status: ProvenanceStatus
    issuer: str | None
    assertion_summary: list[str]
    validation_errors: list[str]


def sha256_file(path: str | Path) -> str:
    digest = sha256()
    with Path(path).open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def build_c2pa_context() -> Context:
    # The trust-anchor file is mounted read-only from the reviewed runtime image.
    anchors = TRUST_ANCHORS_PATH.read_text(encoding="utf-8")
    settings = Settings.from_dict({
        "verify": {"verify_cert_anchors": True},
        "trust": {"trust_anchors": anchors},
    })
    return Context(settings=settings)


def _manifest_metadata(reader: Reader) -> tuple[str | None, list[str]]:
    manifest = reader.get_active_manifest() or {}
    claim_info = manifest.get("claim_generator_info") or []
    issuer = None
    if isinstance(claim_info, list) and claim_info and isinstance(claim_info[0], dict):
        issuer = claim_info[0].get("name")
    issuer = issuer or manifest.get("claim_generator")
    assertions = [
        item.get("label")
        for item in manifest.get("assertions", [])
        if isinstance(item, dict) and item.get("label")
    ]
    return issuer, assertions


def verify_asset(asset_path: str, *, expected_provenance: bool) -> ProvenanceVerdict:
    asset_sha256 = sha256_file(asset_path)
    try:
        with build_c2pa_context() as context:
            reader = Reader.try_create(asset_path, context=context)
            if reader is None:
                status = (
                    ProvenanceStatus.STRIPPED
                    if expected_provenance
                    else ProvenanceStatus.UNSIGNED
                )
                return ProvenanceVerdict(
                    asset_sha256=asset_sha256,
                    status=status,
                    issuer=None,
                    assertion_summary=[],
                    validation_errors=[],
                )

            with reader:
                validation_state = (reader.get_validation_state() or "").lower()
                issuer, assertions = _manifest_metadata(reader)
                if validation_state != "valid" or not issuer:
                    results = reader.get_validation_results() or {}
                    return ProvenanceVerdict(
                        asset_sha256=asset_sha256,
                        status=ProvenanceStatus.INVALID,
                        issuer=issuer,
                        assertion_summary=assertions,
                        validation_errors=[json.dumps(results, sort_keys=True)],
                    )
                return ProvenanceVerdict(
                    asset_sha256=asset_sha256,
                    status=ProvenanceStatus.VERIFIED,
                    issuer=issuer,
                    assertion_summary=assertions,
                    validation_errors=[],
                )
    except (C2paError, OSError, ValueError) as exc:
        return ProvenanceVerdict(
            asset_sha256=asset_sha256,
            status=ProvenanceStatus.INVALID,
            issuer=None,
            assertion_summary=[],
            validation_errors=[str(exc)],
        )
Step 2: Admit or downgrade at ingestion

Use the verdict before OCR or embedding. Workflows that require authenticated media must fail closed on invalid or stripped. Lower-assurance workflows may admit unsigned assets but must preserve that trust state.

# File: provenance/intake_gate.py
from provenance.verify_asset import ProvenanceStatus, verify_asset

PROVENANCE_REQUIRED_WORKFLOWS = {
    "evidence_review",
    "newsroom_intake",
    "regulated_kyc",
}


class AssetRejected(Exception):
    pass


def admit_media_asset(asset_path: str, *, workflow: str) -> dict:
    verdict = verify_asset(
        asset_path,
        expected_provenance=workflow in PROVENANCE_REQUIRED_WORKFLOWS,
    )
    if verdict.status in {ProvenanceStatus.INVALID, ProvenanceStatus.STRIPPED}:
        raise AssetRejected(f"asset rejected with provenance status={verdict.status}")

    return {
        "asset_sha256": verdict.asset_sha256,
        "provenance_status": verdict.status.value,
        "provenance_issuer": verdict.issuer,
        "provenance_assertions": verdict.assertion_summary,
    }
Step 3: Preserve the verdict, not the full manifest

Once the verdict is recorded, downstream sanitization may strip non-essential metadata or rebuild the media bytes as required by AID-H-002.003. Do not reverse the order, or provenance evidence will be destroyed before verification completes.

Action: Make provenance status part of the input admission contract. Verify first, classify every asset as verified, unsigned, invalid, or stripped, and use that verdict to block or downgrade trust before OCR, chunking, or embedding begins.

AID-H-002.007-G002

Propagate provenance state into downstream chunk, embedding, and retrieval metadata and require step-up review when high-impact decisions rely on unsigned or invalid media.
Production implementation: provenance-preserving derivation

Derive chunk and embedding metadata only from a verified source record. Never upgrade unsigned, invalid, or stripped to verified during parsing or retrieval.

# File: provenance/derive_records.py
from __future__ import annotations
import hashlib
import json

ALLOWED_STATES = {"verified", "unsigned", "invalid", "stripped"}

def derive_chunk(source: dict, *, page: int, start: int, end: int, text: str, embedding_model_sha256: str) -> dict:
    required = {"asset_id", "source_media_sha256", "provenance_state", "provenance_receipt_sha256", "policy_version"}
    if set(source) != required or source["provenance_state"] not in ALLOWED_STATES:
        raise ValueError("source provenance record differs")
    for field in ("source_media_sha256", "provenance_receipt_sha256", "embedding_model_sha256"):
        value = embedding_model_sha256 if field == "embedding_model_sha256" else source[field]
        if len(value) != 64 or any(c not in "0123456789abcdef" for c in value):
            raise ValueError(field + " is not lower-case SHA-256")
    if page < 1 or start < 0 or end <= start or not text:
        raise ValueError("chunk boundary is invalid")
    text_sha256 = hashlib.sha256(text.encode("utf-8")).hexdigest()
    identity = json.dumps(
        {"source_media_sha256": source["source_media_sha256"], "page": page, "start": start, "end": end, "text_sha256": text_sha256},
        sort_keys=True, separators=(",", ":"),
    ).encode()
    return {
        "chunk_id": hashlib.sha256(identity).hexdigest(),
        "asset_id": source["asset_id"],
        "source_media_sha256": source["source_media_sha256"],
        "text_sha256": text_sha256,
        "page": page,
        "start": start,
        "end": end,
        "provenance_state": source["provenance_state"],
        "provenance_receipt_sha256": source["provenance_receipt_sha256"],
        "embedding_model_sha256": embedding_model_sha256,
        "policy_version": source["policy_version"],
    }

Action: Reconcile every produced page/chunk/embedding to the signed derivation manifest. The retrieval or decision service requires fresh step-up approval when policy classifies the propagated state and intended use as consequential; missing lineage denies use rather than defaulting to unsigned.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-002.007: External Media & Document Provenance Verification | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version