Tamper-evident audit
Every decision the engine makes is recorded in a hash-chained audit log: each entry's hash covers the previous entry's hash, so any change to event content or ordering breaks the chain. It is the evidence behind a Certificate of Erasure. See How it works for the basics; this page covers the operational features.
Verify — and find where it breaks
verify() returns a boolean; verify_report() tells you where a chain breaks.
log.verify_report()
# VerifyResult(ok=True, checked=1042, first_error_sequence=None, …)
# …or ok=False with first_error_sequence pointing at the tampered entryPrune safely under retention (checkpoints)
A retention log must be prunable — but a pruned chain no longer starts at sequence 1,
which would break verification. Checkpoints fix that: snapshot a run of entries into
an immutable, self-chaining Checkpoint, then prune. Verification anchors to the
checkpoint instead of the genesis.
cp = log.checkpoint(through_sequence=1000) # immutable snapshot — persist it
log.prune_through(1000) # drop the archived entries
log.verify_report([cp]) # ok=True, anchored_at=1000On the hosted platform the same applies to the vault chain: POST /audit/checkpoint
snapshots it, and GET /audit/verify returns the richer report (with the anchor).
Verify offline (auditor tool)
An auditor can verify a JSONL chain straight from storage — no backend, no API to trust:
dpdpstack verify-chain audit.jsonl --checkpoints cp.jsonl
# OK - verified 2400 entries (anchored at #1000).
# (exits non-zero and names the broken entry if the chain was tampered with)Crypto-shred PII in the log ([crypto])
The chain normally holds no PII (subject is an opaque ref). When you must record PII
inside an entry, seal it: the PII is encrypted into an opaque token that the entry
hash covers. Verification runs on the ciphertext, so you can later destroy the key
(right-to-erasure) — the payload becomes unreadable while the chain still verifies.
from dpdpstack.sealing import generate_seal_key
key = generate_seal_key() # keep secret; deleting it shreds the data
e = log.record("evidence", subject="user_42",
private={"aadhaar": "2341 2341 2346"}, seal_key=key)
AuditLog.open_sealed(e, key) # -> {"aadhaar": "…"} (with the key)
log.verify() # True — even after the key is destroyedKey rotation (zero-downtime): pass a list of keys, newest first. New entries seal with the first key; unsealing tries all, so older-key entries still open. Because the ciphertext is part of the entry hash, chain entries are never re-encrypted — keep an old key around to read old entries, and retire it once they've been pruned or shredded.
new = generate_seal_key()
log.record("evidence", subject="user_43", private={…}, seal_key=[new, key]) # seals with `new`
AuditLog.open_sealed(e, [new, key]) # still opens the old-key entry