audit-3: append-only audit log + strict legacy-prefix rule + anchor cross-check
ci / lint-test-leaks (pull_request) Canceled after 0s
ci / lint-test-leaks (pull_request) Canceled after 0s
- verify_audit_chain(): rows with entry_hash IS NULL are only tolerated BEFORE the first hashed row (documented legacy prefix). A NULL hash AFTER the chain started is tampering and fails verification at that row id — previously any NULL row anywhere was silently skipped, so an attacker with UPDATE could erase intermediate evidence undetected. - migration 0004: audit_log becomes append-only. SQLite: BEFORE UPDATE/ DELETE triggers RAISE(ABORT). Postgres: BEFORE triggers RAISE EXCEPTION. Closes the hole for the table owner and direct SQL connections. - monitor.verify_chain_against_anchors(): compares the chain tail against anchors published to Nostr. Explicit statuses: no_anchors (never anchored / notes missing on relays), local_only (no fetcher), ok (relay note attests the tail or the last local anchor), mismatch. - tests: NULL-before-genesis ok, NULL-in-middle fails at that id, NULL late fails, UPDATE rejected, DELETE rejected, INSERT still allowed, no-anchors explicit, local-only explicit, mismatch detected, match ok. Gate: ruff 0 · 81 unit + 1 PG integration · alembic 0001→0004 on PG · gitleaks clean · make check exit 0.
This commit is contained in:
+11
-4
@@ -209,14 +209,21 @@ def _build_entry(
|
||||
def verify_audit_chain(sess) -> tuple[bool, int | None]:
|
||||
"""Re-walk the chain. Returns (ok, first_broken_entry_id or None).
|
||||
|
||||
Legacy rows (entry_hash IS NULL, written before migration 0002) are
|
||||
skipped and documented: the chain begins GENESIS at the first hashed row,
|
||||
so a migrated database verifies clean without any tampering.
|
||||
Rows with entry_hash IS NULL are ONLY tolerated BEFORE the first hashed
|
||||
row (the documented legacy prefix from before migration 0002). A NULL
|
||||
hash AFTER the chain has started is tampering — an attacker erasing the
|
||||
hash of an intermediate row to hide an edit — and fails verification at
|
||||
that row's id.
|
||||
"""
|
||||
prev_hash: str | None = None # None = still in legacy prefix
|
||||
chain_started = False
|
||||
for entry in sess.query(AuditEntry).order_by(AuditEntry.id).all():
|
||||
if entry.entry_hash is None:
|
||||
continue # legacy row: outside the chain by documented genesis
|
||||
if chain_started:
|
||||
# tampering: a hash was erased in the middle of the chain
|
||||
return False, entry.id
|
||||
continue # legacy prefix row: outside the chain by genesis
|
||||
chain_started = True
|
||||
if prev_hash is None:
|
||||
expected_prev = "GENESIS" # first hashed row anchors the chain
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user