Chain Verification¶
Provena maintains a SHA-256 hash chain across all audit records, creating a tamper-evident log. If any record is modified, deleted, or reordered after the fact, the chain breaks and verification fails. This gives you cryptographic proof that your governance trail has not been altered.
How the Hash Chain Works¶
Each record's chain hash is computed from four inputs:
- The previous record's chain hash (linking it to its predecessor)
- The content hash (SHA-256 of the raw context bytes)
- The source type string
- The timestamp in ISO format
The computation follows a Merkle-style pattern:
The very first record uses a deterministic genesis hash as its predecessor:
This anchors the chain to a known starting point.
Verifying Chain Integrity¶
Call trail.verify_chain() to recompute every chain hash from the genesis
forward and compare against stored values:
from provena import ContextTrail
trail = ContextTrail(backend="memory")
# Log some context
trail.log(content="First entry", source="retriever")
trail.log(content="Second entry", source="tool:api")
trail.log(content="Third entry", source="agent:planner")
# Verify the entire chain
verdict = trail.verify_chain()
print(verdict.intact) # True
print(verdict.total_records) # 3
print(verdict.broken_at) # None
print(verdict.details) # "Chain intact"
The ChainVerdict dataclass returned by verify_chain() contains:
| Field | Type | Description |
|---|---|---|
intact |
bool |
True if every link in the chain is valid |
total_records |
int |
Number of records that were checked |
broken_at |
int | None |
Record ID where the chain first broke, or None |
details |
str |
Human-readable summary of the verification |
HMAC Signing¶
For stronger integrity guarantees, enable HMAC-SHA256 signing. When a signing key is provided, each chain hash is computed using HMAC instead of plain SHA-256. This means an attacker cannot forge valid chain hashes without the key.
Using the signing_key parameter¶
trail = ContextTrail(
backend="memory",
signing_key="my-secret-governance-key",
)
trail.log(content="Signed entry", source="retriever")
verdict = trail.verify_chain()
print(verdict.intact) # True
print(trail.is_signed) # True
Using the PROVENA_SIGNING_KEY environment variable¶
import os
os.environ["PROVENA_SIGNING_KEY"] = "my-secret-governance-key"
trail = ContextTrail(backend="memory")
print(trail.is_signed) # True
The environment variable is checked when no signing_key parameter is passed.
The parameter takes precedence over the environment variable.
Key management
Store the signing key in a secrets manager or environment variable. Do not hard-code it in application source. Rotate keys by creating a new trail with the new key -- existing trails remain verifiable with their original key.
Tamper Detection Example¶
Here is what happens when stored data is modified outside of Provena:
from provena import ContextTrail
trail = ContextTrail(backend="memory")
trail.log(content="Original entry one", source="retriever")
trail.log(content="Original entry two", source="retriever")
trail.log(content="Original entry three", source="retriever")
# Verify before tampering
verdict = trail.verify_chain()
assert verdict.intact is True
# Simulate tampering: modify the content_hash of the second record
records = trail._backend._records # Access internal storage for demo
records[1]["content_hash"] = "tampered_hash_value"
# Verify after tampering
verdict = trail.verify_chain()
print(verdict.intact) # False
print(verdict.broken_at) # 2
print(verdict.details) # "Chain broken at record 2"
The chain breaks at the tampered record because the recomputed hash no longer matches the stored value. Every record after the tampered one would also fail if checked individually, since each depends on its predecessor.
CLI Verification¶
Verify a trail database from the command line:
# Verify with default database path
provena verify
# Verify a specific database
provena --db audit.db verify
# Verify with a signing key
provena --db audit.db --signing-key "my-secret-key" verify
# Using environment variables
export PROVENA_DB=audit.db
export PROVENA_SIGNING_KEY="my-secret-key"
provena verify
Output on success:
Output on failure:
The CLI exits with code 0 on success and code 1 on failure, making it suitable for CI/CD pipelines and automated compliance checks.
Performance¶
Chain verification is designed to be fast:
- Hashing: SHA-256 computation adds sub-millisecond overhead per record
- HMAC signing: HMAC-SHA256 adds negligible additional cost over plain SHA-256
- Verification: Full chain verification reads all records sequentially; for a trail with 10,000 records, expect verification to complete in under one second
- Logging: Each
trail.log()call performs one hash computation and one database write -- the chain does not require re-reading previous records because the previous hash is cached in memory
Verification in CI/CD
Add provena --db $TRAIL_DB verify as a step in your deployment pipeline.
This ensures that the governance trail has not been corrupted before
promoting a build. The non-zero exit code on failure integrates naturally
with CI systems.