How must an instance verify provenance without the original data, only hashes?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session must verify that a piece of data has a specific provenance (was derived from a specific input, by a specific process), but the original input data is unavailable—only hashes of inputs, outputs, and transformations are available. Does not apply to sessions where original data is available, or where provenance is not required.
Core Distinction
The separation between **hash-only verification** (using cryptographic hashes to prove relationships between unavailable data) and **data-inclusive verification** (re-checking provenance with the actual data present). Relying on hash-only verification creates the **Hash Chain Assumption**: the session assumes that if hashes match, the data must match, but without the original data, hashes only prove data equality (same hash = same data), not that the claimed transformation was performed correctly.
What Was Observed
In hash-only provenance architectures, failing to account for hash-only limitations produces three primary failure modes:
- **Transformation Fabrication:** The session claims input X was transformed to output Y via process P. Without the original X or Y, an attacker can fabricate a "valid-looking" transformation if they can generate the same output hash from a different input.
- **Input Substitution:** The session claims provenance from input A, but actually used input A' (with the same hash prefix as A, or with a hash that the fabricator can produce). Without the data, the receiver cannot distinguish A from A'.
- **Process Misattribution:** The session claims transformation T was performed, but actually used a different process T'. If both T and T' can produce the same output from the given input, the hashes alone cannot distinguish them.
Recorded Elements
- **`provenance_hash_chain_receipt`:** A chain of (input_hash, process_hash, output_hash) triples, each signed by the process that produced them, forming a cryptographic record of the claimed provenance.
- **`hash_equivalence_attestation_signature`:** When verification is hash-only, a cryptographic attestation from the process owner that "for this input hash and process, the output hash is uniquely determined" (i.e., the process is deterministic and the input is sufficiently specified).
- **`hash_collision_resistance_receipt`:** The cryptographic proof that the hash function used has sufficient collision resistance for the claimed security level, so hash equality is a reliable proxy for data equality.
- **`provenance_attestation_signature`:** For critical provenance claims, an attestation from a trusted third party that independently verified the provenance using either the original data or an equivalent deterministic verification.
Condition of Validity
* **Applies when:** Original data is unavailable, provenance must be verified, and hash-based evidence is the only available proof. * **Does not apply when:** Original data is available and can be directly verified, or provenance is not required.
Decision Boundary
- **Is the original data available?** If yes, verify directly. If no, proceed to hash-only verification.
- **Is a `provenance_hash_chain_receipt` present?** If yes, the claimed chain exists. If no, provenance cannot be verified.
- **Is a `hash_equivalence_attestation_signature` present?** If yes, the hashes uniquely determine the data given the process. If no, hash equality is necessary but not sufficient.
- **Conversely, when can a session accept a hash chain as sufficient?** Only when the hash function is verified collision-resistant AND the process owner has attested determinism AND the process is verified to produce unique output given the input hash.
Operational Effect
- **Transformation Integrity:** Attestations prevent fabricated transformations.
- **Hash Reliability:** Collision resistance is explicit, not assumed.
- **Verification Boundaries:** The session knows when hash-only verification is sufficient vs. when data-inclusive verification is needed.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class HashProvenanceVerdict(Enum):
DATA_AVAILABLE = auto()
HASH_VERIFICATION_SUFFICIENT = auto()
HASH_VERIFICATION_LIMITED = auto()
INSUFFICIENT_HASH_EVIDENCE = auto()
@dataclass(frozen=True)
class HashProvenanceLink:
input_hash: str
process_hash: str
output_hash: str
determinism_attested: bool
process_owner_signature: str | None
@dataclass(frozen=True)
class ProvenanceClaim:
claim_id: str
data_available: bool
links: tuple[HashProvenanceLink, ...]
hash_collision_resistance_bits: int
@dataclass(frozen=True)
class HashPolicy:
min_collision_resistance_bits: int
require_determinism_attestation: bool
@dataclass(frozen=True)
class HashProvenanceManifest:
verdict: HashProvenanceVerdict
directive: str
manifest_digest: str
class HashOnlyProvenanceGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_provenance(
cls, claim: ProvenanceClaim, policy: HashPolicy
) -> HashProvenanceManifest:
# Gate 1: Data available
if claim.data_available:
digest = cls._hash(json.dumps({
"claim": claim.claim_id,
"type": "DATA_AVAILABLE"
}, sort_keys=True))
return HashProvenanceManifest(
verdict=HashProvenanceVerdict.DATA_AVAILABLE,
directive=f"PROCEED: Data available for claim {claim.claim_id}, verify directly.",
manifest_digest=digest
)
# Gate 2: Hash chain presence
if not claim.links:
return HashProvenanceManifest(
verdict=HashProvenanceVerdict.INSUFFICIENT_HASH_EVIDENCE,
directive=f"HALT: No hash chain for claim {claim.claim_id}.",
manifest_digest=cls._hash(f"NO_CHAIN:{claim.claim_id}")
)
# Gate 3: Collision resistance
if claim.hash_collision_resistance_bits < policy.min_collision_resistance_bits:
return HashProvenanceManifest(
verdict=HashProvenanceVerdict.HASH_VERIFICATION_LIMITED,
directive=f"HALT: Hash collision resistance {claim.hash_collision_resistance_bits} < {policy.min_collision_resistance_bits}.",
manifest_digest=cls._hash(f"WEAK_HASH:{claim.claim_id}")
)
# Gate 4: Determinism attestation
if policy.require_determinism_attestation:
unattested = [l for l in claim.links if not l.determinism_attested or not l.process_owner_signature]
if unattested:
return HashProvenanceManifest(
verdict=HashProvenanceVerdict.HASH_VERIFICATION_LIMITED,
directive=f"HALT: {len(unattested)} links lack determinism attestation.",
manifest_digest=cls._hash(f"NO_ATTEST:{len(unattested)}")
)
# Gate 5: Sufficient
digest = cls._hash(json.dumps({
"claim": claim.claim_id,
"chain_length": str(len(claim.links)),
"collision_resistance": str(claim.hash_collision_resistance_bits),
"links": tuple(
{"input": l.input_hash, "process": l.process_hash, "output": l.output_hash}
for l in claim.links
)
}, sort_keys=True))
return HashProvenanceManifest(
verdict=HashProvenanceVerdict.HASH_VERIFICATION_SUFFICIENT,
directive=f"PROCEED: {len(claim.links)}-link hash chain verified with determinism attestation.",
manifest_digest=digest
)