Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-026: Tamper-evident audit log: git-native primary, self-chained fallback

Date: 2026-06-12 Status: Accepted (owner-delegated review, 2026-06-12) Author: Rômulo Febasi Revision: rewritten 2026-06-12 after the multi-user / git-versioned review (the original draft assumed a single writer with no external witness: wrong for the common case; see Context).


Context

.synapse/audit.ndjson is append-only and every record carries a payload_sha256, but syn audit verify only re-hashes each record’s payload, it cannot detect deletion, reordering, or truncation of records. The first draft of this ADR fixed that with a single linear hash-chain (seq + prev) plus one ed25519-signed checkpoint. That design assumed one writer, one key, one reader, no external witness.

A Synapse workspace is created locally and offline, but in practice it is very likely a git repository with more than one person editing the same versioned project. That breaks every assumption of the single-writer draft:

  • whose key signs the single checkpoint, when Alice and Bob both append?
  • a linear prev chain assumes total order; git is a DAG (partial order). Two clones extend the chain from the same parent → divergent chains the merge cannot linearize.
  • an append-only prev-chained NDJSON is the worst possible format for git merges: every concurrent append conflicts and the conflict breaks the chain.

Crucially, git is itself a Merkle DAG: every commit hashes its tree and parent(s), giving ordered, tamper-evident, branch/merge-aware, distributed history for free, which a hand-rolled linear chain cannot. Today Synapse only reads git (gix in at.rs/capture_git.rs); it never commits, the user versions the workspace, so git commits are coarser than Synapse mutations and the audit.ndjson is the fine-grained record committed alongside them.

The conclusion: in a versioned workspace the linear hash-chain + single checkpoint is both redundant with git’s Merkle DAG and harmful to merges. The audit log should be a semantic layer that rides on whatever integrity substrate exists. This ADR covers the log format and verification; the identity/signing/multi-user trust model it relies on is ADR-030.

Decision

A two-tier integrity model, selected by whether the workspace is a git repo (gix discovery):

Tier 1: git-versioned (primary, multi-user).

  • Delegate ordering and sequence tamper-evidence to git’s Merkle DAG. Drop the global linear prev chain and the single signed checkpoint: git already provides ordered, tamper-evident, distributed history, and signed commits provide attribution (ADR-030).
  • Make the audit log merge-safe via per-author segments: .synapse/audit/<author-id>.ndjson, where <author-id> derives from the writer’s Synapse identity (ADR-016). Each author appends only to their own segment → concurrent appends never conflict, and there is no global chain to break. seq is monotonic per author; the global order is the git commit topology (record timestamp is an advisory tiebreaker, not authority). Retention (ADR-013) applies per segment.
  • Each record stays the semantic unit: mutation kind, proposal id, confidence tier (ADR-022), model/agent attribution, accepting identity (BR-12.3), and payload_sha256 (content integrity vs the Markdown). It does not carry a cross-record chain.
  • syn audit verify becomes git-aware: (a) re-hash each record’s payload vs the Markdown blob at the commit that introduced the record, not the working tree, which legitimately changes under later mutations; (b) assert the git commit DAG is intact (gix); (c) delegate signature/identity checks to ADR-030 (commits signed by known, non-revoked identities); (d) optionally compare against the remote to detect a force-push/history rewrite (reuse ADR-011 git-drift). git blame on a segment already attributes each line to a commit + author: verification leverages that.

Tier 2, not versioned (fallback, single local user).

  • Here there is no Merkle DAG to lean on, so the original design is correct and necessary: keep the self-chained log (seq + prev = SHA-256 of the prior record’s canonical bytes, genesis = zeros) plus an ed25519-signed .synapse/audit.head.json checkpoint ({origin, size, head_hash, timestamp, sig}) refreshed at the end of every mutating command. verify walks the chain, verifies the checkpoint signature, and asserts count/head_hash, catching edit, reorder, mid-delete, and end-truncation.
  • syn doctor warns when a workspace is unversioned: “not under git: audit integrity is local-only; git init (and a remote) is recommended.”

Naming and boundary (both tiers): this is tamper-evident, not tamper-proof. Tier 1’s strength is real only with a shared/pushed remote or multiple clones (a witness); a single local clone, like Tier 2, is only as strong as what its owner can rewrite. A key-holder who deliberately rewrites both the content and its attestation is out of scope. Zero new dependencies (SHA-256 + ed25519 + gix already in tree). Implemented in SYN-239 (log + verify); the signing/identity half is SYN-249 / ADR-030.

Consequences

  • Improves: the common case (versioned, multi-user) gets ordered, tamper-evident, distributed history from git’s battle-tested Merkle DAG instead of a fragile hand-rolled chain; the audit log is conflict-free under concurrent edits (per-author segments) and git blame-friendly; the unversioned single-user case keeps full self-contained tamper-evidence.
  • Worsens: two code paths (versioned vs not) to maintain; per-author segment layout is a format change with a migration (fold the existing single audit.ndjson into the current author’s segment, or keep it read-only as a pre-split tail); Tier-1 attribution depends on ADR-030 signed commits being adopted, else git log author is forgeable.
  • Open: exact <author-id> derivation (identity fingerprint vs short hash), and its default when no identity is seeded yet (a local anonymous id until syn setup runs); whether audit verify should require a clean git DAG or only warn; bootstrapping which identities are “known” for a shared workspace (an allowlist: ties to ADR-010 federation and ADR-030). Revisit if an external auditor must verify membership without reading the files (then a Merkle history tree earns its keep: still deferred).

Alternatives considered

  • The single-writer linear chain + one signed checkpoint (the original draft). Redundant with git’s Merkle DAG and merge-hostile in the common multi-user case. Demoted to the Tier-2 fallback, correct only when the workspace is not versioned.
  • A multi-writer CRDT / hash-DAG inside audit.ndjson. Reinvents git’s commit DAG, badly, inside a file git already version-controls. Rejected: lean on git.
  • Merkle history tree (Crosby–Wallach) in v1. O(log n) proofs only matter for a remote auditor who won’t read the files; the local verifier reads them anyway. Deferred.
  • RSA/bilinear accumulators; Certificate-Transparency witness networks. Trusted setup / multi-party equivocation defense: no surface here. Rejected (full rationale retained in ADR-030).
  • Calling it “tamper-proof”/“immutable.” False for a local file. Rejected.

Research grounding

Git object model as a Merkle DAG; Crosby & Wallach, Efficient Data Structures for Tamper-Evident Logging (USENIX Security 2009); Schneier & Kelsey, Secure Audit Logs (1999) and Ma & Tsudik, FssAgg (the truncation result) (both grounding the Tier-2 fallback; RFC 9162 (Certificate Transparency) and Syta et al. CoSi (a single signer cannot self-prevent equivocation) which is why Tier 1 relies on git/remote as the witness). Signing/identity specifics: ADR-030.