Changelog
Copy MarkdownThis project follows Semantic Versioning. Until 1.0, a minor version may change anything: names, options, struct fields, database columns, and the signed content format. No compatibility layers are kept during that period.
Unreleased
0.1.2
Changed
- Expanded the retained canonical decoder and erasable-value failure-path suites so malformed historical values and keyring contract violations remain covered by the release gate.
0.1.1
Added
- A closed historical-verifier registry keyed by the algorithm and canonical version stored on each ledger entry. The current writer and retained verification schemes are separate, so a future writer can be added without removing the implementation needed to verify older evidence.
- Authenticated erasable values backed by
Chronicle.ErasureKeyring.Chronicle.erasable/2and per-schemaerasablepolicies store AES-256-GCM ciphertext;Chronicle.Erasure.destroy/1removes plaintext access while the ledger continues to verify.
Changed
- Deleted the raw-key low-level verifier and added
Chronicle.Integrity.verify_entry/5. Verification now resolves the entry's key identifier through its configured sequence epoch internally; no compatibility path accepts raw key material. - Canonical version 1 now enforces its existing aggregate byte limit as a running budget and flattens accepted iodata once. Accepted encodings and the published integrity vector are unchanged.
- Key-resolver and keyring exceptions now expose only the exception module; exception messages, fields, and stacktraces do not cross the secret-manager boundary in ordinary error terms.
0.1.0
First release.
Chronicle records application facts and Ecto record versions in one append-only, hash-chained ledger authenticated with HMAC-SHA-256. Both go through the same model, so an authorization denial and a database update occupy the same ordered history and are verified by the same walk.
Recording
Chronicle.insert/2,update/2anddelete/2mirror theEcto.Repowrite surface. The audit record commits in the same transaction as the change it describes, and a write that cannot be signed fails the operation. There is no unsigned mode.Chronicle.record/3for facts that are not database mutations — a denied authorization, an export, a payment provider's response.Chronicle.transaction/3,group/3,span/3andrun/3gather related work into one signed unit carrying its own outcome, duration and child count. Each accepts adoblock or an explicit zero-arity function.Chronicle.Multiadds audited steps to an ordinaryEcto.Multi, which stays anEcto.Multi.- Ambient actor, tenant, subject and correlation data through
Chronicle.Context, with explicit capture forTaskand Oban, and request context for Phoenix.
Integrity
- A SHA-256 hash chain over a versioned canonical binary encoding, independent of Erlang's External Term Format, with fixed resource limits and rejection of terms that cannot survive a restart.
- HMAC-SHA-256 signatures over domain-separated digests for content, chain position and authentication, so a digest computed for one purpose can never verify as another.
- Total order established by a ledger-head row lock, so concurrent writers cannot fork the chain.
- Sequence-bounded key epochs, expressing rotation boundaries as ledger positions rather than timestamps, with dual-key transition proofs and pluggable keyrings for KMS, Vault or PKCS#11.
Chronicle.verify_all/2recomputes every content digest, chain link and signature, and checks sequence gaps, the mutable ledger head, unsigned rows, duplicate entries and orphaned group children.Chronicle.CheckpointStoreanchors verified positions outside the audit database, which is what makes a whole-ledger rewind detectable.Chronicle.Verifier, a supervised process that verifies on a schedule and advances a checkpoint only after a store verifies completely.
Reading
Chronicle.history/2returns record-local versions numbered in committed ledger order, computed in the same statement that returns the page.Chronicle.at/2reconstructs a past state from a single stored version rather than replaying deltas.Chronicle.revert/2returns a changeset without persisting it.Chronicle.Querytimeline and group queries, paginated by immutable ledger position rather than by timestamp or offset.
Protection
- Credential-shaped field names are protected by default, matched by substring, underscore-separated segment, and exact name. Configured fields extend the built-in list rather than replacing it.
- Per-schema
redact,hashandomitpolicies, plusChronicle.secret/1,hash/1andomit/0for values whose sensitivity is in their content rather than their name. - A version containing a withheld field reports itself incomplete, and reconstruction fails rather than inventing a value.
Operations
mix chronicle.installgenerates the Ecto migration and a configuration example.mix chronicle.doctorchecks configuration, storage, signing, protected fields per audited schema, and ledger integrity.mix chronicle.keys.rotateplans a rotation, without mutating by default.Chronicle.health/2performs bounded reads only, and reports healthy only when the next write could actually be signed.- Structured
Chronicle.Errorvalues carrying a matchable reason alongside a human-readable message, and telemetry for verification runs.
Supported
- Elixir 1.17 or later. That is the first release requiring Erlang/OTP 25, where
:crypto.hash_equals/2is available. - PostgreSQL and SQLite. SQLite cannot execute
SELECT ... FOR UPDATE, so it does not exercise the lock that establishes total order.
Chronicle is tamper-evident and rollback-resistant, not tamper-proof, and it records only what is written through it. The README states the boundaries in full; read them before relying on this for anything that matters.